54 Commits

Author SHA1 Message Date
Dorian
5c0931e8f2 fix(docker): replace corepack with npm i -g pnpm; copy lockfile
Node 22's bundled Corepack strict-validates pnpm package signatures, and
the Portainer build host couldn't complete `corepack prepare pnpm@9.12.3
--activate` (exit 1). Reproducible failure mode behind networks that
can't reach Corepack's signing-key host or against pinned pnpm versions
whose signatures aren't yet shipped in Corepack's known list.

Switch all three stages to install pnpm via plain `npm i -g pnpm@9.12.3`,
which has none of those constraints. Also copy pnpm-lock.yaml in so
`--frozen-lockfile` actually does what it says (was previously running
`--frozen-lockfile=false` because the lockfile wasn't being copied).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:07:09 +01:00
Dorian
c56a47e2c4 feat(web): cyberpunk vue 3 dashboard with primal/amber/extension login
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>
2026-05-06 15:59:00 +01:00
Dorian
de353878f6 feat(api): nostr nip-98 login, jwt sessions, datum digest poller
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>
2026-05-06 15:58:35 +01:00
Dorian
2dc9be4678 chore: scaffold pnpm workspace, container, deploy docs
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>
2026-05-06 15:57:57 +01:00