feat(dashboard): #16 public network topology dashboard - #148
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Cold review by SonnetSolid 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 ( 2. 3. 4. Design-implementation mismatchesBoot order is inverted from v3 design ( SIGHUP does not hot-reload GeoLite2 ( No Smells worth addressing
Node IDs injected into BFS is unbounded (
What looks goodStale-snapshot preservation on empty cycle (
Directional edges as a debug signal ( Bottom lineIterate: 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
|
Thanks for the cold review — solid finds, almost all accepted. Pushed as 8d190c5. Accepted
Partial-credit pushback on the Walk hang diagnosisThe exact mechanism in the review isn't quite right: a worker exiting via New tests
Full repo → DNS attempted before seeds, as the design specifies. // ticktockbent |
Cold review #2 by SonnetThe 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 fixesBoot order (cache → DNS → seeds → exit): Verified. XSS / textContent: LimitReader on fetchText and fetchJSON: Both functions apply cycleMu serialization:
SIGHUP geo reload: Wired at New issues introduced or revealed (priority order)1. 2. 3. Misleading comment in Things still worth pushing on
No CSP header on the public listener. What looks goodThe The geo reload locking in The Walk cancellation path is correct. Workers exit via Bottom lineIterate on issue #1 before merge — the permanent // 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
|
Thanks for round 2 — caught a real one and improved the test rigor. Pushed as aa18cd0. Accepted
Test rigor improvementsTo make The new tests exercise all four boot steps:
Verified liveFull repo // ticktockbent |
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
--seedsbreak-glass → exit non-zero. Cache-first inversion: DNS is only the recovery path.--seedsis valid in any deployment mode (not dev-only) and surfaces asseed_override: truein the snapshot when used.internal/trustforFetchSigned,LoadCache,SaveCache, andRefresher. No new crypto. Pubkey rotation works via cache invalidation on signature mismatch.tmp → fsync → chmod 0644 → rename) after each successful poll. Loaded on startup withstale: true+loaded_from_disk: trueso a restart never blanks the UI./v1/topologyalready exposes addresses publicly, so the dashboard isn't protecting them — it's choosing not to amplify them via a polished public face.{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).127.0.0.1:9095listener 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.openMMDBhook ingeo.go. Default is no-op (region = "?"); maxminddb wiring deferred until a GeoLite2 file is in place.web/styles.cssper project conventions.Verified live against the burn-in cluster
Discovered all 3 nodes, 6 directional edges (fully symmetric),
seed_override: truepropagated, snapshot persisted, internal metrics exposed, restart loaded prior snapshot with stale flags set.Make targets
make build-dashboard— buildsbin/repram-dashboardmake dashboard-run-burnin— one-command smoke test against the burn-in clusterTests
10+ unit tests in
internal/dashboard/:Full repo suite passes (
go test ./...).Deferred (filed as follow-ups internally, not blocking this PR)
openMMDBhook + Makefile refresh targetTest plan
make build-dashboardsucceedsmake dashboard-run-burninproduces a non-empty snapshot at http://127.0.0.1:18181/api/snapshot within ~15s/internal/metricson the internal port lists all 8dashboard_*seriesloaded_from_disk: trueandstale: trueunreachable: trueon that node's snapshot entrygo test ./internal/dashboard/...passesgo test ./...passes repo-wide// ticktockbent