From 41481a98120cd4bda21103b2dde4ac796a3bbbec Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Fri, 22 May 2026 22:26:49 -0700 Subject: [PATCH] Fix invite link showing http and add draft board link during live draft - Use x-forwarded-proto header (allowlisted to http/https only) when building the invite link origin so it shows https behind a TLS proxy - Show "View Draft Board" link in the DraftInfoCard header when the draft is actively running, matching the link already shown on active/completed seasons Co-Authored-By: Claude Sonnet 4.6 --- app/components/league/DraftInfoCard.tsx | 33 ++++-- app/routes/leagues/$leagueId.server.ts | 10 +- app/routes/leagues/$leagueId.tsx | 1 + docs/ci-build-optimization.md | 133 ++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 docs/ci-build-optimization.md 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