chore(release): v1.28.25 — every integration held to the same standard

Platform-wide hardening: the failure classes found live this week hunted down
across every integration.

Fitbit carried the same tombstone wedge fixed for Google — probe now matches
soft-deleted rows and the update resurrects them; the comment describing a
partial unique index that never existed is corrected. Sleep segment ids are
stable across re-scoring for Withings (session+segment-start), WHOOP/Polar
(one segment per stage, no positional index) and Oura (run-start keyed), each
with a session-scoped sweep that soft-deletes rows a re-score orphaned — so
legacy and affected nights heal as they are re-read.

One platform rule for deleted rows: source-owned readings resurrect on
re-import (Withings, WHOOP, Oura, Polar, Nightscout, mood webhook, CSV
re-imports, Apple stats day-totals); Apple sample-grain deletions stick per
the paired-client contract, and the mood bulk route's tombstone match is now
a true no-op reported as duplicate. Import counts reconcile against actual
createMany counts.

Google lifecycle: a dead token registers on the cycle's hard-fail ledger, so
an expired connection parks instead of stamping success hourly; backfill and
sleep-repair markers stamp only after a clean pass (pg-boss retries are now
real); failed measurement writes hold the watermark; no-op updates are
skipped; mapping.md tells the current truth.

Scale: doctor-report reads only consumed columns and enabled sections;
exports and backups keyset-paginate; achievements tally via SQL buckets;
series charts day-bucket dense kinds beyond 90 days; BP pairing is a linear
two-pointer; distinct-type scans run server-side; the graded-series cold
fallback is bounded; the fitbit defer tracker loops instead of spreading.
This commit is contained in:
Marc-André Bombeck committed 2026-07-11 01:54:12 +02:00
1 parent 1085a1e056
commit 9c63d3ce20
79 files changed
+3221 -564

No files matched your search

+57 -1
View File
@@ -103,7 +103,11 @@ beforeEach(() => {
updateMany: ReturnType<typeof vi.fn>;
};
measurement.findMany.mockResolvedValue([]);
measurement.createMany.mockResolvedValue({ count: 0 });
// Echo the attempted chunk size, matching Postgres semantics when no row
// collides — the route now reconciles `inserted` against this count.
measurement.createMany.mockImplementation(
async (arg: { data: unknown[] }) => ({ count: arg.data.length }),
);
measurement.updateMany.mockResolvedValue({ count: 0 });
(
prisma.$transaction as unknown as {
@@ -226,6 +230,29 @@ describe("POST /api/import/csv — batched write + per-row envelope", () => {
expect(mMeasurement().createMany).not.toHaveBeenCalled();
});
it("resurrects a tombstoned externalId row on re-import (deletedAt: null in update)", async () => {
// The ext-probe is deliberately deletedAt-less, so a tombstoned IMPORT
// row matches like a live one; the update must carry the resurrection
// (IMPORT rows are re-importable by design).
mMeasurement().findMany.mockResolvedValue([
{ type: "WEIGHT", externalId: "ext-tomb" },
]);
const res = await POST(
csvRequest(
[HEADER, "WEIGHT,81.0,kg,2026-05-01T08:00:00Z,,,ext-tomb"].join("\n"),
),
);
const body = (await res.json()) as CsvEnvelope;
expect(body.data?.updated).toBe(1);
expect(body.data?.rows[0].status).toBe("updated");
const updateArg = mMeasurement().updateMany.mock.calls[0][0] as {
data: { value: number; deletedAt: Date | null };
};
expect(updateArg.data.value).toBe(81.0);
expect(updateArg.data.deletedAt).toBeNull();
});
it("inserts an externalId row when it does not exist yet", async () => {
mMeasurement().findMany.mockResolvedValue([]);
const res = await POST(
@@ -274,4 +301,33 @@ describe("POST /api/import/csv — batched write + per-row envelope", () => {
// Only the first survivor reaches the bulk insert.
expect(mMeasurement().createMany.mock.calls[0][0].data).toHaveLength(1);
});
it("reconciles `inserted` against the createMany count when skipDuplicates absorbs a race", async () => {
// Two fresh rows attempted, but a concurrent double-submit already
// landed one of them — `skipDuplicates` absorbs the conflict and the
// count comes back short. The envelope must sum to the DB truth: one
// inserted, one downgraded to skipped/duplicate.
mMeasurement().createMany.mockImplementation(
async (arg: { data: unknown[] }) => ({ count: arg.data.length - 1 }),
);
const res = await POST(
csvRequest(
[
HEADER,
"WEIGHT,80.5,kg,2026-05-01T08:00:00Z,,,",
"WEIGHT,81.5,kg,2026-05-01T09:00:00Z,,,",
].join("\n"),
),
);
expect(res.status).toBe(200);
const body = (await res.json()) as CsvEnvelope;
expect(body.data?.inserted).toBe(1);
expect(body.data?.skipped).toBe(1);
const statuses = body.data?.rows.map((r) => r.status).sort();
expect(statuses).toEqual(["inserted", "skipped"]);
expect(body.data?.rows.find((r) => r.status === "skipped")?.reason).toBe(
"duplicate",
);
});
});
+29 -1
View File
@@ -239,12 +239,14 @@ export const POST = apiHandler(async (request: NextRequest) => {
...plainInserts.map(buildCreateData),
];
const CREATE_CHUNK = 200;
let createdCount = 0;
await prisma.$transaction(async (tx) => {
for (let i = 0; i < toCreate.length; i += CREATE_CHUNK) {
await tx.measurement.createMany({
const res = await tx.measurement.createMany({
data: toCreate.slice(i, i + CREATE_CHUNK),
skipDuplicates: true,
});
createdCount += res.count;
}
for (const m of extUpdates.values()) {
await tx.measurement.updateMany({
@@ -262,11 +264,37 @@ export const POST = apiHandler(async (request: NextRequest) => {
notesEncrypted: encryptNote(m.notes ?? null),
glucoseContext:
(m.glucoseContext as GlucoseContext | undefined) ?? null,
// No-op on a live row, a deliberate RESURRECTION on a
// tombstoned one — IMPORT rows are re-importable by design, so
// a re-imported externalId brings the row back (mirrors the
// source-owned sync resurrect rule).
deletedAt: null,
},
});
}
});
// Reconcile `inserted` against what `createMany` actually wrote
// (mirrors the batch route's raced-duplicate downgrade). Under
// `skipDuplicates` a conflicting row is silently absorbed — the key is
// present in the table either way, so we cannot identify the SPECIFIC
// raced lines; we only need the counters and per-line statuses to sum
// to the truth. Downgrade enough `inserted` lines to `duplicate` so
// the envelope matches the DB write count.
const racedDuplicates = toCreate.length - createdCount;
if (racedDuplicates > 0) {
let downgraded = 0;
for (const [line, outcome] of writeOutcome) {
if (downgraded >= racedDuplicates) break;
if (outcome === "inserted") {
writeOutcome.set(line, "duplicate");
inserted--;
skipped++;
downgraded++;
}
}
}
// One bounded rollup re-fold per touched (type, day) — a 10 000-row CSV
// pays at most ~N (type, day) recomputes, not 10 000 per-row hooks.
// Best-effort: a populator hiccup never fails the importer.