Commit Graph
7029 Commits
Author SHA1 Message Date
Marc-André Bombeck 1dd74bfa20 test(coach): follow the related-metric chip copy 2026-09-27 13:07:18 +02:00
Marc-André Bombeck 722945702a fix(privacy): clear the last readable copies of practitioner contact details
The encryption backfill now also removes the two copies of a practitioner's
contact details that outlived the sealing. An appointment reminder's copy of
the address is cleared, since the reminder resolves it from the practitioner
when it fires. An address-book edit's audit row written before the sealing
kept the old phone number and address in its previous values; those values
leave the row and the changed fields stay named. Rows of a deleted account,
which no per-account job reaches, are scrubbed by the discovery pass itself.

A route whose readable geometry is a JSON null rather than SQL NULL had
nothing to seal but matched discovery every night. The pass now sets it to
SQL NULL so it drops out.
2026-09-27 13:07:17 +02:00
Marc-André Bombeck 3482581ce1 fix(encounters): name the practice address on an appointment nudge again
Sealing the practitioner's address left the appointment arm reading the old
readable column, which is now empty, so a booked visit's push lost the address
on booking, rescheduling and restoring a deleted visit.

The reminder no longer keeps its own copy of the address. The reminder tick
resolves it from the owning visit's practitioner when it builds the push and
decrypts it there, so an edit of the address also reaches a nudge that is
already booked. MeasurementReminder.location stays NULL on appointment rows:
booking writes none, a re-anchor clears one an earlier release wrote, and a
backup neither carries nor restores one. A checkup's own free-text location is
unchanged.
2026-09-27 13:07:10 +02:00
Marc-André Bombeck 9892088620 fix(coach): account for every stored table and keep table controls on screen
A withheld table now leaves a line saying why (module off, or the stored
tables could not be read) and a failed read shows an error row with a
retry, so the Data used count matches what is listed. A table copied from
an earlier answer names that answer's date under its title. The preview
shows the latest twelve periods of a time series, and the truncation note
says the latest rows are kept, which is what the server keeps. The title,
copy and view controls sit above the scrolling table with the title kept
as a hidden caption, and a reply with tables takes the full column width.

The step headers and the related-metric chip read as label: value in all
seven locales, so a metric name no longer sits capitalised mid-sentence.
2026-09-27 13:07:07 +02:00
Marc-André Bombeck 25ad1810c2 fix(coach): offer the period before only when the record reaches back
The "compare with the period before" chip now needs a first reading
before the window's start, the same history check the other history chips
use, and is never offered on an all-time table, which has no period
before it: the table tool reads all time as the current period whatever
it is asked.
2026-09-27 13:06:55 +02:00
Marc-André Bombeck 203068d5ba fix(coach): keep a billed reply when its tables fail, and fit tables once
Tables are now projected outside the provider's failure path: a
projection defect costs the turn its tables and is annotated, instead of
refunding tokens already billed and dropping the reply.

The at-rest ceiling for a message's tables is applied once, when the
dialog is assembled, before anything streams. A table trimmed only at
write time used to show live and read back as withheld after a reload.
2026-09-27 13:06:02 +02:00
Marc-André Bombeck 49df950043 test(coach): pin all-time step months across history lengths and read paths 2026-09-27 13:04:30 +02:00
Marc-André Bombeck de2200bd5c fix(coach): make table weeks and months mean one thing per metric
A total's weeks and months are now the sum of its days, labelled as a
total, and a level's stay a mean. All time is read as UTC calendar months
from the rollup tier's MONTH fold, or the same fold of the live table when
the tier has no rows, so a month no longer reads as a mean of daily totals
for a short history and a monthly total for a long one. Months are keyed
by their UTC start instead of being re-read in the user's zone, which
named every month one early west of UTC.

The chart's long ranges had the same shift: tier rows are now stamped at
noon UTC of the bucket's first day, which falls inside the bucket's own
week and month in every zone.

