Commit Graph
1149 Commits
Author SHA1 Message Date
Marc-André Bombeck c79fc6cc7b feat: lay down the day view and timeline contract
Land every file the day view, the timeline and life events share before
any of them is built, so the server, the day view and the timeline can be
built side by side without editing the same file (#613).

Schema and migration 0387: LifeEvent with a short category list, calendar
start and end dates, a day/month/year precision, encrypted title and note,
a soft-delete tombstone and an index for the per-user live list. Every new
column is annotated for the column-reader sweep. Life events travel in both
backup purposes with their two ends in one file, are wiped with the
account, and are registered for key rotation.

src/lib/day/contract.ts carries the constants and wire types for
GET /api/day/{date}, /api/day/index, /api/timeline, /api/timeline/readiness
and the life-event routes, free of Zod so the navigation chrome can import
it; the schemas beside it feed the routes and the published contract. The
Coach and MCP tool name get_day is reserved there, and life events are
listed as the one section no model ever sees.

The opt-in timeline module, its navigation entry, the day-section and
timeline-lane surfaces, the day and timeline query keys (evicted by every
record data bundle), and stub routes that answer 501 behind their
admissions. The four cross-section reads declare the whole record for now;
the life-event list is a profile read, and life events join the profile
sharing domain and its consent copy in all seven locales. The day,
timeline and lifeEvents message namespaces carry their base copy as
release-pending keys.

The aggregate bundle cap moves to 4400: the branch already measured 4365
against 4335, and the new copy adds 10 KB.
2026-10-08 13:03:37 +02:00
Marc-André Bombeck 861ba21086 Merge tickets part 1 into release/v1.42.0
# Conflicts:
#	src/__tests__/i18n-reverse-coverage.test.ts
2026-10-08 11:35:07 +02:00
Marc-André Bombeck 7eb5659ab9 Merge iOS server asks and client work into release/v1.42.0
# Conflicts:
#	docs/api/openapi.yaml
#	src/lib/openapi/routes/auth.ts
2026-10-08 11:27:42 +02:00
Marc-André Bombeck 12e422c25e Lighten the client shell and finish four client surfaces
- Load each settings section on its own: the section route imported all
  twenty-one statically. A preload starts the chunk beside the record
  gate's read, and the loading state paints the settings card shell.
  /settings/[section] goes from 611 to 403 KB gz.
- Take Zod out of the shell every route shares: the account-access parse
  run on each boot is plain TypeScript now, held to the published schema
  by an equivalence test over good and broken blocks. The canonical unit
  table moves to its own module, so the display transform no longer
  pulls Zod in either. Layout entry 199 to 136 KB gz; /insights 369 to
  304, /medications 428 to 365, /labs 400 to 337.
- Open a saved Coach conversation at its last answer: an instant jump
  instead of the smooth scroll that lost the end, then follow late
  layout (charts, tables) for a few seconds until the reader scrolls.
- Show the document archive as a month grid of preview tiles, two
  across on a phone and three or four on a desktop, with the month
  headings kept.
- Add the admin card for the measurement table maintenance: a status
  read on the maintenance route, one calm confirmation, and the run's
  state while it is queued, running, finished or stopped.
- Re-base the bundle budget: /insights at 304, /settings/[section]
  watched at 403, aggregate cap 4335 with the reason written down.
2026-10-08 11:25:11 +02:00
Marc-André Bombeck 920bd19617 Merge managed profile handover into release/v1.42.0
# Conflicts:
#	docs/api/openapi.yaml
#	src/__tests__/i18n-reverse-coverage.test.ts
2026-10-08 11:21:25 +02:00
Marc-André Bombeck 8617f809f0 feat(sharing): hand a managed profile over to the person it describes
A guardian with a fresh second factor mints a one-time handover link
(hlp_ token, HMAC hash stored only, 1/7/14 days, at most one open per
profile, withdrawable) with a proposed access level per guardian: end,
view only or manage, view only by default. The link is shown once with a
copy button and a QR code; nothing is mailed. The panel sits at the end
of each managed-profile row and hints when the person is under 16.

The person opens /claim/<token>, sees a minimal preview (name, expiry,
who keeps which access, no health data) and chooses a username, a
required email and a password under the registration policy. The claim
is one transaction under the managed-profile lock: the token is
consumed, the credentials are set, the marker is cleared, every pending
invitation the record offered is withdrawn, each guardian's access is
set to its proposal, the disclaimer, onboarding and AI consent are
reset, and every step is audited on the record and on each guardian's
own account. A failure anywhere leaves the profile managed and the link
unused. Each guardian is notified on their own channels.

The new owner then sees each guardian's access on the first screen after
signing in and decides finally; until then the proposal holds, and the
decision waits at the top of Shared access. A guardian whose access
moved since the claim is left alone. The same screen points the owner
at their own notification channels, since reminders no longer reach the
guardians.

Both anonymous routes refuse a single-sign-on-only instance, refuse a
live session before spending the per-address rate limit, check the link
before probing names, and answer one 404 for every unusable token. The
token travels in request bodies only; hlp_ joins the redaction and
secret-shape rules and /claim/ the path-secret registry. Usernames
starting with managed- are reserved. The session-surface and acting-
account guards list the new surfaces with their reasons.
2026-10-08 11:18:14 +02:00
Marc-André Bombeck bb563b6506 Answer the native client's open server asks
- Publish timezone, glucoseUnit and a features block on the account
  payload; glucoseUnit reads as the closed pair the contract names, and
  features.trackIntake tells a separately released client that
  medications take the record-only switch.
- Refuse a new dose for a record-only medication with
  medication.intake.notTracked on every dose-writing route; the bulk
  route reports it per entry as intake_not_tracked, and correcting an
  already resolved event stays open.
- Answer passkey sign-in failures by name instead of a 500:
  passkey.challenge.expired, passkey.unknown, passkey.response.invalid
  and passkey.verification.failed, each with an audit row.
- Carry one account-wide adherence figure in meta.aggregate on the
  batched compliance read, weighted by expected doses, and accept
  ?days=30|60|90|180|365 for a report window.
- Accept ?days=30|90|180|365 on the mood insights read so stability
  covers the period shown, and name the window it covers.
- Keep only the reminder that announces a dose off APNs when the phone
  owns medication reminders; follow-ups for a dose still open are
  pushed again, marked by a followUp flag the reminder cron sets.
- Document which batch validation issues belong to an entry and pin it
  with a test that splits and resends a batch; publish rejectedFields
  items as ValidationIssue.
2026-10-08 10:32:11 +02:00
Marc-André Bombeck 7bcb6f71d9 Merge environment data into release/v1.42.0
# Conflicts:
#	src/__tests__/i18n-reverse-coverage.test.ts
2026-10-08 10:04:45 +02:00
Marc-André Bombeck 3c98d95a40 feat(environment): air quality, pollen and UV per day, locations sealed at rest
Fetch the Open-Meteo air-quality feed (CAMS, domains=auto) with the weather:
17 hourly variables folded into local days by fixed rules (means and maxima
only with 18 of 24 hours, ozone as the highest 8-hour running mean, UV only
with the midday hours), a value the feed did not cover stored as null, never
as zero. Requests are cut into 90-day chunks, newest first, and chunks before
mid 2022 ask for the eight pollutant and index variables only. An air-quality
failure never costs the weather row; the day stays unfetched and the nightly
gap fill (up to 90 days per account, newest first) asks for it again.

Every Open-Meteo request asks an instance-wide budget kept in Postgres first
(500 a minute, 4000 an hour, 8000 a day, weighted by the hosted pricing
rule). A refused request is not sent and the run ends as done with
budget_blocked. OPENMETEO_AIR_QUALITY_URL and ENVIRONMENT_AIR_QUALITY_DISABLED
are honoured; the account switch lives on PATCH /api/environment/preferences.

The home, each dated location period and each stored day keep their coarse
location sealed as one value under healthlog/environment-location/v1. The
writers write the sealed value only, the readers open it and fall back to the
readable columns for rows the free-text encryption backfill has not reached,
and the backfill now seals all three and clears the readable columns. The
backup carries the location readable in a portable file and sealed in a
disaster-recovery file; a restore seals either under the receiving key.

Three correlation channels join the environment family (PM2.5, the ozone
8-hour high, the day's highest pollen count), read through the averaged
same-day window and left empty while air quality is off. The Coach and MCP
gain get_environment: module-gated, clamped to the lookback limit, no
coordinate or place name anywhere, with provenance and the attribution
lines. The settings section gets the switch, the coverage and the newest
day's values; the dashboard shows a quiet chip on a day with high pollen,
a warm night or very poor air. Copernicus and Open-Meteo are credited next
to the values.
2026-10-08 10:02:39 +02:00
Marc-André Bombeck 996d2519d1 feat(import): read Health Connect exports
Upload the ZIP the Android Health Connect app writes and import its
`health_connect_export.db` in the background, the same way an Apple
Health export is imported: streamed to disk with a SHA-256, capped at
1 GiB, one import per account, a job row of kind `health_connect` that
the shared reconcile cron and staging sweep now look up on its own
queue.

The worker extracts the database (4 GiB ceiling, zip-bomb checks) and
opens it read-only with `immutable=1`, `trusted_schema=OFF` and
`query_only`. A view in place of a record table refuses the file,
columns are found by name, and schema versions below 9 are refused.
Records are read in batches of 500 and aggregated in SQLite: steps,
energy and distance as one total per day from the app the person's
Health Connect priority list ranks first, heart rate per minute for 90
days and as hourly means before that, sleep stages with the unknown
stage counted only in sessions without real stages, workouts, cycle
days, water and micronutrients. Apps whose integration the account has
connected are left out, and a reading already stored under another
source is skipped. Rows carry source HEALTH_CONNECT, `hc:<uuid>` or the
`stats:` ids, so a second import of the same export writes nothing.

Both staged files are removed on every way out; the job result holds
counts per type and per app only. Settings gain a Health Connect card
with the export steps, progress and the outcome, in all seven
languages, and the import is documented in
docs/integrations/health-connect.md.
2026-10-08 09:54:42 +02:00
Marc-André Bombeck 2731c46399 feat(workouts): edit a hand-entered workout from its detail page
A workout logged by hand could only be deleted and logged again. The
detail page now offers an edit control beside delete for a MANUAL workout
carrying the form's `manual:` id, in one's own record. It opens the log
sheet on the stored values and saves by re-posting that id through
`POST /api/workouts/batch`, which already applies such a re-post as an
overwrite of that one row. No new route.

The overwrite nulls every overwritable column a re-post leaves out, and
the form has no fields for heart rate, steps, elevation or pauses. The
edit therefore sends those back as stored. A field left as shown sends the
stored value rather than one re-parsed from its display, so saving an
untouched edit keeps the start's seconds and a distance shown in miles at
its exact metres, and answers `duplicate`.

The detail read's `avgHr` / `maxHr` can come from a same-session twin;
the read now also serves the row's own `storedAvgHr` / `storedMaxHr`,
which is what the edit sends back.
2026-10-08 09:28:56 +02:00
Marc-André Bombeck ba99551faa feat(apple-health): show when each type last arrived and how
The Apple Health card listed each type's newest sample. A type the phone
only hands over on "Sync all" can carry a reading from this morning and
never have arrived in the background, so the sample time could not answer
whether a type flows on its own.

The batch route's per-type ledger now records, in one upsert per batch,
when each Apple Health type last arrived, the trigger that carried it
(including the new `manual`), and when a batch last brought a new or
updated sample of it. Duplicate-only re-sends move the arrival but keep the
last new value; scoped credentials and MANUAL rows write nothing, the same
rule as the sync checkpoint. A ledger failure leaves a warn line and never
fails the ingest.

`GET /api/integrations/healthkit` lays `lastReceivedAt`, `lastTrigger` and
`lastNewSampleAt` over each `metricFreshness` entry (null for a type no live
sync has carried, absent on the workouts entry and when the ledger read
failed), and the card shows them as a second line per type.

The per-type newest-sample read walks live types through
`listLiveMeasurementTypes` and probes each type's newest row instead of
grouping every live row of the source.
2026-10-08 09:22:49 +02:00
Marc-André Bombeck aca497d9ac fix(insights): correlate residuals, not the calendar
Two daily series that both follow the season (temperature and blood
pressure, daylight and sleep) correlated strongly with nothing between
them. In a seeded simulation of such a pair the engine surfaced a
finding in 58 % of 400-day runs and 30 % of 180-day runs, and a real
next-day effect under the season came back with the wrong sign.

Every daily pair now goes through seasonallyAdjustedPearson: a linear
trend and one annual harmonic are removed from both series (trend only
under 120 paired days), the residuals are correlated, and the p-value
uses the Pyper-Peterman effective sample size. Shrinkage and the
phrasing tier read the effective size too. BH-FDR and the effect floor
are unchanged. Pairs too short to fit the basis are not tested.

The weather channels are tested once over the mean of the day before
and the day itself against the same day's outcome (lagDays 0), which
pairs the overnight low with the night it shaped now that sleep is
keyed on the wake day. Discovered pairs carry an additive nEff, the
same-day templates are wired, and the matrix skips the weather read
when the environment module is off.

scripts/compare-correlation-findings.ts counts findings per account
before and after, read-only, for the release-candidate dry run.
2026-10-08 08:32:15 +02:00
Marc-André Bombeck e63be67b5a feat: lay down the shared v1.42 contract
Land every file the v1.42 work shares before any of it is built, so the
parts built side by side never edit the same file.

Schema and migrations 0379-0386: the HEALTH_CONNECT measurement source,
ImportJob.kind, the air-quality columns and the per-user switch on the
environment tables, sealed environment locations with nullable plaintext,
managed-profile handover links, the per-type HealthKit arrival ledger, lab
unit-conversion provenance, and user-defined vaccines. Every new column is
annotated for the column-reader sweep.

HEALTH_CONNECT reaches every exhaustive list: the source labels, the
read enum (not the write allowlist), the source-priority ladder cap, now
the number of sources rather than a fixed eight, and the write-path smoke
test. RMSSD gets its own priority class and a 300 ms ceiling. The batch
syncTrigger gains `manual`, with its label on the Apple Health card.

Queues for the Health Connect import, the compaction-tombstone purge and
the measurement-table maintenance are registered with stub handlers;
host-metric sampling moves to every five minutes. Environment and work_mem
variables reach the compose whitelist, the production example and the
manifest. The proxy admits the handover link and redirects it at the edge,
and the Health Connect upload bypasses the proxy body limit.

The new routes answer 501 behind their final authentication and are
published in the contract; backup, wipe and key rotation classify the new
models and columns; the seven message bundles carry the base copy. Guards
that list release-pending keys and columns fail once an entry is wired, so
those lists empty themselves before the tag.

Also re-anchor a login-throttle fixture to the wall clock, where its
expiry is compared, and re-base the aggregate bundle cap on a measured
12 KB of new locale strings.
2026-10-08 07:56:59 +02:00
Marc-André Bombeck b184a4bbac chore(release): v1.41.2 2026-10-07 22:48:49 +02:00
Marc-André Bombeck 352a36fa47 chore(release): v1.41.1 2026-10-07 16:56:20 +02:00
Marc-André Bombeck dd72634791 Rebuild the Coach thinking row as one calm line
While a turn runs the Coach now shows a single row: a brain icon, the
current step in the answer's text size with a shimmer passing through it
and fading dots, and a chevron once there are steps to open. There is no
running clock any more, and the interim table preview with its sparkline
no longer renders under the row; interim tables stay in state until the
answer's own results show them.

When the answer is done the row reads "Thought process · N steps" with
proper plurals, and "Answer needed" while a clarifying question waits.
It never opens by itself, and a row opened during the run closes when
the turn ends. Opened, the steps form a vertical list with an icon per
kind of work on a thin connector line, and a reasoning round shows its
summary under its title.

The shimmer and the dots are a `text-shimmer` utility and a reusable
WaitingText component, both still under reduced motion.

The demo's write block now carries `meta.errorCode: "demo.readOnly"`,
listed in the error-code catalogue, and the Coach answers it with an
honest line instead of the provider-outage copy.

Removes the interim preview component, the elapsed-seconds and summary
helpers, and their message keys in all seven locales.
2026-10-07 16:55:25 +02:00
Marc-André Bombeck 5bbc915073 chore(release): v1.41.0 2026-10-06 23:09:56 +02:00
Marc-André Bombeck 5361239ae5 fix(coach): keep a turn's fact and plan only with the answer that carries them
remember_fact and propose_plan wrote their rows during the tool round. An
answer that was then blocked, failed or abandoned left the fact in memory
without its note and Undo, and left the plan listed, activatable and fed
into later turns and the briefing. Both are now staged under an id minted
up front and written in the transaction that stores the assistant message,
only when its provenance carries them. A background proposal counts as
offered only once an answer carrying it is stored.

Plan text is model text: ifCue, thenAction and target pass the outbound
dose and risk screen in the person's locale, with their scheduled doses and
medication names, and the injection screen; a trip declines as "unsafe".

The trail route serves recalled facts and the proposal's fact to the owner
whatever the AI capability (only the model-written entries are gated), and
withholds medication facts while the medications module is off.

Accepting a fact proposal is decided under a per-message advisory lock, so
concurrent taps write one fact. Editing a fact re-reads its wording: text
naming a medication moves to "medication", other health wording moves a
non-health fact to "condition", and a health fact the person edited counts
as confirmed.
2026-10-06 21:49:27 +02:00
Marc-André Bombeck 072c1e9394 Compare glucose signals by the hour and date stale strip readings
The signals of the day held today's newest single reading against the
7- and 30-day mean of every reading. For glucose, whose level climbs
with each meal, a fasting morning read as low against an all-day mean
and fed that into the briefing and status prompts. A type in
SAME_HOURS_TYPES now compares today's mean so far with the earlier
days cut at the local time of today's latest reading, the basis the
coincident-deviation flag and the metric strip already use. Below a
week of like-for-like days there is no signal. The signal carries
basis "sameHours" and both briefing prompts say what it means. Other
metrics keep the reading-against-window comparison.

The usual-range strip called the newest reading today's on every
metric page, even when it was days old. CoachReadBaseline now carries
latestDate (the reading's day in the account's zone) and latestIsToday
(isFromToday), on the REST response and the get_metric_baseline MCP
output. The strip names the date when the reading is not from today,
in all seven locales.
2026-10-06 20:53:39 +02:00
Marc-André Bombeck 3e968f03a9 fix(insights): place a glucose day in progress against the same hours
The usual-range strip on a metric page placed the latest single reading
against a band built from whole-day means. For glucose the day mean climbs
with every meal, so at nine in the morning a fasting reading sat under a
band of whole days and the strip said "below your range" on an ordinary
morning; the MCP baseline read reported the same.

Glucose is now placed by its day mean. A day still in progress, in the
reader's own zone, is held against the earlier days cut at the local time
of today's latest reading, the comparison the health status already makes;
with fewer than seven earlier days that have readings in those hours there
is no verdict. The baseline carries `basis: "sameHours"` on the coach-read
route and the MCP read, and the strip names the range as the one for this
time of day, in all seven locales. Other metrics keep their placement.
2026-10-06 20:29:57 +02:00
Marc-André Bombeck 85ebdad5c9 Merge remote-tracking branch 'origin/v141/integration' into v141/d-settings 2026-10-06 19:54:43 +02:00
Marc-André Bombeck 8e7fd0b873 chore(coach): publish the trail route, list it among the mixed reads, and ship its strings in every locale 2026-10-06 19:53:43 +02:00
Marc-André Bombeck 656d8b8563 Merge remote-tracking branch 'origin/release/v1.41.0' into v141/d-settings 2026-10-06 19:44:56 +02:00
Marc-André Bombeck df0689ffbe feat(auth): publish the resolved Coach reasoning on the account payload
GET /api/auth/me gains coachReasoning: the level a turn runs at, the
person's own pick, the highest level the settings may offer, whether
reasoning can run at all, whether the lowest option is a real off, and
the layer that decided. Resolved on the server so the web settings and
the native client read the same answer.

The web reads it through useCoachReasoning. The Coach preferences route
names the stored level on its wide event, and a unit test round-trips
the level through the account-settings backup section.
2026-10-06 19:44:52 +02:00
Marc-André Bombeck acec87f02c feat(coach): give every turn its memory and let the Coach remember and propose plans
What a person told the Coach never reached a tool-mode turn: facts and plans
rode the snapshot's memory block, which tool mode does not send, and the
extraction ran only past twenty turns and failed to parse on every
non-Codex chain, because the status chain runs in JSON-object mode while the
prompt asked for a bare array.

- Memory block: WHAT YOU KNOW ABOUT THIS PERSON, built per turn behind the
  wire egress check (nothing is read on a refusal), fenced as data, at most
  1,500 characters, never trimmed before the inventory. Confirmed facts
  first, then the newest; active plans with a server-computed progress line;
  reminders the person asked for. Medication facts stay out while the
  medications module is off. Building it stamps last_used_at without moving
  updated_at. buildCoachToolRequest and buildCoachProviderPrompts take it as
  an optional memoryBlock; the snapshot memory keeps only the narrative and
  trend recall.
- remember_fact: preferences, goals and context are saved at once
  (source coach, source_message_id set); anything that names a condition,
  limitation or medication, by category or by wording, is only proposed.
  A fact must come from the person's current message; one note per answer.
- The person's answer reads the proposed fact from the encrypted trail on
  the assistant message, never from the request.
- propose_plan writes a proposed plan; the tap activates it with its review
  window counted from the tap. One per answer, three open at most.
- Progress: one sentence per active plan from the person's own day series
  and units, also offered to the briefing (two lines at most, behind the
  briefing capability). The plan review and the progress line read a step
  plan as daily totals instead of per-sample means.
- Background extraction asks for {"facts":[]} / {"plans":[]} and reads
  either shape, runs once a conversation has been quiet for 30 minutes, and
  stores health facts as proposals offered once at the next turn.
- Unanswered plan and fact proposals lapse after 14 days in the reminder
  sweep.
- Memory list API: POST saves one of the caller's own messages (the
  remember button), PATCH edits a fact's wording, GET reports source, origin
  and last use and never lists a waiting proposal.
- A structural guard keeps the egress check ahead of every read and the
  callers of both builders to the turn and the briefing.
2026-10-06 19:32:31 +02:00
Marc-André Bombeck 210becb28b feat(coach): lay down the reasoning, trail and memory contract
Add the shared contract the reasoning release builds on, with no change
to how a turn runs today.

- Schema: operator reasoning controls on app_settings (0376), the
  encrypted trail text on coach_messages (0377), and source,
  last_used_at and source_message_id on coach_facts (0378).
- Provider contract: CompletionParams.reasoning and onReasoning,
  CompletionResult.reasoning and providerState, AiMessage.providerState.
- Reasoning levels, thinking budgets and the background-job table in
  src/lib/ai/reasoning/levels.ts; coachPrefsJson.reasoning, absent
  meaning medium, salvaged field by field like every other pref.
- Stream: activity, memoryNote and planProposal frames, done.stop and
  done.withheldResults, result.interim, a compare chart kind, eight
  table refs, three more clarification kinds with an assumed choice,
  the change_assumption chip, and assumptions, activity, stop,
  memoryNote and planProposal on the provenance. Zod mirror, type
  parity and the native-client key rule cover every new frame.
- Chat request: memoryDecision and planDecision, parsed and not yet
  acted on.
- Stub contracts for the memory functions and the activity recorder,
  inert until their implementations land.
- Backup, restore and rotation carry the new columns; a fact's message
  reference is resolved against the restored messages like its
  conversation reference.
- Message keys for the trail, memory, plans, assumptions, replies,
  interim tables, comparison charts and reasoning settings in all seven
  locales, each reachable from a catalog and checked both ways.
2026-10-06 18:39:50 +02:00
Marc-André Bombeck f6b0131731 fix(daily): serve the briefing as it stands today on every surface
Only the digest applied briefingForToday. The dashboard snapshot (the
web dashboard, its server prefetch and the iOS cold-launch read) and
the /insights advisor payload still served an earlier day's text and
the raw model deltas ("+33.72 bpm").

The snapshot read now resolves the briefing after the cache, beside
the capability decision, from the payload's generation moment and the
snapshot's own newest reading per type; a withheld briefing reads
"preparing", never "ready" with nothing in it. The digest takes the
resolved briefing from that read on its own clock instead of resolving
again. The advisor route resolves dailyBriefing on every branch it
returns, reading the newest reading per signal type in one grouped
query only when a reading-backed signal is present; text generated on
an earlier day no longer counts as fresh on the read GET (so the warm
is enqueued) and no longer short-circuits a provider-backed POST.

The OpenAPI descriptions of both advisor responses say so.
2026-10-05 22:03:58 +02:00
Marc-André Bombeck 28517affd0 chore(release): v1.40.6 2026-10-05 21:38:46 +02:00
Marc-André Bombeck b01c193274 chore(release): v1.40.5 2026-10-05 18:46:44 +02:00
Marc-André Bombeck 5aa3f3346d chore(release): v1.40.4 2026-10-05 17:46:26 +02:00
Marc-André Bombeck 2a3bd67b68 fix(insights): cut glucose days at today's latest reading and say so
The same-hours comparison cut the earlier days at the current clock time.
At two in the afternoon a day holding only its fasting reading was then
held against mornings plus lunch, and read as below its range again. The
earlier days are now cut at the local time of today's latest reading.

A range read this way is the usual range for this time of day, not the
whole-day band other surfaces show. The deviation carries a basis, the
health-status API publishes it as `basis` (`day` or `sameHours`), and the
Today lead and the health-status card name the range as the one for this
time of day, in all seven locales.
2026-10-05 13:10:42 +02:00
Marc-André Bombeck 25c34b0e7c chore(release): v1.40.3 2026-10-05 12:53:40 +02:00
Marc-André Bombeck 524975d5f3 fix(daily): decide the line under the Today lead on the server
The muted line under the lead repeated what the lead had just said: a lead
about blood pressure sat above "Your latest blood pressure is sitting in
the optimal band · ↓ ~10 mmHg…". The web client only dropped the headline
when the lead quoted it word for word, and other clients had no rule at
all. The digest now carries `signalLine`, built next to the lead: when the
lead already talks about the top signal's metric (matched in every shipped
language) or says its headline, only the delta remains, and with no delta
there is no line. The hero renders the field verbatim.
2026-10-05 12:19:41 +02:00
Marc-André Bombeck fd895bfd5f chore(release): v1.40.2 2026-10-05 08:44:10 +02:00
Marc-André Bombeck bfcf71b7eb fix(dashboard): keep numbers out of the morning push line
The digest line is the body of the morning push and lands on a locked
phone. It took the briefing's first sentence, the top signal's headline
or the deterministic signal sentence verbatim, so a resting heart rate or
a blood pressure average could sit on the lock screen, and its score
floor stated the score.

