Commit Graph
12 Commits
Author SHA1 Message Date
Marc-André Bombeck 111047491e fix(medications): end every medication and intake read on the row id
Postgres returns rows that tie on an ORDER BY in no particular order,
and not the same one twice. The medication reads ordered by columns two
rows share all the time: every read that hands schedules on included
them with no order at all, medications were listed by createdAt alone
(one import writes many in a millisecond), intakes by scheduledFor (a
twice-daily medication puts every morning dose on one instant), and
"the latest intake" lookups by takenAt. A client read the same account
in a different order from one request to the next, and a latest-intake
lookup picked either of two rows.

Every findMany / findFirst on a medication table, nested includes
included, now ends its order on the id in the direction of its last key,
and schedules are read by window, then id (MEDICATION_SCHEDULE_ORDER, the
order the backup already wrote). medication-read-order-guard.test.ts
holds the tree to it. medication-read-order.test.ts writes tying rows in
the opposite order to the one each read promises, through the medication
list and today's intakes, and fails without the tie-breakers.

The parity suite's own state reader had the same gap: it sorts rows by
content with the ids blanked, and rows that tie then kept whatever order
the unordered read returned, so their <id:N> labels swapped under a
loaded run. Its reads are ordered by id, which for a cuid is the order
the rows were written in.

The JSON import's schema left the route, so its entry in the inline
schema strictness register goes; the schema module says why its objects
are not strict.
2026-10-23 21:54:25 +02:00
Marc-André Bombeck 0dcafeeb70 fix(visits): open an upcoming appointment where it lives
Visits are the second tab of `/checkups`, and the bare path opens the
first one, the preventive-care list, which never shows an appointment.
Every surface that listed an upcoming appointment linked it there: the
dashboard's next-appointment line, the Today rail item, the Today
overview fact and the appointment's push notification. A tap on the
appointment landed on the list it is not on.

Each now opens the visit itself (`/checkups?visit=<id>`), or the visits
tab (`/checkups?view=visits`) for a line naming several appointments.
The checkups page reads `view` to pick a tab. The rail item stops
borrowing the check-up item's action: it carries its own label
("View appointment") and intent `visit.view`. The appointment push names
its visit through the producer's `metadata.url` override; a check-up's
push keeps the checkups list.

The same mis-route sat on three links that lead to visits without
being upcoming: a condition's visit rows, the address book's back link
and the onboarding and timeline prompts to add a visit.

`visit-link-guard.test.ts` keeps the visit paths in the builder and
counts every bare `/checkups` per file, each with the reason it names
the checkups list.
2026-10-21 12:22:36 +02:00
Marc-André Bombeck 78bacb0d86 refactor(visits): build every link to a visit in one place
Seven surfaces spelled `/checkups?visit=<id>` by hand, some encoding
the id and some not. They now share `visitPath()` from
`src/lib/encounters/visit-links.ts`, the place the next fix extends.
No behaviour change: a cuid encodes to itself.
2026-10-21 12:22:22 +02:00
Marc-André Bombeck 9f3da5ac7d fix(timeline): keep every medication still taken on its row, and the chronicle honest
An active medication whose courses had all ended had no stretch left in
the current window: the courses decided when it was taken, none reached
into the window, and the medication's own open span was set aside because
it had courses. The row disappeared while the day view listed the
medication as running. The time since its last course now stands in as
its stretch, open to today.

The server cut a lane's items at the window edge, so a medication arrived
with only some of its courses and without the dose change that set the
dose of the piece running into the window. A medication touching the
window is now sent whole.

On a phone, every entry wears its lane's colour: the icon in the lane
colour, and a medication listed inside an illness keeps a dot on its own
rail instead of reading as the illness's orange. A month without an event
is no longer called empty when it had readings (its header stays and
carries the means) or while a medication was taken in it; only a month
with none of the three gets the "No entries" line.
2026-10-09 16:05:40 +02:00
Marc-André Bombeck 1440ad4301 feat(timeline): one row per medication, no cap on value lines, range popover
A medication arrived as up to four kinds of item (its own span, its
courses, its dose changes, its pauses) and was drawn item by item, so a
medication with a course read as two bars on two rows. Every item of one
medication now carries the medication's id as `group`, and the lane draws
one row per group: the courses are the stretches it was taken, each dose
change cuts them into pieces labelled with their dose, and a pause is a
gap in the same bar. The selection bar and the phone chronicle list one
entry per medication with the dose in the text.

The value series cap of six is gone. The response carries
`availableSeries`, every series the record can show, and the picker is
one compact "Values (n)" button whose menu lists them. Past the fifth
line the five data colours repeat; every line is named at its left.

