Commit Graph
2403 Commits
Author SHA1 Message Date
Marc-André Bombeck b4e3e22d43 fix(ssr): clear the budget timer once a raced read answers
The mood and dashboard prefetches and the sign-in location lookup raced
their work against a bare setTimeout that was never cleared, leaving
one live timer behind per render or sign-in after the work had already
answered. A shared helper races the work and clears the timer.
2026-10-08 22:57:47 +02:00
Marc-André Bombeck ef1976832b refactor(css): move the shell and sheet rules into the components layer
Unlayered, the safe-area, keyboard and sheet-clearance rules outranked
every utility on the same element, so a later bg-* on the shell or a
bottom-* on a sheet would have been ignored without a trace. They now
sit under the utilities. The bottom sheet drops its bottom-0, which the
layered keyboard rule replaces with the same zero until the keyboard
opens; the keyboard rules that must win keep their !important. The
toast lift stays unlayered to outrank the toaster's own rules.
2026-10-08 22:57:47 +02:00
Marc-André Bombeck 938b5151de fix(ui): drop the home-indicator padding of a bottom sheet on the keyboard
A bottom sheet standing on the on-screen keyboard kept its safe-area
inset as bottom padding, leaving about 34 px of empty space between
its last row and the keys on an iPhone.
2026-10-08 22:57:47 +02:00
Marc-André Bombeck 6b41c11f30 fix(pwa): set the status-bar colour with the theme before first paint
The theme-color tags were rewritten to the app's theme only after
hydration, so with a light system and the app in dark mode the status
bar stayed light on a cold start. The existing boot script now sets
them along with the theme class, and once more after parsing in case
the tags stream in after it.
2026-10-08 22:57:47 +02:00
Marc-André Bombeck ca47077944 fix(pwa): lift toasts above the bottom bar wherever the phone shell shows it
The toaster's mobile offset applies below 600 px only, while the bottom
bar follows the shell up to md and on a touch screen held sideways. In
between, the update toast and its reload button sat over the tabs.
2026-10-08 22:57:47 +02:00
Marc-André Bombeck 055c48a3fe fix(handover): show the link holder display names only
The claim preview is anonymous: whoever holds the link has proved
nothing yet. It listed each guardian's grant id and fell back to their
login name. It now carries the display name alone, null when there is
none, and the page says "a guardian" for that.

The notifications to the guardians fell back to the new owner's login
name when the record had no display name. They now use sentences about
the person the guardian looked after instead.
2026-10-08 22:37:57 +02:00
Marc-André Bombeck f71becef29 fix(vaccinations): answer 409 to a concurrent duplicate vaccine name
Two adds of the same custom vaccine name both passed the name lookup,
and the loser failed on the unique index with a 500. A unique violation
on the add or the rename now answers the same 409 the lookup gives.
2026-10-08 22:37:57 +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 509166afc4 fix(environment): keep one account from draining the Open-Meteo budget
The backfill route shared the analytics-read bucket and sent every
explicit range without a key, so one account could queue two-year
backfills until the instance-wide daily budget was gone for everyone.

