feat(dashboard): add assistant health panel with quick actions
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
let _fastTimer = null;
|
||||
let _slowTimer = null;
|
||||
let _dashboardClient = null;
|
||||
|
||||
function formatUptime(seconds) {
|
||||
const d = Math.floor(seconds / 86400);
|
||||
@@ -80,6 +81,11 @@ function renderSkeleton(el) {
|
||||
<div class="text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Assistant Health</h2>
|
||||
<div id="ops-assistant-health">
|
||||
<div class="text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Event Stream</h2>
|
||||
<div class="event-stream" id="ops-events">
|
||||
<div class="event-row event-level-info">Loading events...</div>
|
||||
@@ -411,6 +417,128 @@ function updateContextHealth(contextData) {
|
||||
`;
|
||||
}
|
||||
|
||||
async function applyAssistantPatch(patches, statusEl) {
|
||||
if (!_dashboardClient) {return;}
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'Saving...';
|
||||
statusEl.className = 'text-sm text-muted';
|
||||
}
|
||||
try {
|
||||
const result = await _dashboardClient.call('config.patch', { patches });
|
||||
const rejected = result?.rejected ?? [];
|
||||
const persistError = result?.persistError;
|
||||
const applied = result?.applied ?? [];
|
||||
const persisted = result?.persisted === true;
|
||||
|
||||
if (statusEl) {
|
||||
if (persistError) {
|
||||
statusEl.textContent = `Save failed: ${persistError}`;
|
||||
statusEl.className = 'text-sm text-error';
|
||||
} else if (rejected.length > 0) {
|
||||
statusEl.textContent = `Rejected: ${rejected.join(', ')}`;
|
||||
statusEl.className = 'text-sm text-error';
|
||||
} else if (!persisted) {
|
||||
statusEl.textContent = `Runtime saved (${applied.length} updated)`;
|
||||
statusEl.className = 'text-sm text-muted';
|
||||
} else {
|
||||
statusEl.textContent = `Saved (${applied.length} updated)`;
|
||||
statusEl.className = 'text-sm text-success';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`;
|
||||
statusEl.className = 'text-sm text-error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateAssistantHealth(configData) {
|
||||
const el = document.getElementById('ops-assistant-health');
|
||||
if (!el) {return;}
|
||||
|
||||
const automation = configData?.automation ?? {};
|
||||
const memory = configData?.memory ?? {};
|
||||
const tts = configData?.tts ?? {};
|
||||
|
||||
const deliveryMode = automation.delivery_mode ?? 'shared_session';
|
||||
const announce = deliveryMode === 'announce';
|
||||
const dailyBriefing = Boolean(automation.daily_briefing?.enabled);
|
||||
const memoryDaily = Boolean(memory.daily_log?.enabled);
|
||||
const memoryProactive = Boolean(memory.proactive_extract?.enabled);
|
||||
const proactiveThreshold = Number(memory.proactive_extract?.min_tool_calls ?? 1);
|
||||
const ttsEnabled = Boolean(tts.enabled);
|
||||
|
||||
const chip = (label, value) => `
|
||||
<div class="assistant-chip">
|
||||
<span class="assistant-chip-label">${escapeHtml(label)}</span>
|
||||
<span class="assistant-chip-value ${value ? 'text-success' : 'text-muted'}">${value ? 'ON' : 'OFF'}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="assistant-health-grid">
|
||||
${chip('Announce Mode', announce)}
|
||||
${chip('Daily Briefing', dailyBriefing)}
|
||||
${chip('Memory Daily Log', memoryDaily)}
|
||||
${chip('Proactive Extract', memoryProactive)}
|
||||
${chip('TTS Replies', ttsEnabled)}
|
||||
<div class="assistant-chip">
|
||||
<span class="assistant-chip-label">Extract Threshold</span>
|
||||
<span class="assistant-chip-value">${Number.isFinite(proactiveThreshold) ? proactiveThreshold : 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="assistant-actions">
|
||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-announce">
|
||||
${announce ? 'Disable Announce Mode' : 'Enable Announce Mode'}
|
||||
</button>
|
||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-daily-briefing">
|
||||
${dailyBriefing ? 'Disable Daily Briefing' : 'Enable Daily Briefing'}
|
||||
</button>
|
||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-memory-daily">
|
||||
${memoryDaily ? 'Disable Daily Log' : 'Enable Daily Log'}
|
||||
</button>
|
||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-memory-proactive">
|
||||
${memoryProactive ? 'Disable Proactive Extract' : 'Enable Proactive Extract'}
|
||||
</button>
|
||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-tts">
|
||||
${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
|
||||
</button>
|
||||
</div>
|
||||
<div id="ops-assistant-status" class="text-sm text-muted"></div>
|
||||
`;
|
||||
|
||||
const statusEl = el.querySelector('#ops-assistant-status');
|
||||
const buttons = el.querySelectorAll('.assistant-action-btn');
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const action = button.getAttribute('data-action');
|
||||
let patches = null;
|
||||
if (action === 'toggle-announce') {
|
||||
patches = { 'automation.delivery_mode': announce ? 'shared_session' : 'announce' };
|
||||
} else if (action === 'toggle-daily-briefing') {
|
||||
patches = { 'automation.daily_briefing.enabled': !dailyBriefing };
|
||||
} else if (action === 'toggle-memory-daily') {
|
||||
patches = { 'memory.daily_log.enabled': !memoryDaily };
|
||||
} else if (action === 'toggle-memory-proactive') {
|
||||
patches = { 'memory.proactive_extract.enabled': !memoryProactive };
|
||||
} else if (action === 'toggle-tts') {
|
||||
patches = { 'tts.enabled': !ttsEnabled };
|
||||
}
|
||||
if (!patches) {return;}
|
||||
await applyAssistantPatch(patches, statusEl);
|
||||
// Force immediate refresh of slow sections after applying.
|
||||
const refreshed = await fetchSlow(_dashboardClient);
|
||||
if (refreshed) {
|
||||
updateServices(refreshed.services);
|
||||
updateSessionAnalytics(refreshed.sessionAnalytics);
|
||||
updateContextHealth(refreshed.contextUsage);
|
||||
updateAssistantHealth(refreshed.config);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _updateChannels(channelsData) {
|
||||
const el = document.getElementById('ops-channels');
|
||||
if (!el) {return;}
|
||||
@@ -479,13 +607,14 @@ async function fetchFast(client) {
|
||||
|
||||
async function fetchSlow(client) {
|
||||
try {
|
||||
const [health, services, sessionAnalytics, contextUsage] = await Promise.all([
|
||||
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 };
|
||||
return { health, services, sessionAnalytics, contextUsage, config };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -497,6 +626,7 @@ let _lastHealth = null;
|
||||
let _lastMetrics = null;
|
||||
|
||||
async function loadDashboard(el, client) {
|
||||
_dashboardClient = client;
|
||||
renderSkeleton(el);
|
||||
|
||||
// Fetch everything initially
|
||||
@@ -518,6 +648,7 @@ async function loadDashboard(el, client) {
|
||||
updateServices(slow.services);
|
||||
updateSessionAnalytics(slow.sessionAnalytics);
|
||||
updateContextHealth(slow.contextUsage);
|
||||
updateAssistantHealth(slow.config);
|
||||
}
|
||||
|
||||
// Fast refresh: 3 seconds for metrics, events, requests
|
||||
@@ -541,6 +672,7 @@ async function loadDashboard(el, client) {
|
||||
updateServices(data.services);
|
||||
updateSessionAnalytics(data.sessionAnalytics);
|
||||
updateContextHealth(data.contextUsage);
|
||||
updateAssistantHealth(data.config);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
@@ -561,5 +693,6 @@ export const DashboardPage = {
|
||||
}
|
||||
_lastHealth = null;
|
||||
_lastMetrics = null;
|
||||
_dashboardClient = null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1544,6 +1544,33 @@ tr:hover td {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.assistant-health-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.assistant-chip {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background-color: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.assistant-chip-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.assistant-chip-value {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ── Responsive: Mobile ─────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
Reference in New Issue
Block a user