A candidate with a figure in it is now passed over for the next, and the
floor says the score is ready instead of what it is. The hero's lead in
the app keeps its numbers. Tests that pinned the numeric line are turned
around, and the push builder is checked on a digest whose lead carries a
reading.
2026-10-05 08:34:28 +02:00
Marc-André Bombeck 9e647051d5 fix(dashboard): tell a steady score only when its band held, and say at least at the read's edge
The steady line compared the newest stored score with the number on
screen but not with its band, version or composition, so a score whose
band moved overnight at the same number was still called steady. A run
that reached back past the 120 days read was also told as if it started
there.

The newest stored day now has to match the ring in value, band and,
where the digest carries them, algorithm version and composition. A run
the read cannot see the start of is marked as open (steadyAtLeast on the
digest score) and the hero says "at least" in seven locales.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 359203d09a Merge remote-tracking branch 'origin/feat/coach-lookback-limit' into release/v1.40.2 2026-10-05 07:31:34 +02:00
Marc-André Bombeck 929c0ebaa4 Merge remote-tracking branch 'origin/main' into feat/coach-lookback-limit
# Conflicts:
#	src/lib/ai/coach/snapshot.ts
#	src/lib/ai/coach/tools/executor.ts
#	src/lib/insights/features.ts
2026-10-05 07:31:03 +02:00
Marc-André Bombeck 03c43ef70a Merge remote-tracking branch 'origin/feat/documents-procedure-filter' into release/v1.40.2 2026-10-05 07:17:17 +02:00
Marc-André Bombeck 98ac9c3d80 Merge remote-tracking branch 'origin/feat/dashboard-today-overview' into release/v1.40.2 2026-10-05 07:17:17 +02:00
Marc-André Bombeck 0219a85e92 feat(documents): filter the archive by procedure or surgery
The vault's filter row gains a procedure facet beside the condition
facet. It lists the record's procedures and surgeries that hold at least
one live document, headed the way the visits page heads them (the
reason, else the practice, else the kind) with the body site and date
beneath, and narrows the list through the existing `encounterId` filter.
Same control, placement and clear behaviour as the condition facet; the
choice rides the URL as `?encounter=`, and an upload dropped into the
filtered view is filed against the procedure.

