feat: add operator refresh and force-sync controls
This commit is contained in:
+60
-4
@@ -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">
|
||||
|
||||
+18
-1
@@ -326,6 +326,13 @@ button.small {
|
||||
padding: 0.45rem 0.75rem;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
.import-label:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.import-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -377,7 +384,8 @@ pre {
|
||||
|
||||
.column-body,
|
||||
.list-stack,
|
||||
.markdown-list {
|
||||
.markdown-list,
|
||||
.status-strip-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
@@ -497,6 +505,13 @@ pre {
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.status-strip-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
padding: 0.95rem 1.1rem;
|
||||
background: rgba(15, 23, 42, 0.78);
|
||||
@@ -562,8 +577,10 @@ pre {
|
||||
.quick-actions,
|
||||
.tab-bar,
|
||||
.button-row,
|
||||
.button-inline-row,
|
||||
.filter-row,
|
||||
.status-strip,
|
||||
.status-strip-row,
|
||||
.status-bar,
|
||||
.feature-signal-row {
|
||||
flex-direction: column;
|
||||
|
||||
+9
-1
@@ -46,4 +46,12 @@ export const pushRemoteState = async (state: AppState) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchBackendHealth = async () => request<{ ok: boolean; backend: string; rows: number }>('/api/health')
|
||||
export const fetchBackendHealth = async () =>
|
||||
request<{
|
||||
ok: boolean
|
||||
backend: string
|
||||
endpoint: string
|
||||
databaseId: string
|
||||
collectionId: string
|
||||
documentPresent: boolean
|
||||
}>('/api/health')
|
||||
|
||||
Reference in New Issue
Block a user