18d3778241630eecf1a9aee893a10bfe4ae65ae1
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
18d3778241 |
Break today's food down by kind in the overview
The Food chart splits by kind but today's overview did not, so the one place you look first still lumped dry and fresh into a single total. It is a line under the stat tiles rather than part of the Meals tile. That tile is about 90px wide with a 0.7rem sub-line, and two kinds will not sit in it without wrapping into a mess — so the tile keeps the day's total, which is the headline figure, and the breakdown gets the room it needs. It follows the chart's conventions so the two read as one breakdown rather than two arbitrary lists: the same layer order, "No kind" last, a kind deleted since still named through its tombstone, and a meal logged without an amount adding nothing. Hidden unless a meal that day carries a kind, which keeps the overview untouched for anyone not using them — the case the first assertion in the new suite pins down. This was a gap rather than a reversal: when the split was scoped to "the grams chart only", the options named the counts chart, the by-hour heatmap and Timing as staying put. The overview's food total was in neither list, so it was never decided either way. |
||
|
|
5a08fb4510 |
Let a meal say what kind of food it was
Grams alone put dry and fresh in the same total, so the log could not show that fresh had been creeping up or that a soft stomach followed a switch. A meal can now carry a kind the user names themselves. The whole thing is optional, and that constraint shaped most of it. "No kind" is a real value rather than a missing one: it is what every meal already logged carries, so nothing needed migrating; it is always offered in the picker; and with no kinds defined the picker, the legend and the split are all absent, so the app is byte-for-byte the one it was for anyone who never wants this. The checks cover that case specifically, because it is the one nobody would notice breaking. Kinds are a third synced collection beside events and exercises, with the same contract — uuid ids, per-item last-write-wins, tombstoned deletes — so renaming a kind updates the meals logged as it, and deleting one leaves them readable under the name the tombstone kept. FoodKindStore duplicates ExerciseStore closely; Store and ExerciseStore were already near-twins, so a third in that shape is this file's pattern and leaves two working collections untouched. Folding all three into one store over a table name is the tidier end state and a separate job. Two decisions worth naming. The default kind is a flag on the kind rather than a profile field: the profile is last-write-wins across the whole row, and this codebase already carries a special case for pedigree_id because that dropped a value once — per-item LWW means two devices that each choose a default resolve to the newer instead. And each kind keeps a colorIndex fixed at creation, so deleting one never repaints the charts of the kinds around it. The bars stack by kind with a line fitted per kind. Each line sits at that kind's own daily amount rather than at the top of its segment: the segment's height is what the kind ate, but its position is an accident of what is stacked beneath it. So a line can cross a segment it does not belong to — dashed and in the kind's colour, with the figures named underneath either way. One sentence per kind would grow with the list, so only kinds whose move beats their own scatter get one and the rest fold into a clause. Both tests are ones foodTrend already applied; nothing new is being claimed. A guest labels a meal with a kind that exists but cannot add, rename or delete one, exactly as with the exercise library. |
||
|
|
9e47aa53ff |
Stop the food trend's figures contradicting each other
The caption read like "down about 329 g a week — roughly 460 g a day then, 320 g a day now". Subtract the two amounts and you get 140 g, not 329. The arithmetic behind it was self-consistent, but the sentence was not, and a caption a reader can disprove by subtracting its own numbers is wrong whatever the code was doing. The rate was slope x 7, while the line only covers the complete days in the window. Today is never fitted, being unfinished, so a 7-day window leaves at most five days and any marked day takes another — in this case about three. The rate was therefore stretched well past the days it was measured from, and the two endpoints, which were not, could never agree with it. It quotes the change between the two ends now, which is the one figure a reader can check: "down about 140 g — from roughly 460 g a day to 320 g." The change is derived from the rounded ends rather than from the slope, so the subtraction works exactly rather than to within the rounding. The weekly rate goes rather than being repaired. It cannot reconcile on a short window, and it only ever meant anything where the fit spanned a week or more — which is not something to leave as a trap for whichever window the reader happens to have picked. The check that let this through asserted the sentence contained certain phrases, not that its numbers agreed with each other. There is now one that parses all three figures back out and asserts the move is exactly the difference of the ends, across each window length; it fails against the old wording, which is the only evidence worth having that it would have caught this. |
||
|
|
556e4d75a8 |
Measure the time between two events by long-pressing them
"How long after eating did he poo?" is answerable from the log, but only by reading two times off the screen and subtracting them — and the pair is often on different days, so it is rarely on screen together at all. Hold one row, hold another, and a bar along the bottom does the subtraction and keeps it until you clear it, which is what lets you change day between the two picks. Any row that is a single moment can be picked: history, notes, weigh-ins. Sleep and walk rows cannot, being spans — measuring from one would need a rule about which end, and a rule you have to remember is worse than the feature. The picks are a module-level variable rather than storage. A measurement is a question you are asking now, not a setting; but module-level is also what carries it through the re-render a background sync causes every minute, which would otherwise wipe a half-made measurement. Ids that stop resolving — deleted here, tombstoned by another device — leave the pick on the next render instead of lingering as half a pair. Two additions beyond what was asked. Holding a picked row unpicks it: that is not a third selection but an undo of one, and without it a mis-press costs a clear. And the reading is ordered by time rather than by which was pressed first, so it is always chronological and never negative — pressing upward through a log is the natural way to read it. The press mechanics are all load-bearing: a finger that travels is a scroll and cancels, a fired press swallows the click that would otherwise also open the edit dialog, and the platform's own long-press menu is suppressed. That last part needs user-select: none on the rows, which costs the ability to select a note's text to copy. Worth stating plainly — it is a real loss, taken because holding a row now means something else. checks/extract.mjs gained getters for mutable bindings while writing the checks for this. It only ever returned a let's value at load time, so measurePick went stale the moment the code reassigned it and the checks were quietly asserting against a snapshot. Any future check reading a mutable binding would have hit the same thing. |
||
|
|
e4a5c3fe29 |
Leave a day that doesn't count off the trends entirely
The Sleep and Walk trends already kept a day marked "not counted" out of the window average and out of the "yesterday" comparison. What they still drew was that day's own curve, when it was the day you had selected — as the boldest line on the panel. So the single day you had said not to trust was the one the chart led with, against references that had carefully excluded it. It is left off now, along with its legend chip and, for the sleep trend, the projected tail that continued it. What remains is the average and yesterday, which is what you would want to look at on a day like that. This reverses part of an earlier fix. That one stopped the curve being drawn as a flat zero, on the reasoning that marking a day means "don't let it drag the average" rather than "pretend nothing happened". The flat zero was certainly wrong, but so was the conclusion: a real curve for an untrusted day is still the wrong thing to lead with. Absent is the honest third option. The walk trend gets the same treatment. It is the same panel in different units, and the two disagreeing about what a marked day means would be worse than either answer. |
||
|
|
7451650b6f |
Give the food trend's figures, not only its rate
"Up about 40 g a week" is a rate with nothing to anchor it: it says the line slopes without saying where it sits. The sentence now names both ends of the fit — "roughly 280 g a day then, 400 g a day now" — which is the reading anyone actually wants from a growth chart. When the fit is not trustworthy it quotes the average instead, and says the day-to-day variation is larger than any trend. That difference is the point. The average is a measurement and survives the noise; the ends of the line are the line's own output, and quoting them on a fit nobody should read would dress a guess up as a reading. Everything rounds to 10 g for the same reason — "287 g a day" would be false precision from four noisy points. The wording moves into a pure foodTrendSentence() so those rules can be checked, which is worth doing precisely because they are judgement rather than arithmetic: the checks now pin that a clear climb gives both figures, a flat run gives the average and no endpoints, a see-saw gives neither, and nothing is ever quoted to the gram. |
||
|
|
668f1f039e |
Draw a trend line through the food bars
The daily grams bars bounce around enough to hide a steady climb, so they cannot answer the question you actually have about a growing puppy: is he eating more than he was? A least-squares fit through them can. Two kinds of day stay out of the fit. Today is half-eaten, and including it would pull the line down every morning and let it drift back up as meals go in — a line that tracks the clock rather than the dog. A day marked "not counted" has a hatch rather than a figure, and fitting a zero there would invent a dip. The line is drawn only across the days it was fitted on, so it never implies it knows about the ones it skipped. The caption is the part that needed the care. A straight line through seven noisy points will always have a slope, and announcing it as a fact is the same mistake the walking goal made. So a direction is named only when the fitted climb is larger than the scatter of the days around it, and only when it clears 5 g a week and a twentieth of a typical day; otherwise it says the variation is larger than any trend, which over a short window is usually the truth. It names its window too — "over the last 14 days" — because the 7/14/30 picker already drove this (weeklyData builds the array the fit runs on) but nothing on screen said so, and the line moves too little between windows to show it. When there are fewer than four complete days it now says why there is no line rather than leaving bars with nothing through them. Meals can be logged without an amount, so the note counts them: a day can read low because he ate little or because nobody typed the number, and the chart should not let those look the same. The checks cover the refusals rather than the arithmetic — a see-saw is not reported as a trend, a slope under the scatter is not either, three days will not fit, a marked day does not shift the line, and each window length reaches the fit intact. |
||
|
|
14cad44d9b |
Stop the page growing wider than the screen
A phone had started allowing zoom-out, which is how a document wider than the viewport announces itself. The suspect was the "Ate" modal, but the dialogs are not it: all seven open with showModal(), so their containing block is the viewport and width: calc(100% - 32px) cannot exceed it. It was the month grid, added three commits ago: left: 50%; transform: translateX(-50%); width: 268px; max-width bounded the panel's width and nothing bounded its position. Centred on the date button — which sits near the right edge of the bar — a 268px panel hangs off the side of a phone, and being absolutely positioned it drags the document's scrollable width out with it. Anchoring to the button cannot be made safe: pin it right and it overflows the left on a narrow screen, pin it left and it overflows the right. So it is a child of the day bar now, pinned to that bar's inner edge and capped at the bar's own width. The bar spans the content width exactly, so the panel is on screen at every size by construction. A long unbroken word was a second way in, and a pre-existing one. A history row's note is a flex item with neither min-width: 0 nor a break rule, so a URL or something copied off a food bag sets its content-based minimum and widens the row. The Notes log directly below already guarded against precisely this, so the history row had simply been missed; exercise names and their instructions had the same gap. The checks gained the general form of both, since this class of bug is invisible until a phone starts zooming out: every element that renders text the user typed must be able to break a long word, the grid must stay edge-anchored inside the bar, and no fixed width may exceed the content box of a 320px phone. Each was confirmed to fail with its fix reverted. |
||
|
|
59cf567946 |
Add frontend checks, run from the repo
The server has go test; the frontend had nothing, and the things most likely to break there are the ones hardest to see: the arithmetic behind the charts, a panel lost while shuffling tabs, a label that truncates on a phone none of us owns. There is no browser in this loop, so these are what can be checked without one. They read the real code rather than copying it. The app is one long IIFE with nothing exported, and adding a module system or a build step to make it testable would be a large change in service of a small one — so checks/extract.mjs reads src/app.js, brace-matches the declarations a check asks for, and evaluates them. Rename a function and it throws by name. A check quietly exercising a stale copy of the code would be worse than no check, and that is the failure mode this avoids. calendarGridStart is pulled out of renderCalendar as part of this. It is the one line of the month grid that is easy to get wrong and impossible to notice — a month starting on the week's first day needs no backing up, one starting the day before needs six — so it earns a name and a test. The width figures are estimates, not measurements: layout numbers come out of style.css so they cannot drift, text is sized from per-character advances, and the pass mark demands a few pixels of headroom because the estimate is only good to a few percent. They will catch a sixth tab or a longer label. They will not settle a two-pixel question, and nothing here replaces looking at a phone. No new dependencies: nodejs is already in the devShell for `node --check`, and checks/ sits outside src/ so it is not served with the app. |