Skip to content

feat(dashboard): #16 public network topology dashboard - #148

Merged
TickTockBent merged 3 commits into
mainfrom
worktree-issue-16-dashboard
May 12, 2026
Merged

feat(dashboard): #16 public network topology dashboard#148
TickTockBent merged 3 commits into
mainfrom
worktree-issue-16-dashboard

Conversation

@TickTockBent

Copy link
Copy Markdown
Owner

Summary

Closes #16. Adds cmd/dashboard/, a standalone Go service that polls the public REPRAM mesh and serves a single-page topology graph. Informational by design — proof-of-life plus a visible demonstration of the privacy property (structure visible, contents never).

Design v3 (full): #16 (comment)

What lands

  • Boot order: cached omega list → DNS resolution → --seeds break-glass → exit non-zero. Cache-first inversion: DNS is only the recovery path. --seeds is valid in any deployment mode (not dev-only) and surfaces as seed_override: true in the snapshot when used.
  • Trust reuse: leans on internal/trust for FetchSigned, LoadCache, SaveCache, and Refresher. No new crypto. Pubkey rotation works via cache invalidation on signature mismatch.
  • Snapshot persistence: written atomically (tmp → fsync → chmod 0644 → rename) after each successful poll. Loaded on startup with stale: true + loaded_from_disk: true so a restart never blanks the UI.
  • Privacy boundary: addresses are stripped server-side before serialization. The UI never displays IPs, ports, keys, payloads, or stored content. /v1/topology already exposes addresses publicly, so the dashboard isn't protecting them — it's choosing not to amplify them via a polished public face.
  • Side-channel guardrails: counters not rates (since-boot only), goroutines rounded to nearest 10, heap at native precision (line is acknowledged not hidden), no per-node sparklines.
  • Directional edges: each peer-awareness link kept as {from, to}. Symmetric pairs render solid; asymmetric ones render dashed — surfaces topology-sync convergence state as a free debug signal (Enhance topology sync to propagate peer lists #36).
  • Self-observability: 8 Prometheus metrics on a separate 127.0.0.1:9095 listener at /internal/metrics (polls_total, polls_failed_total, nodes_unreachable, snapshot_age_seconds, omega_refresh_unix_seconds, omega_expires_unix_seconds, omega_refresh_failures_total, geo_lookup_misses_total). Operator alerts on snapshot staleness / omega expiration distance.
  • Geo: country-level lookup pluggable behind a openMMDB hook in geo.go. Default is no-op (region = "?"); maxminddb wiring deferred until a GeoLite2 file is in place.
  • Frontend: vanilla JS, no build step, no framework. Small from-scratch SVG force-directed graph (chose 60 lines of code over 200KB of vendored vis-network for v1; can swap in if the graph outgrows it). Hackerpunk CRT aesthetic vendored from web/styles.css per project conventions.

Verified live against the burn-in cluster

./bin/repram-dashboard --seeds=10.0.20.72:18080,10.0.10.81:18080,10.0.10.104:18080 \
  --state-dir=/tmp/repram-dashboard-state \
  --listen=127.0.0.1:18181 --internal-addr=127.0.0.1:18182 --poll-interval=15s

Discovered all 3 nodes, 6 directional edges (fully symmetric), seed_override: true propagated, snapshot persisted, internal metrics exposed, restart loaded prior snapshot with stale flags set.

Make targets

  • make build-dashboard — builds bin/repram-dashboard
  • make dashboard-run-burnin — one-command smoke test against the burn-in cluster

Tests

10+ unit tests in internal/dashboard/:

  • builder: address stripping, is_root derivation, directional edge preservation, asymmetric-edge survival, goroutine rounding, seed-override propagation, uptime parsing
  • snapshot: roundtrip preserves fields, absent-file returns (nil, nil), atomic write keeps prior file intact on failure
  • poller: 3-node fake-cluster BFS walk, unreachable marking, metric parsing, seed normalization
  • geo: empty-path, missing-file, nil-IP all return "?"

Full repo suite passes (go test ./...).

Deferred (filed as follow-ups internally, not blocking this PR)

  • Real GeoLite2/maxminddb integration via the openMMDB hook + Makefile refresh target
  • vis-network swap-in if graph exceeds ~50 nodes
  • Pubkey-rotation operator-runbook doc

Test plan

  • make build-dashboard succeeds
  • make dashboard-run-burnin produces a non-empty snapshot at http://127.0.0.1:18181/api/snapshot within ~15s
  • Visit http://127.0.0.1:18181/ — graph renders, table populates, sidebar updates on node click
  • /internal/metrics on the internal port lists all 8 dashboard_* series
  • Stop and restart the dashboard — first response carries loaded_from_disk: true and stale: true
  • Stop one burn-in node, wait one poll cycle, confirm unreachable: true on that node's snapshot entry
  • go test ./internal/dashboard/... passes
  • go test ./... passes repo-wide

// ticktockbent

Standalone Go service that polls the public REPRAM mesh and serves a
single-page graph view. Informational by design — proof-of-life plus a
visible demonstration of the privacy property (structure visible,
contents never).

- Boot order: cached omega list → DNS resolution → --seeds break-glass → exit
- Reuses internal/trust for omega fetch, verify, refresh, and cache
- Snapshot persisted atomically (tmp→fsync→rename), loaded on restart
- Public listener serves /api/snapshot + embedded HTML/JS; internal
  listener (default 127.0.0.1:9095) exposes /internal/metrics for
  operator self-observability
- Addresses stripped server-side; UI shows id/enclave/region/uptime/heap
  but never IPs or stored content
- Edges directional with asymmetric pairs rendered dashed — surfaces
  topology-sync convergence state as a debug signal
- Country-level geo lookup pluggable; mmdb wiring deferred behind a
  build hook (no-op default returns "?")
- Frontend: vanilla JS + small from-scratch SVG force-directed graph,
  no build step, hackerpunk CRT aesthetic vendored from web/styles.css

Design v3: #16 (comment)

// ticktockbent
@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
repram Ready Ready Preview, Comment May 12, 2026 3:54pm

Request Review

@TickTockBent

Copy link
Copy Markdown
Owner Author

Cold review by Sonnet

Solid implementation — atomic writes, privacy boundary, and directional edges land correctly. There are real bugs in Walk's shutdown path and a metric that is never incremented; the SIGHUP geo-reload is dead code; and the boot order is inverted from the design spec.

Bugs / correctness issues (priority order)

1. Walk goroutines can hang on ctx cancellation (poller.go:165-200)
When ctx fires mid-cycle, a worker's <-ctx.Done() arm returns without decrementing pending. If that worker held the last item, pending never reaches 0, queue is never closed, and other workers block on <-queue until the shared HTTP client timeout fires. On a SIGHUP-triggered cycle followed immediately by shutdown the race window is real. The test doesn't verify all workers exited.

2. dashboard_geo_lookup_misses_total is never incremented (metrics.go:55-58, builder.go:119-122)
Registered, documented in the design, but geoLookupMissesTotal.Inc() is called nowhere. Will read 0 forever. Builder needs the counter passed in and Inc() called when Region() returns "?".

3. dashboard_snapshot_age_seconds is only ever set to 0 (orchestrator.go:327)
The design says alert on this > 300s. It's set to 0 on success and never updated between cycles. A 4-minute-stale snapshot reports 0. Needs a background tick or computed-at-scrape-time value.

4. OmegaRefreshedAt is set to time.Now() at poll time, not omega-fetch time (orchestrator.go:303-306)
Every 60s cycle resets refreshed := time.Now(). The displayed "omega refreshed N ago" will always say "just now" regardless of when the cache was actually written. Needs the cache mtime or a fetchedAt field on SignedList.

Design-implementation mismatches

Boot order is inverted from v3 design (orchestrator.go:135-176)
Design specifies: (1) cache → (2) DNS → (3) seeds → (4) exit. The code checks --seeds before cache (lines 135-139 short-circuit before any cache/DNS attempt). An operator supplying --seeds bypasses a still-valid cache — the opposite of the intended break-glass semantics.

SIGHUP does not hot-reload GeoLite2 (main.go:85, geo.go:70)
Design v3: "SIGHUP hot-reloads GeoLite2 DB." SignalHUP() triggers omega refresh only. geo.Reload() is implemented correctly but is dead code — never called anywhere.

No UPSTREAM.md for vendored assets (cmd/dashboard/web/)
Design v3 requires an UPSTREAM.md recording upstream URL, license, and sha256 next to any vendored file. The impl dropped vis-network for a hand-rolled SVG (reasonable), but there's no supply-chain record and no Makefile target.

Smells worth addressing

fetchText has no body size limit (poller.go:295): io.ReadAll against a malicious node is unbounded. A node returning 100MB of "Prometheus metrics" exhausts memory. io.LimitReader(resp.Body, 1<<20) is a one-liner.

Node IDs injected into innerHTML (app.js:103-111): n.id, n.enclave, and n.region go directly into tr.innerHTML. A compromised node returning <script>…</script> as its ID gets rendered. Blast radius is the dashboard UI only, but it's a stored-XSS path. Use textContent/appendChild consistently, as the banners panel already does.

BFS is unbounded (poller.go:129): visited has no cap. A malicious node returning 10,000 fabricated peers causes 10,000 dial attempts. A cap on visited size (e.g., 1,000 nodes) bounds it.

pollCtx deadline equals PollInterval (orchestrator.go:263): a long graph walk could run past 60s; the ticker fires the next cycle before the first completes. Two concurrent cycles then race on rootsConsecutiveMisses. The cycle budget and the next-cycle timer shouldn't share the same value.

What looks good

Stale-snapshot preservation on empty cycle (orchestrator.go:316-323): re-storing the prior snapshot with Stale=true rather than overwriting with an empty result is exactly right. Simple, no special-casing needed in the HTTP layer.

SaveSnapshot write ordering (snapshot.go:156-175): Chmod on the open handle before Close, then Rename. Correctly avoids the window where a reader sees wrong permissions on the final file.

TestPollerWalksFullCluster uses real httptest servers, not mocks — catches real BFS termination, real response parsing, and real concurrency. Honest test coverage.

Directional edges as a debug signal (builder.go:141-153, app.js:163-173): asymmetric edges dashed in the UI is a free partition indicator from topology sync's eventual-consistency. Right call, small cost.

Bottom line

Iterate: the boot-order inversion and dead geo hot-reload are design mismatches that ship wrong behavior; the XSS path and unbounded BFS should be fixed before public exposure.

// claude-sonnet-cold-review

Boot order, timestamp accuracy, side-channel hardening, and frontend XSS
prevention — all from @claude-sonnet-cold-review on PR #148.

Bugs:
- OmegaRefreshedAt now reflects actual fetch time (cache file mtime on
  warm start, time.Now() on DNS fetch / refresher update), not the
  current poll cycle's clock
- dashboard_geo_lookup_misses_total wired through builder — was
  registered but never incremented
- dashboard_snapshot_age_seconds converted to NewGaugeFunc so it ages
  between cycles instead of being pinned at 0 on success

Design realignment:
- Boot order corrected to cache → DNS → seeds → exit. --seeds is now
  the last-resort break-glass per v3 design, not a first-pick override
  that ignored a still-valid cache
- SIGHUP now hot-reloads the GeoLite2 DB in addition to triggering an
  omega refresh — geo.Reload() was dead code before
- Documented (in app.js) the deliberate choice to ship a from-scratch
  SVG graph instead of vendoring vis-network, with the supply-chain
  note for future contributors

Security / resource:
- fetchText and fetchJSON cap responses at MaxResponseBytes (1 MiB) via
  io.LimitReader — was io.ReadAll with no cap, exploitable by any node
  serving a multi-GB body
- BFS visited map capped at MaxVisitedNodes (1024) — a malicious node
  returning thousands of fake peers can no longer drive unbounded dial
  attempts per cycle
- app.js node-table and node-detail use textContent / appendChild
  consistently instead of innerHTML interpolation — closes stored-XSS
  path via compromised node ID / region / enclave strings

Concurrency:
- Orchestrator.cycle holds cycleMu via TryLock; a slow walk no longer
  overlaps with the next ticker fire, eliminating a race on
  rootsConsecutiveMisses. Skipped cycles increment cyclesSkipped for
  visibility
- Walk's results-channel send is now ctx-guarded, so a cancelled cycle
  no longer leaks a blocked worker against a slow collector
- pollCtx budget is now PollInterval - 5s, leaving headroom for the
  next cycle's mutex acquisition

Tests:
- New orchestrator_test.go covers boot-order non-shortcircuit, cycle
  serialization (verifies cyclesSkipped increments under contention),
  and prior-snapshot-loaded-as-stale on cold start
- Existing tests updated for NewBuilder signature (now takes a geo
  miss counter)

Repo-wide `go test ./... -race` passes. Live smoke test against the
burn-in cluster confirms boot-order log message, snapshot-age ageing,
geo miss counter, and omega timestamp accuracy in seed-override mode.

// ticktockbent
@TickTockBent

Copy link
Copy Markdown
Owner Author

Thanks for the cold review — solid finds, almost all accepted. Pushed as 8d190c5.

Accepted

Finding Fix
OmegaRefreshedAt set to time.Now() per cycle orchestrator.go now records fetch time at the source (DNS fetch / refresher update / cache mtime on warm start) and threads it through applyRoots
dashboard_geo_lookup_misses_total never incremented NewBuilder now takes the counter; builder.go:119 Inc() on real-IP-misses-with-geo-configured
dashboard_snapshot_age_seconds always 0 Converted to prometheus.NewGaugeFunc computed at scrape time against an atomic lastSuccessfulPoll
Boot order inverted from v3 orchestrator.go:Boot now follows cache → DNS → seeds → exit. The dashboard log message changes accordingly: "cache and DNS unavailable; booting from N operator-supplied seeds"
SIGHUP doesn't reload geo Wired in the run-loop's HUP handler alongside the existing omega refresh trigger
No UPSTREAM.md for vendored assets Added an explicit comment block at the top of app.js documenting the from-scratch SVG choice (no vendoring done; the note flags the supply-chain requirement for whenever that changes)
fetchText unbounded io.ReadAll io.LimitReader(resp.Body, MaxResponseBytes) — 1 MiB cap. Applied to fetchJSON too, same reasoning
BFS visited map uncapped MaxVisitedNodes = 1024 enforced in the enqueue closure
innerHTML injection from node fields renderTable and renderNodeDetail rewritten to use textContent / appendChild consistently
pollCtx deadline == PollInterval Cycle now serialized via cycleMu.TryLock(); pollCtx budget is PollInterval - 5s to leave headroom; skipped cycles increment a counter

Partial-credit pushback on the Walk hang diagnosis

The exact mechanism in the review isn't quite right: a worker exiting via <-ctx.Done() never dequeued an item, so pending accounting holds, and workersWg.Wait() doesn't depend on pending either way. But there IS a real exposure adjacent to what you spotted — results <- res on poller.go:174 was unguarded by ctx, so a worker could block on send if the collector were slow and ctx fired. Buffer of 64 makes it unlikely in practice; the fix (select on ctx in the send) is trivial. Done.

New tests

  • orchestrator_test.go::TestConcurrentCyclesAreSerialized — verifies cyclesSkipped increments when a slow cycle is still in flight at the next tick
  • orchestrator_test.go::TestBootLoadsPriorSnapshotAsStale — verifies cold-start serves the disk snapshot with stale: true + loaded_from_disk: true
  • orchestrator_test.go::TestBootDoesNotShortCircuitToSeeds — asserts the applyRoots source-preservation that the boot-order code relies on

Full repo go test ./... -race passes. Live smoke against the burn-in cluster confirms:

omega DNS resolution failed: lookup _bootstrap.repram.io: no such host
cache and DNS unavailable; booting from 3 operator-supplied seeds, trust chain bypassed

→ DNS attempted before seeds, as the design specifies. dashboard_snapshot_age_seconds now ages (was 8s after an 8s wait), and dashboard_geo_lookup_misses_total correctly reports 3 (one miss per RFC1918 burn-in IP with no-op geo).

// ticktockbent

@TickTockBent

Copy link
Copy Markdown
Owner Author

Cold review #2 by Sonnet

The implementation is in better shape than round 1 — the Walk send-unguarded bug, LimitReader, textContent rewrites, and snapshot-age GaugeFunc all landed correctly. Two bugs remain: one causes a permanently-misleading UI banner in the most common cold-start scenario, and one test is named for a property it does not actually verify.

Verification of v1-review fixes

Boot order (cache → DNS → seeds → exit): Verified. orchestrator.go:161–198 follows the correct sequence. A still-valid cache short-circuits before DNS; seeds are last resort. Correct.

XSS / textContent: renderTable and renderNodeDetail use textContent/appendChild throughout. Template literals at lines 119, 145 interpolate only n.heap_mb (a float) and at line 190 (edgeKey) are used only as Set keys, never injected into the DOM. innerHTML = '' at lines 82 and 182 clears containers — no untrusted content in those assignments. Correct.

LimitReader on fetchText and fetchJSON: Both functions apply io.LimitReader(resp.Body, MaxResponseBytes)poller.go:304 and poller.go:326. Correct.

cycleMu serialization: TryLock at orchestrator.go:297 skips the second cycle and increments cyclesSkipped. The geo reload in the SIGHUP case happens unconditionally before the cycle call, so a skipped cycle never skips the geo reload. Correct.

cacheFileMtime() vs SaveCache race: SaveCache does an atomic rename; cacheFileMtime stats the final path. The stat either sees the old file or the new one — no torn state. Correct; not racy.

dashboard_snapshot_age_seconds: Converted to prometheus.NewGaugeFunc at metrics.go:66–75, computed from lastSuccessfulPoll at scrape time. Correct.

dashboard_geo_lookup_misses_total: Incremented at builder.go:127 when Region() returns "?" and geo is configured. Correct.

OmegaRefreshedAt set to time.Now() per cycle: Fixed. DNS-boot path uses time.Now() at fetch time (orchestrator.go:183); cache-boot path uses cacheFileMtime() (orchestrator.go:165); refresher updates use time.Now() at update time (orchestrator.go:404). Correct.

SIGHUP geo reload: Wired at orchestrator.go:257–262. geo.Reload() is called when GeoDBPath != "". Correct.

New issues introduced or revealed (priority order)

1. omega_refresh_failed is permanently true when booting via --seeds. orchestrator.go:188 calls markRefreshFailed(true) when DNS fails. If seeds then succeed (line 193), the flag is never cleared — there is no markRefreshFailed(false) on the seeds path, and since source == RootSourceSeeds, the Run() method never starts a refresher (orchestrator.go:222), so onOmegaUpdate (the only other clearing path) is never called. Every cycle will publish omega_refresh_failed: true, and the UI will permanently show the "OMEGA REFRESH FAILED" banner alongside "SEED OVERRIDE // trust chain bypassed" — two overlapping warnings where only one is meaningful. Fix: call markRefreshFailed(false) at orchestrator.go:194 (seeds path success), since in that branch the flag has no useful meaning; the SeedOverride banner already communicates that the trust chain was bypassed.

2. TestBootDoesNotShortCircuitToSeeds does not test what it claims. orchestrator_test.go:24–43. The test comment says it verifies "the v3 boot order: cache → DNS → seeds → exit." It does not — it only calls applyRoots() directly and checks that source is recorded correctly. Boot() is never called with both a valid cache and --seeds present, so the actual short-circuit prevention in Boot() is untested. The comment even admits this ("We can't run the full Boot path here without DNS infrastructure"). The test is fine as a unit test of applyRoots; it is mislabeled. Either rename it to TestApplyRootsPreservesSource or write a second test that stubs the DNS call and verifies cache-present + seeds-present → omega source selected.

3. Misleading comment in main.go:57–58. "The orchestrator owns the inversion (seeds take precedence when present)" is the opposite of what the code does. Seeds are last resort; cache takes precedence. Residue from v1's inverted-boot-order bug. Should read something like "seeds are the break-glass: used only when cache and DNS both fail."

Things still worth pushing on

TestBootLoadsPriorSnapshotAsStale makes a live DNS attempt. orchestrator_test.go:93 calls o.Boot(context.Background()) with no DNS stub. On this machine it resolves fast (NXDOMAIN immediately), but in a CI environment where DNS times out, this test adds a 10-second wall-clock penalty. The fetchCtx timeout is 10s (orchestrator.go:176). Worth adding a t.Setenv to break DNS or passing a configurable DNS lookup func so the test is hermetic.

No CSP header on the public listener. server.go sets no Content-Security-Policy. Mitigated by the textContent discipline in app.js, but a CSP (default-src 'self'; script-src 'self') is a cheap defense-in-depth layer for a public-facing dashboard.

What looks good

The GaugeFunc for dashboard_snapshot_age_seconds is the right design — a gauge that must age between cycles cannot be incremented once per success. The closure over lastSuccessfulPoll.Load() is clean and the atomic.Int64 is the right backing store.

The geo reload locking in geo.go:85–92 (open new impl outside lock, take write lock, swap, release, close old) is correct. A concurrent Region() call might read from the old impl for one lookup after the swap, but the old impl is closed only after the lock is released and after Region() has copied the interface ref — safe in practice, and the noopLookup path handles the close gracefully.

The Walk cancellation path is correct. Workers exit via case <-ctx.Done() without touching pending; the phantom-pending scenario from the v1 review doesn't cause a hang because all workers exit via ctx, workersWg.Wait() completes, results is closed, and the collector drains. The select guard on results <- res (line 200) closes the actual gap that existed.

Bottom line

Iterate on issue #1 before merge — the permanent omega_refresh_failed banner in --seeds mode is user-visible and wrong; the one-line fix is obvious. Issues #2 and #3 are low-stakes but the misnamed test is technically misleading for the next reviewer.

// claude-sonnet-cold-review-r2

Addresses cold-review-r2 on PR #148:

- orchestrator.Boot now clears omega_refresh_failed when falling
  through to --seeds, preventing the UI from permanently double-
  bannering ("SEED OVERRIDE" + "OMEGA REFRESH FAILED") in a mode where
  the refresher never runs.
- main.go's boot-order comment rewritten — was a v1 leftover saying the
  opposite of what the code now does.
- Config gains OmegaPubkey and OmegaDNS injection points so tests can
  exercise Boot() against a stubbed trust chain instead of needing real
  DNS. Production callers leave both nil and get the baked-in defaults.
- New tests actually exercise Boot():
  - TestBootValidCacheBeatsSeeds: a fresh signed cache wins over an
    operator-supplied --seeds list (regression guard against the v1
    inverted boot order).
  - TestBootDNSBeatsSeedsWhenCacheAbsent: DNS-resolved omega list wins
    over seeds when no cache is present.
  - TestBootSeedsAreLastResort: with cache absent and DNS down, seeds
    are adopted AND omega_refresh_failed is cleared.
  - TestBootExitsWhenNothingAvailable: empty cache, dead DNS, no seeds
    → Boot returns error so the operator's start script surfaces it.
- TestBootLoadsPriorSnapshotAsStale and TestBootDoesNotShortCircuitTo
  Seeds (renamed to TestApplyRootsPreservesSource — was a unit test of
  applyRoots mislabeled as a boot-order test) are now hermetic via the
  same stub.
- Public listener now applies a defense-in-depth security-header
  middleware: Content-Security-Policy (locked to 'self' plus Google
  Fonts for the hackerpunk stylesheet), X-Content-Type-Options:nosniff,
  Referrer-Policy:no-referrer. Frame-ancestors 'none' covers
  click-jacking. The internal listener intentionally does not get
  these — it's scraped by Prometheus, not browsed.

Full repo `go test ./... -race` passes. Live smoke against the burn-in
cluster confirms omega_refresh_failed:false in seeds mode and the CSP/
nosniff headers on the public listener (and absent from the internal
listener, as intended).

// ticktockbent
@TickTockBent

Copy link
Copy Markdown
Owner Author

Thanks for round 2 — caught a real one and improved the test rigor. Pushed as aa18cd0.

Accepted

Finding Fix
omega_refresh_failed permanently true in --seeds mode orchestrator.go:198-201 calls markRefreshFailed(false) on the seeds-success path. The seed_override banner is the meaningful signal; double-bannering with a no-op refresh-failed flag was actively misleading
Stale comment in main.go:57-58 claiming "seeds take precedence" Rewritten to reflect actual semantics ("--seeds is the operator's last-resort break-glass")
TestBootDoesNotShortCircuitToSeeds doesn't actually exercise Boot() Renamed to TestApplyRootsPreservesSource and replaced with four new tests that actually call Boot() against a stubbed trust chain
TestBootLoadsPriorSnapshotAsStale makes a live DNS call Now hermetic via the same stub resolver — no more 10s CI penalty in DNS-less environments
No CSP on public listener Added a securityHeaders middleware on the public mux only: CSP locked to 'self' plus Google Fonts for the hackerpunk stylesheet, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, frame-ancestors 'none'. Internal listener intentionally does not get these — it's a Prometheus scrape target, not browsed

Test rigor improvements

To make Boot() testable without live DNS, I added OmegaPubkey ed25519.PublicKey and OmegaDNS trust.DNSConfig injection points to Config. Production callers leave both nil and get the baked-in defaults. Tests inject their own keypair and a stub TXTResolver, exactly the pattern internal/trust itself uses.

The new tests exercise all four boot steps:

  • TestBootValidCacheBeatsSeeds — a fresh signed cache wins over an operator-supplied --seeds list. This is the regression guard against the v1 inverted boot order; if that ever flips back, this test fails.
  • TestBootDNSBeatsSeedsWhenCacheAbsent — DNS-resolved omega list wins over seeds when no cache exists.
  • TestBootSeedsAreLastResort — with cache absent and DNS stub failing, seeds are adopted AND omega_refresh_failed is cleared.
  • TestBootExitsWhenNothingAvailable — empty cache, dead DNS, no seeds → Boot returns error so the operator's start script surfaces the failure.

Verified live

seed_override: True
omega_refresh_failed: False    ← was permanently True before
roots_unreachable: False

curl http://127.0.0.1:18181/
  Content-Security-Policy: default-src 'self'; script-src 'self'; ...
  Referrer-Policy: no-referrer
  X-Content-Type-Options: nosniff

curl http://127.0.0.1:18182/internal/metrics
  (no CSP — Prometheus scrape target, intentional)

Full repo go test ./... -race passes.

// ticktockbent

@TickTockBent
TickTockBent merged commit 732708f into main May 12, 2026
4 checks passed
@TickTockBent
TickTockBent deleted the worktree-issue-16-dashboard branch May 12, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add public network dashboard

1 participant