Skip to content

Make chorely installable, with an offline shell - #8

Merged
BleakMidwinter90 merged 1 commit into
mainfrom
feat/pwa
Aug 10, 2026
Merged

Make chorely installable, with an offline shell#8
BleakMidwinter90 merged 1 commit into
mainfrom
feat/pwa

Conversation

@BleakMidwinter90

Copy link
Copy Markdown
Owner

This app is opened on a phone in a kitchen far more often than at a desk. It should behave like an app there.

The icon

Generated from a single vector definition by scripts/generate-icons.mjs: a ring divided into three unequal arcs — the same idea the Balance screen draws as a bar. It means something, and it still reads at 32px. Committed as PNGs so neither the Docker build nor CI needs an image toolchain.

Getting it to read took an actual geometric fix rather than a nudge. A round line cap overruns its endpoint by half the stroke width — roughly eight degrees at this radius — which swallowed the gaps entirely and rendered the mark as a plain unbroken ring. The gap is now computed from the cap geometry instead of guessed at.

The service worker

Hand-written and deliberately small. A generated Workbox bundle would be several times the size for rules simple enough to read in one sitting, and a misbehaving service worker is close to undebuggable from someone else's phone.

The rule that matters most: only GET navigations and immutable static assets are ever touched. Server Actions are POSTs, and a worker that intercepted or replayed one would corrupt a household's data. Everything else falls straight through.

Navigations are network-first, not cache-first. A chore list is only useful when it's current, so the cache is a fallback for a dead connection rather than a performance trick. When both fail it serves a static /offline page that needs no database, no session and no network — the one situation where every other page is unavailable.

The push handler is included here too, ready for the notifications work.

The install prompt

Two genuinely different worlds, handled honestly:

  • Chromium fires beforeinstallprompt, so there's a real install dialog. Chrome's own mini-infobar is suppressed so the offer appears where it belongs in the page.
  • iOS Safari has no such API and never will, so it simply names the two taps required.
  • Neither available → nothing renders. An install button that does nothing is worse than no button.

Browser-only facts (display-mode, user agent) are read through useSyncExternalStore rather than an effect, with server snapshots that deliberately resolve to "nothing to offer" so the server and first client render agree and hydration stays quiet. The server cannot possibly know whether an app is already installed.

One caveat, documented in the README

Service workers require a secure context. On a home network over plain HTTP everything still works, but you don't get the offline shell — so registration is skipped rather than left to fail noisily.

Verified

Against the running server: /manifest.webmanifest (correct application/manifest+json), /sw.js, /offline, and all four icons — every one 200 with the right content type. Manifest parsed and checked for name, start_url, display mode, 3 icons and 2 shortcuts. Lint, typecheck, 119 tests and build all clean.

The app is opened on a phone in a kitchen far more often than at a desk,
so it should behave like an app there rather than like a web page.

Icon. Generated from one vector definition by scripts/generate-icons.mjs:
a ring divided into three unequal arcs, which is the same idea the Balance
screen draws as a bar. It means something, and it survives down to 32px.
Committed as PNGs so neither Docker nor CI needs an image toolchain.

Getting it to read took an actual geometric fix. A round line cap overruns
its endpoint by half the stroke width - about eight degrees at this radius
- which completely swallowed the gaps and rendered the mark as a plain
unbroken ring. The gap is now computed from the cap geometry rather than
guessed.

Service worker, hand-written and small. A generated Workbox bundle would
be several times the size for rules simple enough to read in one sitting,
and a misbehaving service worker is close to undebuggable from someone
else's phone. The important rule: only GET navigations and immutable
static assets are ever touched. Server Actions are POSTs, and a worker
that intercepted or replayed those would corrupt household data.

Navigations are network-first. A chore list is only useful when current,
so the cache is a fallback for a dead connection rather than a speed
trick. Falls back to a static /offline page that needs no database, no
session and no network.

Install prompt handles both worlds honestly: Chromium's beforeinstallprompt
gives a real dialog, and iOS Safari - which has no such API and never will
- gets told which two taps to make. Nothing renders at all when neither
route exists, because an install button that does nothing is worse than no
button.

Browser-only state is read through useSyncExternalStore rather than an
effect, with server snapshots that resolve to 'nothing to offer' so
hydration stays quiet. The server cannot know whether an app is installed.

Verified against the running server: manifest, service worker, offline
page and every icon all serve with correct content types.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@BleakMidwinter90
BleakMidwinter90 merged commit efa33b0 into main Aug 10, 2026
2 checks passed
@BleakMidwinter90
BleakMidwinter90 deleted the feat/pwa branch August 10, 2026 01:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant