Commit Graph
4 Commits
Author SHA1 Message Date
Alexander Heldt 51d015c231 Add push reminders for sleep, pee, poo and meals
A closed PWA has no timers, so reminders are evaluated on the server: the
event log is already there (clients sync on every mutation), and a ticker
re-checks each enabled rule once a minute and pushes the ones that are due.

Two rule shapes. "sleep" measures from the last sleep-end and fires only
while the puppy is awake. "pee"/"poo"/"eat" measure from the newest event of
that type and stay quiet while the puppy is asleep — otherwise they nag all
night, and suppressing them means an overdue rule instead fires promptly on
waking, which is when it actually matters. Sleep state is derived exactly the
way currentSleepState() does in app.js, tie-break included, so both sides
always agree. Rules read the event's own timestamp rather than when it synced,
so a pee logged offline at 03:10 cancels the reminder retroactively.

Every push carries a tag, so a repeat replaces the previous notification
instead of stacking another one on the lock screen. last_fired is server-owned
and not writable by a client, so a stale device can't force a re-fire.

Web Push is implemented directly rather than pulled in as a dependency: RFC
8291 encryption in the RFC 8188 aes128gcm coding with an RFC 8292 VAPID token,
stdlib only, checked against the RFC 8291 test vector. The key is generated
into vapid.json beside the DB or supplied via -vapid-key; without one the
server logs a warning, skips registering the routes, and the client hides the
UI. Subscriptions a push service reports as 404/410 are dropped.

PNG icons are added because iOS gates push on a Home Screen install and
rejects SVG for apple-touch-icon, and Android has no notification icon
without them.
2026-08-20 17:19:18 +00:00
Alexander Heldt 2e817a086d Add training tracking: exercises with instructions, one-tap session log, consistency overview
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.
2026-07-12 16:54:02 +00:00
Alexander Heldt 5c016ca49e Add self-service account deletion
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.
2026-07-09 19:20:40 +00:00
Alexander Heldt acf2931fb4 Add accounts and multi-tenancy
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.
2026-07-09 18:20:36 +00:00