feat(infra): commit live infra-audit/cluster work to reconcile git with prod
This work (network_hosts inventory + infra_audit MCP tool, /api/cluster + Sacred Valley cluster card, topbar cluster-health pill + SW self-heal) was built in an earlier session and DEPLOYED to CT 311 as alpha.24–26, but was never committed to git — prod was running code absent from the repo. Commits it as-is (already prod-validated) so git matches the live state, and restores its alpha.24/25/26 CHANGELOG entries. Files are disjoint from the fold-in work; both now ship together under alpha.27. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,29 @@ import { el, mount, clear } from '../dom.js';
|
||||
import { navigate } from '../router.js';
|
||||
import { on } from '../state.js';
|
||||
import { toggleSidebar, toggleRail } from './chrome.js';
|
||||
import { api } from '../api.js';
|
||||
|
||||
// Cluster health → topbar pill. Returns [status, label, title].
|
||||
function classifyCluster(c) {
|
||||
if (!c || c.error) return ['unknown', 'cluster ?', 'Cluster status unavailable'];
|
||||
if (!c.quorate) return ['down', 'no quorum', 'Cluster has LOST quorum'];
|
||||
if ((c.nodes_online ?? 0) < (c.nodes_total ?? 0)) return ['down', 'node down', `${c.nodes_online}/${c.nodes_total} nodes online`];
|
||||
if (c.ha && c.ha.services_error > 0) return ['warn', 'HA issue', `${c.ha.services_error} HA service(s) in error`];
|
||||
return ['ok', 'healthy', `Quorate · ${c.nodes_online}/${c.nodes_total} nodes · HA ok`];
|
||||
}
|
||||
|
||||
function startClusterHealth(pill, labelEl) {
|
||||
async function tick() {
|
||||
let c = null;
|
||||
try { c = await api.get('/api/cluster'); } catch { c = { error: 'fetch' }; }
|
||||
const [status, label, title] = classifyCluster(c);
|
||||
pill.className = 'icon-btn cluster-health status-' + status;
|
||||
pill.title = title;
|
||||
labelEl.textContent = label;
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 30000);
|
||||
}
|
||||
|
||||
function captureModal() {
|
||||
const root = document.getElementById('modal-root');
|
||||
@@ -37,17 +60,24 @@ export function renderTopbar(root) {
|
||||
|
||||
const bell = el('button', { class: 'icon-btn', onclick: () => navigate('/inbox') }, 'Inbox');
|
||||
|
||||
const chLabel = el('span', { class: 'ch-label' }, '…');
|
||||
const clusterPill = el('button', { class: 'icon-btn cluster-health status-unknown', title: 'Cluster health', onclick: () => navigate('/sacred-valley') },
|
||||
el('span', { class: 'dot' }), chLabel);
|
||||
|
||||
mount(root,
|
||||
el('button', { class: 'chrome-toggle', title: 'Toggle menu', onclick: toggleSidebar }, '☰'),
|
||||
el('div', { class: 'brand' }, 'VOID'),
|
||||
el('button', { class: 'icon-btn', onclick: captureModal }, '+ Capture'),
|
||||
el('div', { class: 'topbar-search' }, searchInput),
|
||||
el('div', { class: 'topbar-spacer' }),
|
||||
clusterPill,
|
||||
bell,
|
||||
el('button', { class: 'chrome-toggle', title: 'Toggle companion chat', onclick: toggleRail }, '◆'),
|
||||
el('button', { class: 'icon-btn', onclick: () => alert('Agent-switching ships post-Plan-2.') }, 'Owner')
|
||||
);
|
||||
|
||||
startClusterHealth(clusterPill, chLabel);
|
||||
|
||||
on('pending-count', (n) => {
|
||||
const old = bell.querySelector('.badge');
|
||||
if (old) old.remove();
|
||||
|
||||
@@ -4,6 +4,26 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Void</title>
|
||||
<script>
|
||||
// Always-fresh: Void 2 ships NO service worker. Proactively unregister any
|
||||
// worker (notably the legacy Void 1 caching SW that still controls the
|
||||
// void.hynesy.com origin in returning browsers) and drop its caches on every
|
||||
// load, so assets are never served stale. Runs before any module script.
|
||||
(function () {
|
||||
try {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then(function (rs) { rs.forEach(function (r) { r.unregister(); }); })
|
||||
.catch(function () {});
|
||||
}
|
||||
if (window.caches && caches.keys) {
|
||||
caches.keys()
|
||||
.then(function (ks) { ks.forEach(function (k) { caches.delete(k); }); })
|
||||
.catch(function () {});
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<!--
|
||||
Cradle aesthetic: Cinzel for marquee headings (Sacred Valley, view titles),
|
||||
Cormorant Garamond for body display in cards. System UI for chrome.
|
||||
|
||||
@@ -448,6 +448,25 @@ ul.plain li:last-child { border-bottom: none; }
|
||||
.tile.status-warn .dot { background: var(--warn); box-shadow: 0 0 7px var(--warn); }
|
||||
.tile.status-down .dot { background: var(--bad); box-shadow: 0 0 7px var(--bad); }
|
||||
.tile.status-unknown .dot { background: var(--muted); }
|
||||
/* cluster-health card — reuse the tile status palette */
|
||||
.sv-cluster .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; background: var(--muted); }
|
||||
.sv-cluster .status-ok .dot { background: var(--ok); box-shadow: 0 0 7px var(--ok); }
|
||||
.sv-cluster .status-down .dot { background: var(--bad); box-shadow: 0 0 7px var(--bad); }
|
||||
.sv-cluster .warn { color: var(--warn); }
|
||||
.sv-cluster .cl-badge { font-family: var(--font-mono); font-size: 11px; font-weight: 700; letter-spacing: .06em; padding: 1px 7px; border-radius: 4px; }
|
||||
.sv-cluster .cl-badge.ok { color: var(--ok); border: 1px solid var(--accent-soft); }
|
||||
.sv-cluster .cl-badge.bad { color: var(--bad); border: 1px solid var(--bad); background: var(--accent-soft); }
|
||||
/* topbar cluster-health pill */
|
||||
.cluster-health { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.cluster-health .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
|
||||
.cluster-health .ch-label { font-size: 12px; letter-spacing: .04em; text-transform: lowercase; }
|
||||
.cluster-health.status-ok .dot { background: var(--ok); box-shadow: 0 0 7px var(--ok); }
|
||||
.cluster-health.status-ok .ch-label { color: var(--ok); }
|
||||
.cluster-health.status-warn .dot { background: var(--warn); box-shadow: 0 0 7px var(--warn); }
|
||||
.cluster-health.status-warn .ch-label { color: var(--warn); }
|
||||
.cluster-health.status-down { border-color: var(--bad); }
|
||||
.cluster-health.status-down .dot { background: var(--bad); box-shadow: 0 0 7px var(--bad); }
|
||||
.cluster-health.status-down .ch-label { color: var(--bad); }
|
||||
.tile-go { color: var(--lb); font-size: 12px; opacity: 0; transition: opacity .25s; }
|
||||
.tile:hover .tile-go { opacity: 1; }
|
||||
/* Tile root is a div hosting a stretched primary link + a small alt control. */
|
||||
|
||||
44
public/views/cards/cluster.js
Normal file
44
public/views/cards/cluster.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// public/views/cards/cluster.js — Proxmox cluster health + quorum across nodes.
|
||||
import { el, mount } from '../../dom.js';
|
||||
import { api } from '../../api.js';
|
||||
|
||||
let body, timer;
|
||||
|
||||
function nodeRow(n, master) {
|
||||
const tags = [];
|
||||
if (n.local) tags.push('local');
|
||||
if (master === n.name) tags.push('master');
|
||||
return el('div', { class: 'sv-row status-' + (n.online ? 'ok' : 'down') },
|
||||
el('span', { class: 'k' }, el('span', { class: 'dot' }), n.name),
|
||||
el('span', {}, (n.online ? 'online' : 'OFFLINE') + (tags.length ? ' · ' + tags.join(' · ') : '')));
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!body) return;
|
||||
try {
|
||||
const c = await api.get('/api/cluster');
|
||||
if (c.error) { mount(body, el('span', { class: 'muted' }, 'Cluster: ' + c.error)); return; }
|
||||
const haIssues = c.ha?.services_error || 0;
|
||||
const rows = el('div', { class: 'sv-cluster' },
|
||||
el('div', { class: 'sv-row' },
|
||||
el('span', { class: 'k' }, 'Quorum'),
|
||||
el('span', { class: 'cl-badge ' + (c.quorate ? 'ok' : 'bad') }, c.quorate ? 'QUORATE' : 'NO QUORUM')),
|
||||
el('div', { class: 'sv-row' },
|
||||
el('span', { class: 'k' }, 'Nodes'),
|
||||
el('span', { class: c.nodes_online < c.nodes_total ? 'warn' : '' }, `${c.nodes_online}/${c.nodes_total} online`)),
|
||||
...(c.nodes || []).map(n => nodeRow(n, c.ha?.master)),
|
||||
el('div', { class: 'sv-row' },
|
||||
el('span', { class: 'k' }, 'HA services'),
|
||||
el('span', { class: haIssues ? 'warn' : '' },
|
||||
haIssues ? `${c.ha.services_total} · ${haIssues} ⚠` : `${c.ha?.services_total ?? 0} · ok`))
|
||||
);
|
||||
mount(body, rows);
|
||||
} catch { mount(body, el('span', { class: 'muted' }, 'Cluster unavailable')); }
|
||||
}
|
||||
|
||||
export default {
|
||||
id: 'cluster', title: 'Cluster · HZ', size: 'm',
|
||||
mount(e) { body = e; load(); },
|
||||
start() { timer = setInterval(load, 30000); },
|
||||
stop() { clearInterval(timer); body = null; }
|
||||
};
|
||||
@@ -13,8 +13,9 @@ import inbox from './cards/inbox.js';
|
||||
import search from './cards/search.js';
|
||||
import speedtest from './cards/speedtest.js';
|
||||
import aiUsage from './cards/ai_usage.js';
|
||||
import cluster from './cards/cluster.js';
|
||||
|
||||
const CARD_MODULES = [clock, weather, hostPerf, jobs, inbox, search, speedtest, aiUsage];
|
||||
const CARD_MODULES = [clock, weather, hostPerf, cluster, jobs, inbox, search, speedtest, aiUsage];
|
||||
const BY_ID = new Map(CARD_MODULES.map(d => [d.id, d]));
|
||||
|
||||
let active = []; // mounted cards needing stop()
|
||||
|
||||
Reference in New Issue
Block a user