Add operator console (Fleet, Trace, Docs) with screenshots
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
Unify loopback ops, hop testing, and reading order. Overview lists high-level docs first, then architecture, then the rest. Credits: Scott Lindsey, George Lambert, NATS.IO, Grok-Code by Grok.com.
This commit is contained in:
parent
a32c91475c
commit
cf4ef9e3af
17 changed files with 457 additions and 143 deletions
|
|
@ -12,6 +12,8 @@ operator --HTTP 127.0.0.1:3850--> fleet control
|
||||||
replicas on loopback health ports
|
replicas on loopback health ports
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Operator console (Fleet · Trace · Docs): http://127.0.0.1:3850/ — see `packages/verae-fleet/docs/CONSOLE.md`.
|
||||||
|
|
||||||
- **list** — `node src/cli.js list`
|
- **list** — `node src/cli.js list`
|
||||||
- **monitor** — `serve` probes `/health` and reconciles
|
- **monitor** — `serve` probes `/health` and reconciles
|
||||||
- **restart** — crash or 503 → same instance id respawned
|
- **restart** — crash or 503 → same instance id respawned
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
Summaries, NATS contracts, and message flows for every Verae Time × Zapier module.
|
Summaries, NATS contracts, and message flows for every Verae Time × Zapier module.
|
||||||
|
|
||||||
**Live catalog:** https://zapier.georgelambert.org/
|
**Live catalog:** https://zapier.georgelambert.org/
|
||||||
|
**Overview (start here):** https://git.georgelambert.org/marchon/overview
|
||||||
|
**Operator console:** http://127.0.0.1:3850/ (Fleet · Trace · Docs)
|
||||||
**Monorepo:** https://git.georgelambert.org/marchon/master-zapier-plan-draft (`main` and `master`)
|
**Monorepo:** https://git.georgelambert.org/marchon/master-zapier-plan-draft (`main` and `master`)
|
||||||
|
|
||||||
## Git repos (Forgejo on NS1)
|
## Git repos (Forgejo on NS1)
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
**Job:** Service catalog, per-service configs, monitor, restart, pause/on/off, keep tree-node replica floor.
|
**Job:** Service catalog, per-service configs, monitor, restart, pause/on/off, keep tree-node replica floor.
|
||||||
|
|
||||||
**Config:** `fleet.json` (min/max) + `machines.json` (hosts) + `services/<id>.json`. Monitor shows message RTT min/avg/p50/p90.
|
**Config:** `fleet.json` (min/max) + `machines.json` (hosts) + `services/<id>.json`. Operator console at `:3850` (Fleet · Trace · Docs).
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Each runtime piece is its **own git repo** on Forgejo (`git.georgelambert.org`,
|
||||||
| **verae-archive-worm** | `packages/verae-archive-worm` | Bloom WORM node |
|
| **verae-archive-worm** | `packages/verae-archive-worm` | Bloom WORM node |
|
||||||
| **verae-archive-aggregator** | `packages/verae-archive-aggregator` | Merge archive replies |
|
| **verae-archive-aggregator** | `packages/verae-archive-aggregator` | Merge archive replies |
|
||||||
| **verae-tree-node** | `packages/verae-tree-node` | Merkle leaf proofs (bulk summaries) |
|
| **verae-tree-node** | `packages/verae-tree-node` | Merkle leaf proofs (bulk summaries) |
|
||||||
| **verae-fleet** | `packages/verae-fleet` | Catalog, min copies, pause/restart, SSH hosts |
|
| **verae-fleet** | `packages/verae-fleet` | Catalog, min copies, pause/restart, SSH hosts, operator console |
|
||||||
| **verae-zapier-simulator** | `packages/verae-zapier-simulator` | Trace console before `zapier-platform push` |
|
| **verae-zapier-simulator** | `packages/verae-zapier-simulator` | Trace console before `zapier-platform push` |
|
||||||
| **zapier-user-docs** | `packages/zapier-user-docs` | Customer signup → register → lookup |
|
| **zapier-user-docs** | `packages/zapier-user-docs` | Customer signup → register → lookup |
|
||||||
| **zapier-docs-master** | `packages/docs-master` | Per-module `SUMMARY.md` + `NATS.md` |
|
| **zapier-docs-master** | `packages/docs-master` | Per-module `SUMMARY.md` + `NATS.md` |
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ Uptime is **NATS durability + fleet replica floors + more than one machine**, no
|
||||||
| archive-aggregator, job-poller, webhook-deliver | 1 | yes |
|
| archive-aggregator, job-poller, webhook-deliver | 1 | yes |
|
||||||
| zappier-edge, middleware-http | 1 | yes |
|
| zappier-edge, middleware-http | 1 | yes |
|
||||||
|
|
||||||
If a tree node is paused, crashes, or fails `/health`, fleet **starts another copy** until three are available. Monitor: http://127.0.0.1:3850/ (green / yellow / red rows).
|
If a tree node is paused, crashes, or fails `/health`, fleet **starts another copy** until three are available. Operator console: http://127.0.0.1:3850/ (Fleet tab — green / yellow / red; Trace tab for hop tests; Docs tab for reading order).
|
||||||
|
|
||||||
## Restart and pause
|
## Restart and pause
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
# Documentation index
|
# Documentation index
|
||||||
|
|
||||||
Pointers into the rest of the documentation. Live HTML catalog: https://zapier.georgelambert.org/
|
Read **high-level first**, then **architecture**, then the rest. Live catalog: https://zapier.georgelambert.org/
|
||||||
|
|
||||||
## This overview
|
Operator console (loopback): http://127.0.0.1:3850/
|
||||||
|
|
||||||
| Topic | File |
|
## 1. High-level (non-technical)
|
||||||
|-------|------|
|
|
||||||
| TOC / intro | [README.md](README.md) |
|
|
||||||
| System | [01-system.md](01-system.md) |
|
|
||||||
| Modules and repos | [02-modules-and-repos.md](02-modules-and-repos.md) |
|
|
||||||
| NATS 3-server cluster | [03-nats-cluster.md](03-nats-cluster.md) |
|
|
||||||
| Uptime | [04-uptime.md](04-uptime.md) |
|
|
||||||
| Local network failures | [05-network-failures.md](05-network-failures.md) |
|
|
||||||
| Address routing | [06-address-routing.md](06-address-routing.md) |
|
|
||||||
| External search / storage | [07-external-resources.md](07-external-resources.md) |
|
|
||||||
| Diagrams | [08-diagrams.md](08-diagrams.md) |
|
|
||||||
| Expansion template | [09-expansion-template.md](09-expansion-template.md) |
|
|
||||||
|
|
||||||
## Catalog (HTML)
|
| Document | What it is |
|
||||||
|
|----------|------------|
|
||||||
|
| [README.md](README.md) | Overview (start here) |
|
||||||
|
| [01-system.md](01-system.md) | What runs where, in plain language |
|
||||||
|
| [02-modules-and-repos.md](02-modules-and-repos.md) | Short names of every independent repo |
|
||||||
|
| [zapier-docs-master](https://git.georgelambert.org/marchon/zapier-docs-master) | One-line SUMMARY + NATS per module |
|
||||||
|
| [zapier-user-docs](https://git.georgelambert.org/marchon/zapier-user-docs) | Signup → register → lookup |
|
||||||
|
|
||||||
|
## 2. Architecture
|
||||||
|
|
||||||
| Document | URL |
|
| Document | URL |
|
||||||
|----------|-----|
|
|----------|-----|
|
||||||
|
| NATS cluster | [03-nats-cluster.md](03-nats-cluster.md) |
|
||||||
|
| Uptime | [04-uptime.md](04-uptime.md) |
|
||||||
|
| Network failures | [05-network-failures.md](05-network-failures.md) |
|
||||||
|
| Address routing | [06-address-routing.md](06-address-routing.md) |
|
||||||
|
| External resources | [07-external-resources.md](07-external-resources.md) |
|
||||||
|
| Diagrams | [08-diagrams.md](08-diagrams.md) |
|
||||||
| Catalog home | https://zapier.georgelambert.org/ |
|
| Catalog home | https://zapier.georgelambert.org/ |
|
||||||
| Composition | https://zapier.georgelambert.org/docs/02-architecture/composition.html |
|
| Composition | https://zapier.georgelambert.org/docs/02-architecture/composition.html |
|
||||||
| NATS gateway | https://zapier.georgelambert.org/docs/02-architecture/nats-gateway.html |
|
| NATS gateway | https://zapier.georgelambert.org/docs/02-architecture/nats-gateway.html |
|
||||||
|
|
@ -29,6 +32,13 @@ Pointers into the rest of the documentation. Live HTML catalog: https://zapier.g
|
||||||
| Module NATS map | https://zapier.georgelambert.org/docs/02-architecture/modules-and-nats.html |
|
| Module NATS map | https://zapier.georgelambert.org/docs/02-architecture/modules-and-nats.html |
|
||||||
| Tree nodes | https://zapier.georgelambert.org/docs/02-architecture/tree-nodes.html |
|
| Tree nodes | https://zapier.georgelambert.org/docs/02-architecture/tree-nodes.html |
|
||||||
| Fleet | https://zapier.georgelambert.org/docs/02-architecture/fleet.html |
|
| Fleet | https://zapier.georgelambert.org/docs/02-architecture/fleet.html |
|
||||||
|
|
||||||
|
## 3. Remaining documentation
|
||||||
|
|
||||||
|
| Document | URL |
|
||||||
|
|----------|-----|
|
||||||
|
| Expansion template | [09-expansion-template.md](09-expansion-template.md) |
|
||||||
|
| Operator console | https://git.georgelambert.org/marchon/verae-fleet/src/branch/main/docs/CONSOLE.md |
|
||||||
| User guide | https://zapier.georgelambert.org/user-docs/README.md |
|
| User guide | https://zapier.georgelambert.org/user-docs/README.md |
|
||||||
| Tree-node lookup (users) | https://zapier.georgelambert.org/packages/zapier-user-docs/09-lookup-tree-nodes.html |
|
| Tree-node lookup (users) | https://zapier.georgelambert.org/packages/zapier-user-docs/09-lookup-tree-nodes.html |
|
||||||
| Zapier developer setup | https://zapier.georgelambert.org/docs/04-activate/SETUP-ZAPIER-DEVELOPER.html |
|
| Zapier developer setup | https://zapier.georgelambert.org/docs/04-activate/SETUP-ZAPIER-DEVELOPER.html |
|
||||||
|
|
@ -48,8 +58,8 @@ In **zapier-docs-master**: `modules/<package>/SUMMARY.md` and `NATS.md` for ever
|
||||||
|
|
||||||
| Surface | URL |
|
| Surface | URL |
|
||||||
|---------|-----|
|
|---------|-----|
|
||||||
| Fleet monitor | http://127.0.0.1:3850/ |
|
| Operator console (Fleet · Trace · Docs) | http://127.0.0.1:3850/ |
|
||||||
| Zapier simulator | http://127.0.0.1:3847/ |
|
| Standalone simulator | http://127.0.0.1:3847/ |
|
||||||
|
|
||||||
## A–Z subject index
|
## A–Z subject index
|
||||||
|
|
||||||
|
|
@ -68,3 +78,8 @@ In **zapier-docs-master**: `modules/<package>/SUMMARY.md` and `NATS.md` for ever
|
||||||
| SSH hosts | verae-fleet machines.json |
|
| SSH hosts | verae-fleet machines.json |
|
||||||
| Zapier never NATS | 01-system |
|
| Zapier never NATS | 01-system |
|
||||||
| zappier-edge | metering, portal |
|
| zappier-edge | metering, portal |
|
||||||
|
| operator console | http://127.0.0.1:3850/ ; verae-fleet docs/CONSOLE.md |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Design: **Scott Lindsey**, **George Lambert**, **NATS.IO**, and **Grok-Code** by [Grok.com](https://grok.com).
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,59 @@ Zapier → HTTPS `zappier-edge` → HTTPS `verae-middleware` → **NATS cluster*
|
||||||
| [zapier-docs-master](https://git.georgelambert.org/marchon/zapier-docs-master) | Per-module SUMMARY + NATS |
|
| [zapier-docs-master](https://git.georgelambert.org/marchon/zapier-docs-master) | Per-module SUMMARY + NATS |
|
||||||
|
|
||||||
Clone any of them: `git clone ssh://git@git.georgelambert.org:2223/marchon/<name>.git` (SSH port **2223**).
|
Clone any of them: `git clone ssh://git@git.georgelambert.org:2223/marchon/<name>.git` (SSH port **2223**).
|
||||||
|
|
||||||
|
## Operator console (loopback)
|
||||||
|
|
||||||
|
Fleet, message **Trace**, and **Docs** share one shell at http://127.0.0.1:3850/ (`verae-fleet`). Not public. See [verae-fleet CONSOLE.md](https://git.georgelambert.org/marchon/verae-fleet/src/branch/main/docs/CONSOLE.md).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Where to read next
|
||||||
|
|
||||||
|
Start at the top. Architecture is second. Everything else is reference.
|
||||||
|
|
||||||
|
### 1. High-level (non-technical)
|
||||||
|
|
||||||
|
| Document | What it is |
|
||||||
|
|----------|------------|
|
||||||
|
| **This overview** | Whole system in one place |
|
||||||
|
| [Modules and repositories](02-modules-and-repos.md) | Short names of every independent repo |
|
||||||
|
| [zapier-docs-master](https://git.georgelambert.org/marchon/zapier-docs-master) | One-line SUMMARY + NATS per module |
|
||||||
|
| [zapier-user-docs](https://git.georgelambert.org/marchon/zapier-user-docs) | Signup → register a hash → lookup |
|
||||||
|
|
||||||
|
### 2. Architecture
|
||||||
|
|
||||||
|
| Document | What it is |
|
||||||
|
|----------|------------|
|
||||||
|
| [NATS 3-server cluster](03-nats-cluster.md) | Who may connect; private URLs |
|
||||||
|
| [Uptime](04-uptime.md) | Replica floors, restart, SSH hosts |
|
||||||
|
| [Local network failures](05-network-failures.md) | Reconnect, bloom silence, failover |
|
||||||
|
| [Address routing](06-address-routing.md) | Unplanned functions as new subjects |
|
||||||
|
| [External resources](07-external-resources.md) | Chain, WORM, tree-node search |
|
||||||
|
| [Diagrams](08-diagrams.md) | SVG + mermaid |
|
||||||
|
| [Fleet](https://zapier.georgelambert.org/docs/02-architecture/fleet.html) | Operator replica control |
|
||||||
|
| [Module NATS map](https://zapier.georgelambert.org/docs/02-architecture/modules-and-nats.html) | Address table |
|
||||||
|
| [Archive / bloom](https://zapier.georgelambert.org/docs/02-architecture/archive-nats.html) | Multipart and multi-receipt |
|
||||||
|
| [Tree nodes](https://zapier.georgelambert.org/docs/02-architecture/tree-nodes.html) | Bulk Merkle leaves |
|
||||||
|
| [Composition](https://zapier.georgelambert.org/docs/02-architecture/composition.html) | zappier + middleware HTTPS |
|
||||||
|
|
||||||
|
### 3. Remaining documentation
|
||||||
|
|
||||||
|
| Document | What it is |
|
||||||
|
|----------|------------|
|
||||||
|
| [Documentation index](INDEX.md) | A–Z and catalog URLs |
|
||||||
|
| [Expansion template](09-expansion-template.md) / [verae-nats-process](https://git.georgelambert.org/marchon/verae-nats-process) | Copy this to add an address |
|
||||||
|
| [Operator console](https://git.georgelambert.org/marchon/verae-fleet/src/branch/main/docs/CONSOLE.md) | Screenshots of Fleet / Trace / Docs |
|
||||||
|
| [Simulator](https://git.georgelambert.org/marchon/verae-zapier-simulator) | Standalone trace (also inside the console) |
|
||||||
|
| [Zapier developer setup](https://zapier.georgelambert.org/docs/04-activate/SETUP-ZAPIER-DEVELOPER.html) | CLI / login / push |
|
||||||
|
| Live catalog | https://zapier.georgelambert.org/ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Design: **Scott Lindsey**, **George Lambert**, **NATS.IO**, and **Grok-Code** by [Grok.com](https://grok.com).
|
||||||
|
|
|
||||||
BIN
packages/overview/screenshots/console-docs.png
Normal file
BIN
packages/overview/screenshots/console-docs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
packages/overview/screenshots/console-fleet.png
Normal file
BIN
packages/overview/screenshots/console-fleet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 211 KiB |
BIN
packages/overview/screenshots/console-trace.png
Normal file
BIN
packages/overview/screenshots/console-trace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
|
|
@ -35,7 +35,7 @@ node src/cli.js ssh-check ns1 # marchon@70.88.205.138 with ~/.ssh/id_ed25519
|
||||||
|
|
||||||
Private keys stay on disk (`~/.ssh/id_ed25519`); git stores only the path. Optional overrides: `machines.secrets.json` (gitignored).
|
Private keys stay on disk (`~/.ssh/id_ed25519`); git stores only the path. Optional overrides: `machines.secrets.json` (gitignored).
|
||||||
|
|
||||||
The UI shows message-processing **min / avg / p50 / p90** RTT per service, instance, and machine.
|
Operator console (Fleet · Trace · Docs) at http://127.0.0.1:3850/ — see [docs/CONSOLE.md](docs/CONSOLE.md). Message-processing **min / avg / p50 / p90** RTT is on the Fleet tab.
|
||||||
|
|
||||||
Zapier cloud apps are listed but **not spawned**. NATS on NS1 is **monitored only** (loopback `:4222`, never a public bind).
|
Zapier cloud apps are listed but **not spawned**. NATS on NS1 is **monitored only** (loopback `:4222`, never a public bind).
|
||||||
|
|
||||||
|
|
|
||||||
28
packages/verae-fleet/docs/CONSOLE.md
Normal file
28
packages/verae-fleet/docs/CONSOLE.md
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Operator console
|
||||||
|
|
||||||
|
Loopback UI at **http://127.0.0.1:3850/** — Fleet, Trace (simulator), and Docs in one shell. Not a public site. NATS stays private; SSH identity files stay on disk.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/verae-fleet
|
||||||
|
node src/cli.js serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tabs
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| **Trace** | In-process Zapier simulator: run a step, hop log, monitors/suggestions |
|
||||||
|
| **Docs** | Reading order: overview → module summaries → architecture → remaining docs |
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Design tokens match the zappier portal/admin (indigo shell). Customer portal is unchanged.
|
||||||
|
|
||||||
|
See [overview](https://git.georgelambert.org/marchon/overview).
|
||||||
BIN
packages/verae-fleet/docs/screenshots/console-docs.png
Normal file
BIN
packages/verae-fleet/docs/screenshots/console-docs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
packages/verae-fleet/docs/screenshots/console-fleet.png
Normal file
BIN
packages/verae-fleet/docs/screenshots/console-fleet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 211 KiB |
BIN
packages/verae-fleet/docs/screenshots/console-trace.png
Normal file
BIN
packages/verae-fleet/docs/screenshots/console-trace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
|
|
@ -3,146 +3,333 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title>Verae fleet monitor</title>
|
<title>Verae operator console</title>
|
||||||
<style>
|
<style>
|
||||||
:root { --ink:#12202c; --muted:#5b6d78; --line:#d5dee4; --bg:#f3efe8; --ok:#0f6e56; --err:#a32020; --warn:#8a5a00; }
|
:root {
|
||||||
* { box-sizing: border-box; }
|
--bg:#f4f5fb; --panel:#fff; --ink:#171a26; --muted:#6b7186; --line:#e5e7f0;
|
||||||
body { margin:0; font:14px/1.45 system-ui,sans-serif; color:var(--ink); background:var(--bg); }
|
--accent:#4f46e5; --accent-ink:#fff; --soft:#eef0fe;
|
||||||
header { background:var(--ink); color:#f6f3ee; padding:.9rem 1.1rem; }
|
--ok:#047857; --ok-bg:#e3f6e8; --warn:#8a5a00; --warn-bg:#fff3bf;
|
||||||
header h1 { margin:0; font-size:1.1rem; }
|
--err:#dc2626; --err-bg:#f9d4d4; --radius:12px;
|
||||||
header p { margin:.35rem 0 0; color:#c5d0d8; font-size:13px; }
|
--shadow:0 1px 2px rgba(23,26,38,.05), 0 8px 24px rgba(23,26,38,.06);
|
||||||
main { padding:1rem; max-width:1280px; margin:0 auto; }
|
}
|
||||||
h2 { font-size:13px; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); margin:1.2rem 0 .4rem; }
|
* { box-sizing:border-box; }
|
||||||
table { width:100%; border-collapse:collapse; background:#fff; }
|
body { margin:0; font:14px/1.45 -apple-system,"Segoe UI",sans-serif; background:var(--bg); color:var(--ink); }
|
||||||
th, td { text-align:left; padding:.4rem .5rem; border-bottom:1px solid var(--line); font-size:13px; vertical-align:top; }
|
header.app {
|
||||||
th { font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); }
|
background:linear-gradient(160deg,#312e81 0%,#4f46e5 60%,#7c74f0 100%);
|
||||||
tr.state-operational { background:#e3f6e8; }
|
color:#eef0fe; padding:1rem 1.25rem 0; display:flex; flex-wrap:wrap; gap:.75rem 1.5rem; align-items:flex-end;
|
||||||
tr.state-degraded { background:#fff3bf; }
|
}
|
||||||
tr.state-down { background:#f9d4d4; }
|
header.app h1 { margin:0; font-size:1.2rem; }
|
||||||
.pill { font:700 10px system-ui; letter-spacing:.06em; text-transform:uppercase; padding:.12rem .35rem; border-radius:4px; }
|
header.app p { margin:.2rem 0 .8rem; font-size:13px; opacity:.85; max-width:42rem; }
|
||||||
.good { background:#c8efd4; color:var(--ok); }
|
nav { display:flex; gap:.25rem; }
|
||||||
.bad { background:#f3c0c0; color:var(--err); }
|
nav button {
|
||||||
.warn { background:#ffe08a; color:var(--warn); }
|
border:0; background:transparent; color:#eef0fe; padding:.55rem .9rem; border-radius:10px 10px 0 0;
|
||||||
button { margin-right:.25rem; font:650 12px system-ui; border:1px solid var(--line); background:#fff; border-radius:5px; padding:.25rem .45rem; cursor:pointer; }
|
font:650 13px system-ui; cursor:pointer; opacity:.8;
|
||||||
code { font:12px ui-monospace,Menlo,monospace; }
|
}
|
||||||
.inst { font:12px ui-monospace,Menlo,monospace; margin:.15rem 0; }
|
nav button.on { background:var(--bg); color:var(--accent); opacity:1; }
|
||||||
.rtt { font:12px ui-monospace,Menlo,monospace; white-space:nowrap; }
|
main { padding:1rem 1.25rem 2.5rem; max-width:1280px; margin:0 auto; }
|
||||||
form.add { display:flex; flex-wrap:wrap; gap:.4rem; margin:.5rem 0 1rem; align-items:end; }
|
.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 { 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; }
|
||||||
|
button.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 label { font-size:11px; color:var(--muted); display:flex; flex-direction:column; gap:.15rem; }
|
||||||
form.add input, form.add select { padding:.35rem .45rem; border:1px solid var(--line); border-radius:5px; font:13px ui-monospace,Menlo,monospace; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header class="app">
|
||||||
<h1>Verae fleet monitor</h1>
|
<div>
|
||||||
<p>Spread replicas across machines. Track message-processing RTT (min / avg / p50 / p90) for capacity planning. Paused copies do not count toward the tree-node floor.</p>
|
<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>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<p id="meta"></p>
|
<section id="fleet" class="panel on">
|
||||||
<h2>Machines</h2>
|
<p id="meta" class="muted"></p>
|
||||||
<form class="add" id="addMachine">
|
<h2>Machines</h2>
|
||||||
<label>id <input name="id" required placeholder="ns1-b"/></label>
|
<form class="add" id="addMachine">
|
||||||
<label>host <input name="host" required placeholder="10.0.0.12"/></label>
|
<label>id <input name="id" required placeholder="ns1-b"/></label>
|
||||||
<label>kind
|
<label>host <input name="host" required placeholder="70.88.205.138"/></label>
|
||||||
<select name="kind"><option value="local">local</option><option value="ssh">ssh</option><option value="agent">agent</option></select>
|
<label>kind <select name="kind"><option value="local">local</option><option value="ssh" selected>ssh</option><option value="agent">agent</option></select></label>
|
||||||
</label>
|
<label>user <input name="user" value="marchon"/></label>
|
||||||
<label>user <input name="user" value="marchon"/></label>
|
<label>ssh port <input name="sshPort" type="number" value="22"/></label>
|
||||||
<label>ssh port <input name="sshPort" type="number" value="22"/></label>
|
<label>identity file <input name="identityFile" value="~/.ssh/id_ed25519"/></label>
|
||||||
<label>identity file <input name="identityFile" placeholder="~/.ssh/id_ed25519" value="~/.ssh/id_ed25519"/></label>
|
<label>capacity <input name="capacity" type="number" min="1" value="8"/></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>roles <input name="roles" value="*" placeholder="tree-node,archive-worm"/></label>
|
<button class="act primary" type="submit">Add machine</button>
|
||||||
<button type="submit">Add machine</button>
|
</form>
|
||||||
</form>
|
<div class="cards" id="machines"></div>
|
||||||
<table>
|
<h2>Services</h2>
|
||||||
<thead><tr><th>Machine</th><th>SSH / host</th><th>Kind</th><th>Capacity</th><th>Running</th><th>RTT ms min / avg / p50 / p90</th><th></th></tr></thead>
|
<div id="rows"></div>
|
||||||
<tbody id="machines"></tbody>
|
</section>
|
||||||
</table>
|
<section id="trace" class="panel">
|
||||||
<h2>Services</h2>
|
<div class="layout">
|
||||||
<table>
|
<aside class="trace">
|
||||||
<thead><tr><th>Service</th><th>State</th><th>min/max</th><th>available</th><th>RTT ms min / avg / p50 / p90</th><th>instances</th><th>actions</th></tr></thead>
|
<h2>Zapier step (simulator)</h2>
|
||||||
<tbody id="rows"></tbody>
|
<label class="muted">Action</label>
|
||||||
</table>
|
<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. Operator console is loopback only.</p>
|
||||||
|
<h2>High-level (start here)</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/README.md">Overview</a> — whole system in plain language</li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/02-modules-and-repos.md">Modules and repositories</a> — short names of every independent repo</li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/docs-master/README.md">docs-master</a> — one-line SUMMARY + NATS for each module</li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/user-docs/README.md">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.md">NATS.IO 3-server cluster</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/04-uptime.md">Uptime / replica floors</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/05-network-failures.md">Local network failures</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/06-address-routing.md">Address routing</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/08-diagrams.md">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.html">Composition</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/modules-and-nats.html">Module NATS map</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/archive-nats.html">Archive / bloom</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/tree-nodes.html">Tree nodes</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/docs/02-architecture/fleet.html">Fleet</a></li>
|
||||||
|
</ul>
|
||||||
|
<h2>Remaining documentation</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/INDEX.md">Full documentation index</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/overview/09-expansion-template.md">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.html">Zapier developer setup</a></li>
|
||||||
|
<li><a href="https://zapier.georgelambert.org/packages/verae-zapier-simulator/README.md">Simulator README</a></li>
|
||||||
|
<li><a href="https://git.georgelambert.org/marchon/verae-fleet">verae-fleet repo</a></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<div id="toast"></div>
|
||||||
<script>
|
<script>
|
||||||
async function j(url, opts) { const r = await fetch(url, opts); return r.json(); }
|
const $ = (id) => document.getElementById(id);
|
||||||
async function act(path) { await j(path, { method:'POST' }); await draw(); }
|
async function j(url, opts) {
|
||||||
function rttCell(r) {
|
const r = await fetch(url, opts);
|
||||||
if (!r || !r.count) return '<span class="rtt">—</span>';
|
const body = await r.json().catch(() => ({}));
|
||||||
return `<span class="rtt">${r.minMs} / ${r.avgMs} / ${r.p50Ms} / ${r.p90Ms} <span style="color:#5b6d78">n=${r.count}</span></span>`;
|
return { ok: r.ok, status: r.status, body };
|
||||||
}
|
}
|
||||||
async function draw() {
|
function toast(msg) {
|
||||||
const s = await j('/api/status');
|
const t = $('toast');
|
||||||
document.getElementById('meta').textContent = 'probe ' + (s.monitor?.t || '—') + ' · NATS ' + (s.nats?.url || '') + ' · machines ' + (s.machines || []).filter(m => m.enabled).length;
|
t.textContent = typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2);
|
||||||
document.getElementById('machines').innerHTML = (s.machines || []).map((m) => {
|
t.style.display = 'block';
|
||||||
const on = m.enabled;
|
clearTimeout(t._tm);
|
||||||
return `<tr class="${on ? (m.running > 0 ? 'state-operational' : '') : 'state-down'}">
|
t._tm = setTimeout(() => { t.style.display = 'none'; }, 8000);
|
||||||
<td><strong>${m.id}</strong><br><span style="color:#5b6d78">${m.title || ''}</span></td>
|
|
||||||
<td><code>${m.kind === 'ssh' ? (m.user || 'marchon') + '@' : ''}${m.host}${m.kind === 'agent' ? ':' + m.agentPort : ''}${m.kind === 'ssh' && m.identityFile ? ' key=' + m.identityFile : ''}</code></td>
|
|
||||||
<td>${m.kind} ${on ? '<span class="pill good">on</span>' : '<span class="pill bad">off</span>'}</td>
|
|
||||||
<td>${m.running} / ${m.capacity}</td>
|
|
||||||
<td>${m.available} avail</td>
|
|
||||||
<td>${rttCell(m.rtt)}</td>
|
|
||||||
<td>
|
|
||||||
<button onclick="act('/api/machines/${m.id}/${on ? 'disable' : 'enable'}')">${on ? 'disable' : 'enable'}</button>
|
|
||||||
${m.kind === 'ssh' ? `<button onclick="act('/api/machines/${m.id}/check')">ssh-check</button>` : ''}
|
|
||||||
</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
document.getElementById('rows').innerHTML = Object.values(s.services).map((sv) => {
|
|
||||||
const health = sv.health || 'down';
|
|
||||||
const healthLabel = health === 'operational' ? 'operational' : health === 'degraded' ? 'degraded' : 'not operational';
|
|
||||||
const healthCls = health === 'operational' ? 'good' : health === 'degraded' ? 'warn' : 'bad';
|
|
||||||
const floor = sv.belowFloor ? '<span class="pill bad">below floor</span>' : (sv.keepFloor ? '<span class="pill good">floor ok</span>' : '');
|
|
||||||
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'} pid=${i.pid || '—'} :${i.healthPort}
|
|
||||||
${i.rtt && i.rtt.count ? `rtt ${i.rtt.minMs}/${i.rtt.avgMs}/${i.rtt.p50Ms}/${i.rtt.p90Ms}` : ''}
|
|
||||||
<button onclick="act('/api/instances/${i.id}/pause')">pause</button>
|
|
||||||
<button onclick="act('/api/instances/${i.id}/resume')">resume</button>
|
|
||||||
<button onclick="act('/api/instances/${i.id}/restart')">restart</button>
|
|
||||||
</div>`;
|
|
||||||
}).join('') || '<span style="color:#5b6d78">none</span>';
|
|
||||||
return `<tr class="state-${health}">
|
|
||||||
<td><strong>${sv.id}</strong><br><span style="color:#5b6d78">${sv.title || ''}</span></td>
|
|
||||||
<td><span class="pill ${healthCls}">${healthLabel}</span></td>
|
|
||||||
<td>${sv.min} / ${sv.max} ${floor}</td>
|
|
||||||
<td>${sv.available} run=${sv.running} paused=${sv.paused}</td>
|
|
||||||
<td>${rttCell(sv.rtt)}</td>
|
|
||||||
<td>${inst}</td>
|
|
||||||
<td>
|
|
||||||
<button onclick="act('/api/services/${sv.id}/start')">on</button>
|
|
||||||
<button onclick="act('/api/services/${sv.id}/pause')">pause</button>
|
|
||||||
<button onclick="act('/api/services/${sv.id}/resume')">resume</button>
|
|
||||||
<button onclick="act('/api/services/${sv.id}/stop')">off</button>
|
|
||||||
</td>
|
|
||||||
</tr>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
document.getElementById('addMachine').addEventListener('submit', async (e) => {
|
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();
|
e.preventDefault();
|
||||||
const f = e.target;
|
const f = e.target;
|
||||||
const roles = f.roles.value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
||||||
await j('/api/machines', {
|
await j('/api/machines', {
|
||||||
method: 'POST',
|
method:'POST', headers:{'content-type':'application/json'},
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
id: f.id.value,
|
id: f.id.value, host: f.host.value, kind: f.kind.value, user: f.user.value,
|
||||||
host: f.host.value,
|
sshPort: Number(f.sshPort.value||22), identityFile: f.identityFile.value,
|
||||||
kind: f.kind.value,
|
capacity: Number(f.capacity.value||8),
|
||||||
user: f.user.value,
|
roles: f.roles.value.split(',').map(s=>s.trim()).filter(Boolean), enabled: true,
|
||||||
sshPort: Number(f.sshPort.value || 22),
|
|
||||||
identityFile: f.identityFile.value,
|
|
||||||
capacity: Number(f.capacity.value || 8),
|
|
||||||
roles,
|
|
||||||
enabled: true,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
f.reset();
|
f.reset();
|
||||||
await draw();
|
await drawFleet();
|
||||||
});
|
});
|
||||||
draw();
|
async function bootTrace() {
|
||||||
setInterval(draw, 1500);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { listServices } from './load.js';
|
import { listServices } from './load.js';
|
||||||
|
import { ACTIONS, Simulator } from '../../verae-zapier-simulator/src/pipeline.js';
|
||||||
|
|
||||||
const PUBLIC = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'public');
|
const PUBLIC = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'public');
|
||||||
|
|
||||||
|
|
@ -24,12 +25,27 @@ function json(res, code, obj) {
|
||||||
export function startControlServer(sup, mon) {
|
export function startControlServer(sup, mon) {
|
||||||
const bind = sup.loaded.control?.bind || '127.0.0.1';
|
const bind = sup.loaded.control?.bind || '127.0.0.1';
|
||||||
const port = Number(sup.loaded.control?.port || 3850);
|
const port = Number(sup.loaded.control?.port || 3850);
|
||||||
|
const sim = new Simulator();
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
const url = new URL(req.url, `http://${bind}:${port}`);
|
const url = new URL(req.url, `http://${bind}:${port}`);
|
||||||
try {
|
try {
|
||||||
if (req.method === 'GET' && url.pathname === '/health') {
|
if (req.method === 'GET' && url.pathname === '/health') {
|
||||||
return json(res, 200, { ok: true, role: 'fleet-control' });
|
return json(res, 200, { ok: true, role: 'operator-console' });
|
||||||
|
}
|
||||||
|
if (req.method === 'GET' && url.pathname === '/api/sim/actions') {
|
||||||
|
return json(res, 200, { actions: ACTIONS, state: sim.snapshot() });
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && url.pathname === '/api/sim/reset') {
|
||||||
|
sim.reset();
|
||||||
|
return json(res, 200, { ok: true, state: sim.snapshot() });
|
||||||
|
}
|
||||||
|
if (req.method === 'POST' && url.pathname === '/api/sim/run') {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const c of req) chunks.push(c);
|
||||||
|
const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
||||||
|
const result = await sim.run(body);
|
||||||
|
return json(res, 200, result);
|
||||||
}
|
}
|
||||||
if (req.method === 'GET' && url.pathname === '/api/services') {
|
if (req.method === 'GET' && url.pathname === '/api/services') {
|
||||||
return json(res, 200, { services: listServices(sup.loaded), fleetPath: sup.loaded.fleetPath });
|
return json(res, 200, { services: listServices(sup.loaded), fleetPath: sup.loaded.fleetPath });
|
||||||
|
|
@ -92,6 +108,14 @@ export function startControlServer(sup, mon) {
|
||||||
fs.createReadStream(dest).pipe(res);
|
fs.createReadStream(dest).pipe(res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (req.method === 'GET' && url.pathname.startsWith('/docs/screenshots/')) {
|
||||||
|
const dest = path.normalize(path.join(PUBLIC, '..', url.pathname));
|
||||||
|
const root = path.normalize(path.join(PUBLIC, '..'));
|
||||||
|
if (!dest.startsWith(root) || !fs.existsSync(dest)) return json(res, 404, { error: 'not found' });
|
||||||
|
res.writeHead(200, { 'content-type': 'image/png' });
|
||||||
|
fs.createReadStream(dest).pipe(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
json(res, 404, { error: 'not found' });
|
json(res, 404, { error: 'not found' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
json(res, 400, { error: err.message });
|
json(res, 400, { error: err.message });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue