feat: surface backend sync health in cockpit
This commit is contained in:
+43
-5
@@ -79,6 +79,8 @@ function App() {
|
||||
const [promptTarget, setPromptTarget] = useState<(typeof PROMPT_TARGETS)[number]>('OpenClaw')
|
||||
const [promptFeatureId, setPromptFeatureId] = useState('')
|
||||
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 initialLocalStateRef = useRef(appState)
|
||||
|
||||
@@ -104,9 +106,12 @@ function App() {
|
||||
setStatusMessage('Seeded Appwrite on Unraid from the local BuildPulse state.')
|
||||
}
|
||||
setBackendMode('appwrite')
|
||||
setSyncStatus('synced')
|
||||
setLastSyncedAt(nowIso())
|
||||
} catch {
|
||||
if (cancelled) return
|
||||
setBackendMode('local-cache')
|
||||
setSyncStatus('degraded')
|
||||
setStatusMessage('Appwrite backend unavailable, so BuildPulse is using the local cache for now.')
|
||||
} finally {
|
||||
hasHydratedRemote.current = true
|
||||
@@ -123,11 +128,19 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (!hasHydratedRemote.current || backendMode !== 'appwrite') return
|
||||
|
||||
setSyncStatus('pending')
|
||||
const timer = window.setTimeout(() => {
|
||||
void pushRemoteState(appState).catch(() => {
|
||||
setBackendMode('local-cache')
|
||||
setStatusMessage('Saved locally. Appwrite sync tripped over itself, so the cache is carrying the load.')
|
||||
})
|
||||
setSyncStatus('syncing')
|
||||
void pushRemoteState(appState)
|
||||
.then(() => {
|
||||
setSyncStatus('synced')
|
||||
setLastSyncedAt(nowIso())
|
||||
})
|
||||
.catch(() => {
|
||||
setBackendMode('local-cache')
|
||||
setSyncStatus('degraded')
|
||||
setStatusMessage('Saved locally. Appwrite sync tripped over itself, so the cache is carrying the load.')
|
||||
})
|
||||
}, 350)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
@@ -548,6 +561,18 @@ function App() {
|
||||
|
||||
const currentFeatureCount = groupedFeatures.now.length
|
||||
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 (
|
||||
<div className="app-shell">
|
||||
@@ -576,6 +601,19 @@ function App() {
|
||||
</div>
|
||||
</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">
|
||||
<div className="section-heading compact">
|
||||
<div>
|
||||
@@ -1209,7 +1247,7 @@ function App() {
|
||||
|
||||
<footer className="status-bar">
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -106,6 +106,7 @@ body::before {
|
||||
|
||||
.hero-stats div,
|
||||
.quick-actions,
|
||||
.status-strip,
|
||||
.status-bar,
|
||||
.tab {
|
||||
border-radius: 18px;
|
||||
@@ -123,6 +124,7 @@ body::before {
|
||||
.hero-stats span,
|
||||
.meta-row,
|
||||
small,
|
||||
.status-strip,
|
||||
.status-bar {
|
||||
color: #b7c4db;
|
||||
}
|
||||
@@ -135,6 +137,7 @@ small,
|
||||
.project-card,
|
||||
.quick-actions,
|
||||
.focus-card,
|
||||
.status-strip,
|
||||
.tab-bar,
|
||||
.status-bar,
|
||||
.view-stack,
|
||||
@@ -355,6 +358,7 @@ button.small {
|
||||
.item-card-header,
|
||||
.meta-row,
|
||||
.feature-signal-row,
|
||||
.status-strip,
|
||||
.status-bar,
|
||||
.filters-heading {
|
||||
display: flex;
|
||||
@@ -487,12 +491,33 @@ pre {
|
||||
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 {
|
||||
padding: 0.95rem 1.1rem;
|
||||
background: rgba(15, 23, 42, 0.78);
|
||||
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 {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -524,6 +549,7 @@ pre {
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.status-strip,
|
||||
.status-bar {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -537,6 +563,7 @@ pre {
|
||||
.tab-bar,
|
||||
.button-row,
|
||||
.filter-row,
|
||||
.status-strip,
|
||||
.status-bar,
|
||||
.feature-signal-row {
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user