The route now allows three backfills a rolling hour per account (429
environment.backfill_rate_limited) and one queued backfill per
twenty-minute slot, keyed on the account (409
environment.backfill_pending). Every fetch made for an account is also
charged to that account's own share of the daily budget, one twentieth
of it, so a refused request ends the run as budget-blocked and the
nightly run continues.
2026-10-08 22:37:56 +02:00
Marc-André Bombeck 51374274aa List only the sources an account uses on the source-priority page
The read and save of /api/auth/me/source-priority now carry inUse: the
ranked sources the account has connected or holds at least one
measurement from, one index probe per source. The settings page lists
only those, hides a metric nothing delivers, and opens one metric group
at a time, the first by default, with the winning source named on each
collapsed line. The ladders stay whole: moving a source swaps it with
its nearest listed neighbour, so a hidden source keeps its slot.
2026-10-08 20:40:39 +02:00
Marc-André Bombeck f363405df0 Merge the settings and admin UI round into release/v1.42.0 2026-10-08 20:21:39 +02:00
Marc-André Bombeck f4d2ec8988 Tidy admin settings cards
Offer every shipped locale as the server default language, move the app
log refresh out of the header status slot, let filter selects fill their
grid cells, name the coverage table as a focusable region, hide the
legacy-key badge when only one key exists, keep the encryption cards
still while they load, and stop repeating the page title in card titles.
2026-10-08 20:20:03 +02:00
Marc-André Bombeck 5be7a9c242 Label the privacy page's sign-in link in German like its headings 2026-10-08 19:37:31 +02:00
Marc-André Bombeck 0bcf465e38 Close the off-switch token comment
A broken comment terminator swallowed the light theme's off-track
value, leaving the track unset.
2026-10-08 19:36:02 +02:00
Marc-André Bombeck e227f8e4f3 Give off switches and disabled primary buttons light-mode contrast
In the light theme an off switch's track was the input grey at ~1.4:1
on the card, and a disabled primary button was white on a half-opacity
violet at ~2:1. An off track now has its own token at ~3.5:1, and a
disabled primary button a pale violet fill with a dark violet label at
~5.5:1. The dark theme keeps its look through the same tokens.
2026-10-08 19:17:50 +02:00
Marc-André Bombeck e9a5614733 Colour insight series from the chart palette, not the status tokens
Metric pages coloured their series with success, info, warning,
destructive or a raw palette yellow, picked per page, so a steps line
read as 'good' and a resting-pulse line as an alarm. Series now take
the chart tokens by hue family (chart-2 green, chart-3 pink, chart-4
cyan, chart-5 orange), and the systolic/diastolic pair and the pulse
pair stay paired. Status colours remain where they carry meaning:
target bands, time in range, mood score scales. The mood tag scale
drops its raw palette reds and greens for the status tokens, which are
tuned for the light theme.
2026-10-08 19:08:51 +02:00
Marc-André Bombeck b3fb9afa2c Lead the privacy and about pages with the reader's language
The privacy policy is German first in every body, but its section
headings, title and table of contents led in English and its date read
2026-08-11 on both lines. Headings now lead in German with the English
title beside them, the contents list follows, and each date line writes
the date as its language does. The about page's headings, date and
sign-in link follow the reader's locale. No legal wording changed.
2026-10-08 19:02:00 +02:00
Marc-André Bombeck f944c07cdf Explain a dead share link and a broken invite in the reader's terms
A practice opening an expired or revoked share link met the app's own
404, 'this page does not exist', with a button to a dashboard it has no
account for. The share segment now has its own not-found page: link
expired or invalid, ask the person who shared it for a new one, and no
link anywhere. It still answers 404 for every failure class alike.

A malformed invite link fell through to the plain registration form.
It now says the invitation is invalid and offers sign-in; the shape
check is public, so this reveals nothing about which tokens exist, and
a well-formed token still goes on to registration.
2026-10-08 18:55:45 +02:00
Marc-André Bombeck 005cb32ea8 Give every empty list one empty state and one add action
EmptyState now has a single action shape: centred at its own width on
the 44 px tap floor, never stretched edge to edge on some pages and
compact on others; the per-call-site size switch is gone, and the
dashed card takes the card radius. While a list is empty its empty
state carries the add action, so the header drops its own add button
there (measurements, mood, medications, labs, vaccinations, documents,
illness, check-ups, visits, procedures). The dashboard medication tile
of an account without medications offers 'Add a medication' instead of
'no data in the chosen range'.
2026-10-08 18:53:24 +02:00
Marc-André Bombeck b53782810f Keep the phone shell on a phone held sideways
A phone in landscape is 844 to 932 px wide, past md, so it got the
desktop rail and top bar: a sliver of content and no thumb-reachable
navigation. Two variants now decide the shell: shell-desktop from md up
except on a touch screen at most 520 px tall, shell-mobile otherwise.
The sidebar, top bar, bottom nav, the main column's bottom clearance,
the Coach button, the selection bar and the two full-height Coach pages
all key on them instead of a bare md.
2026-10-08 18:36:45 +02:00
Marc-André Bombeck a0c5ab6591 Hold data-shaped page bodies until their reads settle
Several pages stacked cards whose final height is the data's: the
illness day timeline, the measurement list, the insights arrange list,
the system status cards, the score notices that land with the slower
report. Each resolving on its own pushed the rest down the page, for a
CLS of 0.11 to 0.18 at desktop width. RevealWhenSettled mounts the body
at once but keeps it out of layout until no query is in flight (two
frames running, two seconds at most), with a placeholder in its place.

