aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/compute/compute.html
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenserver/frontend/html/compute/compute.html')
-rw-r--r--src/zenserver/frontend/html/compute/compute.html925
1 files changed, 0 insertions, 925 deletions
diff --git a/src/zenserver/frontend/html/compute/compute.html b/src/zenserver/frontend/html/compute/compute.html
deleted file mode 100644
index c07bbb692..000000000
--- a/src/zenserver/frontend/html/compute/compute.html
+++ /dev/null
@@ -1,925 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Zen Compute Dashboard</title>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
- <link rel="stylesheet" type="text/css" href="../zen.css" />
- <script src="../util/sanitize.js"></script>
- <script src="../theme.js"></script>
- <script src="../banner.js" defer></script>
- <script src="../nav.js" defer></script>
- <style>
- .grid {
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- }
-
- .chart-container {
- position: relative;
- height: 300px;
- margin-top: 20px;
- }
-
- .stats-row {
- display: flex;
- justify-content: space-between;
- margin-bottom: 12px;
- padding: 8px 0;
- border-bottom: 1px solid var(--theme_border_subtle);
- }
-
- .stats-row:last-child {
- border-bottom: none;
- margin-bottom: 0;
- }
-
- .stats-label {
- color: var(--theme_g1);
- font-size: 13px;
- }
-
- .stats-value {
- color: var(--theme_bright);
- font-weight: 600;
- font-size: 13px;
- }
-
- .rate-stats {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 16px;
- margin-top: 16px;
- }
-
- .rate-item {
- text-align: center;
- }
-
- .rate-value {
- font-size: 20px;
- font-weight: 600;
- color: var(--theme_p0);
- }
-
- .rate-label {
- font-size: 11px;
- color: var(--theme_g1);
- margin-top: 4px;
- text-transform: uppercase;
- }
-
- .worker-row {
- cursor: pointer;
- transition: background 0.15s;
- }
-
- .worker-row:hover {
- background: var(--theme_p4);
- }
-
- .worker-row.selected {
- background: var(--theme_p3);
- }
-
- .worker-detail {
- margin-top: 20px;
- border-top: 1px solid var(--theme_g2);
- padding-top: 16px;
- }
-
- .worker-detail-title {
- font-size: 15px;
- font-weight: 600;
- color: var(--theme_bright);
- margin-bottom: 12px;
- }
-
- .detail-section {
- margin-bottom: 16px;
- }
-
- .detail-section-label {
- font-size: 11px;
- font-weight: 600;
- color: var(--theme_g1);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 6px;
- }
-
- .detail-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 12px;
- }
-
- .detail-table td {
- padding: 4px 8px;
- color: var(--theme_g0);
- border-bottom: 1px solid var(--theme_border_subtle);
- vertical-align: top;
- }
-
- .detail-table td:first-child {
- color: var(--theme_g1);
- width: 40%;
- font-family: monospace;
- }
-
- .detail-table tr:last-child td {
- border-bottom: none;
- }
-
- .detail-mono {
- font-family: monospace;
- font-size: 11px;
- color: var(--theme_g1);
- }
-
- .detail-tag {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 4px;
- background: var(--theme_border_subtle);
- color: var(--theme_g0);
- font-size: 11px;
- margin: 2px 4px 2px 0;
- }
- </style>
-</head>
-<body>
- <div class="container" style="max-width: 1400px; margin: 0 auto;">
- <zen-banner cluster-status="nominal" load="0" tagline="Node Overview" logo-src="../favicon.ico"></zen-banner>
- <zen-nav>
- <a href="/dashboard/">Home</a>
- <a href="compute.html">Node</a>
- <a href="orchestrator.html">Orchestrator</a>
- </zen-nav>
- <div class="timestamp">Last updated: <span id="last-update">Never</span></div>
-
- <div id="error-container"></div>
-
- <!-- Action Queue Stats -->
- <div class="section-title">Action Queue</div>
- <div class="grid">
- <div class="card">
- <div class="card-title">Pending Actions</div>
- <div class="metric-value" id="actions-pending">-</div>
- <div class="metric-label">Waiting to be scheduled</div>
- </div>
- <div class="card">
- <div class="card-title">Running Actions</div>
- <div class="metric-value" id="actions-running">-</div>
- <div class="metric-label">Currently executing</div>
- </div>
- <div class="card">
- <div class="card-title">Completed Actions</div>
- <div class="metric-value" id="actions-complete">-</div>
- <div class="metric-label">Results available</div>
- </div>
- </div>
-
- <!-- Action Queue Chart -->
- <div class="card" style="margin-bottom: 30px;">
- <div class="card-title">Action Queue History</div>
- <div class="chart-container">
- <canvas id="queue-chart"></canvas>
- </div>
- </div>
-
- <!-- Performance Metrics -->
- <div class="section-title">Performance Metrics</div>
- <div class="card" style="margin-bottom: 30px;">
- <div class="card-title">Completion Rate</div>
- <div class="rate-stats">
- <div class="rate-item">
- <div class="rate-value" id="rate-1">-</div>
- <div class="rate-label">1 min rate</div>
- </div>
- <div class="rate-item">
- <div class="rate-value" id="rate-5">-</div>
- <div class="rate-label">5 min rate</div>
- </div>
- <div class="rate-item">
- <div class="rate-value" id="rate-15">-</div>
- <div class="rate-label">15 min rate</div>
- </div>
- </div>
- <div style="margin-top: 20px;">
- <div class="stats-row">
- <span class="stats-label">Total Retired</span>
- <span class="stats-value" id="retired-count">-</span>
- </div>
- <div class="stats-row">
- <span class="stats-label">Mean Rate</span>
- <span class="stats-value" id="rate-mean">-</span>
- </div>
- </div>
- </div>
-
- <!-- Workers -->
- <div class="section-title">Workers</div>
- <div class="card" style="margin-bottom: 30px;">
- <div class="card-title">Worker Status</div>
- <div class="stats-row">
- <span class="stats-label">Registered Workers</span>
- <span class="stats-value" id="worker-count">-</span>
- </div>
- <div id="worker-table-container" style="margin-top: 16px; display: none;">
- <table id="worker-table">
- <thead>
- <tr>
- <th>Name</th>
- <th>Platform</th>
- <th style="text-align: right;">Cores</th>
- <th style="text-align: right;">Timeout</th>
- <th style="text-align: right;">Functions</th>
- <th>Worker ID</th>
- </tr>
- </thead>
- <tbody id="worker-table-body"></tbody>
- </table>
- <div id="worker-detail" class="worker-detail" style="display: none;"></div>
- </div>
- </div>
-
- <!-- Queues -->
- <div class="section-title">Queues</div>
- <div class="card" style="margin-bottom: 30px;">
- <div class="card-title">Queue Status</div>
- <div id="queue-list-empty" class="empty-state" style="text-align: left;">No queues.</div>
- <div id="queue-list-container" style="display: none;">
- <table id="queue-list-table">
- <thead>
- <tr>
- <th style="text-align: right; width: 60px;">ID</th>
- <th style="text-align: center; width: 80px;">Status</th>
- <th style="text-align: right;">Active</th>
- <th style="text-align: right;">Completed</th>
- <th style="text-align: right;">Failed</th>
- <th style="text-align: right;">Abandoned</th>
- <th style="text-align: right;">Cancelled</th>
- <th>Token</th>
- </tr>
- </thead>
- <tbody id="queue-list-body"></tbody>
- </table>
- </div>
- </div>
-
- <!-- Action History -->
- <div class="section-title">Recent Actions</div>
- <div class="card" style="margin-bottom: 30px;">
- <div class="card-title">Action History</div>
- <div id="action-history-empty" class="empty-state" style="text-align: left;">No actions recorded yet.</div>
- <div id="action-history-container" style="display: none;">
- <table id="action-history-table">
- <thead>
- <tr>
- <th style="text-align: right; width: 60px;">LSN</th>
- <th style="text-align: right; width: 60px;">Queue</th>
- <th style="text-align: center; width: 70px;">Status</th>
- <th>Function</th>
- <th style="text-align: right; width: 80px;">Started</th>
- <th style="text-align: right; width: 80px;">Finished</th>
- <th style="text-align: right; width: 80px;">Duration</th>
- <th>Worker ID</th>
- <th>Action ID</th>
- </tr>
- </thead>
- <tbody id="action-history-body"></tbody>
- </table>
- </div>
- </div>
-
- <!-- System Resources -->
- <div class="section-title">System Resources</div>
- <div class="grid">
- <div class="card">
- <div class="card-title">CPU Usage</div>
- <div class="metric-value" id="cpu-usage">-</div>
- <div class="metric-label">Percent</div>
- <div class="progress-bar">
- <div class="progress-fill" id="cpu-progress" style="width: 0%"></div>
- </div>
- <div style="position: relative; height: 60px; margin-top: 12px;">
- <canvas id="cpu-chart"></canvas>
- </div>
- <div style="margin-top: 12px;">
- <div class="stats-row">
- <span class="stats-label">Packages</span>
- <span class="stats-value" id="cpu-packages">-</span>
- </div>
- <div class="stats-row">
- <span class="stats-label">Physical Cores</span>
- <span class="stats-value" id="cpu-cores">-</span>
- </div>
- <div class="stats-row">
- <span class="stats-label">Logical Processors</span>
- <span class="stats-value" id="cpu-lp">-</span>
- </div>
- </div>
- </div>
- <div class="card">
- <div class="card-title">Memory</div>
- <div class="stats-row">
- <span class="stats-label">Used</span>
- <span class="stats-value" id="memory-used">-</span>
- </div>
- <div class="stats-row">
- <span class="stats-label">Total</span>
- <span class="stats-value" id="memory-total">-</span>
- </div>
- <div class="progress-bar">
- <div class="progress-fill" id="memory-progress" style="width: 0%"></div>
- </div>
- </div>
- <div class="card">
- <div class="card-title">Disk</div>
- <div class="stats-row">
- <span class="stats-label">Used</span>
- <span class="stats-value" id="disk-used">-</span>
- </div>
- <div class="stats-row">
- <span class="stats-label">Total</span>
- <span class="stats-value" id="disk-total">-</span>
- </div>
- <div class="progress-bar">
- <div class="progress-fill" id="disk-progress" style="width: 0%"></div>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- // Configuration
- const BASE_URL = window.location.origin;
- const REFRESH_INTERVAL = 2000; // 2 seconds
- const MAX_HISTORY_POINTS = 60; // Show last 2 minutes
-
- // Data storage
- const history = {
- timestamps: [],
- pending: [],
- running: [],
- completed: [],
- cpu: []
- };
-
- // CPU sparkline chart
- const cpuCtx = document.getElementById('cpu-chart').getContext('2d');
- const cpuChart = new Chart(cpuCtx, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- data: [],
- borderColor: '#58a6ff',
- backgroundColor: 'rgba(88, 166, 255, 0.15)',
- borderWidth: 1.5,
- tension: 0.4,
- fill: true,
- pointRadius: 0
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- animation: false,
- plugins: { legend: { display: false }, tooltip: { enabled: false } },
- scales: {
- x: { display: false },
- y: { display: false, min: 0, max: 100 }
- }
- }
- });
-
- // Queue chart setup
- const ctx = document.getElementById('queue-chart').getContext('2d');
- const chart = new Chart(ctx, {
- type: 'line',
- data: {
- labels: [],
- datasets: [
- {
- label: 'Pending',
- data: [],
- borderColor: '#f0883e',
- backgroundColor: 'rgba(240, 136, 62, 0.1)',
- tension: 0.4,
- fill: true
- },
- {
- label: 'Running',
- data: [],
- borderColor: '#58a6ff',
- backgroundColor: 'rgba(88, 166, 255, 0.1)',
- tension: 0.4,
- fill: true
- },
- {
- label: 'Completed',
- data: [],
- borderColor: '#3fb950',
- backgroundColor: 'rgba(63, 185, 80, 0.1)',
- tension: 0.4,
- fill: true
- }
- ]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: {
- display: true,
- labels: {
- color: '#8b949e'
- }
- }
- },
- scales: {
- x: {
- display: false
- },
- y: {
- beginAtZero: true,
- ticks: {
- color: '#8b949e'
- },
- grid: {
- color: '#21262d'
- }
- }
- }
- }
- });
-
- // Helper functions
-
- function formatBytes(bytes) {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- }
-
- function formatRate(rate) {
- return rate.toFixed(2) + '/s';
- }
-
- function showError(message) {
- const container = document.getElementById('error-container');
- container.innerHTML = `<div class="error">Error: ${escapeHtml(message)}</div>`;
- }
-
- function clearError() {
- document.getElementById('error-container').innerHTML = '';
- }
-
- function updateTimestamp() {
- const now = new Date();
- document.getElementById('last-update').textContent = now.toLocaleTimeString();
- }
-
- // Fetch functions
- async function fetchJSON(endpoint) {
- const response = await fetch(`${BASE_URL}${endpoint}`, {
- headers: {
- 'Accept': 'application/json'
- }
- });
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
- return await response.json();
- }
-
- async function fetchHealth() {
- try {
- const response = await fetch(`${BASE_URL}/compute/ready`);
- const isHealthy = response.status === 200;
-
- const banner = document.querySelector('zen-banner');
-
- if (isHealthy) {
- banner.setAttribute('cluster-status', 'nominal');
- banner.setAttribute('load', '0');
- } else {
- banner.setAttribute('cluster-status', 'degraded');
- banner.setAttribute('load', '0');
- }
-
- return isHealthy;
- } catch (error) {
- const banner = document.querySelector('zen-banner');
- banner.setAttribute('cluster-status', 'degraded');
- banner.setAttribute('load', '0');
- throw error;
- }
- }
-
- async function fetchStats() {
- const data = await fetchJSON('/stats/compute');
-
- // Update action counts
- document.getElementById('actions-pending').textContent = data.actions_pending || 0;
- document.getElementById('actions-running').textContent = data.actions_submitted || 0;
- document.getElementById('actions-complete').textContent = data.actions_complete || 0;
-
- // Update completion rates
- if (data.actions_retired) {
- document.getElementById('rate-1').textContent = formatRate(data.actions_retired.rate_1 || 0);
- document.getElementById('rate-5').textContent = formatRate(data.actions_retired.rate_5 || 0);
- document.getElementById('rate-15').textContent = formatRate(data.actions_retired.rate_15 || 0);
- document.getElementById('retired-count').textContent = data.actions_retired.count || 0;
- document.getElementById('rate-mean').textContent = formatRate(data.actions_retired.rate_mean || 0);
- }
-
- // Update chart
- const now = new Date().toLocaleTimeString();
- history.timestamps.push(now);
- history.pending.push(data.actions_pending || 0);
- history.running.push(data.actions_submitted || 0);
- history.completed.push(data.actions_complete || 0);
-
- // Keep only last N points
- if (history.timestamps.length > MAX_HISTORY_POINTS) {
- history.timestamps.shift();
- history.pending.shift();
- history.running.shift();
- history.completed.shift();
- }
-
- chart.data.labels = history.timestamps;
- chart.data.datasets[0].data = history.pending;
- chart.data.datasets[1].data = history.running;
- chart.data.datasets[2].data = history.completed;
- chart.update('none');
- }
-
- async function fetchSysInfo() {
- const data = await fetchJSON('/compute/sysinfo');
-
- // Update CPU
- const cpuUsage = data.cpu_usage || 0;
- document.getElementById('cpu-usage').textContent = cpuUsage.toFixed(1) + '%';
- document.getElementById('cpu-progress').style.width = cpuUsage + '%';
-
- const banner = document.querySelector('zen-banner');
- banner.setAttribute('load', cpuUsage.toFixed(1));
-
- history.cpu.push(cpuUsage);
- if (history.cpu.length > MAX_HISTORY_POINTS) history.cpu.shift();
- cpuChart.data.labels = history.cpu.map(() => '');
- cpuChart.data.datasets[0].data = history.cpu;
- cpuChart.update('none');
-
- document.getElementById('cpu-packages').textContent = data.cpu_count ?? '-';
- document.getElementById('cpu-cores').textContent = data.core_count ?? '-';
- document.getElementById('cpu-lp').textContent = data.lp_count ?? '-';
-
- // Update Memory
- const memUsed = data.memory_used || 0;
- const memTotal = data.memory_total || 1;
- const memPercent = (memUsed / memTotal) * 100;
- document.getElementById('memory-used').textContent = formatBytes(memUsed);
- document.getElementById('memory-total').textContent = formatBytes(memTotal);
- document.getElementById('memory-progress').style.width = memPercent + '%';
-
- // Update Disk
- const diskUsed = data.disk_used || 0;
- const diskTotal = data.disk_total || 1;
- const diskPercent = (diskUsed / diskTotal) * 100;
- document.getElementById('disk-used').textContent = formatBytes(diskUsed);
- document.getElementById('disk-total').textContent = formatBytes(diskTotal);
- document.getElementById('disk-progress').style.width = diskPercent + '%';
- }
-
- // Persists the selected worker ID across refreshes
- let selectedWorkerId = null;
-
- function renderWorkerDetail(id, desc) {
- const panel = document.getElementById('worker-detail');
-
- if (!desc) {
- panel.style.display = 'none';
- return;
- }
-
- function field(label, value) {
- return `<tr><td>${label}</td><td>${value ?? '-'}</td></tr>`;
- }
-
- function monoField(label, value) {
- return `<tr><td>${label}</td><td class="detail-mono">${value ?? '-'}</td></tr>`;
- }
-
- // Functions
- const functions = desc.functions || [];
- const functionsHtml = functions.length === 0 ? '<span style="color:var(--theme_faint);font-size:12px;">none</span>' :
- `<table class="detail-table">${functions.map(f =>
- `<tr><td>${escapeHtml(f.name || '-')}</td><td class="detail-mono">${escapeHtml(f.version || '-')}</td></tr>`
- ).join('')}</table>`;
-
- // Executables
- const executables = desc.executables || [];
- const totalExecSize = executables.reduce((sum, e) => sum + (e.size || 0), 0);
- const execHtml = executables.length === 0 ? '<span style="color:var(--theme_faint);font-size:12px;">none</span>' :
- `<table class="detail-table">
- <tr style="font-size:11px;">
- <td style="color:var(--theme_faint);padding-bottom:4px;">Path</td>
- <td style="color:var(--theme_faint);padding-bottom:4px;">Hash</td>
- <td style="color:var(--theme_faint);padding-bottom:4px;text-align:right;">Size</td>
- </tr>
- ${executables.map(e =>
- `<tr>
- <td>${escapeHtml(e.name || '-')}</td>
- <td class="detail-mono">${escapeHtml(e.hash || '-')}</td>
- <td style="text-align:right;white-space:nowrap;">${e.size != null ? formatBytes(e.size) : '-'}</td>
- </tr>`
- ).join('')}
- <tr style="border-top:1px solid var(--theme_g2);">
- <td style="color:var(--theme_g1);padding-top:6px;">Total</td>
- <td></td>
- <td style="text-align:right;white-space:nowrap;padding-top:6px;color:var(--theme_bright);font-weight:600;">${formatBytes(totalExecSize)}</td>
- </tr>
- </table>`;
-
- // Files
- const files = desc.files || [];
- const filesHtml = files.length === 0 ? '<span style="color:var(--theme_faint);font-size:12px;">none</span>' :
- `<table class="detail-table">${files.map(f =>
- `<tr><td>${escapeHtml(f.name || f)}</td><td class="detail-mono">${escapeHtml(f.hash || '')}</td></tr>`
- ).join('')}</table>`;
-
- // Dirs
- const dirs = desc.dirs || [];
- const dirsHtml = dirs.length === 0 ? '<span style="color:var(--theme_faint);font-size:12px;">none</span>' :
- dirs.map(d => `<span class="detail-tag">${escapeHtml(d)}</span>`).join('');
-
- // Environment
- const env = desc.environment || [];
- const envHtml = env.length === 0 ? '<span style="color:var(--theme_faint);font-size:12px;">none</span>' :
- env.map(e => `<span class="detail-tag">${escapeHtml(e)}</span>`).join('');
-
- panel.innerHTML = `
- <div class="worker-detail-title">${escapeHtml(desc.name || id)}</div>
- <div class="detail-section">
- <table class="detail-table">
- ${field('Worker ID', `<span class="detail-mono">${escapeHtml(id)}</span>`)}
- ${field('Path', escapeHtml(desc.path || '-'))}
- ${field('Platform', escapeHtml(desc.host || '-'))}
- ${monoField('Build System', desc.buildsystem_version)}
- ${field('Cores', desc.cores)}
- ${field('Timeout', desc.timeout != null ? desc.timeout + 's' : null)}
- </table>
- </div>
- <div class="detail-section">
- <div class="detail-section-label">Functions</div>
- ${functionsHtml}
- </div>
- <div class="detail-section">
- <div class="detail-section-label">Executables</div>
- ${execHtml}
- </div>
- <div class="detail-section">
- <div class="detail-section-label">Files</div>
- ${filesHtml}
- </div>
- <div class="detail-section">
- <div class="detail-section-label">Directories</div>
- ${dirsHtml}
- </div>
- <div class="detail-section">
- <div class="detail-section-label">Environment</div>
- ${envHtml}
- </div>
- `;
- panel.style.display = 'block';
- }
-
- async function fetchWorkers() {
- const data = await fetchJSON('/compute/workers');
- const workerIds = data.workers || [];
-
- document.getElementById('worker-count').textContent = workerIds.length;
-
- const container = document.getElementById('worker-table-container');
- const tbody = document.getElementById('worker-table-body');
-
- if (workerIds.length === 0) {
- container.style.display = 'none';
- selectedWorkerId = null;
- return;
- }
-
- const descriptors = await Promise.all(
- workerIds.map(id => fetchJSON(`/compute/workers/${id}`).catch(() => null))
- );
-
- // Build a map for quick lookup by ID
- const descriptorMap = {};
- workerIds.forEach((id, i) => { descriptorMap[id] = descriptors[i]; });
-
- tbody.innerHTML = '';
- descriptors.forEach((desc, i) => {
- const id = workerIds[i];
- const name = desc ? (desc.name || '-') : '-';
- const host = desc ? (desc.host || '-') : '-';
- const cores = desc ? (desc.cores != null ? desc.cores : '-') : '-';
- const timeout = desc ? (desc.timeout != null ? desc.timeout + 's' : '-') : '-';
- const functions = desc ? (desc.functions ? desc.functions.length : 0) : '-';
-
- const tr = document.createElement('tr');
- tr.className = 'worker-row' + (id === selectedWorkerId ? ' selected' : '');
- tr.dataset.workerId = id;
- tr.innerHTML = `
- <td style="color: var(--theme_bright);">${escapeHtml(name)}</td>
- <td>${escapeHtml(host)}</td>
- <td style="text-align: right;">${escapeHtml(String(cores))}</td>
- <td style="text-align: right;">${escapeHtml(String(timeout))}</td>
- <td style="text-align: right;">${escapeHtml(String(functions))}</td>
- <td style="color: var(--theme_g1); font-family: monospace; font-size: 11px;">${escapeHtml(id)}</td>
- `;
- tr.addEventListener('click', () => {
- document.querySelectorAll('.worker-row').forEach(r => r.classList.remove('selected'));
- if (selectedWorkerId === id) {
- // Toggle off
- selectedWorkerId = null;
- document.getElementById('worker-detail').style.display = 'none';
- } else {
- selectedWorkerId = id;
- tr.classList.add('selected');
- renderWorkerDetail(id, descriptorMap[id]);
- }
- });
- tbody.appendChild(tr);
- });
-
- // Re-render detail if selected worker is still present
- if (selectedWorkerId && descriptorMap[selectedWorkerId]) {
- renderWorkerDetail(selectedWorkerId, descriptorMap[selectedWorkerId]);
- } else if (selectedWorkerId && !descriptorMap[selectedWorkerId]) {
- selectedWorkerId = null;
- document.getElementById('worker-detail').style.display = 'none';
- }
-
- container.style.display = 'block';
- }
-
- // Windows FILETIME: 100ns ticks since 1601-01-01. Convert to JS Date.
- const FILETIME_EPOCH_OFFSET_MS = 11644473600000n;
- function filetimeToDate(ticks) {
- if (!ticks) return null;
- const ms = BigInt(ticks) / 10000n - FILETIME_EPOCH_OFFSET_MS;
- return new Date(Number(ms));
- }
-
- function formatTime(date) {
- if (!date) return '-';
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
- }
-
- function formatDuration(startDate, endDate) {
- if (!startDate || !endDate) return '-';
- const ms = endDate - startDate;
- if (ms < 0) return '-';
- if (ms < 1000) return ms + ' ms';
- if (ms < 60000) return (ms / 1000).toFixed(2) + ' s';
- const m = Math.floor(ms / 60000);
- const s = ((ms % 60000) / 1000).toFixed(0).padStart(2, '0');
- return `${m}m ${s}s`;
- }
-
- async function fetchQueues() {
- const data = await fetchJSON('/compute/queues');
- const queues = data.queues || [];
-
- const empty = document.getElementById('queue-list-empty');
- const container = document.getElementById('queue-list-container');
- const tbody = document.getElementById('queue-list-body');
-
- if (queues.length === 0) {
- empty.style.display = '';
- container.style.display = 'none';
- return;
- }
-
- empty.style.display = 'none';
- tbody.innerHTML = '';
-
- for (const q of queues) {
- const id = q.queue_id ?? '-';
- const badge = q.state === 'cancelled'
- ? '<span class="status-badge failure">cancelled</span>'
- : q.state === 'draining'
- ? '<span class="status-badge" style="background:color-mix(in srgb, var(--theme_warn) 15%, transparent);color:var(--theme_warn);">draining</span>'
- : q.is_complete
- ? '<span class="status-badge success">complete</span>'
- : '<span class="status-badge" style="background:color-mix(in srgb, var(--theme_p0) 15%, transparent);color:var(--theme_p0);">active</span>';
- const token = q.queue_token
- ? `<span class="detail-mono">${escapeHtml(q.queue_token)}</span>`
- : '<span style="color:var(--theme_faint);">-</span>';
-
- const tr = document.createElement('tr');
- tr.innerHTML = `
- <td style="text-align: right; font-family: monospace; color: var(--theme_bright);">${escapeHtml(String(id))}</td>
- <td style="text-align: center;">${badge}</td>
- <td style="text-align: right;">${q.active_count ?? 0}</td>
- <td style="text-align: right; color: var(--theme_ok);">${q.completed_count ?? 0}</td>
- <td style="text-align: right; color: var(--theme_fail);">${q.failed_count ?? 0}</td>
- <td style="text-align: right; color: var(--theme_warn);">${q.abandoned_count ?? 0}</td>
- <td style="text-align: right; color: var(--theme_warn);">${q.cancelled_count ?? 0}</td>
- <td>${token}</td>
- `;
- tbody.appendChild(tr);
- }
-
- container.style.display = 'block';
- }
-
- async function fetchActionHistory() {
- const data = await fetchJSON('/compute/jobs/history?limit=50');
- const entries = data.history || [];
-
- const empty = document.getElementById('action-history-empty');
- const container = document.getElementById('action-history-container');
- const tbody = document.getElementById('action-history-body');
-
- if (entries.length === 0) {
- empty.style.display = '';
- container.style.display = 'none';
- return;
- }
-
- empty.style.display = 'none';
- tbody.innerHTML = '';
-
- // Entries arrive oldest-first; reverse to show newest at top
- for (const entry of [...entries].reverse()) {
- const lsn = entry.lsn ?? '-';
- const succeeded = entry.succeeded;
- const badge = succeeded == null
- ? '<span class="status-badge" style="background:var(--theme_border_subtle);color:var(--theme_g1);">unknown</span>'
- : succeeded
- ? '<span class="status-badge success">ok</span>'
- : '<span class="status-badge failure">failed</span>';
- const desc = entry.actionDescriptor || {};
- const fn = desc.Function || '-';
- const workerId = entry.workerId || '-';
- const actionId = entry.actionId || '-';
-
- const startDate = filetimeToDate(entry.time_Running);
- const endDate = filetimeToDate(entry.time_Completed ?? entry.time_Failed);
-
- const queueId = entry.queueId || 0;
- const queueCell = queueId
- ? `<a href="/compute/queues/${queueId}" style="color: var(--theme_ln); text-decoration: none; font-family: monospace;">${escapeHtml(String(queueId))}</a>`
- : '<span style="color: var(--theme_faint);">-</span>';
-
- const tr = document.createElement('tr');
- tr.innerHTML = `
- <td style="text-align: right; font-family: monospace; color: var(--theme_g1);">${escapeHtml(String(lsn))}</td>
- <td style="text-align: right;">${queueCell}</td>
- <td style="text-align: center;">${badge}</td>
- <td style="color: var(--theme_bright);">${escapeHtml(fn)}</td>
- <td style="text-align: right; font-size: 12px; white-space: nowrap; color: var(--theme_g1);">${formatTime(startDate)}</td>
- <td style="text-align: right; font-size: 12px; white-space: nowrap; color: var(--theme_g1);">${formatTime(endDate)}</td>
- <td style="text-align: right; font-size: 12px; white-space: nowrap;">${formatDuration(startDate, endDate)}</td>
- <td style="font-family: monospace; font-size: 11px; color: var(--theme_g1);">${escapeHtml(workerId)}</td>
- <td style="font-family: monospace; font-size: 11px; color: var(--theme_g1);">${escapeHtml(actionId)}</td>
- `;
- tbody.appendChild(tr);
- }
-
- container.style.display = 'block';
- }
-
- async function updateDashboard() {
- try {
- await Promise.all([
- fetchHealth(),
- fetchStats(),
- fetchSysInfo(),
- fetchWorkers(),
- fetchQueues(),
- fetchActionHistory()
- ]);
-
- clearError();
- updateTimestamp();
- } catch (error) {
- console.error('Error updating dashboard:', error);
- showError(error.message);
- }
- }
-
- // Start updating
- updateDashboard();
- setInterval(updateDashboard, REFRESH_INTERVAL);
- </script>
-</body>
-</html>