/**
* Flynn Live Ops Dashboard
*
* Shows core counters, model performance, event stream, active requests,
* and channel status. Fast metrics refresh every 3s, slow health every 10s.
*/
let _fastTimer = null;
let _slowTimer = null;
let _dashboardClient = null;
let _lastPlaybookRollbackPatches = null;
let _lastBriefingTestAt = null;
let _assistantSaveState = null;
let _lastAssistantConfig = null;
let _assistantManualOverrides = new Set();
let _assistantModelDefaultsDraft = null;
let _assistantDraftState = new Map();
let _assistantDraftTouchedAt = 0;
let _lastCouncilTask = '';
let _lastCouncilResult = null;
let _lastCouncilError = null;
let _lastServices = [];
let _lastLocalBackends = [];
let _lastDockerDependencies = [];
let _lastObservabilitySources = [];
let _lastObservabilitySeries = null;
let _lastServiceLogs = null;
let _selectedLogSourceId = null;
let _logRefreshTimer = null;
let _logViewerState = {
lines: 200,
sinceSeconds: 900,
level: 'all',
autoRefresh: true,
paused: false,
status: null,
tone: 'neutral',
};
let _localBackendActionState = new Map();
let _dockerDependencyActionState = new Map();
let _serviceConfigState = {
open: false,
serviceName: null,
status: null,
tone: 'neutral',
advancedPatch: '',
};
const MODEL_DEFAULT_TASK_KEYS = ['compaction', 'memory_extraction', 'classification', 'tool_summarisation', 'complex_reasoning'];
const MODEL_DEFAULT_TIER_KEYS = ['default', 'fast', 'complex', 'local'];
const HEARTBEAT_CHECK_KEYS = ['gateway', 'model', 'channels', 'memory', 'disk', 'process_memory', 'backup', 'provider_errors'];
const LOCAL_BACKEND_ACTION_LABELS = {
start: 'Start',
restart: 'Restart',
stop: 'Stop',
update: 'Update',
};
const DOCKER_DEPENDENCY_ACTION_LABELS = {
start: 'Start',
restart: 'Restart',
stop: 'Stop',
update: 'Update',
};
const LOG_LEVEL_OPTIONS = ['all', 'info', 'warn', 'error'];
const LOG_LINES_OPTIONS = [100, 200, 500, 1000];
const LOG_SINCE_OPTIONS = [
{ value: 300, label: '5m' },
{ value: 900, label: '15m' },
{ value: 3600, label: '1h' },
{ value: 21600, label: '6h' },
];
const SERVICE_TOGGLE_PATCH_PATHS = {
heartbeat: 'automation.heartbeat.enabled',
daily_briefing: 'automation.daily_briefing.enabled',
gmail: 'automation.gmail.enabled',
gcal: 'automation.gcal.enabled',
gdocs: 'automation.gdocs.enabled',
gdrive: 'automation.gdrive.enabled',
gtasks: 'automation.gtasks.enabled',
backup: 'backup.enabled',
audio_transcription: 'audio.enabled',
sandbox: 'sandbox.enabled',
};
const ASSISTANT_DRAFT_TTL_MS = 2 * 60 * 1000;
function formatUptime(seconds) {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const parts = [];
if (d > 0) {parts.push(`${d}d`);}
if (h > 0) {parts.push(`${h}h`);}
if (m > 0) {parts.push(`${m}m`);}
parts.push(`${s}s`);
return parts.join(' ');
}
function timeAgo(timestamp) {
const secs = Math.floor((Date.now() - timestamp) / 1000);
if (secs < 60) {return `${secs}s ago`;}
if (secs < 3600) {return `${Math.floor(secs / 60)}m ago`;}
return `${Math.floor(secs / 3600)}h ago`;
}
function formatTime(timestamp) {
const d = new Date(timestamp);
return d.toLocaleTimeString('en-GB', { hour12: false });
}
function formatDay(day) {
const parsed = new Date(`${day}T00:00:00`);
if (Number.isNaN(parsed.getTime())) {return day;}
return parsed.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
}
function formatLogTime(timestamp) {
if (!timestamp || !Number.isFinite(Number(timestamp))) {
return '--:--:--';
}
return new Date(Number(timestamp)).toLocaleTimeString('en-GB', { hour12: false });
}
function formatNumber(value) {
return (value ?? 0).toLocaleString();
}
function formatSessionDurationFromMessages(avgMessagesPerSession) {
if (!avgMessagesPerSession || avgMessagesPerSession <= 0) {return '—';}
return `${avgMessagesPerSession.toFixed(1)} msgs/session`;
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function getAssistantStateSnapshot(configData) {
const automation = configData?.automation ?? {};
const memory = configData?.memory ?? {};
const audio = configData?.audio ?? {};
const talkMode = audio.talk_mode ?? {};
const tts = configData?.tts ?? {};
const queue = configData?.server?.queue ?? {};
return {
announce: (automation.delivery_mode ?? 'shared_session') === 'announce',
dailyBriefing: Boolean(automation.daily_briefing?.enabled),
memoryDaily: Boolean(memory.daily_log?.enabled),
memoryProactive: Boolean(memory.proactive_extract?.enabled),
memoryMinToolCalls: Number(memory.proactive_extract?.min_tool_calls ?? 1),
talkModeEnabled: Boolean(talkMode.enabled),
talkWakePhrase: typeof talkMode.wake_phrase === 'string' ? talkMode.wake_phrase : 'hey flynn',
talkTimeoutMs: Number(talkMode.timeout_ms ?? 120000),
talkManualToggle: talkMode.allow_manual_toggle !== false,
ttsEnabled: Boolean(tts.enabled),
ttsChannels: Array.isArray(tts.enabled_channels) ? tts.enabled_channels : [],
queueMode: queue.mode ?? 'collect',
};
}
function buildPlaybookPatches(playbook) {
if (playbook === 'executive') {
const patches = {
'automation.delivery_mode': 'announce',
'automation.daily_briefing.enabled': true,
'memory.daily_log.enabled': true,
'memory.proactive_extract.enabled': true,
'memory.proactive_extract.min_tool_calls': 1,
'audio.talk_mode.enabled': true,
'tts.enabled': true,
'tts.enabled_channels': [],
'server.queue.mode': 'interrupt',
};
for (const key of _assistantManualOverrides) {
delete patches[key];
}
return patches;
}
if (playbook === 'operator') {
const patches = {
'automation.delivery_mode': 'announce',
'automation.daily_briefing.enabled': true,
'memory.daily_log.enabled': true,
'memory.proactive_extract.enabled': true,
'memory.proactive_extract.min_tool_calls': 2,
'audio.talk_mode.enabled': false,
'tts.enabled': false,
'server.queue.mode': 'steer_backlog',
};
for (const key of _assistantManualOverrides) {
delete patches[key];
}
return patches;
}
const patches = {
'automation.delivery_mode': 'shared_session',
'automation.daily_briefing.enabled': false,
'memory.daily_log.enabled': false,
'memory.proactive_extract.enabled': false,
'memory.proactive_extract.min_tool_calls': 3,
'audio.talk_mode.enabled': false,
'tts.enabled': false,
'server.queue.mode': 'collect',
};
for (const key of _assistantManualOverrides) {
delete patches[key];
}
return patches;
}
function buildRollbackPatchesFromSnapshot(snapshot) {
return {
'automation.delivery_mode': snapshot.announce ? 'announce' : 'shared_session',
'automation.daily_briefing.enabled': snapshot.dailyBriefing,
'memory.daily_log.enabled': snapshot.memoryDaily,
'memory.proactive_extract.enabled': snapshot.memoryProactive,
'memory.proactive_extract.min_tool_calls': Number.isFinite(snapshot.memoryMinToolCalls) ? snapshot.memoryMinToolCalls : 1,
'audio.talk_mode.enabled': snapshot.talkModeEnabled,
'audio.talk_mode.wake_phrase': snapshot.talkWakePhrase,
'audio.talk_mode.timeout_ms': Number.isFinite(snapshot.talkTimeoutMs) ? snapshot.talkTimeoutMs : 120000,
'audio.talk_mode.allow_manual_toggle': snapshot.talkManualToggle,
'tts.enabled': snapshot.ttsEnabled,
'tts.enabled_channels': snapshot.ttsChannels,
'server.queue.mode': snapshot.queueMode,
};
}
function getByPath(obj, dottedPath) {
if (!obj || typeof obj !== 'object') {return undefined;}
const parts = dottedPath.split('.');
let cursor = obj;
for (const part of parts) {
if (!cursor || typeof cursor !== 'object' || !(part in cursor)) {
return undefined;
}
cursor = cursor[part];
}
return cursor;
}
function valuesMatch(expected, actual) {
if (Array.isArray(expected) && Array.isArray(actual)) {
if (expected.length !== actual.length) {return false;}
for (let i = 0; i < expected.length; i++) {
if (expected[i] !== actual[i]) {return false;}
}
return true;
}
return expected === actual;
}
function valuesMatchForPath(path, expected, actual) {
if (valuesMatch(expected, actual)) {
return true;
}
// Some optional string paths are normalized by config handlers:
// writing "" is persisted as "unset" (undefined).
if (typeof expected === 'string' && expected.trim().length === 0 && (actual === undefined || actual === null)) {
const optionalStringSuffixes = ['scaffold_path'];
if (optionalStringSuffixes.some((suffix) => path.endsWith(suffix))) {
return true;
}
}
return false;
}
function setAssistantSaveState(message, tone = 'neutral') {
_assistantSaveState = {
message,
tone,
at: Date.now(),
};
}
function writeAssistantDraftValue(control) {
if (!control || !control.id) {return;}
const isCheckbox = control.tagName === 'INPUT' && control.type === 'checkbox';
if (isCheckbox) {
_assistantDraftState.set(control.id, { kind: 'checkbox', value: Boolean(control.checked) });
} else if (control.tagName === 'SELECT') {
const selectedOption = Array.from(control.options ?? []).find((option) => option.selected);
_assistantDraftState.set(control.id, { kind: 'value', value: selectedOption?.value ?? '' });
} else {
_assistantDraftState.set(control.id, { kind: 'value', value: control.value ?? '' });
}
_assistantDraftTouchedAt = Date.now();
}
function bindAssistantDraftTracking(rootEl) {
const controls = rootEl.querySelectorAll('input[id], select[id], textarea[id]');
controls.forEach((control) => {
control.addEventListener('input', () => writeAssistantDraftValue(control));
control.addEventListener('change', () => writeAssistantDraftValue(control));
});
}
function applyAssistantDraftState(rootEl) {
if (_assistantDraftState.size === 0) {return;}
const now = Date.now();
if (_assistantDraftTouchedAt > 0 && (now - _assistantDraftTouchedAt) > ASSISTANT_DRAFT_TTL_MS) {
_assistantDraftState = new Map();
_assistantDraftTouchedAt = 0;
return;
}
for (const [id, draft] of _assistantDraftState.entries()) {
const control = rootEl.querySelector(`#${id}`);
if (!control) {continue;}
const isCheckbox = control.tagName === 'INPUT' && control.type === 'checkbox';
if (draft.kind === 'checkbox' && isCheckbox) {
control.checked = Boolean(draft.value);
} else if (control.tagName === 'SELECT') {
const options = Array.from(control.options ?? []);
const desired = String(draft.value ?? '');
let matchedIndex = -1;
for (let i = 0; i < options.length; i++) {
const option = options[i];
const isSelected = option.value === desired;
option.selected = isSelected;
if (isSelected) {
matchedIndex = i;
}
}
if (options.length > 0) {
const fallbackIndex = matchedIndex >= 0 ? matchedIndex : 0;
options[fallbackIndex].selected = true;
if ('selectedIndex' in control) {
control.selectedIndex = fallbackIndex;
}
}
try {
control.value = desired;
} catch {
// Some DOM shims expose readonly value on select nodes.
}
} else if ('value' in control) {
try {
control.value = String(draft.value ?? '');
} catch {
// Ignore rare non-writable value surfaces from non-browser DOM shims.
}
}
}
}
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)})
`;
}
function extractCouncilResultFromOutput(output) {
if (typeof output !== 'string' || output.trim().length === 0) {
return null;
}
const marker = '{"pipeline_version"';
const idx = output.lastIndexOf(marker);
if (idx < 0) {
return null;
}
try {
return JSON.parse(output.slice(idx));
} catch {
return null;
}
}
// ── Initial full render ─────────────────────────────────────────
function renderSkeleton(el) {
el.innerHTML = `
Live Ops Dashboard
Core Counters
Model Performance
Session Analytics
Context Health
Assistant Health
Event Stream
Active Requests
Services
Click a service card to configure.
Local LLM Backends
User-level daemon status and controls for local providers.
Docker Dependencies
Status for docker-compose services Flynn depends on (for example local Whisper transcription).
Service Health Graphs
Bounded 1h trend view (30s buckets) for compose dependencies and systemd daemons.
Service Logs
Recent core service logs with server-side token masking.
`;
}
// ── Section updaters (targeted DOM updates) ─────────────────────
function updateCounters(metrics, health) {
const el = document.getElementById('ops-counters');
if (!el) {return;}
const sessions = health?.sessions ?? 0;
const errCount = metrics?.errors ?? 0;
const cards = [
{ label: 'Messages Processed', value: String(metrics?.messagesProcessed ?? 0), cls: '' },
{ label: 'Active Sessions', value: String(sessions), cls: '' },
{ label: 'Queue Depth', value: String(metrics?.queueDepth ?? 0), cls: '' },
{ label: 'Uptime', value: formatUptime(metrics?.uptime ?? 0), cls: '' },
{ label: 'Active Requests', value: String(metrics?.activeRequests ?? 0), cls: '' },
{ label: 'Errors', value: String(errCount), cls: errCount > 0 ? 'text-red-500' : '' },
];
el.innerHTML = cards.map(c =>
``,
).join('');
}
function updateModelTable(metrics) {
const el = document.getElementById('ops-model-table');
if (!el) {return;}
const mc = metrics?.modelCalls;
const calls = mc?.recentCalls ?? [];
if (calls.length === 0) {
el.innerHTML = 'No model calls recorded yet
';
return;
}
const totalCalls = mc.total ?? 0;
const avgLatency = mc.avgLatency ?? 0;
const errorRate = mc.errorRate ?? 0;
const summaryHtml = `
Total Calls: ${totalCalls}
Avg Latency: ${avgLatency}ms
Error Rate: ${(errorRate * 100).toFixed(2)}%
`;
// Show newest first
const rows = [...calls].reverse().map(c => {
const status = c.error ? '✗' : '✓';
return `
| ${timeAgo(c.timestamp)} |
${escapeHtml(c.provider)} |
${c.latency}ms |
${c.tokensPerSec.toFixed(1)} |
${c.inputTokens}/${c.outputTokens} |
${status} |
`;
}).join('');
el.innerHTML = `
${summaryHtml}
| Time |
Provider |
Latency |
Tokens/sec |
In/Out |
Status |
${rows}
`;
}
function updateEvents(eventsData) {
const el = document.getElementById('ops-events');
if (!el) {return;}
const events = eventsData?.events ?? [];
if (events.length === 0) {
el.innerHTML = 'No events recorded yet
';
return;
}
// Events come newest-first from the API; show newest at bottom for log feel
const reversed = [...events].reverse();
el.innerHTML = reversed.map(e => {
const time = formatTime(e.timestamp);
const level = (e.level || 'info').toUpperCase();
const levelColor = e.level === 'error' ? 'text-red-500' : e.level === 'warn' ? 'text-amber-500' : 'text-zinc-400';
return `[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}
`;
}).join('');
// Auto-scroll to bottom
el.scrollTop = el.scrollHeight;
}
function updateActiveRequests(requestsData) {
const el = document.getElementById('ops-requests');
if (!el) {return;}
const requests = requestsData?.requests ?? [];
if (requests.length === 0) {
el.innerHTML = 'No active requests
';
return;
}
const rows = requests.map(r => {
const duration = r.durationMs < 1000
? `${r.durationMs}ms`
: `${(r.durationMs / 1000).toFixed(1)}s`;
const started = formatTime(r.startedAt);
return `
| ${escapeHtml(r.sessionId)} |
${escapeHtml(r.channel)} |
${duration} |
${started} |
`;
}).join('');
el.innerHTML = `
| Session |
Channel |
Duration |
Started |
${rows}
`;
}
function updateSessionAnalytics(analyticsData) {
const el = document.getElementById('ops-session-analytics');
if (!el) {return;}
const daily = analyticsData?.daily ?? [];
const topSessions = analyticsData?.topSessions ?? [];
const topTools = analyticsData?.topTools ?? [];
const topTopics = analyticsData?.topTopics ?? [];
const totalSessions = analyticsData?.totalSessions ?? 0;
const totalMessages = analyticsData?.totalMessages ?? 0;
const avgMessagesPerSession = analyticsData?.averageMessagesPerSession ?? 0;
const summaryHtml = `
Sessions (Window)
${formatNumber(totalSessions)}
Messages (Window)
${formatNumber(totalMessages)}
Avg Session
${formatSessionDurationFromMessages(avgMessagesPerSession)}
`;
const topToolsHtml = topTools.length > 0
? `${topTools.map((tool) =>
`
${escapeHtml(tool.toolName)} (${formatNumber(tool.executions)})
`,
).join('')}
`
: 'No tool usage captured in this window
';
const topTopicsHtml = topTopics.length > 0
? `${topTopics.map((topic) =>
`
${escapeHtml(topic.topic)} (${formatNumber(topic.occurrences)})
`,
).join('')}
`
: 'No indexed topics captured in this window
';
const topSessionsHtml = topSessions.length > 0
? `
| Session |
Messages |
Last Active |
${topSessions.map((session) => `
| ${escapeHtml(session.sessionId)} |
${formatNumber(session.messages)} |
${timeAgo(session.lastActivity * 1000)} |
`).join('')}
`
: 'No session activity in this window
';
const dailyHtml = daily.length > 0
? `
| Day |
Sessions |
Messages |
${daily.slice(0, 7).map((row) => `
| ${formatDay(row.day)} |
${formatNumber(row.sessions)} |
${formatNumber(row.messages)} |
`).join('')}
`
: 'No daily activity in this window
';
el.innerHTML = `
${summaryHtml}
Top Tools
${topToolsHtml}
Top Topics
${topTopicsHtml}
Top Sessions
${topSessionsHtml}
Daily Trend (Last 7 Rows)
${dailyHtml}
`;
}
function updateContextHealth(contextData) {
const el = document.getElementById('ops-context-health');
if (!el) {return;}
const sessions = contextData?.sessions ?? [];
if (sessions.length === 0) {
el.innerHTML = 'No active context usage snapshots
';
return;
}
const sorted = [...sessions].sort((a, b) => (b.budget?.usagePct ?? 0) - (a.budget?.usagePct ?? 0));
const top = sorted.slice(0, 8);
const highest = top[0]?.budget?.usagePct ?? 0;
const overThreshold = sessions.filter(s => (s.budget?.shouldCompact ?? false)).length;
const summary = `
Highest Usage
${highest.toFixed(1)}%
Sessions Near Limit
${overThreshold}
Active Snapshots
${sessions.length}
`;
const rows = top.map((entry) => {
const budget = entry.budget ?? {};
const usage = budget.usagePct ?? 0;
const cls = usage >= 95 ? 'text-red-500' : usage >= 85 ? 'text-amber-500' : '';
return `
| ${escapeHtml(entry.sessionId)} |
${usage.toFixed(1)}% |
${formatNumber(budget.estimatedTokens ?? 0)} / ${formatNumber(budget.contextWindow ?? 0)} |
${budget.shouldCompact ? 'yes' : 'no'} |
`;
}).join('');
el.innerHTML = `
${summary}
| Session |
Usage |
Estimated Tokens |
Should Compact |
${rows}
`;
}
async function applyAssistantPatch(patches, statusEl) {
if (!_dashboardClient) {
setAssistantSaveState('Save skipped: dashboard client unavailable.', 'error');
return;
}
if (statusEl) {
statusEl.textContent = 'Saving...';
statusEl.className = 'text-sm text-zinc-500';
}
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;
let message = '';
let tone = 'neutral';
if (statusEl) {
if (persistError) {
message = `Save failed: ${persistError}`;
tone = 'error';
} else if (rejected.length > 0) {
message = `Rejected: ${rejected.join(', ')}`;
tone = 'error';
} else if (!persisted) {
message = `Runtime-only save (${applied.length} updated, file persistence unavailable)`;
tone = 'warning';
} else {
// Verify read-after-write so UI cannot claim persistence when value did not stick.
try {
const fresh = await _dashboardClient.call('config.get');
const mismatches = [];
for (const [key, value] of Object.entries(patches)) {
const actual = getByPath(fresh, key);
if (!valuesMatchForPath(key, value, actual)) {
mismatches.push(`${key} expected=${JSON.stringify(value)} actual=${JSON.stringify(actual)}`);
}
}
if (mismatches.length > 0) {
message = `Saved response received but read-back mismatch: ${mismatches.join('; ')}`;
tone = 'error';
} else {
message = `Saved to runtime + config file (${applied.length} updated)`;
tone = 'success';
}
} catch (verifyError) {
message = `Saved response received, but verification failed: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`;
tone = 'warning';
}
}
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) {
const msg = `Save error: ${error instanceof Error ? error.message : String(error)}`;
setAssistantSaveState(msg, 'error');
if (statusEl) {
statusEl.textContent = msg;
statusEl.className = 'text-sm text-red-500';
}
return { persisted: false, applied: [], rejected: [], persistError: msg };
}
}
async function triggerDailyBriefingTest(jobName, statusEl) {
if (!_dashboardClient) {return;}
if (statusEl) {
statusEl.textContent = 'Triggering test briefing...';
statusEl.className = 'text-sm text-zinc-500';
}
try {
const result = await _dashboardClient.call('tools.invoke', {
tool: 'cron.trigger',
args: { name: jobName },
});
if (result?.success) {
const output = typeof result.output === 'string' ? result.output : 'Triggered.';
if (statusEl) {
statusEl.textContent = output;
statusEl.className = 'text-sm text-green-500';
}
_lastBriefingTestAt = Date.now();
return true;
}
if (statusEl) {
statusEl.textContent = result?.error ?? 'Failed to trigger briefing.';
statusEl.className = 'text-sm text-red-500';
}
return false;
} catch (error) {
if (statusEl) {
statusEl.textContent = `Trigger error: ${error instanceof Error ? error.message : String(error)}`;
statusEl.className = 'text-sm text-red-500';
}
return false;
}
}
async function triggerCouncilRun(task, statusEl) {
if (!_dashboardClient) {return false;}
if (!task) {
if (statusEl) {
statusEl.textContent = 'Task is required.';
statusEl.className = 'text-sm text-red-500';
}
return false;
}
if (statusEl) {
statusEl.textContent = 'Running council...';
statusEl.className = 'text-sm text-zinc-500';
}
try {
const result = await _dashboardClient.call('tools.invoke', {
tool: 'council.run',
args: { task },
});
if (!result?.success) {
_lastCouncilError = result?.error ?? 'Council run failed.';
_lastCouncilResult = null;
if (statusEl) {
statusEl.textContent = _lastCouncilError;
statusEl.className = 'text-sm text-red-500';
}
return false;
}
const parsed = extractCouncilResultFromOutput(result.output);
if (!parsed) {
_lastCouncilError = 'Council run succeeded but output could not be parsed.';
_lastCouncilResult = null;
if (statusEl) {
statusEl.textContent = _lastCouncilError;
statusEl.className = 'text-sm text-amber-500';
}
return false;
}
_lastCouncilTask = task;
_lastCouncilResult = parsed;
_lastCouncilError = null;
if (statusEl) {
statusEl.textContent = `Council run complete: ${parsed.stop_snapshot?.stop_reason ?? 'ok'}`;
statusEl.className = 'text-sm text-green-500';
}
return true;
} catch (error) {
_lastCouncilError = `Council run error: ${error instanceof Error ? error.message : String(error)}`;
_lastCouncilResult = null;
if (statusEl) {
statusEl.textContent = _lastCouncilError;
statusEl.className = 'text-sm text-red-500';
}
return false;
}
}
function updateAssistantHealth(configData) {
const el = document.getElementById('ops-assistant-health');
if (!el) {return;}
_assistantModelDefaultsDraft = readAssistantModelDefaultsDraft(el) ?? _assistantModelDefaultsDraft;
const snapshot = getAssistantStateSnapshot(configData);
const automation = configData?.automation ?? {};
const memory = configData?.memory ?? {};
const audio = configData?.audio ?? {};
const talkMode = audio.talk_mode ?? {};
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 talkModeEnabled = Boolean(talkMode.enabled);
const talkWakePhrase = typeof talkMode.wake_phrase === 'string' ? talkMode.wake_phrase : 'hey flynn';
const talkTimeoutMs = Number(talkMode.timeout_ms ?? 120000);
const ttsEnabled = Boolean(tts.enabled);
const briefing = automation.daily_briefing ?? {};
const briefingName = briefing.name ?? 'daily-briefing';
const briefingSchedule = briefing.schedule ?? '0 8 * * *';
const briefingPrompt = briefing.prompt ?? '';
const briefingOutput = briefing.output ?? null;
const briefingModelTier = briefing.model_tier ?? 'default';
const briefingTimezone = briefing.timezone ?? 'system';
const briefingOutputLabel = briefingOutput?.channel && briefingOutput?.peer
? `${briefingOutput.channel}/${briefingOutput.peer}`
: 'not configured';
const briefingReady = dailyBriefing && Boolean(briefingOutput?.channel && briefingOutput?.peer);
const playbookLikeReady = announce || (memoryDaily && memoryProactive);
const modelTier = _assistantModelDefaultsDraft?.primaryTier ?? configData?.agents?.primary_tier ?? 'default';
const delegation = configData?.agents?.delegation ?? {};
const backgroundModels = configData?.agents?.background_models ?? {};
const councils = configData?.councils ?? {};
const councilsDefaults = councils.defaults ?? {};
const councilsGroups = councils.groups ?? {};
const councilsD = councilsGroups.D ?? {};
const councilsP = councilsGroups.P ?? {};
const tiers = configData?.models ?? {};
const modelCatalog = configData?.__modelCatalog ?? [];
const providerList = modelCatalog.length > 0
? modelCatalog.map((entry) => entry.provider)
: ['anthropic', 'openai', 'gemini', 'openrouter', 'github', 'xai', 'ollama', 'llamacpp', 'bedrock', 'zhipuai', 'minimax', 'moonshot', 'synthetic'];
const modelOptionsByProvider = Object.fromEntries(modelCatalog.map((entry) => [entry.provider, entry.models ?? []]));
const checklistRows = [
{ label: 'Set briefing output channel + peer', done: Boolean(briefingOutput?.channel && briefingOutput?.peer) },
{ label: 'Enable assistant behavior profile', done: playbookLikeReady },
{ label: 'Send a test morning briefing', done: _lastBriefingTestAt !== null },
];
const chip = (label, value) => `
${escapeHtml(label)}
${value ? 'ON' : 'OFF'}
`;
const tierOption = (selected) => ['fast', 'default', 'complex', 'local']
.map((tier) => ``)
.join('');
const providerOption = (selected) => providerList
.map((provider) => ``)
.join('');
const modelDataList = (id, provider, selected) => {
const options = modelOptionsByProvider[provider] ?? [];
return `
`;
};
const taskRows = [
{ key: 'compaction', label: 'Compaction' },
{ key: 'memory_extraction', label: 'Memory extraction' },
{ key: 'classification', label: 'Classification' },
{ key: 'tool_summarisation', label: 'Tool summarisation' },
{ key: 'complex_reasoning', label: 'Complex reasoning' },
];
const councilConversations = Array.isArray(_lastCouncilResult?.conversations) ? _lastCouncilResult.conversations : [];
const councilSummary = _lastCouncilResult?.stop_snapshot
? `Last run: ${_lastCouncilResult.stop_snapshot.stop_reason} (round ${_lastCouncilResult.stop_snapshot.round_reached})`
: (_lastCouncilError ? `Last run failed: ${_lastCouncilError}` : 'No council run yet in this dashboard session.');
el.innerHTML = `
${chip('Announce Mode', announce)}
${chip('Daily Briefing', dailyBriefing)}
${chip('Memory Daily Log', memoryDaily)}
${chip('Proactive Extract', memoryProactive)}
${chip('Talk Mode', talkModeEnabled)}
${chip('TTS Replies', ttsEnabled)}
Extract Threshold
${Number.isFinite(proactiveThreshold) ? proactiveThreshold : 1}
Talk controls: wake phrase ${escapeHtml(talkWakePhrase)},
timeout ${Number.isFinite(talkTimeoutMs) ? Math.round(talkTimeoutMs / 1000) : 120}s.
Assistant Playbooks
Executive: announce + voice + aggressive interrupt. Operator: announce + memory-first + steer backlog. Focus: reactive, quieter mode.
Model Tier Defaults
Tier provider/model definitions
${MODEL_DEFAULT_TIER_KEYS.map((tier) => {
const cfg = tiers?.[tier] ?? {};
const provider = _assistantModelDefaultsDraft?.tiers?.[tier]?.provider ?? cfg.provider ?? tiers?.default?.provider ?? 'openai';
const model = _assistantModelDefaultsDraft?.tiers?.[tier]?.model ?? cfg.model ?? '';
return `
${escapeHtml(tier)} tier
`;
}).join('')}
Delegation tiers + background model overrides
${taskRows.map((task) => {
const background = backgroundModels?.[task.key] ?? {};
const draftTask = _assistantModelDefaultsDraft?.tasks?.[task.key] ?? {};
const delegationTier = draftTask.delegationTier ?? delegation?.[task.key] ?? 'fast';
const backgroundEnabled = draftTask.backgroundEnabled ?? Boolean(backgroundModels?.[task.key] && background?.enabled !== false);
return `
`;
}).join('')}
Assistant Activation Checklist
${checklistRows.map((row) => `
${row.done ? '✓' : '○'}
${escapeHtml(row.label)}
`).join('')}
Morning Brief Preview
name: ${escapeHtml(briefingName)}
schedule: ${escapeHtml(briefingSchedule)}
timezone: ${escapeHtml(briefingTimezone)}
tier: ${escapeHtml(briefingModelTier)}
output: ${escapeHtml(briefingOutputLabel)}
${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}
${briefingReady ? '' : '
Enable daily briefing and set output channel/peer to test-send.
'}
${renderAssistantSaveState()}
`;
// Preserve local unsaved form edits across periodic dashboard refreshes.
applyAssistantDraftState(el);
const updateModelOptions = (inputId, provider) => {
const input = el.querySelector(`#${inputId}`);
const list = el.querySelector(`#${inputId}-list`);
if (!input || !list) {return;}
const options = modelOptionsByProvider[provider] ?? [];
list.innerHTML = options.map((model) => ``).join('');
};
const tierRows = ['default', 'fast', 'complex', 'local'];
for (const tier of tierRows) {
const providerSelect = el.querySelector(`#assist-tier-${tier}-provider`);
if (!providerSelect) {continue;}
providerSelect.addEventListener('change', () => {
updateModelOptions(`assist-tier-${tier}-model`, providerSelect.value);
});
}
const taskRowsForModels = MODEL_DEFAULT_TASK_KEYS;
for (const task of taskRowsForModels) {
const providerSelect = el.querySelector(`#assist-bg-${task}-provider`);
if (!providerSelect) {continue;}
providerSelect.addEventListener('change', () => {
updateModelOptions(`assist-bg-${task}-model`, providerSelect.value);
});
}
// Refresh datalist options after draft re-application in case provider selects were restored.
for (const tier of tierRows) {
const providerSelect = el.querySelector(`#assist-tier-${tier}-provider`);
if (!providerSelect) {continue;}
updateModelOptions(`assist-tier-${tier}-model`, providerSelect.value);
}
for (const task of taskRowsForModels) {
const providerSelect = el.querySelector(`#assist-bg-${task}-provider`);
if (!providerSelect) {continue;}
updateModelOptions(`assist-bg-${task}-model`, providerSelect.value);
}
bindAssistantDraftTracking(el);
const statusEl = el.querySelector('#ops-assistant-status');
const buttons = el.querySelectorAll('.assistant-action-btn');
buttons.forEach((button) => {
button.addEventListener('click', async () => {
const rawAction = button.getAttribute('data-action');
const action = rawAction === 'save-council' ? 'save-councils' : rawAction;
let patches = null;
if (action === 'toggle-announce') {
patches = { 'automation.delivery_mode': announce ? 'shared_session' : 'announce' };
_assistantManualOverrides.add('automation.delivery_mode');
} else if (action === 'toggle-daily-briefing') {
patches = { 'automation.daily_briefing.enabled': !dailyBriefing };
_assistantManualOverrides.add('automation.daily_briefing.enabled');
} else if (action === 'toggle-memory-daily') {
patches = { 'memory.daily_log.enabled': !memoryDaily };
_assistantManualOverrides.add('memory.daily_log.enabled');
} else if (action === 'toggle-memory-proactive') {
patches = { 'memory.proactive_extract.enabled': !memoryProactive };
_assistantManualOverrides.add('memory.proactive_extract.enabled');
} else if (action === 'toggle-talk-mode') {
patches = { 'audio.talk_mode.enabled': !talkModeEnabled };
_assistantManualOverrides.add('audio.talk_mode.enabled');
} else if (action === 'toggle-tts') {
patches = { 'tts.enabled': !ttsEnabled };
_assistantManualOverrides.add('tts.enabled');
} else if (action === 'test-daily-briefing') {
await triggerDailyBriefingTest(briefingName, statusEl);
} else if (action === 'playbook-executive') {
_lastPlaybookRollbackPatches = buildRollbackPatchesFromSnapshot(snapshot);
patches = buildPlaybookPatches('executive');
} else if (action === 'playbook-operator') {
_lastPlaybookRollbackPatches = buildRollbackPatchesFromSnapshot(snapshot);
patches = buildPlaybookPatches('operator');
} else if (action === 'playbook-focus') {
_lastPlaybookRollbackPatches = buildRollbackPatchesFromSnapshot(snapshot);
patches = buildPlaybookPatches('focus');
} else if (action === 'playbook-undo') {
if (!_lastPlaybookRollbackPatches) {
if (statusEl) {
statusEl.textContent = 'No playbook changes to undo.';
statusEl.className = 'text-sm text-zinc-500';
}
return;
}
patches = _lastPlaybookRollbackPatches;
_lastPlaybookRollbackPatches = null;
} else if (action === 'save-briefing-output') {
const channel = (el.querySelector('#assist-brief-channel')?.value ?? '').trim();
const peer = (el.querySelector('#assist-brief-peer')?.value ?? '').trim();
if (!channel || !peer) {
if (statusEl) {
statusEl.textContent = 'Briefing output channel and peer are required.';
statusEl.className = 'text-sm text-red-500';
}
return;
}
patches = {
'automation.daily_briefing.output.channel': channel,
'automation.daily_briefing.output.peer': peer,
'automation.daily_briefing.enabled': true,
};
_assistantManualOverrides.add('automation.daily_briefing.enabled');
} else if (action === 'save-model-defaults') {
const tasks = MODEL_DEFAULT_TASK_KEYS;
patches = {
'agents.primary_tier': (el.querySelector('#assist-primary-tier')?.value ?? 'default'),
};
const tiers = MODEL_DEFAULT_TIER_KEYS;
for (const tier of tiers) {
const provider = (el.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim();
const model = (el.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim();
if (!provider || (!model && tier !== 'default')) {
continue;
}
if (provider) {
patches[`models.${tier}.provider`] = provider;
}
if (model) {
patches[`models.${tier}.model`] = model;
}
}
for (const task of tasks) {
const delegationTier = el.querySelector(`#assist-delegation-${task}`)?.value ?? 'fast';
const enabled = Boolean(el.querySelector(`#assist-bg-${task}-enabled`)?.checked);
const provider = (el.querySelector(`#assist-bg-${task}-provider`)?.value ?? '').trim();
const model = (el.querySelector(`#assist-bg-${task}-model`)?.value ?? '').trim();
const fallbackTier = el.querySelector(`#assist-bg-${task}-fallback`)?.value ?? 'fast';
patches[`agents.delegation.${task}`] = delegationTier;
patches[`agents.background_models.${task}.enabled`] = enabled;
if (provider) {
patches[`agents.background_models.${task}.provider`] = provider;
}
if (model) {
patches[`agents.background_models.${task}.model`] = model;
}
patches[`agents.background_models.${task}.fallback_tier`] = fallbackTier;
}
} else if (action === 'save-councils') {
const enabled = Boolean(el.querySelector('#assist-councils-enabled')?.checked);
const dTier = el.querySelector('#assist-council-d-tier')?.value ?? 'complex';
const pTier = el.querySelector('#assist-council-p-tier')?.value ?? 'complex';
const metaTier = el.querySelector('#assist-council-meta-tier')?.value ?? 'complex';
const dArbiter = (el.querySelector('#assist-council-d-arbiter')?.value ?? '').trim();
const dFreethinker = (el.querySelector('#assist-council-d-freethinker')?.value ?? '').trim();
const pArbiter = (el.querySelector('#assist-council-p-arbiter')?.value ?? '').trim();
const pFreethinker = (el.querySelector('#assist-council-p-freethinker')?.value ?? '').trim();
const metaArbiter = (el.querySelector('#assist-council-meta-arbiter')?.value ?? '').trim();
const scaffoldPath = (el.querySelector('#assist-council-scaffold')?.value ?? '').trim();
const maxRoundsRaw = Number(el.querySelector('#assist-council-max-rounds')?.value ?? 2);
const maxRounds = Number.isFinite(maxRoundsRaw) ? Math.max(1, Math.min(6, Math.floor(maxRoundsRaw))) : 2;
if (!dArbiter || !dFreethinker || !pArbiter || !pFreethinker || !metaArbiter) {
if (statusEl) {
statusEl.textContent = 'All council agent names are required.';
statusEl.className = 'text-sm text-red-500';
}
return;
}
patches = {
'councils.enabled': enabled,
'councils.defaults.max_rounds': maxRounds,
'councils.groups.D.model_tier': dTier,
'councils.groups.P.model_tier': pTier,
'councils.meta_model_tier': metaTier,
'councils.groups.D.arbiter_agent': dArbiter,
'councils.groups.D.freethinker_agent': dFreethinker,
'councils.groups.P.arbiter_agent': pArbiter,
'councils.groups.P.freethinker_agent': pFreethinker,
'councils.meta_arbiter_agent': metaArbiter,
'councils.scaffold_path': scaffoldPath,
};
} else if (action === 'run-council') {
const councilTask = (el.querySelector('#assist-council-task')?.value ?? '').trim();
const councilStatusEl = el.querySelector('#assist-council-status');
const ok = await triggerCouncilRun(councilTask, councilStatusEl);
if (ok && _lastAssistantConfig) {
updateAssistantHealth(_lastAssistantConfig);
}
return;
}
if (!patches) {return;}
const patchResult = await applyAssistantPatch(patches, statusEl);
if (action === 'save-model-defaults' && patchResult.applied.length > 0 && patchResult.rejected.length === 0) {
_assistantModelDefaultsDraft = null;
} else if (action === 'save-model-defaults') {
_assistantModelDefaultsDraft = readAssistantModelDefaultsDraft(el) ?? _assistantModelDefaultsDraft;
}
// Force immediate refresh of slow sections after applying.
const refreshed = await fetchSlow(_dashboardClient);
if (refreshed) {
if (refreshed.config) {
_lastAssistantConfig = refreshed.config;
}
updateServices(refreshed.services);
updateLocalBackends(refreshed.localBackends);
updateDockerDependencies(refreshed.dockerDependencies);
if (refreshed.observabilitySources) {
_lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
? refreshed.observabilitySources.sources
: [];
}
if (refreshed.observabilitySeries) {
updateObservabilityGraphs(refreshed.observabilitySeries);
}
updateServiceLogsPanel();
await refreshServiceLogs({ force: true });
updateSessionAnalytics(refreshed.sessionAnalytics);
updateContextHealth(refreshed.contextUsage);
// Only re-render assistant controls from a confirmed config snapshot.
if (refreshed.config) {
updateAssistantHealth(_lastAssistantConfig);
} else if (_lastAssistantConfig) {
updateAssistantHealth(_lastAssistantConfig);
}
}
});
});
}
function readAssistantModelDefaultsDraft(rootEl) {
const primaryTier = rootEl.querySelector('#assist-primary-tier')?.value;
if (!primaryTier) {
return null;
}
const tiers = {};
for (const tier of MODEL_DEFAULT_TIER_KEYS) {
const provider = (rootEl.querySelector(`#assist-tier-${tier}-provider`)?.value ?? '').trim();
const model = (rootEl.querySelector(`#assist-tier-${tier}-model`)?.value ?? '').trim();
if (!provider && !model) {continue;}
tiers[tier] = { provider, model };
}
const tasks = {};
for (const task of MODEL_DEFAULT_TASK_KEYS) {
tasks[task] = {
delegationTier: rootEl.querySelector(`#assist-delegation-${task}`)?.value ?? 'fast',
backgroundEnabled: Boolean(rootEl.querySelector(`#assist-bg-${task}-enabled`)?.checked),
provider: (rootEl.querySelector(`#assist-bg-${task}-provider`)?.value ?? '').trim(),
model: (rootEl.querySelector(`#assist-bg-${task}-model`)?.value ?? '').trim(),
fallbackTier: rootEl.querySelector(`#assist-bg-${task}-fallback`)?.value ?? 'fast',
};
}
return {
primaryTier,
tiers,
tasks,
};
}
function _updateChannels(channelsData) {
const el = document.getElementById('ops-channels');
if (!el) {return;}
const channels = channelsData?.channels ?? [];
if (channels.length === 0) {
el.innerHTML = 'No channels registered
';
return;
}
el.innerHTML = channels.map(ch =>
`
${escapeHtml(ch.name)}
`,
).join('');
}
function updateServices(servicesData) {
const el = document.getElementById('ops-services');
if (!el) {return;}
const services = servicesData?.services ?? [];
_lastServices = services;
if (services.length === 0) {
el.innerHTML = 'No services configured
';
renderServiceConfigModal();
return;
}
el.innerHTML = services.map(svc => {
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
const borderColor = svc.status === 'connected'
? 'border-l-green-500'
: svc.status === 'configured'
? 'border-l-blue-500'
: svc.status === 'error'
? 'border-l-red-500'
: 'border-l-zinc-600 opacity-60';
const statusColor = svc.status === 'connected'
? 'text-green-500'
: svc.status === 'configured'
? 'text-blue-500'
: svc.status === 'error'
? 'text-red-500'
: 'text-zinc-500';
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
return ``;
}).join('');
el.querySelectorAll('.service-card').forEach((card) => {
card.addEventListener('click', () => {
const serviceName = card.getAttribute('data-service-name');
if (!serviceName) {return;}
_serviceConfigState.open = true;
_serviceConfigState.serviceName = serviceName;
_serviceConfigState.status = null;
_serviceConfigState.tone = 'neutral';
renderServiceConfigModal();
});
});
renderServiceConfigModal();
}
function updateLocalBackends(localBackendsData) {
const el = document.getElementById('ops-local-backends');
if (!el) {return;}
const backends = localBackendsData?.backends ?? [];
_lastLocalBackends = backends;
if (backends.length === 0) {
el.innerHTML = 'No local backends detected
';
return;
}
el.innerHTML = backends.map((backend) => {
const backendId = String(backend.id ?? '');
const actionState = _localBackendActionState.get(backendId) ?? null;
const isPending = Boolean(actionState?.pending);
const toneClass = backend.activeState === 'active'
? 'text-green-500'
: backend.activeState === 'failed'
? 'text-red-500'
: 'text-zinc-400';
const configuredText = backend.configured ? 'configured' : 'not configured';
const configuredClass = backend.configured ? 'text-blue-400' : 'text-zinc-500';
const pidText = backend.pid ? String(backend.pid) : '—';
const unitFileText = backend.unitFileState || 'unknown';
const loadText = backend.loadState || 'unknown';
const resultText = backend.result || 'unknown';
const availableActions = Array.isArray(backend.availableActions)
? backend.availableActions.filter((value) => ['start', 'restart', 'stop', 'update'].includes(String(value)))
: [];
const actionMessage = actionState?.message
? `${escapeHtml(String(actionState.message))}
`
: '';
const actionButtons = availableActions.length > 0
? availableActions.map((action) => {
const key = String(action);
const label = LOCAL_BACKEND_ACTION_LABELS[key] ?? key;
return ``;
}).join('')
: 'No actions available';
return `
${escapeHtml(String(backend.name ?? backendId))}
${escapeHtml(String(backend.statusText ?? backend.activeState ?? 'unknown'))}
Unit: ${escapeHtml(String(backend.unit ?? 'unknown'))}
PID: ${escapeHtml(pidText)} · Load: ${escapeHtml(loadText)} · Result: ${escapeHtml(resultText)}
${escapeHtml(configuredText)} · unit file: ${escapeHtml(unitFileText)}
${backend.error ? `
Error: ${escapeHtml(String(backend.error))}
` : ''}
${actionMessage}
${actionButtons}
`;
}).join('');
el.querySelectorAll('.local-backend-action-btn').forEach((button) => {
button.addEventListener('click', async () => {
const backendId = button.getAttribute('data-backend-id');
const action = button.getAttribute('data-action');
if (!backendId || !action) {return;}
await handleLocalBackendAction(backendId, action);
});
});
}
function updateDockerDependencies(dockerDependenciesData) {
const el = document.getElementById('ops-docker-dependencies');
if (!el) {return;}
const dependencies = dockerDependenciesData?.dependencies ?? [];
_lastDockerDependencies = dependencies;
if (dependencies.length === 0) {
el.innerHTML = 'No docker dependencies detected
';
return;
}
el.innerHTML = dependencies.map((dependency) => {
const dependencyId = String(dependency.id ?? '');
const actionState = _dockerDependencyActionState.get(dependencyId) ?? null;
const isPending = Boolean(actionState?.pending);
const state = String(dependency.state ?? 'unknown');
const health = String(dependency.health ?? 'unknown');
const statusText = String(dependency.statusText ?? state);
const configured = Boolean(dependency.configured);
const configuredText = configured ? 'configured' : 'not configured';
const configuredClass = configured ? 'text-blue-400' : 'text-zinc-500';
const toneClass = state === 'running'
? 'text-green-500'
: state === 'not-found'
? 'text-amber-500'
: state === 'unknown'
? 'text-red-500'
: 'text-zinc-400';
const containerName = dependency.containerName ? String(dependency.containerName) : '—';
const availableActions = Array.isArray(dependency.availableActions)
? dependency.availableActions.filter((value) => ['start', 'restart', 'stop', 'update'].includes(String(value)))
: [];
const actionMessage = actionState?.message
? `${escapeHtml(String(actionState.message))}
`
: '';
const actionButtons = availableActions.length > 0
? availableActions.map((action) => {
const key = String(action);
const label = DOCKER_DEPENDENCY_ACTION_LABELS[key] ?? key;
return ``;
}).join('')
: 'No actions available';
return `
${escapeHtml(String(dependency.name ?? dependency.id ?? 'dependency'))}
${escapeHtml(statusText)}
Compose service: ${escapeHtml(String(dependency.service ?? 'unknown'))}
Container: ${escapeHtml(containerName)}
State: ${escapeHtml(state)} · Health: ${escapeHtml(health)}
${escapeHtml(configuredText)}
${dependency.error ? `
Error: ${escapeHtml(String(dependency.error))}
` : ''}
${actionMessage}
${actionButtons}
`;
}).join('');
el.querySelectorAll('.docker-dependency-action-btn').forEach((button) => {
button.addEventListener('click', async () => {
const dependencyId = button.getAttribute('data-dependency-id');
const action = button.getAttribute('data-action');
if (!dependencyId || !action) {return;}
await handleDockerDependencyAction(dependencyId, action);
});
});
}
function getObservabilityName(sourceId) {
const source = _lastObservabilitySources.find((entry) => entry.id === sourceId);
return source?.name ?? sourceId;
}
function buildSparklinePath(points, key, width = 320, height = 56) {
if (!Array.isArray(points) || points.length === 0) {
return null;
}
const values = points.map((point) => Number(point?.[key] ?? 0));
const min = Math.min(...values);
const max = Math.max(...values);
const denom = max === min ? 1 : (max - min);
return values.map((value, idx) => {
const x = values.length === 1 ? 0 : (idx / (values.length - 1)) * width;
const y = height - (((value - min) / denom) * height);
return `${idx === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`;
}).join(' ');
}
function updateObservabilityGraphs(seriesData) {
const el = document.getElementById('ops-observability-graphs');
if (!el) {return;}
const series = Array.isArray(seriesData?.series) ? seriesData.series : [];
_lastObservabilitySeries = seriesData ?? null;
if (series.length === 0) {
el.innerHTML = 'No service trend data available yet
';
return;
}
const cards = [...series]
.sort((a, b) => getObservabilityName(String(a.sourceId)).localeCompare(getObservabilityName(String(b.sourceId))))
.map((entry) => {
const sourceId = String(entry.sourceId ?? '');
const source = _lastObservabilitySources.find((candidate) => candidate.id === sourceId);
const name = source?.name ?? sourceId;
const status = String(source?.status ?? 'unknown');
const points = Array.isArray(entry.points) ? entry.points : [];
const latest = points.length > 0 ? points[points.length - 1] : null;
const first = points.length > 0 ? points[0] : null;
const restartDelta = latest && first ? Math.max(0, Number(latest.restartCount ?? 0) - Number(first.restartCount ?? 0)) : 0;
const errorDelta = latest && first ? Math.max(0, Number(latest.errorCount ?? 0) - Number(first.errorCount ?? 0)) : 0;
const statePath = buildSparklinePath(points, 'stateCode');
const healthPath = buildSparklinePath(points, 'healthCode');
const statusColor = status === 'running'
? 'text-green-400'
: status === 'degraded'
? 'text-amber-400'
: status === 'stopped'
? 'text-zinc-400'
: 'text-red-400';
return `
${escapeHtml(name)}
${escapeHtml(status)}
Source: ${escapeHtml(sourceId)}
Window deltas: restarts ${restartDelta} · errors ${errorDelta}
${statePath
? `
`
: '
No sample points yet
'}
Blue: state code · Green dashed: health code
`;
});
el.innerHTML = cards.join('');
}
function normalizeLogSourceSelection() {
const logSources = _lastObservabilitySources.filter((source) => source.logCapable);
if (logSources.length === 0) {
_selectedLogSourceId = null;
return [];
}
if (!_selectedLogSourceId || !logSources.some((source) => source.id === _selectedLogSourceId)) {
_selectedLogSourceId = logSources[0].id;
}
return logSources;
}
function updateServiceLogsPanel() {
const el = document.getElementById('ops-service-logs');
if (!el) {return;}
const logSources = normalizeLogSourceSelection();
if (logSources.length === 0) {
el.innerHTML = 'No log-capable services currently available
';
return;
}
const snapshot = _lastServiceLogs;
const rawLines = Array.isArray(snapshot?.lines) ? snapshot.lines : [];
const filterLevel = _logViewerState.level;
const lines = rawLines.filter((line) => filterLevel === 'all' || String(line.level ?? 'info') === filterLevel);
const statusToneClass = _logViewerState.tone === 'error'
? 'text-red-400'
: _logViewerState.tone === 'success'
? 'text-green-400'
: 'text-zinc-500';
const redactedBadge = snapshot?.redacted
? 'redacted'
: 'raw-safe';
const linesHtml = lines.length > 0
? lines.map((entry) => {
const level = String(entry.level ?? 'info');
const levelClass = level === 'error' ? 'text-red-400' : level === 'warn' ? 'text-amber-400' : 'text-zinc-400';
const ts = formatLogTime(entry.ts);
return `[${escapeHtml(ts)}] ${escapeHtml(level)} ${escapeHtml(String(entry.text ?? ''))}
`;
}).join('')
: 'No log lines for current filters
';
el.innerHTML = `
${redactedBadge}
${snapshot?.fetchedAt ? `Last fetch ${escapeHtml(formatLogTime(snapshot.fetchedAt))}` : 'No logs fetched yet'}
${escapeHtml(String(_logViewerState.status ?? ''))}
${linesHtml}
`;
el.querySelector('#ops-log-source')?.addEventListener('change', async (event) => {
_selectedLogSourceId = event.target.value;
await refreshServiceLogs({ force: true });
});
el.querySelector('#ops-log-since')?.addEventListener('change', async (event) => {
const parsed = Number(event.target.value);
if (Number.isFinite(parsed) && parsed > 0) {
_logViewerState.sinceSeconds = parsed;
}
await refreshServiceLogs({ force: true });
});
el.querySelector('#ops-log-lines-select')?.addEventListener('change', async (event) => {
const parsed = Number(event.target.value);
if (Number.isFinite(parsed) && parsed > 0) {
_logViewerState.lines = parsed;
}
await refreshServiceLogs({ force: true });
});
el.querySelector('#ops-log-level')?.addEventListener('change', (event) => {
_logViewerState.level = event.target.value;
updateServiceLogsPanel();
});
el.querySelector('#ops-log-auto')?.addEventListener('change', (event) => {
_logViewerState.autoRefresh = Boolean(event.target.checked);
if (_logViewerState.autoRefresh && !_logViewerState.paused) {
void refreshServiceLogs({ force: true });
}
});
el.querySelector('#ops-log-pause')?.addEventListener('change', (event) => {
_logViewerState.paused = Boolean(event.target.checked);
});
el.querySelector('#ops-log-refresh')?.addEventListener('click', async () => {
await refreshServiceLogs({ force: true });
});
}
async function refreshServiceLogs(opts = {}) {
if (!_dashboardClient || !_selectedLogSourceId) {return;}
const force = opts.force === true;
if (!force && (!_logViewerState.autoRefresh || _logViewerState.paused)) {
return;
}
try {
const result = await _dashboardClient.call('system.serviceLogs', {
sourceId: _selectedLogSourceId,
lines: _logViewerState.lines,
sinceSeconds: _logViewerState.sinceSeconds,
});
_lastServiceLogs = result;
_logViewerState.status = `Fetched ${Array.isArray(result?.lines) ? result.lines.length : 0} line(s)`;
_logViewerState.tone = 'success';
} catch (error) {
_logViewerState.status = `Log fetch failed: ${error instanceof Error ? error.message : String(error)}`;
_logViewerState.tone = 'error';
}
updateServiceLogsPanel();
}
async function handleLocalBackendAction(backendId, action) {
if (!_dashboardClient) {return;}
const actionLabel = LOCAL_BACKEND_ACTION_LABELS[action] ?? action;
_localBackendActionState.set(backendId, { pending: true, tone: 'neutral', message: `${actionLabel} requested…` });
updateLocalBackends({ backends: _lastLocalBackends });
try {
const result = await _dashboardClient.call('system.localBackendControl', {
backend: backendId,
action,
});
const status = result?.status;
const resultMessage = typeof result?.message === 'string' ? result.message : null;
if (status && typeof status === 'object') {
_lastLocalBackends = _lastLocalBackends.map((backend) =>
backend.id === backendId ? status : backend);
}
_localBackendActionState.set(backendId, {
pending: false,
tone: 'success',
message: resultMessage ? `${actionLabel} completed: ${resultMessage}` : `${actionLabel} completed`,
});
updateLocalBackends({ backends: _lastLocalBackends });
const refreshed = await fetchSlow(_dashboardClient);
if (refreshed?.localBackends) {
updateLocalBackends(refreshed.localBackends);
}
if (refreshed?.dockerDependencies) {
updateDockerDependencies(refreshed.dockerDependencies);
}
if (refreshed?.observabilitySources) {
_lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
? refreshed.observabilitySources.sources
: [];
updateServiceLogsPanel();
}
if (refreshed?.observabilitySeries) {
updateObservabilityGraphs(refreshed.observabilitySeries);
}
await refreshServiceLogs({ force: true });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
_localBackendActionState.set(backendId, {
pending: false,
tone: 'error',
message: `${actionLabel} failed: ${message}`,
});
updateLocalBackends({ backends: _lastLocalBackends });
}
}
async function handleDockerDependencyAction(dependencyId, action) {
if (!_dashboardClient) {return;}
const actionLabel = DOCKER_DEPENDENCY_ACTION_LABELS[action] ?? action;
_dockerDependencyActionState.set(dependencyId, { pending: true, tone: 'neutral', message: `${actionLabel} requested…` });
updateDockerDependencies({ dependencies: _lastDockerDependencies });
try {
const result = await _dashboardClient.call('system.dockerDependencyControl', {
dependency: dependencyId,
action,
});
const status = result?.status;
const resultMessage = typeof result?.message === 'string' ? result.message : null;
if (status && typeof status === 'object') {
_lastDockerDependencies = _lastDockerDependencies.map((dependency) =>
dependency.id === dependencyId ? status : dependency);
}
_dockerDependencyActionState.set(dependencyId, {
pending: false,
tone: 'success',
message: resultMessage ? `${actionLabel} completed: ${resultMessage}` : `${actionLabel} completed`,
});
updateDockerDependencies({ dependencies: _lastDockerDependencies });
const refreshed = await fetchSlow(_dashboardClient);
if (refreshed?.localBackends) {
updateLocalBackends(refreshed.localBackends);
}
if (refreshed?.dockerDependencies) {
updateDockerDependencies(refreshed.dockerDependencies);
}
if (refreshed?.observabilitySources) {
_lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
? refreshed.observabilitySources.sources
: [];
updateServiceLogsPanel();
}
if (refreshed?.observabilitySeries) {
updateObservabilityGraphs(refreshed.observabilitySeries);
}
await refreshServiceLogs({ force: true });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
_dockerDependencyActionState.set(dependencyId, {
pending: false,
tone: 'error',
message: `${actionLabel} failed: ${message}`,
});
updateDockerDependencies({ dependencies: _lastDockerDependencies });
}
}
function getConfigValue(path, fallbackValue) {
const value = getByPath(_lastAssistantConfig, path);
return value === undefined ? fallbackValue : value;
}
function renderServiceConfigModal() {
const root = document.getElementById('ops-service-config-modal-root');
if (!root) {return;}
if (!_serviceConfigState.open || !_serviceConfigState.serviceName) {
root.innerHTML = '';
return;
}
const service = _lastServices.find((svc) => svc.name === _serviceConfigState.serviceName);
if (!service) {
_serviceConfigState.open = false;
root.innerHTML = '';
return;
}
const quickTogglePath = SERVICE_TOGGLE_PATCH_PATHS[service.name];
const hasQuickToggle = Boolean(quickTogglePath);
const quickToggleValue = hasQuickToggle ? Boolean(getConfigValue(quickTogglePath, false)) : false;
const heartbeatSection = service.name === 'heartbeat'
? `
`
: '';
const toneClass = _serviceConfigState.tone === 'success'
? 'text-green-500'
: _serviceConfigState.tone === 'error'
? 'text-red-500'
: 'text-zinc-500';
root.innerHTML = `
Configure ${escapeHtml(service.name)}
${escapeHtml(service.description ?? '')}
Quick Settings
${hasQuickToggle ? `
` : '
No quick toggle available for this service.
'}
${heartbeatSection}
Advanced Patch (optional JSON)
${escapeHtml(_serviceConfigState.status ?? '')}
`;
const closeModal = () => {
_serviceConfigState.open = false;
renderServiceConfigModal();
};
root.querySelector('#svc-config-close')?.addEventListener('click', closeModal);
root.querySelector('#svc-config-cancel')?.addEventListener('click', closeModal);
root.querySelector('#svc-config-overlay')?.addEventListener('click', closeModal);
root.querySelector('#svc-config-save')?.addEventListener('click', async () => {
if (!_dashboardClient || !_serviceConfigState.serviceName) {return;}
const patches = {};
if (quickTogglePath) {
patches[quickTogglePath] = Boolean(root.querySelector('#svc-quick-enabled')?.checked);
}
if (_serviceConfigState.serviceName === 'heartbeat') {
patches['automation.heartbeat.interval'] = (root.querySelector('#svc-heartbeat-interval')?.value ?? '5m').trim();
patches['automation.heartbeat.notify_cooldown'] = (root.querySelector('#svc-heartbeat-notify-cooldown')?.value ?? '30m').trim();
patches['automation.heartbeat.failure_threshold'] = Number(root.querySelector('#svc-heartbeat-failure-threshold')?.value ?? 2);
patches['automation.heartbeat.disk_threshold_mb'] = Number(root.querySelector('#svc-heartbeat-disk-threshold')?.value ?? 100);
patches['automation.heartbeat.checks'] = HEARTBEAT_CHECK_KEYS.filter(
(check) => Boolean(root.querySelector(`[data-heartbeat-check="${check}"]`)?.checked),
);
}
const advancedRaw = (root.querySelector('#svc-advanced-patch')?.value ?? '').trim();
_serviceConfigState.advancedPatch = advancedRaw;
if (advancedRaw.length > 0) {
try {
const parsed = JSON.parse(advancedRaw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Advanced patch must be a JSON object');
}
Object.assign(patches, parsed);
} catch (error) {
_serviceConfigState.status = error instanceof Error ? error.message : String(error);
_serviceConfigState.tone = 'error';
renderServiceConfigModal();
return;
}
}
if (Object.keys(patches).length === 0) {
_serviceConfigState.status = 'No changes to save.';
_serviceConfigState.tone = 'neutral';
renderServiceConfigModal();
return;
}
try {
const result = await _dashboardClient.call('config.patch', { patches });
const applied = Array.isArray(result?.applied) ? result.applied : [];
const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
if (applied.length === 0) {
_serviceConfigState.status = rejected.length > 0
? `No changes applied. Rejected: ${rejected.join(', ')}`
: 'No changes were applied.';
_serviceConfigState.tone = 'error';
renderServiceConfigModal();
return;
}
_serviceConfigState.status = `Saved ${applied.length} setting(s).${rejected.length > 0 ? ` Rejected: ${rejected.join(', ')}` : ''}`;
_serviceConfigState.tone = rejected.length > 0 ? 'error' : 'success';
const refreshed = await fetchSlow(_dashboardClient);
if (refreshed) {
updateServices(refreshed.services);
updateLocalBackends(refreshed.localBackends);
updateDockerDependencies(refreshed.dockerDependencies);
if (refreshed.observabilitySources) {
_lastObservabilitySources = Array.isArray(refreshed.observabilitySources.sources)
? refreshed.observabilitySources.sources
: [];
}
if (refreshed.observabilitySeries) {
updateObservabilityGraphs(refreshed.observabilitySeries);
}
updateServiceLogsPanel();
await refreshServiceLogs({ force: true });
updateSessionAnalytics(refreshed.sessionAnalytics);
updateContextHealth(refreshed.contextUsage);
if (refreshed.config) {
_lastAssistantConfig = refreshed.config;
updateAssistantHealth(_lastAssistantConfig);
}
}
renderServiceConfigModal();
} catch (error) {
_serviceConfigState.status = `Save failed: ${error instanceof Error ? error.message : String(error)}`;
_serviceConfigState.tone = 'error';
renderServiceConfigModal();
}
});
}
// ── Data fetching ───────────────────────────────────────────────
async function fetchFast(client) {
try {
const [metrics, eventsData, requestsData] = await Promise.all([
client.call('system.metrics'),
client.call('system.events', { limit: 50 }),
client.call('system.activeRequests'),
]);
return { metrics, eventsData, requestsData };
} catch {
return null;
}
}
async function fetchSlow(client) {
const [
health,
services,
localBackends,
dockerDependencies,
observabilitySources,
observabilitySeries,
sessionAnalytics,
contextUsage,
config,
modelCatalog,
] = await Promise.allSettled([
client.call('system.health'),
client.call('system.services'),
client.call('system.localBackends'),
client.call('system.dockerDependencies'),
client.call('system.observabilitySources'),
client.call('system.observabilitySeries', { windowMinutes: 60, bucketSeconds: 30 }),
client.call('system.sessionAnalytics', { days: 14, topLimit: 5 }),
client.call('system.contextUsage'),
client.call('config.get'),
client.call('system.modelCatalog'),
]);
const unwrap = (result) => (result.status === 'fulfilled' ? result.value : null);
const configValue = unwrap(config);
const modelCatalogValue = unwrap(modelCatalog);
if (configValue && typeof configValue === 'object') {
configValue.__modelCatalog = Array.isArray(modelCatalogValue?.providers) ? modelCatalogValue.providers : [];
}
return {
health: unwrap(health),
services: unwrap(services),
localBackends: unwrap(localBackends),
dockerDependencies: unwrap(dockerDependencies),
observabilitySources: unwrap(observabilitySources),
observabilitySeries: unwrap(observabilitySeries),
sessionAnalytics: unwrap(sessionAnalytics),
contextUsage: unwrap(contextUsage),
config: configValue,
};
}
// ── Main load function ──────────────────────────────────────────
let _lastHealth = null;
let _lastMetrics = null;
async function loadDashboard(el, client) {
_dashboardClient = client;
renderSkeleton(el);
// Fetch everything initially
const [fast, slow] = await Promise.all([
fetchFast(client),
fetchSlow(client),
]);
_lastHealth = slow?.health ?? null;
_lastMetrics = fast?.metrics ?? null;
if (fast) {
updateCounters(fast.metrics, _lastHealth);
updateModelTable(fast.metrics);
updateEvents(fast.eventsData);
updateActiveRequests(fast.requestsData);
}
if (slow?.config) {
_lastAssistantConfig = slow.config;
}
if (slow?.services) {
updateServices(slow.services);
}
if (slow?.localBackends) {
updateLocalBackends(slow.localBackends);
}
if (slow?.dockerDependencies) {
updateDockerDependencies(slow.dockerDependencies);
}
if (slow?.observabilitySources) {
_lastObservabilitySources = Array.isArray(slow.observabilitySources.sources)
? slow.observabilitySources.sources
: [];
}
if (slow?.observabilitySeries) {
updateObservabilityGraphs(slow.observabilitySeries);
} else {
updateObservabilityGraphs({ series: [] });
}
updateServiceLogsPanel();
await refreshServiceLogs({ force: true });
if (slow?.sessionAnalytics) {
updateSessionAnalytics(slow.sessionAnalytics);
}
if (slow?.contextUsage) {
updateContextHealth(slow.contextUsage);
}
if (slow?.config) {
updateAssistantHealth(_lastAssistantConfig);
}
// Fast refresh: 3 seconds for metrics, events, requests
_fastTimer = setInterval(async () => {
const data = await fetchFast(client);
if (data) {
_lastMetrics = data.metrics;
updateCounters(data.metrics, _lastHealth);
updateModelTable(data.metrics);
updateEvents(data.eventsData);
updateActiveRequests(data.requestsData);
}
}, 3000);
// Slow refresh: 10 seconds for health, services
_slowTimer = setInterval(async () => {
const data = await fetchSlow(client);
if (data.health) {
_lastHealth = data.health;
updateCounters(_lastMetrics, data.health);
}
if (data.config) {
_lastAssistantConfig = data.config;
}
if (data.services) {
updateServices(data.services);
}
if (data.localBackends) {
updateLocalBackends(data.localBackends);
}
if (data.dockerDependencies) {
updateDockerDependencies(data.dockerDependencies);
}
if (data.observabilitySources) {
_lastObservabilitySources = Array.isArray(data.observabilitySources.sources)
? data.observabilitySources.sources
: [];
updateServiceLogsPanel();
}
if (data.observabilitySeries) {
updateObservabilityGraphs(data.observabilitySeries);
}
if (data.sessionAnalytics) {
updateSessionAnalytics(data.sessionAnalytics);
}
if (data.contextUsage) {
updateContextHealth(data.contextUsage);
}
if (data.config) {
updateAssistantHealth(_lastAssistantConfig);
}
}, 10000);
_logRefreshTimer = setInterval(async () => {
await refreshServiceLogs();
}, 5000);
}
export const DashboardPage = {
async render(el, client) {
await loadDashboard(el, client);
},
teardown() {
if (_fastTimer) {
clearInterval(_fastTimer);
_fastTimer = null;
}
if (_slowTimer) {
clearInterval(_slowTimer);
_slowTimer = null;
}
if (_logRefreshTimer) {
clearInterval(_logRefreshTimer);
_logRefreshTimer = null;
}
_lastHealth = null;
_lastMetrics = null;
_dashboardClient = null;
_lastPlaybookRollbackPatches = null;
_lastBriefingTestAt = null;
_assistantSaveState = null;
_lastAssistantConfig = null;
_assistantManualOverrides = new Set();
_assistantModelDefaultsDraft = null;
_assistantDraftState = new Map();
_assistantDraftTouchedAt = 0;
_lastServices = [];
_lastLocalBackends = [];
_lastDockerDependencies = [];
_lastObservabilitySources = [];
_lastObservabilitySeries = null;
_lastServiceLogs = null;
_selectedLogSourceId = null;
_logViewerState = {
lines: 200,
sinceSeconds: 900,
level: 'all',
autoRefresh: true,
paused: false,
status: null,
tone: 'neutral',
};
_localBackendActionState = new Map();
_dockerDependencyActionState = new Map();
_serviceConfigState = {
open: false,
serviceName: null,
status: null,
tone: 'neutral',
advancedPatch: '',
};
_lastCouncilTask = '';
_lastCouncilResult = null;
_lastCouncilError = null;
},
};