Migrate the remaining hand-rolled card containers in the account,
about, medications, api, mood, notification-status, and advanced
sections to SettingsCard, preserving each card's heading, body, and
test hooks. Leaves the passkey list-row (rounded-lg) for the list-row
token track.
Swap the hand-rolled bg-card border-border rounded-xl border p-4 sm:p-6
container for SettingsCard across the single-card sections (integration
cards, notification channel cards, coach prefs, cycle, modules, mood
reminder, thresholds, dashboard-layout, about-me, low-stock, and the
health-record export panel). Drops the sm: padding split and the
primitive bypass; landmark sections keep their element via as="section".
Extend the demo tenant with body composition, blood glucose with meal
contexts, cardio vitals, recovery and sleep scores, and realistic workouts
with heart-rate samples, all reconciled against the existing series and
dated relative to now. Replace the stale achievement ids the prior seed
referenced with verified registry keys.
The per-worker in-process "last working provider" hint stays volatile
and is deliberately NOT folded into the durable Postgres health ledger.
The ledger already shares the load-bearing skip signal across workers;
the only uncoordinated piece is a soft reorder whose worst case is one
redundant first-hop per worker per hour. Promoting it to a per-request
ledger write would add hot-path write amplification for a negligible
saving. Documented inline so the trade-off is explicit.
The grounding, tone/banned-opener, GLP-1 dose-safety, metric-identifier
ban and forbidden-filler rules were independently encoded across the
comprehensive, status, Coach and narrative prompts and had drifted. Move
the canonical wording into `prompts/shared-contracts.ts` (locale-keyed
fragments) and compose each surface from it.
Where the surfaces had drifted, adopt the stricter wording: most notably
the GLP-1 dose-prescription contract — previously present only in the
comprehensive and Coach prompts — now reaches the status cards and the
period narrative too. A cross-surface coverage test asserts every
enforced fragment appears verbatim in every surface, in de and en.
Extend the demo tenant so it reads as a long-term, fully engaged user
across the whole app, not a thin sample. All series span the existing
ninety-day window with the latest point on today and sit in healthy
bands:
- Body composition (fat, fat-free, lean, muscle mass, total body water,
bone mass, visceral fat, BMI) derived from the same-day weight and
body-fat series so the numbers reconcile with the headline tiles.
- Blood glucose in a healthy non-diabetic band — fasting, post-meal and
bedtime readings stored in mg/dL with the glucose context tagged.
- Cardio fitness and vitals: HRV (SDNN and RMSSD), oxygen saturation,
respiratory rate, periodic VO2 max, active energy, walking/running
distance and flights climbed.
- WHOOP-style recovery, day strain and sleep performance/efficiency/
consistency scores aligned with the seeded sleep nights.
- Three to four workouts a week (running, strength, cycling) with
duration, energy, average and max heart rate, distance, a sparse
per-workout heart-rate sample series, and per-workout strain in the
metadata blob.
Clear the workout tables in the same truncate pass so the seed stays
re-runnable.
The demo seed unlocked twelve achievement ids that no longer exist in
the registry (first-measurement, week-streak-measurements,
three-month-streak, …), so the achievements surface rendered them as
unknown placeholders. Map the unlocked set to live registry keys —
first-entry badges, logging and in-range streaks, compliance and
miss-free streaks, plus the self-context and seven-night sleep badges —
each on a plausible unlock date the seeded history supports.
Wrap the ui Card so every Settings section composes from one bordered
container with the shared p-4 md:p-6 padding contract, replacing the
hand-rolled bg-card border-border rounded-xl border p-4 sm:p-6 div that
bypassed the primitive and stepped padding at sm: instead of md:.
The medication card and table render dose-derived stock from the cached
medications list, but the inventory write routes evicted no server cache
and the client invalidated only the per-item inventory key, so a new
container's stock kept serving the pre-write value for the stale window.
Hard-evict the user's medication caches on every supply write and
invalidate the list key alongside the inventory key on the client.
The /medications card and table render the dose-derived stock
(stockUnitsRemaining / stockDosesRemaining) straight from the
GET /api/medications list payload, which reads through cachedSwr on the
60s-fresh + 10-min-stale medications bucket. Registering, adjusting, or
deleting an inventory container changes that stock, but the inventory
routes never invalidated any cache — so the list served the pre-write
stock for the rest of the stale window. On the client the add / adjust /
delete supply mutations only invalidated the per-medication inventory
query, never the medications-list query that the card reads, so a mounted
list never refetched even once the server caught up.
Hard-evict the per-user medications + compliance buckets from every
supply-affecting write (inventory POST, the per-item PATCH / DELETE, and
the deprecated glp1 POST dose-change + ledger branches) so the next read
reflects the change rather than a stale SWR value. On the client,
centralise the supply mutations on a shared invalidateSupplyQueries helper
that drops both the medications-list key and the per-medication inventory
key. The card's neutral styling is unchanged.
Fold the three standalone module settings pages into the Settings shell so
they open in-context with the same left-nav, heading, and chrome as every
other section. Remove the standalone pages and the now-unused
ModuleSettingsFrame, and drop the orphaned settings i18n blocks.
The comprehensive generator did an ad-hoc JSON.parse with no corrective
retry and failed cold to `invalid-json` on a first-pass miss, while the
strict insight wrapper's `buildRetryCorrectionMessage` sat right next
door. Export that helper and reuse it: on a parse miss, append the
correction to the user prompt and re-run the chain once before declaring
failure. Unifies the JSON-robustness story across the two paths.
Promote the three per-module customise surfaces from standalone
ModuleSettingsFrame pages to first-class Settings sections. They now
render inside the Settings shell — same left nav, heading, and chrome as
account / ai / integrations / notifications — instead of their own
back-button-and-title frame outside it.
Add labs, illness, and vorsorge to the slug registry (after mood) and to
SETTINGS_SECTIONS with FlaskConical / Thermometer / CalendarCheck icons.
labs and illness gate on their toggleable modules; vorsorge has no
module, so its entry is always shown. Three thin section components wrap
the existing LabsSettings / IllnessSettings / VorsorgeSettings content,
mirroring MedicationsSection / CoachSection.
Delete the three standalone /settings/{labs,vorsorge,illness} routes so
the wrench deep links resolve through the dynamic [section] route, and
drop ModuleSettingsFrame now that nothing renders it. Add the section
title / subtitle / description keys across all six locales and remove the
dead per-module *.settings.* blocks the old pages consumed.
The content-hash gate re-stamps the prior text whenever the snapshot is
unchanged, so the "daily" briefing read byte-stable day-over-day. Decouple
phrasing freshness from data freshness for the dailyBriefing.paragraph
alone: on a hash-unchanged hit, re-roll just the paragraph once per
calendar day at a higher, seedless temperature, keeping every structured
finding verbatim from the cached payload.
A new `insightsBriefingRerollDate` column gates it to at most one extra
call per day, and a re-roll miss still stamps the day so a failing
provider cannot be re-driven on every page-open. The hash gate stays
fully intact for the status cards (reference assessments — monotony is
fine).
Recent in-band BP, stable healthy weight, a resting-heart-rate series, a
full week of per-stage sleep nights, on-track medication doses, two
preventive-care reminders, a healthy lab panel, one resolved illness
episode, and a sample Coach conversation — all dated relative to now so
the public demo reads current for screenshots.
The measurement-reminder push already carries the reminder id in its
metadata, but the iOS allowlist stripped it before it reached the device.
Forward the key so a foreground or handled notification can post the
completion directly instead of routing through the capture deep link.
Tune the public demo tenant for App-Store screenshots:
- Extend the measurement loop to include today so every headline metric
has a current reading.
- Retarget blood pressure into the optimal band (~118/76), weight to a
stable healthy BMI, and add a clean resting-heart-rate series so the
resting-pulse tile scores it directly instead of the spot pulse.
- Write a full week of per-stage sleep nights (awake/REM/core/deep,
~7.5h, source APPLE_HEALTH) including last night, in minutes, and skip
the aggregate row for those nights to avoid double-counting.
- Schedule today's doses on-track: morning meds taken, the evening dose
pending and never auto-missed, so today never reads overdue or 0%.
- Add Vorsorge reminders (dental check-up, annual physical), one healthy
lab panel, one resolved illness episode, and an AI-configured Coach
with a short encrypted sample conversation.
All dates stay relative to now so the demo refreshes on every re-seed.
Add an opt-in `responseFormat: "json"` to CompletionParams that the
structured surfaces (comprehensive briefing + per-metric status cards)
set. On local (Ollama / OpenAI-compatible) it adds `format: "json"`; on
Anthropic it prefills the assistant turn with `{` and re-prepends the
brace to the reply, so the first emitted token is already inside a JSON
object. Both cut the first-pass JSON-failure rate that fence-stripping
otherwise has to absorb.
OpenAI already pins `response_format: json_object` and is unchanged. The
period narrative (plain text) and the Coach (streaming prose) leave the
flag unset, so their non-JSON paths are untouched.
Add an optional `seed` to CompletionParams and thread it onto the
OpenAI and local (Ollama / OpenAI-compatible) request bodies. The field
is dropped when unset, so existing callers are byte-identical.
Pin a stable seed for the status/reference cards (defaulted in the
status provider) and the period narrative, so a prompt change is
diff-able against a fixed baseline rather than masked by low-temperature
non-determinism. Anthropic's Messages API has no seed knob, so its
client ignores the field (documented inline). The seed is annotated on
the narrative wide-event.
Fix the target-edit-sheet invalidation that used a bare ["insights"] array
not matching the factory prefix the reads nest under (a silent no-op), and
migrate 12 further bare queryKey call sites onto the centralised factory.
Replace the scattered `?? 0.3` / `?? 1000` temperature and maxTokens
magic numbers across the status provider, Coach route, comprehensive
generator and period-narrative generator with a single typed
`ai-budgets.ts` map. Each entry documents the output contract that
fixes its token ceiling.
Right-size the per-metric status output from 1000 to 250 tokens: the
card renders one `{ "summary": "..." }` of 2-4 sentences (30-60 words),
so 1000 was an order of magnitude over the real ceiling.
The target editor invalidated a bare ["insights"] array on save and reset.
That literal sat outside the centralised factory, so the broad insights
fan-out it intended never matched the reads, which key through
insightsTargets/insightsRoot. Route the editor's reads and invalidations
through userThresholds(), insightsRoot(), insightsTargets() and analytics()
so a saved target actually refreshes every insights surface.
Migrate the other call sites the lint rule does not reach onto the factory:
the Coach conversation rail + thread reads, the per-metric personal-records
badge, the per-day measurement drill-down, the dashboard intake
compliance-chart fan-out, the per-medication side-effects list, the
onboarding auth invalidations, the notification-preferences read, and the
PWA persister's auth-me lookup.
Add the matching factory keys: coachConversations, coachConversation,
personalRecordsByMetric, measurementDrilldown, complianceChartInline and
medicationSideEffects. Each new key reuses the prefix its reads already use
so no invalidation shape drifts.
The dev-only SHA-256 key padding was gated on the absence of a production
marker, so a staging or preview container with NODE_ENV unset silently
derived a weak key the operator never recorded — rotating to a real key
later orphaned those rows (decrypt is fail-closed). Gate the padding on an
explicit NODE_ENV of development or test; every other case, including unset
NODE_ENV, throws the same hard error as in production.
coach_messages is append-only and grows forever — a daily Coach user
accrues encrypted conversation rows with no lifecycle, while every
sibling ledger (push_attempts, audit_logs, host_metrics) already prunes.
Add a daily pg-boss cleanup that hard-deletes rows past a configurable
window (COACH_MESSAGE_RETENTION_DAYS, default 365, 30-day floor so a
misconfig cannot drop live in-context history). Wire it into the
maintenance registrar with the dead-queue contract guard and a focused
helper test.
The sliding-30-day session reaper and the passkey-challenge sweep both
filter on expires_at, but neither sessions nor auth_challenges indexed
that column, so the cleanup degraded to a sequential scan on a busy
instance. Every sibling expiry table already indexes its expiry column;
add the two missing indexes via migration 0178.
The floating Coach launcher inherited the button's dark foreground token,
which left the sparkle glyph as a dark outline on the purple-pink
gradient. The glyph is decorative and the button carries its own
accessible name, so the white mark reads cleaner without affecting the
control's non-text contrast, which the offset focus ring already
provides.
The expired-nonce callback test asserted the global fetch spy was never
called. A preceding successful-connection test fires its webhook
subscriptions fire-and-forget, and those promises resolve into the next
test's freshly installed spy, so the broad assertion failed whenever the
background calls landed in time. Filter the recorded calls to the token
endpoint and assert none reached it, matching the replay and cross-user
tests in the same file and keeping the check true to its intent.
KeyFindingRow and SignalRow rendered an identical tone bar + metric icon
+ headline/body/delta and the same link-when-routed behaviour, differing
only in their data-slot and body copy. Extract a single BriefingRow and
have both delegate to it.
The demo seed's random-walk BP generator could drift below the
physiological floor and mint a systolic-0 row (iOS #33). Clamp the walk
to the same band the input validator enforces (systolic >= 40,
diastolic >= 20), reusing the VALUE_RANGES floors, so a fresh seed never
writes a sub-floor reading. Add a read-side selection guard on the blood
pressure series endpoint that excludes rows below those floors from both
the series and the implicit latest point, so a corrupt row that slipped
in before the floor was enforced can never surface as the latest
reading. Export VALUE_RANGES so both sites share one source of truth.
Add aria-expanded to the coach drawer's history-collapse control so its
expanded state is exposed, matching the expand toggle. Give the Vorsorge
trend strip an sr-only summary of the latest value and seven-day
direction, and key the bars on measuredAt rather than the array index.
Remove the integration keys left dead after the integrations refactor:
the WHOOP and Fitbit overlap notes, the Fitbit experimental note, the
per-provider credentials-help strings (WHOOP, Fitbit, Polar, Oura) and
the Nightscout help. None carry a t() call site any longer. Add the
Vorsorge trend-strip screen-reader summary strings (latest value plus
seven-day direction) across all locales.
Add a coarseOnly option to buildTieredSeries that skips the raw 0–14d
read and the DAY (14–30d) / WEEK (30–90d) band reads, returning only the
MONTH / YEAR bands plus the anomaly envelope. The Coach snapshot's
coarse timeline tail consumes only month/year + anomalies, so the
near-term bands were read and discarded every turn; it now passes
coarseOnly. The briefing path is unchanged. Look the learn-guide prompt
example up by slug rather than by array index so reordering the catalog
cannot silently change the example link.
Drop steps from the signals-of-day set: Apple Health steps ingest as
many intraday stats-prefixed samples, so the newest raw row is a
partial-day fragment, not a daily total — same exclusion already applied
to per-night sleep. Add a glucose signal off the canonical stored value
(the model reads the snapshot, not display units) so the block matches
the priority comment that already claimed it; drop the dead sleep entry
from SIGNAL_PRIORITY. Track the raw extreme magnitude when keeping the
most extreme recent anomaly so a larger anomaly is no longer dropped by
comparing against an already-rounded value. Thread the injected clock
through trendSlope so the emerging-trend cutoff is deterministic under a
test clock. Rename the anomaly day-count to anomalyDaysAgo so the
snapshot hash staleness-tiers it instead of letting the raw count drive
the fingerprint. Cap the returned set at three to match the schema.
Wire the Coach snapshot's core-metric history onto the shared tiered
series so blood pressure, weight, and pulse carry a coarse tail the
in-window weekly fold could not produce — 90d-1y monthly and >1y yearly
buckets plus the preserved peak/trough envelope, so a spike from months
ago survives a coarse bucket. The tiered read skips the raw 0-14d band
the snapshot already holds, so no duplicate round-trip; the coarse tail
is bounded per band and sheds first under the prompt-budget degrader, so
token size holds.
Surface the active illness episodes and Rest Mode state to the Coach
prompt: the snapshot already carries the illness block, and the system
prompt now instructs the Coach to read an off-band vital as plausibly
illness-explained and avoid pushing a heavier cadence while the user is
in Rest Mode.
Add a static catalog of the public /learn guides and inject a compact
list into the system prompt. The Coach may reference at most one guide
by topic and URL when genuinely helpful, and only a slug the catalog
lists — a fabricated /learn URL is impossible by construction.
Every Settings → Integrations block now paints one consistent shape:
heading (icon + name + status pill) → a white primary description → a
grey secondary description that ends with the setup-guide link rendered
inline, instead of a standalone bottom-of-card element.
OAuth-only (WHOOP/Fitbit), BYO-credential (Withings/Oura/Polar) and
manual-key (Nightscout) cards share the same skeleton via the new
IntegrationCardDescription helper; the credential form stays as an extra
block where it applies. The "where to find these credentials" hint moves
above the Save button instead of sitting below it.
Rewrite the per-provider description strings across all six locales into a
parallel two-part shape with even length, and drop the now-orphaned
overlap/experimental sub-notes and the standalone setup-guide link.
Wave A removed the inline ask-the-Coach / suggested-questions block from
the overview hero (Coach is reached via the FAB + drawer). Update the
stale assertion that still expected onAskCoach/onPickPrompt + the launch
context on the page.
Remove the single-night "Last night" hypnogram from the sleep overview: its
per-stage breakdown (time + %) duplicated the phase-distribution stacked bar
directly below it. The /api/sleep/night route stays (the dashboard reads it);
only the redundant surface is gone.
Promote the chronotype to the prominent bottom block of the view. Split it out
of the rhythm section (now the sleep-debt headline alone) into its own section:
a large band label with the natural sleep-midpoint clock beneath it and an
expandable disclosure for social jetlag + MSFsc. It reads the same
["sleep-rhythm"] cache, so it adds no round-trip, and keeps the calm
"still learning your rhythm — N of M nights" state.
Fix the sleep-quality assessment slot. It disabled the status fetch whenever the
AI narrative was absent, so the card could not read hasProvider and fell back to
its no-provider default — surfacing the "connect an AI provider" CTA even for
accounts that have a provider connected, stacked under the grounded note. The
card now stays enabled and reports the truthful state: no provider shows its
clean AI-settings CTA alone; a connected provider with no text yet shows the
grounded note; warm or preparing AI text wins. Exactly one block fills the slot.
Redesign the Coach frontend toward a calm, prompt-first reading
experience:
- Narrow the reading column to a centred max-w-2xl with a roomier
vertical rhythm so the conversation reads like a document rather than
a packed log.
- Make the page-surface conversation rail collapsible and collapsed by
default. A single labelled toggle in the rail strip opens it (lg+),
the rail heading carries a matching close control, and the column
folds to zero width with a smooth grid transition. Below lg the
bottom tray is unchanged. The fullscreen header keeps only the
minimize control.
- Replace the boxy default scrollbar on the thread, history list, and
composer with a thin, rounded, subtle bar across WebKit and Firefox,
scoped to the components.
- Polish the pre-stream beat: the Thinking indicator now pairs bouncing
dots with the label, and the assistant turn fades in so the hand-off
to streamed tokens reads as one motion.
- Return voice dictation to the composer via the Web Speech API. The
mic mounts only where the browser supports recognition, dictates into
the textarea, and is keyboard- and screen-reader-accessible.
New labels land in all six locale bundles.
The score dials wore a token-tinted drop-shadow bloom, a conic specular
sheen, a breathing pulse on the green ring, and a dark-to-neon two-stop
arc gradient. Against the flat 2-D Recharts charts that read as a glowing
3-D gauge and made the overview restless.
Make the calm treatment the default everywhere, not only behind the `flat`
prop:
- Drop the bloom, the sheen, and the pulse for every ring. `.wellness-ring-arc`
is now a plain flat stroke; the sheen overlay paints nothing. The shared
cycle ring inherits the same quiet.
- Mute the arc colours toward the chart palette: the two gradient stops sit a
hair apart for a whisper of depth instead of a neon sweep, pulled down and
desaturated per hue in both the Dracula and Alucard sets. Band semantics
(green/yellow/red thresholds) are unchanged.
- `flat` now only drops the entrance sweep + count-up, so the dashboard hero
ring paints its value at once. The /insights strip keeps the subtle spring
fill; prefers-reduced-motion still zeroes it.
Score and band logic, thresholds, and the data-band/aria contract are
untouched.
Enrich the Vorsorge cards with a discreet 7-day trend strip and a path
into the underlying readings.
- A quiet seven-bar strip of the reminder metric's last seven readings
sits under the metric context on both the dashboard summary card and
the full-grid cards, mirroring how a dashboard tile carries a small
trend. Neutral muted bars, no status colour. A free-text reminder or a
metric with fewer than two readings shows nothing, so card height stays
stable. The strip reads a bounded last-7 window per type through the
existing measurements endpoint, cached by type under the measurements
invalidation prefix.
- The card kebab gains a 'Show measurements' item for measurement-linked
reminders that opens /measurements pre-filtered to that type.
- The measurements list accepts a ?type=<MEASUREMENT_TYPE> deep link that
seeds the existing client-side type filter while leaving the rail
interactive; unknown tokens are dropped. The ?add= behaviour is
unchanged.
- New labels added across all six locales.
Render the new signalsOfDay list at the top of the daily-briefing card — a
present-focused row (headline + concrete nudge) sharing the tone bar, metric
icon, and sub-page routing of the key-finding row, with matching typography
(text-sm headline, text-xs muted body) so the card reads consistently with the
rest of the insight surfaces. Add the signalsTitle string across all six
locales.
Remove the "Ask the coach" action button and the guided-questions chip strip
from the overview hero band: the Coach stays the bottom-right drawer mounted by
the insights layout shell. The hero is now greeting + subtitle + baseline meta
+ the optional Health Score panel, and no longer reads the coach flags or the
launch context. Update the coach-cascade / disable-cascade fixtures and the
hero-strip slot tests to pin the affordances absent.
Compute a present-focused "signals of the day" block in extractFeatures from
the in-memory measurement set (no extra query): the freshest reading vs the
user's own 7d/30d windows, the normal-swing verdict, an emerging slope, and a
recent anomaly per salient metric, ranked by clinical priority and capped at
five. Add a signalsOfDay array to the dailyBriefing schema feeding the
briefing alongside the existing keyFindings cap, and rewrite the briefing
prompt (EN + DE) to lead with NOW: a warmer, motivating read that surfaces the
emerging signal and gives one concrete, doable nudge while still drawing on the
tiered history.
The signals block lands inside the compacted features payload, so it also
seeds insightsSnapshotHash — a fresh daily signal now changes the fingerprint
and forces the daily 04:30 warm to regenerate instead of re-stamping a
days-old narrative. Bump PROMPT_VERSION to 4.29.0.