Session 18: Admin dashboard + Tank01 prefetch endpoint (1443 tests)
This commit is contained in:
+116
-1
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user