Commit Graph
3505 Commits
Author SHA1 Message Date
Marc-André Bombeck cbc3b3118d fix(measurements): recompute a stored fold mean only where all its samples remain
The fold repair and the regular folds rewrote an existing daily or hourly
stats: mean from the samples still in the table. Fold tombstones are purged
75 days after they were written, so for an older window those samples can
be a fragment: a re-upload after an anchor reset, the rows of one later
fold run. A mean over them replaced a stored mean that had been taken from
the whole day, and on old days it moved a lot.

A stored mean is now recomputed only for a day that starts at or after the
tombstone horizon (foldConstituentHorizon): every fold tombstone of such a
day was written after dayStart + the fold threshold and is still inside the
retention, so the live samples plus the fold leftovers are the whole day.
That is the last 75 days for daily means and, since dense days fold 90 days
late, the 75 days before the raw window for hourly means. A window also
needs at least one fold leftover; without one the rows in hand are not
known to be all of it.

Beyond the horizon:
- The repair leaves the day as it is and counts it per type as
  windowsSkippedBeyondHorizon.
- The daily-mean consolidation and the dense hourly fold leave a day or
  hour that already has a live stats: row exactly as it is, the stored
  mean and its live samples alike (daysLeftAsStored, hoursLeftAsStored).
  Up to v1.41 the fold overwrote such a row with the mean of the live rows
  alone, and the first v1.42 cut with the mean of those plus whatever
  tombstones were left; both are a mean of part of a day. Dropping the
  live rows instead would lose samples that may not be in the stored
  mean, so they stay as synced. They are few, the folded_window guard
  refuses new ones, and the nightly pass walks over them without writing.
  A day or hour without a stored mean still folds as before.

Inside the horizon, a pass that recomputes a stored mean soft-deletes the
live samples it took in as fold leftovers instead of deleting them, so a
second pass computes the same mean from the same samples. Before, a fold
that deleted them left the repair a fragment, and the repair then rewrote
the mean from the tombstones alone.

The repair, the mean consolidation and the dense fold now take a per-account
advisory lock after the restore lock in every day transaction, and the
repair reads the day's samples inside it, so the two never compute the
same window from two views of its samples or write the same row at once.
The repair also stops at the fold boundary: a day the fold has not
finished is the fold's to complete.
2026-10-09 01:03:02 +02:00
Marc-André Bombeck 312e5de2b2 fix(security): keep the theme boot script a literal and drop regex tag stripping
The boot script interpolated its two colours; it is one string literal now,
checked against THEME_COLOR by the script's own tests. The retired-route
guard escapes every regex metacharacter including the backslash, and the
two markup tests read text by walking the string instead of stripping tags
with a regex.
2026-10-09 00:41:56 +02:00
Marc-André Bombeck 4641943481 Merge the interface review fixes into release/v1.42.0 2026-10-08 23:04:04 +02:00
Marc-André Bombeck 6208a5357a docs(api): describe the folded_window duplicate and the fold-free sync feed 2026-10-08 22:58:42 +02:00
Marc-André Bombeck 055cbcff0e fix(insights): count device strain by day, not by cycle
The band writes one strain row per physiological cycle, stamped at its
start, and a bedtime either side of midnight puts two starts on one
calendar day. The strain page counted those rows as days and drew an
extra sparkline point the recovery chart does not have. Collapse the
rows to one per local day at the day's mean, the chart's own value.
2026-10-08 22:57:47 +02:00
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 dd76b434c4 fix(documents): cancel a layout read in flight before flipping the switch
A first read of the vault layout that was still running when the switch
was flipped landed afterwards and painted the old layout until the save
answered, so the switch jumped back for a moment.
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 dcbd564962 fix(sources): stop listing a source whose readings were all deleted
The in-use probe matched tombstoned readings, so a source stayed marked
on the priority ladder after every reading from it was deleted. Both
the read and the save response of the source-priority route go through
this one probe.
2026-10-08 22:57:47 +02:00
Marc-André Bombeck a5546507fa fix(insights): keep a pill save when the overview arrangement saves after it
The arrange card sent the tile list it copied on mount, with a token
read fresh from the cache, so a pill-order save made beside it was
overwritten without a conflict. Send the current server tiles, and let
an untouched section draft follow the server copy so the pill
manager's ECG toggle no longer reads as a pending edit.
2026-10-08 22:57:47 +02:00
Marc-André Bombeck 32cb75c1aa fix(db): let the maintenance job run behind a pooler that refuses startup options 2026-10-08 22:57:30 +02:00
Marc-André Bombeck 8ac64c3e4f chore(release): empty the release-pending lists for v1.42
The lab unit conversion moves to the next release, so its note string
leaves all seven bundles and the i18n pending list. The three columns
that hold the conversion provenance stay (migration 0385, nothing writes
them yet); the backup now carries them both ways, so the round-trip
pending list is empty too and the conversion needs no backup change when
it lands.
2026-10-08 22:37:57 +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 7c6510eba9 fix(db): let a PgBouncer host turn the session startup options off
The statement timeout and, since v1.42, work_mem reach Postgres as the
libpq options startup parameter, which PgBouncer refuses unless it is in
ignore_startup_parameters. A host behind it could drop the string by
setting the timeout to 0; work_mem is now always sent, so that no longer
worked. DATABASE_SESSION_OPTIONS_DISABLED sends no session settings,
documented in the example env, the env manifest, the compose whitelist
and the scaling guide.
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 98183700ce fix(handover): keep a claimed handover when its Guardian deletes their account
created_by_id cascaded, so a Guardian who deleted their own account took
every handover they had issued with them, including a claimed one whose
decision the new owner had not made yet. The claim had already applied
the proposal, possibly MANAGE for a co-Guardian, and the owner lost the
one place that asks them to confirm it.

