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>
87 lines
3.3 KiB
JavaScript
87 lines
3.3 KiB
JavaScript
import net from 'node:net';
|
|
|
|
// Doc/infra sanity check. Pure functions with an injected `probe(host, port) ->
|
|
// Promise<bool>` so they're testable offline; the default tcpProbe is used in prod.
|
|
|
|
const LAN_RE = /(?<![\d.])(192\.168\.\d{1,3}\.\d{1,3})(?::(\d{1,5}))?(?![\d])/g;
|
|
|
|
// Pull unique LAN endpoints from free text. host-only refs come back with port:null.
|
|
export function extractEndpoints(text) {
|
|
const seen = new Map();
|
|
for (const m of String(text || '').matchAll(LAN_RE)) {
|
|
const host = m[1];
|
|
const port = m[2] ? Number(m[2]) : null;
|
|
const key = `${host}:${port ?? ''}`;
|
|
if (!seen.has(key)) seen.set(key, { host, port });
|
|
}
|
|
return [...seen.values()];
|
|
}
|
|
|
|
export function parseUrl(url) {
|
|
try {
|
|
const u = new URL(url);
|
|
const port = u.port ? Number(u.port) : (u.protocol === 'https:' ? 443 : 80);
|
|
return { host: u.hostname, port };
|
|
} catch { return null; }
|
|
}
|
|
|
|
// Default reachability probe: a TCP connect with a short timeout.
|
|
export function tcpProbe(host, port, timeoutMs = 2500) {
|
|
return new Promise((resolve) => {
|
|
const sock = new net.Socket();
|
|
let done = false;
|
|
const finish = (ok) => { if (done) return; done = true; sock.destroy(); resolve(ok); };
|
|
sock.setTimeout(timeoutMs);
|
|
sock.once('connect', () => finish(true));
|
|
sock.once('timeout', () => finish(false));
|
|
sock.once('error', () => finish(false));
|
|
sock.connect(port, host);
|
|
});
|
|
}
|
|
|
|
// Cross-check every IP:port referenced in the wiki against live reachability.
|
|
// Flags stale references (e.g. a CT that moved off an old IP) grouped by page.
|
|
export async function auditDocs({ pages, probe }) {
|
|
const map = new Map(); // host:port -> { host, port, pages:Set }
|
|
for (const p of pages || []) {
|
|
for (const ep of extractEndpoints(p.body_md)) {
|
|
const key = `${ep.host}:${ep.port ?? ''}`;
|
|
if (!map.has(key)) map.set(key, { host: ep.host, port: ep.port, pages: new Set() });
|
|
map.get(key).pages.add(p.title);
|
|
}
|
|
}
|
|
const all = [...map.values()];
|
|
const probable = all.filter(e => e.port != null);
|
|
const unprobed = all.filter(e => e.port == null).map(e => ({ host: e.host, port: null, pages: [...e.pages] }));
|
|
const unreachable = [];
|
|
for (const e of probable) {
|
|
if (!(await probe(e.host, e.port))) unreachable.push({ host: e.host, port: e.port, pages: [...e.pages] });
|
|
}
|
|
return {
|
|
ok: unreachable.length === 0,
|
|
summary: { endpoints: all.length, probed: probable.length, reachable: probable.length - unreachable.length, unreachable: unreachable.length },
|
|
unreachable,
|
|
unprobed
|
|
};
|
|
}
|
|
|
|
// Probe each registered service's LAN url; flag any that don't answer.
|
|
export async function auditServices({ services, probe }) {
|
|
let probed = 0;
|
|
const unreachable = [];
|
|
for (const s of services || []) {
|
|
const hp = parseUrl(s.url);
|
|
if (!hp) continue;
|
|
probed++;
|
|
if (!(await probe(hp.host, hp.port))) unreachable.push({ id: s.id, url: s.url, host: hp.host, port: hp.port });
|
|
}
|
|
return { ok: unreachable.length === 0, summary: { probed, unreachable: unreachable.length }, unreachable };
|
|
}
|
|
|
|
// Full sanity sweep used by the API route / MCP tool.
|
|
export async function runAudit({ pages = [], services = [], probe = tcpProbe }) {
|
|
const docs = await auditDocs({ pages, probe });
|
|
const svc = await auditServices({ services, probe });
|
|
return { ok: docs.ok && svc.ok, docs, services: svc };
|
|
}
|