/** * Cluster Dashboard — admin surface showing registered cluster nodes. * * Polls GET /api/v1/admin/cluster every 10s. Renders one card per node * with runtime stats from the JSONB stats column. New keys added to * stats are rendered automatically — no code change required. */ (function () { 'use strict'; var mount = document.getElementById('extension-mount'); if (!mount) return; var REFRESH_MS = 10000; var timer = null; mount.innerHTML = '
' + '
' + '

Cluster

' + 'loading...' + '
' + '
' + '
'; load(); timer = setInterval(load, REFRESH_MS); // Cleanup on navigation (SPA) window.addEventListener('beforeunload', function () { if (timer) clearInterval(timer); }); function load() { var api = (window.sw && window.sw.api) || (typeof API !== 'undefined' && API); if (!api) return; var get = api._get || api.get; if (!get) return; get.call(api, '/api/v1/admin/cluster').then(render).catch(function () { // 404 = SQLite (no cluster route registered), or other fetch error render({ data: [] }); }); } function render(resp) { var nodes = (resp && resp.data) || []; var status = document.getElementById('clusterStatus'); var grid = document.getElementById('clusterGrid'); status.textContent = nodes.length + ' node' + (nodes.length !== 1 ? 's' : '') + ' registered'; if (nodes.length === 0) { grid.innerHTML = '

No nodes registered. The cluster registry requires PostgreSQL — SQLite runs single-node only.

'; return; } grid.innerHTML = nodes.map(nodeCard).join(''); } function nodeCard(node) { var stats = {}; try { stats = typeof node.stats === 'string' ? JSON.parse(node.stats) : (node.stats || {}); } catch (e) { /* ignore */ } var uptimeSec = stats.uptime_sec || 0; var uptime = formatDuration(uptimeSec); var heap = formatBytes(stats.heap_alloc || 0); var gcPause = ((stats.gc_pause_ns || 0) / 1e6).toFixed(1) + ' ms'; var wsClients = stats.ws_clients != null ? stats.ws_clients : '-'; var goroutines = stats.goroutines != null ? stats.goroutines : '-'; var gcCycles = stats.gc_cycles != null ? stats.gc_cycles : '-'; var heartbeatAge = timeSince(node.heartbeat); return ( '
' + '
' + '' + esc(node.node_id) + '' + '' + heartbeatAge + '' + '
' + (node.endpoint ? '
' + esc(node.endpoint) + '
' : '') + '
' + stat('Uptime', uptime) + stat('WS Clients', wsClients) + stat('Goroutines', goroutines) + stat('Heap', heap) + stat('GC Pause', gcPause) + stat('GC Cycles', gcCycles) + '
' + '
' ); } function stat(label, value) { return '
' + label + '' + value + '
'; } function formatBytes(b) { if (b < 1024) return b + ' B'; if (b < 1024 * 1024) return (b / 1024).toFixed(1) + ' KB'; return (b / (1024 * 1024)).toFixed(1) + ' MB'; } function formatDuration(sec) { if (sec < 60) return Math.floor(sec) + 's'; if (sec < 3600) return Math.floor(sec / 60) + 'm ' + Math.floor(sec % 60) + 's'; var h = Math.floor(sec / 3600); var m = Math.floor((sec % 3600) / 60); return h + 'h ' + m + 'm'; } function timeSince(iso) { if (!iso) return '-'; var ms = Date.now() - new Date(iso).getTime(); if (ms < 0) ms = 0; var sec = Math.floor(ms / 1000); if (sec < 60) return sec + 's ago'; return Math.floor(sec / 60) + 'm ago'; } function esc(s) { var d = document.createElement('div'); d.appendChild(document.createTextNode(s || '')); return d.innerHTML; } })();