useDashboardLifecycle() orchestrates the three pieces that make the
dashboard alive:
1. Audio gesture unlock — unlockOnFirstGesture() on mount so playChime()
is ready when Phase 6 wires alert triggers.
2. Snapshot-then-WS — useSnapshot()'s isSuccess gates createDashboardWS
so we never open the WS before initial state is in TanStack cache
(per spec §10.2 race resolution). After connect+setReady the server
stops buffering and starts streaming deltas.
3. Globe ring eviction — every 5min calls useGlobeEvents.evict(now)
so expired pulse rings drop off.
Topic routing (typed switch, no `as any` — every payload narrows to a
local interface that matches the verified backend shape):
cve_new → useCveStore.push
kev_added → useKevStore.push
ransomware_victim → useRansomwareStore.push
coinbase_price → usePrices.pushTick (ISO ts → number, snake_case
volume_24h → camelCase)
earthquake → globe point + ring pulse (4s TTL)
iss_position → globe point (id='iss-current' so each tick replaces
the previous) + setQueryData(iss_position) so the
ISS panel sees the latest
wiki_itn → ticker push (source 'Wikipedia')
gdelt_spike → ticker push (source 'GDELT', headline includes
z-score and count)
space_weather/internet_outage/bgp_hijack/scan_firehose → setQueryData
to merge into snapshot (panels read from there)
heartbeat → no-op (connection liveness only)
Small bug fix in browserDriver: WebSocket.send() throws when readyState
is CONNECTING. createDashboardWS.setReady() can be called before the
socket opens (we call it immediately after snapshot resolves), so the
inner sock.send is now guarded by a readyState===OPEN check. The
onOpen handler in createDashboardWS resends init when the socket
actually opens — belt-and-suspenders.
This is the load-bearing wiring per the plan. Panels stop rendering
single snapshot rows and start growing as WS events arrive.
|
||
|---|---|---|
| .. | ||
| backend | ||
| conf | ||
| frontend | ||
| .env.example | ||
| .gitignore | ||
| Justfile | ||
| README.md | ||
| cloudflared.compose.yml | ||
| compose.yml | ||
| dev.compose.yml | ||
README.md
Monitor the Situation
Operator-grade real-time situational awareness dashboard. Single-binary Go backend, React 19 frontend, Postgres + Redis, fronted by nginx and (optionally) a Cloudflare Tunnel.
The phrase "Monitoring the situation" is a Twitter/X meme from June 2025. This is the version that actually monitors the situation.
Stack
| Layer | Tech |
|---|---|
| Backend | Go 1.22, chi router, coder/websocket, goose migrations |
| Frontend | React 19, Vite, TanStack Query, Zustand, MapLibre, D3 |
| Storage | Postgres 16 (BRIN-indexed time-series), Redis 7 |
| Ingress | nginx (dev + prod), Cloudflare Tunnel (prod) |
| Build / run | just recipes, multi-stage Docker, air for live reload |
Data sources
| Panel | Source | Cadence | Auth |
|---|---|---|---|
| Mass-scan firehose | DShield (SANS ISC) | 1h | none |
| Internet outages + BGP hijacks | Cloudflare Radar | 5m | CF_RADAR_TOKEN (Radar:Read scope) |
| CVE velocity + EPSS | NVD CVE 2.0 + FIRST EPSS | 2h | NVD_API_KEY (optional, raises rate limit) |
| CISA KEV | CISA KEV catalog | 1h | none |
| Ransomware victims | ransomware.live | 15m | none |
| Live BTC + ETH ticks | Coinbase Advanced Trade WS | persistent | none |
| Earthquakes (M2.5+) | USGS GeoJSON feed | 1m | none |
| Space weather | NOAA SWPC (5 endpoints) | 1m / 3h | none |
| World events | Wikipedia ITN + GDELT v2 API | 5m / 15m | none |
| ISS position | wheretheiss.at + CelesTrak | 10s / 24h | none |
| IP enrichment | GreyNoise Community | on-demand | GREYNOISE_API_KEY (optional, free tier) |
Quickstart (development)
cp .env.example .env
# fill .env: POSTGRES_PASSWORD, JWT_SECRET, NOTIFICATION_ENCRYPTION_KEY
just dev-start
just migrate-dev
open http://localhost:8432
JWT signing keys auto-generate at backend/keys/private.pem on first boot. The dev stack binds host ports 8432 (nginx) / 5432 (backend) / 4432 (postgres) / 6432 (redis) / 3432 (vite).
Smoke checks
curl -s http://localhost:8432/api/v1/healthz
curl -s http://localhost:8432/api/v1/snapshot | jq .
docker run --rm -i --network host ghcr.io/vi/websocat:latest \
"ws://localhost:8432/api/v1/ws?topics=heartbeat"
Production (Cloudflare Tunnel)
cp .env.example .env
# fill production secrets including CLOUDFLARE_TUNNEL_TOKEN
just tunnel-start
just migrate
Tests
cd backend && go test -race ./...
Layout
backend/ Go services (cmd/api, internal/{events,bus,ws,snapshot,collectors,...})
frontend/ React 19 dashboard
conf/ nginx and per-environment Docker configs
migrations/ goose SQL migrations (mounted into the backend container)