Commit Graph
23 Commits
Author SHA1 Message Date
Marc-André Bombeck 7ed953a029 fix(ui): route the hand-built segment rows through Segmented
The narrative period, the lab result type, both admin filters, the chart
range tabs, the mood chart's own copy of them, the sleep-stage window and
the re-authentication method each spelled a segmented control by hand,
with their own sizes and fills. They render Segmented now; the range tabs
keep their long names as a hover title, and the admin filters gain an
accessible group name in every language. A test refuses a new button row
whose aria-pressed compares a state to each button's value, and names
the rows that remain with why they are not one choice on one track.
2026-10-20 16:43:59 +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 07d991adf7 fix(labs): keep the value and draw-date column inside the dialog body
The single-column grid used the implicit auto track, which grows to the
widest cell's min-content. The draw date's date and time inputs size
from the font, so with wider metrics that min-content passed the dialog
body (about 422 px against 398 px) and the value input ran 24 px into
the dialog's padding. An explicit grid-cols-1 track is minmax(0, 1fr)
and stays at the body's width; the date row already shrinks inside it.

Refs #1214
2026-10-14 21:01:13 +02:00
Marc-André Bombeck 63882bf3ec fix(labs): give the draw date a full-width row
The lab form placed the draw date in the second column of a two-column
grid. DateTimeField sets its date and time halves side by side from sm
up, so the date half was squeezed until a date could only be set
through the calendar. The value and the date now stack in one column,
as in the measurement form, and the date can be typed again.

Refs #1214
2026-10-14 18:58:55 +02:00
Marc-André Bombeck 03f374794e Settle the form sheets on one anatomy
Field labels no longer lose the space before '(optional)' or set the
colon apart from a unit, and a question label carries no colon after its
question mark. Cancel next to a primary is the outline button in every
form, the illness sheet's selects span the field width, and the reminder
sheet drops a description that repeated the page subtitle. On a phone a
sheet now opens with focus on itself rather than its first field, so a
date or name field no longer raises the keyboard over a sheet nobody has
read yet.
2026-10-08 16:20:42 +02:00
Marc-André Bombeck fe13fa966b fix(labs): print range bars and lab values in the reader's number format
Range bars and the lab list, detail rows, chart tooltip and reference
ranges printed numbers with a plain `String()`, so a German reader saw
"1.8 mIU/L" and "61.3 kg" under a German interface. The same was true
of the mean, GMI, eA1C and the advanced indices on the glucose panel.

`RangeBar` now prints value, bounds and delta through the locale number
formatter and takes an optional `formatValue` for surfaces with their
own precision. Lab values keep their trim rule (whole numbers bare, at
most two decimals) in `formatLabValue`, which now hands the digits to a
caller-supplied locale formatter; `useLabNumber` wires it to the
reader's locale for every lab surface. Inputs are unchanged.
2026-10-01 22:09:30 +02:00
Marc-André Bombeck 4a13004917 fix(labs): fit the lab list, values list and add sheet to a phone
The compact list was a five-column table at every width. Below the fixed
columns (badge, 12rem range bar, 72px trend, chevron) the name column
collapsed to nothing, the badge sat over the value and the row panned
sideways off the card. Below lg the same cells now fold into two lines
per row; the desktop subgrid is unchanged.

The values table needed ~510px, so on a phone its edit and delete
buttons sat in a sideways-scrolling strip and the range bar labels were
clipped. Below lg each reading is a stacked row with its actions beside
it.