The usage read publishes the choices as `linkedProcedures` (additive),
decrypting only the label columns, and returns none when a sharing
grant does not cover the visits section.

On a phone an active facet now gives up label width before the search
field does, and the search keeps room for its icon and padding.
2026-10-04 21:59:22 +02:00
Marc-André Bombeck 5e5e387fab chore(release): v1.40.1 2026-10-04 20:55:14 +02:00
Marc-André Bombeck 393ac955f6 Make the Coach lookback a limit on every read
The saved Coach window only seeded the snapshot and each tool's default.
A tool could still read back to all time, the availability probe reported
counts and means for history outside the window, and several blocks read
fixed windows of their own (labs 12 months, correlations 180 days, the
coarse tail, the all-time extremes) whatever the setting said.

coachPrefs.defaultWindow is now enforced as a limit through one module,
history-reach.ts: the turn context clamps the client window, the snapshot
clamps its window and gives every fixed-window block either the shorter
window or, when the block's own window cannot fit, leaves it out. Every
retrieval tool clamps its window, an earlier period or a stored table that
reaches past the limit answers outside_reach, and the probe reads only
inside the limit and names older history as outside_reach without any
figure. Follow-up chips that would read past the limit are not offered.
The default (allTime) is no limit and reads as before. MCP clients pass no
limit and keep their own window arguments.

