feat(gateway-ui): rewrite all page renderers with Tailwind classes

Convert dashboard, chat, sessions, usage, and settings pages from
legacy CSS to Tailwind utility classes. Responsive grid layouts,
mobile-friendly touch targets, zinc/blue color palette. All element
IDs and event bindings preserved for functional compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William Valentin
2026-02-18 13:06:06 -08:00
parent 765e19933d
commit 02d63fe573
5 changed files with 532 additions and 473 deletions
+235 -222
View File
@@ -126,46 +126,46 @@ function buildRollbackPatchesFromSnapshot(snapshot) {
function renderSkeleton(el) {
el.innerHTML = `
<h1 class="page-title">Live Ops Dashboard</h1>
<h1 class="text-2xl font-semibold text-zinc-50 mb-6">Live Ops Dashboard</h1>
<h2 class="section-title">Core Counters</h2>
<div class="stats-grid" id="ops-counters">
<div class="stat-card"><div class="stat-label">Loading...</div><div class="stat-value">—</div></div>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Core Counters</h2>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8" id="ops-counters">
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors"><div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Loading...</div><div class="text-2xl font-bold font-mono text-zinc-50">—</div></div>
</div>
<h2 class="section-title">Model Performance</h2>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Model Performance</h2>
<div id="ops-model-table">
<div class="text-muted text-sm">Loading...</div>
<div class="text-sm text-zinc-500">Loading...</div>
</div>
<h2 class="section-title">Session Analytics</h2>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Session Analytics</h2>
<div id="ops-session-analytics">
<div class="text-muted text-sm">Loading...</div>
<div class="text-sm text-zinc-500">Loading...</div>
</div>
<h2 class="section-title">Context Health</h2>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Context Health</h2>
<div id="ops-context-health">
<div class="text-muted text-sm">Loading...</div>
<div class="text-sm text-zinc-500">Loading...</div>
</div>
<h2 class="section-title">Assistant Health</h2>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Assistant Health</h2>
<div id="ops-assistant-health">
<div class="text-muted text-sm">Loading...</div>
<div class="text-sm text-zinc-500">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>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Event Stream</h2>
<div class="max-h-72 overflow-y-auto bg-zinc-900 border border-zinc-800 rounded-lg p-2 font-mono text-xs" id="ops-events">
<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words text-zinc-400">Loading events...</div>
</div>
<h2 class="section-title">Active Requests</h2>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Active Requests</h2>
<div id="ops-requests">
<div class="text-muted text-sm">Loading...</div>
<div class="text-sm text-zinc-500">Loading...</div>
</div>
<h2 class="section-title">Services</h2>
<div id="ops-services" class="services-grid">
<div class="text-muted text-sm">Loading...</div>
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Services</h2>
<div id="ops-services" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<div class="text-sm text-zinc-500">Loading...</div>
</div>
`;
}
@@ -185,13 +185,13 @@ function updateCounters(metrics, health) {
{ 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 ? 'error' : '' },
{ label: 'Errors', value: String(errCount), cls: errCount > 0 ? 'text-red-500' : '' },
];
el.innerHTML = cards.map(c =>
`<div class="stat-card">
<div class="stat-label">${c.label}</div>
<div class="stat-value ${c.cls}">${c.value}</div>
`<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">${c.label}</div>
<div class="text-2xl font-bold font-mono text-zinc-50 ${c.cls}">${c.value}</div>
</div>`,
).join('');
}
@@ -204,7 +204,7 @@ function updateModelTable(metrics) {
const calls = mc?.recentCalls ?? [];
if (calls.length === 0) {
el.innerHTML = '<div class="text-muted text-sm">No model calls recorded yet</div>';
el.innerHTML = '<div class="text-sm text-zinc-500">No model calls recorded yet</div>';
return;
}
@@ -213,41 +213,43 @@ function updateModelTable(metrics) {
const errorRate = mc.errorRate ?? 0;
const summaryHtml = `
<div class="metrics-summary">
<div class="metric"><span>Total Calls:</span> <span class="metric-value">${totalCalls}</span></div>
<div class="metric"><span>Avg Latency:</span> <span class="metric-value">${avgLatency}ms</span></div>
<div class="metric"><span>Error Rate:</span> <span class="metric-value">${(errorRate * 100).toFixed(2)}%</span></div>
<div class="flex flex-wrap gap-4 md:gap-6 mb-3 text-sm text-zinc-400">
<div><span>Total Calls:</span> <span class="font-mono text-zinc-50">${totalCalls}</span></div>
<div><span>Avg Latency:</span> <span class="font-mono text-zinc-50">${avgLatency}ms</span></div>
<div><span>Error Rate:</span> <span class="font-mono text-zinc-50">${(errorRate * 100).toFixed(2)}%</span></div>
</div>
`;
// Show newest first
const rows = [...calls].reverse().map(c => {
const status = c.error ? '<span class="text-error">✗</span>' : '<span class="text-success">✓</span>';
return `<tr>
<td>${timeAgo(c.timestamp)}</td>
<td>${escapeHtml(c.provider)}</td>
<td>${c.latency}ms</td>
<td>${c.tokensPerSec.toFixed(1)}</td>
<td>${c.inputTokens}/${c.outputTokens}</td>
<td>${status}</td>
const status = c.error ? '<span class="text-red-500">✗</span>' : '<span class="text-green-500">✓</span>';
return `<tr class="hover:bg-zinc-800/50">
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${timeAgo(c.timestamp)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(c.provider)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.latency}ms</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.tokensPerSec.toFixed(1)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.inputTokens}/${c.outputTokens}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${status}</td>
</tr>`;
}).join('');
el.innerHTML = `
${summaryHtml}
<table>
<thead>
<tr>
<th>Time</th>
<th>Provider</th>
<th>Latency</th>
<th>Tokens/sec</th>
<th>In/Out</th>
<th>Status</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Time</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Provider</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Latency</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Tokens/sec</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">In/Out</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Status</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
@@ -258,7 +260,7 @@ function updateEvents(eventsData) {
const events = eventsData?.events ?? [];
if (events.length === 0) {
el.innerHTML = '<div class="event-row event-level-info">No events recorded yet</div>';
el.innerHTML = '<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words text-zinc-400">No events recorded yet</div>';
return;
}
@@ -268,8 +270,8 @@ function updateEvents(eventsData) {
el.innerHTML = reversed.map(e => {
const time = formatTime(e.timestamp);
const level = (e.level || 'info').toUpperCase();
const cls = `event-level-${e.level || 'info'}`;
return `<div class="event-row ${cls}">[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}</div>`;
const levelColor = e.level === 'error' ? 'text-red-500' : e.level === 'warn' ? 'text-amber-500' : 'text-zinc-400';
return `<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words ${levelColor}">[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}</div>`;
}).join('');
// Auto-scroll to bottom
@@ -283,7 +285,7 @@ function updateActiveRequests(requestsData) {
const requests = requestsData?.requests ?? [];
if (requests.length === 0) {
el.innerHTML = '<div class="text-muted text-sm">No active requests</div>';
el.innerHTML = '<div class="text-sm text-zinc-500">No active requests</div>';
return;
}
@@ -292,26 +294,28 @@ function updateActiveRequests(requestsData) {
? `${r.durationMs}ms`
: `${(r.durationMs / 1000).toFixed(1)}s`;
const started = formatTime(r.startedAt);
return `<tr>
<td>${escapeHtml(r.sessionId)}</td>
<td>${escapeHtml(r.channel)}</td>
<td>${duration}</td>
<td>${started}</td>
return `<tr class="hover:bg-zinc-800/50">
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(r.sessionId)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(r.channel)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${duration}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${started}</td>
</tr>`;
}).join('');
el.innerHTML = `
<table>
<thead>
<tr>
<th>Session</th>
<th>Channel</th>
<th>Duration</th>
<th>Started</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Channel</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Duration</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Started</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
@@ -329,95 +333,95 @@ function updateSessionAnalytics(analyticsData) {
const avgMessagesPerSession = analyticsData?.averageMessagesPerSession ?? 0;
const summaryHtml = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Sessions (Window)</div>
<div class="stat-value">${formatNumber(totalSessions)}</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Sessions (Window)</div>
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalSessions)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Messages (Window)</div>
<div class="stat-value">${formatNumber(totalMessages)}</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Messages (Window)</div>
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalMessages)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Avg Session</div>
<div class="stat-value">${formatSessionDurationFromMessages(avgMessagesPerSession)}</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Avg Session</div>
<div class="text-2xl font-bold font-mono text-zinc-50">${formatSessionDurationFromMessages(avgMessagesPerSession)}</div>
</div>
</div>
`;
const topToolsHtml = topTools.length > 0
? `<ul class="tool-list">${topTools.map((tool) =>
`<li><span class="text-accent">${escapeHtml(tool.toolName)}</span> <span class="text-muted">(${formatNumber(tool.executions)})</span></li>`,
).join('')}</ul>`
: '<div class="text-muted text-sm">No tool usage captured in this window</div>';
? `<div>${topTools.map((tool) =>
`<div class="py-1.5 border-b border-zinc-800/50 last:border-0 text-sm"><span class="text-blue-500">${escapeHtml(tool.toolName)}</span> <span class="text-zinc-500">(${formatNumber(tool.executions)})</span></div>`,
).join('')}</div>`
: '<div class="text-sm text-zinc-500">No tool usage captured in this window</div>';
const topTopicsHtml = topTopics.length > 0
? `<ul class="tool-list">${topTopics.map((topic) =>
`<li><span class="text-accent">${escapeHtml(topic.topic)}</span> <span class="text-muted">(${formatNumber(topic.occurrences)})</span></li>`,
).join('')}</ul>`
: '<div class="text-muted text-sm">No indexed topics captured in this window</div>';
? `<div>${topTopics.map((topic) =>
`<div class="py-1.5 border-b border-zinc-800/50 last:border-0 text-sm"><span class="text-blue-500">${escapeHtml(topic.topic)}</span> <span class="text-zinc-500">(${formatNumber(topic.occurrences)})</span></div>`,
).join('')}</div>`
: '<div class="text-sm text-zinc-500">No indexed topics captured in this window</div>';
const topSessionsHtml = topSessions.length > 0
? `<table>
? `<div class="overflow-x-auto"><table class="w-full text-sm">
<thead>
<tr>
<th>Session</th>
<th>Messages</th>
<th>Last Active</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Messages</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Last Active</th>
</tr>
</thead>
<tbody>
${topSessions.map((session) => `
<tr>
<td>${escapeHtml(session.sessionId)}</td>
<td>${formatNumber(session.messages)}</td>
<td>${timeAgo(session.lastActivity * 1000)}</td>
<tr class="hover:bg-zinc-800/50">
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(session.sessionId)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(session.messages)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${timeAgo(session.lastActivity * 1000)}</td>
</tr>
`).join('')}
</tbody>
</table>`
: '<div class="text-muted text-sm">No session activity in this window</div>';
</table></div>`
: '<div class="text-sm text-zinc-500">No session activity in this window</div>';
const dailyHtml = daily.length > 0
? `<table>
? `<div class="overflow-x-auto"><table class="w-full text-sm">
<thead>
<tr>
<th>Day</th>
<th>Sessions</th>
<th>Messages</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Day</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Sessions</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Messages</th>
</tr>
</thead>
<tbody>
${daily.slice(0, 7).map((row) => `
<tr>
<td>${formatDay(row.day)}</td>
<td>${formatNumber(row.sessions)}</td>
<td>${formatNumber(row.messages)}</td>
<tr class="hover:bg-zinc-800/50">
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatDay(row.day)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(row.sessions)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(row.messages)}</td>
</tr>
`).join('')}
</tbody>
</table>`
: '<div class="text-muted text-sm">No daily activity in this window</div>';
</table></div>`
: '<div class="text-sm text-zinc-500">No daily activity in this window</div>';
el.innerHTML = `
${summaryHtml}
<div class="services-grid">
<div class="service-card">
<span class="service-name">Top Tools</span>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
<div class="text-sm font-semibold text-zinc-50 mb-3">Top Tools</div>
${topToolsHtml}
</div>
<div class="service-card">
<span class="service-name">Top Topics</span>
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
<div class="text-sm font-semibold text-zinc-50 mb-3">Top Topics</div>
${topTopicsHtml}
</div>
</div>
<div class="services-grid">
<div class="service-card">
<span class="service-name">Top Sessions</span>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
<div class="text-sm font-semibold text-zinc-50 mb-3">Top Sessions</div>
${topSessionsHtml}
</div>
<div class="service-card">
<span class="service-name">Daily Trend (Last 7 Rows)</span>
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
<div class="text-sm font-semibold text-zinc-50 mb-3">Daily Trend (Last 7 Rows)</div>
${dailyHtml}
</div>
</div>
@@ -430,7 +434,7 @@ function updateContextHealth(contextData) {
const sessions = contextData?.sessions ?? [];
if (sessions.length === 0) {
el.innerHTML = '<div class="text-muted text-sm">No active context usage snapshots</div>';
el.innerHTML = '<div class="text-sm text-zinc-500">No active context usage snapshots</div>';
return;
}
@@ -440,18 +444,18 @@ function updateContextHealth(contextData) {
const overThreshold = sessions.filter(s => (s.budget?.shouldCompact ?? false)).length;
const summary = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Highest Usage</div>
<div class="stat-value ${highest >= 90 ? 'error' : ''}">${highest.toFixed(1)}%</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Highest Usage</div>
<div class="text-2xl font-bold font-mono text-zinc-50 ${highest >= 90 ? 'text-red-500' : ''}">${highest.toFixed(1)}%</div>
</div>
<div class="stat-card">
<div class="stat-label">Sessions Near Limit</div>
<div class="stat-value ${overThreshold > 0 ? 'error' : ''}">${overThreshold}</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Sessions Near Limit</div>
<div class="text-2xl font-bold font-mono text-zinc-50 ${overThreshold > 0 ? 'text-red-500' : ''}">${overThreshold}</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Snapshots</div>
<div class="stat-value">${sessions.length}</div>
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Active Snapshots</div>
<div class="text-2xl font-bold font-mono text-zinc-50">${sessions.length}</div>
</div>
</div>
`;
@@ -459,28 +463,30 @@ function updateContextHealth(contextData) {
const rows = top.map((entry) => {
const budget = entry.budget ?? {};
const usage = budget.usagePct ?? 0;
const cls = usage >= 95 ? 'text-error' : usage >= 85 ? 'status-warning' : '';
return `<tr>
<td>${escapeHtml(entry.sessionId)}</td>
<td class="${cls}">${usage.toFixed(1)}%</td>
<td>${formatNumber(budget.estimatedTokens ?? 0)} / ${formatNumber(budget.contextWindow ?? 0)}</td>
<td>${budget.shouldCompact ? 'yes' : 'no'}</td>
const cls = usage >= 95 ? 'text-red-500' : usage >= 85 ? 'text-amber-500' : '';
return `<tr class="hover:bg-zinc-800/50">
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(entry.sessionId)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono ${cls}">${usage.toFixed(1)}%</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(budget.estimatedTokens ?? 0)} / ${formatNumber(budget.contextWindow ?? 0)}</td>
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${budget.shouldCompact ? 'yes' : 'no'}</td>
</tr>`;
}).join('');
el.innerHTML = `
${summary}
<table>
<thead>
<tr>
<th>Session</th>
<th>Usage</th>
<th>Estimated Tokens</th>
<th>Should Compact</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Usage</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Estimated Tokens</th>
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Should Compact</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
@@ -488,7 +494,7 @@ async function applyAssistantPatch(patches, statusEl) {
if (!_dashboardClient) {return;}
if (statusEl) {
statusEl.textContent = 'Saving...';
statusEl.className = 'text-sm text-muted';
statusEl.className = 'text-sm text-zinc-500';
}
try {
const result = await _dashboardClient.call('config.patch', { patches });
@@ -500,22 +506,22 @@ async function applyAssistantPatch(patches, statusEl) {
if (statusEl) {
if (persistError) {
statusEl.textContent = `Save failed: ${persistError}`;
statusEl.className = 'text-sm text-error';
statusEl.className = 'text-sm text-red-500';
} else if (rejected.length > 0) {
statusEl.textContent = `Rejected: ${rejected.join(', ')}`;
statusEl.className = 'text-sm text-error';
statusEl.className = 'text-sm text-red-500';
} else if (!persisted) {
statusEl.textContent = `Runtime saved (${applied.length} updated)`;
statusEl.className = 'text-sm text-muted';
statusEl.className = 'text-sm text-zinc-500';
} else {
statusEl.textContent = `Saved (${applied.length} updated)`;
statusEl.className = 'text-sm text-success';
statusEl.className = 'text-sm text-green-500';
}
}
} catch (error) {
if (statusEl) {
statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`;
statusEl.className = 'text-sm text-error';
statusEl.className = 'text-sm text-red-500';
}
}
}
@@ -524,7 +530,7 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
if (!_dashboardClient) {return;}
if (statusEl) {
statusEl.textContent = 'Triggering test briefing...';
statusEl.className = 'text-sm text-muted';
statusEl.className = 'text-sm text-zinc-500';
}
try {
const result = await _dashboardClient.call('tools.invoke', {
@@ -536,7 +542,7 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
const output = typeof result.output === 'string' ? result.output : 'Triggered.';
if (statusEl) {
statusEl.textContent = output;
statusEl.className = 'text-sm text-success';
statusEl.className = 'text-sm text-green-500';
}
_lastBriefingTestAt = Date.now();
return true;
@@ -544,13 +550,13 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
if (statusEl) {
statusEl.textContent = result?.error ?? 'Failed to trigger briefing.';
statusEl.className = 'text-sm text-error';
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-error';
statusEl.className = 'text-sm text-red-500';
}
return false;
}
@@ -592,103 +598,103 @@ function updateAssistantHealth(configData) {
];
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 class="flex justify-between items-center px-3 py-2.5 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">
<span class="text-zinc-400">${escapeHtml(label)}</span>
<span class="font-bold ${value ? 'text-green-500' : 'text-zinc-500'}">${value ? 'ON' : 'OFF'}</span>
</div>
`;
el.innerHTML = `
<div class="assistant-health-grid">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2 mb-4">
${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 class="flex justify-between items-center px-3 py-2.5 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">
<span class="text-zinc-400">Extract Threshold</span>
<span class="font-bold">${Number.isFinite(proactiveThreshold) ? proactiveThreshold : 1}</span>
</div>
</div>
<div class="assistant-actions">
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-announce">
<div class="flex flex-wrap gap-2 mb-4">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors 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">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors 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">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors 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">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors 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">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-tts">
${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
</button>
</div>
<div class="assistant-playbooks">
<div class="assistant-preview-title">Assistant Playbooks</div>
<div class="assistant-playbook-grid">
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-executive">
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
<div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Playbooks</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 my-3">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-executive">
Executive
</button>
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-operator">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-operator">
Operator
</button>
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-focus">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-focus">
Focus
</button>
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-undo" ${_lastPlaybookRollbackPatches ? '' : 'disabled'}>
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-undo" ${_lastPlaybookRollbackPatches ? '' : 'disabled'}>
Undo Last Playbook
</button>
</div>
<div class="text-sm text-muted">Executive: announce + voice + aggressive interrupt. Operator: announce + memory-first + steer backlog. Focus: reactive, quieter mode.</div>
<div class="text-sm text-zinc-500">Executive: announce + voice + aggressive interrupt. Operator: announce + memory-first + steer backlog. Focus: reactive, quieter mode.</div>
</div>
<div class="assistant-setup">
<div class="assistant-preview-title">Assistant Activation Checklist</div>
<ul class="assistant-checklist">
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
<div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Activation Checklist</div>
<div class="space-y-1 mb-4">
${checklistRows.map((row) => `
<li class="${row.done ? 'done' : ''}">
<span class="assistant-check">${row.done ? '✓' : '○'}</span>
<div class="flex items-center gap-2 py-1 text-sm ${row.done ? 'text-zinc-50' : 'text-zinc-500'}">
<span class="w-5 text-center font-bold">${row.done ? '✓' : '○'}</span>
<span>${escapeHtml(row.label)}</span>
</li>
</div>
`).join('')}
</ul>
<div class="assistant-setup-grid">
<label class="assistant-field">
<span>Briefing output channel</span>
<input id="assist-brief-channel" type="text" value="${escapeHtml(briefingOutput?.channel ?? '')}" placeholder="telegram" />
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">Briefing output channel</span>
<input id="assist-brief-channel" type="text" value="${escapeHtml(briefingOutput?.channel ?? '')}" placeholder="telegram" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
<label class="assistant-field">
<span>Briefing output peer/chat id</span>
<input id="assist-brief-peer" type="text" value="${escapeHtml(briefingOutput?.peer ?? '')}" placeholder="123456789" />
<label class="flex flex-col gap-1.5">
<span class="text-sm text-zinc-400">Briefing output peer/chat id</span>
<input id="assist-brief-peer" type="text" value="${escapeHtml(briefingOutput?.peer ?? '')}" placeholder="123456789" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
</label>
</div>
<div class="assistant-actions">
<button class="btn btn-secondary assistant-action-btn" data-action="save-briefing-output">
<div class="flex flex-wrap gap-2">
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-briefing-output">
Save Briefing Output
</button>
</div>
</div>
<div class="assistant-preview">
<div class="assistant-preview-header">
<div class="assistant-preview-title">Morning Brief Preview</div>
<button class="btn btn-secondary assistant-action-btn" data-action="test-daily-briefing" ${briefingReady ? '' : 'disabled'}>
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
<div class="flex items-center justify-between mb-3">
<div class="text-sm font-semibold text-zinc-50">Morning Brief Preview</div>
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="test-daily-briefing" ${briefingReady ? '' : 'disabled'}>
Send Test Briefing
</button>
</div>
<div class="assistant-preview-meta text-sm text-muted">
<span>name: <code>${escapeHtml(briefingName)}</code></span>
<span>schedule: <code>${escapeHtml(briefingSchedule)}</code></span>
<span>timezone: <code>${escapeHtml(briefingTimezone)}</code></span>
<span>tier: <code>${escapeHtml(briefingModelTier)}</code></span>
<span>output: <code>${escapeHtml(briefingOutputLabel)}</code></span>
<div class="flex flex-wrap gap-3 text-sm text-zinc-500 mb-3">
<span>name: <code class="font-mono">${escapeHtml(briefingName)}</code></span>
<span>schedule: <code class="font-mono">${escapeHtml(briefingSchedule)}</code></span>
<span>timezone: <code class="font-mono">${escapeHtml(briefingTimezone)}</code></span>
<span>tier: <code class="font-mono">${escapeHtml(briefingModelTier)}</code></span>
<span>output: <code class="font-mono">${escapeHtml(briefingOutputLabel)}</code></span>
</div>
<div class="assistant-preview-body"><code>${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}</code></div>
${briefingReady ? '' : '<div class="text-sm text-muted">Enable daily briefing and set output channel/peer to test-send.</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>'}
</div>
<div id="ops-assistant-status" class="text-sm text-muted"></div>
<div id="ops-assistant-status" class="text-sm text-zinc-500 mt-4"></div>
`;
const statusEl = el.querySelector('#ops-assistant-status');
@@ -722,7 +728,7 @@ function updateAssistantHealth(configData) {
if (!_lastPlaybookRollbackPatches) {
if (statusEl) {
statusEl.textContent = 'No playbook changes to undo.';
statusEl.className = 'text-sm text-muted';
statusEl.className = 'text-sm text-zinc-500';
}
return;
}
@@ -734,7 +740,7 @@ function updateAssistantHealth(configData) {
if (!channel || !peer) {
if (statusEl) {
statusEl.textContent = 'Briefing output channel and peer are required.';
statusEl.className = 'text-sm text-error';
statusEl.className = 'text-sm text-red-500';
}
return;
}
@@ -765,7 +771,7 @@ function _updateChannels(channelsData) {
const channels = channelsData?.channels ?? [];
if (channels.length === 0) {
el.innerHTML = '<div class="text-muted text-sm">No channels registered</div>';
el.innerHTML = '<div class="text-sm text-zinc-500">No channels registered</div>';
return;
}
@@ -784,27 +790,34 @@ function updateServices(servicesData) {
const services = servicesData?.services ?? [];
if (services.length === 0) {
el.innerHTML = '<div class="text-muted text-sm">No services configured</div>';
el.innerHTML = '<div class="text-sm text-zinc-500">No services configured</div>';
return;
}
el.innerHTML = services.map(svc => {
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
const statusClass = svc.status === 'connected'
? 'connected'
const borderColor = svc.status === 'connected'
? 'border-l-green-500'
: svc.status === 'configured'
? 'configured'
? 'border-l-blue-500'
: svc.status === 'error'
? 'error'
: svc.status === 'not_configured'
? 'not-configured'
: 'disconnected';
? '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 `<div class="service-card service-${statusClass}">
<span class="service-type-icon">${typeIcon}</span>
<span class="service-name">${escapeHtml(svc.name)}${itemCount}</span>
<span class="service-status">${escapeHtml(svc.status)}</span>
<span class="service-description text-muted text-xs">${escapeHtml(svc.description)}</span>
return `<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-1 border-l-4 ${borderColor}">
<div class="flex items-center gap-2">
<span class="text-sm shrink-0">${typeIcon}</span>
<span class="text-sm font-semibold text-zinc-50">${escapeHtml(svc.name)}${itemCount}</span>
</div>
<span class="text-xs uppercase ${statusColor}">${escapeHtml(svc.status)}</span>
<span class="text-xs text-zinc-500">${escapeHtml(svc.description)}</span>
</div>`;
}).join('');
}