Restyle staff and console UIs; add UI-Docs walkthrough and review PDF.
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
Match CS/sales/accounting/access-staff to the portal indigo system with dollar amounts, skip links, and empty states. Fleet replica actions move into overflow menus, roles become chips, Docs become cards, and the header copy reflects the 0.0.0.0 bind. Simulator uses the same shell (orange only for faults). Portal API keys are masked; admin customers edit in a drawer. Catalog uses system-ui. New UI-Docs repo holds screenshots, usage notes, and UI-REVIEW.pdf.
This commit is contained in:
parent
ddf772454b
commit
cb07f5b321
72 changed files with 3284 additions and 242 deletions
|
|
@ -11,7 +11,7 @@ node src/cli.js serve
|
|||
|
||||
| Tab | Purpose |
|
||||
|-----|---------|
|
||||
| **Fleet** | Machines (SSH/local), replica floors, green/yellow/red, RTT min/avg/p50/p90 spark, pause/restart/off with confirm, ssh-check toast |
|
||||
| **Fleet** | Machines (SSH/local), replica floors, green/yellow/red, RTT spark. Replica and service actions live in one **⋯** overflow menu. Power column shows on/paused/off as status, not toggles. Add-machine roles are chips. |
|
||||
| **Trace** | In-process Zapier simulator: run a step, hop log, monitors/suggestions |
|
||||
| **Docs** | Reading order: overview → module summaries → architecture → remaining docs |
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 210 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 391 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 285 KiB |
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Verae operator console</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%234f46e5'/%3E%3C/svg%3E"/>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#f4f5fb; --panel:#fff; --ink:#171a26; --muted:#6b7186; --line:#e5e7f0;
|
||||
|
|
@ -11,21 +12,29 @@
|
|||
--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);
|
||||
--fault:#ea580c;
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font:14px/1.45 -apple-system,"Segoe UI",sans-serif; background:var(--bg); color:var(--ink); }
|
||||
body { margin:0; font:14px/1.45 -apple-system,"SF Pro Text","Segoe UI",sans-serif; background:var(--bg); color:var(--ink); }
|
||||
.skip { position:absolute; left:-999px; }
|
||||
.skip:focus { left:1rem; top:1rem; z-index:30; background:#fff; color:var(--accent); padding:.5rem .9rem; border-radius:8px; }
|
||||
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; }
|
||||
header.app p { margin:.2rem 0 .8rem; font-size:13px; opacity:.85; max-width:44rem; }
|
||||
nav { display:flex; gap:.15rem; }
|
||||
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;
|
||||
border:0; background:transparent; color:#c7c9ff; min-height:44px; padding:.7rem 1.15rem;
|
||||
border-radius:10px 10px 0 0; font:650 14px system-ui; cursor:pointer;
|
||||
}
|
||||
nav button.on { background:var(--bg); color:var(--accent); opacity:1; }
|
||||
nav button:hover { color:#fff; background:rgba(255,255,255,.08); }
|
||||
nav button.on { background:var(--bg); color:var(--accent); }
|
||||
nav button:focus-visible, button.act:focus-visible, a.act:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
|
||||
outline:2px solid #fff; outline-offset:2px;
|
||||
}
|
||||
nav button.on:focus-visible { outline-color:var(--accent); }
|
||||
main { padding:1rem 1.25rem 2.5rem; max-width:1280px; margin:0 auto; }
|
||||
.panel { display:none; }
|
||||
.panel.on { display:block; }
|
||||
|
|
@ -47,44 +56,71 @@
|
|||
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); }
|
||||
button.act.fault { background:var(--fault); color:#fff; border:0; }
|
||||
.table-wrap { overflow:auto; border-radius:var(--radius); box-shadow:var(--shadow); }
|
||||
table { width:100%; border-collapse:collapse; background:var(--panel); overflow:visible; }
|
||||
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; }
|
||||
.inst { font:12px ui-monospace,Menlo,monospace; margin:.2rem 0; display:flex; gap:.4rem; align-items:center; flex-wrap:wrap; }
|
||||
form.add { display:flex; flex-wrap:wrap; gap:.4rem; margin:.4rem 0 1rem; align-items:end; }
|
||||
form.add .roles-field { flex: 1 0 100%; }
|
||||
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;
|
||||
}
|
||||
.roles { display:flex; flex-wrap:wrap; gap:.35rem; max-width:22rem; }
|
||||
.roles label {
|
||||
display:inline-flex; align-items:center; gap:.25rem; font:650 11px system-ui; color:var(--ink);
|
||||
background:var(--soft); border:1px solid #d7dbf8; border-radius:999px; padding:.2rem .55rem; cursor:pointer;
|
||||
}
|
||||
.roles input { accent-color:var(--accent); }
|
||||
#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 { display:grid; grid-template-columns:4.5rem 1.2rem minmax(7rem,11rem) minmax(0,1fr); gap:.4rem; padding:.28rem .7rem; border-left:3px solid transparent; }
|
||||
.hop .mod, .hop .addr, .hop strong { overflow-wrap:anywhere; word-break:break-word; }
|
||||
.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; }
|
||||
.docs .card p { margin:.35rem 0 .7rem; color:var(--muted); font-size:13px; }
|
||||
.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; }
|
||||
.more { position:relative; display:inline-block; }
|
||||
.more-btn { min-width:2rem; font-size:16px; line-height:1; }
|
||||
.more-pop {
|
||||
display:none; position:absolute; right:0; top:calc(100% + 4px); z-index:6;
|
||||
background:#fff; border:1px solid var(--line); border-radius:10px;
|
||||
box-shadow:var(--shadow); min-width:11rem; padding:.25rem; z-index:40;
|
||||
}
|
||||
.more.open .more-pop { display:block; }
|
||||
.more-pop button {
|
||||
display:block; width:100%; text-align:left; border:0; background:none;
|
||||
padding:.45rem .6rem; border-radius:8px; font:650 12px system-ui; cursor:pointer; color:var(--ink);
|
||||
}
|
||||
.more-pop button:hover { background:var(--soft); }
|
||||
.more-pop button.danger { color:var(--err); }
|
||||
.faults { border:1px solid #fed7aa; background:#fff7ed; border-radius:10px; padding:.55rem .7rem; }
|
||||
.empty { text-align:center; padding:1.4rem 1rem; color:var(--muted); }
|
||||
.empty svg { display:block; margin:0 auto .5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<header class="app">
|
||||
<div>
|
||||
<div class="kicker">loopback · not public NATS</div>
|
||||
<div class="kicker">operator LAN · binds 0.0.0.0:3850</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>
|
||||
<p>Fleet, message trace, and docs in one shell. HTTP listens on all interfaces; NATS stays private. Pause does not count toward replica floors. SSH keys stay on disk.</p>
|
||||
</div>
|
||||
<nav>
|
||||
<button class="on" data-tab="fleet">Fleet</button>
|
||||
|
|
@ -92,7 +128,7 @@
|
|||
<button data-tab="docs">Docs</button>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<main id="main">
|
||||
<section id="fleet" class="panel on">
|
||||
<p id="meta" class="muted"></p>
|
||||
<h2>Machines</h2>
|
||||
|
|
@ -104,7 +140,18 @@
|
|||
<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>
|
||||
<label class="roles-field">roles
|
||||
<div class="roles" id="roleChips">
|
||||
<label><input type="checkbox" name="role" value="*"/> *</label>
|
||||
<label><input type="checkbox" name="role" value="tree-node" checked/> tree-node</label>
|
||||
<label><input type="checkbox" name="role" value="archive-worm" checked/> archive-worm</label>
|
||||
<label><input type="checkbox" name="role" value="archive-aggregator"/> archive-aggregator</label>
|
||||
<label><input type="checkbox" name="role" value="job-poller"/> job-poller</label>
|
||||
<label><input type="checkbox" name="role" value="webhook-deliver"/> webhook-deliver</label>
|
||||
<label><input type="checkbox" name="role" value="nats"/> nats</label>
|
||||
<label><input type="checkbox" name="role" value="edge"/> edge</label>
|
||||
</div>
|
||||
</label>
|
||||
<button class="act primary" type="submit">Add machine</button>
|
||||
</form>
|
||||
<div class="cards" id="machines"></div>
|
||||
|
|
@ -128,9 +175,12 @@ invoice-1003</textarea>
|
|||
<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>
|
||||
<div class="faults">
|
||||
<p class="muted" style="margin:.1rem 0 .35rem">Orange is inject-only — not the product brand.</p>
|
||||
<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>
|
||||
</div>
|
||||
<p>
|
||||
<button class="act primary" id="run">Run step</button>
|
||||
<button class="act" id="reset">Reset world</button>
|
||||
|
|
@ -150,43 +200,44 @@ invoice-1003</textarea>
|
|||
</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 class="muted">Reading order: high-level first, then architecture, then the rest. Catalog defaults to colored PDFs. Operator HTTP binds all interfaces; NATS does not.</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>
|
||||
<div class="cards">
|
||||
<article class="card"><h3>Overview</h3><p>Whole system in plain language.</p><a class="act primary" href="https://zapier.georgelambert.org/overview/README.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Modules and repositories</h3><p>Short names of every independent repo.</p><a class="act primary" href="https://zapier.georgelambert.org/overview/02-modules-and-repos.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>docs-master</h3><p>One-line SUMMARY + NATS for each module.</p><a class="act primary" href="https://zapier.georgelambert.org/docs-master/README.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>User guide</h3><p>Signup → register → lookup (non-operator).</p><a class="act primary" href="https://zapier.georgelambert.org/user-docs/README.pdf">Open PDF</a></article>
|
||||
</div>
|
||||
<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>
|
||||
<div class="cards">
|
||||
<article class="card"><h3>NATS.IO 3-server cluster</h3><p>Loopback NATS, not a public bus.</p><a class="act primary" href="https://zapier.georgelambert.org/overview/03-nats-cluster.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Uptime / replica floors</h3><p>Pause does not count toward floors.</p><a class="act primary" href="https://zapier.georgelambert.org/overview/04-uptime.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Local network failures</h3><p>What the monitors flag.</p><a class="act primary" href="https://zapier.georgelambert.org/overview/05-network-failures.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Address routing</h3><p>Access planes to internal subjects.</p><a class="act primary" href="https://zapier.georgelambert.org/overview/06-address-routing.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Diagrams</h3><p>System, cluster, routing.</p><a class="act" href="https://zapier.georgelambert.org/overview/08-diagrams.pdf">PDF</a> <a class="act" href="https://zapier.georgelambert.org/overview/diagrams/system.svg">SVG</a></article>
|
||||
<article class="card"><h3>Composition</h3><p>How packages sit together.</p><a class="act primary" href="https://zapier.georgelambert.org/docs/02-architecture/composition.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Module NATS map</h3><p>Subjects per repo.</p><a class="act primary" href="https://zapier.georgelambert.org/docs/02-architecture/modules-and-nats.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Archive / bloom</h3><p>WORM nodes.</p><a class="act primary" href="https://zapier.georgelambert.org/docs/02-architecture/archive-nats.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Tree nodes</h3><p>Merkle proofs for hashes.</p><a class="act primary" href="https://zapier.georgelambert.org/docs/02-architecture/tree-nodes.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Fleet</h3><p>Replica floors and spawn.</p><a class="act primary" href="https://zapier.georgelambert.org/docs/02-architecture/fleet.pdf">Open PDF</a></article>
|
||||
</div>
|
||||
<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>
|
||||
<div class="cards">
|
||||
<article class="card"><h3>Full index</h3><p>Every remaining document.</p><a class="act primary" href="https://zapier.georgelambert.org/overview/INDEX.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Expansion template</h3><p>New verae.* worker.</p><a class="act" href="https://zapier.georgelambert.org/overview/09-expansion-template.pdf">PDF</a> <a class="act" href="https://git.georgelambert.org/marchon/verae-nats-process">repo</a></article>
|
||||
<article class="card"><h3>Zapier developer setup</h3><p>Platform CLI, not a customer surface.</p><a class="act primary" href="https://zapier.georgelambert.org/docs/04-activate/SETUP-ZAPIER-DEVELOPER.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>Simulator README</h3><p>Hop-by-hop lab.</p><a class="act primary" href="https://zapier.georgelambert.org/packages/verae-zapier-simulator/README.pdf">Open PDF</a></article>
|
||||
<article class="card"><h3>verae-fleet repo</h3><p>Source for this console.</p><a class="act primary" href="https://git.georgelambert.org/marchon/verae-fleet">Open repo</a></article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div id="toast"></div>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const EMPTY_SVG = '<svg width="80" height="64" viewBox="0 0 80 64" fill="none" aria-hidden="true"><rect x="16" y="8" width="48" height="48" rx="8" fill="#eef0fe"/><rect x="24" y="20" width="32" height="4" rx="2" fill="#4f46e5" opacity=".35"/><rect x="24" y="30" width="24" height="4" rx="2" fill="#4f46e5" opacity=".2"/><rect x="24" y="40" width="28" height="4" rx="2" fill="#4f46e5" opacity=".2"/></svg>';
|
||||
async function j(url, opts) {
|
||||
const r = await fetch(url, opts);
|
||||
const body = await r.json().catch(() => ({}));
|
||||
|
|
@ -211,6 +262,14 @@ function pill(health) {
|
|||
const cls = health === 'operational' ? 'good' : health === 'degraded' ? 'warn' : 'bad';
|
||||
return `<span class="pill ${cls}">${label}</span>`;
|
||||
}
|
||||
function moreMenu(items) {
|
||||
return `<div class="more">
|
||||
<button type="button" class="act more-btn" aria-haspopup="menu" aria-label="Actions">⋯</button>
|
||||
<div class="more-pop" role="menu">${items.map((it) =>
|
||||
`<button type="button" role="menuitem" class="${it.danger ? 'danger' : ''}" data-path="${it.path}" data-confirm="${it.confirm || ''}">${it.label}</button>`
|
||||
).join('')}</div>
|
||||
</div>`;
|
||||
}
|
||||
async function act(path, { confirm: c } = {}) {
|
||||
if (c && !confirm(c)) return;
|
||||
const r = await j(path, { method:'POST' });
|
||||
|
|
@ -218,39 +277,68 @@ async function act(path, { confirm: c } = {}) {
|
|||
else if (!r.ok) toast(r.body.error || r.body);
|
||||
await drawFleet();
|
||||
}
|
||||
document.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.more-pop button[data-path]');
|
||||
if (item) {
|
||||
act(item.dataset.path, { confirm: item.dataset.confirm || undefined });
|
||||
item.closest('.more')?.classList.remove('open');
|
||||
return;
|
||||
}
|
||||
const btn = e.target.closest('.more-btn');
|
||||
if (btn) {
|
||||
const wrap = btn.closest('.more');
|
||||
document.querySelectorAll('.more.open').forEach((m) => { if (m !== wrap) m.classList.remove('open'); });
|
||||
wrap.classList.toggle('open');
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('.more.open').forEach((m) => m.classList.remove('open'));
|
||||
});
|
||||
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) => {
|
||||
$('meta').textContent = 'probe ' + (s.monitor?.t || '—') + ' · NATS ' + (s.nats?.url || '') + ' · HTTP 0.0.0.0:3850';
|
||||
const machines = s.machines || [];
|
||||
$('machines').innerHTML = machines.length ? 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>
|
||||
<div class="muted">${(m.roles||[]).map((r) => `<span class="pill">${r}</span>`).join(' ')}</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) => {
|
||||
}).join('') : `<div class="empty">${EMPTY_SVG}<div>No machines yet</div><p>Add a machine above. Roles are chips, not a truncated text field.</p></div>`;
|
||||
const services = Object.values(s.services || {});
|
||||
if (!services.length) {
|
||||
$('rows').innerHTML = `<div class="empty">${EMPTY_SVG}<div>No services</div><p>Fleet catalog is empty on this host.</p></div>`;
|
||||
return;
|
||||
}
|
||||
$('rows').innerHTML = '<div class="table-wrap"><table><thead><tr><th>Service</th><th>Health</th><th>Floor</th><th>RTT</th><th>Instances</th><th>Power</th></tr></thead><tbody>' +
|
||||
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>
|
||||
${moreMenu([
|
||||
{ label: 'Pause replica', path: `/api/instances/${i.id}/pause` },
|
||||
{ label: 'Resume replica', path: `/api/instances/${i.id}/resume` },
|
||||
{ label: 'Restart replica', path: `/api/instances/${i.id}/restart`, confirm: `Restart ${i.id}?`, danger: true },
|
||||
])}
|
||||
</div>`;
|
||||
}).join('') || '<span class="muted">none</span>';
|
||||
const pausedAll = (sv.instances || []).length && (sv.instances || []).every((i) => i.paused);
|
||||
const power = h === 'down' || !sv.available ? 'off' : pausedAll ? 'paused' : 'on';
|
||||
const powerCls = power === 'on' ? 'good' : power === 'paused' ? 'warn' : 'bad';
|
||||
return `<tr class="${h}">
|
||||
<td><strong>${sv.id}</strong><br><span class="muted">${sv.title||''}</span></td>
|
||||
<td>${pill(h)}</td>
|
||||
|
|
@ -258,26 +346,32 @@ async function drawFleet() {
|
|||
<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>
|
||||
<span class="pill ${powerCls}" title="Status, not a toggle">${power}</span>
|
||||
${moreMenu([
|
||||
{ label: 'Start (keep floor)', path: `/api/services/${sv.id}/start` },
|
||||
{ label: 'Pause all replicas', path: `/api/services/${sv.id}/pause` },
|
||||
{ label: 'Stop (disable keepFloor)', path: `/api/services/${sv.id}/stop`, confirm: `Turn off ${sv.id}? This disables keepFloor.`, danger: true },
|
||||
])}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('') + '</tbody></table>';
|
||||
}).join('') + '</tbody></table></div>';
|
||||
}
|
||||
$('addMachine').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const f = e.target;
|
||||
const roles = [...f.querySelectorAll('input[name=role]:checked')].map((el) => el.value);
|
||||
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,
|
||||
roles: roles.length ? roles : ['tree-node'], enabled: true,
|
||||
}),
|
||||
});
|
||||
f.reset();
|
||||
f.querySelector('input[name=role][value="tree-node"]').checked = true;
|
||||
f.querySelector('input[name=role][value="archive-worm"]').checked = true;
|
||||
await drawFleet();
|
||||
});
|
||||
async function bootTrace() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue