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">
|
||||
|
||||
Reference in New Issue
Block a user