diff --git a/src/App.tsx b/src/App.tsx index a830fc5..7622a11 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(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 (
@@ -576,6 +601,19 @@ function App() {
+
+
+ + {backendLabel} + +
+
+ + {syncLabel} + +
+
+
@@ -1209,7 +1247,7 @@ function App() {
) diff --git a/src/index.css b/src/index.css index d45d821..c7060e7 100644 --- a/src/index.css +++ b/src/index.css @@ -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;