From 2d2febb7aacbf02e2c70f309322c327932b8dc8b Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Sat, 9 May 2026 12:49:35 +0200 Subject: [PATCH] feat: add operator refresh and force-sync controls --- src/App.tsx | 64 ++++++++++++++++++++++++++++++++++++++++++--- src/index.css | 19 +++++++++++++- src/store/remote.ts | 10 ++++++- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7622a11..5877173 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import type { ChangeEvent } from 'react' import './index.css' import { createAgentSessionPrompt, createMarkdownPackage, createJsonExport, createPulseJsonl } from './features/export/exporters' import { loadAppState, replaceAppState, saveAppState, validateAppState } from './store/storage' -import { fetchRemoteState, pushRemoteState } from './store/remote' +import { fetchBackendHealth, fetchRemoteState, pushRemoteState } from './store/remote' import { FEATURE_COLUMNS, FEATURE_PRIORITIES, FEATURE_STATUSES, PULSE_TYPES, RISK_LEVELS } from './store/types' import type { AppState, Feature, FeatureColumn, ParkingLotItem, PulseEvent, RiskLevel, TabKey } from './store/types' import { arrayToLines, formatDateTime, linesToArray, nowIso, slugify } from './utils/format' @@ -81,6 +81,7 @@ function App() { const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting') const [syncStatus, setSyncStatus] = useState<'connecting' | 'synced' | 'pending' | 'syncing' | 'degraded'>('connecting') const [lastSyncedAt, setLastSyncedAt] = useState(null) + const [syncAction, setSyncAction] = useState<'idle' | 'refreshing' | 'pushing'>('idle') const hasHydratedRemote = useRef(false) const initialLocalStateRef = useRef(appState) @@ -319,6 +320,55 @@ function App() { })) } + const refreshFromBackend = async () => { + setSyncAction('refreshing') + setSyncStatus('syncing') + + try { + const [health, remoteState] = await Promise.all([fetchBackendHealth(), fetchRemoteState()]) + + if (!health.ok) throw new Error('Backend health check failed.') + + if (remoteState && validateAppState(remoteState)) { + setAppState(remoteState) + saveAppState(remoteState) + setBackendMode('appwrite') + setSyncStatus('synced') + setLastSyncedAt(nowIso()) + setStatusMessage('Reloaded BuildPulse state from Appwrite.') + } else { + setBackendMode('appwrite') + setSyncStatus('synced') + setStatusMessage('Backend reachable, but there is no valid remote state to reload yet.') + } + } catch { + setBackendMode('local-cache') + setSyncStatus('degraded') + setStatusMessage('Refresh failed. Staying on the local cache until Appwrite behaves again.') + } finally { + setSyncAction('idle') + } + } + + const forceSyncNow = async () => { + setSyncAction('pushing') + setSyncStatus('syncing') + + try { + await pushRemoteState(appState) + setBackendMode('appwrite') + setSyncStatus('synced') + setLastSyncedAt(nowIso()) + setStatusMessage('Forced a clean sync to Appwrite.') + } catch { + setBackendMode('local-cache') + setSyncStatus('degraded') + setStatusMessage('Forced sync failed. Local cache still has the wheel.') + } finally { + setSyncAction('idle') + } + } + const openFeatureHandoff = (featureId: string) => { setPromptFeatureId(featureId) setPromptTarget('OpenClaw') @@ -602,16 +652,22 @@ function App() {
-
+
{backendLabel} -
-
{syncLabel}
+
+ + +
diff --git a/src/index.css b/src/index.css index c7060e7..2f202aa 100644 --- a/src/index.css +++ b/src/index.css @@ -326,6 +326,13 @@ button.small { padding: 0.45rem 0.75rem; } +button:disabled, +.import-label:disabled { + opacity: 0.65; + cursor: wait; + transform: none; +} + .import-label { display: inline-flex; align-items: center; @@ -377,7 +384,8 @@ pre { .column-body, .list-stack, -.markdown-list { +.markdown-list, +.status-strip-row { display: flex; flex-direction: column; gap: 0.75rem; @@ -497,6 +505,13 @@ pre { border: 1px solid rgba(148, 163, 184, 0.14); } +.status-strip-row { + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; +} + .status-bar { padding: 0.95rem 1.1rem; background: rgba(15, 23, 42, 0.78); @@ -562,8 +577,10 @@ pre { .quick-actions, .tab-bar, .button-row, + .button-inline-row, .filter-row, .status-strip, + .status-strip-row, .status-bar, .feature-signal-row { flex-direction: column; diff --git a/src/store/remote.ts b/src/store/remote.ts index 6ecaf00..7bc2fe7 100644 --- a/src/store/remote.ts +++ b/src/store/remote.ts @@ -46,4 +46,12 @@ export const pushRemoteState = async (state: AppState) => { }) } -export const fetchBackendHealth = async () => request<{ ok: boolean; backend: string; rows: number }>('/api/health') +export const fetchBackendHealth = async () => + request<{ + ok: boolean + backend: string + endpoint: string + databaseId: string + collectionId: string + documentPresent: boolean + }>('/api/health')