Commit Graph
685 Commits
Author SHA1 Message Date
Marc-André Bombeck dcb2598bb2 refactor(ai): keep each language's outbound-screen banks in its own module
outbound-screen.ts held every locale's dose, risk, causal, continuation
and change-stem patterns plus the shared units and slot builder, and
grew with each language. The banks now live in
outbound-screen/locales/<locale>.ts, one per language, each exporting
the same LocaleBank shape; the units, target-dose shape, object slot
and generic-name stems are in outbound-screen/shared.ts. The screen
composes them per locale and keeps the matching logic, the
continuation rule and the general rationale.

No pattern changed: compiling the old and the new module and comparing
every bank of every locale, with and without a medication-name list,
gives the same regular expressions (108 of 108 identical), and the
screen tests still block and pass the same sentences in every locale.
The screen is 457 lines instead of 1,597, and buildDoseChangeClass is
a four-line composition.
2026-10-22 15:26:37 +02:00
Marc-André Bombeck 8c11bb8385 refactor(coach): keep the population-norm banks in one module per locale
The reference-sentence exemption's patterns move out of
coach-prose-grounding.ts into population-norms/<locale>.ts, composed
into the same Record the check already reads. The patterns themselves
are unchanged. A test feeds every locale one reference sentence the
English bank cannot match, so a module that is not wired fails.
2026-10-22 15:21:10 +02:00
Marc-André Bombeck f987efd9f3 refactor(insights): move the insights reply-language footers into their own module
The per-locale footer table is prompt text, not prompt logic; it moves beside the generator, which drops back under its size baseline after pt-BR joined the table.
2026-10-22 15:19:33 +02:00
Marc-André Bombeck 3577da6966 fix(coach): narrow the pt-BR population-norm patterns
A sentence the grounding check reads as population guidance has its
numbers exempted from the evidence check. Two of the pt-BR patterns
matched sentences about the reader's own readings: "(entre|de) ...
geralmente" fired on "Sua pressão de 158/97 geralmente fica...", and a
singular "médico ... recomenda" with a 30-character gap fired on "Seu
médico recomenda manter a pressão em 158/97". Either let an invented
figure through unchecked.

Every pt-BR entry now needs a population subject or a reference noun:
plural guideline, doctor or specialist subjects next to their verb,
plural adults needing something, and "a/o <metric> normal" without a
possessive. Portuguese month names join the written-date pass so "21 de
julho" is not graded as a figure, and quilos, por cento and passos
join the unit words.

Negative cases prove the reader's-own sentences are checked again;
positive cases keep the reference sentences exempt.
2026-10-22 14:50:17 +02:00
Marc-André Bombeck 68b428e3f5 feat(ai): screen Brazilian Portuguese model output for dose, risk and causal claims
The outbound screen runs the reader's bank plus English. For a pt-BR
reader that meant English only, so "Pare de tomar a metformina",
"Dobre a dose de insulina" or "Seu risco é de cerca de 12%" passed every
surface unchanged.

Add the pt-BR banks the other locales carry: the dose-change class
(change, take, skip, stop, halve, double and one-more imperatives in
the você form, with the medication nouns, drug classes and stop words a
Brazilian reply uses), the target-dose patterns, the risk-figure and
risk-engine patterns, the causal phrases, and the continuation and
change-stem pair that keeps "continue tomando conforme prescrito"
exempt. UI joins the insulin units, and the count words and dosage
forms gain the Portuguese spellings.

Tests pin each class blocking in pt-BR and the restatements, negated
warnings, referrals, past changes and dietary advice passing.
2026-10-22 14:50:10 +02:00
Marc-André Bombeck 764794496c feat(i18n): register Brazilian Portuguese as a locale
Add pt-BR to the locale list and everything that hangs off it: the
client loader, Intl and date-fns mapping, the reply-language footers of
the Coach and insights prompts (English body, pt-BR footer, the same
path Korean and Dutch take), the citation-coverage normative words, the
no-key fallback copy, the AUDIT-C standard-drink line, country labels,
period search, CSV yes/no cells and the Telegram bare-word aliases.

