From d70a5ace7f18674563bd7c4c3ecda795c977d7f4 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Sat, 11 Apr 2026 21:52:19 +0200 Subject: [PATCH 1/3] fix: Last Sync Timestamp Display for Backups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …before it was always showing "Last backup: Never", since lastSync was not passed from the API: --- src/persistence.ts | 9 +++++---- src/routes/api.ts | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) 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..955da619c 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 } 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 From 2a918e93e8706f439c81d92e004f1a240ca041b4 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Sat, 11 Apr 2026 21:56:11 +0200 Subject: [PATCH 2/3] fix: Update Last Sync Time Display in E2E Tests Changed the storage status message from displaying the last backup ID to showing the last sync time. Updated the validation regex for lastSync to ensure it matches the correct timestamp format. --- test/e2e/r2_persistence.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 32de979867cd1265b4958f4910c4366fa2b9c669 Mon Sep 17 00:00:00 2001 From: Anton Begehr Date: Sat, 11 Apr 2026 22:01:26 +0200 Subject: [PATCH 3/3] feat: Enhance SyncResponse with Debug Information Added a debug property to the SyncResponse interface to include additional context during storage sync operations. Updated the API response to return the last sync timestamp and detailed debug information, improving the clarity of sync status responses. --- src/client/api.ts | 1 + src/routes/api.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) 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/routes/api.ts b/src/routes/api.ts index 955da619c..20536dbb3 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -3,7 +3,7 @@ import type { AppEnv } from '../types'; import { createAccessMiddleware } from '../auth'; import { ensureGateway, findExistingGatewayProcess, killGateway, waitForProcess } from '../gateway'; import { createSnapshot, getLastSync, signalRestoreNeeded } from '../persistence'; -import type { StorageStatusResponse } from '../client/api'; +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; @@ -236,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 =