feat: add operator refresh and force-sync controls

This commit is contained in:
OpenClaw Bot
2026-05-09 12:49:35 +02:00
parent b33ed20238
commit 2d2febb7aa
3 changed files with 87 additions and 6 deletions
+60 -4
View File
@@ -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<string | null>(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() {
</header>
<section className="status-strip card">
<div>
<div className="status-strip-row">
<span className={`pill status-${backendMode === 'appwrite' ? 'healthy' : backendMode === 'connecting' ? 'connecting' : 'degraded'}`}>
{backendLabel}
</span>
</div>
<div>
<span className={`pill status-${syncStatus === 'synced' ? 'healthy' : syncStatus === 'pending' || syncStatus === 'syncing' || syncStatus === 'connecting' ? 'connecting' : 'degraded'}`}>
{syncLabel}
</span>
</div>
<div className="button-inline-row">
<button type="button" className="ghost small" disabled={syncAction !== 'idle'} onClick={() => void refreshFromBackend()}>
{syncAction === 'refreshing' ? 'Refreshing…' : 'Refresh from backend'}
</button>
<button type="button" className="ghost small" disabled={syncAction !== 'idle'} onClick={() => void forceSyncNow()}>
{syncAction === 'pushing' ? 'Syncing…' : 'Force sync now'}
</button>
</div>
</section>
<section className="project-card card">