Add launch evidence: Lighthouse and installability on production (Maestro 2c8f8887)
CI / ci (push) Successful in 11m4s
CI / ci (pull_request) Successful in 11m16s

Record the 2026-10-11 Lighthouse 13.5.0 mobile run against production
mealquest:743a155 (signed in): accessibility 100 on Home, Plan, Discover
and Dinner detail, meeting the >= 95 gate; performance recorded, not gated.
Record the Playwright installability probe (PASS on all four pages), used
because Lighthouse 10+ removed the PWA category.

The session cookie Lighthouse echoed into configSettings.extraHeaders is
redacted from the committed reports.

Maestro task: 2c8f8887-85e5-4734-b90f-9678f17d3778

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 20:12:55 -04:00
co-authored by Claude Opus 5.5
parent 8b0e2f25a9
commit b19dad7b40
11 changed files with 44840 additions and 1 deletions
+1 -1
View File
@@ -85,7 +85,7 @@ behaviour, exercise it (hit the endpoint, render the page).
| `migrations/` | SQL migrations (planned). |
| `web/` | React PWA source. |
| `deploy/` | Deployment files (planned). |
| `docs/` | Project documentation, including [ARCHITECTURE.md](docs/ARCHITECTURE.md). |
| `docs/` | Project documentation, including [ARCHITECTURE.md](docs/ARCHITECTURE.md), launch evidence in [LAUNCH.md](docs/LAUNCH.md), and Lighthouse reports in `docs/lighthouse/`. |
| `design/` | Requirements and UI handoff. **Read-only reference** — do not edit. |
| `Dockerfile` | Multi-stage image build, at the repository root. |
| `Makefile` | `dev`, `test`, `build`, `docker`. |
+53
View File
@@ -0,0 +1,53 @@
# Launch evidence
Evidence that the production PWA meets the launch gates. The raw reports and
the check script are committed under [lighthouse/](lighthouse/).
## Run conditions
| | |
| --- | --- |
| Target | Production, <https://mealquest.cubecraftlabs.com> |
| Build | `mealquest:743a155` |
| Session | Signed in (household session cookie, supplied at run time) |
| Date | 2026-10-11 |
| Tool | Lighthouse 13.5.0, mobile form factor |
The session cookie value is redacted from the committed reports
(`configSettings.extraHeaders`); nothing else in them is changed.
## Lighthouse
Reports: [lighthouse/2026-10-11/](lighthouse/2026-10-11/) — one
`.report.json` and one `.report.html` per page.
| Page | Route | Performance | Accessibility | Best practices |
| --- | --- | --- | --- | --- |
| Home | `/` | 71 | 100 | 100 |
| Plan | `/plan` | 72 | 100 | 100 |
| Discover | `/discover` | 66 | 100 | 100 |
| Dinner detail | `/dinner/<id>` | 70 | 100 | 100 |
**Gate: accessibility ≥ 95 on every page — met** (100 on all four).
Performance is recorded for reference but is not gated.
## Installability
Lighthouse 10 and later removed the PWA category, so installability was checked
directly with a Playwright probe:
[installability-check.mjs](lighthouse/2026-10-11/installability-check.mjs).
It loads each of the four pages above in a signed-in mobile Chrome session
(the cookie comes from the `CK` environment variable, never the file) and
checks:
- **Manifest** — the linked manifest is fetched (HTTP 200) and has
`name` "MealQuest", `display` "standalone", a `start_url`, and icons at
192 px, 512 px and a `maskable` purpose.
- **Service worker** — registered and activated, and controlling the page on
the single-page-app routes.
The script's pass condition gates the manifest fields and icons and the
service-worker registration; the manifest HTTP status, the worker's activation
state and whether it controls the page are printed per page for review.
**Result: PASS on all four pages.**
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,31 @@
import { chromium } from 'playwright';
const ck = process.env.CK;
const pages = [['home','/'],['plan','/plan'],['discover','/discover'],['dinner','/dinner/01M4PKT9QYA6C4JCF67XE7RA6R']];
const browser = await chromium.launch({ executablePath: '/usr/bin/google-chrome', args: ['--no-sandbox'] });
const ctx = await browser.newContext({ userAgent: 'Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Mobile Safari/537.36' });
await ctx.addCookies([{ name: '__Host-mealquest_session', value: ck, url: 'https://mealquest.cubecraftlabs.com/' }]);
let fail = 0;
for (const [name, path] of pages) {
const page = await ctx.newPage();
page.on('console', m => { if (/sw|service|error/i.test(m.text())) console.log(' [console]', m.text().slice(0,120)); });
page.on('pageerror', e => console.log(' [pageerror]', String(e).slice(0,120)));
await page.goto('https://mealquest.cubecraftlabs.com' + path, { waitUntil: 'networkidle', timeout: 30000 });
const href = await page.evaluate(() => document.querySelector('link[rel="manifest"]')?.href || null);
const sw = await page.evaluate(async () => {
try { await navigator.serviceWorker.ready; } catch (e) { return { err: String(e) }; }
const regs = await navigator.serviceWorker.getRegistrations();
return { registrations: regs.length, scope: regs[0]?.scope || null, state: regs[0]?.active?.state || null, controlling: !!navigator.serviceWorker.controller };
});
const manifest = href ? await page.evaluate(async (h) => { const r = await fetch(h); return { status: r.status, body: await r.json() }; }, href) : { status: 'no link' };
const m = manifest.body || {};
const icons = (m.icons || []).map(i => i.sizes + (i.purpose ? ':' + i.purpose : ''));
const okSw = sw.registrations > 0;
const okIcons = icons.some(s => s.startsWith('192')) && icons.some(s => s.startsWith('512')) && icons.some(s => s.includes('maskable'));
const okManifest = m.name === 'MealQuest' && m.display === 'standalone' && !!m.start_url && okIcons;
const pass = okSw && okManifest;
if (!pass) fail++;
console.log(`${name}: url=${page.url().replace('https://mealquest.cubecraftlabs.com','')} manifest=${manifest.status} name=${m.name} display=${m.display} icons=[${icons.join(',')}] SW=${JSON.stringify(sw)} => ${pass ? 'INSTALLABLE' : 'NOT INSTALLABLE'}`);
await page.close();
}
await browser.close();
process.exit(fail ? 1 : 0);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long