The column is nullable now and cleared on delete (migration 0390). An
unclaimed link without a creator is refused like any link whose creator
lost access. A decision that names a Guardian whose account is gone is
refused with 422 instead of failing on a grant for a missing account.
2026-10-08 22:37:56 +02:00
Marc-André Bombeck c7cee9c8a7 fix(coach): hold get_day to the Coach exclusions and keep free text out
get_day read through the day loader rather than the snapshot, so it
ignored the person's Coach exclusions: a person who kept mood or
medications from the Coach had both sent to the model as soon as it
asked for a day. Each exclusion token now maps to the day sections and
measurement types it covers; the sections are not loaded and the types
are dropped from the projection. A guard runs every token through the
executor.

A dated document reached the model under its title or file name, and a
visit under its reason or the practitioner's name. Both now carry their
kind. The remaining titles are fenced as the person's own text in the
Coach, as the MCP endpoint already did.
2026-10-08 22:37:56 +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 78cde56542 fix(environment): fail closed on a travel period that does not open
A location period whose sealed location this host cannot open was
dropped from the resolver, so its days fell through to the home and the
next refresh overwrote the stored travel days with the weather at home.
Those days are now skipped and the run logs an error signal.

A restore now opens every sealed location once. One that does not open
is kept sealed, so the refresh keeps skipping its days, and is named in
the restore report under a new catalogue.
2026-10-08 22:37:56 +02:00
Marc-André Bombeck 342d73fb36 fix(google-health): read a full day of intraday samples once per local day
A watch worn without its phone reaches Google's cloud hours after the
samples were taken. The two-hour intraday overlap is measured from the
end of the previous clean cycle, so anything arriving later than that
was never fetched. The first cycle of each local day now reads intraday
samples with the full day of overlap; later cycles keep two hours.
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 8fc467f8c5 Merge the second UI round into release/v1.42.0 2026-10-08 20:05:44 +02:00
Marc-André Bombeck aa3049a14c Emit the strain device block on every wellness read
The published WellnessScoreValue lists every field the engine sends, and
the contract test holds the two sets equal. The computed path now sends
device: null like anchor and components, and the device path has its
own contract case.
2026-10-08 18:45:01 +02:00
Marc-André Bombeck 651ec3aea8 Merge complete-window folding and the fold repair into release/v1.42.0 2026-10-08 18:42:46 +02:00
Marc-André Bombeck adc7baca14 Fold only complete local days and repair means folded from part of one
The daily-mean consolidation and the dense hourly fold cut at
`now - threshold` instead of a local day boundary. A day was folded in two
runs and the second run overwrote the stored mean with the mean of the
later part alone: every mean-type day, and the boundary hour of every
heart-rate, HRV and SpO2 day, plus the derived resting figure and the DAY
rollup of such a day. The Health Connect importer split its raw window at
the same unaligned instant.

