From 0a664ddb2164096df324b699c3bb4fb57ef72505 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 18 Feb 2026 18:25:04 -0800 Subject: [PATCH] feat(dashboard): show persistent assistant-health save status --- docs/plans/state.json | 11 +++++ src/gateway/ui/pages/dashboard.js | 74 ++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index 8f68fca..c0ff662 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -5658,6 +5658,17 @@ "docs/plans/state.json" ], "test_status": "pnpm typecheck passing" + }, + "dashboard-assistant-health-persistence-indicator": { + "status": "completed", + "date": "2026-02-19", + "updated": "2026-02-19", + "summary": "Added a persistent Assistant Health save-state indicator that survives rerenders and explicitly reports whether updates were saved runtime-only or persisted to config file, with timestamped status messages.", + "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 bde4007..905510d 100644 --- a/src/gateway/ui/pages/dashboard.js +++ b/src/gateway/ui/pages/dashboard.js @@ -10,6 +10,7 @@ let _slowTimer = null; let _dashboardClient = null; let _lastPlaybookRollbackPatches = null; let _lastBriefingTestAt = null; +let _assistantSaveState = null; function formatUptime(seconds) { const d = Math.floor(seconds / 86400); @@ -122,6 +123,36 @@ function buildRollbackPatchesFromSnapshot(snapshot) { }; } +function setAssistantSaveState(message, tone = 'neutral') { + _assistantSaveState = { + message, + tone, + at: Date.now(), + }; +} + +function renderAssistantSaveState() { + if (!_assistantSaveState) { + return '
No recent save action.
'; + } + + const toneClass = _assistantSaveState.tone === 'success' + ? 'text-green-500' + : _assistantSaveState.tone === 'warning' + ? 'text-amber-500' + : _assistantSaveState.tone === 'error' + ? 'text-red-500' + : 'text-zinc-500'; + + const at = new Date(_assistantSaveState.at).toLocaleTimeString(undefined, { + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + }); + + return `
${escapeHtml(_assistantSaveState.message)} (at ${escapeHtml(at)})
`; +} + // ── Initial full render ───────────────────────────────────────── function renderSkeleton(el) { @@ -492,43 +523,49 @@ function updateContextHealth(contextData) { async function applyAssistantPatch(patches, statusEl) { if (!_dashboardClient) { - console.warn('[Flynn] applyAssistantPatch: no client'); + setAssistantSaveState('Save skipped: dashboard client unavailable.', 'error'); 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 ?? []; const persisted = result?.persisted === true; + let message = ''; + let tone = 'neutral'; if (statusEl) { if (persistError) { - statusEl.textContent = `Save failed: ${persistError}`; - statusEl.className = 'text-sm text-red-500'; + message = `Save failed: ${persistError}`; + tone = 'error'; } else if (rejected.length > 0) { - statusEl.textContent = `Rejected: ${rejected.join(', ')}`; - statusEl.className = 'text-sm text-red-500'; + message = `Rejected: ${rejected.join(', ')}`; + tone = 'error'; } else if (!persisted) { - statusEl.textContent = `Runtime saved (${applied.length} updated)`; - statusEl.className = 'text-sm text-amber-500'; + message = `Runtime-only save (${applied.length} updated, file persistence unavailable)`; + tone = 'warning'; } else { - statusEl.textContent = `Saved & persisted (${applied.length} updated)`; - statusEl.className = 'text-sm text-green-500'; + message = `Saved to runtime + config file (${applied.length} updated)`; + tone = 'success'; } + setAssistantSaveState(message, tone); + statusEl.textContent = message; + statusEl.className = `text-sm ${tone === 'success' ? 'text-green-500' : tone === 'warning' ? 'text-amber-500' : tone === 'error' ? 'text-red-500' : 'text-zinc-500'}`; } + return { persisted, applied, rejected, persistError }; } catch (error) { - console.error('[Flynn] config.patch error:', error); + const msg = `Save error: ${error instanceof Error ? error.message : String(error)}`; + setAssistantSaveState(msg, 'error'); if (statusEl) { - statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`; + statusEl.textContent = msg; statusEl.className = 'text-sm text-red-500'; } + return { persisted: false, applied: [], rejected: [], persistError: msg }; } } @@ -700,7 +737,7 @@ function updateAssistantHealth(configData) {
${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}
${briefingReady ? '' : '
Enable daily briefing and set output channel/peer to test-send.
'} -
+ ${renderAssistantSaveState()} `; const statusEl = el.querySelector('#ops-assistant-status'); @@ -765,15 +802,7 @@ function updateAssistantHealth(configData) { 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; - } } }); }); @@ -963,5 +992,6 @@ export const DashboardPage = { _dashboardClient = null; _lastPlaybookRollbackPatches = null; _lastBriefingTestAt = null; + _assistantSaveState = null; }, };