The chart loading shells were also ~100 px shorter than the card they
became: they now reserve the day-dot row, its caption and the collapsed
data-table disclosure, and the range-tab row at its 44 px height.
2026-10-08 18:34:11 +02:00
Marc-André Bombeck 1a52384a1d Open stat strips on the chart's window instead of all time
The strip painted the all-time summary until the chart reported its
visible window, then swapped to it, so every number jumped a beat after
load. The chart now reports only once its read has settled (a failed
read settles too), and until then the strip holds its cells at their
final height.
2026-10-08 18:20:57 +02:00
Marc-André Bombeck f7b6ea5dad Read Apple Health liveness off the data, not only the batch stamp
The status verdict looked only at the user's last-batch column, which
moves on a live batch alone. Rows that reached the account another way,
such as the export.zip import, left it null, and the card said 'waiting
for first data' beside a four-digit record count. The verdict now takes
the newest of that stamp, the per-type arrival ledger and the newest
stored sample.
2026-10-08 18:18:46 +02:00
Marc-André Bombeck 605d9b1fb4 Stop rewarding a doubled or a skipped dose
The 'took it twice' and 'skipped a dose' badges rewarded over- and
under-dosing. Both definitions, their metrics and their copy in every
locale are gone. An unlock row already persisted for either id stays in
the database and in backups, but no longer renders: the evaluator only
walks the shipped catalogue.
2026-10-08 18:12:13 +02:00
Marc-André Bombeck 3a20f23f0f Give touch screens 44 px targets through the primitives
A pointer-coarse variant sits beside the pointer-fine one. Under it the
default, small and icon buttons, tab triggers, the sidebar rail rows and
the phone logo link reach the 44 px touch floor; a mouse keeps the
compact desktop density. The rail's height steps now apply to a mouse
only. A phone held sideways crosses the md breakpoint at about 390 px of
height, where the fixed footer left the list three rows: there the whole
rail scrolls as one column, without a native scrollbar.
2026-10-08 17:52:44 +02:00
Marc-André Bombeck 492c2a4131 Merge remote-tracking branch 'origin/release/v1.42.0' into v142/ui-audit 2026-10-08 17:44:00 +02:00
Marc-André Bombeck 4673973a79 Title the mood sheet like the other capture sheets; fit the documents search hint
The mood page's sheet now reads 'Stimmung erfassen' like the same sheet
everywhere else, and the documents search placeholder is short enough to
show whole on a phone.
2026-10-08 17:34:21 +02:00
Marc-André Bombeck 105c293544 Size the catalog row skeleton to the row, drop the sparkline focus stop
The catalog's loading rows were 6 px shorter than the loaded ones, so the
groups below moved when a probe landed. The decorative score sparklines
sit in an aria-hidden wrapper, and Recharts' accessibility layer put a
focusable surface inside it.
2026-10-08 17:34:20 +02:00
Marc-André Bombeck 8a7eddaf26 Merge remote-tracking branch 'origin/release/v1.42.0' into v142/pwa 2026-10-08 17:24:38 +02:00
Marc-André Bombeck 2cd68779ec fix(pwa): follow the app's theme in the browser bar and toasts, lift toasts over the bottom bar
The theme-color tags followed the operating system's scheme, while the
app's theme is its own setting and defaults to dark: a phone in light
mode showed a light address bar or status bar over the dark app. The
client now rewrites both tags to the theme the page painted. The
toaster read Sonner's "system" for the same reason and now gets the
resolved theme as well.

Below 600 px Sonner stacks toasts 16 px above the bottom edge, which on
a phone is on top of the bottom bar: the "new version" hint covered the
tabs and the add button until dismissed. A mobile offset lifts toasts
above the bar and the home indicator.

