server: status host labels — notable names doing things (#698)
* server: status host labels become notable-name-action pairs
box-1 -> a stable whimsical label ("hypatia-sketching") hashed from the
machine id: notable names real and fictional x actions that deliberately
avoid the dashboard's status vocabulary (no sleeping/resting — a box is
not an instance). No index; derivation is deterministic per box and the
hash direction means the label leaks nothing. Dry-run pins a fixture
machine id so committed fixtures are stable across dev machines.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: finish the label rename in status.md
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
6 files changed,
+48
−13
deploy/status-publish.sh+41 −6docs/status.md+3 −3test/status-contract.test.mjs+1 −1test/status-fixtures/fleet-at-capacity.json+1 −1test/status-fixtures/fleet-getting-full.json+1 −1test/status-fixtures/fleet.json+1 −1
diff --git a/deploy/status-publish.sh b/deploy/status-publish.sh| index 73c2a4d..c2e0b6d 100755 |
| --- a/deploy/status-publish.sh |
| +++ b/deploy/status-publish.sh |
| @@ -23,8 +23,9 @@ |
| # (plenty / getting-full / at-capacity; comfortable / busy / critical). |
| # The caps, ratios, free GB, and percentages are read internally to pick |
| # the word and MUST NEVER reach the JSON — no remaining-slot counts, no GB, |
| -# no percentages, no cap values. Host labels are anonymous ordinals |
| -# ("box-1"), never peer URLs, IPs, or hostnames. |
| +# no percentages, no cap values. Host labels are whimsical name-action |
| +# pairs hashed from the machine id ("ada-stargazing") — stable per box, |
| +# never peer URLs, IPs, hostnames, or the machine id itself. |
| # |
| # books.json — AGGREGATED INCOME ONLY. Active-subscription count (by tier — |
| # tier names are the public price book), MRR total in EUR, gross collected |
| @@ -181,6 +182,34 @@ load_word() { case "$1" in 0) echo comfortable ;; 1) echo busy ;; *) echo critic |
| # ---- fleet.json -------------------------------------------------------------- |
| +# box_label derives a stable, whimsical, anonymous name for this box — |
| +# "ada-stargazing" — from a hash of the machine id. Notable names, real and |
| +# fictional (history, literature, myth, science, pop culture), paired with an |
| +# action. The hash direction only (id -> label) is published, so the label |
| +# leaks nothing; the action list deliberately avoids the dashboard's own |
| +# status vocabulary (no sleeping/waking/resting — a box is not an instance). |
| +box_label() { |
| + local names=(ada alan grace marie nikola leonardo cleopatra homer sappho |
| + darwin einstein curie turing hopper tesla gandalf frodo sherlock watson |
| + ahab ishmael gatsby scout atticus hermione matilda mowgli merlin arthur |
| + morgana beowulf odysseus penelope achilles athena apollo freya thor loki |
| + amelia boudicca cyrano quixote sancho ophelia viola rosalind puck oberon |
| + titania darcy pip scrooge nemo phileas zorro heidi galileo kepler newton |
| + archimedes hypatia confucius rumi frida vincent wolfgang ludwig clara) |
| + local actions=(stargazing juggling whistling painting sailing gardening |
| + baking skating humming doodling fishing knitting strolling yodeling |
| + beekeeping fencing waltzing sketching brewing climbing cartwheeling |
| + birdwatching pondering inventing exploring composing conducting |
| + spelunking kayaking orienteering puppeteering tinkering) |
| + local idfile=${STATUS_MACHINE_ID_FILE:-/etc/machine-id} |
| + local seed |
| + seed=$( { cat "$idfile" 2>/dev/null || hostname; } | sha256sum | cut -c1-16) |
| + # two independent hex slices -> indices; bash arithmetic handles hex via 16# |
| + local n=$(( 16#${seed:0:8} % ${#names[@]} )) |
| + local a=$(( 16#${seed:8:8} % ${#actions[@]} )) |
| + printf '%s-%s' "${names[$n]}" "${actions[$a]}" |
| +} |
| + |
| build_fleet() { |
| local out=$1 now total systemd_n hib awake_json asleep_json versions |
| now=$(now_iso) |
| @@ -283,12 +312,14 @@ build_fleet() { |
| # Mesh membership isn't enabled on this box, and the status-publisher's S3 |
| # key is scoped to the status bucket, so it couldn't list mesh heartbeats |
| # anyway — multi-box joins here when the mesh goes live (it will need a read |
| - # grant on the mesh prefix). Labels are ALWAYS anonymous ordinals ("box-1", |
| - # "box-2", …), never peer URLs, IPs, or hostnames. Per-box awake/asleep are |
| - # the fleet-wide counts (one box = the whole fleet), already public above. |
| + # grant on the mesh prefix). Labels are whimsical name-action pairs derived |
| + # from a HASH of the machine id ("ada-stargazing") — stable per box, and |
| + # never peer URLs, IPs, hostnames, or the machine id itself. Per-box |
| + # awake/asleep are the fleet-wide counts (one box = the whole fleet), |
| + # already public above. |
| local boxes |
| boxes=$(sqj "SELECT json_array(json_object( |
| - 'label', 'box-1', |
| + 'label', '$(box_label)', |
| 'awake', json('$awake_json'), |
| 'asleep', json('$asleep_json'), |
| 'load', json('$load_json')))") |
| @@ -575,6 +606,10 @@ u_str LISTEN 0 4096 /run/lchat/charlie.example.sock 12003 * 0 |
| u_str LISTEN 0 4096 /run/lchat/delta.example.sock 12004 * 0 |
| EOF |
| printf '%s\n' lchat-home lchat-push lchat-cloud lchat-litestream > "$FIXDIR/active-units.txt" # connect down on purpose |
| + # Fixed machine-id fixture so the dry-run label is deterministic everywhere |
| + # (a dev Mac has no /etc/machine-id; hostname fallback would vary per machine). |
| + echo dry-run-fixture-machine-id > "$FIXDIR/machine-id" |
| + export STATUS_MACHINE_ID_FILE="$FIXDIR/machine-id" |
| echo 42 > "$FIXDIR/replication-lag.txt" |
| # Capacity fixtures (control caps + disk + meminfo): one set per public band |
diff --git a/docs/status.md b/docs/status.md| index 1064530..0bfcc08 100644 |
| --- a/docs/status.md |
| +++ b/docs/status.md |
| @@ -32,7 +32,7 @@ aggregation boundary lives in the generator and holds by construction: |
| a box's `load` is `comfortable` / `busy` / `critical`. **Never** remaining |
| slot counts, free GB, percentages, or cap values — the caps and ratios are |
| read internally to pick the word and die in the generator. Host labels are |
| - anonymous ordinals (`box-1`), never peer URLs, IPs, or hostnames. |
| + whimsical name-action pairs hashed from the machine id (`ada-stargazing`) — stable per box; never peer URLs, IPs, hostnames, or the machine id itself. |
| - **Books: aggregated income only** (decision #19 in #550: income publishes |
| aggregated, never per-payer; invariant 12: no PII in our stores). Never |
| customer ids, emails, subscription ids, per-payer lines, or timestamps of |
| @@ -50,7 +50,7 @@ aggregation boundary lives in the generator and holds by construction: |
| { |
| "generated_at": "2026-07-11T13:00:00Z", |
| "instances": { "total": 6, "systemd": 2, "hibernating": 4, "awake": 2, "asleep": 2 }, |
| - "hosts": { "count": 1, "boxes": [ { "label": "box-1", "awake": 2, "asleep": 2, "load": "comfortable" } ] }, |
| + "hosts": { "count": 1, "boxes": [ { "label": "ada-stargazing", "awake": 2, "asleep": 2, "load": "comfortable" } ] }, |
| "capacity": { "level": "plenty" }, |
| "versions": [ { "version": "v100", "count": 3 } ], |
| "services": { "home": true, "push": true, "cloud": true, "connect": true }, |
| @@ -78,7 +78,7 @@ at-capacity outright), and memory utilization (`/proc/meminfo`). The caps are |
| read from `/data/control.env`. A box's `load` word bands memory utilization |
| the same way (`comfortable` / `busy` / `critical`, null when unknown). Only |
| the words publish — never the ratios, GB, or caps behind them. `hosts` v1 is |
| -the local box alone (`box-1`); multi-box joins when the mesh goes live. |
| +the local box alone; multi-box joins when the mesh goes live. |
| `books.json` (or `{"available": false, "reason": "…"}` when billing isn't |
| configured / Stripe is unreachable): |
diff --git a/test/status-contract.test.mjs b/test/status-contract.test.mjs| index ba0d166..011d376 100644 |
| --- a/test/status-contract.test.mjs |
| +++ b/test/status-contract.test.mjs |
| @@ -116,7 +116,7 @@ await test("hosts + capacity carry only the allowed abstract schema", () => { |
| for (const b of f.hosts.boxes) { |
| assert.deepEqual(Object.keys(b).sort(), ["asleep", "awake", "label", "load"], name + ": box keys"); |
| // Labels are anonymous ordinals — never a hostname, IP, or URL. |
| - assert.match(b.label, /^box-\d+$/, name + ": box label must be an ordinal"); |
| + assert.match(b.label, /^[a-z]+-[a-z]+$/, name + ": box label must be a name-action pair"); |
| assert.ok([null, "comfortable", "busy", "critical"].includes(b.load), |
| name + ": box load is a band word or null, got " + b.load); |
| } |
diff --git a/test/status-fixtures/fleet-at-capacity.json b/test/status-fixtures/fleet-at-capacity.json| index acf75f3..adf06c0 100644 |
| --- a/test/status-fixtures/fleet-at-capacity.json |
| +++ b/test/status-fixtures/fleet-at-capacity.json |
| @@ -1 +1 @@ |
| -{"generated_at":"2026-07-11T14:22:57Z","instances":{"total":6,"systemd":2,"hibernating":4,"awake":2,"asleep":2},"hosts":{"count":1,"boxes":[{"label":"box-1","awake":2,"asleep":2,"load":"critical"}]},"capacity":{"level":"at-capacity"},"versions":[{"version":"v100","count":3},{"version":"v101","count":2},{"version":"unknown","count":1}],"services":{"home":true,"push":true,"cloud":true,"connect":false},"replication":{"healthy":true,"lag_seconds":42}} |
| +{"generated_at":"2026-07-11T14:49:29Z","instances":{"total":6,"systemd":2,"hibernating":4,"awake":2,"asleep":2},"hosts":{"count":1,"boxes":[{"label":"hypatia-sketching","awake":2,"asleep":2,"load":"critical"}]},"capacity":{"level":"at-capacity"},"versions":[{"version":"v100","count":3},{"version":"v101","count":2},{"version":"unknown","count":1}],"services":{"home":true,"push":true,"cloud":true,"connect":false},"replication":{"healthy":true,"lag_seconds":42}} |
diff --git a/test/status-fixtures/fleet-getting-full.json b/test/status-fixtures/fleet-getting-full.json| index 0e0487b..b1089f3 100644 |
| --- a/test/status-fixtures/fleet-getting-full.json |
| +++ b/test/status-fixtures/fleet-getting-full.json |
| @@ -1 +1 @@ |
| -{"generated_at":"2026-07-11T14:22:57Z","instances":{"total":6,"systemd":2,"hibernating":4,"awake":2,"asleep":2},"hosts":{"count":1,"boxes":[{"label":"box-1","awake":2,"asleep":2,"load":"comfortable"}]},"capacity":{"level":"getting-full"},"versions":[{"version":"v100","count":3},{"version":"v101","count":2},{"version":"unknown","count":1}],"services":{"home":true,"push":true,"cloud":true,"connect":false},"replication":{"healthy":true,"lag_seconds":42}} |
| +{"generated_at":"2026-07-11T14:49:29Z","instances":{"total":6,"systemd":2,"hibernating":4,"awake":2,"asleep":2},"hosts":{"count":1,"boxes":[{"label":"hypatia-sketching","awake":2,"asleep":2,"load":"comfortable"}]},"capacity":{"level":"getting-full"},"versions":[{"version":"v100","count":3},{"version":"v101","count":2},{"version":"unknown","count":1}],"services":{"home":true,"push":true,"cloud":true,"connect":false},"replication":{"healthy":true,"lag_seconds":42}} |
diff --git a/test/status-fixtures/fleet.json b/test/status-fixtures/fleet.json| index 30a4301..6dede19 100644 |
| --- a/test/status-fixtures/fleet.json |
| +++ b/test/status-fixtures/fleet.json |
| @@ -1 +1 @@ |
| -{"generated_at":"2026-07-11T14:22:57Z","instances":{"total":6,"systemd":2,"hibernating":4,"awake":2,"asleep":2},"hosts":{"count":1,"boxes":[{"label":"box-1","awake":2,"asleep":2,"load":"comfortable"}]},"capacity":{"level":"plenty"},"versions":[{"version":"v100","count":3},{"version":"v101","count":2},{"version":"unknown","count":1}],"services":{"home":true,"push":true,"cloud":true,"connect":false},"replication":{"healthy":true,"lag_seconds":42}} |
| +{"generated_at":"2026-07-11T14:49:28Z","instances":{"total":6,"systemd":2,"hibernating":4,"awake":2,"asleep":2},"hosts":{"count":1,"boxes":[{"label":"hypatia-sketching","awake":2,"asleep":2,"load":"comfortable"}]},"capacity":{"level":"plenty"},"versions":[{"version":"v100","count":3},{"version":"v101","count":2},{"version":"unknown","count":1}],"services":{"home":true,"push":true,"cloud":true,"connect":false},"replication":{"healthy":true,"lag_seconds":42}} |