The dose-history purge was the one destructive verb in the intake family
that removed rows outright. Both siblings, the single-event DELETE and the
bulk delete, stamp `deletedAt` and bump `syncVersion`; the purge called
`deleteMany`, so a mis-tap was final and a client offline at purge time kept
every row forever, because a row that no longer exists produces no tombstone
for the sync feed to carry. It tombstones now, scoped to live rows so a
second purge counts zero. No inventory refund: a purge wipes a history as
bookkeeping, and refunding every dose ever taken would inflate the current
container count instead.
The manual water quick-add and the lab-result un-delete both wrote health
data and reached only the wide event, which expires. Months later nothing
could answer who put a day's total there or who brought a reading back, and
with a second person able to act on a shared record that question has a
second possible answer. Both write a durable row now, named after the
neighbours they sit beside.
The refused-delegation audit comment claimed the row was written for the
owner's benefit. It is not: it is filed under the caller, it is only written
for a header refusal, and a header names an account the caller merely
asserts. Filing under that claim would let anyone write rows into any
account's activity feed by naming it. The comment says what the row is now,
and points at `recordDelegatedAccess`, which is the half the owner does see.
Two comments in the biomarker surfaces still claimed the `SetNull` foreign
key left the readings behind when a marker is deleted. That stopped being
true when delete became a "drop this and its history" action and the
confirmation copy was rewritten in six locales to say so.
The purge route gains a call-shape assertion rather than a count assertion,
because a count comes back identical from a `deleteMany`. The lab restore
route had no test at all and now has one.
Sharing lived in a settings panel and a menu three levels deep. That is where
a grant is managed and not where it is used: somebody who opens this app every
morning to check on a parent does not go through settings to do it, and the
design standards draw the same line, settings being account utilities rather
than feature destinations. So the dashboard carries one compact card listing
the records shared with this account, each row a one-tap switch through the
existing flow.
It is a doorway and not a preview. No numbers, no last reading. A slice of
somebody else's record on the delegate's own dashboard would sit outside the
one frame the design leans on, the banner naming the record you are inside.
An account nobody has shared with sees nothing at all.
The other half is the receipt: a reading, a lab result or a dose recorded
under a switch now says which record it landed in, because "Saved" alone is
the confirmation somebody acting for another person does not need. The undo
on the dose toast goes at the same time, since a delegate cannot remove what
they recorded and an undo they can see is one the server refuses.
Also here: the lines the owner's activity view owes each admitted verb, as a
translated mapping with a fallback, so a later release filing a verb it has
never heard of still renders a sentence rather than a blank row.
Every page a switch can reach now renders the controls the grant covers and
none of the others. Readings, lab results and illness entries keep their add
path at WRITE and lose edit, delete and selection at both levels. Mood,
screeners, cycle days, checkup reminders and the document vault lose every
mutation control at both levels, because no grant admits those verbs and the
routes behind them refuse a switched session outright.
Absent, not disabled, throughout. The banner already says whose record this
is, so no chip repeats it beside each missing button; the one exception is
the measurements empty state, where somebody who may add will look for the
edit control beside the row they just entered, and one muted line says where
the limit is.
Two dead ends went with them: the custom-metric section on the measurements
page linked every row into a route the shell turns a delegate away from, and
the wrench glyphs point at settings pages sharing does not cover.
Several accessibility scans ran against a page that had not finished
arriving, so axe reported violations the app does not have. The worst of
them was the medications case, which waited for a level-one heading and
then checked that a level-one heading exists. A page that really lost its
heading would have waited out the timeout under a misleading name instead
of failing the rule, which makes it a check that cannot fail rather than a
flaky one.
The filter rail was the other bad sentinel. It renders beside the loading
silhouettes rather than after them, so it was on screen while the rows were
still grey rectangles. Under an 8x CPU throttle the rail and the loading
marker resolved in the same 40ms poll and the rows a poll later.
Every case now gates on a marker the real component carries in its
resolved, non-empty branch, and on a component other than the one the scan
asserts. Both the desktop table and the mobile card list carry the same
marker so the wait holds at any viewport. No sentinel names a heading, the
app frame, a busy marker or the filter rail any more.
Six integration cards and the lab-OCR dialog keyed their tone off `res.ok`.
A sync that imported nothing because every reading was refused rendered a
green tick and a count, and a re-scan in which every confirmed row turned out
to be a duplicate raised a green toast saying zero readings were saved.
Each surface now renders the outcome the route resolved: a tick only when
everything landed, a warning when some of it did not, an error when none of
it did, and a neutral note when there was nothing new. The lab-OCR toast
picks its variant the same way.
Replaces the directory-scoped import guard with one that covers the whole
component tree and pins per-file occurrence counts, so a listed file cannot
quietly add a second tick. Its docstring states what it cannot catch.
A dose logged from another screen marked the Today hero and the dashboard
stale without re-reading them, so returning to the home page kept showing the
old state until the 120 s poll or a hard reload. The daily reads run with
refetchOnMount off, so a bundle invalidation with the default active
refetch-type never touched them while they were unmounted.
Route every intake-affecting medication write through one blessed
invalidateMedicationReads (bundle invalidation plus a forced inactive refetch
of the digest and snapshot): Take all due, the intake edit dialog, the history
bulk/row delete, the dose-history ledger and its add dialog, the lifecycle and
danger-zone actions, and the intake import. Give the Vorsorge reminder
mutations the same daily-rail refresh, bust the biomarker assessment and the
labs-changes read after a lab write, and refetch the daily reads after a
briefing regenerate so the Today lead updates without a reload.
Restore the dashboard-layout concurrency belt: the settings layout query now
always loads its own tokened copy, so a home-page visit can no longer strand a
save without its base token and silently drop the v1.32.16 conflict guard.
Add a structural guard that fails CI when a new site invalidates a daily-reads
bundle without the paired inactive refetch, so this class cannot recur a
fourth time, plus behavioural coverage for the batch take and the Vorsorge
refresh.
Refs #581
Sweeps the banned gap-5/space-y-5/pl-5 spacing residue the audit found
scattered across coach pages, settings coach cards, the labs/records/
custom-metrics form family, onboarding, and a handful of one-off
surfaces (glucose panel, clinician view, illness correlation card,
invite-tokens, the mental-health check-in wizard including its stray
sm:p-6 shell steps) down onto the 4/6 scale.
Two medication chart colours (dose-strength-curve, drug-level-chart)
painted raw --dracula-purple with no light-theme override; both now
read --chart-1, the same alias the mood-chart fix already established.
Raises ~40 sub-text-xs muted-prose sites below the UI-STANDARDS §3/§5
floor (settings API/MCP/sharing/coach-prefs cards, insights workout
detail, suggested prompts, tab-strip group popovers, sources rail,
metric-stat-strip, the health-score provisional pill) to text-xs,
leaving genuine tabular-nums Badges untouched — the one exemption the
standard carries.
Routes the 10 hand-rolled settings/insights "no data yet" paragraphs
(coach memory, coach reminders, security sessions/activity/keys, API
tokens, sharing, insights pill order, per-metric overrides, nutrient
intake) through the shared EmptyState primitive instead of a bare
muted <p>, so they read the same, centered, icon-ready shape every
other empty list in the app already uses. The notification-status
card's empty copy stays on SettingsCardHeader's description slot —
converting it would mean restructuring the card, not routing it.
A real lab report is 10-20 analytes sharing one blood-draw date,
usually in the past; the add form closed on every save, resetting
takenAt to now and clearing the biomarker pick, so entering a full
panel meant re-setting the date away from now on every single row.
The new button keeps the sheet open, clears the reading, and
deliberately preserves takenAt (and the biomarker pick when not
locked to one detail page) so the next analyte in the same panel just
needs a pick + a value.
Sweeps every surface that renders a date from before this year without
a year suffix — illness episodes/flares/day-logs, lab and custom-metric
charts, the mood chart family, medication titration/side-effects/dose-
history/next-dose captions (incl. the Dec→Jan rollover), cycle
predictions and history, the BBT chart, the intraday-pulse day-nav, the
date-range filter chip, and the nutrient/compliance charts. Each now
routes through dateShortSmart / dateWithWeekdaySmart (format-locale.ts)
or the matching date-format.ts helper, which add the year only when it
differs from the current one — mirroring the workout list's existing
model instead of a bare "19.02." that reads as this year's.
Also fixes two sites that bypassed the app's locale entirely: the
trusted-devices expiry (bare toLocaleDateString, host locale) and the
admin token-name timestamp (hard-coded de-DE + Europe/Berlin) now both
render through the user's own locale, hour-cycle, and timezone
preference.
Two directions, folding the vault and labs pipelines into one linking path
without weakening either manual flow.
Vault document → lab values (auto-stage). After the per-document auto-index
job transcribes a stored document, a lab-looking document is run through the
same inbound extraction the manual "Extract" button uses, but the facts land
PENDING for the existing human review — nothing is committed. Gated on both
the documents and labs modules plus the auto-AI-read consent gate the
auto-index external path already uses; a text-structuring pass over the
transcription the index just produced, so no second vision call is spent. The
staging is a guarded STORED→EXTRACTED transition, and a keyword heuristic (or
an explicit LAB_RESULT kind) keeps an arbitrary photo from burning a call.
Everyone without the opt-in keeps the manual extract button.
Lab scan → vault. A lab report scanned in the labs area is now also filed
into the Documents vault (kind LAB_RESULT) through the existing upload —
encrypted at rest, EXIF-stripped, thumbnailed, sha256-deduped — and the
committed labs are cross-linked back to it with the same APPROVED-observation
provenance the manual vault → labs path produces. Text-mode scans keep the
image on-device and file nothing. Idempotent: a re-commit inserts no rows and
links nothing new, and the vault dedupe means no duplicate document.
Owner-scoped and module-gated on both sides; the cross-link is best-effort so
it never fails the authoritative write.
- Lab groups not backfilled to a catalog biomarker now carry a muted
'not linked to a biomarker yet' affordance in both list and card views, so
an inert row no longer reads as a broken link next to clickable neighbours.
- The biomarker detail page links to the document vault filtered to
lab-result reports (gated on the documents module), giving a path from a
value to its source scans. A per-value document relation remains a
data-model follow-up.
- Admin SettingsToggle: name the Radix switch via aria-label so it no longer
announces as a bare 'switch, on/off' (one shared component, six call sites).
- Replace the phantom text-destructive-foreground / text-warning-foreground
utilities with the vetted destructive pattern (bg-destructive text-white
dark:bg-destructive/60) and a readable foreground on the warning wash — no
more dead classes riding on cascade order.
- Drop the sub-AA opacity tiers: lab sparkline + titration marker off
muted/70, state icons (query-error, chart empty/error, med sort) off
muted/60 to the full token so they clear the light-theme floor.
- Chart range toggles expose aria-pressed (lab, custom-metric, health, mood,
compliance) so the active window is conveyed to assistive tech.
The dynamic /i18n/[locale] route handler answered with Next's default
Cache-Control (private, max-age=0, must-revalidate) and no validator —
its immutable-cache header was overridden, so a non-PWA browser would
re-download the full catalog on every hard load. Static public/ files
keep an ETag (repeat loads revalidate to a 304) and the service
worker's cache-first rule covers the offline relaunch; the ?v= build
key the layout appends busts both per deploy.
scripts/generate-i18n-boot.mjs writes public/i18n/<locale>.js for all
six locales as a prebuild/predev step (the sw-version.js pattern);
output is gitignored. The route handler is removed.
Measured on a production build, authenticated DE session: dashboard
document 522 KB raw / 139 KB gz -> 170 KB raw / 24.6 KB gz; the DE
catalog moves into a one-time 119 KB gz cached asset.
H2
Two independent every-load costs, one per layer:
1. The static EN import in load-locale.ts put the full en.json catalog
(334 KB raw / ~106 KB gz) into the client JS of EVERY route, for
every user in every language. No catalog is statically imported by
client code anymore: the EN fallback floor of the t() chain loads
lazily on the first actual key miss (the locale-integrity guard
keeps every locale key-complete, so the miss path is drift-only),
and the labs biomarker canonical-name lookup fetches EN on demand.
2. The root layout serialized the ENTIRE active-locale catalog into the
RSC flight payload of every document (392 KB of a 505 KB dashboard
HTML, re-downloaded on every hard load, uncacheable). The handoff is
now a versioned, immutable-cacheable boot script
(/i18n/<locale>?v=<build>, new route handler) that assigns
self.__HL_I18N before the hydration scripts run, plus a server-side
cache seed in load-locale.ts behind a typeof-window branch that
Turbopack compiles out of the browser bundle (verified: zero catalog
bytes in any client chunk, zero route manifests referencing one).
SSR text stays correct and the first client render holds the same
bundle the server rendered with — no EN flash, no hydration drift.
The boot script is public (proxy allowlist: the login page needs it
pre-auth; the body is the same catalog JSON that ships in the public
repo) and cache-first in the service worker, so an offline relaunch
finds it next to the cached shell.
Measured (client-reference manifests, gzip): route JS baseline
/ 391->287 KB gz, /insights 377->273, /measurements 379->275,
/insights/mood 394->290 — the ~104 KB gz EN catalog left every route.
H2
Every next/dynamic chart import used to point at its own module, so
Turbopack emitted a separate chunk group per boundary — the production
build carried EIGHT ~312 KB copies of the recharts module graph, and
/insights (the worst-LCP route) downloaded two of them (~92 KB gz of
pure duplication, one fully unused at load).
All chart loaders now resolve the single chart-runtime barrel
(src/components/charts/chart-runtime.ts), which owns the only path to
recharts: one shared chunk group, recharts exists exactly once in the
build (verified: 8 -> 1 recharts-fingerprinted chunks), cached across
routes. Charts stay recharts and stay pixel-identical; every call site
keeps its own matching skeleton and ssr:false flag.
Riders of the same rule:
- the mood-trio barrel (mood-charts.ts, the v1.16.7 precedent this
generalises) folds into the runtime and is removed
- the vitals sparkline body extracts to delta-sparkline.tsx and loads
through the runtime; the tile keeps the fixed 40 px band, so the
async gap is an identically-sized empty box
- /cycle's BBT chart (the last static recharts import on a live route)
defers through the runtime with a card-matching skeleton
H3
Residual grey-on-content sweep after the page-description lift:
multi-sentence explainer paragraphs are content and read in the
regular foreground, matching the metric sub-page explainer that set
the pattern — the labs biomarker and custom-metric detail leads, the
recovery metric and resilience explainers, and the onboarding
carousel/done-screen body copy. Muted stays reserved for meta:
timestamps, units, captions under a stat, one-sentence description
slots.
Eight data surfaces still rendered a read failure as a bare
text-destructive paragraph with no retry: the biomarker manager, the lab
values list, the three record managers (allergies, family history,
conditions), the illness episode detail, and the intake history table;
the lab biomarker detail carried an inline near-duplicate of the
primitive from before it existed. Replace all of them with
QueryErrorCard wired to the query's refetch, keeping each surface's own
error string. Inline role="alert" text remains the pattern for mutation
errors inside forms.
The Card has been a gap-based flex column since v1.4.33 (gap-4 md:gap-6;
CardHeader carries no bottom padding), but 29 CardHeader pb-0/1/2/2.5 and
8 CardContent pt-6 overrides from the padding-era shadcn card survived.
Under the gap model they invert their intent: pb-* renders gap PLUS
padding (looser, not tighter), pb-0 is a no-op, and pt-6 on a header-less
CardContent stacks on the Card's own py-4/6 and makes the card top-heavy
(40-48 px top vs 16-24 px bottom). Delete them all; a genuinely tighter
header-to-body distance is expressed on the Card itself (className="gap-2"),
the only knob that controls it. An ESLint guard for these slots is a
follow-up.
Add a PageHeader primitive (one H1 vocabulary, optional description,
back-link, and action slots) and route the hand-rolled headers through
it, so every module reads with the same title/subtitle rhythm and no
icon beside the page title. Mental Wellbeing gains a visible header.
Add a QueryErrorCard and wire the list surfaces so a failed read shows
a retry card instead of falling through to the empty state, and swap
their full-page spinners for skeletons that hold the layout.
The mental-wellbeing instrument card carried a font-medium title and a
gap-4 body, drifting from the medication card it should mirror; bring its
title to text-lg font-semibold and its body to the shared space-y-3.5
rhythm.
Several insight grids left a lone trailing tile stranded at half width
with an empty column beside it. Gate each multi-column grid on having
more than one item so a single tile spans the full row, and give the
fixed five-card mood-correlation grid a trailing-card span so it no
longer orphans on the two-up breakpoint.
Add a little bottom breathing room to the device-score tile so its
rolling-average line no longer reads flush against the card edge.
Resolve the catalog slug from both the current-locale display name and the
English canonical name. A marker stores the catalog name as it read at
creation time, so one minted under the English default and then viewed in
another locale never matched a map built only from the current locale's
names — its rich per-biomarker description silently fell back to the generic
line. The English bundle is always in the client chunk, so adding its names
makes the lookup independent of the locale the marker was created in.
Tidy the header control cluster:
- Drop the standalone "adjust target range" control and its sheet; the
reference bounds are already editable inside the full marker editor behind
the pencil. Remove the now-dead handler, mutation, and `targetRange.*`
strings across all locales.
- Give the show-all-values, edit, and delete controls one shared ghost-icon
treatment instead of the show-all control's separate muted variant.
- Add hover tooltips to edit and delete, matching the other icon controls.
- Order the controls left → right: delete, edit, show all values, add value.
The insights alert cards computed medication compliance with the naive
`schedules.length × days` denominator, so a fully-adherent weekly
injectable read ~14% and fired a false low-compliance alert (and a paused
medication mis-fired the same way). Route the alert path through the
canonical `calculateCompliance` engine like every other compliance
surface — pause eras, schedule revisions and dose windows honoured.
Document confirm: committing a numeric observation against an existing
biomarker silently replaced the stated unit with the catalog's, with no
conversion — a stated "6.1 mmol/L" against a mg/dL marker committed
"6.1 mg/dL". Reject on a unit mismatch (fail closed) so the user
reconciles on the review screen, the same posture as the unit-required
guard.
Wire grip strength, waist (with waist-to-height) and pain into the
briefing feature set and the grounding allow-set so the clinical-depth
signals reach the daily narrative; the mental-health screeners stay
excluded by construction.
Biomarker detail: fall back to a generic description for catalog-less
markers, and keep the heading + description with a retry on the error
state. Collapse the medication External-API and Lifecycle groups by
default. Re-enable the insights scroll-to-top regression and assert the
shipped main-content reset.
Align the EN haemoglobin/haematocrit display names to British spelling.
Drop the redundant `(userId, deletedAt)` index on inbound documents — a
strict prefix of the new three-column index. Trim the dead
MedicalDisclaimer variants to the one live `dataPosture`. Move the
crisis-resources card off the retired amber tint to a calm
destructive-subtle border. Add a registry invariant asserting every
biomarker signal key resolves to a real catalog slug.
Bring the lab biomarker detail in line with the Insights metric pages:
- Lead with the static catalog description directly beneath the heading,
the explainer-caption position the metric pages use.
- Drop the early big-value + reference-badge block that did not match the
assessment-card styling.
- Split numeric and qualitative markers: numeric keeps the stat strip and
chart, qualitative shows only the latest result text (no strip, no chart,
no range badge). The assessment card now renders for any marker with
readings and self-suppresses for qualitative-only markers.
- Move the full reading history off the detail page onto a dedicated
/labs/[biomarkerId]/values route, reached by a top-right list control,
matching the metric pages' show-all-values affordance.
The detail page fetched a single limit=500 window and sorted client-side,
silently truncating any marker that crossed 500 readings. Switch to an
offset-paginated useInfiniteQuery: it loads a page at a time, a 'Load more'
control reveals the rest, and the chart and stat strip render the
accumulated set so loading more extends the trend.
Add coverage for the new biomarker-assessment seam and the marker detail
surface:
- the generator serves today's cache, re-stamps unchanged readings without an
LLM call, and regenerates only when the latest-reading fingerprint changes
- the read-only endpoint returns the envelope, 422s a missing id, and honours
the auth + insightStatus gates
- the detail page paints the stat strip, the chart placeholder, and the
assessment card
- the biomarker resource route drops the marker with its readings in one
userId-narrowed transaction and rejects an inverted target range
Hold the input gate independent of the worker's forced run so an idle marker
re-stamps cached text instead of regenerating, and list the new route on the
non-Coach gate allowlist.
Give each biomarker a detail page that matches the metric sub-pages: a
numbers-first Min/Max/Median/Mean strip over the numeric readings and a
data-grounded AI assessment card.
The assessment is served by a new read-only endpoint
GET /api/insights/biomarker-assessment?biomarkerId=&locale=, returning the
same envelope as /api/insights/metric-status so InsightStatusCard consumes
it unchanged. A new generator reads the marker's LabResult history and
reuses the shared provider runner, the stale-while-revalidate read-only
path, and the persist helper. Regeneration is gated on a fingerprint of the
latest reading, so the assessment is rebuilt only when a new reading lands;
an idle marker re-stamps the cached text without an LLM call. The
biomarker: scope rides the existing on-demand status queue.
The stat strip and the assessment card both self-gate on numeric data, so a
qualitative-only or brand-new marker paints neither.
Add a control beside the marker actions that opens a focused sheet for
the reference low and high only, so a user can tune the band without the
full marker editor. Persists through the existing biomarker PUT and
invalidates the detail, catalog, and result reads.
The per-marker catalog description used to render on every labs overview
row, both in the list and the card layout. It reads better as a caption
beneath the detail-page heading, mirroring the metric pages' explainer
line. Resolve the catalog slug from the marker name and fall back to the
user's own context note.
Convert the per-biomarker reading history from a static reverse-chronological
list to a shadcn Table with clickable sort headers. Reuse the shared
useTableSort hook and SortableHead chrome already backing the measurement and
mood lists; the sample-date column opens descending, value/range/note open
ascending. The sort runs client-side over the in-memory reading feed.
Add isError branches with a retry affordance to the mood, illness, and
vorsorge lists so a failed read no longer renders as "no entries yet"
and invites duplicate re-entry, matching the labs and cycle pattern.
Make the admin backups table reachable on a phone with a card fallback,
name the admin language and timezone selects and the AI raw-data switch
for screen readers, and replace the touch-unreachable title= help on the
glucose clinical panels with a tap-capable info popover.
Route the password-strength label through the AA-tuned semantic tokens,
underline the register login link so it is not distinguished by colour
alone, give the labs load error alert semantics, and explain why the
target editor Save button is disabled with aria-invalid and a live
alert.
Standardise the labs marker subtitle to text-xs across both the compact list
and the card grid, and build the localized name->slug lookup map once per
locale instead of re-scanning the catalog seeds per group on every render.
Add relative to the mobile section strips in the settings and admin shells so
the active chip's offsetLeft is measured from the strip, giving an exact
horizontal scroll target.
Drop the orphaned insights.sleep.headlineCaption and headlineCaptionSuffix
keys from all six locales; nothing references them after the sleep headline
removal.
Surface a short, factual subtitle under every catalog-linked lab marker on
the Labs page, mirroring the descriptive line the Insights overview carries
under its metric headings. The text states what each biomarker measures,
sourced from the seed catalog via a new labs.catalog.desc.<slug> i18n map in
all six locales.
A reading's analyte resolves back to its catalog slug by a case- and
whitespace-insensitive match against the localised catalog name; free-text
markers with no catalog match carry no subtitle rather than a fabricated one.
Rendered in both the compact list view and the card/tile view.
Code-review and design follow-ups before the v1.22 cut. No behaviour change
beyond the items below.
Coach / insights:
- Coach SSE route: a non-provider error raised inside the open producer (e.g. a
DB failure persisting the reply) now records on the wide event and forwards to
GlitchTip instead of masquerading as a provider outage; it still surfaces an
error frame since the stream is open.
- Lab ↔ outcome correlations: hedge faint-tier pairs ("a faint hint, if
anything … too small to lean on") so the narrated confidence never outruns the
effect, matching the daily engine.
- Reminder sweep: mint the plan-review reminder and clear the plan's reviewDate
in one transaction so a half-applied write can't duplicate the reminder.
- Fold the two raw provider-fallback runners onto one shared chain walker.
Design / accessibility:
- Lift sub-44px tap targets to the 44px mobile floor (suggested-action card,
Coach bubble affordances), brighten the resting timestamp clock for non-text
contrast, and route the lab chart height through the shared constant.
- Replace ad-hoc arbitrary type sizes with the scale token and static size-11
icon buttons with the named icon-lg variant.
- Settings: add the missing About card-header descriptions, normalise the
low-stock numeric widths, and relax the notification-status rows from nested
cards to a divided list.
Housekeeping:
- Drop three unused exported types (and the now-orphaned source const), re-stamp
v1.21.5 comments to v1.22, and register the live-judge eval harness as a knip
entry so the script-only module resolves.
Replace the hand-rolled native scan dropzone button with the shared
Button primitive, swap the inline-style chart height for a height
class, and remove the Learn link from the biomarker value view where it
does not belong.
Rebuild the biomarker manager as compact rows (name + unit inline,
reference range / panel on a muted sub-line) with a hide toggle, an
edit button, and a delete, replacing the one-card-per-marker layout.
Hidden markers collect in a separate restorable section. Filter hidden
markers out of the lab-entry picker, keeping the currently selected one
visible so an existing reading still resolves its label.
Touch targets meet the 44px mobile floor (size-11 sm:size-9).
Add a `hidden` boolean to the Biomarker model (migration
0195_biomarker_hidden) so a marker the user no longer needs can be
soft-removed instead of deleted: it leaves the active catalog and the
lab-entry pickers but keeps its readings and canonical unit/range.
Thread `hidden` through the DTO, the update schema, the PUT handler
(field-by-field), and the list endpoint (visible markers first). Regen
the OpenAPI contract.
Add a single discreet LearnMoreLink to the per-vital baseline tiles
(via a new footer slot on the shared tile), the glucose clinical panel,
the resilience tile, and the lab biomarker detail. Each pointer is
registry-resolved and renders nothing when the concept has no mapped
guide, so only surfaces with a real article show one.
Add a dependency-free DateTimeField that paints the date part in the
user's date-order preference and the time part in the hour-cycle
preference over a hidden native datetime-local input, preserving the
local yyyy-MM-ddTHH:mm value contract. Migrate the seven DateTimeInput
callsites (measurement add/edit, mood add/edit, lab add/edit,
medication intake quick-add) so the chosen format is honoured there too,
and cover the field's value contract and per-preference rendering with a
component test.
Swap the medication scheduling, inventory, illness, vorsorge, lab-OCR
and profile-birthdate date inputs from the native field to the
DateField primitive, which paints the chosen DMY/MDY/YMD/AUTO order
over an ISO yyyy-MM-dd value. The committed value contract, min/max
bounds and aria wiring are unchanged.
The extraction already self-scores each field; the review screen only
hinted at a shaky numeric value. Add a calm per-row low-confidence badge
when any tracked field scores below the threshold, including qualitative
rows that had no confidence cue. Informative secondary badge, no alarm
colour.
Achievements: the bucket was 60 s hard-TTL while the app-wide unlock
notifier polls every 2 minutes, so every poll landed on an expired
entry and fired a cold ~400 ms rebuild in the foreground. Add a
10-minute stale-while-revalidate window and read the route through
cachedSwr; the route already persists pendingUnlocks outside the cached
factory, so a stale serve never skips an unlock write.
Achievements builder: the vitals array ran through several full-array
passes, each re-deriving a Berlin day-key (and hour) per row via
Intl.DateTimeFormat. Compute each row's day-key once and thread the
parallel array into the green-day, weekly-consistency, engagement, and
hidden passes; the hidden pass reuses a single precomputed hour array.
Results are byte-identical to the per-row derivation (parity test added).
dashboard/summary: read through cachedSwr so the iOS-polled endpoint
serves its existing 1 h stale window instead of re-paying the cold
rebuild every 60 s on the plain cached path.
Charts: route the three recharts consumers that bypassed the shared
next/dynamic boundary (lab biomarker chart, sleep stage stacked bar,
recommendation rationale charts) through dynamic imports with matching
loading skeletons, taking recharts off the first-load JS for
/labs/[id], /insights/sleep, and the insights recommendations surface.
Size the full-page Coach surface to clear the mobile BottomNav band plus the
iOS safe area so the docked composer and Stop control never sit under the nav;
desktop full-bleed height is unchanged (the nav is mobile-only).
Stop truncating the More-hub labels: shrink the label type and allow a
two-line clamp so long words (German Benachrichtigungen, and the long es/fr/it
labels) read in full instead of ellipsising.
Raise the lab reading-history edit/delete controls to 44px on touch
(compact on desktop), and drop the lab-detail Add button to icon-only on the
narrowest phones.
Remove the inline trash icon from the lab list and tile rows; delete now
lives only on the value detail view, beside Edit, so a stray tap on the
overview cannot remove a biomarker. Make the reference-range badge beside
each analyte heading quieter on the overview (smaller text, tighter
padding, no icon) so it reads as a subtle qualifier instead of competing
with the heading.
Replace the separate, capability-gated scan button with a single Add
menu: "Scan a document" (OCR) or "Add a value manually". The scan entry
appears only when the capability probe reports a usable mode, and routes
to native vision or local in-browser OCR (tesseract.js, lazy-loaded,
deu+eng, images only) per the resolved mode. Add the local-OCR opt-in
toggle to the Labs settings page with honest "less accurate — review
every value" copy.
Surface range confidence prominently on the OCR review screen for new
numeric biomarkers, where the extracted range becomes the catalog range:
flag low range-confidence or a missing range so the user verifies it.