The add sheet's three footer buttons did not fit a phone row in German
and pushed Cancel off the left edge; the secondary save takes a row of
its own there, and the footer wraps instead of overflowing on desktop.
The scan review's 'links to existing marker' chip truncates instead of
running past its row.
2026-10-01 09:44:29 +02:00
Marc-André Bombeck cb12882aff fix(labs): keep the biomarker picked after "Save and add another" (#1029)
The reset after "Save and add another" cleared the biomarker and passed
the picker `undefined`, which switched it to its own internal state. It
went on showing the marker it last held while the form value was empty,
so the next save refused with "Pick a biomarker first" under a visibly
picked marker, and a later pick could land on the first catalogue entry.

The picker is always controlled now (an empty pick shows the
placeholder), and "Save and add another" keeps the marker, the draw date
and the visit, clearing only the reading, its note and its printed
range. Focus moves to the value field for the next entry.
2026-09-24 08:02:11 +02:00
Marc-André Bombeck 2dc553057d fix(labs): read a reference range printed in French, Spanish, Italian or Polish
The free-text reference-range parser decided what a printed window meant from
an alternation of German and English words. The app ships six locales, and a
lab prints in the language of the country it stands in, so `jusqu'à 5,0`,
`hasta 5,0`, `fino a 5,0` and `do 5,0` all fell through to `low: null,
high: null`. The string was stored, the reading was then judged against the
biomarker's own band or against nothing at all, and no surface anywhere said
that the lab had printed a window the app could not read.

The vocabulary is TABULATED here, not derived from `messages/<locale>.json`
the way an app-owned word list would be. These words are prose a lab printed
on paper; the app owns no string they could be read out of, and the language
of a report has nothing to do with the language of the UI. So the table is
hand-written, keyed by the shipped `Locale` union so that adding a locale
fails to compile until someone has answered how that language prints a range,
and every locale's words are tried against every input at once. The cost is
written down at the table: a report in a language absent from it stays
text-only.

Three things had to change beside the word lists. Introducing words are now
stripped only on a second pass, because Italian "da", Polish "od" and French
"de" also state a floor on their own and stripping first read "da 3,5" as a
bare number. Input and table are diacritic-folded and their apostrophes
normalised, so a table row written "jusqu'a" meets a report's "jusqu'à" and
the curled apostrophe a word processor emits. And one-letter separators ("a",
"e", "y") are only accepted with whitespace on both sides, so "3.5e5" is not
read as a window. Decimal commas already worked and are pinned by the new
cases.

An unreadable window now has a name. `isUnreadableRange` separates "the report
stated nothing" from "the report stated something nobody could read" — the two
were indistinguishable at every call site, which is what let the French case
pass unnoticed. The manual form already showed the reader that the text was
kept but not read; the two ingest paths now count it into their wide event, so
an unsupported notation surfaces as a number rather than as silence.

The German and English lists are the previous alternation split word for word,
and the existing suite covering them passes unchanged.
2026-08-23 14:16:15 +02:00
Marc-André Bombeck c47122ccfe feat(encounters): the lab moment, the condition side, and closing a checkup
A lab panel files itself against the visit it came from, and never has to. The
OCR review and the manual form both offer the suggestion through the one shared
field, anchored on the draw date rather than on now, so backdating a reading
moves the offer with it. The commit writes one link PER RESULT ROW, which is
why the join is m:n: a marker re-run on a different day belongs to its own
visit. Best-effort by design — the readings are the write that matters and must
not be lost to a link that could not be made.

The condition side gets a Visits about this card with an action that pre-links
the episode. From the episode side only, and that is a decision: an episode
spans weeks and a visit is a point, so matching the other way would be wrong
often enough to teach a person to ignore every suggestion in the product.

A due checkup gains a second action beside erledigt. Filing the visit as done
satisfies the checkup through the same forward-only, idempotent primitive the
checkbox uses, so doing both is one satisfaction and no error. The two actions
answer different questions — one says it happened, the other says where.

An appointment now appears where a person looks, and NEITHER surface relaxes
the reminder-table exclusion that keeps it out of the preventive-care lists.
Both read the visit table: a quiet line under the dashboard checkup block for
anything inside two weeks, and one rail item for today or tomorrow. A diff in
either that touches origin is the wrong fix.

A visit write now also refreshes the reminder reads. Closing a checkup moves
its next-due date on three surfaces that would otherwise keep showing it due
until their own poll.

OpenAPI regenerated in the same commit.
2026-08-08 14:08:09 +02:00
Marc-André Bombeck fd2bd67ff3 feat(labs): keep the reference range a report prints beside a value
A lab runs its own method on its own device, so the window a physician
evaluates a value against is the one printed next to it on that report,
not a catalog band averaged over every lab. Reading a report pulled the
value and dropped that range: the extraction asked for it, the commit
handed it to the biomarker resolver, and the resolver only uses it when
it mints a NEW marker. For every analyte the user already had — the
normal case after a first panel — the printed range was read and then
thrown away.

Add `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText`
to `LabResult` and carry the range from the report to the reading. The
text is stored whether or not numbers can be read out of it, because
"bis 5,0" and "negativ" state a window too; a range that could not be
parsed is kept verbatim, never replaced with invented bounds.

`resolveEffectiveReferenceRange` is the one place the precedence lives:
the source window wins for that single reading, and the catalog stays
the net for readings without one. A printed string with no derivable
bound does not displace the catalog. Every surface that judges a
reading routes through it — the API verdict, the doctor report, the
insights blocks and per-marker assessment, the coach snapshot, the MCP
read and the FHIR `referenceRange` — so a value cannot read in range on
one surface and out on another.

The API keeps `referenceLow` / `referenceHigh` as the window in force,
so a client that reads only those two renders the right range and the
right verdict with no change of its own; the catalog band, the source
window, its origin and a divergence flag ride alongside for clients
that want to show both.

Read side: the chart draws the source window as a second, visually
distinct band with the reading it came from named beneath it, the
reading rows name the printed range and the catalog band where the two
disagree, and the doctor-report PDF prints the report's own string with
a footnote naming the saved range. Entry side: one optional field on
the lab form, folded away until asked for, and the printed range is
editable on the scan review screen.

The range parser covers the notations German and English reports
actually print — two-sided, one-sided, comma or dot decimals, thousands
separators, trailing units — reads units without ever converting them,
and refuses to guess on anything ambiguous.
2026-08-07 21:40:41 +02:00
Marc-André Bombeck d5a9360c52 Give the caregiver a front door and a receipt
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.
2026-08-03 12:12:08 +02:00
Marc-André Bombeck 62eb431d66 release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0

* test: stabilize water capture re-entry
2026-07-22 04:14:15 +02:00
Marc-André Bombeck 3a45fcc3bd fix(ui): close remaining consistency-audit findings
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.
2026-07-18 03:52:32 +02:00
Marc-André Bombeck a2f926576e feat(labs): add "save & add another" to the manual result form
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.
2026-07-18 03:52:32 +02:00
Marc-André Bombeck 3ad59cde98 feat(labs): compact editable biomarker list with hide
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).
2026-06-27 12:48:54 +02:00
Marc-André Bombeck 01b24c4d73 feat(date): add DateTimeField and migrate datetime inputs
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.
2026-06-26 13:17:55 +02:00
Marc-André Bombeck 576264d1d9 fix(labs): raise lab-form result-type toggles to the touch floor 2026-06-20 12:53:41 +02:00
Marc-André Bombeck a5b3715a46 feat(labs): record qualitative lab results
Add first-class qualitative lab findings (positive / negative / borderline
serology) alongside the existing numeric path. A LabResult is now EITHER a
numeric reading (value + unit, optional reference bounds) OR a qualitative one
(valueText, e.g. "negativ"), never both, never neither.

