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:
+235
-222
@@ -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('');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user