feat(dashboard): show persistent assistant-health save status
This commit is contained in:
@@ -5658,6 +5658,17 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm typecheck passing"
|
"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": {
|
"overall_progress": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ let _slowTimer = null;
|
|||||||
let _dashboardClient = null;
|
let _dashboardClient = null;
|
||||||
let _lastPlaybookRollbackPatches = null;
|
let _lastPlaybookRollbackPatches = null;
|
||||||
let _lastBriefingTestAt = null;
|
let _lastBriefingTestAt = null;
|
||||||
|
let _assistantSaveState = null;
|
||||||
|
|
||||||
function formatUptime(seconds) {
|
function formatUptime(seconds) {
|
||||||
const d = Math.floor(seconds / 86400);
|
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 ─────────────────────────────────────────
|
// ── Initial full render ─────────────────────────────────────────
|
||||||
|
|
||||||
function renderSkeleton(el) {
|
function renderSkeleton(el) {
|
||||||
@@ -492,43 +523,49 @@ function updateContextHealth(contextData) {
|
|||||||
|
|
||||||
async function applyAssistantPatch(patches, statusEl) {
|
async function applyAssistantPatch(patches, statusEl) {
|
||||||
if (!_dashboardClient) {
|
if (!_dashboardClient) {
|
||||||
console.warn('[Flynn] applyAssistantPatch: no client');
|
setAssistantSaveState('Save skipped: dashboard client unavailable.', 'error');
|
||||||
return;
|
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 ?? [];
|
||||||
const persisted = result?.persisted === true;
|
const persisted = result?.persisted === true;
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
let tone = 'neutral';
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
if (persistError) {
|
if (persistError) {
|
||||||
statusEl.textContent = `Save failed: ${persistError}`;
|
message = `Save failed: ${persistError}`;
|
||||||
statusEl.className = 'text-sm text-red-500';
|
tone = 'error';
|
||||||
} else if (rejected.length > 0) {
|
} else if (rejected.length > 0) {
|
||||||
statusEl.textContent = `Rejected: ${rejected.join(', ')}`;
|
message = `Rejected: ${rejected.join(', ')}`;
|
||||||
statusEl.className = 'text-sm text-red-500';
|
tone = 'error';
|
||||||
} else if (!persisted) {
|
} else if (!persisted) {
|
||||||
statusEl.textContent = `Runtime saved (${applied.length} updated)`;
|
message = `Runtime-only save (${applied.length} updated, file persistence unavailable)`;
|
||||||
statusEl.className = 'text-sm text-amber-500';
|
tone = 'warning';
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = `Saved & persisted (${applied.length} updated)`;
|
message = `Saved to runtime + config file (${applied.length} updated)`;
|
||||||
statusEl.className = 'text-sm text-green-500';
|
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) {
|
} 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) {
|
if (statusEl) {
|
||||||
statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`;
|
statusEl.textContent = msg;
|
||||||
statusEl.className = 'text-sm text-red-500';
|
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>
|
<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>'}
|
${briefingReady ? '' : '<div class="text-sm text-zinc-500 mt-2">Enable daily briefing and set output channel/peer to test-send.</div>'}
|
||||||
</div>
|
</div>
|
||||||
<div id="ops-assistant-status" class="text-sm text-zinc-500 mt-4"></div>
|
${renderAssistantSaveState()}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const statusEl = el.querySelector('#ops-assistant-status');
|
const statusEl = el.querySelector('#ops-assistant-status');
|
||||||
@@ -765,15 +802,7 @@ function updateAssistantHealth(configData) {
|
|||||||
updateSessionAnalytics(refreshed.sessionAnalytics);
|
updateSessionAnalytics(refreshed.sessionAnalytics);
|
||||||
updateContextHealth(refreshed.contextUsage);
|
updateContextHealth(refreshed.contextUsage);
|
||||||
// Capture status message before re-render destroys the DOM element
|
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -963,5 +992,6 @@ export const DashboardPage = {
|
|||||||
_dashboardClient = null;
|
_dashboardClient = null;
|
||||||
_lastPlaybookRollbackPatches = null;
|
_lastPlaybookRollbackPatches = null;
|
||||||
_lastBriefingTestAt = null;
|
_lastBriefingTestAt = null;
|
||||||
|
_assistantSaveState = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user