The locale enums in the extract, managed-profile and record-settings
schemas now derive from the locale list instead of repeating it, and
the five notification and login-alert routes share one isLocale guard
instead of hand-rolled chains, so a later locale is one edit there.
A language tag is matched exactly and case-insensitively before its
primary subtag, so pt-BR and pt-PT browsers land on the Portuguese
bundle rather than English, and AUDIT-C no longer cuts a region tag to
two letters.

PHQ-9, GAD-7 and WHO-5 are not marked validated for pt-BR: the wording
has not been read against the published Brazilian forms, so readers get
the English items plus the existing note.
2026-10-22 14:50:00 +02:00
Marc-André Bombeck 402daa2753 Merge pull request #1251 from dareoon/feat/i18n-pt-br 2026-10-22 14:19:17 +02:00
Marc-André Bombeck 190a83c513 feat(coach): read lab results over the whole record
The Coach read lab results from the last twelve months only, even with
the lookback set to its widest option, so a slow change in LDL or PSA
across years stayed out of reach. Labs are sparse, so they are no
longer held to the one-year window that bounds the dense readings: they
reach back over the whole record, and a lookback limit the person set
still applies.

The snapshot block keeps the latest value per marker, now over that
wider window, at its old size: it shares the prompt's size cap and is
never shed. get_labs with an analyte adds each matching marker's
history, summarised so it stays bounded: the newest five earlier
readings in full and older ones as one mean / min / max / count per
year, ten years at most, older years counted. With a heavy fixture
(30 markers, quarterly for 20 years) the overview stays at 4.3k
characters and a named LDL read grows from 0.2k to 1.2k characters for
80 readings. The MCP get_labs read and the lab resource follow, since
they run the same tool.

The widest lookback option no longer reads "All": it names what it
reads, twelve months in detail and every lab result, in every locale.

Refs #1263
2026-10-22 10:50:56 +02:00
Marc-André Bombeck f8a88d1361 perf(auth): read the operator settings once per request
An app boot's /api/auth/me read the AppSettings singleton six times, each
reader with its own few columns: the MFA policy, the Assistant switches,
the module availability, the provider presence, the AI usage bounds and
the reasoning controls. They now take their columns from one read,
memoised for the request and read afresh inside a background job like the
readers themselves. No answer changes.
2026-10-22 07:25:00 +02:00
Marc-André Bombeck b5f5a5a580 perf(i18n): build the every-locale match indexes on first use
Seven matchers built their index over every shipped catalog at module
load (the GLP-1 tag map, the nutrient and metric names of the MCP reads,
the SNOMED and LOINC lab names, the Coach's health words, the lab-name
pattern), so loading the worker graph read all eight catalogs at boot.
Each index is now built the first time it is asked for.

The boot profile records the change: the longest busy stretch of the
main thread at boot fell from 753 ms to 241 ms, the live heap after five
idle minutes from 164 to 152 MB, and the catalog chunk sources (two of
9.7 MB) are gone.
2026-10-22 07:01:03 +02:00
Marc-André Bombeck c86c7d1a79 refactor(lint): enforce the lib → components and client → server boundaries
Add the in-repo rule `healthlog/import-boundaries`:

- a file under src/lib may not import src/components or src/app (alias,
  relative, re-export and dynamic forms, type-only included);
- a "use client" file may not make a value import of the database client,
  the crypto module, the Prisma client entry, the job queue, pg, pg-boss or
  next/headers. Type-only imports and the Prisma enums entry stay legal.

The four lib files that still import upward are listed in the rule's allow
map with a reason each; a listed file that stops importing upward is
reported, so the map can only shrink.

Five of the nine upward imports are gone by moving pure modules to their
layer, without behaviour change:

- supply-runway estimate → src/lib/medications/supply-runway.ts
  (read by the low-stock job and the medication list read);
