Make chorely installable, with an offline shell - #8
Merged
Conversation
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/offlinepage 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:
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.Browser-only facts (
display-mode, user agent) are read throughuseSyncExternalStorerather 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(correctapplication/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.