feat(dashboard): show persistent assistant-health save status

This commit is contained in:
William Valentin
2026-02-18 18:25:04 -08:00
parent 45261a090a
commit 0a664ddb21
2 changed files with 63 additions and 22 deletions
+11
View File
@@ -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": {
+52 -22
View File
@@ -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 '<div id="ops-assistant-status" class="text-sm text-zinc-500 mt-4">No recent save action.</div>';
}
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 `<div id="ops-assistant-status" class="text-sm ${toneClass} mt-4">${escapeHtml(_assistantSaveState.message)} <span class="text-zinc-500">(at ${escapeHtml(at)})</span></div>`;
}
// ── 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) {
<div class="max-h-44 overflow-y-auto bg-zinc-950 border border-zinc-800 rounded-md p-3"><code class="text-sm text-zinc-400 font-mono whitespace-pre-wrap">${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}</code></div>
${briefingReady ? '' : '<div class="text-sm text-zinc-500 mt-2">Enable daily briefing and set output channel/peer to test-send.</div>'}
</div>
<div id="ops-assistant-status" class="text-sm text-zinc-500 mt-4"></div>
${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;
},
};