feat(01-21): expose Google sync terminal status

- return redacted per-resource outcomes from manual sync
- poll owner-scoped progress and invalidate committed workout views
This commit is contained in:
Marc-André Bombeck committed 2026-07-29 15:01:13 +02:00
1 parent f3008c9ee0
commit ba6d06be3b
3 files changed
+279 -7

No files matched your search

+65 -1
View File
@@ -3,9 +3,64 @@ import { apiSuccess, apiError } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { syncUserGoogleHealth } from "@/lib/google-health/sync";
import type {
GoogleHealthReasonCode,
GoogleHealthResourceOutcome,
GoogleHealthResourceStatus,
} from "@/lib/google-health/sync-progress";
import { resolveSyncOutcome } from "@/lib/outcome/written-outcome";
import { NextRequest } from "next/server";
const RESOURCE_STATUSES = new Set<GoogleHealthResourceStatus>([
"pending",
"complete",
"partial",
"empty",
"truncated",
"failed",
]);
const REASON_CODES = new Set<GoogleHealthReasonCode>([
"collection_failed",
"token_failed",
"upsert_failed",
"rollup_failed",
"existing_page_limit",
]);
function boundedInteger(value: unknown, maximum = 2_147_483_647): number {
return typeof value === "number" && Number.isFinite(value)
? Math.min(maximum, Math.max(0, Math.trunc(value)))
: 0;
}
function publicResourceOutcome(value: unknown): GoogleHealthResourceOutcome {
const item =
value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
return {
resource:
typeof item.resource === "string"
? item.resource
.toLowerCase()
.replace(/[^a-z0-9-]/g, "-")
.slice(0, 48)
: "unknown",
pages: boundedInteger(item.pages),
fetched: boundedInteger(item.fetched),
mapped: boundedInteger(item.mapped),
written: boundedInteger(item.written),
status: RESOURCE_STATUSES.has(item.status as GoogleHealthResourceStatus)
? (item.status as GoogleHealthResourceStatus)
: "failed",
durationMs: boundedInteger(item.durationMs, 86_400_000),
truncated: item.truncated === true,
reasonCode: REASON_CODES.has(item.reasonCode as GoogleHealthReasonCode)
? (item.reasonCode as GoogleHealthReasonCode)
: null,
};
}
/**
* Manually trigger a Google Health sync for the current user (v1.26.0).
*
@@ -63,5 +118,14 @@ export const POST = apiHandler(async (request: NextRequest) => {
if (result.failed && result.imported === 0) {
return apiError("Google Health sync failed", 502);
}
return apiSuccess({ ...resolveSyncOutcome(result), fullSync });
const resources = Array.isArray(result.resources)
? result.resources.slice(0, 16).map(publicResourceOutcome)
: undefined;
return apiSuccess({
...resolveSyncOutcome(result),
...(result.runId ? { runId: result.runId.slice(0, 128) } : {}),
...(result.state ? { state: result.state } : {}),
fullSync,
...(resources ? { resources } : {}),
});
});
@@ -0,0 +1,98 @@
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess } from "@/lib/api-response";
import {
readGoogleHealthSyncProgress,
type GoogleHealthResourceOutcome,
} from "@/lib/google-health/sync-progress";
import { annotate } from "@/lib/logging/context";
import type { NextRequest } from "next/server";
function count(value: unknown, maximum = 2_147_483_647): number {
return typeof value === "number" && Number.isFinite(value)
? Math.min(maximum, Math.max(0, Math.trunc(value)))
: 0;
}
function resource(value: unknown): GoogleHealthResourceOutcome {
const item =
value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
const statuses = [
"pending",
"complete",
"partial",
"empty",
"truncated",
"failed",
];
const reasons = [
"collection_failed",
"token_failed",
"upsert_failed",
"rollup_failed",
"existing_page_limit",
];
return {
resource:
typeof item.resource === "string"
? item.resource
.toLowerCase()
.replace(/[^a-z0-9-]/g, "-")
.slice(0, 48)
: "unknown",
pages: count(item.pages),
fetched: count(item.fetched),
mapped: count(item.mapped),
written: count(item.written),
status: statuses.includes(String(item.status))
? (item.status as GoogleHealthResourceOutcome["status"])
: "failed",
durationMs: count(item.durationMs, 86_400_000),
truncated: item.truncated === true,
reasonCode: reasons.includes(String(item.reasonCode))
? (item.reasonCode as GoogleHealthResourceOutcome["reasonCode"])
: null,
};
}
function publicProgress(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object") return null;
const item = value as Record<string, unknown>;
const states = [
"in_progress",
"complete",
"partial",
"zero",
"truncated",
"failed",
"interrupted",
];
return {
runId: typeof item.runId === "string" ? item.runId.slice(0, 128) : "",
state: states.includes(String(item.state)) ? item.state : "failed",
startedAt: typeof item.startedAt === "string" ? item.startedAt : "",
updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : "",
...(typeof item.terminalAt === "string"
? { terminalAt: item.terminalAt }
: {}),
imported: count(item.imported),
failed: item.failed === true,
resources: Array.isArray(item.resources)
? item.resources.slice(0, 16).map(resource)
: [],
};
}
/**
* Return only the authenticated user's bounded current/most-recent run.
* Query parameters are intentionally ignored so this endpoint cannot enumerate
* another connection's progress.
*/
export const GET = apiHandler(async (_request: NextRequest) => {
const { user } = await requireAuth();
annotate({ action: { name: "google_health.sync.status" } });
return apiSuccess(
publicProgress(await readGoogleHealthSyncProgress(user.id)),
);
});