Commit Graph

49 Commits

Author SHA1 Message Date
Alexander Heldt a1a6ec8720 Give the Sleep trend average line its own color
All three blue lines were hard to tell apart; the N-day average is now
teal (the --weight token) and slightly more opaque, while the projected
tail stays blue on purpose — it continues today's line.
2026-07-15 13:18:42 +00:00
Alexander Heldt 9b6ee20b07 Don't scroll to the history when a chart day is tapped
Selecting the day (and the new selected-day highlight) is enough;
the scroll jump lost your place in the charts.
2026-07-15 12:48:23 +00:00
Alexander Heldt 3e3eee678e Show a meal's grams in the history list
Same pattern as weigh-ins: "80 g · note" or just "80 g" when there's
no note.
2026-07-15 12:47:39 +00:00
Alexander Heldt 6d54ecf238 Label every 1h gridline on both sleep charts
Instead of thinning labels on dense axes, keep them all and drop the
label font to 7px past 12 steps (bars) / 16 steps (trend).
2026-07-15 12:45:53 +00:00
Alexander Heldt 83af641a12 Use 1-hour gridlines on the daily Sleep bar chart
niceAxisSleepHours drops from 2-hour to 1-hour steps and is shared with
the trend chart again (the trend-only helper it duplicated is gone).
Labels thin to every other gridline past 12 steps — this chart's plot
area is shorter than the trend's.
2026-07-15 10:56:16 +00:00
Alexander Heldt ba6e2c5e7d Make the Sleep trend chart taller with a finer 1-hour y-axis
220px viewBox (was 160) and 1-hour gridlines via a trend-specific axis
helper, so curves that run close together separate visually. Labels
drop to every other gridline past 16 steps to keep the text readable.
2026-07-15 10:49:27 +00:00
Alexander Heldt 0e24ac989d Show each curve's slept hours in the Sleep trend legend
Today, Yesterday and the N-day average chips now carry their totals
(e.g. "Yesterday 12.8h"), matching the projected chip.
2026-07-15 10:43:22 +00:00
Alexander Heldt ab0e51108c Project today's end-of-day sleep total on the Sleep trend chart
A dashed tail continues today's line from now to midnight by adding the
increments the N-day average curve makes over the same stretch, so the
projection follows the usual daily rhythm instead of extrapolating the
current rate (which overshoots right after a long night). The legend
shows the projected total; with no history there's no average and no
projection.
2026-07-15 10:36:22 +00:00
Alexander Heldt cebe923d68 Highlight the selected day in the per-day charts
A soft accent band behind the selected day's bars/cells (sleep, counts,
food, training heatmap) — visible even when the day's values are zero —
plus an accent-colored day label, and an accent ring on the selected
row of the sleep timeline. The band tracks the day picker, so tapping
into a chart immediately shows which slice you landed on.
2026-07-15 10:24:48 +00:00
Alexander Heldt 8dc4fca4e2 Add a Sleep trend chart: cumulative sleep today vs yesterday and the recent average
Answers "is the puppy behind on sleep right now?": cumulative hours
slept sampled at each hour boundary, today's line ending at the current
moment, with yesterday and the mean of the last N days as reference
curves. N follows the 7/14/30 chart-days picker, and days with no sleep
logged are skipped so a young log doesn't drag the average down.
2026-07-15 10:18:08 +00:00
Alexander Heldt b030cfb72a Dim quick actions that don't fit the current sleep state
Asleep: everything but Sleep end fades; awake: only Sleep end fades; no
sleep history: nothing. Buttons stay clickable — it's a visual nudge for
the thumb, not a lockout, so corrections still work.
2026-07-15 10:17:20 +00:00
Alexander Heldt 7cb59a7a23 Focus a text field when the edit dialog opens, never the date input
showModal() with no explicit focus autofocuses the first control — the
date input — which on iOS pops the native calendar over the form. Focus
weight/grams/note per event type instead, like openNoteDialog does.
2026-07-15 10:16:56 +00:00
Alexander Heldt 131346a7c7 Hide dialog weight/grams fields on event types that don't use them
The JS already sets .hidden per type, but 'dialog label { display: block }'
outranks the UA [hidden] rule, so the fields rendered anyway. Add the
explicit [hidden] override, matching the pattern used elsewhere in the file.
2026-07-15 10:16:29 +00:00
Alexander Heldt b608dfc342 Show a compact year-less date in the day bar so it fits small iPhones
The native date input can't be told to drop the year, so a button face
shows e.g. "Jul 10" and the real input sits hidden behind it, still
holding the value and opening the native picker on tap.
2026-07-15 10:13:19 +00:00
Alexander Heldt cdd701f0b4 Bring back the big timer; the bar pill takes over on scroll
The big asleep/awake card returns below the frozen bar, and the bar
pill becomes its twin: both tick together, but the pill stays
visibility-hidden — slot reserved, so the bar never shifts — while the
card is on screen, appearing only once the card scrolls out of sight
(rAF-throttled scroll check against the bar's bottom edge).
2026-07-13 21:31:29 +00:00
Alexander Heldt 8981214e55 Fit the whole top bar on one row
Timer, day arrows, date picker and Today all share a single non-wrapping
row: the timer slims to 1rem, buttons and gaps tighten, arrows sit
around the picker, and a narrow-phone media query compacts further.
2026-07-13 21:26:07 +00:00
Alexander Heldt b6301156f9 Let the chart window be picked: 7, 14 or 30 days (default 7)
A 7d/14d/30d picker in the charts card sets how many days every
rolling chart covers — daily bars, sleep timeline, hour heatmap and
training consistency grid. The choice is stored per device like the
theme; headers show the current window, day labels thin out and bar
gaps tighten as the window widens, and the sleep timeline grows rows
to fit.
2026-07-13 21:20:31 +00:00
Alexander Heldt 6b12820592 Pin the timer left in the day bar with the slack before the day controls 2026-07-13 20:56:12 +00:00
Alexander Heldt cee4455651 Make the food chart y-axis much finer
The grams axis now targets ~10 segments instead of ~4, so a 240 g day
gets 25 g gridlines; steps stay round numbers at any scale.
2026-07-13 20:52:48 +00:00
Alexander Heldt 695c030f33 Make the timer pill bigger and put it first in the day bar 2026-07-13 20:50:41 +00:00
Alexander Heldt a0e084dae8 Extend the daily charts from 7 to 14 days
Sleep, daily counts and food grams now share a 14-day window like the
pattern charts. Bar gaps tighten to fit 14 columns and the x-axis
labels today plus every second day so labels don't collide; tooltips
and click-through per bar are unchanged.
2026-07-13 20:45:22 +00:00
Alexander Heldt c103082ca5 Replace the big clock with a timer pill in the frozen day bar
The full-width asleep/awake card becomes a compact colored pill next to
the day controls: state emoji plus the live counter, with "since" moved
to the tooltip. One glance at the pinned bar now gives both the day and
the current state.
2026-07-13 20:40:01 +00:00
Alexander Heldt 0009e63d23 Freeze the day picker at the top of the page
The day bar moves above everything else and sticks while scrolling, so
switching days never requires scrolling back up. A fixed strip painted
over the safe-area inset keeps content from peeking through above the
stuck bar on notched phones in standalone mode.
2026-07-13 20:39:06 +00:00
Alexander Heldt a202f3e929 Support multiple photos per event
photoId now holds one or more photo UUIDs, comma-separated. The server
never interprets the field (photos are uploaded and served individually
by UUID), so no schema change is needed and legacy single-photo events
are already valid one-element lists.

Both dialogs let you keep adding photos, previewed as thumbnails with a
per-photo remove button; the file input allows multi-select and no
longer forces the camera, so the gallery is available too. History rows
show every photo, each opening in the lightbox.
2026-07-13 20:34:07 +00:00
Alexander Heldt 3a829161e3 Track food eaten by weight in grams
Logging a meal now opens the dialog (like weigh-ins) with an optional
Amount (g) field; existing meals can get an amount via the edit dialog,
where clearing the field drops it. The overview's Meals tile shows the
day's total grams, and the weekly card gains a Food (grams) chart with
a self-scaling axis that stays hidden until any meal has an amount.

Events carry a new grams field (REAL column, auto-migrated); addEvent's
growing optional parameters are folded into an options object.
2026-07-13 20:28:53 +00:00
Alexander Heldt 68964b55e4 Add a Changelog button at the bottom of the page
Opens a dialog listing the loaded build's full changelog, grouped by
date. Fetched cache-first through the service worker, so the list always
matches the running version; the update banner still covers what a
waiting build adds.
2026-07-12 17:36:44 +00:00
Alexander Heldt c8dceb14d4 Add CLAUDE.md with changelog upkeep reminder 2026-07-12 17:30:41 +00:00
Alexander Heldt 1c4bea51df Increase the daily counts chart y-axis granularity
1-unit gridlines up to 10, 2-unit up to 20, 5-unit beyond — previously
anything above 10 was squeezed into five segments.
2026-07-12 17:30:16 +00:00
Alexander Heldt 094af3906d Show a changelog in the update banner
changelog.json is a hand-maintained, newest-first list of user-visible
changes; it is part of the SW asset cache and the build hash, so even a
changelog-only edit rolls a new version.

When the update banner appears, the app fetches changelog.json twice:
the plain URL is answered cache-first by the old controlling worker
(the running build's copy) while a cache-busting query bypasses every
SW cache and hits the network (the waiting build's copy). Entries the
fresh copy has that the cached one lacks are exactly what the update
brings, and are listed under the banner message.
2026-07-12 17:29:32 +00:00
Alexander Heldt 2b4731185c Keep PWA builds coherent by bypassing the HTTP cache
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).
2026-07-12 17:13:13 +00:00
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