Blog Β· release
Latch 0.8.0
Charts and insight: /progress grows four tabs. Closes issue #12 β the quantified side
of the game was a wall of tiles and a heatmap, with the five check-in scores, the locked
hours, the training rates and the health readings all sitting in the log unlooked-at.
/statsis now/progress, with four tabs: Overview (the tiles, rates, heatmap and activity strips, unchanged), Trends (the charts), Insight, and Log (the training board, event kinds and sessions)./statsstays an alias forever β π΄ as a 307, not a 301: a 301 is cached by the browser indefinitely, so a rollback would serve a redirect to a route that no longer exists from a cache nothing in the app can clear.- π΄ THE TABS ARE LINKS AND EACH COMPUTES ONLY ITSELF. The activity strips are one query
per activity, the weekly series walk every session in the log, and Insight runs every
module's catalogue; four panels pre-built and hidden with CSS would make every visit pay
for every tab and get slower the longer you play. It also means each tab is a real URL β
which is not cosmetic: the hidden-timer leak test now iterates
routes.TABS, because the session table that leaks a live sentence moved behind a tab and a sweep fetching only/progresswould have quietly stopped testing the row that file exists for.
core/chart.py β the geometry, in Python, with tests
No charting library, and not as a sacrifice: a line chart is a list of coordinates, and the
app's CSP is default-src 'self'. The rules are the ones a picture can break silently.
- π΄ A GAP IS A BREAK, NOT A JOIN. A
Nonemeans nobody measured; one polyline through it draws a confident straight line across a fortnight nobody measured. Each run of consecutive known points is its own<polyline>and a run of one is a dot. This rule already existed in a Jinja macro on/health, built with{% set %}list mutation β correct, untestable, and a second copy of the one rule that must never have two answers. That macro is gone;/healthdraws through the same primitive. - π΄ A SHARED Y-AXIS ACROSS INCOMMENSURABLE UNITS IS A LIE. Steps (~20,000) and sleep
(~8) cannot share a scale β and normalising both to the box makes the vertical distance
between the lines look like a comparison when it means nothing.
normalise="each"scales each line to its own range, emits no y-axis labels (they could only describe one series), reports every line's real minβmax in the legend, and makes the template print the sentence: only the time axis is shared. - π΄ A BOUNDED SCALE IS NOT AUTO-RANGED. Check-in scores are 1β10 because the slider is,
not because the data is; auto-ranging a fortnight that ran 5.8β6.2 draws a quarter-point of
noise full-height. β οΈ And the reverse rule was tried and rejected: forcing
lo = 0for all-positive data is a rule about bars, whose length is the quantity. A line is read for shape, and a zero floor squashes sleep's 4.9β9.1 into the top quarter of the box. - π΄ AN UNMEASURED BUCKET IS NOT A ZERO BAR. A week before your log began is drawn as a
faint full-height band, because a zero-height bar is pixel-identical to a week you did
nothing β which is the exact confusion the
Noneexists to prevent. - π΄ THE WEEK IN PROGRESS IS KEPT ON BARS AND DROPPED FROM LINES, and the difference is not a whim: a bar can be drawn hollow and captioned so far, and a polyline vertex cannot. A partial last point is indistinguishable from a fall, so every trend would end in a cliff and teach the reader to distrust the right-hand edge β which is where they look first.
- π΄ TOO LITTLE DATA IS SAID, NOT DRAWN. One point is not a line; a flat stroke reads as steady about a thing measured once. An all-zero bar chart refuses too, because the only scale available for it is invented β an axis reading 0 Β· 0.5 Β· 1 for a count of whole days.
- π΄ THE EDGE X LABELS ARE NOT CENTRED. An SVG has no margin, so a label centred on the last x hangs half of itself past the viewBox and is clipped β and the date a chart ends on is the entire subject of the health export lag. It read "19 Se".
- β οΈ Tick density is derived from the viewBox width, not a constant: six date labels across the 380-unit per-activity charts print "23 Mar20 Apr".
The charts
Locked hours per week β π΄ split at the week boundary, so an eleven-day lock lands in the weeks it was actually spent in rather than as one tower beside two empty weeks. Training done and failed per week. Days reflected per week β days, never entries, the same rule the streak beside it uses. Hours per week for every activity that declares a duration source, which is where issue #12's gear hours comes from, plus headspace, trance sits and play for free and with no kit-specific query in core.
Modules contribute their own: Module.progress_panels, asked for and never registered,
exactly like badge_rules. The Handler owns entries.scores, so the five check-in score
lines are its panel; health owns health_daily, so the steps/sleep/mood overlay is its.
Delete either package and its chart leaves with it.
- π΄ THE HEALTH CHART ENDS AT THE LAST EXPORTED DAY, NOT TODAY. Apple delivers day D at about 01:15 on D+1, so today is empty until tomorrow and a chart drawn to today shows every metric falling off a cliff at the right-hand edge, every single day. Trimming alone is not enough β the reader still has to be told β so the end date is printed under it.
The Insight block
Rule-based observations with the numbers under them, as structured rows so the AI Handler can narrate the same computed facts later rather than a second set. Three rules, all about not saying something:
- π΄ A comparison needs a sample on BOTH sides (five days minimum each) and is omitted rather than hedged. "Headspace is two points higher on workout days" from two workout days is noise wearing a lab coat β and the short side is usually the interesting one, so a floor on the total would not catch it.
- π΄ A difference has to clear a floor to be called one. Two means are never exactly equal; without a floor every comparison finds something, every time, forever.
- π΄ THE CATALOGUE IS DECLARED IN CODE AND EVERYTHING THAT CLEARS ITS FLOORS IS PRINTED. The tempting build β compute thirty comparisons, show the three biggest β manufactures findings: over enough comparisons the largest gap is large by construction, and ranking by effect size is exactly the search that makes it meaningless.
- π΄ "The longest since last Tuesday" is a vacuous sentence that reads like an achievement
β nothing since a recent date could be longer, because the current run is the only run
since then. It needs a real span behind it (a month), or it is not said. Caught by reading
the
detailline under it, which is why every observation carries one: an observation you cannot audit is an oracle, and the whole argument for computing these locally is that you can see what they came from. - β οΈ And a fourth, about language: these are associations in one person's log, not causes. The page says so once under the block rather than hedging every sentence into uselessness.
Modules contribute via Module.insights; a module that needs another's data reaches for
it softly (the Handler's _health), so deleting the health module deletes the health
comparisons and nothing else. A module that raises costs its own observations and no others.
Also
stats.log_startis now one function;tokens._log_startdelegates to it. The weekly buckets use the same instant to tell a week you did nothing from a week nobody was watching, and two answers to "when did this log begin" would drift the moment either grew a condition.- 483 tests (was 433): the geometry, the floors, the tabs, the gap rule end-to-end through the template, the health chart's trimmed axis, and the no-CDN rule asserted on the rendered page rather than on the source.
- β οΈ
READMEsaid.venv/bin/pytest, which cannot importtests.test_stampβ the suite has to be run as.venv/bin/python -m pytest.