mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(labs): review a unit that was only a calc note, keep unseen category places
A document unit that held only "(calc)" read as no unit and went straight to the record; it now goes to review. Saving a category order keeps the saved keys not on screen after the shown ones. The changelog says what each fix does and no more.
This commit is contained in:
1 parent
69ba8bec91
commit
4dcb041612
6 files changed
+64
-15
No files matched your search
+17
-13
@@ -31,18 +31,20 @@
|
||||
(built-in categories in a fixed order, your own panels alphabetically,
|
||||
Other last). A category that turns up later joins the end of your
|
||||
order, and Other stays last if you left it there. The order is saved
|
||||
with your account and applied by the server, so the app shows the same
|
||||
order. The labs list layout accepts `categoryOrder`, and
|
||||
with your account and applied by the server, so every device that
|
||||
groups the list shows the same order. The labs list layout accepts
|
||||
`categoryOrder`, and
|
||||
`GET /api/labs/overview` answers `categories` in that order. (#1349)
|
||||
- **Blood ketones, and the glucose-ketone index worked out for you.**
|
||||
Ketones (beta-hydroxybutyrate, mmol/L) are a measurement of their own
|
||||
now, so a ketogenic diet no longer has to live in a custom metric with
|
||||
the index typed in by hand beside it. Choose "Blood ketones" when you
|
||||
add a measurement and type the glucose from the same fingerstick into
|
||||
the field below: both readings are saved together with one time, or
|
||||
neither is. The glucose-ketone index is glucose in mmol/L divided by
|
||||
ketones, taken from the glucose reading at the same time or, failing
|
||||
that, the nearest one within 15 minutes. It is never stored, so it
|
||||
add a measurement and, if the meter gave one, type the glucose from the
|
||||
same fingerstick into the field below: both readings are saved together
|
||||
with one time, or neither is. The glucose-ketone index is glucose in
|
||||
mmol/L divided by ketones, for every ketone reading above zero, taken
|
||||
from the glucose reading at the same time or, failing that, the nearest
|
||||
one within 15 minutes. It is never stored, so it
|
||||
follows any reading you edit or delete. It shows under the chart on the
|
||||
new Ketones page, in the day view, and beside the ketones in the doctor
|
||||
report and the clinician share; the FHIR export carries the ketone
|
||||
@@ -58,10 +60,12 @@
|
||||
could copy it into the unit, so a marker was created in "g/dL (calc)"
|
||||
or just "(calc)" and the next report's plain "g/dL" was refused as a
|
||||
different unit. The read now drops that note (also "calculated",
|
||||
"computed" and the German "berechnet"), a unit that was only the note
|
||||
counts as no unit, and a marker already saved with the note accepts
|
||||
the plain unit. Any other text in brackets still counts as part of
|
||||
the unit. (#1313)
|
||||
"computed" and the German "berechnet"), a marker already saved in
|
||||
"g/dL (calc)" accepts the plain "g/dL", and a unit that was only the
|
||||
note counts as no unit and goes to review, so a person decides whether
|
||||
the value really has none. A marker saved in just "(calc)" keeps that
|
||||
unit until you change it on the marker. Any other text in brackets
|
||||
still counts as part of the unit. (#1313)
|
||||
- **The sync feed no longer loses a reading written during a long
|
||||
import, and no longer starts over for a quiet account.** A Garmin or
|
||||
Withings import writes a batch in one transaction and stamps its
|
||||
@@ -80,8 +84,8 @@
|
||||
- **Changing the calendar subscription while its link is being renewed.**
|
||||
A subscription turned off at the same moment its link was renewed could
|
||||
come back under the new link, and an option changed at that moment
|
||||
could be lost to the new link. Turning it off now wins, and the change
|
||||
lands on the link that stays live.
|
||||
could be lost to the new link. Turning it off now wins whichever comes
|
||||
first, and the change lands on the link that stays live.
|
||||
|
||||
## [1.47.1] — 2026-11-07
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
type ModuleGroupsOpen,
|
||||
type ModuleSortDir,
|
||||
} from "@/lib/module-list-prefs";
|
||||
import { mergeCategoryOrder } from "@/lib/settings/module-list-layout";
|
||||
|
||||
import { BiomarkerManager } from "./biomarker-manager";
|
||||
import type {
|
||||
@@ -134,7 +135,11 @@ export function LabsSettings() {
|
||||
/>
|
||||
|
||||
{prefs.groupBy === "category" ? (
|
||||
<LabsCategoryOrderCard onChange={setCategoryOrder} />
|
||||
<LabsCategoryOrderCard
|
||||
onChange={(shown) =>
|
||||
setCategoryOrder(mergeCategoryOrder(shown, prefs.categoryOrder))
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{manual ? (
|
||||
|
||||
@@ -221,6 +221,9 @@ describe("runInboundExtraction — stated-status-only mapping", () => {
|
||||
expect(
|
||||
result.facts.map((f) => (f.data as ObservationFactData).unit),
|
||||
).toEqual(["g/dL", null]);
|
||||
// A unit that was only the note goes to review: no unit is a choice a
|
||||
// person makes, not one a read makes for them.
|
||||
expect(result.facts.map((f) => f.needsReview)).toEqual([false, true]);
|
||||
});
|
||||
|
||||
it("stores the document's real span, not the model's echo of it", async () => {
|
||||
|
||||
@@ -399,9 +399,15 @@ export async function runInboundExtraction(
|
||||
// A unit of "1" is the user's explicit no-unit choice. A read that
|
||||
// states it itself goes to review rather than straight to the record,
|
||||
// so a dropped unit never passes as a dimensionless value.
|
||||
// So does a unit column that held only a "(calc)" note: it reads as no
|
||||
// unit, and a person decides whether the value really has none.
|
||||
needsReview:
|
||||
confidence < INBOUND_CONFIDENCE_FLOOR ||
|
||||
(raw.type === "OBSERVATION" && isDimensionlessUnit(raw.unit)),
|
||||
(raw.type === "OBSERVATION" &&
|
||||
(isDimensionlessUnit(raw.unit) ||
|
||||
(raw.unit !== null &&
|
||||
raw.unit.trim() !== "" &&
|
||||
observationUnit(raw.unit) === null))),
|
||||
data,
|
||||
provenance: {
|
||||
sourceText: anchor.sourceText,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
normaliseModuleListOrder,
|
||||
resolveModuleListLayout,
|
||||
storedModuleListLayout,
|
||||
mergeCategoryOrder,
|
||||
} from "../module-list-layout";
|
||||
|
||||
describe("resolveModuleListLayout", () => {
|
||||
@@ -143,3 +144,15 @@ describe("storedModuleListLayout", () => {
|
||||
).toEqual({ labs: { sortDir: "manual" } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeCategoryOrder", () => {
|
||||
it("keeps the place of a saved category that is not on screen", () => {
|
||||
expect(
|
||||
mergeCategoryOrder(["lipids", "liver"], ["liver", "thyroid", "lipids"]),
|
||||
).toEqual(["lipids", "liver", "thyroid"]);
|
||||
});
|
||||
|
||||
it("is the shown order when nothing else was saved", () => {
|
||||
expect(mergeCategoryOrder(["b", "a"], [])).toEqual(["b", "a"]);
|
||||
});
|
||||
});
|
||||
@@ -62,6 +62,24 @@ export const MODULE_LIST_ID_MAX = 64;
|
||||
*/
|
||||
export const MODULE_LIST_CATEGORY_ORDER_MAX = 200;
|
||||
|
||||
/**
|
||||
* The category order to save after a move: the categories on screen in
|
||||
* their new order, then every saved key that is not on screen now, in its
|
||||
* saved order. A category without readings at the moment (or one of another
|
||||
* record a guardian arranged) keeps its place in the list instead of being
|
||||
* dropped by a move that never showed it.
|
||||
*/
|
||||
export function mergeCategoryOrder(
|
||||
shown: readonly string[],
|
||||
saved: readonly string[],
|
||||
): string[] {
|
||||
const onScreen = new Set(shown);
|
||||
return [...shown, ...saved.filter((key) => !onScreen.has(key))].slice(
|
||||
0,
|
||||
MODULE_LIST_CATEGORY_ORDER_MAX,
|
||||
);
|
||||
}
|
||||
|
||||
export interface ModuleListPrefs {
|
||||
view: ModuleListView;
|
||||
order: string[];
|
||||
|
||||
Reference in new issue
Block a user