Commit Graph
4 Commits
Author SHA1 Message Date
Alexander Heldt fab0ae1420 Add a Meals log to Habits, with a bulk label for unlabelled meals
A meal's amount and its kind are both optional by design, which is what
makes them easy to lose track of: a meal with no kind sits in the grey
band on the food chart, one with no grams is dropped from it silently,
and there was nowhere to go and see which meals those were.

The panel lists every meal across every day, ignoring the day picker as
the Notes log does — the ones worth finding are spread through history
rather than sitting on the day being viewed. Each filter chip carries
its own count, so the size of the gap reads off the panel without
selecting anything. Rows go through attachRowHandlers, so tapping one
opens the usual edit dialog and long-press still measures.

On the "no kind" filter it also offers to label the whole list at once,
which is what history needs after kinds arrived months into logging.
backfillFoodKind does it in one pass — one save, one sync, one render,
following setDefaultFoodKind rather than calling updateEvent hundreds of
times; sync already posts the whole event list, so it costs nothing
extra on the wire. The optional cut-off date filters the list as well as
the edit, so the number on the button is the rows on screen: labelling
all of history as one kind is wrong if the food was switched partway,
and once labelled the early meals cannot be told apart. There is no
undo, so the guards are the live count, the number on the button and a
confirm. Owner-only; the server's guest rules would refuse it anyway.

Meals are the only event type that gets this — the only one with
optional fields worth filling in afterwards.

Nothing appears until there is a meal to list, and the kind filter and
bulk block wait for a kind to exist, so an account not using kinds is
unchanged.
2026-09-22 15:38:13 +00:00
Alexander Heldt 41827664de Put the day's food breakdown inside the Meals card
The total sat in the Meals card and the per-kind split sat in a row beneath the
whole stats grid, so the two halves of the same figure were in different
places. The split now goes under the total it adds up to.

I argued against this when the row went in, on the grounds that a 90px tile
cannot hold a breakdown. That was true of the shape I had in mind — "Dry 260 g
· Fresh 100 g" inline is about 110px — and not of the one that belongs there:
stacked, a line per kind, it is about 60px.

A single kind gets no line of its own. The total is already that kind's figure,
so the name joins it — "540 g · Dry" — rather than repeating the number
underneath. Same rule the chart's day readout uses, and for the same reason.

A day with no kinds on its meals leaves the tile exactly as it was, which is
the overview anyone not using kinds keeps.

The changelog entry for this has shipped and said the breakdown appears "under
the stat tiles". It does not any more, so it is corrected in place: leaving a
description that is now wrong would be worse than editing an entry some readers
have already seen.
2026-09-22 15:10:51 +00:00
Alexander Heldt 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.
2026-09-22 10:38:47 +00:00
Alexander Heldt 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.
2026-09-22 10:31:27 +00:00