Course-ops dashboard for the Vibe Coding Course. Watch all. Do all. Behind the scenes. Audit everything.
A closed-loop ops brain for the Vibe Coding Course. Not a passive admin panel β Mission Control:
- Watches every signal that matters (live via Supabase Realtime)
- Detects drift / stuck students / quiet days
- Auto-creates Mission cards on the Kanban when signals trip
- You drag through
DETECTED β INVESTIGATING β FIXING β SHIPPED - Audits every action to an append-only
mc_eventslog (actor + payload + ts) - Streams it all live in the right-sidebar activity ticker
The Live Activity ticker reads the same
mc_eventsrows the audit trail uses β what you see is what's actually true.
| Layer | Choice | Why |
|---|---|---|
| Frontend | React 18 + Vite + Tailwind + framer-motion + lucide-react | Premium feel, fast iteration |
| Backend | Express (server/index.js) β tiny ops API |
Holds DISCORD_BOT_TOKEN + STRIPE_SECRET_KEY + SUPABASE_SERVICE_ROLE_KEY server-side, away from the browser |
| DB + Auth | Supabase (project yhtmuibgdnxhbgboajhc) |
Same project as Hyper-Vibe-Coding-Course β admins log straight in |
| Audit spine | mc_events β append-only, immutability-triggered |
Powers the activity feed + queryable history; service-role-only writes |
| Realtime | Supabase Realtime (postgres_changes) |
DB events arrive without polling |
| DnD | @hello-pangea/dnd |
Maintained react-beautiful-dnd fork |
| Hosting (SPA) | Vercel | Same as the course |
| Hosting (API) | Render (blueprint shipped β render.yaml) |
Vercel can't run a long-lived Node process |
git clone https://github.com/welshDog/WelshDog-Mission-Control.git
cd WelshDog-Mission-Control
npm install
cp .env.example .env.local # then fill in the secrets β see .env.example
npm run dev:full # Vite :5174 + Express :3011 side by sideRequired env vars (see .env.example for the full list with rationale):
| Var | What | Where used |
|---|---|---|
VITE_SUPABASE_URL |
Course Supabase URL | Client (SPA login + reads) |
VITE_SUPABASE_ANON_KEY |
Course Supabase anon key | Client |
VITE_ADMIN_ALLOWLIST |
Comma-separated admin emails | Client AdminAuth gate |
SUPABASE_URL |
Same as VITE_ β Express uses it too | Server |
SUPABASE_SERVICE_ROLE_KEY |
Service role β bypasses RLS for audit writes | Server only β never expose |
DISCORD_BOT_TOKEN (or DISCORD_TOKEN) |
Catch Stragglers DM delivery | Server only |
STRIPE_SECRET_KEY |
Refund Stripe charges | Server only |
MAX_GRANT_PER_CALL |
Grant Tokens hard cap (default 10000) | Server |
Then apply the migrations (via Supabase MCP apply_migration against project yhtmuibgdnxhbgboajhc β NEVER supabase db push):
supabase/migrations/20260523130000_create_mc_missions_table.sql
supabase/migrations/20260524000000_mc_events_and_missions_schema_bump.sql
The "do behind the scenes" panel. 5/6 live end-to-end.
| Button | Status | What it does |
|---|---|---|
| π©Ί Health Pulse | β live | Scans course signals (stuck students, quiet days) β auto-creates Mission cards |
| βοΈ Morning Brief | β live | 60-second summary of the last 24h |
| π€ Catch Stragglers | β live (v0.4.0) | Idle-student finder + tone-tagged DM drafter (you approve before send). Smoke-tested 2026-05-25. |
| π Grant Tokens | β live (v0.7.0) | Pick user + amount + reason β award_tokens() RPC + audit. Idempotent. |
| π Refund | β live (v0.8.0) | Stripe charge refund + matching BROski$ deduction in one click. Both sides idempotent (Stripe Idempotency-Key + spend_tokens() p_source_id). |
| π§Ή Drift Scan | Re-run the quiz true/false positional scan. Deferred until there's a drift signal to scan against. |
ADHD pacing: one new button per commit. Each ships a real working thing.
Every protected endpoint runs requireAdmin middleware (v0.6.0):
- Pulls
Authorization: Bearer <jwt>from the request - Verifies the JWT via
supabase.auth.getUser(token) - Looks up
users.roleand rejects with403if notadmin - Attaches
req.user = { id, email }so handlers stamp the verified actor intomc_events.actorβ no "trust the client payload" surface
Every mutation writes:
- An
mc_missionsKanban card (operator-visible state) - An immutable
mc_eventsrow (queryable history with structured payload)
The two are deliberately separate: state vs history. mc_events cannot be UPDATEd or DELETEd (DB triggers block it for every role, including service_role); corrections happen by INSERTing a new event with a *.corrected type.
The SPA + API split (Vercel can't run a long-lived Node process):
- SPA β Vercel β standard Vite build, already wired
- API β Render β use the included
render.yamlblueprint:- Render dashboard β New β Blueprint β connect this repo β
main - Set the 5 secrets in the Render dashboard (
sync: falseso they stay out of git) - First deploy ~3 min, then
curl https://<your-svc>.onrender.com/api/healthβ{"ok":true,...}
- Render dashboard β New β Blueprint β connect this repo β
- Wire SPA β API β two options (documented at the top of
render.yaml):- A (recommended): Vercel
rewritesinvercel.jsonβ no client code changes - B:
VITE_MC_API_URLenv + prefix everyfetch('/api/...')call
- A (recommended): Vercel
| Version | Highlights |
|---|---|
v0.9.0 (this commit) |
ActivityTicker rebuilt on mc_events realtime β spine pays off in the UI |
v0.8.0 |
Refund (Stripe + token deduction, idempotent both sides) |
v0.7.1 |
UI polish β pipeline columns + SOON badge + Kanban header spacing |
v0.7.0 |
Grant Tokens (preview + commit + idempotency via award_tokens()) |
v0.6.0 |
requireAdmin JWT middleware + emitEvent() helper + first mc_events consumer |
v0.5.0 |
mc_events spine migration (append-only, immutability triggers, realtime) + mc_missions owner + priority |
v0.4.0 |
Catch Stragglers full-panel overlay + read-phase + Express /api/send-dm |
v0.3.0 |
Pivot to course-ops β Missions Kanban + Agent Actions |
.env*files never committed (.gitignoreblocks them).DISCORD_BOT_TOKEN,STRIPE_SECRET_KEY,SUPABASE_SERVICE_ROLE_KEYare server-only β never prefixedVITE_.- Apply DB migrations via Supabase MCP
apply_migrationβ NEVERsupabase db push. mc_eventsis append-only β corrections by INSERT, never UPDATE/DELETE (triggers enforce this even for service_role).mc_missionsis RLS-locked toauthenticated; the AdminAuth allowlist gates the app client-side as defence in depth.- Stripe refunds always include an
Idempotency-Keyheader. Matchingp_source_idfeedsspend_tokens()so retries are safe both sides. git fetchbefore every push β parallel auto-commits run out-of-band.
πΆβΎοΈ Built by @welshDog β Stop apologising for your brain. Start building.