diff --git a/server/src/__tests__/health.test.ts b/server/src/__tests__/health.test.ts index a3551f568b9..a0c70dd58a5 100644 --- a/server/src/__tests__/health.test.ts +++ b/server/src/__tests__/health.test.ts @@ -279,6 +279,8 @@ describe("GET /health", () => { it("surfaces redacted database backup warnings for anonymous authenticated probes", async () => { const backupDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-health-redacted-backups-")); const backupFile = path.join(backupDir, "paperclip-20260705-031702.sql.gz"); + const snapshotMarkerDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-health-redacted-snapshots-")); + fs.writeFileSync(path.join(snapshotMarkerDir, "state-snapshot.failure"), "failed reading /srv/paperclip/tenant-secret/state.json from https://backend.internal\n"); fs.writeFileSync(backupFile, "backup"); fs.utimesSync( backupFile, @@ -313,6 +315,11 @@ describe("GET /health", () => { maxAgeHours: 26, now: new Date("2026-07-06T13:00:00.000Z"), }, + stateSnapshotHealth: { + enabled: true, + markerDir: snapshotMarkerDir, + maxAgeHours: 48, + }, }), ); @@ -336,11 +343,21 @@ describe("GET /health", () => { }, ], }, + stateSnapshot: { + enabled: true, + status: "warning", + warnings: [ + { code: "instance_state_snapshot_missing", message: "No recent instance-state snapshot was found." }, + { code: "instance_state_snapshot_last_failure", message: "Instance-state snapshot failure marker is present." }, + ], + }, warnings: [ { code: "database_backup_stale", message: "Latest database backup is stale.", }, + { code: "instance_state_snapshot_missing", message: "No recent instance-state snapshot was found." }, + { code: "instance_state_snapshot_last_failure", message: "Instance-state snapshot failure marker is present." }, ], }); }); diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts index 5b55ccf1fa1..6034108f5ed 100644 --- a/server/src/routes/health.ts +++ b/server/src/routes/health.ts @@ -74,6 +74,23 @@ function getCloudHealthStatus(env: CloudInstanceEnv) { }; } +function redactedStateSnapshotWarning(warning: { code: string; message: string }) { + const messages: Record = { + instance_state_snapshot_last_failure: "Instance-state snapshot failure marker is present.", + instance_state_snapshot_missing: "No recent instance-state snapshot was found.", + instance_state_snapshot_stale: "Latest instance-state snapshot is stale.", + }; + return { code: warning.code, message: messages[warning.code] ?? "Instance-state snapshot health warning." }; +} + +function redactedStateSnapshotHealth(stateSnapshot: ReturnType) { + return { + enabled: stateSnapshot.enabled, + status: stateSnapshot.status, + warnings: stateSnapshot.warnings.map(redactedStateSnapshotWarning), + }; +} + export function healthRoutes( db?: Db, opts: { @@ -249,7 +266,9 @@ export function healthRoutes( if (!exposeFullDetails) { const redactedDatabaseBackup = databaseBackup ? redactedDatabaseBackupHealth(databaseBackup) : undefined; - const redactedWarnings = redactedDatabaseBackup?.warnings.length ? redactedDatabaseBackup.warnings : undefined; + const redactedStateSnapshot = stateSnapshot ? redactedStateSnapshotHealth(stateSnapshot) : undefined; + const combinedRedactedWarnings = [...(redactedDatabaseBackup?.warnings ?? []), ...(redactedStateSnapshot?.warnings ?? [])]; + const redactedWarnings = combinedRedactedWarnings.length ? combinedRedactedWarnings : undefined; res.json({ status: "ok", deploymentMode: opts.deploymentMode, @@ -258,7 +277,7 @@ export function healthRoutes( bootstrapStatus, bootstrapInviteActive, ...(redactedDatabaseBackup ? { databaseBackup: redactedDatabaseBackup } : {}), - ...(stateSnapshot ? { stateSnapshot: { enabled: stateSnapshot.enabled, status: stateSnapshot.status, warnings: stateSnapshot.warnings } } : {}), + ...(redactedStateSnapshot ? { stateSnapshot: redactedStateSnapshot } : {}), ...(redactedWarnings ? { warnings: redactedWarnings } : {}), ...(devServer ? { devServer } : {}), ...(cloud ? { cloud } : {}),