mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +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
@@ -13,11 +13,16 @@ vi.mock("@/lib/db", () => ({
|
||||
fitbitConnection: { findUnique: vi.fn(async () => null) },
|
||||
googleHealthConnection: { findUnique: vi.fn(async () => null) },
|
||||
moodEntry: { count: vi.fn(async () => 0) },
|
||||
measurement: { groupBy: vi.fn(async () => []) },
|
||||
$queryRaw: vi.fn(async () => []),
|
||||
workout: { groupBy: vi.fn(async () => []) },
|
||||
},
|
||||
}));
|
||||
|
||||
// v1.42 — the per-source type walk that replaced the measurement groupBy.
|
||||
vi.mock("@/lib/measurements/live-types", () => ({
|
||||
listLiveMeasurementTypes: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() }));
|
||||
|
||||
vi.mock("@/lib/api-response", () => ({
|
||||
@@ -53,6 +58,7 @@ vi.mock("@/lib/oura/credentials", () => ({
|
||||
|
||||
import { GET } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { listLiveMeasurementTypes } from "@/lib/measurements/live-types";
|
||||
import { ATTEMPT_STALE_AFTER_MS } from "@/lib/integrations/sync-verdict";
|
||||
|
||||
const userFind = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
@@ -315,7 +321,7 @@ describe("/api/integrations/status — the freshness read failed", () => {
|
||||
});
|
||||
|
||||
it("reports the degradation instead of passing an empty list off as an answer", async () => {
|
||||
(prisma.measurement.groupBy as ReturnType<typeof vi.fn>).mockRejectedValue(
|
||||
vi.mocked(listLiveMeasurementTypes).mockRejectedValue(
|
||||
new Error("statement timeout"),
|
||||
);
|
||||
|
||||
@@ -331,9 +337,7 @@ describe("/api/integrations/status — the freshness read failed", () => {
|
||||
});
|
||||
|
||||
it("reports no degradation when the read simply found nothing", async () => {
|
||||
(prisma.measurement.groupBy as ReturnType<typeof vi.fn>).mockResolvedValue(
|
||||
[],
|
||||
);
|
||||
vi.mocked(listLiveMeasurementTypes).mockResolvedValue([]);
|
||||
(prisma.workout.groupBy as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||
|
||||
const envelope = await fetchEnvelope();
|
||||
|
||||
Reference in new issue
Block a user