Compare commits

..
Author SHA1 Message Date
dev_agentandClaude Opus 5.5 cd4bc8d9bb Gate the installability probe on every condition LAUNCH.md claims (Maestro 2c8f8887, F1-F3)
CI / ci (push) Successful in 10m9s
CI / ci (pull_request) Successful in 10m12s
F1: the probe now gates each page on manifest HTTP 200, name, display,
start_url, 192x192 and 512x512 icons, a maskable icon, and a service
worker that is ready, registered and activated. It prints a PASS/FAIL
line per condition and exits 1 if any condition fails on any page.
LAUNCH.md lists exactly these gates; the unchecked claim that the worker
controls the page is removed.

F2: navigator.serviceWorker.ready is raced against a 10 second timeout;
expiry fails the "service worker ready" condition.

F3: the browser context uses Playwright's Pixel 5 mobile emulation
(viewport, touch, isMobile, user agent), and LAUNCH.md says so.

Re-run against production mealquest:743a155 on 2026-10-11: every
condition PASS on all four pages, exit 0.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:28:03 -04:00
dev_agentandClaude Opus 5.5 b19dad7b40 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>
2026-10-11 20:12:55 -04:00
8 changed files with 9 additions and 153 deletions
-42
View File
@@ -52,45 +52,3 @@ It prints a PASS or FAIL line per condition per page and exits 1 if any
condition fails on any page.
**Result: PASS on all four pages.**
## Production smoke — 2026-10-11/12
An end-to-end smoke of the launch flows against production, run in the
household's owner session.
| | |
| --- | --- |
| Target | Production, <https://mealquest.cubecraftlabs.com> |
| Build | `mealquest:743a155`, then `mealquest:561f249` mid-smoke |
| Session | Owner session (signed in) |
| Google Chat | Test space "MealQuest - Test" (not the family space) |
| Google Calendar | Test calendar "MealQuest - Test" (not the family calendar) |
| Dates | 2026-10-11 to 2026-10-12 |
The smoke spanned two deploys: Feature 24 shipped mid-smoke through the new
auto-deploy path, moving production from `743a155` to `561f249`.
| Step | Result |
| --- | --- |
| Sign-in session and CSRF | ✓ |
| `/home` shows the Regulars | ✓ |
| Suggestion batch through the real LLM | ✓ 10 grounded cards, each "Why this?" citing the Regulars |
| Try it | ✓ dinner created with a grounded photo, added to the Quest log |
| Pass | ✓ declined dinner never re-suggested; deck 10 → 8 |
| AI draft, Week 42 (Oct 12–18) | ✓ 7 days, no duplicates, Quest-log pick marked `is_new`, `ai_note` cites the Regulars |
| Day swap | ✓ Monday → Slow Cooker French Dip Sandwiches (crock pot) |
| Push to Calendar | ✓ synced; owner confirmed 7 events; a second push made no duplicates |
| Supply run | ✓ 27 items, AI title "A Beefy Week with Pizza Night" |
| Have-it toggle | ✓ needed items 27 → 26 |
| Send to Google Chat | ✓ `sent_to_chat_at` stamped; owner confirmed receipt in the test space |
| Test push notification | ✓ HTTP 202, `event_key` `plan_ready:test-…`, 3 subscriptions; owner confirmed receipt on their iPhone |
| Quick dinner ideas | ✓ 6 ideas |
| Cook from what I have | ✓ 4 ideas, with uses and missing ingredients computed |
**Result: PASS on every step.**
Evidence form: server-side API responses and timestamps, plus the owner's
explicit confirmations for the steps that land outside the app (calendar
events, Google Chat message, push notification on the phone). The owner's
confirmation quotes are recorded on Maestro task
`0544e318-9b5f-4264-9884-1ec8cb63fd49`. Phone screenshots were not captured.
-3
View File
@@ -4,9 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#5a358c" />
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<title>MealQuest</title>
</head>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

