The service worker populated a new build's cache with plain addAll(),
which the browser may satisfy from its HTTP cache — and static assets
were served without Cache-Control, so Safari's heuristic caching could
hold app.js for days. Together that could install a mixed build: a
fresh index.html whose buttons reference listeners a stale app.js never
registers.
Install now fetches assets with cache: "reload", and the server marks
all static assets no-cache (revalidation is a cheap 304; fast/offline
loads are the SW cache's job anyway).
Exercises (name + how-to note) are a new synced collection with the same
LWW/tombstone contract as events, served by POST /api/exercises/sync.
Training sessions are ordinary events (type "training") referencing an
exercise by id, so they ride the existing event sync unchanged.
The Training panel lists each exercise with last-trained / this-week /
streak stats, expandable instructions, and a one-tap Log button with the
usual undo/add-note snackbar. An exercise-by-day heatmap shows the last
14 days of consistency, and history and the daily overview count
training sessions like any other event.
The update banner only fires when the browser sees sw.js change, but the
cache name was a hand-bumped constant — so a deploy that touched only
app.js/index.html/style.css left sw.js byte-identical, no new worker
installed, and the banner never showed (and cached assets never refreshed).
Have the server render sw.js at serve time, substituting a __BUILD_HASH__
placeholder with a SHA-256 over the assets the worker caches (index.html,
style.css, app.js, manifest.json, icon.svg). Any asset change now yields a
new cache name and a byte-different sw.js, which is exactly the signal that
makes the browser install a new worker. The hash is memoised and only
recomputed when a file's size/modtime changes, so it needs no server
restart. Served unsubstituted (dev/static host), sw.js stays a valid constant.
Settings → Delete account removes the signed-in account and everything it
owns. DELETE /api/me re-checks the password (guarding an unattended session),
then wipes the user's events, config, sessions and user row in one transaction
and removes their photos/<user_id>/ directory. The client clears the account's
local cache and returns to the login screen.
Bumps the service-worker cache so clients pick up the new UI.
Verified: wrong password is rejected (401, data intact); correct password
returns 204, invalidates the session, drops all rows to zero and removes the
photo dir; the email can be re-registered afterwards. Confirmed end to end in
a headless-browser run of the Settings → delete flow.
Every event, profile and photo is now scoped to a signed-in account, so
separate people can track separate puppies on one server.
Server:
- users + sessions tables; bcrypt passwords; random session tokens stored
hashed and set as an HttpOnly cookie. Middleware gates /api/* behind a
valid session.
- register/login/logout/me endpoints. Registration requires a shared invite
code (-invite-code / PUPPY_INVITE_CODE); empty disables it.
- events, config and photos are keyed by user_id; the sync upsert guards
against cross-user overwrites and reads are scoped, so accounts are isolated.
Photos live under photos/<user_id>/ and are only served to their owner.
- in-place schema migration adds user_id and reshapes config; legacy
single-tenant data (including imported events.json) is parked ownerless and
adopted by the first account to register.
Client:
- login/register gate in front of the app; the tracker only boots once the
session check resolves. localStorage is namespaced per user.
- 401s bounce back to login; an offline reload falls back to the last cached
session so offline-first still works. Logout clears the session and reloads.
Deployment:
- module.nix gains inviteCodeFile (secret via EnvironmentFile) and
secureCookies options.
Verified end to end (curl + a headless-browser run of the auth flow):
isolation between accounts, invite enforcement, first-user adoption, photo
ownership, and session persistence across reload.
Replace the JSON-file event and config stores with a SQLite database
(modernc.org/sqlite, pure-Go so the static build keeps CGO_ENABLED=0).
Last-write-wins now rides on the upsert's WHERE clause rather than a
Go-side map compare; the sync protocol and HTTP handlers are unchanged.
On first start the server auto-imports any legacy events.json/config.json
sitting in the data dir, renaming them to *.imported. The -data flag now
points at puppy.db; photos still live on the filesystem alongside it.