Restore to commit 74e578279624c6045ca440a3459ebfa1f8d54191
This commit is contained in:
962
chat/public/admin-resources.html
Normal file
962
chat/public/admin-resources.html
Normal file
@@ -0,0 +1,962 @@
|
||||
<!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;">×</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/contact-messages">Contact Messages</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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user