mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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:
1 parent
f3008c9ee0
commit
ba6d06be3b
3 files changed
+279
-7
No files matched your search
@@ -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)),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user