pwa-mobile.spec.ts covers the hint's position and its Reload, a bottom
sheet standing on a stand-in keyboard, 16 px fields in portrait and held
sideways, and the sign-in card held sideways.
2026-10-08 17:24:30 +02:00
Marc-André Bombeck a4ad8ebe92 fix(mobile): fit the shell and sheets to safe areas and the on-screen keyboard
Safe areas. The installed app draws under the status bar and beside the
notch, and each 4rem header band padded the inset inside its own
border-box height: on a phone with a 59 px inset the top bar kept a 5 px
content box and the logo and account menu straddled its bottom border.
The authenticated shell now takes the top and side insets once on its
outer wrapper (`shell-safe-area`, the strip painted in the chrome
colour), so the top bar, the sidebar band and any docked panel start
below the status bar together and the seam between them holds. The two
Coach full-height pages subtract the inset at every width, the full-
height day sheet keeps its handle clear of the Dynamic Island, side
sheets keep clear of the status bar and the notch, the bottom bar and
the Coach button take the side insets of a phone held sideways, a
footer-less responsive sheet keeps its last row above the home
indicator, and the sign-in and onboarding shells pad themselves. The
sign-in shell grows with its content (`min-h-dvh`), so a phone held
sideways can scroll to the top of the card.

Keyboard. iOS Safari and Chrome on Android shrink only the visual
viewport when the keyboard opens, so a bottom sheet kept Save and the
Coach composer behind it. `KeyboardInsetBridge` publishes the covered
height (`--keyboard-inset`, `data-keyboard` on <html>); bottom sheets
stand on the keyboard and give up its height to their scrolling body,
and the bottom bar and the Coach button step aside while it is open.
Pinch-zoom and toolbar-sized changes do not count as a keyboard. The
sheet primitive names its side in `data-sheet-side`, which consumers do
not override the way they replace `data-slot`.

pwa-mobile-shell-guard.test.ts pins the structure no headless browser
can show (no browser in CI reports a safe-area inset).
2026-10-08 17:24:17 +02:00
Marc-André Bombeck 56a3008cf0 fix(mobile): keep form fields at 16 px on every touch screen
iOS and iPadOS Safari zoom the page into any focused field whose text
is below 16 px. The fields switched to 14 px at a width breakpoint
(`md:text-sm`, `sm:text-sm`), which a phone held sideways (844 px) and
every iPad pass, and the native select was 14 px at every width, so the
profile settings zoomed on a portrait phone too.

A `pointer-fine:` variant (a mouse or trackpad is the primary pointer)
now gates the compact size on Input, Textarea, NativeSelect, DateField,
TimeField and the Coach composer; touch screens keep 16 px at any width
and the desktop look is unchanged.
2026-10-08 17:23:57 +02:00
Marc-André Bombeck ac212cf6b7 fix(pwa): give the offline page a way back and reload once on any stale chunk
Without a connection a signed-in page opens the worker's offline page:
signed-in pages carry `private, no-store` from the proxy and are never
cached, so this is also what an offline launch of the installed app
shows. The page now offers a real 64 px retry control, reloads by itself
when the connection returns, follows the device's light or dark scheme
and keeps clear of the notch and home indicator. The worker's comment on
the page cache says plainly that only public pages land in it.

The single reload after a stale-shell chunk failure moves into
src/lib/pwa/chunk-reload.ts and now runs in the root error boundary too,
and it recognises a failed dynamic import as WebKit and Firefox word it.