- weekday BYDAY tokens → src/lib/medications/scheduling/weekday-tokens.ts
  (read by the medication extraction prompt);
- settings section slugs → src/lib/settings/section-slugs.ts
  (read by the record-settings classification);
- the command palette index → src/components/command-palette/ (it is a
  client index built from component registries).

Proof the rule can fail: an import of @/components/ui/card appended to
src/lib/medications/supply-runway.ts reported libUpward, and a planted
"use client" file importing @/lib/db reported clientServerOnly; both were
removed again.
2026-10-22 04:15:11 +02:00
Marc-André Bombeck faa3ef867b feat(insights): correlate intake, alcohol days, training, rings, State of Mind and room climate
The discovery matrix gains the families people log but could not test:
water, caffeine and up to four varying micronutrients from the nutrient
log, alcohol days from the mood tag, workout minutes (one per session) and
the day's training impulse, exercise, stand and mindful minutes, State of
Mind as an outcome, and the overnight room climate against that night's
sleep only. Every channel runs through the same seasonal adjustment,
effective-n p-value, Benjamini-Hochberg control and effect-size floor, is
read only while its module is on, and drops out below the paired-day floor.

State of Mind, mindful minutes and alcohol days are statistics for the
person: the Coach, the period narrative and the assessment cards drop every
pair that touches them, after the shared scan so the remaining pairs keep
the statistics the correlations page shows. A guard walks the import
closure of the analysis modules and refuses any path to a provider.
2026-10-21 21:39:15 +02:00
Marc-André Bombeck bf3830203f fix(hrv): keep RMSSD on the display surfaces for an account with no SDNN
Fitbit and Google Health HRV is filed as RMSSD since migration 0420, so
an account whose only HRV comes from them lost it wherever a display
surface read SDNN alone. The dashboard HRV tile keeps its RMSSD fallback
with Recovery off while the account has no SDNN, and its toggle stays
offered for either estimator. A briefing HRV signal counts as measured
today when either series has a reading. The doctor report already files
RMSSD as its own ungated leaf; a guard now holds that. Readiness, HRV
balance and stress stay on SDNN; a test pins that an RMSSD-only account
renormalises over the other components.
2026-10-21 14:32:56 +02:00
Marc-André Bombeck e897549dff fix(ai): scrub fence markers until none can re-form
The shared data/instruction fence removed embedded markers in a single
pass. Removing a marker joins the text either side of it, so a marker
split around another one came back whole after the scrub:
`<<<DOCUMENT_TEXT_E<<<DOCUMENT_TEXT_END>>>ND>>>` became
`<<<DOCUMENT_TEXT_END>>>`, and the same split around any other marker
worked too. Content could close its own fence wherever the helper is used:
the Coach snapshot and self-report, MCP tool results, document reads and
lab scans.

The scrub now repeats until a pass changes nothing, which proves no marker
is left. Content nested deeper than eight levels has every run of three or
more angle brackets cut to two, after which no marker can exist or
re-form. Ordinary text, angle brackets included, is unchanged.
2026-10-21 11:51:22 +02:00
Marc-André Bombeck bfc7991935 fix(labs): treat a PDF's text layer as a fenced hint and keep the day's counter current
The text layer went to the model as the authority for digits. It is not
one: a scanned PDF often carries an invisible OCR layer with its own
mistakes, a subset font can map to the wrong characters, and a crafted
PDF can carry invisible text that differs from what the reviewer sees.
The prompt now calls it a hint only, makes the image authoritative, forbids
adding a value the image does not show, and asks for a low value
confidence where the two disagree. The layer is fenced with the document
text markers, which it cannot close, and so is text mode's OCR'd text.
Each page's layer is capped while it is collected, and a read shares its
text budget out per page, cut at a line and marked, so a long first page
no longer pushes the later ones out.

