Files
shopify-ai-backup/chat/public/admin-resources.html
southseact-3d 25d23d8dd1 Add comprehensive feature request admin functionality
- Update data model to include status, adminReply, and updatedAt fields
- Hide user emails from public API responses for privacy
- Add admin-only endpoints: list, reply, update status, delete
- Create admin-feature-requests.html with full management UI
- Add status badges and admin replies to public feature requests page
- Add Feature Requests link to all admin page sidebars

Admin capabilities:
- View all feature requests with author emails (admin only)
- Reply to feature requests with admin responses visible to public
- Update status: pending, planned, in-progress, completed, declined
- Delete feature requests
- Filter and sort by status, votes, date
2026-02-10 13:27:36 +00:00

965 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin - Resource Usage</title>
<link rel="stylesheet" href="/styles.css" />
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
color: var(--text);
margin: 8px 0;
}
.stat-value.warning { color: var(--warning); }
.stat-value.danger { color: var(--danger); }
.stat-label {
color: var(--muted);
font-size: 13px;
}
.section-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: var(--text);
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.data-table th {
background: var(--background);
padding: 10px 12px;
text-align: left;
font-weight: 600;
color: var(--muted);
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.data-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
color: var(--text);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tr:hover {
background: var(--background);
}
.memory-bar {
display: flex;
align-items: center;
gap: 12px;
}
.memory-bar-track {
flex: 1;
height: 8px;
background: var(--background);
border-radius: 4px;
overflow: hidden;
}
.memory-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.memory-bar-fill.low { background: var(--success); }
.memory-bar-fill.medium { background: var(--warning); }
.memory-bar-fill.high { background: var(--danger); }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.badge.running { background: rgba(46, 204, 113, 0.15); color: var(--success); }
.badge.queued { background: rgba(241, 196, 15, 0.15); color: var(--warning); }
.badge.done { background: rgba(52, 152, 219, 0.15); color: var(--info); }
.badge.error { background: rgba(231, 76, 60, 0.15); color: var(--danger); }
.empty-state {
text-align: center;
padding: 48px 24px;
color: var(--muted);
}
.pill {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: var(--background);
color: var(--muted);
}
.pill.active { background: rgba(46, 204, 113, 0.15); color: var(--success); }
.code {
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 12px;
background: var(--background);
padding: 2px 6px;
border-radius: 4px;
}
.collapsible-header {
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
}
.collapsible-header:hover {
opacity: 0.8;
}
.collapsible-content {
display: none;
padding: 12px 0 0 0;
}
.collapsible-content.open {
display: block;
}
.collapsible-arrow {
transition: transform 0.2s ease;
}
.collapsible-header.open .collapsible-arrow {
transform: rotate(90deg);
}
.nested-table {
margin-left: 24px;
width: calc(100% - 24px);
}
.session-row {
cursor: pointer;
}
.session-row:hover {
background: var(--background);
}
.memory-breakdown {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 16px;
}
.breakdown-item {
background: var(--background);
border-radius: 8px;
padding: 12px;
}
.breakdown-value {
font-size: 20px;
font-weight: bold;
margin-bottom: 4px;
}
.breakdown-label {
font-size: 12px;
color: var(--muted);
}
.refresh-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
animation: pulse 2s infinite;
}
.refresh-indicator.active {
background: var(--success);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.two-col {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 24px;
}
</style>
<script src="/admin.js"></script>
</head>
<body>
<div class="sidebar-overlay"></div>
<div class="app-shell">
<aside class="sidebar">
<div class="brand">
<div class="brand-mark">A</div>
<div>
<div class="brand-title">Admin</div>
<div class="brand-sub">Site management</div>
</div>
<button id="close-sidebar" class="ghost" style="margin-left: auto; display: none;">&times;</button>
</div>
<div class="sidebar-section">
<div class="section-heading">Navigation</div>
<a class="ghost" href="/admin/build">Build models</a>
<a class="ghost" href="/admin/plan">Plan models</a>
<a class="ghost" href="/admin/plans">Plans</a>
<a class="ghost" href="/admin/accounts">Accounts</a>
<a class="ghost" href="/admin/affiliates">Affiliates</a>
<a class="ghost" href="/admin/withdrawals">Withdrawals</a>
<a class="ghost" href="/admin/tracking">Tracking</a>
<a class="ghost active" href="/admin/resources">Resources</a>
<a class="ghost" href="/admin/external-testing">External Testing</a>
<a class="ghost" href="/admin/contact-messages">Contact Messages</a>
<a class="ghost" href="/admin/feature-requests">Feature Requests</a>
<a class="ghost" href="/admin/login">Login</a>
</div>
</aside>
<main class="main">
<div class="admin-shell">
<div class="topbar" style="margin-bottom: 24px;">
<button id="menu-toggle">
<span></span><span></span><span></span>
</button>
<div>
<div class="pill">Admin</div>
<div class="title" style="margin-top: 6px;">Resource Usage</div>
<div class="crumb">Memory and CPU allocation breakdown by session and message.</div>
</div>
<div class="admin-actions">
<button id="auto-refresh" class="ghost">Auto Refresh: ON</button>
<button id="admin-refresh" class="ghost">Refresh</button>
<button id="admin-logout" class="primary">Logout</button>
</div>
</div>
<!-- System Overview -->
<div class="section-card">
<div class="section-title">
<span class="refresh-indicator active" id="refresh-indicator"></span>
System Overview
</div>
<div class="stats-grid" id="system-stats">
<div class="stat-card" style="grid-column: 1 / -1; text-align: center; padding: 32px;">
<div style="color: var(--muted);">Loading system overview...</div>
</div>
</div>
</div>
<!-- Memory Breakdown -->
<div class="section-card">
<div class="section-title">Memory Breakdown</div>
<div class="stats-grid" id="memory-stats">
<div class="stat-card" style="grid-column: 1 / -1; text-align: center; padding: 32px;">
<div style="color: var(--muted);">Loading memory stats...</div>
</div>
</div>
<div class="memory-bar" style="margin-top: 16px;">
<span style="min-width: 80px;">RSS:</span>
<div class="memory-bar-track">
<div class="memory-bar-fill" id="memory-bar-rss"></div>
</div>
<span id="memory-bar-rss-text" class="code">0 MB / 0 MB</span>
</div>
<div class="memory-bar" style="margin-top: 8px;">
<span style="min-width: 80px;">Heap:</span>
<div class="memory-bar-track">
<div class="memory-bar-fill" id="memory-bar-heap"></div>
</div>
<span id="memory-bar-heap-text" class="code">0 MB / 0 MB</span>
</div>
</div>
<!-- CPU & Load -->
<div class="section-card">
<div class="section-title">CPU & System Load</div>
<div class="stats-grid" id="cpu-stats">
<div class="stat-card" style="grid-column: 1 / -1; text-align: center; padding: 32px;">
<div style="color: var(--muted);">Loading CPU stats...</div>
</div>
</div>
<div style="margin-top: 12px; font-size: 13px; color: var(--muted);">
Load Average (1m / 5m / 15m): <span id="load-avg" class="code">- / - / -</span>
</div>
</div>
<!-- Two Column: Sessions & Processes -->
<div class="two-col">
<!-- Active Sessions -->
<div class="section-card">
<div class="section-title">Sessions by Memory Usage</div>
<div id="sessions-table-container">
<table class="data-table" id="sessions-table">
<thead>
<tr>
<th>Session</th>
<th>Messages</th>
<th>Running</th>
<th>Memory</th>
</tr>
</thead>
<tbody id="sessions-tbody">
<tr><td colspan="4" class="empty-state">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Running Processes -->
<div class="section-card">
<div class="section-title">Running Processes</div>
<div id="processes-table-container">
<table class="data-table" id="processes-table">
<thead>
<tr>
<th>Message</th>
<th>Session</th>
<th>Age</th>
</tr>
</thead>
<tbody id="processes-tbody">
<tr><td colspan="3" class="empty-state">No running processes</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- OpenCode & Streams -->
<div class="two-col">
<!-- OpenCode Instances -->
<div class="section-card">
<div class="section-title">OpenCode Process Manager</div>
<div id="opencode-stats">
<div class="breakdown-item" style="grid-column: 1 / -1; text-align: center; padding: 16px;">
<div style="color: var(--muted);">Loading OpenCode stats...</div>
</div>
</div>
</div>
<!-- Active Streams -->
<div class="section-card">
<div class="section-title">Active SSE Streams</div>
<div id="streams-stats">
<div class="breakdown-item" style="grid-column: 1 / -1; text-align: center; padding: 16px;">
<div style="color: var(--muted);">Loading streams stats...</div>
</div>
</div>
<div id="streams-table-container" style="margin-top: 12px;">
<table class="data-table" id="streams-table">
<thead>
<tr>
<th>Message</th>
<th>Session</th>
<th>Streams</th>
</tr>
</thead>
<tbody id="streams-tbody">
<tr><td colspan="3" class="empty-state">No active streams</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Child Processes -->
<div class="section-card">
<div class="section-title">Child Processes</div>
<div id="child-processes-table-container">
<table class="data-table" id="child-processes-table">
<thead>
<tr>
<th>PID</th>
<th>Session</th>
<th>Message</th>
<th>Age</th>
<th>Started</th>
</tr>
</thead>
<tbody id="child-processes-tbody">
<tr><td colspan="5" class="empty-state">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Session Details (Collapsible) -->
<div class="section-card" id="session-details-section" style="display: none;">
<div class="section-title">Session Details</div>
<div id="selected-session-info" style="margin-bottom: 16px; padding: 12px; background: var(--background); border-radius: 8px;">
<!-- Filled by JS -->
</div>
<div class="collapsible-header" onclick="toggleMessages()">
<span>Messages in Session</span>
<span class="collapsible-arrow"></span>
</div>
<div class="collapsible-content" id="messages-content">
<table class="data-table" id="messages-table">
<thead>
<tr>
<th>ID</th>
<th>Role</th>
<th>Status</th>
<th>Model</th>
<th>Content</th>
<th>Reply</th>
<th>Memory</th>
<th>Created</th>
</tr>
</thead>
<tbody id="messages-tbody">
<!-- Filled by JS -->
</tbody>
</table>
</div>
</div>
<!-- Maps & Data Structures -->
<div class="section-card">
<div class="section-title">Internal Data Structures</div>
<div class="stats-grid" id="maps-stats">
<div class="breakdown-item" style="grid-column: 1 / -1; text-align: center; padding: 16px;">
<div style="color: var(--muted);">Loading maps stats...</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
let resourceData = null;
let autoRefresh = true;
let refreshInterval = null;
let loadError = null;
const byId = (id) => document.getElementById(id);
function showError(message) {
loadError = message;
const errorHtml = `
<div class="stat-card" style="grid-column: 1 / -1; text-align: center; padding: 32px; border-color: var(--danger);">
<div style="color: var(--danger); font-weight: 600; margin-bottom: 8px;">Error Loading Data</div>
<div style="color: var(--muted); font-size: 13px;">${escapeHtml(message)}</div>
<div style="margin-top: 16px;">
<button onclick="loadResources()" class="ghost">Retry</button>
</div>
</div>
`;
byId('system-stats').innerHTML = errorHtml;
byId('memory-stats').innerHTML = errorHtml;
byId('cpu-stats').innerHTML = errorHtml;
byId('sessions-tbody').innerHTML = `<tr><td colspan="4" class="empty-state" style="color: var(--danger);">${escapeHtml(message)}</td></tr>`;
byId('processes-tbody').innerHTML = `<tr><td colspan="3" class="empty-state" style="color: var(--danger);">${escapeHtml(message)}</td></tr>`;
byId('child-processes-tbody').innerHTML = `<tr><td colspan="5" class="empty-state" style="color: var(--danger);">${escapeHtml(message)}</td></tr>`;
byId('streams-tbody').innerHTML = `<tr><td colspan="3" class="empty-state" style="color: var(--danger);">${escapeHtml(message)}</td></tr>`;
}
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
function formatNumber(value) {
return (Number(value) || 0).toLocaleString();
}
function formatDuration(ms) {
if (ms < 1000) return ms + 'ms';
if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
if (ms < 3600000) return (ms / 60000).toFixed(1) + 'm';
if (ms < 86400000) return (ms / 3600000).toFixed(1) + 'h';
return (ms / 86400000).toFixed(1) + 'd';
}
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (days > 0) return `${days}d ${hours}h ${mins}m`;
if (hours > 0) return `${hours}h ${mins}m ${secs}s`;
if (mins > 0) return `${mins}m ${secs}s`;
return `${secs}s`;
}
function setText(id, value) {
const el = byId(id);
if (!el) return;
el.textContent = value;
}
function getMemoryBarColor(percent) {
if (percent < 50) return 'low';
if (percent < 80) return 'medium';
return 'high';
}
function updateMemoryBars() {
if (!resourceData) return;
const sys = resourceData.system;
const limits = sys.limits;
const memory = sys.memory;
// RSS bar
const rssPercent = Math.min(100, (memory.raw.rss / limits.memoryBytes) * 100);
byId('memory-bar-rss').style.width = rssPercent + '%';
byId('memory-bar-rss').className = 'memory-bar-fill ' + getMemoryBarColor(rssPercent);
byId('memory-bar-rss-text').textContent = `${memory.rss} / ${limits.memoryMb} (${rssPercent.toFixed(1)}%)`;
// Heap bar
const heapPercent = Math.min(100, (memory.raw.heapUsed / limits.memoryBytes) * 100);
byId('memory-bar-heap').style.width = heapPercent + '%';
byId('memory-bar-heap').className = 'memory-bar-fill ' + getMemoryBarColor(heapPercent);
byId('memory-bar-heap-text').textContent = `${memory.heapUsed} / ${limits.memoryMb} (${heapPercent.toFixed(1)}%)`;
// Load average
const cpu = sys.cpu;
setText('load-avg', `${cpu.loadAvg1m} / ${cpu.loadAvg5m} / ${cpu.loadAvg15m}`);
}
function renderSystemStats() {
if (!resourceData) return;
const sys = resourceData.system;
const totals = resourceData.totals;
byId('system-stats').innerHTML = `
<div class="stat-card">
<div class="stat-label">Total Sessions</div>
<div class="stat-value">${formatNumber(totals.sessions)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Messages</div>
<div class="stat-value">${formatNumber(totals.totalMessages)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Running Messages</div>
<div class="stat-value" style="${totals.runningMessages > 0 ? 'color: var(--success);' : ''}">${formatNumber(totals.runningMessages)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Queued Messages</div>
<div class="stat-value" style="${totals.queuedMessages > 0 ? 'color: var(--warning);' : ''}">${formatNumber(totals.queuedMessages)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Error Messages</div>
<div class="stat-value" style="${totals.errorMessages > 0 ? 'color: var(--danger);' : ''}">${formatNumber(totals.errorMessages)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Process Uptime</div>
<div class="stat-value">${sys.process.uptimeFormatted}</div>
</div>
<div class="stat-card">
<div class="stat-label">OpenCode Instances</div>
<div class="stat-value" style="${resourceData.opencode.runningInstances > 0 ? 'color: var(--success);' : ''}">${formatNumber(resourceData.opencode.runningInstances)}</div>
</div>
`;
byId('memory-stats').innerHTML = `
<div class="stat-card">
<div class="stat-label">RSS Memory</div>
<div class="stat-value">${sys.memory.rss}</div>
</div>
<div class="stat-card">
<div class="stat-label">Heap Used</div>
<div class="stat-value">${sys.memory.heapUsed}</div>
</div>
<div class="stat-card">
<div class="stat-label">Heap Total</div>
<div class="stat-value">${sys.memory.heapTotal}</div>
</div>
<div class="stat-card">
<div class="stat-label">External</div>
<div class="stat-value">${sys.memory.external}</div>
</div>
<div class="stat-card">
<div class="stat-label">Est. Session Memory</div>
<div class="stat-value">${totals.totalEstimatedMemoryMb}</div>
</div>
<div class="stat-card">
<div class="stat-label">Memory Limit</div>
<div class="stat-value">${sys.limits.memoryMb}</div>
</div>
`;
byId('cpu-stats').innerHTML = `
<div class="stat-card">
<div class="stat-label">CPU User</div>
<div class="stat-value">${sys.cpu.userPercent}</div>
</div>
<div class="stat-card">
<div class="stat-label">CPU System</div>
<div class="stat-value">${sys.cpu.systemPercent}</div>
</div>
<div class="stat-card">
<div class="stat-label">Load 1m</div>
<div class="stat-value">${sys.cpu.loadAvg1m}</div>
</div>
<div class="stat-card">
<div class="stat-label">CPU Cores</div>
<div class="stat-value">${sys.limits.cpuCores}</div>
</div>
`;
updateMemoryBars();
}
function renderSessions() {
if (!resourceData) return;
const sessions = resourceData.sessions;
const tbody = byId('sessions-tbody');
if (!sessions || sessions.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No active sessions</td></tr>';
return;
}
tbody.innerHTML = sessions.slice(0, 20).map(session => `
<tr class="session-row" onclick="showSessionDetails('${session.id}')">
<td>
<div style="font-weight: 600;">${escapeHtml(session.title || 'Untitled')}</div>
<div class="pill">${session.cli}</div>
${session.appId ? `<div class="pill" style="margin-left: 4px;">${escapeHtml(session.appId)}</div>` : ''}
<div style="font-size: 11px; color: var(--muted); margin-top: 4px;">${session.id.slice(0, 8)}...</div>
</td>
<td>
<span class="code">${session.messageCount}</span>
${session.errorMessages > 0 ? `<span class="badge error">${session.errorMessages} errors</span>` : ''}
</td>
<td>
${session.runningMessages > 0 ? `<span class="badge running">${session.runningMessages} running</span>` : '-'}
${session.queuedMessages > 0 ? `<span class="badge queued" style="margin-left: 4px;">${session.queuedMessages} queued</span>` : ''}
</td>
<td>
<span class="code">${session.estimatedSessionMemoryKb}</span>
<div style="font-size: 11px; color: var(--muted);">${session.totalMessageMemoryKb} messages</div>
</td>
</tr>
`).join('');
}
function renderProcesses() {
if (!resourceData) return;
const processes = resourceData.runningProcesses;
const tbody = byId('processes-tbody');
if (!processes || processes.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="empty-state">No running processes</td></tr>';
return;
}
tbody.innerHTML = processes.map(proc => `
<tr>
<td>
<span class="code">${proc.messageId.slice(0, 8)}...</span>
<div style="font-size: 11px; color: var(--muted); margin-top: 4px;">${escapeHtml(proc.messagePreview || '').slice(0, 50)}</div>
</td>
<td><span class="code">${proc.sessionId ? proc.sessionId.slice(0, 8) + '...' : '-'}</span></td>
<td><span class="pill active">${proc.age}</span></td>
</tr>
`).join('');
}
function renderChildProcesses() {
if (!resourceData) return;
const childProcs = resourceData.childProcesses;
const tbody = byId('child-processes-tbody');
if (!childProcs || childProcs.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No child processes</td></tr>';
return;
}
tbody.innerHTML = childProcs.map(proc => `
<tr>
<td><span class="code">${proc.pid}</span></td>
<td><span class="code">${proc.sessionId ? proc.sessionId.slice(0, 8) + '...' : '-'}</span></td>
<td><span class="code">${proc.messageId ? proc.messageId.slice(0, 8) + '...' : '-'}</span></td>
<td><span class="pill active">${proc.age}</span></td>
<td style="font-size: 12px; color: var(--muted);">${new Date(proc.startTime).toLocaleString()}</td>
</tr>
`).join('');
}
function renderStreams() {
if (!resourceData) return;
const streams = resourceData.activeStreams;
const tbody = byId('streams-tbody');
// OpenCode stats
const opencode = resourceData.opencode;
byId('opencode-stats').innerHTML = `
<div class="breakdown-item">
<div class="breakdown-value">${opencode.mode}</div>
<div class="breakdown-label">Mode</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value" style="${opencode.isReady ? 'color: var(--success);' : 'color: var(--warning);'}">${opencode.isReady ? 'Ready' : 'Not Ready'}</div>
<div class="breakdown-label">Status</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${opencode.pendingRequests}</div>
<div class="breakdown-label">Pending Requests</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${opencode.sessionWorkspaces}</div>
<div class="breakdown-label">Session Workspaces</div>
</div>
`;
// Streams stats
byId('streams-stats').innerHTML = `
<div class="breakdown-item">
<div class="breakdown-value">${streams.length}</div>
<div class="breakdown-label">Active Stream Groups</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${streams.reduce((sum, s) => sum + s.streamCount, 0)}</div>
<div class="breakdown-label">Total Stream Connections</div>
</div>
`;
if (!streams || streams.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="empty-state">No active streams</td></tr>';
return;
}
tbody.innerHTML = streams.map(stream => `
<tr>
<td><span class="code">${stream.messageId.slice(0, 8)}...</span></td>
<td><span class="code">${stream.sessionId ? stream.sessionId.slice(0, 8) + '...' : '-'}</span></td>
<td><span class="pill">${stream.streamCount} connections</span></td>
</tr>
`).join('');
}
function renderMaps() {
if (!resourceData) return;
const maps = resourceData.maps;
byId('maps-stats').innerHTML = `
<div class="breakdown-item">
<div class="breakdown-value">${formatNumber(maps.sessionQueues)}</div>
<div class="breakdown-label">Session Queues</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${formatNumber(maps.activeStreams)}</div>
<div class="breakdown-label">Active Stream Maps</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${formatNumber(maps.runningProcesses)}</div>
<div class="breakdown-label">Running Process Maps</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${formatNumber(maps.childProcesses)}</div>
<div class="breakdown-label">Child Process Maps</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${formatNumber(maps.oauthStates)}</div>
<div class="breakdown-label">OAuth States</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${formatNumber(maps.loginAttempts)}</div>
<div class="breakdown-label">Login Attempts</div>
</div>
<div class="breakdown-item">
<div class="breakdown-value">${formatNumber(maps.apiRateLimit)}</div>
<div class="breakdown-label">API Rate Limits</div>
</div>
`;
}
function showSessionDetails(sessionId) {
if (!resourceData) return;
const session = resourceData.sessions.find(s => s.id === sessionId);
if (!session) return;
const section = byId('session-details-section');
section.style.display = 'block';
// Session info
byId('selected-session-info').innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
<div>
<div style="font-size: 12px; color: var(--muted);">Session ID</div>
<div class="code">${session.id}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">User ID</div>
<div class="code">${session.userId}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">Title</div>
<div>${escapeHtml(session.title || 'Untitled')}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">App</div>
<div>${session.appId ? escapeHtml(session.appId) : '-'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">Model</div>
<div class="pill">${session.model || session.cli}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">Age</div>
<div>${session.age}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">Created</div>
<div style="font-size: 12px;">${new Date(session.createdAt).toLocaleString()}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">Workspace</div>
<div class="code" style="font-size: 11px;">${session.workspaceDir || '-'}</div>
</div>
<div>
<div style="font-size: 12px; color: var(--muted);">OpenCode Session</div>
<div class="code" style="font-size: 11px;">${session.opencodeSessionId || '-'}</div>
</div>
</div>
`;
// Messages table
const messages = session.messages || [];
const tbody = byId('messages-tbody');
if (messages.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-state">No messages in this session</td></tr>';
} else {
tbody.innerHTML = messages.map(msg => `
<tr>
<td><span class="code">${msg.id.slice(0, 8)}...</span></td>
<td><span class="pill">${msg.role}</span></td>
<td><span class="badge ${msg.status}">${msg.status}</span></td>
<td><span class="code" style="font-size: 11px;">${escapeHtml(msg.model || '')}</span></td>
<td style="max-width: 150px;">
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(msg.content || '')}">
${escapeHtml(msg.content || '-').slice(0, 50)}
</div>
</td>
<td style="max-width: 150px;">
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(msg.reply || '')}">
${escapeHtml(msg.reply || '-').slice(0, 50)}
</div>
</td>
<td><span class="code">${msg.estimatedMemoryKb}</span></td>
<td style="font-size: 11px; color: var(--muted);">${new Date(msg.createdAt).toLocaleString()}</td>
</tr>
`).join('');
}
// Scroll to section
section.scrollIntoView({ behavior: 'smooth' });
}
function toggleMessages() {
const content = byId('messages-content');
const header = content.previousElementSibling;
content.classList.toggle('open');
header.classList.toggle('open');
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
async function loadResources() {
try {
// Ensure credentials are included so admin session cookie is sent
const response = await fetch('/api/admin/resources', { credentials: 'same-origin' });
if (!response.ok) {
if (response.status === 401) {
window.location.href = '/admin/login';
return;
}
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
resourceData = data;
loadError = null;
renderSystemStats();
renderSessions();
renderProcesses();
renderChildProcesses();
renderStreams();
renderMaps();
// Update timestamp
if (data.timestamp) {
setText('refresh-indicator', '');
}
} catch (error) {
console.error('Error loading resources:', error);
const errorMessage = error.message || 'Failed to load resources';
showError(errorMessage);
}
}
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
byId('auto-refresh').textContent = autoRefresh ? 'Auto Refresh: ON' : 'Auto Refresh: OFF';
byId('auto-refresh').classList.toggle('active', autoRefresh);
if (autoRefresh) {
refreshInterval = setInterval(loadResources, 5000);
} else {
clearInterval(refreshInterval);
}
}
// Event listeners
byId('admin-refresh').addEventListener('click', () => {
loadResources();
});
byId('auto-refresh').addEventListener('click', toggleAutoRefresh);
byId('admin-logout').addEventListener('click', async () => {
try {
await fetch('/api/admin/logout', { method: 'POST', credentials: 'same-origin' });
window.location.href = '/admin/login';
} catch (error) {
console.error('Logout failed:', error);
}
});
// Initial load
loadResources();
refreshInterval = setInterval(loadResources, 5000);
</script>
</body>
</html>