Step length and walking speed keep two decimals, loud-sound events count
as a total in whole events, and HRV counts a night measured by both
estimators once and charts the estimator the person mostly has. The model
summary names what a period's value is and gives a total's range total.
2026-09-27 13:03:31 +02:00
Marc-André Bombeck 7c32dca7ab fix(coach): count distinct sources in the finished steps header 2026-09-27 13:03:28 +02:00
Marc-André Bombeck bc6c782745 fix(coach): quote formula starts behind whitespace, in fullwidth and pipe when copying a table 2026-09-27 13:03:27 +02:00
Marc-André Bombeck c1aa235e27 fix(workouts): read a route that does not open as no track
A damaged or foreign-key route ciphertext threw out of readRouteGeometry, so
one bad track answered the workout detail with a 500 and failed the activity
insight job. It now reads as no track with a warning on the request event, and
still never falls back to the readable column.
2026-09-27 12:56:01 +02:00
Marc-André Bombeck e7f3385374 Merge branch 'coach/wp3-charts' into release/v1.39.4 2026-09-27 12:47:28 +02:00
Marc-André Bombeck b5d33502dc feat(coach): show result tables as charts with a chart/table toggle
A table the answer referenced opens under the prose as its chart; the
others sit under Data used in the evidence disclosure and open as
tables. The plot is one image whose name points at the table view, two
series carry legend buttons, and the chart loads through the shared
chart runtime. A metric chart token for a metric a shown table already
covers is dropped.
2026-09-27 12:47:06 +02:00
Marc-André Bombeck d63d2e6d83 Merge branch 'coach/wp7-continue' into release/v1.39.4
# Conflicts:
#	src/lib/ai/coach/turn/pipeline.ts
#	src/lib/ai/coach/turn/provenance.ts
2026-09-27 12:43:23 +02:00
Marc-André Bombeck 968b8b1efd Merge branch 'coach/wp4-followups' into release/v1.39.4 2026-09-27 12:41:44 +02:00
Marc-André Bombeck b5023ae935 feat(coach): keep looking after the round cap
When the tool loop forces an answer at its round cap, the reply leads
its chips with "Keep looking". Tapping it runs a normal turn, under the
budget gate as ever, with two server-written context lines: the question
that was left unfinished (at most 1000 characters) and what the forced
turn already read, each table by the name show_result takes, so the
continued turn shows those again instead of fetching them twice.

An answer is continued at most once. The continued reply records the
message it continues in metricSource.continuationOf and offers no
further "Keep looking", even when it is forced again; a chip on a reply
that is itself a continuation is refused.
2026-09-27 12:41:20 +02:00
Marc-André Bombeck 66718a1c7d feat(coach): follow-up chips under the latest answer
Up to three chips, derived on the server from what the turn read: the
other view of a table with a chart, the period before, a year earlier,
a wider window, and a metric a discovered pattern links to. A chip is
offered only when its read was done this turn and the record holds the
metric; the year-ago and wider-window chips also need the record's
first reading to reach back that far. The model may name chips in a
FOLLOWUPS block, but only to reorder ones the rules already allow; every
label is rendered from the catalog.

A tapped chip resolves against the chips stored on the conversation's
latest reply. "As a chart" and "as a table" are answered from the
stored table without a model call or a budget reservation: the rows are
copied unchanged and the message is tagged providerType "reuse". Other
chips run a normal turn with a server-written context line. A chip that
is no longer current falls back to a plain message.

A setting turns the chips off (absent means on).
2026-09-27 12:36:01 +02:00
Marc-André Bombeck c4bf5a916d feat(coach): choose a chart for each result table on the server
A table over time is drawn as a line for a level metric and as bars
for a total, with at most two series in one unit. Category counts get
bars, horizontal above six categories, the top eight and one other.
Fewer than two or more than 400 rows, or no numeric column, stay table
only. show_result's view now counts: table drops the chart, chart turns
an earlier day table into counts per range with a histogram (fixed
widths for the common metrics, Freedman-Diaconis otherwise, none below
ten values).
2026-09-27 12:34:06 +02:00
Marc-André Bombeck 02801bdfda Merge branch 'fix/restore-large-account-clearing' into release/v1.39.4 2026-09-27 12:14:04 +02:00
Marc-André Bombeck db10ba714e test(coach): pin the answered-clarification line on the local path
The line that resolves an answered clarifying question was already
appended to the system prompt on both paths; nothing proved it for the
no-tools (local model) path. Three route-level cases now do: the local
prompt and the tool-mode prompt both carry the server-resolved line, and
a turn without an answer carries neither. Removing the line from the
local prompt fails the first case.

The golden harness gains a coachMessage.findMany mock for this; it
defaults to no rows, so no existing transcript changes and the golden
snapshots are untouched.

