Admin nav refactor#7
Merged
Merged
Conversation
Turns the read-only admin dashboard into an actual operator control
surface. Four buttons on /admin, each POSTing to a CSRF-gated route
that redirects back with a flash message:
1. Deposit BTC → Thunder — shells out to grpcurl for the enforcer's
cusf.mainchain.v1.WalletService/CreateDepositTransaction, then
writes a row into the pool DB's deposits table. Assumes the
enforcer wallet already holds spendable BTC UTXOs (operator
pre-funds manually with bitcoin-cli sendtoaddress).
2. Trigger payout cycle now — POSTs to a new admin HTTP surface
added to the payout worker; runs runOnce() synchronously and
returns the tick result.
3. Remove stale Thunder tx — form with txid input, calls Thunder's
remove_from_mempool RPC. Fixes the class of stuck states where
a phantom tx pins the reserve UTXO and every subsequent transfer
hits "utxo double spent".
4. Nudge Thunder mine — POSTs Thunder's mine RPC. Same idempotent
nudge the systemd timer runs, exposed for manual use.
Security:
- CSRF: per-render random token, single-use, 1h TTL, in-memory store.
HTTP Basic auth alone doesn't defend against a malicious cross-site
POST that piggybacks the cached auth header — the CSRF token does.
- Payout worker admin HTTP is loopback-bound by default and unauth'd;
the loopback bind is the boundary.
- Dashboard now writes to the deposits table, so the systemd unit
loses ReadOnlyPaths=@root@ in favour of ReadWritePaths=@root@/data.
Deploy:
- New env: PAYOUT_ADMIN_URL, ENFORCER_GRPC_ADDR, GRPCURL_BIN,
THUNDER_SIDECHAIN_ID on the dashboard; PAYOUT_ADMIN_BIND,
PAYOUT_ADMIN_PORT on the payout worker.
- Docker dashboard image now bakes grpcurl (v1.9.1, multi-arch via
TARGETARCH). Compose publishes payout admin port on 127.0.0.1 so
the dashboard container can reach it via host.docker.internal.
- Systemd units + docker-compose.yml + .env.example all updated.
Smoke-tested locally: payout admin HTTP returns clean JSON on
/healthz and /tick; dashboard renders 4 forms with fresh CSRF tokens;
POST without token / with a fake token gets rejected with a flash;
POST with a valid token executes the action (nudge-mine correctly
reports "fetch failed" when Thunder RPC is unreachable).
Two follow-ups after the first deploy of the write actions:
1. Deposit action's DB log crashed with "db.prepare is not a function".
Root causes:
- openDb() returns a lazy WRAPPER, not a raw better-sqlite3 handle;
we need to call .get() first to unwrap.
- The dashboard's existing handle is opened readonly:true, so even
after unwrap the INSERT into `deposits` would fail.
Fix: openDb now accepts { readonly } (default true, unchanged for the
existing read path); server.js opens a second writable handle just for
admin write actions and hands the unwrapped result to createDeposit.
Also hardens createDeposit to skip the DB log gracefully if the
handle is missing rather than throwing.
2. Operator asked for visibility into how much BTC is available to
deposit. New enforcerBalance() probe shells out to grpcurl for
cusf.mainchain.v1.WalletService/GetBalance (returns confirmed +
pending sats + hasSynced). Rendered as a new "Pool BTC (enforcer
wallet)" card next to the Thunder reserve card, and echoed inline
inside the deposit form ("Spendable now: N sats") so the operator
knows what they can spend without leaving the page.
Verified locally: syntax clean, gRPC probe against the live enforcer
returns confirmed=200977300 pending=19998800 hasSynced=true.
Turns the read-only monolithic /admin page into a real operator
console. Same functionality, but broken into focused tabs with clear
navigation between them, and a matching public-side nav bar.
Structure
---------
Public (unchanged auth: none):
GET / Overview
GET /blocks Block history
GET /worker/:name Per-worker
GET /worker-lookup?name=X Redirects to /worker/X (nav search box)
Admin (Basic auth on every route; router lives in lib/admin-router.js):
GET /admin Overview (balances + health)
GET /admin/workers Per-worker balance table
GET /admin/deposits Deposits history + New Deposit form
GET /admin/payouts Recent + in-flight + Trigger button
GET /admin/tools Nudge mine + Remove-from-mempool
GET /admin/worker/:id Per-worker audit (existed, now shares nav)
GET /admin/logout 401 clears browser Basic-auth cache
GET /admin/api/summary JSON (was /api/admin/summary)
GET /admin/api/worker/:id JSON (was /api/admin/worker/:id)
POST /admin/action/{nudge-mine,remove-from-mempool,trigger-payout,deposit}
Robustness
----------
- Central error middleware inside the admin router — any thrown
error becomes a red flash-redirect, never a leaked stack trace.
- Action forms carry a hidden `return_to` so each POST redirects back
to the page it was fired from; whitelist against a fixed set of
admin paths prevents open-redirect abuse.
- Writable SQLite handle moved to lib/db-admin.js; server.js opens
two distinct handles and only the admin router receives the
writable one, so no read path can accidentally trigger a write.
- Security headers on every response: X-Content-Type-Options nosniff,
X-Frame-Options DENY, Referrer-Policy same-origin,
Permissions-Policy interest-cohort=().
- Fmt helpers (fmtN, fmtTs, ago, fmtSats, fmtPct) centralised in
lib/fmt.js and dropped onto res.locals via middleware — views no
longer duplicate the same 15-line preamble.
Shared partials (views/partial/):
head.ejs — <head> + optional refresh + title
nav-public.ejs — public top nav + worker-name search box
nav-admin.ejs — admin top nav (5 tabs + Log out), orange bar
flash.ejs — green/red banner after any action redirect
CSS additions (public/style.css):
.top-admin (distinct color for admin surface)
.topnav / .topsearch / .logout / .flash-{ok,err}
Old dashboard/views/admin.ejs (381 lines) removed — its sections are
distributed across the five new admin-*.ejs views.
Not changed
-----------
- Auth scheme stays HTTP Basic (per operator ask). /admin/logout is
the only new user-visible endpoint that touches auth state.
- No JSON API refactor / SPA — every page is still server-rendered EJS.
- Public routes and their JSON APIs (/api/*) unchanged.
Smoke tested locally: all 7 admin routes render, security headers
present, POST actions carry the return_to through the whitelist,
/admin/logout returns 401 with a valid ASCII WWW-Authenticate header.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.