- schema: make `value` nullable, add `value_text`; migration 0181 is additive
  (DROP NOT NULL + ADD COLUMN), every existing numeric row is untouched.
- validation: numeric-XOR-qualitative refine on create + update; unit/bounds
  apply to the numeric path only; valueText capped at 120 chars. The single
  resource PUT rejects a cross-type edit.
- serialise: carry valueText through both DTOs; a qualitative row reports the
  neutral `unknown` range verdict (no number to classify).
- FHIR: a qualitative result emits valueCodeableConcept — SNOMED qualifier
  value for confidently-known terms (Negative 260385009, Positive 10828004,
  Not detected 260415000, Detected 260373001), text-only for borderline /
  grenzwertig and any unrecognised term. The raw text always rides .text.
- UI: a numeric/qualitative toggle in the lab form; list, history, detail,
  chart, sparkline, and the doctor-report PDF all render qualitative readings
  as their text and never plot or divide on a null value.
- i18n: new labs.form keys across all six locales.
- openapi: response carries a nullable value + valueText; regenerated.
2026-06-20 11:17:43 +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 9c8c7051aa fix(labs): pin add-entry action bars to the sheet footer; tighten header
The lab and biomarker forms portal their Cancel/Save row into the
ResponsiveSheet sticky footer (footerSlot pattern) so Save stays reachable
on mobile instead of scrolling away. The Labs header folds Manage
biomarkers into an overflow kebab beside the primary Add, mirroring the
medication header. The biomarker detail gains a self-edit Pencil opening
the pre-filled form. Add buttons on the labs detail and measurements page
meet the min-h-11 sm:min-h-9 tap floor.
2026-06-16 23:12:04 +02:00
Marc-André Bombeck 04c95e5ba1 feat(labs): structured entry, per-biomarker chart, and edit/delete UI
Rework the Labs surface to match the rest of the app:

- Add a biomarker manager on the Labs page (define/edit/delete markers,
  seed from a common panel) — the catalog lives with Labs, not Settings.
- Replace the free-text add form with a structured one: pick a biomarker,
  enter the value against its known unit/range; '+ define new' inline.
- New /labs/[biomarkerId] detail page with a proper dashboard-style chart
  (Recharts line + shaded reference band + dashed bounds, range tabs,
  tooltip) replacing the 72x20 sparkline, plus the reading history with
  full edit/amend/delete and a delete-Undo toast.
- List cards group by linked marker and deep-link to the detail chart.

The reference band uses a muted token fill, never an alarm colour; the
neutral ReferenceRangeBadge is reused for the in/out-of-range verdict.
2026-06-16 21:06:08 +02:00
Marc-André Bombeck 72fb51c257 feat(labs): add the Laborwerte page, navigation, and OpenAPI contract
A dedicated /labs page hosts manual entry and a per-analyte view: the
four essentials (biomarker, value, unit, date) stay visible, the
reference range, panel grouping and note sit under an optional section.
Results group by analyte with a small inline trend once an analyte has
two or more readings. The reference-range badge is deliberately neutral
and calm — an out-of-range value reads as a quiet directional marker, not
an alarming red.

Adds the labs section toggle to the health-record export panel, the /labs
nav entries, the /api/labs OpenAPI routes, and the labs copy across all
six locales.
2026-06-14 23:39:52 +02:00