diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 00000000..999aa347 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,110 @@ +name: Lighthouse (warn-only) + +on: + pull_request: + paths: + - 'web/**' + - 'app.py' + - 'requirements.txt' + - 'package.json' + - '.github/workflows/lighthouse.yml' + - 'lighthouse-budget.json' + workflow_dispatch: + +jobs: + lighthouse: + name: Desktop Lighthouse (pipeline, library, reader) + runs-on: ubuntu-latest + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + + - name: Install Python deps + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Install Node deps + run: npm ci --no-audit --no-fund + + - name: Build runtime bundle + run: npm run build + + - name: Start backend + env: + OPENAI_API_KEY: dummy-ci-key + STORYFORGE_LOG_LEVEL: WARNING + run: | + nohup python app.py >server.log 2>&1 & + for i in {1..30}; do + curl -fsS http://localhost:7860/api/health && exit 0 || sleep 1 + done + echo "::error::Backend never became healthy" + tail -n 80 server.log + exit 1 + + - name: Run Lighthouse on 3 routes + run: | + mkdir -p lh-reports + for route in pipeline library reader; do + npx --yes lighthouse \ + "http://localhost:7860/#${route}" \ + --preset=desktop \ + --quiet \ + --chrome-flags="--headless=new --no-sandbox" \ + --budget-path=lighthouse-budget.json \ + --output=json \ + --output=html \ + --output-path="lh-reports/${route}" \ + --only-categories=performance,accessibility,best-practices \ + || echo "::warning::Lighthouse run for ${route} returned non-zero (warn-only)" + done + + - name: Summarize scores + if: always() + run: | + echo "## Lighthouse summary (desktop, warn-only)" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Route | Perf | A11y | BP | FCP (ms) | LCP (ms) | TBT (ms) | CLS |" >> "$GITHUB_STEP_SUMMARY" + echo "|---|---:|---:|---:|---:|---:|---:|---:|" >> "$GITHUB_STEP_SUMMARY" + for route in pipeline library reader; do + f="lh-reports/${route}.report.json" + if [ -f "$f" ]; then + node -e " + const j=require('./${f}'); + const a=j.audits; + const c=j.categories; + const row=['${route}', + Math.round(c.performance.score*100), + Math.round(c.accessibility.score*100), + Math.round(c['best-practices'].score*100), + Math.round(a['first-contentful-paint'].numericValue), + Math.round(a['largest-contentful-paint'].numericValue), + Math.round(a['total-blocking-time'].numericValue), + (a['cumulative-layout-shift'].numericValue).toFixed(3) + ]; + console.log('| ' + row.join(' | ') + ' |'); + " >> "$GITHUB_STEP_SUMMARY" + else + echo "| ${route} | (no report) | | | | | | |" >> "$GITHUB_STEP_SUMMARY" + fi + done + + - name: Upload Lighthouse artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: lighthouse-reports + path: lh-reports/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 22799418..d49920ab 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,13 @@ web/js/**/*.js.map # Vitest global setup is plain JS with no .ts source — keep it tracked !web/js/__tests__/setup.js # Playwright visual test spec output is .ts source — no exceptions needed for .js -web/dist/ +web/dist/* +# …but track the runtime bundle (perf/forge-shell P2): production prefers it, +# and rebuilding it on every container boot would add ~3s to startup. +!web/dist/ +!web/dist/js/ +web/dist/js/* +!web/dist/js/runtime-bundle.min.js # ── IDE & OS ─────────────────────────────────────────────────────── .DS_Store diff --git a/lighthouse-budget.json b/lighthouse-budget.json new file mode 100644 index 00000000..360e2bd0 --- /dev/null +++ b/lighthouse-budget.json @@ -0,0 +1,24 @@ +[ + { + "path": "/*", + "timings": [ + { "metric": "first-contentful-paint", "budget": 2000 }, + { "metric": "largest-contentful-paint", "budget": 2800 }, + { "metric": "total-blocking-time", "budget": 200 }, + { "metric": "cumulative-layout-shift", "budget": 0.1 }, + { "metric": "speed-index", "budget": 2200 }, + { "metric": "interactive", "budget": 3000 } + ], + "resourceSizes": [ + { "resourceType": "script", "budget": 200 }, + { "resourceType": "stylesheet", "budget": 80 }, + { "resourceType": "image", "budget": 200 }, + { "resourceType": "font", "budget": 100 }, + { "resourceType": "document", "budget": 80 }, + { "resourceType": "total", "budget": 600 } + ], + "resourceCounts": [ + { "resourceType": "third-party", "budget": 10 } + ] + } +] diff --git a/package.json b/package.json index 108d388b..bd74fa52 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -p tsconfig.build.json", + "build": "tsc -p tsconfig.build.json && node scripts/build-runtime-bundle.mjs", + "build:bundle": "node scripts/build-runtime-bundle.mjs", "build:css": "tailwindcss -i web/css/main.css -o web/css/main.built.css --minify", "build:vite": "vite build", "preview": "vite preview", diff --git a/scripts/build-runtime-bundle.mjs b/scripts/build-runtime-bundle.mjs new file mode 100644 index 00000000..335fa954 --- /dev/null +++ b/scripts/build-runtime-bundle.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node +/** + * build-runtime-bundle.mjs — concat + minify the 13 runtime + +