diff --git a/app/components/league/DraftInfoCard.tsx b/app/components/league/DraftInfoCard.tsx
index a7d3bab..ad8429f 100644
--- a/app/components/league/DraftInfoCard.tsx
+++ b/app/components/league/DraftInfoCard.tsx
@@ -18,6 +18,8 @@ export interface DraftInfoCardProps {
isDraftOrderSet: boolean;
/** true when season status is pre_draft or draft */
isDraftOrPreDraft: boolean;
+ /** true when season status is specifically draft (live) */
+ isDraft?: boolean;
draftDateTime?: string | Date | null;
draftBoardHref: string;
draftRoomHref: string;
@@ -56,8 +58,10 @@ function DraftInfoCardVariant({
draftIncrementTime,
sportsCount,
isDraftOrderSet,
+ isDraft,
draftDateTime,
draftTimezone,
+ draftBoardHref,
draftRoomHref,
queueBuilderHref,
userDraftPosition,
@@ -115,15 +119,26 @@ function DraftInfoCardVariant({
Draft Info
- {isDraftOrderSet ? (
-
- ) : queueBuilderHref ? (
-
- ) : null}
+
+ {isDraft && (
+
+ View Draft Board
+
+
+ )}
+ {isDraftOrderSet ? (
+
+ ) : queueBuilderHref ? (
+
+ ) : null}
+
diff --git a/app/routes/leagues/$leagueId.server.ts b/app/routes/leagues/$leagueId.server.ts
index a2cfb61..e10b048 100644
--- a/app/routes/leagues/$leagueId.server.ts
+++ b/app/routes/leagues/$leagueId.server.ts
@@ -203,8 +203,14 @@ export async function loader(args: Route.LoaderArgs) {
// Check if draft order is set
const isDraftOrderSet = draftSlots.length > 0;
- // Extract origin for client use (avoids SSR/client mismatch on invite URLs)
- const origin = new URL(args.request.url).origin;
+ // Extract origin for client use (avoids SSR/client mismatch on invite URLs).
+ // Respect x-forwarded-proto so the invite URL shows https when behind a TLS-terminating proxy.
+ const requestUrl = new URL(args.request.url);
+ const rawProto = args.request.headers.get("x-forwarded-proto");
+ const proto = rawProto?.split(",")[0].trim() ?? "";
+ const origin = (proto === "https" || proto === "http")
+ ? `${proto}://${requestUrl.host}`
+ : requestUrl.origin;
// Fetch recent audit log entries for the "Recent Activity" summary widget
const recentActivity = season
diff --git a/app/routes/leagues/$leagueId.tsx b/app/routes/leagues/$leagueId.tsx
index dbc089d..998d765 100644
--- a/app/routes/leagues/$leagueId.tsx
+++ b/app/routes/leagues/$leagueId.tsx
@@ -228,6 +228,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
sportsCount={sportsCount}
isDraftOrderSet={isDraftOrderSet}
isDraftOrPreDraft={isDraftOrPreDraft}
+ isDraft={season.status === "draft"}
draftDateTime={season.draftDateTime}
draftTimezone={currentUserTimezone ?? season.overnightPauseTimezone}
draftBoardHref={`/leagues/${league.id}/draft-board/${season.id}`}
diff --git a/docs/ci-build-optimization.md b/docs/ci-build-optimization.md
new file mode 100644
index 0000000..de476c9
--- /dev/null
+++ b/docs/ci-build-optimization.md
@@ -0,0 +1,133 @@
+# CI Build Optimization Plan
+
+The Forgejo Actions build pipeline currently takes ~15 minutes on main branch pushes:
+- ~5 min: `setup-buildx-action`
+- ~5 min: build and push to container registry
+- ~5 min: deploy job (docker pull + migrate + compose up)
+
+This plan cuts that to ~3-5 minutes for typical code changes.
+
+---
+
+## Root Causes
+
+### 1. Dockerfile layer invalidation (biggest win)
+
+The `development-dependencies-env` stage copies *all* source files before running `npm ci`:
+
+```dockerfile
+FROM node:20-alpine AS development-dependencies-env
+COPY . /app # <-- invalidates npm cache on EVERY commit
+WORKDIR /app
+RUN npm ci
+```
+
+Every single push blows the npm install cache layer, even if `package.json` hasn't changed. This means:
+- The build job re-runs `npm ci` inside Docker on every commit (~1-2 min)
+- The server's `docker compose pull` re-downloads the large node_modules layer (~200-300MB) on every deploy
+
+The `production-dependencies-env` stage already does this correctly — the dev stage needs the same fix.
+
+### 2. Buildx setup taking 5 minutes
+
+`setup-buildx-action` normally takes ~20-30 seconds. 5 minutes means QEMU multi-platform emulators are being installed (for arm64 support). Since we only target `linux/amd64`, explicitly declaring the platform skips QEMU installation entirely.
+
+### 3. No npm cache in CI jobs
+
+The `test`, `typecheck`, and `lint` jobs each run `npm ci` independently with no caching. This adds ~30-60s per job on cold runners.
+
+---
+
+## Changes
+
+### Fix 1: Dockerfile — copy only manifests before `npm ci`
+
+**File:** `Dockerfile`
+
+```dockerfile
+FROM node:20-alpine AS development-dependencies-env
+COPY package.json package-lock.json .npmrc /app/ # only manifests
+WORKDIR /app
+RUN npm ci
+
+FROM node:20-alpine AS production-dependencies-env
+COPY ./package.json package-lock.json .npmrc /app/
+WORKDIR /app
+RUN npm ci --omit=dev
+
+FROM node:20-alpine AS build-env
+COPY . /app/
+COPY --from=development-dependencies-env /app/node_modules /app/node_modules
+WORKDIR /app
+RUN npm run build
+
+FROM node:20-alpine
+COPY ./package.json package-lock.json /app/
+COPY --from=production-dependencies-env /app/node_modules /app/node_modules
+COPY --from=build-env /app/build /app/build
+COPY --from=build-env /app/dist /app/dist
+COPY ./drizzle /app/drizzle
+COPY ./scripts /app/scripts
+COPY ./instrument.server.mjs /app/instrument.server.mjs
+WORKDIR /app
+CMD ["npm", "run", "start"]
+```
+
+### Fix 2: Declare platform in build-push-action
+
+**File:** `.forgejo/workflows/deploy.yml` — the `build` job's "Build and Push" step
+
+Add `platforms: linux/amd64`:
+
+```yaml
+- name: 🗄️ Build and Push to Container Registry
+ uses: https://github.com/docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ platforms: linux/amd64
+ tags: ${{ vars.CONTAINER_REGISTRY }}/brackt:latest
+ cache-from: type=registry,ref=${{ vars.CONTAINER_REGISTRY }}/brackt:buildcache
+ cache-to: type=registry,ref=${{ vars.CONTAINER_REGISTRY }}/brackt:buildcache,mode=max
+```
+
+### Fix 3: Add npm cache to CI jobs
+
+**File:** `.forgejo/workflows/deploy.yml` — `test`, `typecheck`, `lint` jobs
+
+Add after checkout in each job:
+
+```yaml
+- name: 📦 Cache node_modules
+ uses: https://github.com/actions/cache@v3
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-npm-
+```
+
+> Note: If Forgejo's runner doesn't support the cache action, this silently no-ops (harmless). An alternative is merging the three parallel jobs into one that installs once and runs all checks sequentially — unconditionally saves two `npm ci` calls.
+
+---
+
+## Expected Results
+
+`docker pull` on the server is incremental — it only downloads layers that changed since the last deploy. After the Dockerfile fix, most deploys only pull the small build-output layer instead of the full node_modules.
+
+| Step | Before | After (code change) | After (dep change) |
+|---|---|---|---|
+| Setup buildx | ~5 min | ~30 sec | ~30 sec |
+| Build + push | ~5 min | ~1-2 min | ~3-4 min |
+| Deploy (docker pull + up) | ~5 min | ~1-2 min | ~3-4 min |
+| **Total** | **~15 min** | **~3-5 min** | **~8-10 min** |
+
+---
+
+## Verification
+
+1. Push a code-only commit (no package.json change) — confirm Docker build uses registry cache for npm install layers and the build step completes in under 2 minutes
+2. Push a commit changing package.json — confirm npm cache is correctly invalidated and reruns
+3. Check the `setup-buildx-action` log — should show ~30s, no QEMU download
+4. Check deploy job — `docker compose pull` should show most layers as `Already exists`
+5. Confirm deployed app is functional