diff options
| author | zousar <[email protected]> | 2026-02-18 23:19:14 -0700 |
|---|---|---|
| committer | zousar <[email protected]> | 2026-02-18 23:19:14 -0700 |
| commit | 2ba28acaf034722452f82cfb07afc0a4bb90eeab (patch) | |
| tree | c00dea385597180673be6e02aca6c07d9ef6ec00 /src/zenserver/frontend/html | |
| parent | updatefrontend (diff) | |
| parent | structured compute basics (#714) (diff) | |
| download | zen-2ba28acaf034722452f82cfb07afc0a4bb90eeab.tar.xz zen-2ba28acaf034722452f82cfb07afc0a4bb90eeab.zip | |
Merge branch 'main' into zs/web-ui-improvements
Diffstat (limited to 'src/zenserver/frontend/html')
| -rw-r--r-- | src/zenserver/frontend/html/compute.html | 991 |
1 files changed, 991 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/compute.html b/src/zenserver/frontend/html/compute.html new file mode 100644 index 000000000..668189fe5 --- /dev/null +++ b/src/zenserver/frontend/html/compute.html @@ -0,0 +1,991 @@ +<!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> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0d1117; + color: #c9d1d9; + padding: 20px; + } + + .container { + max-width: 1400px; + margin: 0 auto; + } + + h1 { + font-size: 32px; + font-weight: 600; + margin-bottom: 10px; + color: #f0f6fc; + } + + .header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + + .health-indicator { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + padding: 8px 16px; + border-radius: 6px; + background: #161b22; + border: 1px solid #30363d; + } + + .health-indicator .status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #6e7681; + } + + .health-indicator.healthy .status-dot { + background: #3fb950; + } + + .health-indicator.unhealthy .status-dot { + background: #f85149; + } + + .grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 30px; + } + + .card { + background: #161b22; + border: 1px solid #30363d; + border-radius: 6px; + padding: 20px; + } + + .card-title { + font-size: 14px; + font-weight: 600; + color: #8b949e; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .metric-value { + font-size: 36px; + font-weight: 600; + color: #f0f6fc; + line-height: 1; + } + + .metric-label { + font-size: 12px; + color: #8b949e; + margin-top: 4px; + } + + .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 #21262d; + } + + .stats-row:last-child { + border-bottom: none; + margin-bottom: 0; + } + + .stats-label { + color: #8b949e; + font-size: 13px; + } + + .stats-value { + color: #f0f6fc; + 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: #58a6ff; + } + + .rate-label { + font-size: 11px; + color: #8b949e; + margin-top: 4px; + text-transform: uppercase; + } + + .progress-bar { + width: 100%; + height: 8px; + background: #21262d; + border-radius: 4px; + overflow: hidden; + margin-top: 8px; + } + + .progress-fill { + height: 100%; + background: #58a6ff; + transition: width 0.3s ease; + } + + .timestamp { + font-size: 12px; + color: #6e7681; + text-align: right; + margin-top: 30px; + } + + .error { + color: #f85149; + padding: 12px; + background: #1c1c1c; + border-radius: 6px; + margin: 20px 0; + font-size: 13px; + } + + .section-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 20px; + color: #f0f6fc; + } + + .worker-row { + cursor: pointer; + transition: background 0.15s; + } + + .worker-row:hover { + background: #1c2128; + } + + .worker-row.selected { + background: #1f2d3d; + } + + .worker-detail { + margin-top: 20px; + border-top: 1px solid #30363d; + padding-top: 16px; + } + + .worker-detail-title { + font-size: 15px; + font-weight: 600; + color: #f0f6fc; + margin-bottom: 12px; + } + + .detail-section { + margin-bottom: 16px; + } + + .detail-section-label { + font-size: 11px; + font-weight: 600; + color: #8b949e; + 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: #c9d1d9; + border-bottom: 1px solid #21262d; + vertical-align: top; + } + + .detail-table td:first-child { + color: #8b949e; + width: 40%; + font-family: monospace; + } + + .detail-table tr:last-child td { + border-bottom: none; + } + + .detail-mono { + font-family: monospace; + font-size: 11px; + color: #8b949e; + } + + .detail-tag { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + background: #21262d; + color: #c9d1d9; + font-size: 11px; + margin: 2px 4px 2px 0; + } + + .status-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + } + + .status-badge.success { + background: rgba(63, 185, 80, 0.15); + color: #3fb950; + } + + .status-badge.failure { + background: rgba(248, 81, 73, 0.15); + color: #f85149; + } + </style> +</head> +<body> + <div class="container"> + <div class="header"> + <div> + <h1>Zen Compute Dashboard</h1> + <div class="timestamp">Last updated: <span id="last-update">Never</span></div> + </div> + <div class="health-indicator" id="health-indicator"> + <div class="status-dot"></div> + <span id="health-text">Checking...</span> + </div> + </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" style="width: 100%; border-collapse: collapse; font-size: 13px;"> + <thead> + <tr> + <th style="text-align: left; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Name</th> + <th style="text-align: left; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Platform</th> + <th style="text-align: right; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Cores</th> + <th style="text-align: right; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Timeout</th> + <th style="text-align: right; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Functions</th> + <th style="text-align: left; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">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> + + <!-- 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" style="color: #6e7681; font-size: 13px;">No actions recorded yet.</div> + <div id="action-history-container" style="display: none;"> + <table id="action-history-table" style="width: 100%; border-collapse: collapse; font-size: 13px;"> + <thead> + <tr> + <th style="text-align: right; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; width: 60px;">LSN</th> + <th style="text-align: center; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; width: 70px;">Status</th> + <th style="text-align: left; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Function</th> + <th style="text-align: right; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; width: 80px;">Started</th> + <th style="text-align: right; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; width: 80px;">Finished</th> + <th style="text-align: right; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; width: 80px;">Duration</th> + <th style="text-align: left; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Worker ID</th> + <th style="text-align: left; color: #8b949e; padding: 6px 8px; border-bottom: 1px solid #30363d; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">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: ${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}/apply/ready`); + const isHealthy = response.status === 200; + + const indicator = document.getElementById('health-indicator'); + const text = document.getElementById('health-text'); + + if (isHealthy) { + indicator.classList.add('healthy'); + indicator.classList.remove('unhealthy'); + text.textContent = 'Healthy'; + } else { + indicator.classList.add('unhealthy'); + indicator.classList.remove('healthy'); + text.textContent = 'Unhealthy'; + } + + return isHealthy; + } catch (error) { + const indicator = document.getElementById('health-indicator'); + const text = document.getElementById('health-text'); + indicator.classList.add('unhealthy'); + indicator.classList.remove('healthy'); + text.textContent = 'Error'; + throw error; + } + } + + async function fetchStats() { + const data = await fetchJSON('/stats/apply'); + + // 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('/apply/sysinfo'); + + // Update CPU + const cpuUsage = data.cpu_usage || 0; + document.getElementById('cpu-usage').textContent = cpuUsage.toFixed(1) + '%'; + document.getElementById('cpu-progress').style.width = cpuUsage + '%'; + + 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:#6e7681;font-size:12px;">none</span>' : + `<table class="detail-table">${functions.map(f => + `<tr><td>${f.name || '-'}</td><td class="detail-mono">${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:#6e7681;font-size:12px;">none</span>' : + `<table class="detail-table"> + <tr style="font-size:11px;"> + <td style="color:#6e7681;padding-bottom:4px;">Path</td> + <td style="color:#6e7681;padding-bottom:4px;">Hash</td> + <td style="color:#6e7681;padding-bottom:4px;text-align:right;">Size</td> + </tr> + ${executables.map(e => + `<tr> + <td>${e.name || '-'}</td> + <td class="detail-mono">${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 #30363d;"> + <td style="color:#8b949e;padding-top:6px;">Total</td> + <td></td> + <td style="text-align:right;white-space:nowrap;padding-top:6px;color:#f0f6fc;font-weight:600;">${formatBytes(totalExecSize)}</td> + </tr> + </table>`; + + // Files + const files = desc.files || []; + const filesHtml = files.length === 0 ? '<span style="color:#6e7681;font-size:12px;">none</span>' : + `<table class="detail-table">${files.map(f => + `<tr><td>${f.name || f}</td><td class="detail-mono">${f.hash || ''}</td></tr>` + ).join('')}</table>`; + + // Dirs + const dirs = desc.dirs || []; + const dirsHtml = dirs.length === 0 ? '<span style="color:#6e7681;font-size:12px;">none</span>' : + dirs.map(d => `<span class="detail-tag">${d}</span>`).join(''); + + // Environment + const env = desc.environment || []; + const envHtml = env.length === 0 ? '<span style="color:#6e7681;font-size:12px;">none</span>' : + env.map(e => `<span class="detail-tag">${e}</span>`).join(''); + + panel.innerHTML = ` + <div class="worker-detail-title">${desc.name || id}</div> + <div class="detail-section"> + <table class="detail-table"> + ${field('Worker ID', `<span class="detail-mono">${id}</span>`)} + ${field('Path', desc.path)} + ${field('Platform', 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('/apply/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(`/apply/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="padding: 6px 8px; color: #f0f6fc; border-bottom: 1px solid #21262d;">${name}</td> + <td style="padding: 6px 8px; color: #c9d1d9; border-bottom: 1px solid #21262d;">${host}</td> + <td style="padding: 6px 8px; color: #c9d1d9; border-bottom: 1px solid #21262d; text-align: right;">${cores}</td> + <td style="padding: 6px 8px; color: #c9d1d9; border-bottom: 1px solid #21262d; text-align: right;">${timeout}</td> + <td style="padding: 6px 8px; color: #c9d1d9; border-bottom: 1px solid #21262d; text-align: right;">${functions}</td> + <td style="padding: 6px 8px; color: #8b949e; border-bottom: 1px solid #21262d; font-family: monospace; font-size: 11px;">${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 fetchActionHistory() { + const data = await fetchJSON('/apply/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:#21262d;color:#8b949e;">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 tr = document.createElement('tr'); + tr.innerHTML = ` + <td style="padding: 6px 8px; color: #8b949e; border-bottom: 1px solid #21262d; text-align: right; font-family: monospace;">${lsn}</td> + <td style="padding: 6px 8px; border-bottom: 1px solid #21262d; text-align: center;">${badge}</td> + <td style="padding: 6px 8px; color: #f0f6fc; border-bottom: 1px solid #21262d;">${fn}</td> + <td style="padding: 6px 8px; color: #8b949e; border-bottom: 1px solid #21262d; text-align: right; font-size: 12px; white-space: nowrap;">${formatTime(startDate)}</td> + <td style="padding: 6px 8px; color: #8b949e; border-bottom: 1px solid #21262d; text-align: right; font-size: 12px; white-space: nowrap;">${formatTime(endDate)}</td> + <td style="padding: 6px 8px; color: #c9d1d9; border-bottom: 1px solid #21262d; text-align: right; font-size: 12px; white-space: nowrap;">${formatDuration(startDate, endDate)}</td> + <td style="padding: 6px 8px; color: #8b949e; border-bottom: 1px solid #21262d; font-family: monospace; font-size: 11px;">${workerId}</td> + <td style="padding: 6px 8px; color: #8b949e; border-bottom: 1px solid #21262d; font-family: monospace; font-size: 11px;">${actionId}</td> + `; + tbody.appendChild(tr); + } + + container.style.display = 'block'; + } + + async function updateDashboard() { + try { + await Promise.all([ + fetchHealth(), + fetchStats(), + fetchSysInfo(), + fetchWorkers(), + fetchActionHistory() + ]); + + clearError(); + updateTimestamp(); + } catch (error) { + console.error('Error updating dashboard:', error); + showError(error.message); + } + } + + // Start updating + updateDashboard(); + setInterval(updateDashboard, REFRESH_INTERVAL); + </script> +</body> +</html> |