pwa-offline.spec.ts runs in the service-worker project and proves the
offline strip, the offline page and the automatic recovery against the
real worker.
2026-10-08 17:23:47 +02:00
Marc-André Bombeck fb3a13925d Merge remote-tracking branch 'origin/release/v1.42.0' into v142/ui-audit 2026-10-08 17:20:46 +02:00
Marc-André Bombeck 0f51a828c4 Keep chart ticks readable and insights strings in the reader's language
A word-length unit on the y-axis ticks ('Schritte', 'Atemzüge/min',
'mL/(kg·min)') was wider than the axis gutter and lost its leading
digits; such units stay off the ticks, where the stat strip above
already names them. The sleep stage toggle reads '7T / 14T / 30T' like
every other range control, the 'Mood entries' and '7-day' target sources
are named in the reader's language, and the BMI and medication empty
states ask the coach in that language too.
2026-10-08 17:10:41 +02:00
Marc-André Bombeck 3f00d2effd Merge remote-tracking branch 'origin/release/v1.42.0' into v142/ui-audit 2026-10-08 17:02:21 +02:00
Marc-André Bombeck 4d18c4e101 Stop insights sub-pages from shifting after first paint
Weight, blood pressure and pulse painted no stat strip until their read
landed, so the read strip and chart jumped down a card's height; the
strip now holds its place with the skeleton the HealthKit pages already
used, shared from the strip itself. The tab strip's scroll arrows render
from the start and only hide while the row fits, instead of mounting once
it measured as overflowing and shoving every pill sideways. The sleep
page shows the stage chart's skeleton while loading rather than a
'no stage data yet' card. Load shift on weight, blood pressure and pulse
drops from 0.3 to under 0.01.
2026-10-08 17:02:01 +02:00
Marc-André Bombeck 17b8b35397 Merge remote-tracking branch 'origin/release/v1.42.0' into v142/qol-1
# Conflicts:
#	src/app/insights/mood/page.tsx
#	src/components/charts/mood-heatmap.tsx
#	src/components/documents/document-bulk-bar.tsx
2026-10-08 16:49:03 +02:00
Marc-André Bombeck 41ed8eb6a3 feat(insights): mood calendar and chart arrive together; add via the header plus
On /insights/mood the line chart used to paint first and the calendar,
whose aggregate is the slower read, pushed it down when it landed
(CLS 0.16 at 390 px on a cold load). The page now holds both on
final-height skeletons until the chart's read has settled and the
calendar's has landed, then shows them in the same frame. The calendar's
height is written in container-query units, so its box is the right size
from the first paint, in the skeleton and the painted grid alike. A
server wrapper prefetches both reads in parallel (time-boxed, skipped
under a record switch, off when SSR prefetch is off), so on a warm cache
they are in the first HTML. The route and the prefetch share
`moodInsightsWire`, so the seeded cell is the route's shape.

The "Log a mood entry" and "Take a mental-wellbeing check-in" text links
under the heading are gone. Logging is the header plus, which opens the
mood form in place; the check-in keeps its own place in the navigation.
The plus is one shared component, `SubPageAddButton`, now also behind
the measurement pages' capture link and the workouts page, which drew
its own. An audit of every Insights page found no other text add-button
of this kind; the remaining buttons are empty-state calls to action.
A sweep keeps it that way.

`e2e/v142-qol.spec.ts` covers all three surfaces of this change set.
2026-10-08 16:45:49 +02:00
Marc-André Bombeck 4273af231e feat(documents): choose how the vault is laid out, and confirm every delete
The wrench beside Upload opens Settings, Appearance, Documents, the way
the illness journal offers its display options: preview cards or a
compact list, and months stacked as blocks or flowing across the full
width with the month name inline above the document that opens it. The
choice is stored per account in `users.documents_layout_json` behind
GET/PUT /api/documents/inbound/layout (preserve-when-absent, module
gated, the read delegable like the rest of the vault), carried in the
account-settings backup and cleared by the data wipe.

Deleting from the vault now always asks first. The bulk bar's Delete and
a focused card's Delete key both go through one confirmation on the page,
and the detail sheet's Delete opens its own; before, all three deleted on
the spot with only an undo toast behind them.

The selection bar is two tight lines: the count with an X at its right
edge, then one action row with Delete alone at the right in the
destructive style. Below 1024 px the verbs are icon-only at the 44 px tap
floor, so the row stays one line at 360 px. The bar carries the
selection-bar slot the Coach launcher already steps aside for.
2026-10-08 16:45:06 +02:00
Marc-André Bombeck 3e1f630fa1 Fix copy, colour and frame details across the record pages
- Achievements: '1 Tag', not '1 Tage'; point totals grouped; whole
  descriptions instead of a clamp that hid the target at the end.
