Node's fetch wraps the underlying network error in `.cause`; the bare
`err.message` is just "fetch failed" which tells us nothing about
DNS vs connection refused vs network unreachable. Add formatErr() that
walks .cause and includes its .code, plus the url being attempted, so
logs distinguish between the actual failure modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-facing port unified at 1337 — vibes-aligned and easier to remember
than 8080/8420. Updates: api default PORT, .env.example, docker-compose
mapping (1337:1337), healthcheck target, Dockerfile EXPOSE, Vite dev
proxy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend in @gashboard/web (Vue 3 + Vite + Pinia + TS, ~1.2k LoC):
Auth flow:
- Two signing paths: NIP-07 browser extension (Alby/nos2x/Primal
extension via window.nostr) and NIP-46 remote signer (Primal
app, Amber, nsecbunker via bunker:// URI).
- applesauce-signers lazy-loaded only on bunker login so users
with NIP-07 don't pay the cost.
- NIP-98 event built client-side, posted to /api/auth/login,
JWT persisted in localStorage. Pinia auth store handles
login/logout/state restore on reload.
Dashboard (composes the live /api/datum/stats poll, 5s):
- PoolHero — combined hashrate as the headline number,
block height, subscribed count, accepted/rejected shares.
- LotteryWidget — rotating self-deprecating odds copy
("you're 0.3× as likely to find a block as get hit by
lightning today"). Uses ~720 EH/s as the network-hashrate
constant (TODO: fetch live).
- ShareTicker — SVG sparkline of the last 60 polls.
- MinerCard ×N — nickname (QU4CK/P1XEL/N4N0/M1N1), live
hashrate, last share, lifetime tickets, reject %, status
glow (green hashing / amber stale / red idle), affectionate
roast subtitle per ASIC type.
- BlockCelebration — full-screen overlay with celebration
copy. Dormant for now (Datum's lastBlockFoundAt isn't
surfaced yet); preview via window.gashboardCelebrate().
Cyberpunk theme:
- Pure CSS vars, no Tailwind. Dark bg, neon cyan/magenta
accents, monospace, glow shadows.
- Optional CRT scanlines toggle (persists to localStorage).
- Mobile-aware grid breakpoints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend in @gashboard/api (Express 5 + TS, ~1.2k LoC):
Auth (NIP-98 over HTTP, lifted from indeehub pattern):
- Client signs a kind-27235 event with method+URL, base64s as
Authorization: Nostr <event>. Server verifies sig, freshness
(±120s), method/URL tags via constant-time string compare.
- npub allowlist decoded to hex once at boot, fail-closed if any
entry is malformed or list is empty.
- HS256 JWT sessions returning {token, npub, expiresAt}.
- express-rate-limit on POST /api/auth/login (10/min/IP).
Datum integration (the trickier half):
- HTTP Digest *SHA-256* client (community-fork Datum uses sha-256,
not md5; node has no first-class support — hand-rolled in
digest.ts: parse challenge → ha1=sha256(user:realm:pw),
ha2=sha256(method:URI), response=sha256(...) → retry).
- HTML parsers for /clients (per-worker) and /threads (auth-less
fallback) using node-html-parser.
- Profile matcher: UserAgent contains "NerdQAxe" → NerdQAxe;
else worker-name suffix on auth username → workerNameMatchers.
Live UA strings observed: NerdQAxe self-IDs; Bitaxe / Avalon
Nano 3 / Avalon Mini 3 all report cgminer/4.11.1, must match
via workername.
- 5s poll interval, 10s AbortController timeout per upstream call,
in-memory snapshot, /api/datum/stats + SSE /api/datum/stream.
Hardened-by-default Express setup:
helmet CSP (frame-ancestors 'none', script-src 'self'),
pino with redaction (auth header, *.password, *.token, *.jwt,
*.sig), AppError class + central errorHandler, zod env validation,
graceful shutdown on SIGTERM/SIGINT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-app pnpm workspace for the gashboard (mining dashboard) project:
@gashboard/api (Express 5 + TS) and @gashboard/web (Vue 3 + Vite + TS).
Shared tsconfig.base.json. Multi-stage Dockerfile (node:22.12-alpine,
non-root, healthchecked) and docker-compose.yml ready to deploy as a
Portainer Stack on Umbrel — joins umbrel_main_network so it can reach
the Datum container directly. .env.example documents every var; README
covers the Portainer deploy flow and the security posture.
Note: Dockerfile has a TODO marker to SHA256-pin the base image before
shipping to production.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>