Track walks as timed exercise

A walk is a start and an end, so it reuses the shape sleep already has rather
than inventing one: walk-start / walk-end events, paired into windows, with a
trailing unmatched start meaning "out right now". The server stores type as an
opaque string, so nothing there changes and the events ride the existing sync.

Pairing boundaries into windows was written out twice already, once for sleep
and once for its inverse, so this pulls the scan into pairWindows(open, close)
and makes all three callers of it. Same for the latest-boundary lookup behind
currentSleepState, which currentWalkState now shares — including the updatedAt
tie-break, which matters as soon as a start and an end land in the same minute.

They are called walks, not exercise. "Exercise" is already taken by the
training definitions (their own synced collection, and exerciseId on training
events), and two meanings of the word in one app would be worse than the
slightly narrower name.

The day's total leads the overview tile with the count underneath, since the
question is how much exercise the puppy got rather than how many outings it
took. The Walks panel lists the day's windows and carries the total in its
heading so a collapsed panel still answers it, and the daily charts gain a
minutes-per-day bar chart that stays hidden until there is a walk to draw —
the grams chart's rule. The panel also states the five-minute rule for the
puppy's current age, the same way the sleep trend states a goal band.

Walk boundaries answer to the walk state, not the sleep one, so "Walk end" is
disabled with no walk running and stays undimmed mid-walk even while the puppy
is logged asleep.
This commit is contained in:
Alexander Heldt
2026-08-31 21:23:40 +00:00
parent ead575df2c
commit f162ac5732
5 changed files with 243 additions and 56 deletions
+25 -2
View File
@@ -111,14 +111,18 @@
<!-- Explicit rows, grouped by what the buttons mean, rather than one
auto-fit grid: a start/end pair has to stay side by side at every
width (a flat grid split them apart at some column counts, and
orphaned the last button on a row of its own). Row 1 is the timed
pair, row 2 the one-tap moments, row 3 the two that open a dialog
orphaned the last button on a row of its own). A row per timed
pair, then the one-tap moments, then the two that open a dialog
to type a value. -->
<div class="actions">
<div class="action-row">
<button class="action sleep" data-type="sleep-start">😴 Sleep start</button>
<button class="action sleep" data-type="sleep-end">⏰ Sleep end</button>
</div>
<div class="action-row">
<button class="action walk" data-type="walk-start">🦮 Walk start</button>
<button class="action walk" data-type="walk-end">🏁 Walk end</button>
</div>
<div class="action-row three-up">
<button class="action eat" data-type="eat">🍽️ Ate</button>
<button class="action pee" data-type="pee">💧 Pee</button>
@@ -154,6 +158,11 @@
<div class="stat-label">Awake</div>
<div class="stat-value" id="stat-awake">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Walks</div>
<div class="stat-value" id="stat-walk">0m</div>
<div class="stat-sub" id="stat-walk-count" hidden></div>
</div>
<div class="stat">
<div class="stat-label">Meals</div>
<div class="stat-value" id="stat-meals">0</div>
@@ -178,6 +187,7 @@
<div class="last-row"><span>Last poo</span><span id="last-poo"></span></div>
<div class="last-row"><span>Last meal</span><span id="last-eat"></span></div>
<div class="last-row"><span>Last sleep</span><span id="last-sleep"></span></div>
<div class="last-row"><span>Last walk</span><span id="last-walk"></span></div>
</div>
</section>
@@ -221,6 +231,15 @@
<p id="wake-empty" class="empty">No wake windows yet for this day.</p>
</section>
<section class="walks" data-panel="walks">
<h2>Walks <span class="muted-note" id="walk-total"></span></h2>
<ul id="walk-list" class="wake-list"></ul>
<p id="walk-empty" class="empty">No walks yet for this day. Use 🦮 Walk start / 🏁 Walk end to time one.</p>
<!-- Age-based guidance ("the five-minute rule"), only when a birthday
is set and the puppy is still growing. -->
<p id="walk-goal" class="muted-note" hidden></p>
</section>
<section class="weekly" data-panel="weekly">
<h2 id="daily-charts-title">Last 7 days</h2>
<div class="chart-days-picker" role="group" aria-label="How many days the charts cover">
@@ -241,6 +260,10 @@
<label class="lg eat"><input type="checkbox" data-metric="meals" checked /><span class="sw"></span>Meals</label>
</div>
</div>
<div class="chart" id="walk-chart-wrap" hidden>
<div class="chart-title">Walks (minutes)</div>
<svg id="chart-walk" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Minutes walked per day"></svg>
</div>
<div class="chart" id="grams-chart-wrap" hidden>
<div class="chart-title">Food (grams)</div>
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day"></svg>