diff --git a/docs/plans/state.json b/docs/plans/state.json index e720e6f..8f68fca 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -5647,6 +5647,17 @@ "docs/plans/state.json" ], "test_status": "pnpm typecheck passing" + }, + "dashboard-assistant-health-save-reliability": { + "status": "completed", + "date": "2026-02-19", + "updated": "2026-02-19", + "summary": "Hardened dashboard slow-refresh behavior so Assistant Health config state still updates when non-config slow endpoints fail. `fetchSlow` now uses per-call settlement and partial updates, preventing toggle saves from appearing unsaved due to unrelated fetch failures.", + "files_modified": [ + "src/gateway/ui/pages/dashboard.js", + "docs/plans/state.json" + ], + "test_status": "pnpm typecheck passing" } }, "overall_progress": { diff --git a/src/gateway/ui/pages/dashboard.js b/src/gateway/ui/pages/dashboard.js index 5fb29eb..bde4007 100644 --- a/src/gateway/ui/pages/dashboard.js +++ b/src/gateway/ui/pages/dashboard.js @@ -491,13 +491,18 @@ function updateContextHealth(contextData) { } async function applyAssistantPatch(patches, statusEl) { - if (!_dashboardClient) {return;} + if (!_dashboardClient) { + console.warn('[Flynn] applyAssistantPatch: no client'); + return; + } + console.log('[Flynn] applyAssistantPatch:', JSON.stringify(patches)); if (statusEl) { statusEl.textContent = 'Saving...'; statusEl.className = 'text-sm text-zinc-500'; } try { const result = await _dashboardClient.call('config.patch', { patches }); + console.log('[Flynn] config.patch result:', JSON.stringify(result)); const rejected = result?.rejected ?? []; const persistError = result?.persistError; const applied = result?.applied ?? []; @@ -512,13 +517,14 @@ async function applyAssistantPatch(patches, statusEl) { statusEl.className = 'text-sm text-red-500'; } else if (!persisted) { statusEl.textContent = `Runtime saved (${applied.length} updated)`; - statusEl.className = 'text-sm text-zinc-500'; + statusEl.className = 'text-sm text-amber-500'; } else { - statusEl.textContent = `Saved (${applied.length} updated)`; + statusEl.textContent = `Saved & persisted (${applied.length} updated)`; statusEl.className = 'text-sm text-green-500'; } } } catch (error) { + console.error('[Flynn] config.patch error:', error); if (statusEl) { statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`; statusEl.className = 'text-sm text-red-500'; @@ -758,7 +764,16 @@ function updateAssistantHealth(configData) { updateServices(refreshed.services); updateSessionAnalytics(refreshed.sessionAnalytics); updateContextHealth(refreshed.contextUsage); + // Capture status message before re-render destroys the DOM element + const savedText = statusEl?.textContent ?? ''; + const savedClass = statusEl?.className ?? ''; updateAssistantHealth(refreshed.config); + // Restore status message in the new DOM + const newStatusEl = document.getElementById('ops-assistant-status'); + if (newStatusEl && savedText) { + newStatusEl.textContent = savedText; + newStatusEl.className = savedClass; + } } }); }); @@ -839,18 +854,23 @@ async function fetchFast(client) { } async function fetchSlow(client) { - try { - const [health, services, sessionAnalytics, contextUsage, config] = await Promise.all([ - client.call('system.health'), - client.call('system.services'), - client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }), - client.call('system.contextUsage'), - client.call('config.get'), - ]); - return { health, services, sessionAnalytics, contextUsage, config }; - } catch { - return null; - } + const [health, services, sessionAnalytics, contextUsage, config] = await Promise.allSettled([ + client.call('system.health'), + client.call('system.services'), + client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }), + client.call('system.contextUsage'), + client.call('config.get'), + ]); + + const unwrap = (result) => (result.status === 'fulfilled' ? result.value : null); + + return { + health: unwrap(health), + services: unwrap(services), + sessionAnalytics: unwrap(sessionAnalytics), + contextUsage: unwrap(contextUsage), + config: unwrap(config), + }; } // ── Main load function ────────────────────────────────────────── @@ -877,10 +897,16 @@ async function loadDashboard(el, client) { updateEvents(fast.eventsData); updateActiveRequests(fast.requestsData); } - if (slow) { + if (slow?.services) { updateServices(slow.services); + } + if (slow?.sessionAnalytics) { updateSessionAnalytics(slow.sessionAnalytics); + } + if (slow?.contextUsage) { updateContextHealth(slow.contextUsage); + } + if (slow?.config) { updateAssistantHealth(slow.config); } @@ -899,12 +925,20 @@ async function loadDashboard(el, client) { // Slow refresh: 10 seconds for health, services _slowTimer = setInterval(async () => { const data = await fetchSlow(client); - if (data) { + if (data.health) { _lastHealth = data.health; updateCounters(_lastMetrics, data.health); + } + if (data.services) { updateServices(data.services); + } + if (data.sessionAnalytics) { updateSessionAnalytics(data.sessionAnalytics); + } + if (data.contextUsage) { updateContextHealth(data.contextUsage); + } + if (data.config) { updateAssistantHealth(data.config); } }, 10000);