mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(01-19): expose privacy-safe ECG import outcomes
- return and render bounded ECG result counters with partial-failure wording - document archive limits, encrypted storage, and exact WHOOP constraints
This commit is contained in:
1 parent
a686c7200b
commit
4b64556b38
3 files changed
+118
-11
No files matched your search
@@ -36,6 +36,32 @@ interface ImportJobStatusResponse {
|
||||
failureReason: string | null;
|
||||
}
|
||||
|
||||
function boundedCount(value: unknown): number {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
||||
? value
|
||||
: 0;
|
||||
}
|
||||
|
||||
function privacySafeResult(
|
||||
value: Record<string, unknown> | null,
|
||||
): Record<string, unknown> | null {
|
||||
if (!value) return null;
|
||||
const ecg =
|
||||
typeof value.ecg === "object" && value.ecg !== null
|
||||
? (value.ecg as Record<string, unknown>)
|
||||
: {};
|
||||
return {
|
||||
...value,
|
||||
ecg: {
|
||||
discovered: boundedCount(ecg.discovered),
|
||||
imported: boundedCount(ecg.imported),
|
||||
updated: boundedCount(ecg.updated),
|
||||
skipped: boundedCount(ecg.skipped),
|
||||
failed: boundedCount(ecg.failed),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const GET = apiHandler(
|
||||
async (_request: NextRequest, ctx: RouteContext) => {
|
||||
const { user } = await requireAuth();
|
||||
@@ -68,7 +94,9 @@ export const GET = apiHandler(
|
||||
uploadBytes: row.uploadBytes,
|
||||
exportedAt: row.exportedAt?.toISOString() ?? null,
|
||||
progress: (row.progress as Record<string, unknown>) ?? {},
|
||||
result: (row.result as Record<string, unknown> | null) ?? null,
|
||||
result: privacySafeResult(
|
||||
(row.result as Record<string, unknown> | null) ?? null,
|
||||
),
|
||||
failureReason: row.failureReason,
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ interface JobStatus {
|
||||
totals?: { recordsRead?: number; rowsUpserted?: number };
|
||||
clinical?: { skipped?: number };
|
||||
cumulativeEstimates?: { days?: number; rows?: number };
|
||||
ecg?: {
|
||||
discovered?: number;
|
||||
imported?: number;
|
||||
updated?: number;
|
||||
skipped?: number;
|
||||
failed?: number;
|
||||
};
|
||||
} | null;
|
||||
failureReason: string | null;
|
||||
}
|
||||
@@ -152,6 +159,8 @@ export function AppleHealthImportCard() {
|
||||
const isFailed = status?.status === "failed";
|
||||
const busy = uploading || isRunning;
|
||||
const percent = status?.progress?.percent ?? null;
|
||||
const ecg = status?.result?.ecg;
|
||||
const ecgWritten = (ecg?.imported ?? 0) + (ecg?.updated ?? 0);
|
||||
|
||||
return (
|
||||
<ImportCardShell
|
||||
@@ -241,7 +250,7 @@ export function AppleHealthImportCard() {
|
||||
{isDone && (
|
||||
<WrittenOutcomeLine
|
||||
outcome={classifyWrittenOutcome({
|
||||
written: status?.result?.totals?.rowsUpserted ?? 0,
|
||||
written: (status?.result?.totals?.rowsUpserted ?? 0) + ecgWritten,
|
||||
// The clinical count is a documented, deliberate exclusion
|
||||
// (electrocardiograms, lab documents), not a refused row — it
|
||||
// must not tip the archive into a warning. Only "nothing
|
||||
@@ -262,6 +271,17 @@ export function AppleHealthImportCard() {
|
||||
testId="import-apple-health-result"
|
||||
/>
|
||||
)}
|
||||
{isDone && (ecg?.discovered ?? 0) > 0 && (
|
||||
<p
|
||||
data-testid="import-apple-health-ecg-result"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
ECGs: {ecg?.discovered ?? 0} discovered, {ecg?.imported ?? 0}{" "}
|
||||
imported, {ecg?.updated ?? 0} updated, {ecg?.skipped ?? 0} skipped,{" "}
|
||||
{ecg?.failed ?? 0} failed. ECG failures do not discard supported
|
||||
records imported from export.xml.
|
||||
</p>
|
||||
)}
|
||||
{isDone && (status?.result?.cumulativeEstimates?.days ?? 0) > 0 && (
|
||||
<AppleHealthEstimateWarning
|
||||
days={status?.result?.cumulativeEstimates?.days ?? 0}
|
||||
|
||||
Reference in new issue
Block a user