Adds the twelve-month option, rewrites the picker text to state what is
read with the numbers the readers use, shows the same options in the
managed-profile settings, counts cluster members with a singular, and
drops the opacity on switched-off rows that put their text below WCAG AA.
2026-10-04 20:45:21 +02:00
Marc-André Bombeck 5cfacba16a feat(dashboard): add a Today overview to the hero
The hero led with the first sentence of the cached briefing, which was
often a greeting ("Good morning.") while the sentence with content was
never shown. Without an AI provider it fell back to "Your health score
today is N.", which the hero then removed because the ring already
shows the number, leaving a ring and "Nothing needs your attention".

The daily digest now publishes the lead resolved on the server and a
short list of facts about the day:

- lead: the reaction line or the first briefing sentence that is not a
  greeting and does not repeat the score while AI text is available,
  otherwise a deterministic sentence about the strongest signal (a vital
  outside its personal range, a night an hour off the usual, vitals in
  range, last night). A fact the lead was built from leaves the list.
- today: up to five statements in priority order (Rest Mode, doses taken
  today, an appointment today or tomorrow that is not already on the
  rail, last night's sleep against the usual, vitals against their
  personal range, cycle day). Module-gated on the server, never padded,
  never asking for anything. A phone shows the first four.
- restMode: passed through while an illness episode is active.
- score.steadyWeeks: how long the score has held, read off the stored
  daily scores, shown under the ring in place of a delta.

