feat: surface backend sync health in cockpit

This commit is contained in:
OpenClaw Bot
2026-05-09 10:20:30 +02:00
parent d0806dccb8
commit b33ed20238
2 changed files with 70 additions and 5 deletions
+40 -2
View File
@@ -79,6 +79,8 @@ function App() {
const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw') const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw')
const [promptFeatureId, setPromptFeatureId] = useState('') const [promptFeatureId, setPromptFeatureId] = useState('')
const [backendMode, setBackendMode] = useState<'connecting' | 'appwrite' | 'local-cache'>('connecting') 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 hasHydratedRemote = useRef(false) const hasHydratedRemote = useRef(false)
const initialLocalStateRef = useRef(appState) const initialLocalStateRef = useRef(appState)
@@ -104,9 +106,12 @@ function App() {
setStatusMessage('Seeded Appwrite on Unraid from the local BuildPulse state.') setStatusMessage('Seeded Appwrite on Unraid from the local BuildPulse state.')
} }
setBackendMode('appwrite') setBackendMode('appwrite')
setSyncStatus('synced')
setLastSyncedAt(nowIso())
} catch { } catch {
if (cancelled) return if (cancelled) return
setBackendMode('local-cache') setBackendMode('local-cache')
setSyncStatus('degraded')
setStatusMessage('Appwrite backend unavailable, so BuildPulse is using the local cache for now.') setStatusMessage('Appwrite backend unavailable, so BuildPulse is using the local cache for now.')
} finally { } finally {
hasHydratedRemote.current = true hasHydratedRemote.current = true
@@ -123,9 +128,17 @@ function App() {
useEffect(() => { useEffect(() => {
if (!hasHydratedRemote.current || backendMode !== 'appwrite') return if (!hasHydratedRemote.current || backendMode !== 'appwrite') return
setSyncStatus('pending')
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
void pushRemoteState(appState).catch(() => { setSyncStatus('syncing')
void pushRemoteState(appState)
.then(() => {
setSyncStatus('synced')
setLastSyncedAt(nowIso())
})
.catch(() => {
setBackendMode('local-cache') setBackendMode('local-cache')
setSyncStatus('degraded')
setStatusMessage('Saved locally. Appwrite sync tripped over itself, so the cache is carrying the load.') setStatusMessage('Saved locally. Appwrite sync tripped over itself, so the cache is carrying the load.')
}) })
}, 350) }, 350)
@@ -548,6 +561,18 @@ function App() {
const currentFeatureCount = groupedFeatures.now.length const currentFeatureCount = groupedFeatures.now.length
const recentPulsePreview = [...appState.pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 3) const recentPulsePreview = [...appState.pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 3)
const backendLabel =
backendMode === 'appwrite' ? 'Appwrite backend · Unraid server' : backendMode === 'connecting' ? 'Connecting to Appwrite…' : 'Local cache fallback'
const syncLabel =
syncStatus === 'synced'
? `Synced${lastSyncedAt ? ` ${formatDateTime(lastSyncedAt)}` : ''}`
: syncStatus === 'pending'
? 'Changes queued'
: syncStatus === 'syncing'
? 'Syncing now…'
: syncStatus === 'degraded'
? 'Sync degraded · local cache active'
: 'Connecting…'
return ( return (
<div className="app-shell"> <div className="app-shell">
@@ -576,6 +601,19 @@ function App() {
</div> </div>
</header> </header>
<section className="status-strip card">
<div>
<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>
</section>
<section className="project-card card"> <section className="project-card card">
<div className="section-heading compact"> <div className="section-heading compact">
<div> <div>
@@ -1209,7 +1247,7 @@ function App() {
<footer className="status-bar"> <footer className="status-bar">
<span>{statusMessage}</span> <span>{statusMessage}</span>
<span>{backendMode === 'appwrite' ? 'Appwrite backend · Unraid server' : backendMode === 'connecting' ? 'Connecting to Appwrite…' : 'Local cache fallback'} · schema {appState.schema_version}</span> <span>{backendLabel} · {syncLabel} · schema {appState.schema_version}</span>
</footer> </footer>
</div> </div>
) )
+27
View File
@@ -106,6 +106,7 @@ body::before {
.hero-stats div, .hero-stats div,
.quick-actions, .quick-actions,
.status-strip,
.status-bar, .status-bar,
.tab { .tab {
border-radius: 18px; border-radius: 18px;
@@ -123,6 +124,7 @@ body::before {
.hero-stats span, .hero-stats span,
.meta-row, .meta-row,
small, small,
.status-strip,
.status-bar { .status-bar {
color: #b7c4db; color: #b7c4db;
} }
@@ -135,6 +137,7 @@ small,
.project-card, .project-card,
.quick-actions, .quick-actions,
.focus-card, .focus-card,
.status-strip,
.tab-bar, .tab-bar,
.status-bar, .status-bar,
.view-stack, .view-stack,
@@ -355,6 +358,7 @@ button.small {
.item-card-header, .item-card-header,
.meta-row, .meta-row,
.feature-signal-row, .feature-signal-row,
.status-strip,
.status-bar, .status-bar,
.filters-heading { .filters-heading {
display: flex; display: flex;
@@ -487,12 +491,33 @@ pre {
color: #dbe7ff; color: #dbe7ff;
} }
.status-strip {
padding: 0.95rem 1.1rem;
background: rgba(15, 23, 42, 0.72);
border: 1px solid rgba(148, 163, 184, 0.14);
}
.status-bar { .status-bar {
padding: 0.95rem 1.1rem; padding: 0.95rem 1.1rem;
background: rgba(15, 23, 42, 0.78); background: rgba(15, 23, 42, 0.78);
border: 1px solid rgba(148, 163, 184, 0.14); border: 1px solid rgba(148, 163, 184, 0.14);
} }
.pill.status-healthy {
background: rgba(52, 211, 153, 0.16);
color: #bbf7d0;
}
.pill.status-connecting {
background: rgba(96, 165, 250, 0.18);
color: #bfdbfe;
}
.pill.status-degraded {
background: rgba(248, 113, 113, 0.18);
color: #fecaca;
}
.sr-only { .sr-only {
position: absolute; position: absolute;
width: 1px; width: 1px;
@@ -524,6 +549,7 @@ pre {
} }
.hero-card, .hero-card,
.status-strip,
.status-bar { .status-bar {
flex-direction: column; flex-direction: column;
} }
@@ -537,6 +563,7 @@ pre {
.tab-bar, .tab-bar,
.button-row, .button-row,
.filter-row, .filter-row,
.status-strip,
.status-bar, .status-bar,
.feature-signal-row { .feature-signal-row {
flex-direction: column; flex-direction: column;