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