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 [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,11 +128,19 @@ 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')
|
||||||
setBackendMode('local-cache')
|
void pushRemoteState(appState)
|
||||||
setStatusMessage('Saved locally. Appwrite sync tripped over itself, so the cache is carrying the load.')
|
.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)
|
}, 350)
|
||||||
|
|
||||||
return () => window.clearTimeout(timer)
|
return () => window.clearTimeout(timer)
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user