Choosing a range opens a popover anchored to the "Range" segment (a sheet
on a phone) with "Apply", so the chart no longer moves; the URL keeps the
range as before. The selection bar sits on the card under a hairline
instead of a grey box, the legend explains the dashed stroke and the
hollow point, and no timeline text joins its parts with a middle dot or
calls an open span "ongoing".
2026-10-09 15:02:14 +02:00
Marc-André Bombeck bde6880c3e fix(timeline): read the sleep line as time asleep per night
Sleep is stored one row per stage, and the timeline read it like any
other type: the live path took the mean of a day's rows and the long
path the rolled-up month of them, so IN_BED, AWAKE and every CORE
fragment counted as a night, and a quarter of seven-hour nights read as
under two hours.

The sleep series now reconstructs nights the way the sleep page and the
day view do (CORE + DEEP + REM of the night's winning writer, keyed by
its wake day) and averages the bucket's nights, on every window length;
the rollups are not read for it. The stage rows are read a year at a
time with a day's margin on either side, so no read holds a decade of
rows and no night is cut where one read ends. A long window is cached
per record like the other long series.
2026-10-09 14:11:46 +02:00
Marc-André Bombeck b47a39319c fix(timeline): read rolled-up months by their UTC date
West of UTC the local midnight of the boundary month falls hours after the
month's UTC bucket start, so the first rolled-up month went missing and the
first quarter was averaged over two of its months. The rollup window is now
compared by UTC date at both ends.
2026-10-09 12:02:25 +02:00
Marc-André Bombeck 61155d1856 feat(timeline): add a chosen range as a fourth zoom
Beside All, 12 months and 3 months the timeline takes a range of the
person's own choosing, from and to, through the app's date field. Each
change is clamped softly: never past today, never before the record's
first entry, never inverted, never longer than fifteen years. On a phone,
where the chronicle has no zoom control, a range button opens the same
fields with a way back to the whole record. A range with nothing in it
keeps its fields instead of the empty-record notice.

The zoom and the range live in the URL (zoom, from, to), so Back and a
bookmark return to them; a parameter that does not read falls back to
All. The API accepts zoom=range with both ends, at most fifteen years,
and picks the bucket by its length: quarters from two years, months from
120 days, weeks from 42 days and single days below, a new day bucket
whose dashed bridge spans at most three missing days. A range borrows
the grid and the keyboard steps of the fixed zoom nearest its length.
The selection bar heads a day bucket with that day.
2026-10-09 10:02:37 +02:00
Marc-André Bombeck af982e3812 feat(timeline): average value series per quarter, month or week with reading counts
The server picks the bucket per zoom and sends it as `bucket`: quarters
for an `all` window over two years, months for `year` and a shorter
`all`, weeks for `quarter`. Each point carries the readings behind its
mean as `count`; a bucket without a reading stays absent. Means remain
the mean of day values, so pulse-like types keep the hourly-mean day.
`values` accepts up to six series.
2026-10-09 09:03:42 +02:00
Marc-André Bombeck f950dffad4 fix(sharing): keep life events to their owner
Life events rode the profile sharing domain, so a profile share, a
legacy whole-record share and a MANAGE grant all read them, and MANAGE
could write them. No share level was ever worded for them, so in this
release they are the owner's only.

The life-event routes take a bare requireAuth, which refuses under an
acting-account switch. The day section and the timeline's life lane map
to no domain, so a delegate sees the section named not_shared and never
gets the lane, its readiness row or the day index marks. The profile
consent copy no longer names life events, the sharing guard and the
mutation inventory drop the four entries, and the OpenAPI descriptions
say owner-only.
2026-10-08 22:37:57 +02:00
Marc-André Bombeck 43f2c639ae Merge remote-tracking branch 'origin/release/v1.42.0' into v142/t1-server
# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/pl.json
2026-10-08 14:42:48 +02:00
Marc-André Bombeck 0924285cd6 feat: serve the day, its index, the timeline and life events
Replace the day, timeline and life-event stubs with their implementations
(#613) and drop the not-implemented helper, its error code and its OpenAPI
response with the last stub.

loadDay reads one local day for every reader: the readings live inside the
record's own day window (never the UTC rollups), through the canonical-source
engine the mood sheet already uses (resolveDayReadings in linked-context.ts),
with the person's usual range over the 30 days before; the record sections
from their own tables, each in the storage form it speaks (an instant in the
local window, a date string with its own zone, a calendar date). A switched-off
module's section is not read; a section the grant does not cover is named
not_shared and not read.

The day, the day index and the new visit preparation (GET /api/day/notable)
are admitted on the readings and narrowed per section through
actingDomainVisibility, so a scoped grant sees exactly its sections. The
timeline and its readiness stay whole-record reads and narrow their lanes the
same way.

Notable days are deterministic keys: a three-month high or low with history
behind it, a first reading, and in the visit preparation a regular reading
that stopped. The timeline lays out spans and points from the owning tables,
standing items without a date, and series by zoom: months from the rollup
tier inside its five-year window and from one cached live query before it.
The readiness inventory rates each lane with one link per gap.

Life events are created, edited and soft-deleted as MANAGE verbs with audit
rows, encrypted at rest, and join the sharing guard and the handler matrix.
get_day reads a day for the Coach and the MCP endpoint without life events,
notes or document names; its step carries the date it read.
2026-10-08 14:31:13 +02:00