- Illness: the episode note label is translated, day notes read in the
  content colour.
- Coach history: conversation titles read in the content colour.
- Anamnesis and mental wellbeing: drop a second page gutter the shell
  already provides, and the nested main landmark.
- Privacy: repair words broken by a stray hyphen and space.
- Cycle: the ring caption reads 'Tag 9 deines Zyklus' as one phrase.
- Practitioners: the add button says 'Hinzufügen' like every other page;
  a shorter search placeholder fits a phone.
- Empty state cards inset their content from their border, so a phone's
  full-width action no longer runs into the frame.
2026-10-08 16:34:54 +02:00
Marc-André Bombeck 554f36aeab Show whole-number metrics as whole numbers
The dashboard tiles printed every value at one decimal: '121,0 mmHg',
'60,0 bpm', '9.240,0' steps. TrendCard takes the metric's precision, and
blood pressure, pulse, HRV, steps and mg/dL glucose pass zero. The
measurement list names count-shaped units the way the insights pages do
('13 Etagen', '12,5 Atemzüge/min') instead of the source's 'count'.
2026-10-08 16:21:12 +02:00
Marc-André Bombeck b80dc14142 Clear the accessibility findings on about, privacy, achievements, cycle and admin
Inline links on the prose pages carry an underline and their body copy
reads in the content colour, so the links no longer rely on colour alone
and the code chips meet contrast. Locked achievements drop the opacity
that pushed their text below AA. The cycle calendar nests its headers and
day cells in ARIA rows, the login overview's page-size select is named,
and the measurement type badges take a lighter wash in the light theme so
their label holds 4.5:1.
2026-10-08 16:21:00 +02:00
Marc-André Bombeck 0b881fe5b7 Name every capture sheet by what it logs, in one order
The measurement and mood sheets were titled with the bare 'Add', the
medication one with the noun alone. Every capture sheet now carries the
verb phrase the dashboard's add menu shows, and the add menu lists its
entries in the capture picker's order. Normalise 'z.B.' to 'z. B.' in the
German catalogue and drop the register page's subtitle, which repeated
its title.
2026-10-08 16:20:41 +02:00
Marc-André Bombeck fee6088e08 feat: open the day with one tap and from every place a day is shown
A tap on a point, a bar, a calendar cell or a date now opens the day at
once, as a click does; the tooltip's second step is gone and the tapped
value heads the day. The dashboard's charts (not its today area), the
adherence chart, the rhythm events, the ECG list and detail and the rest
mode notes open their days too.

The day's header carries the close control top left, the Coach panel's
own toggle and glyph, the date on one centred line and the arrows on the
right. While the day is docked, the Coach button and the documents bar
move clear of it.

Smooth the layer: the tooltip no longer glides into place on touch, the
neighbouring day is read before a step, the sheet animates in 300 ms and
not at all under reduced motion, and the opener stays put while the page
beside the docked day reflows.
2026-10-08 15:56:40 +02:00
Marc-André Bombeck 866e871102 Merge remote-tracking branch 'origin/release/v1.42.0' into v142/tk-consistency 2026-10-08 15:19:11 +02:00
Marc-André Bombeck 7fbde1bc83 feat: open the day from every chart drawn in days
Move the chart's doors to the day (click, the touch tooltip's button, the
dashed line through the open day, the row of day dots, the caption) into
one hook and give it to every chart with a date axis: recovery, sleep
duration and stages, the mood line and calendar, the intake calendar,
water and caffeine, lab markers, custom metrics, questionnaires and basal
temperature. Heatmaps share one adapter; every chart's keyboard way to a
day is its data table or the dated list beside it.
2026-10-08 15:19:09 +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 dbd9618d44 Merge remote-tracking branch 'origin/release/v1.42.0' into v142/t1-server
# Conflicts:
#	docs/api/openapi.yaml
#	src/lib/ai/coach/stream-events.ts
#	src/lib/ai/coach/types.ts
2026-10-08 14:33:12 +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