Initial import of verae-fleet from zapier monorepo

This commit is contained in:
George Lambert 2026-09-11 15:17:42 -04:00
commit 5de9f586fe
45 changed files with 3208 additions and 0 deletions

338
public/index.html Normal file
View file

@ -0,0 +1,338 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Verae operator console</title>
<style>
:root {
--bg:#f4f5fb; --panel:#fff; --ink:#171a26; --muted:#6b7186; --line:#e5e7f0;
--accent:#4f46e5; --accent-ink:#fff; --soft:#eef0fe;
--ok:#047857; --ok-bg:#e3f6e8; --warn:#8a5a00; --warn-bg:#fff3bf;
--err:#dc2626; --err-bg:#f9d4d4; --radius:12px;
--shadow:0 1px 2px rgba(23,26,38,.05), 0 8px 24px rgba(23,26,38,.06);
}
* { box-sizing:border-box; }
body { margin:0; font:14px/1.45 -apple-system,"Segoe UI",sans-serif; background:var(--bg); color:var(--ink); }
header.app {
background:linear-gradient(160deg,#312e81 0%,#4f46e5 60%,#7c74f0 100%);
color:#eef0fe; padding:1rem 1.25rem 0; display:flex; flex-wrap:wrap; gap:.75rem 1.5rem; align-items:flex-end;
}
header.app h1 { margin:0; font-size:1.2rem; }
header.app p { margin:.2rem 0 .8rem; font-size:13px; opacity:.85; max-width:42rem; }
nav { display:flex; gap:.25rem; }
nav button {
border:0; background:transparent; color:#eef0fe; padding:.55rem .9rem; border-radius:10px 10px 0 0;
font:650 13px system-ui; cursor:pointer; opacity:.8;
}
nav button.on { background:var(--bg); color:var(--accent); opacity:1; }
main { padding:1rem 1.25rem 2.5rem; max-width:1280px; margin:0 auto; }
.panel { display:none; }
.panel.on { display:block; }
h2 { font-size:12px; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); margin:1.2rem 0 .5rem; }
.cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:.75rem; }
.card {
background:var(--panel); border-radius:var(--radius); box-shadow:var(--shadow);
padding:.85rem .95rem; border:1px solid var(--line);
}
.card.operational { background:var(--ok-bg); border-color:#c8efd4; }
.card.degraded { background:var(--warn-bg); border-color:#ffe08a; }
.card.down { background:var(--err-bg); border-color:#f3c0c0; }
.card h3 { margin:0 0 .25rem; font-size:14px; }
.muted { color:var(--muted); font-size:12px; }
.pill { font:700 10px system-ui; letter-spacing:.06em; text-transform:uppercase; padding:.12rem .4rem; border-radius:999px; }
.pill.good { background:#c8efd4; color:var(--ok); }
.pill.warn { background:#ffe08a; color:var(--warn); }
.pill.bad { background:#f3c0c0; color:var(--err); }
button.act, a.act { margin:.15rem .2rem 0 0; border:1px solid var(--line); background:#fff; border-radius:8px; padding:.28rem .5rem; font:650 12px system-ui; cursor:pointer; text-decoration:none; color:var(--ink); display:inline-block; }
button.act.primary, a.act.primary { background:var(--accent); color:#fff; border:0; }
button.act.danger { color:var(--err); }
table { width:100%; border-collapse:collapse; background:var(--panel); border-radius:var(--radius); overflow:hidden; box-shadow:var(--shadow); }
th,td { text-align:left; padding:.45rem .6rem; border-bottom:1px solid var(--line); font-size:13px; vertical-align:top; }
th { font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); background:#fafbff; }
tr.operational { background:var(--ok-bg); }
tr.degraded { background:var(--warn-bg); }
tr.down { background:var(--err-bg); }
.inst { font:12px ui-monospace,Menlo,monospace; margin:.2rem 0; }
form.add { display:flex; flex-wrap:wrap; gap:.4rem; margin:.4rem 0 1rem; align-items:end; }
form.add label { font-size:11px; color:var(--muted); display:flex; flex-direction:column; gap:.15rem; }
form.add input, form.add select, aside input, aside select, aside textarea {
padding:.4rem .5rem; border:1px solid var(--line); border-radius:8px; font:13px ui-monospace,Menlo,monospace;
}
#toast { position:fixed; right:1rem; bottom:1rem; max-width:28rem; background:#171a26; color:#eef0fe; padding:.8rem 1rem; border-radius:10px; display:none; white-space:pre-wrap; font:12px ui-monospace,Menlo,monospace; z-index:9; }
.layout { display:grid; grid-template-columns:minmax(260px,340px) 1fr; gap:1rem; min-height:70vh; }
@media (max-width:900px) { .layout { grid-template-columns:1fr; } }
aside.trace { background:var(--panel); border-radius:var(--radius); box-shadow:var(--shadow); padding:1rem; }
.console { background:#0f171d; color:#d7e2ea; border-radius:10px; min-height:220px; overflow:auto; font:12px/1.35 ui-monospace,Menlo,monospace; }
.hop { display:grid; grid-template-columns:52px 18px 150px 1fr; gap:.4rem; padding:.28rem .7rem; border-left:3px solid transparent; }
.hop.ok { border-color:var(--ok); }
.hop.error { border-color:var(--err); color:#ffb4b0; }
.hop.silent { border-color:#6a7a86; color:#8fa0ab; }
.hop.retry,.hop.delay { border-color:#d39b00; }
.mod { color:#9ad7c2; }
.addr { color:#8cb4ea; }
.docs a { color:var(--accent); }
.docs li { margin:.35rem 0; }
.kicker { letter-spacing:.12em; text-transform:uppercase; font:700 10px system-ui; opacity:.75; }
code { font:12px ui-monospace,Menlo,monospace; }
.spark { display:flex; gap:3px; align-items:flex-end; height:28px; margin:.35rem 0; }
.spark i { display:block; width:8px; background:var(--accent); border-radius:2px 2px 0 0; opacity:.85; }
</style>
</head>
<body>
<header class="app">
<div>
<div class="kicker">loopback · not public NATS</div>
<h1>Verae operator console</h1>
<p>Fleet, message trace, and docs in one shell. Pause does not count toward replica floors. SSH keys stay on disk.</p>
</div>
<nav>
<button class="on" data-tab="fleet">Fleet</button>
<button data-tab="trace">Trace</button>
<button data-tab="docs">Docs</button>
</nav>
</header>
<main>
<section id="fleet" class="panel on">
<p id="meta" class="muted"></p>
<h2>Machines</h2>
<form class="add" id="addMachine">
<label>id <input name="id" required placeholder="ns1-b"/></label>
<label>host <input name="host" required placeholder="70.88.205.138"/></label>
<label>kind <select name="kind"><option value="local">local</option><option value="ssh" selected>ssh</option><option value="agent">agent</option></select></label>
<label>user <input name="user" value="marchon"/></label>
<label>ssh port <input name="sshPort" type="number" value="22"/></label>
<label>identity file <input name="identityFile" value="~/.ssh/id_ed25519"/></label>
<label>capacity <input name="capacity" type="number" min="1" value="8"/></label>
<label>roles <input name="roles" value="tree-node,archive-worm"/></label>
<button class="act primary" type="submit">Add machine</button>
</form>
<div class="cards" id="machines"></div>
<h2>Services</h2>
<div id="rows"></div>
</section>
<section id="trace" class="panel">
<div class="layout">
<aside class="trace">
<h2>Zapier step (simulator)</h2>
<label class="muted">Action</label>
<select id="action"></select>
<label class="muted">Data / batch lines</label>
<textarea id="data" style="width:100%;min-height:72px">invoice-1001
invoice-1002
invoice-1003</textarea>
<label class="muted">SHA256</label>
<input id="sha256" style="width:100%"/>
<label class="muted">Public metadata JSON</label>
<input id="publicMeta" style="width:100%" placeholder='{"source":"zap"}'/>
<p><label><input type="checkbox" id="includeAttached"/> includeAttached</label>
<label><input type="checkbox" id="includeTree"/> includeTree</label></p>
<h2>Faults</h2>
<p class="muted"><label><input type="checkbox" id="failEdge402"/> 402</label>
<label><input type="checkbox" id="failNatsDropEvents"/> drop events</label>
<label><input type="checkbox" id="recover"/> recover</label></p>
<p>
<button class="act primary" id="run">Run step</button>
<button class="act" id="reset">Reset world</button>
<button class="act" data-sc="batch">Batch Merkle</button>
<button class="act" data-sc="leaf">Lookup leaf</button>
<button class="act" data-sc="tree">Lookup tree</button>
</p>
</aside>
<div>
<h2>Response</h2>
<pre id="response" class="card" style="white-space:pre-wrap">Run a step.</pre>
<h2>Hop trace</h2>
<div class="console" id="console"></div>
<h2>Monitors</h2>
<div id="monitors"></div>
</div>
</div>
</section>
<section id="docs" class="panel docs">
<p class="muted">Reading order: high-level first, then architecture, then the rest. Catalog defaults to colored PDFs. Operator console is loopback only.</p>
<p><a class="act primary" href="/docs/CONSOLE.pdf" target="_blank" rel="noopener">Open CONSOLE.pdf</a>
· <a href="https://zapier.georgelambert.org/">PDF catalog</a>
· <a href="https://zapier.georgelambert.org/index-md.html">Markdown indexes</a></p>
<h2>High-level (start here)</h2>
<ul>
<li><a href="https://zapier.georgelambert.org/overview/README.pdf">Overview</a> — whole system in plain language</li>
<li><a href="https://zapier.georgelambert.org/overview/02-modules-and-repos.pdf">Modules and repositories</a> — short names of every independent repo</li>
<li><a href="https://zapier.georgelambert.org/docs-master/README.pdf">docs-master</a> — one-line SUMMARY + NATS for each module</li>
<li><a href="https://zapier.georgelambert.org/user-docs/README.pdf">User guide</a> — signup → register → lookup (non-operator)</li>
</ul>
<h2>Architecture</h2>
<ul>
<li><a href="https://zapier.georgelambert.org/overview/03-nats-cluster.pdf">NATS.IO 3-server cluster</a></li>
<li><a href="https://zapier.georgelambert.org/overview/04-uptime.pdf">Uptime / replica floors</a></li>
<li><a href="https://zapier.georgelambert.org/overview/05-network-failures.pdf">Local network failures</a></li>
<li><a href="https://zapier.georgelambert.org/overview/06-address-routing.pdf">Address routing</a></li>
<li><a href="https://zapier.georgelambert.org/overview/08-diagrams.pdf">Diagrams</a> · <a href="https://zapier.georgelambert.org/overview/diagrams/system.svg">system.svg</a></li>
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/composition.pdf">Composition</a></li>
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/modules-and-nats.pdf">Module NATS map</a></li>
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/archive-nats.pdf">Archive / bloom</a></li>
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/tree-nodes.pdf">Tree nodes</a></li>
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/fleet.pdf">Fleet</a></li>
</ul>
<h2>Remaining documentation</h2>
<ul>
<li><a href="https://zapier.georgelambert.org/overview/INDEX.pdf">Full documentation index</a></li>
<li><a href="https://zapier.georgelambert.org/overview/09-expansion-template.pdf">Expansion template</a> · <a href="https://git.georgelambert.org/marchon/verae-nats-process">verae-nats-process</a></li>
<li><a href="https://zapier.georgelambert.org/docs/04-activate/SETUP-ZAPIER-DEVELOPER.pdf">Zapier developer setup</a></li>
<li><a href="https://zapier.georgelambert.org/packages/verae-zapier-simulator/README.pdf">Simulator README</a></li>
<li><a href="https://git.georgelambert.org/marchon/verae-fleet">verae-fleet repo</a></li>
</ul>
</section>
</main>
<div id="toast"></div>
<script>
const $ = (id) => document.getElementById(id);
async function j(url, opts) {
const r = await fetch(url, opts);
const body = await r.json().catch(() => ({}));
return { ok: r.ok, status: r.status, body };
}
function toast(msg) {
const t = $('toast');
t.textContent = typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2);
t.style.display = 'block';
clearTimeout(t._tm);
t._tm = setTimeout(() => { t.style.display = 'none'; }, 8000);
}
function spark(r) {
if (!r || !r.count) return '';
const vals = [r.minMs, r.avgMs, r.p50Ms, r.p90Ms].map(Number);
const max = Math.max(0.1, ...vals);
return `<div class="spark" title="min avg p50 p90">${vals.map(v => `<i style="height:${Math.max(4, (v/max)*28)}px"></i>`).join('')}</div>
<div class="muted">RTT ms ${r.minMs} / ${r.avgMs} / ${r.p50Ms} / ${r.p90Ms} n=${r.count}</div>`;
}
function pill(health) {
const label = health === 'operational' ? 'operational' : health === 'degraded' ? 'degraded' : 'not operational';
const cls = health === 'operational' ? 'good' : health === 'degraded' ? 'warn' : 'bad';
return `<span class="pill ${cls}">${label}</span>`;
}
async function act(path, { confirm: c } = {}) {
if (c && !confirm(c)) return;
const r = await j(path, { method:'POST' });
if (r.body.check) toast(r.body.check);
else if (!r.ok) toast(r.body.error || r.body);
await drawFleet();
}
document.querySelectorAll('nav button').forEach((b) => b.addEventListener('click', () => {
document.querySelectorAll('nav button').forEach((x) => x.classList.toggle('on', x === b));
document.querySelectorAll('.panel').forEach((p) => p.classList.toggle('on', p.id === b.dataset.tab));
}));
async function drawFleet() {
const { body: s } = await j('/api/status');
$('meta').textContent = 'probe ' + (s.monitor?.t || '—') + ' · NATS ' + (s.nats?.url || '') + ' · loopback :3850';
$('machines').innerHTML = (s.machines || []).map((m) => {
const health = !m.enabled ? 'down' : m.running > 0 ? 'operational' : 'degraded';
return `<article class="card ${health}">
<h3>${m.id} ${pill(health)}</h3>
<div class="muted">${m.title || ''}</div>
<div><code>${m.kind === 'ssh' ? (m.user||'marchon')+'@' : ''}${m.host}</code></div>
<div class="muted">${m.kind} · ${m.running}/${m.capacity} running · ${m.available} avail</div>
${spark(m.rtt)}
<div>
<button class="act" onclick="act('/api/machines/${m.id}/${m.enabled ? 'disable' : 'enable'}')">${m.enabled ? 'disable' : 'enable'}</button>
${m.kind === 'ssh' ? `<button class="act" onclick="act('/api/machines/${m.id}/check')">ssh-check</button>` : ''}
</div>
</article>`;
}).join('');
$('rows').innerHTML = '<table><thead><tr><th>Service</th><th>State</th><th>Floor</th><th>RTT</th><th>Instances</th><th></th></tr></thead><tbody>' +
Object.values(s.services).map((sv) => {
const h = sv.health || 'down';
const inst = (sv.instances || []).map((i) => {
const st = !i.pid ? 'dead' : i.paused ? 'paused' : i.healthy === false ? 'unhealthy' : 'up';
const cls = st === 'up' ? 'good' : st === 'paused' ? 'warn' : 'bad';
return `<div class="inst"><span class="pill ${cls}">${st}</span> ${i.id} @${i.machine||'local'} :${i.healthPort}
<button class="act" onclick="act('/api/instances/${i.id}/pause')">pause</button>
<button class="act" onclick="act('/api/instances/${i.id}/resume')">resume</button>
<button class="act danger" onclick="act('/api/instances/${i.id}/restart',{confirm:'Restart ${i.id}?'})">restart</button>
</div>`;
}).join('') || '<span class="muted">none</span>';
return `<tr class="${h}">
<td><strong>${sv.id}</strong><br><span class="muted">${sv.title||''}</span></td>
<td>${pill(h)}</td>
<td>${sv.available} / min ${sv.min}${sv.belowFloor ? ' <span class="pill bad">below</span>' : ''}</td>
<td>${spark(sv.rtt)}</td>
<td>${inst}</td>
<td>
<button class="act" onclick="act('/api/services/${sv.id}/start')">on</button>
<button class="act" onclick="act('/api/services/${sv.id}/pause')">pause</button>
<button class="act danger" onclick="act('/api/services/${sv.id}/stop',{confirm:'Turn off ${sv.id}? This disables keepFloor.'})">off</button>
</td>
</tr>`;
}).join('') + '</tbody></table>';
}
$('addMachine').addEventListener('submit', async (e) => {
e.preventDefault();
const f = e.target;
await j('/api/machines', {
method:'POST', headers:{'content-type':'application/json'},
body: JSON.stringify({
id: f.id.value, host: f.host.value, kind: f.kind.value, user: f.user.value,
sshPort: Number(f.sshPort.value||22), identityFile: f.identityFile.value,
capacity: Number(f.capacity.value||8),
roles: f.roles.value.split(',').map(s=>s.trim()).filter(Boolean), enabled: true,
}),
});
f.reset();
await drawFleet();
});
async function bootTrace() {
const { body } = await j('/api/sim/actions');
$('action').innerHTML = (body.actions||[]).map(a => `<option value="${a.key}">${a.label}</option>`).join('');
$('action').value = 'timestamp_and_wait';
}
function faults() {
const f = {};
for (const id of ['failEdge402','failNatsDropEvents','recover']) if ($(id).checked) f[id] = true;
return f;
}
async function runSim() {
const action = $('action').value;
let publicMetadata;
try { publicMetadata = $('publicMeta').value ? JSON.parse($('publicMeta').value) : undefined; } catch { publicMetadata = { raw: $('publicMeta').value }; }
const { body } = await j('/api/sim/run', {
method:'POST', headers:{'content-type':'application/json'},
body: JSON.stringify({
action, includeAttached: $('includeAttached').checked, includeTree: $('includeTree').checked,
faults: faults(),
input: { data: $('data').value, sha256: $('sha256').value.trim()||undefined, publicMetadata, items: $('data').value, email: 'ada@example.com' },
}),
});
$('response').textContent = JSON.stringify(body.response, null, 2);
if (body.response?.sha256) $('sha256').value = body.response.sha256;
if (body.response?.leaves) $('sha256').value = body.response.leaves[0];
$('console').innerHTML = (body.trace||[]).map(e => {
const arrow = e.dir === 'out' ? '←' : e.dir === 'in' ? '→' : '·';
return `<div class="hop ${e.status}"><span>${e.tMs}ms</span><span>${arrow}</span><span class="mod">${e.module}</span><span><strong>${e.hop}</strong> <span class="addr">${e.address||''}</span> ${e.note? '— '+e.note:''}</span></div>`;
}).join('');
const m = body.monitor || {};
$('monitors').innerHTML = `<p>${pill(m.ok ? 'operational' : 'degraded')} ${m.stats ? m.stats.hops+' hops · '+m.stats.errors+' errors' : ''}</p>` +
(m.suggestions||[]).map(s => `<div class="card ${s.severity==='high'?'down':'degraded'}"><span class="pill ${s.severity==='high'?'bad':'warn'}">${s.severity}</span> ${s.text}</div>`).join('');
}
$('run').onclick = runSim;
$('reset').onclick = async () => { await j('/api/sim/reset', { method:'POST' }); $('console').innerHTML=''; $('response').textContent='World reset.'; };
document.querySelectorAll('[data-sc]').forEach((b) => b.addEventListener('click', async () => {
const sc = b.dataset.sc;
if (sc === 'batch') { $('action').value = 'batch_timestamp'; $('data').value = 'invoice-1001\ninvoice-1002\ninvoice-1003'; }
if (sc === 'leaf') { $('action').value = 'hash_lookup'; $('includeTree').checked = false; }
if (sc === 'tree') { $('action').value = 'tree_lookup'; $('includeTree').checked = true; }
await runSim();
}));
drawFleet();
bootTrace().then(async () => {
const tab = new URLSearchParams(location.search).get('tab');
if (tab) document.querySelector(`nav button[data-tab="${tab}"]`)?.click();
const demo = new URLSearchParams(location.search).get('demo');
if (demo) {
const b = document.querySelector(`[data-sc="${demo}"]`);
if (b) b.click();
}
});
setInterval(drawFleet, 2000);
</script>
</body>
</html>