feat: harden the offline-first behaviour of the field client #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/offline-first-hardening"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Four gaps that made the PWA fall over in exactly the conditions it was built for. The upload queue itself is unchanged in substance — its design (single drain loop, stable
clientUploadId, reconcile-before-resend, blob released only afterstoragePersisted) is what everything here is built around.1. A lapsed session locked the technician out
The API has no refresh endpoint, so expiry is handled by degrading rather than renewing.
Previously:
AuthContextdropped the session on expiry or on any 401,Apprenders the login screen whenuseris null, andUploadContextstopped the queue. An expired token in a basement therefore meant no capture, no access to the evidence already on the phone, and a queue that would not drain until someone signed in — with no way to sign in offline.Now the token is dropped but the technician is kept.
authenticatedis split fromuser; the shell, camera and queue stay reachable while lapsed. The queue gates the wire on holding a token rather than gating the whole queue on being started, so captures still enqueue during the lapse without firing uploads that can only come back 401. A/signinroute handles re-auth and bounces back once a token exists. A banner two hours before expiry is the only preventive lever available without a refresh endpoint.2. The service worker reloaded the page under a running capture
registerTypewasautoUpdate, which bakesskipWaiting/clientsClaiminto the worker and reloads oncontrolling. That directly contradicted the comment inmain.tsx("Shell updates apply on the next launch"), and a deploy mid-capture destroyed the live camera stream plus any capture reviewed but not yet written to IndexedDB.Now
prompt, with a restart chip that hides on the capture screen and while the queue is draining.3. No media was cached, so a workspace opened offline empty
Both halves were missing, and either alone is inert — bytes without a list are unreachable, a list without bytes renders broken tiles.
CacheFirstroute (jeh-media).oldVersionso existing queued originals survive).Full previews and
/videos/{id}/contentare deliberately excluded: large and range-requested, and a partial response in the cache is worse than none. Cached media is purged on sign-out and when a different technician signs in — but not on a mere expiry, which is the same person on the same device.4. The app never asked to be installed
Storage persistence and the screen wake lock that keeps an upload alive are both materially weaker in a browser tab, and a tab can be discarded with queued originals in it. The client was already built on the assumption of a Home Screen install without ever requesting one. Added
beforeinstallprompthandling for Chromium and spelled-out Share → Add to Home Screen instructions for iOS, which never fires that event.Verification
npm run build(runstsc -bfirst) passes clean. The generated worker was checked directly:skipWaitingnow appears only inside aSKIP_WAITINGmessage listener, thelocation.reload()in the bundle sits inside the update function rather than on every deploy, and thejeh-mediaroute serialized correctly with itssameOriginguard.Not verified in a browser. The repo has no test framework, so offline capture → lapse → reconnect → confirm is still unexercised end to end. That path is worth a manual run before merge.
Not addressed
Remaining findings from the same audit, in rough priority order: the workspace cache still uses
localStorage(ITP-evictable on iOS) rather than IndexedDB;cachedAtis written but never surfaced, so cached data carries no staleness indicator;requestPersistentStorage()is still only requested on fresh sign-in, never on session restore;navigator.onLineremains the only connectivity signal (it liestrueon a captive portal); no Background Sync; no detection of silent IndexedDB eviction; and several manifest gaps (id,screenshots,shortcuts,share_target, iOS splash screens).🤖 Generated with Claude Code