A further read now first books what the reads so far cost beyond their
reservation, then its own ceiling, both against the cap of the owner the
route judged the first read by, so the counter the next read is judged
against never lags. The wire is re-checked before every read, the first
included. A re-check or reservation that itself fails stops the scan with
the pages read rather than failing it, and a scan stopped before any row
answers with the stop's reason (budget, or the wire's own refusal) instead
of an unreadable page.
2026-10-21 11:33:37 +02:00
Marc-André Bombeck 79d905f089 fix(labs): reserve and re-check each page of a scan, and read digits from the text layer
The route reserved one read's budget and checked the wire once, and a
20-page report then ran up to 40 calls for many minutes on that single
admission. Every read after the first now reserves its own share of the
day's budget and asks the wire again (aiEgressRefusal) before it is sent;
when either refuses, the scan stops there, keeps the pages already read
and names the rest with stoppedBy budget or unavailable. The run settles
against everything it reserved, with the provider's reported usage net of
its cached share, and a scan that fails after several calls is charged
for them instead of refunded in full.

A rendered PDF page now goes out with its own text layer, read by pdfjs
beside the image, so a digitally produced report's digits, decimal
separators and units come from its characters rather than from a 1600 px
picture. Splitting the PDF into single-page documents would need a PDF
writer the project does not ship; the text layer needs none and helps
every provider. A scanned page has no text layer and reads as before.

Codex's cut reply keeps the text it streamed (on a non-enumerable
property, so no log or event serialises it), and its closed rows are
salvaged like every other provider's. The merge folds a reading only into
one an earlier page listed, never two rows of the same page, and the
printed range is part of the key. OCR'd text the deadline cannot split
small enough for one read is refused up front as labs.ocr.tooLarge.
2026-10-21 11:10:02 +02:00
Marc-André Bombeck 10cfdf8b66 fix(labs): read a lab report page by page and keep a cut-off read's rows
A scan sent the whole report to one call under an 8000-token ceiling and
validated the reply against a 100-row envelope. A long multi-panel report
lost everything either way: the reply stopped mid-row and failed to parse,
which the person saw as "try a clearer photo", or it held more than 100
rows and the schema refused all of them. One malformed row refused the lot
as well. Reasoning models made it worse, because the call sent no
reasoning setting and OpenAI, Codex and Claude's 5 generation reason by
default against the same output allowance.

