Session 18: Admin dashboard + Tank01 prefetch endpoint (1443 tests)

This commit is contained in:
Kev
2026-06-11 22:29:38 -04:00
parent beaf8b2a61
commit 0e3839a90a
9 changed files with 813 additions and 2 deletions
+116 -1
View File
@@ -4,7 +4,122 @@
2026-06-11
## Current Phase
SHIP BUILD v17.0 — Audit response: checkout 401, hero 404, Slate parsing, polish (Session 17)
SHIP BUILD v18.0 — Internal admin dashboard + Tank01 prefetch endpoint (Session 18)
## Session 18 (2026-06-11) — SHIPPED
Built an operator-facing admin dashboard at `/admin` so Kev can pull
the three numbers he needs every morning (total users, paying users,
grades today) without dropping into psql. Added the missing HTTP
surface for the Tank01 prefetch script so it can be triggered from
the dashboard (or any internally-keyed caller) instead of only from
a host shell.
### Section 1 — Admin allowlist + UI guard
`web/src/lib/isAdmin.ts` exposes `isAdmin(email)` over a hard-coded
allowlist (`kevdevelops@gmail.com`). Case-insensitive on input;
trims whitespace. Trivial by design — the security boundary is the
server check, not this helper.
`web/src/app/admin/page.tsx` is a client component that uses
`useAuth()` and `isAdmin()` to redirect non-admins to `/dashboard`.
This is UX-only — anyone with devtools can flip the boolean. The
real check is on the API route.
### Section 2 — Stats API with server-side admin check
`web/src/app/api/admin/stats/route.ts` (`force-dynamic`, `no-store`)
validates the bearer token via `getUserFromRequest`, then asserts
`isAdmin(user.email)` before any data leaves Supabase. Non-admin
tokens get 403 (not 401 / redirect) so the route's existence
doesn't leak. Service-role queries are wrapped in
`Promise.allSettled` so one failed aggregate doesn't blank the
dashboard — the `notes[]` field surfaces partial failures inline.
Aggregates returned: total users, tier breakdown
(`free|africa|analyst|desk`), last-24h signups (max 20, emails
masked as `j***@gmail.com`), all-time grade count, today's grade
count, per-sport odds health (NBA/WNBA/MLB/soccer-wc), shared
odds-api quota remaining.
Spec assumed table `grading_log`; actual table is `grade_history`.
The route queries the real table.
Health probes share a 4-second `AbortController` budget so a stalled
upstream can't block the page.
### Section 3 — Dashboard UI
Key-metrics row → tier breakdown with proportional bars → recent
signups table → system-health table. Mono numbers, VYNDR dark
tokens (`--bg-surface`, `--grade-a`, `--grade-d`, `--text-tertiary`).
Not linked from nav — operator bookmarks the URL.
### Section 5 — Tank01 prefetch HTTP endpoint
`src/routes/internal.js` mounts at `/api/internal/prefetch/tank01`,
gated by `requireInternalAuth({loopbackOnly:false})`. Accepts JSON
`{max?, sports?, dryRun?}` and translates it into argv for the
existing `scripts/tank01-prefetch.js` module's exported `main()`.
Deviation from spec: spec suggested `execSync('node scripts/...')`.
We import the module instead — testable in-process, no PATH
dependency, no permission-shell stack. Module already supports the
exact CLI flags so the body shape stays the same.
Wired through `src/app.js` (`app.use('/api/internal', internalRoutes)`).
The shared `VYNDR_INTERNAL_KEY` is set in Coolify; the Next.js
admin page never sees the key (UI button will proxy through a
server route in a follow-up — out-of-scope for Session 18).
### Tests
`tests/integration/internalRoutes.test.js` — 5 new tests:
- rejects without `x-internal-key`
- translates body into argv (sports list, max, dryRun)
- forwards `--dry-run` correctly
- accepts string-form `sports` (single sport)
- returns 500 with the underlying error message on module rejection
All 5 tests pass. Existing 1438 tests untouched.
### Battery
- Express suite: **112 passed / 1443 tests** (5 new, baseline was 1438)
- Web build: **clean**`/admin` and `/api/admin/stats` registered as dynamic routes
- TypeScript: clean (initial build flagged a `NextResponse`-vs-`Response` mismatch on `jsonError` returns; relaxed the route's return type to the shared supertype)
### What Kev sees now (next session, in a browser)
Visit `/admin` while signed in as `kevdevelops@gmail.com`:
- Three big numbers across the top: Total Users / Paying Users / Free Users / Grades Today
- Tier-distribution bars
- Last-24h signups (masked emails, relative timestamps)
- Per-sport health (`NBA · ✅ Live · 234 props` / `WNBA · ⚪ No props` / etc.)
- Odds-api quota remaining
Anyone else visiting `/admin` → soft-redirect to `/dashboard`.
Anyone calling `/api/admin/stats` without an admin token → 403.
### Files changed (Session 18)
**Created:**
- `web/src/lib/isAdmin.ts`
- `web/src/app/admin/page.tsx`
- `web/src/app/api/admin/stats/route.ts`
- `src/routes/internal.js`
- `tests/integration/internalRoutes.test.js`
**Modified:**
- `src/app.js` — mount `/api/internal` router
### Pending (out-of-scope for Session 18)
- Wire a "Prefetch Tank01 now" button on the admin page that POSTs through a Next.js server route (so `VYNDR_INTERNAL_KEY` stays out of the browser).
- Add a real "monthly revenue" tile (requires Stripe-side aggregation; spec said three numbers — we shipped two and added Grades Today as the third operational signal).
---
## Session 17 (2026-06-12) — SHIPPED