mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
feat(apple-health): show when each type last arrived and how
The Apple Health card listed each type's newest sample. A type the phone only hands over on "Sync all" can carry a reading from this morning and never have arrived in the background, so the sample time could not answer whether a type flows on its own. The batch route's per-type ledger now records, in one upsert per batch, when each Apple Health type last arrived, the trigger that carried it (including the new `manual`), and when a batch last brought a new or updated sample of it. Duplicate-only re-sends move the arrival but keep the last new value; scoped credentials and MANUAL rows write nothing, the same rule as the sync checkpoint. A ledger failure leaves a warn line and never fails the ingest. `GET /api/integrations/healthkit` lays `lastReceivedAt`, `lastTrigger` and `lastNewSampleAt` over each `metricFreshness` entry (null for a type no live sync has carried, absent on the workouts entry and when the ledger read failed), and the card shows them as a second line per type. The per-type newest-sample read walks live types through `listLiveMeasurementTypes` and probes each type's newest row instead of grouping every live row of the source.
This commit is contained in:
1 parent
c8470cc1e6
commit
ba99551faa
15 files changed
+976
-114
No files matched your search
@@ -38,7 +38,7 @@ vi.mock("@tanstack/react-query", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
import { AppleHealthCard } from "../apple-health-card";
|
||||
import { AppleHealthCard, ArrivalDetail } from "../apple-health-card";
|
||||
|
||||
function render(locale: Locale = "en") {
|
||||
return renderToStaticMarkup(
|
||||
@@ -404,3 +404,77 @@ describe("<AppleHealthCard> — sync progress (#778)", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("<AppleHealthCard> — per-type arrivals (#1173)", () => {
|
||||
const now = new Date("2026-10-08T12:00:00.000Z");
|
||||
const hoursAgo = (h: number) =>
|
||||
new Date(now.getTime() - h * 60 * 60 * 1000).toISOString();
|
||||
const base = {
|
||||
type: "RESPIRATORY_RATE",
|
||||
lastSeenAt: hoursAgo(5),
|
||||
stale: false,
|
||||
};
|
||||
const t = (key: string, params?: Record<string, string | number>) => {
|
||||
const value = key
|
||||
.split(".")
|
||||
.reduce<unknown>(
|
||||
(node, part) => (node as Record<string, unknown>)?.[part],
|
||||
en,
|
||||
);
|
||||
if (typeof value !== "string") return key;
|
||||
return value.replace(/\{(\w+)\}/g, (_, name: string) =>
|
||||
String(params?.[name] ?? `{${name}}`),
|
||||
);
|
||||
};
|
||||
const html = (entry: Parameters<typeof ArrivalDetail>[0]["entry"]) =>
|
||||
renderToStaticMarkup(<ArrivalDetail entry={entry} t={t} now={now} />);
|
||||
|
||||
it("names when a type last arrived and that only Sync all carried it", () => {
|
||||
const out = html({
|
||||
...base,
|
||||
lastReceivedAt: hoursAgo(2),
|
||||
lastTrigger: "manual",
|
||||
lastNewSampleAt: hoursAgo(2),
|
||||
});
|
||||
expect(out).toContain('data-trigger="manual"');
|
||||
expect(out).toContain("Last received 2 h ago");
|
||||
expect(out).toContain("from Sync all");
|
||||
// The last arrival brought something new, so no second date is needed.
|
||||
expect(out).not.toContain("Last new value");
|
||||
});
|
||||
|
||||
it("adds the last new value when the latest arrival held duplicates only", () => {
|
||||
const out = html({
|
||||
...base,
|
||||
lastReceivedAt: hoursAgo(1),
|
||||
lastTrigger: "background",
|
||||
lastNewSampleAt: hoursAgo(72),
|
||||
});
|
||||
expect(out).toContain("in the background");
|
||||
expect(out).toContain("Last new value 3 d ago");
|
||||
});
|
||||
|
||||
it("says a type has not arrived through live sync yet", () => {
|
||||
const out = html({
|
||||
...base,
|
||||
lastReceivedAt: null,
|
||||
lastTrigger: null,
|
||||
lastNewSampleAt: null,
|
||||
});
|
||||
expect(out).toContain('data-state="never"');
|
||||
expect(out).toContain("Not received yet");
|
||||
});
|
||||
|
||||
it("renders nothing when the server sent no arrival facts", () => {
|
||||
expect(html(base)).toBe("");
|
||||
});
|
||||
|
||||
it("titles the list by data type", () => {
|
||||
statusPayload = {
|
||||
lastSyncedAt: hoursAgo(1),
|
||||
syncHealth: { verdict: "fresh", since: null },
|
||||
metricFreshness: [{ ...base }],
|
||||
};
|
||||
expect(render()).toContain("By data type");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user