- Fold up to the start of the account's local day the threshold ends in
  (`foldBoundary`), for every consolidation pass and the Health Connect
  importer's raw/hourly split.
- A window that already has a live `stats:` row is refolded from every
  sample of it, live rows plus the fold's own tombstones; with none left
  the stored mean stays.
- `folded_window` refuses a raw sample only when its local day lies before
  the aligned boundary and a live `stats:` row of its source covers it,
  also within reach of the sample after a timezone change.
- A deletion inside a day the fold had not finished is class B.
- Add a one-time, resumable fold repair per account that recomputes each
  hourly and daily mean from its remaining samples and writes only values
  that differ, recorded in `measurement_fold_repairs` (migration 0389).
- The compaction-tombstone purge waits for the repair per account, and the
  boot sends no purge run before one account is repaired.
- The restore keys live `stats:` ids by source, and writes compaction
  tombstones back from a file older than the account's repair, resetting
  the repair.
2026-10-08 18:42:01 +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 ac482c508d Serve the device's day strain on the strain page when no proxy exists
The recovery page charts the band's own DAY_STRAIN while the strain
score page read only the computed proxy, so one account showed weeks of
strain on one page and 'not enough data' on the other. Without a proxy
row in the window the derived strain read now falls back to DAY_STRAIN:
the score is the reading as a share of the device scale, an additive
'device' block carries the native value, and the course below the card
charts that same series.
2026-10-08 18:17:20 +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 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 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 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 503802e363 fix(pwa): update the worker on Reload instead of unregistering it
The "new version" Reload action unregistered every service worker before
reloading. Unregistering a registration ends its Web Push subscription
and nothing in the app re-creates one, so each update silently turned
push reminders off in the installed app: the server kept the dead
endpoint and the settings card showed push as off.

Reload now asks the registration to update (the new worker activates at
once and drops the previous release's caches), deletes only HealthLog's
own caches, and reloads. The cache-name contract moves into
sw-cache-policy.ts so the session-end cleanup and the poller share it.
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 ec272c9150 Show the callback path, not 'undefined/api/…', on an unset app URL
With NEXT_PUBLIC_APP_URL unset, some providers spliced the missing value
into their redirect template and the integration cards told the reader to
register 'undefined/api/polar/callback'. A callback that is not an
absolute http(s) URL now counts as not configured, so the card shows the
fixed path it shows for every other unconfigured provider.
2026-10-08 17:13:49 +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 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 1a5eab9f7c feat(insights): show every wellness score's course, drop the caveat line
Each score tile in "Your health scores" draws the score over its window
under the band word (the persisted window for recovery, stress and
strain; thirty days for the sleep score and readiness), on a fixed
0-100 scale so a few points do not read as a cliff. A score's detail page
carries its history directly under the score card: the stored nightly
scores get the full chart with the period switch, tooltip and day
access every measured page has; the two composites draw the course
their value carries.

The data was already there for three of the five. The sleep score now
returns the score of every scorable night in its window, judged by the
same blend and the same window baseline as the headline, so the last
point is the headline. Readiness reads its course from what the nightly
job persisted, the COMPUTED recovery row that is this exact blend,
without recomputing a single day. Both fields are additive.

The warning-coloured "descriptive proxy, not clinical" line above the
method is gone from all four score pages that carried it, with its keys
in all seven locales and the unused anatomy disclaimer key.
2026-10-08 16:45:24 +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 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 c41ec02e92 feat: prepare the next visit from the server's notable window
The block under a planned visit read the day index and up to twelve days
to find what changed, so a dose change on a quiet day could fall out of
the picked days. It now reads GET /api/day/notable for the stretch from
the last visit to today: every change the server names, every notable
reading.

Add a guard that walks every chart, chart mount and calendar heatmap and
fails when one drawn in days lacks the day doors and is not on the
reasoned NOT_HERE list. Give the dose-strength curve, the efficacy chart,
the mood dimensions, the intraday pulse header and the environment note
the same doors.
2026-10-08 15:30:12 +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 a81dfaab3d perf(day): leave pulse to the resting heart rate in the extremes
A pulse day value is a mean over thousands of background samples, and
comparing it over two years scanned every one of them: a recent day on a
dense account took about 300 ms, two thirds of it here. Resting heart rate
stays among the compared vitals.
2026-10-08 14:40:37 +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