diff --git a/src/client/api.ts b/src/client/api.ts index 317542891..582db0f5f 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -128,6 +128,7 @@ export interface SyncResponse { success: boolean; message?: string; lastSync?: string; + debug?: object; error?: string; details?: string; } diff --git a/src/persistence.ts b/src/persistence.ts index 2279a1f4a..a17de144a 100644 --- a/src/persistence.ts +++ b/src/persistence.ts @@ -136,9 +136,10 @@ export async function createSnapshot( } /** - * Get the last stored backup handle (for status reporting). + * Get the timestamp of the last sync (for status reporting). */ -export async function getLastBackupId(bucket: R2Bucket): Promise { - const handle = await getStoredHandle(bucket); - return handle?.id ?? null; +export async function getLastSync(bucket: R2Bucket): Promise { + const obj = await bucket.get(HANDLE_KEY); + if (!obj) return null; + return obj.uploaded.toISOString(); } diff --git a/src/routes/api.ts b/src/routes/api.ts index 0bdc41905..20536dbb3 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -2,7 +2,8 @@ import { Hono } from 'hono'; import type { AppEnv } from '../types'; import { createAccessMiddleware } from '../auth'; import { ensureGateway, findExistingGatewayProcess, killGateway, waitForProcess } from '../gateway'; -import { createSnapshot, getLastBackupId, signalRestoreNeeded } from '../persistence'; +import { createSnapshot, getLastSync, signalRestoreNeeded } from '../persistence'; +import type { StorageStatusResponse, SyncResponse } from '../client/api'; // CLI commands can take 10-15 seconds to complete due to WebSocket connection overhead const CLI_TIMEOUT_MS = 20000; @@ -203,16 +204,16 @@ adminApi.get('/storage', async (c) => { if (!c.env.R2_SECRET_ACCESS_KEY) missing.push('R2_SECRET_ACCESS_KEY'); if (!c.env.CLOUDFLARE_ACCOUNT_ID) missing.push('CLOUDFLARE_ACCOUNT_ID'); - const lastBackupId = hasCredentials ? await getLastBackupId(c.env.BACKUP_BUCKET) : null; + const lastSync = hasCredentials ? await getLastSync(c.env.BACKUP_BUCKET) : null; return c.json({ configured: hasCredentials, missing: missing.length > 0 ? missing : undefined, - lastBackupId, + lastSync, message: hasCredentials ? 'R2 storage is configured. Your data will persist across container restarts via SDK snapshots.' : 'R2 storage is not configured. Paired devices and conversations will be lost when the container restarts.', - }); + } satisfies StorageStatusResponse); }); // POST /api/admin/storage/sync - Create a new snapshot @@ -235,9 +236,9 @@ adminApi.post('/storage/sync', async (c) => { return c.json({ success: true, message: 'Snapshot created successfully', - backupId: handle.id, - debug: { mountState, dirContents }, - }); + lastSync: new Date().toISOString(), + debug: { mountState, dirContents, handle }, + } satisfies SyncResponse); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const status = diff --git a/test/e2e/r2_persistence.txt b/test/e2e/r2_persistence.txt index e8120f907..1b2e7679e 100644 --- a/test/e2e/r2_persistence.txt +++ b/test/e2e/r2_persistence.txt @@ -43,7 +43,7 @@ where * result.success == true === -storage status shows last backup id +storage status shows last sync time %require === WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt") @@ -53,7 +53,7 @@ WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt") --- where * result.configured == true -* result.lastBackupId matches /^[0-9a-f-]+$/ +* result.lastSync matches /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ === third sync also succeeds