Two new charts over the last 14 days, in the existing hand-rolled SVG style:
- "When he sleeps": an actogram with one row per day (oldest at top, Today
at bottom) and a midnight-to-midnight track with sleep shaded. Sleep
windows are clipped per day so a night crossing midnight splits across two
rows, and today's open sleep runs to now. Tapping a row selects that day.
- "By hour of day": pee/poo/meal frequency as three 24-cell heatmap rows,
each cell shaded by how often that event lands in that hour — surfacing
daily rhythm the median gap can't show.
Both re-render on the one-minute tick so the current day keeps filling in.
Logging a quick pee/poo/meal/sleep boundary took two taps: open the note
dialog, then Save. Make a single tap on a quick-action log the event
immediately at the current time.
Non-weight quick actions now call addEvent(type, "", Date.now()) directly
and show a brief snackbar with Undo (deletes the just-added event) and Add
note (opens the edit dialog for it), so notes, photos and custom times are
never lost. Weigh-ins still open the dialog since they need a value.
addEvent now returns the created event so the snackbar can reference it.
The service worker used to skipWaiting() on install and claim clients on
activate, so a new build's assets swapped in silently and a long-open tab
kept running stale JS. Switch to the standard update flow: the worker now
waits until the page sends it a SKIP_WAITING message, and the page shows a
"A new version is available — Reload / Later" banner when a new worker
reaches "installed" while one is already controlling the tab (that
controller check suppresses the first-install prompt).
Reload posts SKIP_WAITING and reloads on controllerchange (guarded against
a reload loop and against the initial clients.claim on a fresh install);
Later dismisses until the next update. Since browsers only auto-check on
navigation, also poll registration.update() hourly and on visibilitychange.
Bump the cache to v9 so the old cache is cleaned up on activate.
Settings gains a "Dark mode" switch. Theme preference is device-global
(localStorage), independent of accounts. With no explicit choice the app keeps
following the OS via prefers-color-scheme; picking a mode sets data-theme on
<html>, which the CSS treats as an override (attribute selector beats the media
query). A tiny <head> script applies a saved choice before first paint to avoid
a light/dark flash. Toggling previews live, independent of Save/Cancel.
Bumps the service-worker cache. Verified in a headless-browser run: default
follows OS, enabling dark swaps the palette, the choice persists across reload,
and toggling back restores light.
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.