fix(dashboard): make assistant-health saves resilient to partial refresh failures
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user