69 Commits

Author SHA1 Message Date
Alexander Heldt f9894abfc9 Add free-text day notes with an always-visible notes log
A new 📝 Note quick-action logs a free-text note (with a date, optional
photo, and any text), defaulting to the day you're viewing. Notes are a
distinct "note" event type, so they edit, sync and delete like any other
event, and a new Notes section lists them newest-first across every day —
independent of the day picker — so records like vaccinations stay visible
whatever day you're looking at.
2026-08-02 13:37:39 +00:00
Alexander Heldt 09d9d38c12 Add metric checkboxes to the daily counts chart
The Daily counts legend is now a Pees/Poos/Meals checkbox group, so you
can hide metrics and focus on just the ones you care about. The choice is
persisted per device, at least one metric is always kept visible, and the
y-axis and bar widths adapt to the selected metrics.
2026-08-02 13:26:38 +00:00
Alexander Heldt a44c75d2d4 Let the confetti fade at its peak instead of falling back
Drop the descent phase: each emoji now rises to its peak, slows (ease-out) and
fades out at the top rather than arcing back down. Removes the now-unused --endY.
2026-08-01 18:28:40 +00:00
Alexander Heldt e2f99590f1 Make the pee/poo confetti a bottom-up fireworks fountain
Rework the effect: instead of a small burst from the tapped button, 30 emoji
launch from across the bottom of the screen, rise to random peaks while
drifting sideways, then arc back down and fade — staggered launches and varied
speed/size/rotation for a fuller, firework feel. Still pointer-events:none,
self-removing, and gated by the Settings toggle and reduced-motion.
2026-08-01 18:24:33 +00:00
Alexander Heldt 52c50c97b1 Celebrate a logged pee/poo with emoji confetti
Tapping the pee or poo quick-log button now sets off a short burst of 💧/💩
particles from the button — pure DOM + CSS, pointer-events:none so it never
blocks taps, particles self-remove on animation end. It honours
prefers-reduced-motion, and there's a "Pee/poo confetti" switch in Settings
(device-local, on by default) to turn it off.
2026-08-01 18:18:26 +00:00
Alexander Heldt 1ed325b834 Keep the header and weight rows tidy when the age is long
The age counter can read "16 weeks (3 months and 3 weeks) old", which broke
two tight layouts: the header title never truncated, so a long name (or the
wide status pill) collided with the action buttons and the age wrapped to
three lines; and each weight-log row embedded the full age, wrapping to two
lines.

Add compact age formatters and use them where space is tight: formatAgeShort
("16 wk · 3 mo 3 wk") in the header, formatAgeWeeks ("16 wk") in weight rows;
the verbose form stays on the roomy weight-chart caption. Make the header
robust — the title flexes and the name ellipsis-truncates so the buttons are
never pushed, while the age wraps rather than truncating so it's never cut
off. Weight rows keep the date/age on one line with the value pinned right.
2026-08-01 18:11:03 +00:00
Alexander Heldt 69e312175b Add a radial fan-chart view of the pedigree
Add a Fan / Tree toggle in the pedigree header. The fan places the dog in a
centre disc with each generation as a ring fanning outward, so up to nine
generations fit in one compact circle instead of a tree that doubles in
width every generation. Inner rings are labelled; tap any wedge for that
dog's details. Pedigree-collapse dogs keep their stable hue, and tapping one
lights up every wedge of that dog. The existing zoom applies to the fan too.

Render dispatch is factored into renderPedigree(); the collapse-highlight
now works on any element (tree card or fan wedge) carrying a data-dogkey.
2026-07-26 11:13:16 +00:00
Alexander Heldt a2c9aa9716 Add a collapse/expand-all toggle to the pedigree
A header button folds the whole tree down to just the dog or opens every
branch at once, alongside the per-dog toggles. Grouped with the zoom
controls, which the header now wraps on narrow screens.
2026-07-26 10:50:12 +00:00
Alexander Heldt 12a5bd0548 Make the pedigree tree zoomable
Add zoom to the pedigree: −/reset/+ buttons in the header, ctrl/⌘ + wheel,
and two-finger pinch. Scaling uses the CSS `zoom` property (not transform)
so the container reflows and every part stays reachable by scrolling at any
level. Clamped to 40–160% and the level is remembered per device.
2026-07-26 10:49:54 +00:00
Alexander Heldt 01d64b682b Flag pedigree collapse with repeat badges
When a dog fills more than one position in the pedigree (pedigree collapse),
mark each copy with a ×N badge and a stable per-dog colour, and let tapping
any copy highlight every place that dog appears. Identity is the registration
number (falling back to name), so the same ancestor at different positions is
treated as one. The summary line also reports how many ancestors recur.
2026-07-26 10:39:51 +00:00
Alexander Heldt 8157e95066 Render the pedigree top-down as a family tree
Replace the indented ancestry list with a top-down chart: the dog on top,
sire (left) and dam (right) branching below, connected by lines drawn with
each node's CSS ::before/::after (a nested <ul>/<li> tree). It shows three
generations by default and each dog expands to trace the line further back;
deeper levels get wide, so the view scrolls horizontally.
2026-07-26 10:39:28 +00:00
Alexander Heldt 86e51bb851 Make the pedigree id sticky across profile sync
The dog id in the synced profile shares one last-write-wins timestamp with
name and birthday, and that timestamp comes from each device's own clock.
A device whose profile was stamped later (clock skew, or a more recent
name/birthday edit) would refuse to adopt a newer server profile that had
just gained an id, or even push its empty id back over the stored one — so
the pedigree id set on one device never reached the others.

