|
|
|
@@ -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;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|