Add launch evidence: Lighthouse and installability on production (Maestro 2c8f8887)
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:
@@ -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`. |
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user