fix(dashboard): make assistant-health saves resilient to partial refresh failures

This commit is contained in:
William Valentin
2026-02-18 18:14:22 -08:00
parent 3647198295
commit 45261a090a
2 changed files with 62 additions and 17 deletions
+11
View File
@@ -5647,6 +5647,17 @@
"docs/plans/state.json" "docs/plans/state.json"
], ],
"test_status": "pnpm typecheck passing" "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": { "overall_progress": {
+45 -11
View File
@@ -491,13 +491,18 @@ function updateContextHealth(contextData) {
} }
async function applyAssistantPatch(patches, statusEl) { 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) { if (statusEl) {
statusEl.textContent = 'Saving...'; statusEl.textContent = 'Saving...';
statusEl.className = 'text-sm text-zinc-500'; statusEl.className = 'text-sm text-zinc-500';
} }
try { try {
const result = await _dashboardClient.call('config.patch', { patches }); const result = await _dashboardClient.call('config.patch', { patches });
console.log('[Flynn] config.patch result:', JSON.stringify(result));
const rejected = result?.rejected ?? []; const rejected = result?.rejected ?? [];
const persistError = result?.persistError; const persistError = result?.persistError;
const applied = result?.applied ?? []; const applied = result?.applied ?? [];
@@ -512,13 +517,14 @@ async function applyAssistantPatch(patches, statusEl) {
statusEl.className = 'text-sm text-red-500'; statusEl.className = 'text-sm text-red-500';
} else if (!persisted) { } else if (!persisted) {
statusEl.textContent = `Runtime saved (${applied.length} updated)`; statusEl.textContent = `Runtime saved (${applied.length} updated)`;
statusEl.className = 'text-sm text-zinc-500'; statusEl.className = 'text-sm text-amber-500';
} else { } else {
statusEl.textContent = `Saved (${applied.length} updated)`; statusEl.textContent = `Saved & persisted (${applied.length} updated)`;
statusEl.className = 'text-sm text-green-500'; statusEl.className = 'text-sm text-green-500';
} }
} }
} catch (error) { } catch (error) {
console.error('[Flynn] config.patch error:', error);
if (statusEl) { if (statusEl) {
statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`; statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`;
statusEl.className = 'text-sm text-red-500'; statusEl.className = 'text-sm text-red-500';
@@ -758,7 +764,16 @@ function updateAssistantHealth(configData) {
updateServices(refreshed.services); updateServices(refreshed.services);
updateSessionAnalytics(refreshed.sessionAnalytics); updateSessionAnalytics(refreshed.sessionAnalytics);
updateContextHealth(refreshed.contextUsage); updateContextHealth(refreshed.contextUsage);
// Capture status message before re-render destroys the DOM element
const savedText = statusEl?.textContent ?? '';
const savedClass = statusEl?.className ?? '';
updateAssistantHealth(refreshed.config); 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) { async function fetchSlow(client) {
try { const [health, services, sessionAnalytics, contextUsage, config] = await Promise.allSettled([
const [health, services, sessionAnalytics, contextUsage, config] = await Promise.all([
client.call('system.health'), client.call('system.health'),
client.call('system.services'), client.call('system.services'),
client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }), client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }),
client.call('system.contextUsage'), client.call('system.contextUsage'),
client.call('config.get'), client.call('config.get'),
]); ]);
return { health, services, sessionAnalytics, contextUsage, config };
} catch { const unwrap = (result) => (result.status === 'fulfilled' ? result.value : null);
return null;
} return {
health: unwrap(health),
services: unwrap(services),
sessionAnalytics: unwrap(sessionAnalytics),
contextUsage: unwrap(contextUsage),
config: unwrap(config),
};
} }
// ── Main load function ────────────────────────────────────────── // ── Main load function ──────────────────────────────────────────
@@ -877,10 +897,16 @@ async function loadDashboard(el, client) {
updateEvents(fast.eventsData); updateEvents(fast.eventsData);
updateActiveRequests(fast.requestsData); updateActiveRequests(fast.requestsData);
} }
if (slow) { if (slow?.services) {
updateServices(slow.services); updateServices(slow.services);
}
if (slow?.sessionAnalytics) {
updateSessionAnalytics(slow.sessionAnalytics); updateSessionAnalytics(slow.sessionAnalytics);
}
if (slow?.contextUsage) {
updateContextHealth(slow.contextUsage); updateContextHealth(slow.contextUsage);
}
if (slow?.config) {
updateAssistantHealth(slow.config); updateAssistantHealth(slow.config);
} }
@@ -899,12 +925,20 @@ async function loadDashboard(el, client) {
// Slow refresh: 10 seconds for health, services // Slow refresh: 10 seconds for health, services
_slowTimer = setInterval(async () => { _slowTimer = setInterval(async () => {
const data = await fetchSlow(client); const data = await fetchSlow(client);
if (data) { if (data.health) {
_lastHealth = data.health; _lastHealth = data.health;
updateCounters(_lastMetrics, data.health); updateCounters(_lastMetrics, data.health);
}
if (data.services) {
updateServices(data.services); updateServices(data.services);
}
if (data.sessionAnalytics) {
updateSessionAnalytics(data.sessionAnalytics); updateSessionAnalytics(data.sessionAnalytics);
}
if (data.contextUsage) {
updateContextHealth(data.contextUsage); updateContextHealth(data.contextUsage);
}
if (data.config) {
updateAssistantHealth(data.config); updateAssistantHealth(data.config);
} }
}, 10000); }, 10000);