Make the id sticky on both sides of sync: an empty value never clears a set
one, and a device/server adopts an id it is missing regardless of the blob
timestamp. When both sides have an id, the newer profile still wins, so the
dog can still be changed. Name and birthday keep plain last-write-wins.
2026-07-26 10:19:01 +00:00
Alexander Heldt c2f74e64c8 Gate pedigree behind a dog id set in settings
The pedigree view is now opt-in and tied to your own dog rather than an
always-present free-text search. Add the dog's SKK chip or registration
number in Settings (it rides the synced profile alongside name and
birthday); the 🌳 button stays hidden until one is set, then opens the
page and loads that dog's ancestry directly.

Make repeat opens cheap: memoise the id->hundid resolution server-side so
a cached tree is served without contacting SKK at all, and mirror the
finished tree in localStorage so the page paints instantly and shows the
last-known tree offline.

Adds config.pedigree_id (with an in-place migration for existing DBs).
2026-07-26 09:36:49 +00:00
Alexander Heldt 26ebe3bd86 Add pedigree lookup and ancestry tree page
New 🌳 Pedigree view: enter a dog's ISO chip or SKK registration number
and see its ancestry rendered as a tree. SKK has no public API, so the
server drives SKK Hunddata like a browser: it resolves the input to an
internal hundid via the Hund_sok.aspx/HundData page-method, renders 7
generations per pedigree page, parses the rowspan grid into ahnentafel
positions, and follows each generation's leaves deeper by reading their
hundid out of the __doPostBack response viewstate.

A lookup returns the first 7 generations immediately and crawls deeper in
the background; the client polls and fills the tree in as ancestors
arrive. Finished trees are cached per dog in a new pedigree_cache table
(pedigrees don't change), so a dog is crawled once and repeats are instant.
The endpoints sit behind auth like the rest of /api/*, and the crawl is
kept polite (warmed session, delay between requests, one coalesced job per
dog, hard caps).
2026-07-26 09:22:14 +00:00
Alexander Heldt 374e630d8f Show the age counter as weeks with a months breakdown
Weeks and calendar months disagreed at a glance ("16 weeks" but only
"3 months"). Now the counter reads "16 weeks (3 months and 3 weeks)
old" so the two units line up, and drops the weeks past 4 months to
show just the months form.
2026-07-24 15:17:09 +00:00
Alexander Heldt 22a5ea76aa Make the Sleep trend chart follow the selected day
The curves were always computed relative to today regardless of the day
picker. Anchor them to selectedDay() instead: a past day shows its full
24h curve against the day before it and the average of the days leading
up to it, with no now-cap and no projection. Legend and tooltip labels
show the actual dates when viewing a past day.
2026-07-23 08:45:26 +00:00
Alexander Heldt 0ebaa11c93 Stretch the Sleep trend y-axis above 10h
The interesting detail sits near the goal, in the upper teens of hours,
so the scale is piecewise: 0–10h shares a compressed 26% of the height
with 2h gridlines, and everything above gets the rest with 1h
gridlines. Falls back to linear while the axis is too short to split.
The caption notes the stretched axis.
2026-07-15 19:06:16 +00:00
Alexander Heldt 897311465c Show the age-based sleep goal as a band in the Sleep trend chart
The puppy's age (from the configured birthday) picks the daily goal —
0–8 weeks 20–22h, 8–16 weeks 18–20h, 16–18h to 6 months, 14–16h to 12
months — drawn as a shaded band behind the curves, with a legend chip
naming the range. The Projected chip gains a ✓/▽ marker for whether
today's projection reaches the goal's lower bound, and the y-axis
always extends to cover the band. No birthday (or an adult dog) means
no band, unchanged from before.
2026-07-15 17:35:55 +00:00
Alexander Heldt 8fe8f9d417 Log training sessions onto the selected day, not always today
The Log button stamps the selected day at the current time of day, so a
forgotten session can be back-filled from that day's view. The snackbar
says which day it landed on when it isn't today; Undo/Add note still
work on it.
2026-07-15 13:26:30 +00:00
Alexander Heldt c24d59e672 Make the Sleep trend yesterday line orange
Gray sank into the dashed grid lines; --eat orange stands apart from
today's blue and the average's teal in both themes.
2026-07-15 13:24:55 +00:00
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