mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
a778acac4e96d76c8fd3950c15236aa0c209e3e0
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
97d0c26ceb |
fix(glp1): match side-effect tags by key, not by an English/German word list
The GLP-1 side-effect chips in the mood form write the localised label into the flat `MoodEntry.tags` column, and three readers matched that label against a hand-written list of English and German words: the therapy timeline, the Coach snapshot, and the doctor report's GLP-1 block. The app ships six locales. Tap the nausea chip with the UI in French, Spanish, Italian or Polish and the day never reached the timeline, the Coach grounded its answers on an empty side-effect list, and a clinician read an empty table over a record that had three months of symptoms. Nothing on any of those surfaces said the table was a filter rather than the truth. The three lists had also drifted apart from each other. The chips are a closed set with stable keys, so matching the label was the defect and the key is the fix. `glp1-side-effect-tags.ts` holds the catalogue — seven keys, each with the i18n key its chip renders — and the mood form now draws its strip from it, so the write side and the read side cannot diverge again. `glp1-side-effect-tag-match.ts` builds the label-to-key index by reading `messages/<locale>.json` for every entry in `locales`, rather than transcribing the labels into a second list. Transcribing would have fixed today's four locales and reopened the same hole on the day a seventh is added; deriving means a new bundle is matchable as soon as it lands, with no edit in the matcher. Comparison folds case, accents and separators, so the English and German tags already in the database keep resolving and a straight apostrophe matches the typographic one the French bundle uses. All three readers now report the catalogue key instead of the stored string. The same recorded symptom yields one key whatever language it was tagged in, which also fixes the mixed-language output an account gets after switching UI locale; the doctor-report PDF resolves the key through the label map so the clinician reads it in the report's language. A tag outside the catalogue is still never counted — a free-text mood tag must not reach a doctor as an invented symptom, and the rest of somebody's day does not belong on a therapy timeline. It is no longer dropped without trace: the timeline route annotates the unresolved count on its wide event, so the classification rate stays answerable while the tag text stays in the database. The Coach's copy of the list also carried `vomiting`, `reflux` and `erbrechen`. Those are removed. No chip writes them, so they only ever matched a hand-typed tag in two of six languages; keeping them would have rebuilt the defect inside its own fix. Restoring either symptom means giving it a chip and a label in all six bundles. The response schema for `GET /api/insights/glp1-timeline` now enumerates the keys, and its description no longer documents the bilingual match as if it were the contract. |
||
|
|
310a9a4690 |
feat(doctor-report): put emergency data on page one
Add an EMERGENCY leaf to the identity group of the report catalogue, default-on, no module gate. When the leaf is admitted and the profile holds emergency data, the aggregator collects it and the PDF renders an emergency sheet as page one (banner, blood type, severe allergies, active medications, chronic conditions, implants, advance directive and organ donor, contacts, notes), then breaks to send the rest of the report to page two. The section is withheld when the leaf is not admitted or no data is present. Guards updated for the new leaf counts. |
||
|
|
2da0159577 |
feat(vaccinations): add the immunization history to the doctor report
- IMMUNIZATIONS report leaf in the history group, gated both ways: the vaccinations module and the leaf selection each exclude independently (WR-7 seen red on the selection gate, reverted by content hash) - collector loads the whole live history (reference data, not windowed, like allergies) with server-resolved series positions; the PDF renders a compact per-dose table — date, vaccine, dose, lot — with no due-status or gap analysis - added to the default template so a new report offers it; stored selections and frozen share links never widen - leaf label and section copy in six locales |
||
|
|
4d2f706dad |
feat(doctor-report): a report can mention a visit, and know why it exists
The VISITS leaf ships in the release that made a visit possible, because a selectable checkbox that produces nothing is worse than no checkbox. Leaf, collector and PDF renderer land together, and the leaf carries no LEAF_MODULE entry — like ALLERGIES beside it — because a visit is core and the whole report is already gated by doctorReport. The absence is commented at the map so a later reader does not read it as an oversight. The rows print the date, the practice with its specialty, the kind, the reason, the outcome and the conditions the visit was filed against. The two encrypted columns decrypt at render, fail-soft, and never enter the stored selection. A condition the person has since deleted is not named: the link row survives a soft delete, and printing a label they withdrew would put it in front of a doctor. No FHIR resource type is added. That deferral is already written at the Encounter model, and the bundle builder now says so at the point a reader would look for the missing section: FHIR splits the future from the past across Appointment and Encounter, which is a serialisation question this release does not answer. Two properties are free and worth stating: a stored selection never grows (unknown ids drop) and a clinician share link's sections are frozen write-once, so no existing report and no already-shared link gains a section. The panel also offers a booked visit as the report's reason — one query, one prefilled window end, one dismissible offer that names its own source and never applies itself. A window control that moves on its own is what makes an export panel untrustworthy. |
||
|
|
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. |
||
|
|
3aff2c16db | fix(insights): scope generation guard to prompt fields, clear cache on restore | ||
|
|
9e68ff525f |
feat(share): serve the record a link already implies, and say when there is none
Two downloads under the token — the FHIR document Bundle and the clinical PDF —
behind the same unlock cookie the document route enforces, serving exactly the
link's frozen selection. That is what the removed FHIR toggle promised, in the
form a practice actually uses: open the link, unlock it, save the file. No new
credential type, no new scope, no mint path that does not exist.
The gate is the document route's, step for step: the same live check, the same
token-scoped cookie, the same blunt 404 for every miss class so a probe cannot
tell "no such link" from "revoked" from "documents-only". It differs in one
number, because generating a bundle is far more expensive than serving a stored
blob and must not share a budget with it. Each download writes its own audit
row. The scope cannot exceed the page's: both resolve the same frozen leaf list
through the same resolver, and the insurance leaf is refused at create, so the
insurance number cannot reach this path at all.
The MCP doctor-visit summary stops claiming presence it cannot support. An
account whose owner never saved a report selection resolves to the empty one on
that surface, and an empty scope produces an empty payload — which now reads as
{ present: false, reason }, the same sentinel the other reads use, instead of a
record with every section quietly missing.
Mutation-checked: removing the unlock check, the rate limit, the documents-only
guard or the audit row reddens its own case; pointing the bucket at the document
route's key reddens the bucket assertion; making presence unconditional reddens
the absence case.
The saved profile's shape moves out of the module that reads the row. The panel
is a client component and parses the blob from the /me payload; importing the
reader dragged the database client into the browser bundle, which typecheck and
the unit suite both pass and only `next build` catches.
|
||
|
|
f42934487d |
test(export): prove the scope holds at the artefact, not just at the switch
Five layers, because the defect had five places to hide. The catalogue guard checks what the compiler cannot state: every leaf resolves a label a human can read, every group resolves one, the fenced tier stays last, and the shipped template intersects the fence at nothing. The selection guard pins the three minting functions against each other. A stored blob this build cannot read resolves to the empty selection, never to a default set — that is what makes the revoked legacy links revocable rather than silently re-scoped. A request naming an id the server does not know is refused by name rather than dropped, because dropping it would narrow a scope the caller did express. The egress guard freezes the set of aggregator callers with the source of each one's selection written down. The required brand-typed parameter already stops a new path compiling without an answer; this stops one being added without a human writing the answer down, which is the moment a reviewer asks the question. The behavioural sweep aggregates every leaf in and out against a fixture that carries a row for each, and asserts the payload differs. It is parametrised over the catalogue, so a leaf added later is covered the day it is added. Its positive control asserts the all-on payload is genuinely populated first, so a "no difference" result means the gate is inert and not that the account is empty. Beside it: an unmapped measurement type is excluded rather than passed through, no table behind an unchosen leaf is queried at all, and an empty selection produces an empty record. The artefact layer runs the real route and reads what comes out — the FHIR bundle, the PDF text and the zip's contents — with a positive control beside every exclusion. Mutation-checked: making the gate admit everything reddens the sweep for every leaf; passing an unmapped type through reddens the fail-closed case; dropping the mood query's condition reddens the zero-read case; returning the BMI unconditionally reddens the sweep naming BODY_MASS_INDEX; adding a fenced leaf to the template reddens the purity guard; removing the panel's seeding block reddens the first-run and repeat-run cases; dropping the insurance refusal reddens the share-link case; adding a caller outside the frozen list reddens the egress guard naming the file. The panel suite grew past the practice name it was written for and is renamed for what it now covers: the three states, the fenced tier having no group control, and the scope line naming its sensitive inclusions. Integration coverage moves to the leaf model, including the case this release exists for: an account with data in every domain and a caller that named nothing gets an empty record. The OpenAPI contract catches up with the routes — the required selection, the refused keys, the share summary's new fields, the moved `Patient/$everything`, the Coverage search and the five typed reads. |
||
|
|
5e490f31ec |
feat(export): make the doctor report carry only what was chosen
The report resolved an absent key to "included". A caller that sent no sections object, or an empty group inside one, received the full record at server-chosen defaults: roughly fifty metrics plus the medication list, labs, allergies and family history. The web panel sent every key explicitly so it looked fine from the browser, but the share link, the FHIR REST face, the MCP doctor-visit resource and the cycle-only export card all went through paths where absence meant yes. No arrangement of booleans fixes a default that means yes, so the shape goes. A selection is now an inclusion list of leaf ids. A leaf is the smallest thing that can be released or withheld: every measurement type, and thirteen structured sections — identity, insurer, the glucose panel, labs, the drug list, the administration ledger, adherence, the GLP-1 block, allergies, illness episodes, family history, mood, cycle. Membership is inclusion, absence is exclusion, and there is no defaults constant anywhere to drift. Three locks keep a metric added later off a report chosen earlier. A stored selection is a list of what was chosen, so a leaf that did not exist then is not in it. The catalogue is keyed over the measurement enum, so adding a member without deciding its group fails typecheck in the same commit. And the aggregator's filter inverts: a type the catalogue does not know is excluded and annotated, where it used to be passed through. The aggregator's third parameter is now required and brand-typed, which turned the three surfaces that had no scope at all into compile errors. Each one had to name a source, and each now replays the owner's saved selection — the same object reviewed in Settings. An account that never saved one gets nothing there rather than a guess. Identity and insurance become selectable, and a share link refuses the insurance leaf outright. The share view never decrypted the number; that is structural now instead of incidental. The KVNR is not even read unless the leaf was chosen. Two surfaces gain a record they always implied. A share link serves its frozen selection as a FHIR bundle and as the PDF, behind the same unlock cookie the document route uses, with its own rate bucket, its own audit row and the same blunt 404 for every miss. That is what the removed FHIR toggle promised, in the form a practice actually uses. The AI summary leaves the report. Its text came from the cached briefing with no relation to the selection, so its scope could not be proven, and prose whose scope cannot be proven has no place on a document whose whole point is a proven scope. The PDF table stops being a nine-type whitelist. It prints every selected leaf that has data, sub-headed by group, in the same order the panel shows — so selecting resting heart rate no longer means seeing it in the bundle and not in the PDF. The clinician view is grouped the same way and reads real labels instead of "Phq9 score". Migration 0273 adds the saved-selection column, drops the orphaned prefs column and the two retired share-link scope columns, and revokes every live record share link minted before this model. Their frozen scope was "everything except mood and cycle", which is a scope nobody chose; narrowing it would still be a scope nobody chose. Documents-only shares are untouched. The sharing list shows each closed link in a pick-again state with its label, window and expiry kept. The retired allowFhirApi and resourceTypes stay on the create and list responses as the constant false and empty list. The request refuses them. They come off the response once the native client confirms it does not decode them. Six files are split at seams this change already touched: the aggregator into its gate, its dense-bucket query, its series, its glucose panel, its medication ledger and its clinical records; the export route's three emitters into their own module; the clinician view into its sections, its document list and its downloads. |
||
|
|
cc41b6680a |
refactor(doctor-report): split the type-label keys out of the PDF core
Move DOCTOR_REPORT_TYPE_LABEL_KEYS into a jsPDF-free module so leaf consumers can import the map without dragging jspdf + jspdf-autotable into their graph. The PDF core re-exports the map, keeping a single source of truth and its existing import surface intact. Point the clinician share view at the new module so the public share route no longer pulls the PDF renderer into its server graph. |