The result-table caption test used the regex dotAll flag, which the
TypeScript target rejects (TS1501); it matches across lines with
[\s\S] instead.
2026-09-27 12:11:55 +02:00
Marc-André Bombeck b40de3a4d9 fix(coach): count a table shown again once, and say it is from before
The method line merged a repeated read by keeping the larger count. With
show_result that could let an earlier answer's count stand over the read
this turn just made. A fresh read of the same source, window and period
now always wins, whichever order the steps ran in, and the two are never
added. An entry that rests only on an earlier table says so ("from an
earlier answer"), including a reused table no step accounts for.

The new catalog string is in all seven locales.
2026-09-27 12:11:55 +02:00
Marc-André Bombeck ad541cd5bb fix(coach): show a step for metric tables and for a table shown again
get_metric_table now gets a step like every other read: the domain is
its validated metric, and once the table is in, the window, period and
granularity are the ones the table was built with (after the executor's
defaults). The count is the readings the table folds, taken from the
summary the server computed, so a read past the sixth table still counts.
A step whose call produced a table now names it (resultRef), for the
projected tables of the older tools as well.

show_result gets its own step. It reads nothing new: while it runs it
names no domain, and once the stored table is found it takes the source
of the read that built it and the name the copy carries in this turn. A
table that could not be shown is a failed step with the reason, never an
empty read.

The property test now drives show_result too and checks resultRef; the
coverage test gives each tool the smallest arguments it accepts instead
of passing empty arguments to a tool that requires a metric.
2026-09-27 12:11:54 +02:00
Marc-André Bombeck e0b4f3c427 fix(restore): size the restore budget by the rows written, and keep test housekeeping off the short limit 2026-09-27 11:57:37 +02:00
Marc-André Bombeck 914fcbcbf1 Merge branch 'coach/wp2-tables' into release/v1.39.4
# Conflicts:
#	src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap
2026-09-27 11:55:05 +02:00
Marc-André Bombeck 47efdd6606 feat(coach): result table with copy for a spreadsheet or as text
The table shows its caption, scoped headers, right-aligned tabular
numbers and a dash for a missing reading, twelve rows before Show all.
Copy writes TSV and an HTML table together, falling back to TSV, with
formula starts neutralised and separators flattened inside cells.
2026-09-27 11:54:31 +02:00
Marc-André Bombeck a967ccac44 feat(coach): metric tables and show_result for the chat tools
get_metric_table reads one metric as days, weeks or months through the
chart reader, cut in the user's zone and collapsed to one source per day,
with null for a period without a reading. The model reads a bounded
summary; the full table rides the turn, encrypted at rest. show_result
shows a table an earlier reply of the same conversation fetched, resolved
only against that conversation's own messages. Workouts, labs, sleep and
adherence results project into tables, earlier tables are named in the
context by metadata alone, and result:rN marks are stripped from the reply.
2026-09-27 11:54:30 +02:00
Marc-André Bombeck b84ea0604f Merge branch 'coach/wp1-steps' into release/v1.39.4
# Conflicts:
#	src/app/api/insights/chat/__tests__/route-capability-gate.test.ts
#	src/app/api/insights/chat/__tests__/route-list-query.test.ts
2026-09-27 11:53:39 +02:00
Marc-André Bombeck cbf603c3c9 Merge branch 'fix/sidebar-collapse-bottom' into release/v1.39.4 2026-09-27 11:51:13 +02:00
Marc-André Bombeck 00a5a8862f Merge branch 'coach/wp5-clarify' into release/v1.39.4
# Conflicts:
#	src/app/api/insights/chat/__tests__/__snapshots__/golden-transcripts.test.ts.snap
2026-09-27 11:47:52 +02:00
Marc-André Bombeck d2aba1b86b feat(nav): move the sidebar collapse control to the bottom of the sidebar
The small chevron above the first nav entry becomes a full footer row that
heads the utility group: directly above Admin for an administrator, directly
above Settings for everyone else. PanelLeftClose / PanelLeftOpen mark the two
states; the label reads Collapse while expanded and stays for screen readers
only in the icon rail, where the tooltip names the action. aria-label switches
between collapse and expand; the stored choice keeps its storage key.

The width transition now honours reduced motion. Adds the expand and
sidebar-action labels in all seven locales.
2026-09-27 11:47:48 +02:00
Marc-André Bombeck dc085cff4d Merge branch 'coach/wp6-method' into release/v1.39.4 2026-09-27 11:47:38 +02:00
Marc-André Bombeck 01aba93878 feat(coach): say how an answer was worked out
The evidence disclosure under a Coach answer now carries one muted
line: which sources and windows the turn read, how many readings, and
how they were aggregated ("Blood pressure, last 90 days: 142 readings,
weekly averages · Sleep, last 30 days: no readings in this window").
The server builds it from the recorded steps and tables in the request
locale; it holds catalog strings and counts only, never a reading, and
a source that turned up nothing says so.

The tool-mode prompt gains a recheck rule: when the person questions a
figure, name the source and window, read it again (or reuse the stored
table), and correct the answer if the two differ.
2026-09-27 11:47:11 +02:00
Marc-André Bombeck 1914764ddc feat(coach): show what the Coach looked at above the answer
While a turn runs, the header names the read in progress with a soft
shimmer; when it ends the list folds into "Looked at N sources". Each
row reads domain, window, and the count or the reason nothing was
found. Completed steps are announced through a polite status region at
most once every 1.5 s, and a reloaded conversation stays quiet. The
spinner and shimmer stop under reduced motion.
2026-09-27 11:44:55 +02:00
Marc-André Bombeck ea2eaf2074 feat(coach): stream a step for every read the turn makes
Each tool call now emits a running step as it starts and its final
status as it settles: found with a server-counted number of readings,
empty with the reason, or failed. The no-tools path shows one step for
the record summary. Steps carry catalog labels rendered in the request
language, closed-enum domains and windows, and integers only; a lab
analyte, a value or a model-written argument never reaches one. At most
twelve per turn. They persist on the provenance, so a reload shows the
same list.

The conversation window reaches the step so a call without a window
names the one it actually read. The no-tools transcripts gain the
summary step; a new transcript pins found, empty and failed steps ahead
of the first token.
2026-09-27 11:44:54 +02:00
Marc-André Bombeck d44b875ec6 test(restore): stub the reading count the restore budget now reads 2026-09-27 11:37:51 +02:00
Marc-André Bombeck 6b07d48bf9 feat(coach): ask back when the metric or window is ambiguous
A reply may end with a ---CLARIFY--- block (kind metric, window or
context, 512-byte cap). The server keeps only metric choices the record
holds, drops the question when fewer than two remain, takes window
choices from the presets, screens the question text like any reply, and
never offers a second question right after one. Choice labels come from
the catalog, never from the model.

An answer arrives as clarification { messageId, choiceId? }; it resolves
against the stored question on the conversation's latest message and
adds one server-written line to the next turn's context. The card sits
above the composer, announces politely and takes no focus.

A reply that is only the block, with no question before it, is now an
empty reply instead of falling back to the unstripped text.
2026-09-27 11:33:32 +02:00
Marc-André Bombeck 93b6c086b4 fix(restore): let a large restore outlast the request statement timeout
A restore of an account with 1.9 million readings, most of them deleted
by the nightly consolidation and still awaiting the purge, was cancelled
in its clearing step: `DELETE FROM measurements` ran past the 60 s
statement timeout every pooled connection carries, and the whole restore
rolled back with a message that named no cause (#1031).

Most of that statement was not the delete. Two columns point into
`measurements` with ON DELETE SET NULL and had no index, so Postgres
scanned `personal_records` and `ecg_recordings` once per deleted reading:
38 of 40 s on a seeded account with 300 personal records. Migration 0362
indexes both, which also speeds up the tombstone purge and account
deletion.

The restore transaction now sets its own statement and idle-in-transaction
limits to the transaction budget with SET LOCAL, and the budget counts
the rows the account holds as well as the rows the file carries. A
rolled-back restore names what the database refused it over (timeout,
lock, constraint, storage, connection, other) on the job row, the
console, the audit row and the wide event.
2026-09-27 11:24:41 +02:00
Marc-André Bombeck 78fc22d0f8 Merge branch 'coach/wpF-contracts' into release/v1.39.4 2026-09-27 11:10:37 +02:00
Marc-André Bombeck 1e9b0aa7ff test(api): list the stored-tables route among the per-table module-gated reads 2026-09-27 11:02:14 +02:00
Marc-André Bombeck 0598b58f0e feat(coach): client slots for steps, tables, method, chips and clarification
The streaming hook handles the step, result, followUps and clarification
frames and sends followUp and clarification on the tool route only. The
bubble gains slots for the step list, the tables and the method line; the
thread offers chips under the latest assistant turn only; the conversation
shows the clarification card above the composer and marks a typed answer
as answering it. The tables of a persisted message load lazily when it
scrolls into view, seeded from the stream for the turn that produced them.
The new components render nothing yet.
2026-09-27 10:57:53 +02:00
Marc-André Bombeck fe0a03761e feat(coach): message catalog for the dialog in all seven languages
Adds every string the dialog needs up front: step labels, table titles,
columns and controls, the method line, the follow-up chips and their
setting, the clarification card, and the caption of an answer reused from
a stored table. A key catalog shared by server and client names each one;
a test holds the catalog and the bundles equal in both directions.
2026-09-27 10:50:49 +02:00
Marc-André Bombeck ad72ae3e4c feat(coach): serve a message's stored tables and publish the stream contract
GET /api/insights/chat/{id}/messages/{messageId}/results returns the
tables one assistant message read, narrowed to the conversation's owner
like the conversation read, with a table withheld as module_disabled when
its domain's module is off and as unavailable when it cannot be read. A
data read: no AI capability.

The published document now describes every stream frame as the
CoachStreamEvent union, including the suggestedAction frame it never
named, the new step, result, followUps and clarification frames, their
components, the provenance fields, the followUp and clarification request
fields, and the reuse provider type.
2026-09-27 10:45:18 +02:00
Marc-André Bombeck 8fcd204172 feat(coach): seams for steps, tables, charts, method, chips and clarification
Each later piece of the dialog gets its own module, answering nothing for
now: the step builder, the table projection and chart choice, the result
ref marks, the follow-up derivation, sentinel, resolution and continue
chip, the reuse turn, the clarification parse and answer, the method line,
and the four prompt addenda. The turn pipeline calls them where they
belong, so the frames go out in the documented order (step, token,
provenance, result, cards, clarification, followUps, done) and a blocked
turn drops its tables, chips and clarification. With every piece empty
the stream and the prompt stay byte for byte as before.
2026-09-27 10:43:15 +02:00
Marc-André Bombeck 2feb622b6a Merge branch 'fix/encrypt-routes-and-practitioner-contact' into release/v1.39.4 2026-09-27 10:38:08 +02:00
Marc-André Bombeck 742ef2c9b0 feat(coach): report tool calls as they start and settle
The retrieval loop takes onCallStart and onCallSettled callbacks, fired
for every call with a turn-wide index; a throwing callback is swallowed.
The trace carries each call's schema-validated arguments (never the raw
string, never persisted), the result says whether the answer was forced
at the round cap, and a seedCalls parameter is accepted but not read yet.
2026-09-27 10:37:40 +02:00
Marc-André Bombeck f2e32afb24 fix(privacy): store environment coordinates at 1 decimal instead of 2
The home location of the environment module was rounded to 2 decimals,
about 1 km, which places a home in a neighbourhood rather than the town the
schema comment promised. The weather archive it feeds is gridded at 9 to
25 km, so the extra precision bought nothing.

Every stored coordinate of the module now goes through the same 1-decimal
rounding (about 11 km north-south, 7 km east-west at 50° N): the home, the
travel periods, the geocoder's results, the per-day weather rows, and a
restore of a file written with finer values. Migration 0361 rounds the
existing users, travel periods and per-day rows; running it twice changes
nothing more. The answers keep their shape, plain numbers. The API
descriptions and the schema comment say 1 decimal now.
2026-09-27 10:37:37 +02:00
Marc-André Bombeck a8b3e5d7f4 feat(coach): store a turn's result tables encrypted beside the message
A new nullable column, coach_messages.results_encrypted, holds the tables
of values a turn read as AES-GCM ciphertext; only their metadata rides the
plaintext provenance. readMessageResults serves them to the owner, each
table whole or withheld with a reason. The provenance restore learns the
steps, method, table metadata, chips, clarification and the forced-answer
marker, entry by entry against the wire schema.

The column is registered for key rotation, carried through both backup
formats (ciphertext in a disaster-recovery file, readable JSON in a
portable one) and goes with its row on conversation delete and wipe.
2026-09-27 10:36:27 +02:00
Marc-André Bombeck cde7af3cf8 feat(coach): wire contract for steps, result tables, method, follow-ups and clarification
Adds the TypeScript types for the four additive stream frames and their
provenance fields, a Zod mirror for the published document held equal to
the types by a type test, and a guard that no newer frame reuses a key the
shipped native client decodes. The chat request accepts followUp and
clarification, inert for now.
2026-09-27 10:33:13 +02:00
Marc-André Bombeck 620c538f9d Merge branch 'coach/wp0-pipeline' into release/v1.39.4 2026-09-27 10:25:41 +02:00
Marc-André Bombeck b8e78a78b3 chore(openapi): regenerate for the practitioner backup ciphertext fields
The practitioner entry of the backup payload gains the optional
locationEncrypted and phoneEncrypted fields. The rotation script names each
new column once, at its call, so a dropped call is noticed rather than
covered by a comment.
2026-09-27 10:21:48 +02:00