A PDF of more than one page is now rendered and read a page per call on
every provider (up to 20 pages), as many reads as the run's deadline
allows, and the pages are merged in order with a reading printed twice
listed once. A photo, a one-page PDF on a provider that reads PDFs, and
OCR'd text up to 4000 characters stay one read; longer text is split at
line boundaries. Each read carries an explicit reasoning off, so a model
that can stop reasoning does and one that cannot gets its minimum budget
on top of the answer's. A reply cut at the output limit is detected from
the finish reason (Codex's incomplete stream included), keeps the rows
that closed before the cut and is named in the result instead of retried;
when no read produced a row the run fails as labs.ocr.outputLimit. A
provider failure after the first page keeps the pages already read.
Rows are validated one by one, the row cap is 400 for the scan and the
commit, and a report past it keeps its first 400 and says how many it
had. The run settles what the provider reported it spent rather than the
fixed reservation, so a page-by-page read still counts against the day's
budget in full.
2026-10-21 10:40:45 +02:00
Marc-André Bombeck 28c8406924 fix(ai): park a revoked Codex family and stop at a disconnect mid-refresh
Under the refresh lock, with the row still holding the token just spent,
no peer rotated it, so refresh_token_reused is the issuer refusing the
whole family: it now parks the sign-in instead of being asked again
after every cooldown. Outside the lock it stays a lost race. A refresh
whose compare-and-set write finds the connection gone (the person
disconnected Codex meanwhile) now refuses with reconnect-required
instead of using the rotated token once more.
2026-10-21 01:16:51 +02:00
Marc-André Bombeck 1be2c27b27 fix(ai): serialize Codex refreshes so a lost race never parks a live sign-in
Two callers refreshing one Codex token raced: the loser heard
refresh_token_reused, read it as a dead grant and parked the connection
before the winner wrote its rotated tokens, leaving fresh tokens beside
an expired status. Refreshes now share one flight per process and queue
on the per-credential advisory lock across processes, re-read the row
inside it, use a token a peer already rotated, write the rotated tokens
compare-and-set on the spent one together with connected, and re-read
after a refusal before parking. refresh_token_reused no longer counts as
a dead grant, and neither does a bare 403 without an OAuth error code:
an edge challenge answers a valid grant so, and it keeps the ledger's
timed cooldown instead of parking until the next sign-in.
2026-10-21 00:06:21 +02:00
Marc-André Bombeck 07d70cd275 fix(ai): park a Codex sign-in the issuer refused until the next sign-in
A dead refresh token was asked again on every hop of every run: the
proactive refresh failed and was swallowed, the request 401'd, the on-401
refresh failed again, and the nightly warm repeated that for the briefing
and every card. A refusal that proves the grant dead now flips the
connection to the existing expired state (compare-and-set on the refused
token, one signal per transition), so the chain, the presence probe and
the AI capabilities stop counting it. A new device-code sign-in clears the
refused credential's ledger rows so the fresh one resumes in place.
2026-10-20 20:04:21 +02:00
Marc-André Bombeck 6182d45354 fix(i18n): print server-built sentences in the reader's notation
The decimal lint covered components and app only, and sentences for
people are built in src/lib too: the weekday weight correlation printed
"0.8 kg" into a German sentence, and the no-key fallback and the period
narrative's fallback spelled fractions with a full stop. Those three use
the new formatDecimal from format-locale with the reader's locale. The
rule now covers src/lib and src/hooks; text for the model, operator log
lines, SVG data, zone checks and wire values a client re-formats carry a
reasoned directive.
2026-10-20 16:52:08 +02:00
Marc-André Bombeck d0677adac9 perf(bundle): keep zod out of the documents and Coach client routes
Both routes reached the schema library through a constant: the list of
document kinds, the list and bulk limits, the attachment cap, the Coach
data clusters and preference defaults, the conversation title length,
the scope source list and the report leaf order. Each sat in a schema
module, so importing the number shipped the library. The constants now
live in zod-free modules (lib/documents/inbound-constants,
lib/ai/coach/prefs-constants, lib/ai/coach/scope-sources,
lib/report-selection/catalogue) and the schema modules re-export them
for server code; the Coach page checks its scope parameter against the
closed list instead of parsing it. A guard walks each route's static
client graph with the TypeScript parser and fails on a zod import.

Two guards follow earlier commits in this branch: the fetch-limit
allowlist names the shared document picker sheet, and the card density
test states a floor on the cards it finds.
2026-10-20 14:04:06 +02:00
Marc-André Bombeck e5b7eebe64 Merge branch 'fix/v1437-r2-jobs' into release/v1.43.7 2026-10-20 10:17:01 +02:00
Marc-André Bombeck 2372fee376 fix(coach): count the daily question limit from the person's midnight
The limit on clarifying questions reset at UTC midnight, which is the middle
of the night in Europe and the afternoon on the American west coast. It now
counts from the start of the person's own day.
2026-10-20 09:58:02 +02:00
Marc-André Bombeck 6e54f33901 refactor(i18n): ask the catalogue for a sentence in one helper
`t()` answers a missing key with the key, so "does this code have a
sentence" was a comparison written inline at every site that wanted a
code's sentence or a fallback, twenty-odd times in several spellings.
translateOrNull, translateCodeOrNull and translateCode in
src/lib/i18n/translate-code.ts now hold that probe, every site uses
them, and a guard refuses the inline comparison anywhere else.
2026-10-20 09:53:31 +02:00
Marc-André Bombeck fea1d1e0b5 chore: strip planning tags from comments and enable the rule
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment
lines in 1,308 files lose the work-item identifier at their head. Release
versions and issue references stay; nothing outside comment text
changes, which the script checks per file against a comment-free
printout. healthlog/no-planning-tags is enabled at error level with this
commit, so the tree it lands on is clean.
2026-10-20 09:08:40 +02:00
Marc-André Bombeck 45c027aee2 Merge branch 'fix/v1437-review' into release/v1.43.7
# Conflicts:
#	src/app/api/analytics/route.ts
2026-10-20 06:20:42 +02:00
Marc-André Bombeck 40cf10ba20 test: bring fixtures in line with the nap rule and the recovery read
Correlation fixtures wrote minute-long midday sleeps, which the nap
rule now reads as naps; they write nights. The analytics test mocks the
user read the empty recovery reason makes, and the control-byte guard
asserts the size of its sweep.
2026-10-20 06:06:25 +02:00
dareoon 12065bc0c7 feat(i18n): add Brazilian Portuguese support to medication and insight flows
Add pt-BR locale coverage across medication extraction, coach prose
grounding, no-key insight fallbacks, coach read dates, and lab reference
range parsing.

- Accept pt-BR natively in NaturalLanguageExtractorProps.
- Add Brazilian Portuguese population-norm framing patterns.
- Add pt-BR no-assessment fallback copy for all insight floors.
- Add pt-BR expected date coverage to the Coach read strip test.
- Add pt-BR lab range prose vocabulary.
- Keep existing locale behavior and safety-contract coverage unchanged.
2026-10-19 22:32:17 -03:00
Rob de Graaf c6f61fad33 feat(i18n): add Dutch as an eighth end-to-end locale
Adds messages/nl.json (AI-initial, informal je/jij register, full key
parity with en.json) and registers nl everywhere the locale list lives:
config, loaders, Intl/date-fns mapping, zod enums, OpenAPI spec, scripts,
the managed-profile picker and the notification locale guards.

Dutch follows the Korean pattern for the AI surfaces (English body plus a
Dutch reply-language footer, no safety-contract matrix) and gets its own
banks in the outbound safety screen (dose, risk, causal, continuation),
the prose-grounding and citation-coverage tables, the reference-range
prose and the no-key fallback copy, each with Dutch test fixtures.

PHQ-9 / GAD-7 / WHO-5 are not marked validated for nl yet: the Dutch
wording has not been read against the published forms, so Dutch readers
get the English items plus the existing note until it has.
2026-10-19 11:06:55 +02:00
Marc-André Bombeck e2b7e7350e fix(consent): mint AI consent receipts only on an explicit agreement
The AI settings page posted to /api/consent/ai/web on every mount and the
server minted an ai_full receipt for it, so an account that had agreed to
nothing sent its health data to the operator's provider from background
jobs. Native builds synced a receipt the same way on every launch.

- Both grant routes now require `confirm: true`; anything else writes
  nothing and answers 400 consent.ai.confirmRequired.
- Remove the mount-time heal. A consent card names the operator's service
  and the data that would go there, with one Agree button, in Settings > AI
  and on the Insights overview while AI is closed by the missing consent
  alone.
- Publish the standing consent as `ai.consent` on /api/auth/me (status,
  since, recipients); the settings card renders active-since and the
  withdraw control from it.
- Existing receipts stay valid.
- Guard that no client grants outside a click handler, route tests for the
  confirmation, and an integration test that a fresh operator-key account
  gets no background AI text until it agrees.
2026-10-16 17:11:04 +02:00
Marc-André Bombeck 057fd3b49c perf(bundle): keep Zod off the first load of four eager routes
zod 4.6 grew the shared Zod chunk from 63 to 89 KB gz (the new schema
compiler and its memoizer ride along because every schema goes through
the `z` namespace), and the dashboard, measurements, mood and settings
routes each carried it for values that need no validation.

Move the measurement source list, the notes limit, the screening check
and the Coach source list into value-only modules the schemas are built
from, import the settings classifier and response check from their
leaves, and load the gate's managed settings and anamnesis sections on
demand. Re-base the four route baselines to the measured values and the
aggregate cap to 4975, with the reason in the budget comment.
2026-10-16 06:58:48 +02:00
Marc-André Bombeck 8ae60e2cb3 fix(coach): read free weekdays for a prompt only with the work section released 2026-10-16 05:42:15 +02:00
Marc-André Bombeck 120b0966b1 fix(ai-usage): never regenerate more often on frugal than on standard
The frugal sync gap is the larger of a day and the operator's sync gap,
so an instance gap above 24 hours no longer leaves frugal ahead of
standard.
2026-10-16 05:38:36 +02:00
Marc-André Bombeck b99b70127d fix(anamnesis): one newest AUDIT-C entry, scored against the wording offered
The stored language is the wording the read route offers the account;
a request naming another wording, or a takenAt more than five minutes
ahead, is refused. Latest, offerDue, the Coach block and the doctor
report all read the newest entry whose answers open.

The old one-word alcohol pattern gives way to an AUDIT-C entry in the
Coach block, and the alcohol section lists it with a remove action.
2026-10-16 05:36:42 +02:00
Marc-André Bombeck 4daad8f2fb Merge branch 'v143/life' into release/v1.43.0
# Conflicts:
#	docs/api/openapi.yaml
#	src/__tests__/contract-stub-inventory.test.ts
#	tests/integration/sharing-manage-handler-matrix.test.ts
2026-10-16 03:39:40 +02:00
Marc-André Bombeck 4d2c8c2bbb test(anamnesis): drive the life circumstances, AUDIT-C and its backup through real Postgres and the manage matrix 2026-10-16 01:34:17 +02:00
Marc-André Bombeck 88aa5ccacd test(anamnesis): pin derived values, AUDIT-C wording and cut-offs, and the per-section prompt gate 2026-10-16 01:27:00 +02:00
Marc-André Bombeck 73bb13a982 feat(anamnesis): soft-delete AUDIT-C in its route, admit both AUDIT-C verbs at manage level, and settle the guards 2026-10-16 01:24:56 +02:00
Marc-André Bombeck b838ed4403 feat(dawarich): fetch daily movement values from a Dawarich instance
Connect, status, places, settings, test, sync and disconnect replace their
stubs. A nightly job (03:20) and the connect backfill read the timeline,
visits and residency endpoints through safeFetch and store one row of daily
values per local day: distances, places visited, minutes away from the home
place and the sealed country with its travel-day verdict. A private instance
needs its exact origin in DAWARICH_PRIVATE_ORIGINS.

The connection joins the shared ledger and the connection health model,
watched for errors only. The day carries a movement block, the model
projection drops the travel country, the two movement channels join the
correlation matrix under the environment module, travel days become trip
suggestions, and the days travel in the environment backup section.
2026-10-16 01:20:53 +02:00
Marc-André Bombeck 28a8737124 feat(anamnesis): life-circumstances card with AUDIT-C, screening hints and free-text take-over 2026-10-16 01:09:50 +02:00
Marc-André Bombeck db750ef246 feat(anamnesis): carry life circumstances into the coach context, the doctor report and FHIR 2026-10-16 00:58:34 +02:00
Marc-André Bombeck 6146082824 Merge branch 'v143/supplements' into release/v1.43.0
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/pl.json
#	src/__tests__/sharing-surface-guard.test.ts
#	src/lib/ai/capabilities/types.ts
#	src/lib/export/backup-plan.ts
2026-10-16 00:28:29 +02:00
Marc-André Bombeck eb3cc4237f feat(medications): store a supplement's composition, read its label with the document AI, and carry it through backup and restore 2026-10-15 23:29:40 +02:00
Marc-André Bombeck 74dcc67ef7 feat(insights): let the usage level decide the background AI work
The level now sets, per record: the status-card regeneration gap after a
sync (frugal 24 h, standard the operator's gap, full 1 h) and after a hand
entry (frugal 6 h, otherwise 1 h); the status snapshot cap (10 000 compact
characters on frugal, 14 000 above); background reasoning for the briefing
and the period narratives (off on frugal); the daily briefing's form (a
short form on frugal that keeps every field and at most one
recommendation, the full contract on standard and full); and whether the
automatic document read may act (frugal reads on request only, the stored
switch is kept).

A deterministic simulation of a watch-synced day through the invalidator
counts 122 ingest-driven status generations on full (the v1.42 behaviour),
25 on standard and 2 on frugal, plus the nightly warm on each.

Refs #1217
2026-10-15 22:55:40 +02:00
Marc-André Bombeck 6f673a7b84 feat(ai): resolve an AI usage level per record and publish it as ai.usage
Three levels (frugal, standard, full) with an operator ceiling and default
and a per-account choice within them, resolved in the capability layer for
the record and published on GET /api/auth/me as ai.usage: the level in
effect, the stored choice, the bounds, why (user, default, admin_cap), the
three options with whether each is allowed and an estimate of tokens per
day scaled from the record's last seven full days of coach_usage, and the
measured mean. A delegate sees the level, never the owner's spend.

PUT /api/auth/me/ai-usage stores the account's own choice or null for the
default, refusing a level above the ceiling with 422 ai.usage.aboveCeiling
and meta.maxLevel. The new supplementLabel capability needs standard or
above; below it the capability is unavailable with the new usage_level
reason, refused with 403 ai.usage.levelTooLow and meta.requiredLevel.

Refs #1217
2026-10-15 22:49:01 +02:00
Marc-André Bombeck e573f28ae8 perf(coach): keep the system prompt stable across turns and rounds
The opener hint, the first-name clause, resolved follow-ups, the table rules
and the final-round line used to be written into the system prompt, so the
cached prefix (tools plus system) was rewritten on every turn and again on
the forced final round. They travel on the user side now: the per-turn lines
in the user message, the table rules as a note after the round that produced
the first table, the final-round reason as the last user line. The system
prompt depends on the locale, the Coach preferences and the about-me text
only.

Refs #1217
2026-10-15 22:49:00 +02:00
Marc-André Bombeck c2245d1d79 perf(insights): send snapshots as compact JSON and size the cap against it
The status cards, the briefing, the derived assessments and the Coach
snapshot drop the two-space indentation from the JSON they hand the model.
The content is unchanged (the hash gate hashes the object, so stored hashes
stay valid). MAX_SNAPSHOT_CHARS is now measured on the compact string: 14 000
for the status cards and the Coach, which every snapshot that fitted the old
24 000 pretty-printed characters still fits, so the model reads the same data
for fewer tokens.

Refs #1217
2026-10-15 22:49:00 +02:00
Marc-André Bombeck 6e72de1c35 feat(db): add the v1.43 schema and drop the readable location columns
Migrations 0402 to 0410: AI usage levels, biomarker aliases and merges,
medication ingredients (with the supplement label run kind), life
circumstance fact kinds, AI sections and AUDIT-C, Dawarich context, the
pulse day-mean method, intraday hour buckets, the readable location drop
and five relation link tables. Every new column carries its reader or a
pending-consumer marker.

0409 refuses to run while any home, location period or stored day still
holds a readable location, and names the counts. Locations are read from
the sealed copy only; the encryption backfill no longer handles them, and
the demo seed writes sealed locations only.

The new tables are backed up and wiped, the new sealed columns are in the
rotation registry with labels of their own, the AI usage level and the
life-circumstance confirmations travel with the backup, and the wellbeing
score reads the mental-health screeners only.
2026-10-15 21:48:57 +02:00
Marc-André Bombeck 42b66b2228 Merge branch 'v1423/labs' into release/v1.42.3 2026-10-14 19:34:44 +02:00