+4 -60
View File
@@ -1,5 +1,5 @@
// Command generate-icons writes the PWA icons and favicons in web/public from
// the MealQuest logo. Run it from the repository root:
// Command generate-icons writes the PWA icons in web/public from the
// MealQuest logo. Run it from the repository root:
//
// go run ./web/scripts/generate-icons
//
@@ -9,13 +9,10 @@
package main
import (
"bytes"
"encoding/binary"
"fmt"
"image"
"image/color"
"image/png"
"io"
"math"
"os"
"path/filepath"
@@ -30,9 +27,6 @@ const (
maskableScale = 0.66
)
// faviconSizes are the images packed into favicon.ico.
var faviconSizes = []int{16, 32, 48}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "generate-icons:", err)
@@ -55,8 +49,6 @@ func run() error {
{"icons/icon-192.png", resized(logo, 192)},
{"icons/icon-512.png", resized(logo, 512)},
{"icons/icon-maskable-512.png", maskable(logo, 512)},
{"icons/favicon-16.png", resized(logo, 16)},
{"icons/favicon-32.png", resized(logo, 32)},
{"apple-touch-icon.png", resized(logo, 180)},
}
for _, out := range outputs {
@@ -66,53 +58,9 @@ func run() error {
}
fmt.Printf("wrote %s (%dx%d)\n", path, out.image.Rect.Dx(), out.image.Rect.Dy())
}
ico, err := favicon(logo)
if err != nil {
return err
}
path := filepath.Join(public, "favicon.ico")
if err := os.WriteFile(path, ico, 0o644); err != nil {
return err
}
fmt.Printf("wrote %s (%v)\n", path, faviconSizes)
return nil
}
// favicon is an ICO file holding the logo at each of faviconSizes as an
// embedded PNG: a 6-byte header, a 16-byte directory entry per image, then
// the images.
func favicon(logo image.Image) ([]byte, error) {
var images [][]byte
for _, size := range faviconSizes {
var buf bytes.Buffer
if err := encode(&buf, resized(logo, size)); err != nil {
return nil, fmt.Errorf("encode favicon %dx%d: %w", size, size, err)
}
images = append(images, buf.Bytes())
}
var out bytes.Buffer
le := binary.LittleEndian
out.Write(le.AppendUint16(nil, 0)) // reserved
out.Write(le.AppendUint16(nil, 1)) // type: icon
out.Write(le.AppendUint16(nil, uint16(len(images)))) // image count
offset := 6 + 16*len(images)
for i, size := range faviconSizes {
out.WriteByte(byte(size)) // width
out.WriteByte(byte(size)) // height
out.WriteByte(0) // palette size: none
out.WriteByte(0) // reserved
out.Write(le.AppendUint16(nil, 1)) // colour planes
out.Write(le.AppendUint16(nil, 32)) // bits per pixel
out.Write(le.AppendUint32(nil, uint32(len(images[i]))))
out.Write(le.AppendUint32(nil, uint32(offset)))
offset += len(images[i])
}
for _, img := range images {
out.Write(img)
}
return out.Bytes(), nil
}
// load decodes the logo. The file is a PNG despite its .jpg name, so the
// format is sniffed rather than taken from the extension.
func load(path string) (image.Image, error) {
@@ -136,18 +84,14 @@ func save(path string, img image.Image) error {
if err != nil {
return err
}
if err := encode(f, img); err != nil {
enc := png.Encoder{CompressionLevel: png.BestCompression}
if err := enc.Encode(f, img); err != nil {
f.Close()
return fmt.Errorf("encode %s: %w", path, err)
}
return f.Close()
}
func encode(w io.Writer, img image.Image) error {
enc := png.Encoder{CompressionLevel: png.BestCompression}
return enc.Encode(w, img)
}
func resized(logo image.Image, size int) *image.RGBA {
dst := image.NewRGBA(image.Rect(0, 0, size, size))
draw.CatmullRom.Scale(dst, dst.Rect, logo, logo.Bounds(), draw.Src, nil)
+3 -43
View File
@@ -1,8 +1,7 @@
// @vitest-environment node
// Builds the app with the production config and serves the result, so the
// assertions cover what ships in dist: the web manifest, the service worker,
// every icon and favicon, and the service worker registration that only builds
// include.
// every icon, and the service worker registration that only builds include.
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { build, createServer, preview, type PreviewServer, type ViteDevServer } from 'vite'
import tokens from './styles/tokens.css?raw'
@@ -13,13 +12,9 @@ const icons = [
{ path: '/icons/icon-192.png', size: 192 },
{ path: '/icons/icon-512.png', size: 512 },
{ path: '/icons/icon-maskable-512.png', size: 512 },
{ path: '/icons/favicon-16.png', size: 16 },
{ path: '/icons/favicon-32.png', size: 32 },
{ path: '/apple-touch-icon.png', size: 180 },
]
const faviconSizes = [16, 32, 48]
function token(name: string): string {
const value = tokens.match(new RegExp(`^\\s*${name}:\\s*([^;\\s]+);`, 'm'))?.[1]
if (!value) throw new Error(`${name} is not defined in tokens.css`)
@@ -43,18 +38,6 @@ function pngSize(bytes: Uint8Array): { width: number; height: number } {
return { width: view.getUint32(16), height: view.getUint32(20) }
}
// The images in an ICO file: a 6-byte header (reserved, type 1, count), then
// a 16-byte directory entry per image giving its byte length and offset.
function icoImages(bytes: Uint8Array): Uint8Array[] {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
expect([view.getUint16(0, true), view.getUint16(2, true)]).toEqual([0, 1])
return Array.from({ length: view.getUint16(4, true) }, (_, i) => {
const entry = 6 + 16 * i
const offset = view.getUint32(entry + 12, true)
return bytes.subarray(offset, offset + view.getUint32(entry + 8, true))
})
}
describe('production build', () => {
let server: PreviewServer
let origin: string
@@ -111,25 +94,13 @@ describe('production build', () => {
expect(pngSize(bytes)).toEqual({ width: size, height: size })
})
it('ships /favicon.ico holding the logo as PNGs at every favicon size', async () => {
const bytes = new Uint8Array(await (await get('/favicon.ico')).arrayBuffer())
expect(icoImages(bytes).map((png) => pngSize(png))).toEqual(
faviconSizes.map((size) => ({ width: size, height: size })),
)
})
it('emits sw.js with the precache list injected', async () => {
const sw = await (await get('/sw.js')).text()
expect(sw).not.toContain('self.__WB_MANIFEST')
expect(sw).toContain('"url":"index.html"')
})
it('precaches every icon and favicon so the offline shell keeps them', async () => {
const urls = precacheEntries(await (await get('/sw.js')).text()).map((entry) => entry.url)
for (const path of [...icons.map((icon) => icon.path), '/favicon.ico']) {
expect(urls).toContain(path.slice(1))
for (const { path } of icons.slice(0, 3)) {
expect(sw).toContain(`"url":"${path.slice(1)}"`)
}
expect(new Set(urls).size, 'precache URLs are unique').toBe(urls.length)
})
it('precaches the shell, JS, CSS and every self-hosted font', async () => {
@@ -158,17 +129,6 @@ describe('production build', () => {
expect(html).toContain('<link rel="apple-touch-icon" href="/apple-touch-icon.png" />')
})
it('links the favicons from index.html', async () => {
const html = await (await get('/')).text()
expect(html).toContain('<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />')
expect(html).toContain(
'<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png" />',
)
expect(html).toContain(
'<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16.png" />',
)
})
it('registers /sw.js at scope / from index.html', async () => {
const html = await (await get('/')).text()
expect(html).toContain('<script id="vite-plugin-pwa:register-sw" src="/registerSW.js"></script>')
+2 -5
View File
@@ -17,11 +17,8 @@ export default defineConfig({
srcDir: 'src',
filename: 'sw.ts',
injectRegister: 'script',
// Precache the shell, JS, CSS, the self-hosted fonts and every icon
// and favicon. The glob already covers the manifest icons, so the
// plugin does not add them a second time.
injectManifest: { globPatterns: ['**/*.{html,js,css,woff2,png,ico}'] },
includeManifestIcons: false,
// Precache the shell, JS, CSS and the self-hosted fonts.
injectManifest: { globPatterns: ['**/*.{html,js,css,woff2}'] },
// The web manifest lives in manifest.json: its theme and background
// colours are --color-accent and --color-bg from src/styles/tokens.css,
// and hex colours may not be written in TypeScript.