Public leaderboard + admin connectivity dashboard for the QO2 CTF, backed by the same Supabase project the sandbox writes submissions to.
| Route | Access | What it shows |
|---|---|---|
/ |
Public | Ranked leaderboard — students sorted by completed levels, with progress bars and last-submission time. |
/admin |
Password-protected | Connectivity matrix — every student × every level (done/not done), plus a list of expected students who haven't connected. Also shows the dry-run test_submissions matrix. |
/admin/logs |
Password-protected | Attendance — who started a sandbox session (session_logs), when, and from which source (eval/test), plus who is expected but hasn't entered yet. |
/api/admin/submissions |
Bearer token | JSON backing the admin page (leaderboard, roster, missing, test_leaderboard). |
/api/admin/logs |
Bearer token | JSON backing the logs page (entries, roster, not_entered). |
- The qo2 sandbox (
qo start) inserts{student_id, question_id, flag}on every passed level and a row intosession_logswhen the session starts (seepkg/database/supabase.goin the qo2 repo). Both are best-effort sends that never block or crash the sandbox. qo start -m evalwrites submissions tosubmissionsand logssource=eval;qo start -m test(practice runs withtest.enc) writes totest_submissionsand logssource=test. Practice data never lands in the final-day table.- This app reads those tables with the service role key, server-side only — the key never reaches the browser. The public leaderboard exposes counts only; flags stay out of the public response and are shown only behind the admin password.
- The admin pages derive "not connected" / "did not enter" from
EXPECTED_STUDENTS(your roster) vs. the students actually present insubmissions/session_logs. - The public page uses ISR (
revalidate = 10); the admin pages poll the API every 20s.
npm install
cp .env.example .env.local # fill in your Supabase project details
npm run devOpen http://localhost:3000 for the leaderboard and http://localhost:3000/admin for the admin view.
| Variable | Where | Required | Notes |
|---|---|---|---|
SUPABASE_URL |
server | yes | Project Settings → API → Project URL |
SUPABASE_SERVICE_ROLE_KEY |
server | yes | Server-only. Never expose publicly. |
EXPECTED_STUDENTS |
server | no | Comma-separated student IDs for the "not connected" list. |
TOTAL_LEVELS |
server | no | Fixed level count. Defaults to distinct levels found. |
ADMIN_TOKEN |
server | yes | Password protecting /admin. Empty disables the API. |
Reference schema in supabase/schema.sql (tables: users, sessions, questions,
submissions, test_submissions, session_logs). The submissions, test_submissions,
and session_logs tables must exist in your project. Indexes on student_id and
question_id are recommended.
- Push this repo to GitHub and import it into Vercel.
- Add the environment variables from above (server-only for the keys).
- Deploy. The Next.js App Router is supported natively — no config needed.
netlify.toml is included (build: next build, publish: .next, with the Next.js
plugin). Add the same environment variables and deploy.
- The service role key is used only in server components / serverless functions.
- The admin API is gated by
ADMIN_TOKEN(bearer). Change it before going live. - RLS is enabled on
submissions; reads here bypass it via the service role. If you later let browsers query Supabase directly with the anon key, add a proper RLS policy.