Commit Graph

69 Commits

Author SHA1 Message Date
Alexander Heldt ced415c3a5 Make Save the exercise dialog's default button
All three menu buttons were implicit submit buttons, so pressing Enter
(or the iOS keyboard's "Go") triggered implicit form submission against
the first one — the hidden Delete button — which closed the dialog and
silently discarded the typed exercise. Delete and Cancel are now
type="button", leaving Save as the form's default button.
2026-07-12 17:12:59 +00:00
Alexander Heldt c1ece16347 Remove the redundant "Currently" row from the overview
The big clock already shows the asleep/awake state prominently.
2026-07-12 16:54:02 +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 561b98b64f Make info panels collapsible with remembered state
Each main section with a data-panel key can now be folded by clicking its
heading (rotating chevron, keyboard-accessible via role/tabindex/aria-expanded
and Enter/Space). Collapsed keys are persisted in localStorage and restored on
load, so the layout stays how you left it across reloads.

State is device-global like the theme (a single non-namespaced key, not
per-user data), and only collapsed panels are stored — so any panel added
later defaults to open.
2026-07-10 17:03:29 +00:00
Alexander Heldt f4ce7dcb54 Add sleep timeline and by-hour heatmaps
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.
2026-07-10 17:03:29 +00:00
Alexander Heldt 16799ad7c7 Render sw.js with a per-build asset hash so updates are detected
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.
2026-07-10 12:55:01 +00:00
Alexander Heldt d267ebef86 Log an event that happened "now" in one tap
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.
2026-07-10 12:44:41 +00:00
Alexander Heldt 01ad078b2a Stamp "now" events to the second, not the minute
The time input has no seconds and fromDateTimeInputs floors to :00, so an
event logged at "now" was stored up to ~59s in the past — the big-clock
counter would jump to e.g. "0:40" the instant a sleep boundary was logged.

Track whether the user has actually touched the date/time fields (reset in
openNoteDialog and the "Now" button, set on manual change/input). When
untouched, noteDialogAt() returns the exact Date.now(); an explicitly
picked time is still parsed from the inputs (minute precision is fine there).
2026-07-10 12:36:30 +00:00
Alexander Heldt 8f4034ef47 Fix "Currently" row disagreeing with the big clock about sleep state
The "Currently" row derived its state from isCurrentlyAsleep() (ascending
stable sort, then fold) while the big clock used currentSleepState()
(max-by-`at` with a strict >). When two sleep boundary events shared the
same `at`, the two broke the tie differently, so the row could show
"Asleep" during a wake window while the clock said "Awake".

Make both read from the single currentSleepState() source, and give it a
deterministic tie-breaker: for equal `at`, the later updatedAt (most
recently logged boundary) wins. Remove the now-unused isCurrentlyAsleep().
2026-07-10 12:33:00 +00:00
Alexander Heldt dbcac0653e Prompt to reload when a new version is available
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.
2026-07-10 12:30:30 +00:00
Alexander Heldt 9c0427a1ec Add light/dark mode toggle in settings
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.
2026-07-09 19:41:30 +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
Alexander Heldt 9207aaa4aa Store events and config in SQLite
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.
2026-07-09 16:56:35 +00:00
Alexander Heldt da692d84da Show age in weight graph 2026-07-04 09:27:13 +00:00
Alexander Heldt f14a749169 Track age and weight 2026-07-04 09:20:31 +00:00
Alexander Heldt 709a051afb Add more statistics 2026-06-25 14:24:46 +00:00
Alexander Heldt 06c51bb715 Make y-axis of sleep graph more granular 2026-06-21 17:51:33 +00:00
Alexander Heldt d61e88fa29 Light of day 2026-06-21 17:41:56 +00:00