briefingLead skips greetings and score sentences the same way, so the
morning push improves with it. Visit times are rendered in the profile
timezone and hour-cycle preference; the cycle day comes from the same
verdict the cycle page uses. The ring, the rail and the charts are
unchanged. OpenAPI regenerated; additive fields only.
2026-10-04 20:00:18 +02:00
Marc-André Bombeck dee00613d8 Merge remote-tracking branch 'origin/main' into release/v1.40.0 2026-10-04 12:23:40 +02:00
Marc-André Bombeck 32de7ecf37 Merge pull request #1135 from MBombeck/fix/cycle-backup-and-med-put
fix(backup): carry encrypted free text readable in portable backups
2026-10-04 12:23:35 +02:00
Marc-André Bombeck 4e88243075 Merge remote-tracking branch 'origin/main' into release/v1.40.0 2026-10-04 11:29:47 +02:00
Marc-André Bombeck 24786f6549 Merge pull request #1130 from MBombeck/feat/provider-reasoning-effort
feat(ai): per-provider reasoning effort for Local and gateway models
2026-10-04 11:29:44 +02:00
Marc-André Bombeck ad0f35eadd Merge remote-tracking branch 'origin/main' into release/v1.40.0
# Conflicts:
#	CHANGELOG.md
2026-10-04 11:13:49 +02:00