Commit Graph
9 Commits
Author SHA1 Message Date
Marc-André Bombeck b36c7b6697 feat(insights): serve status notes from the new table, only while available
Every status note reader and writer moves to InsightStatusCache: the
generators, the unchanged-data gates, the stale-while-revalidate read,
the ingest debounce, the previous-note prompt block and the
medication-compliance card with its per-medication lines. A stall now
opens a negative-cache window next to the note instead of a stub row
that shadowed it.

Stored model text is never served while its capability is unavailable,
whatever the reason: the status readers and gates check statusText, the
period narrative hides a model-written row while periodNarrative is
unavailable (the deterministic narrative is data and always shown), and
the daily digest drops reaction lines while reactionLines is
unavailable. A read-only miss now reports why (`unavailable` with the
capability's reason) and keeps hasProvider meaning provider presence, so
a withdrawn consent no longer reads as "no provider".

runStatusCompletion takes the capability as a required argument and
re-checks it before the provider chain is resolved, so a job queued
before a switch flipped still stops at the wire. The comprehensive
briefing re-checks briefing the same way. hasUsableStatusProvider is the
one presence probe.

Refs #975
2026-09-24 09:06:22 +02:00
Marc-André Bombeck 97582dedc0 feat(mood): capture the five values without lengthening the quick check-in
- the five faces are untouched: pick one, save, done in a tap
- a closed section below them opens onto five sliders; picking a face
  suggests a pleasantness value and moving that slider takes the
  suggestion over, while the face selection stays where it is
- an untouched slider writes nothing, and clearing one takes the answer
  back rather than leaving a middle value behind
- the list's edit dialog shows and changes the same five, so nothing is
  written on one screen and invisible on the other
- correcting an entry no longer re-derives pleasantness when the label
  did not move, which used to overwrite a hand-set value on any edit
- factor ratings are no longer offered on a new entry and nothing was
  deleted: existing ratings still render, edit and feed the crosstab
- six locales, and the dimension table's keys are declared dynamic
2026-08-08 01:24:04 +02:00
Marc-André Bombeck 1ae137eb84 test(guards): sweep the directories the structural guards were skipping
`fs.globSync` never matches a leading dot, so every guard built on
`**/*.{ts,tsx}` walked past `src/app/.well-known` — the two OAuth
discovery documents and the apple-app-site-association handler sat
outside fourteen frozen-set sweeps. An unlisted `getSession()` planted
in the authorization-server route left the session-surface guard at nine
passed; it fails now.

Move those fourteen onto one shared walker built on `readdirSync` with
`recursive`, which has no dot rule, and give it a floor every caller has
to state, so a walk that comes back too small throws instead of
reporting a clean tree.

Give the same floor to the guards that compared a discovered set against
a frozen list with nothing asserting the set had been read: gutting the
walker in the inventory-write guard left its invariant test green,
because zero offenders reads exactly like no offenders. Raise the
migration-count floor off one, and assert the locale bundles are found
before the per-locale cases are generated from them.

Document, at each walker that prunes on purpose, what is pruned and that
dot-prefixed directories are not.
2026-08-02 16:21:19 +02:00
Marc-André Bombeck d8a3c08e12 fix(i18n): stop one key template from whitelisting a whole namespace
The reverse-coverage guard asserts that every key in the bundle is reachable
from a call site. It had stopped doing that for most of the bundle. A template
whose only static anchor was a namespace root registered that root as a subtree
base, so a single per-provider OAuth key marked every key under `settings.*`
reachable, and the same shape did it for `measurementReminders.*`. A cache-key
string that happens to start with a namespace word had quietly done it for
`insights.*` too. The guard stayed green throughout, which is the part worth
noticing: a pair guard degrades into a one-end guard without any signal.

Extract the matcher into a helper with its own unit tests, and change what it
accepts. A template carrying both a literal prefix and a literal suffix now
records the pair and requires both ends to match, so the OAuth template covers
the provider keys and nothing else. A template with no suffix whose anchor is a
bare namespace no longer whitelists anything — it fails the guard by name, with
the two honest remedies in the message. The same door is used by the template,
assignment and concatenation arms, so none of them can reopen it. Template
bodies are read by scanning forward from a known opening backtick rather than
by pairing backticks across the tree, which was drifting and dropping whole
files out of the scan.

Three call sites build their keys whole instead of interpolating onto a
namespace root: the two measurement-reminder due helpers, the mental-health
option labels, and the OAuth error copy, which now names its per-provider base.
Two audit-log action names that merely look like keys carry a written
exemption; the API error catalog is a runtime lookup and is allowlisted with
its reason.

With the matcher honest, 149 keys had no call site left. They belong to an
explainer popover, a hypnogram, a passkey table and an insights hero that were
all rebuilt or removed, and they are deleted from all six locales. The
explainer guard stops pinning its title leaves, which no surface has rendered
since the popover went.
2026-07-25 16:23:58 +02:00
Marc-André Bombeck b20f1b5c66 fix: harden imports and daily medication state (#577) 2026-07-21 13:08:28 +02:00
Marc-André Bombeck 323b111a24 Resolve plural tiers through the i18n context
Importing the plural helper directly from the dashboard trend tile cost
~78 KB gz across the route chunks: the module is tiny, but a new edge
from a widely-instantiated component gets duplicated into every chunk
that contains it rather than hoisted. The provider is already in every
chunk, so expose a plural-aware tCount() there and let components call
that instead. Total client JS goes back under budget.

Also fix the two tests the honest-loading-state change invalidated: the
mental-wellbeing landing tests seeded a never-resolving read and so now
render skeletons rather than instrument cards, and the achievements page
needs a router in scope for its module-gate redirect.
2026-07-19 06:39:29 +02:00
Marc-André Bombeck c23f2545cc Give every locale its own strings on the non-prompt surfaces
Four locales received English on surfaces whose translations were
already complete, because the resolving code was a de/en binary.

- Medication reminder push and the Telegram inline-keyboard labels
  admitted only de/en, so es/fr/it/pl got the English template on every
  channel. Route the resolver through a shared coerceLocale().
- The Coach's trailing "reply now in X" clause said English for any
  non-German locale, overriding the correct system-prompt directive.
  Read the language name from a shared map instead.
- Coach refusal copy was a de/en constant pair, answering four locales
  in English at the one turn the Coach is declining to help. Move the
  copy into the bundles; the EN/DE constants now derive from them.
- The sleep-stage chart formatted axis ticks as en-US for four locales;
  the cycle calendar and the device-score tile followed the *browser*
  locale rather than the app locale.
- Three components carried a byte-identical duration formatter that
  branched on German. Lift it to one key-driven helper.

Polish needs a third plural tier: 2-4 takes a "few" form a two-form
scheme cannot express, so "2 godzin temu" read as "2 of-hours ago".
Add a pluralKey() helper driven by Intl.PluralRules and a Few tier to
the relative-time and stale-hint keys. The five locales whose rules
have no "few" category resolve exactly as before by construction, and
their Few values mirror Other. Teach the reverse-coverage guard to
expand a pluralKey() base into its three tiers.
2026-07-19 06:02:27 +02:00
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00
Marc-André Bombeck d50e277a2b chore(i18n): remove orphaned translation keys and add a reverse-coverage guard
Delete 177 translation keys that no call site reaches — across all six locales
— covering the medications, cycle, labs, notifications, nav, targets, and
admin namespaces that drifted out of use as their surfaces were rebuilt. The
forward call-site guard only catches keys referenced from code that never
shipped to the bundle; nothing watched the reverse, so orphans piled up.

Add a reverse-coverage test that walks messages/en.json and asserts every leaf
is reachable: a literal t() key, a subtree returned by t("ns"), a dynamic key
built inside a t(`…`) template or by string concatenation (the per-metric
${i18nPrefix}.* pages, cycle.calendar.flow${LEVEL}, medications.site${Suffix},
"notifications.event" + type), or an explicit allowlist for DB-driven keys
(mood.tag / mood.tagCategory). A new orphan now prints as a punch list.

Drop the unrendered admin.carrierUnknown and dashboard.bpInTargetShort from the
drift / trend-card guards — both lost their call sites and were removed here.
2026-06-18 18:47:05 +02:00