尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Real-time judging system on MongoDB. Judges score teams on a weighted rubric, Change Streams trigger an aggregation pipeline that re-ranks the leaderboard and ▎ push it live over SSE, and an LLM writes team feedback when the event closes. Node.js, Express, native driver.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A live event judging and leaderboard system built on MongoDB.
Judges score teams at a live event, and the leaderboard updates in real time for everyone watching. When an event closes, an LLM reads each team's judge comments and writes short feedback for that team.
Stack: Node.js + Express 5, the MongoDB native driver (no Mongoose), MongoDB Atlas,
plain HTML/CSS/JS, Server-Sent Events, node --test.
git clone [email protected]:mihirr-spec/ScoreStream.git
cd ScoreStream
npm install
cp .env.example .env # Windows PowerShell: Copy-Item .env.example .env
Fill in .env:
| Key | What to put |
|---|---|
MONGODB_URI | Atlas connection string (Atlas → Connect → Drivers). Also allow your IP under Network Access. |
DB_NAME | e.g. judging_system |
ADMIN_USERNAME | Admin login name (optional, defaults to admin) |
ADMIN_PASSWORD | Any password you choose for the admin page |
JWT_SECRET | A long random string (e.g. node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") |
AI_API_KEY | Your OpenRouter key. Leave it empty to use the offline MOCK feedback. |
AI_BASE_URL | https://openrouter.ai/api/v1 |
AI_MODEL | Main free OpenRouter model, e.g. nvidia/nemotron-3-ultra-550b-a55b:free |
AI_FALLBACK_MODEL | Used if the main model fails twice, e.g. gemini-3.5-flash-lite (optional) |
AI_FALLBACK_BASE_URL | Optional different provider for the fallback, e.g. https://generativelanguage.googleapis.com/v1beta/openai (Google Gemini) |
AI_FALLBACK_API_KEY | Key for that provider (free Gemini key from aistudio.google.com). Empty = use AI_API_KEY |
PORT | 3000 |
Then:
npm run setup-db # collections + $jsonSchema validators + indexes (safe to re-run)
npm run dev # start the server (auto-restarts when you edit files)
Open http://localhost:3000 for the landing page, then Log in (one screen, three roles):
| Role | How to log in | What they get |
|---|---|---|
| Admin | Create an account on the Administrator tab, or use ADMIN_USERNAME + ADMIN_PASSWORD from .env (sees every event) | Dashboard and Events list. Inside each event: invite judges by their judge code and teams by their team code, watch judging progress live, rankings, AI feedback, and End Event. Registered admins only see their own events |
| Judge | Register on the Judge tab (name, password) to get a permanent judge code. Sign in with judge code + password. | Accept or decline invitations, then per event rate the teams one at a time (stars + feedback, repo and slides at hand), then "Submit & next". Scoring locks when the admin ends the event |
| Team | Register on the Team tab (team name, members, password) to get a permanent team code. Sign in with team code + password. | Accept or decline invitations, then per event: share a GitHub repo and slides, live rank, average, every judge's marks and comments, and AI feedback once the event ends |
How teams and judges join an event: the admin enters their code, which creates a pending invitation. They see a pop-up on their dashboard ("admin wants to add you to event") and accept or decline. Only accepting adds them, and the admin's event page updates live when they do. Duplicates are impossible: one account per team, and unique indexes allow each team in an event only once and only one open invitation per team per event.
Ending an event closes scoring, recomputes the final ranking and writes AI feedback for every team: the model merges the judges' short notes (e.g. "nice UI, laggy", "hgih latency") into one written review.
Excel: "Invite from Excel" reads a Team code column and sends one invitation per row. "Export to Excel"
downloads the event's teams with codes, ranks and averages.
Submissions: in each event a team can share its GitHub repo link and upload its slides
(PDF, PowerPoint, Word, Excel, CSV or image, up to 20 MB) until the event ends. Judges preview both inside the scoring card, and the
admin sees them in the event's team list. Slide files are stored in MongoDB with GridFS
(slides.files + slides.chunks), so no separate file server is needed and files survive redeploys.
The public leaderboard is at http://localhost:3000/leaderboard.html.
| Command | What it does |
|---|---|
npm run setup-db | Creates/updates all collections, validators and indexes |
npm run dev / npm start | Runs the server (with / without auto-restart) |
npm run explain | Prints COLLSCAN vs IXSCAN for the main queries |
npm test | Runs the API tests: the 6 required cases plus team accounts, invitations and duplicates, submissions, judge invitations and admin accounts (separate <DB_NAME>_test database) |
npm run explain -- --drop-indexes # BEFORE: drops scores indexes -> COLLSCAN, examines ALL scores
npm run setup-db # put the indexes back (important!)
npm run explain # AFTER: IXSCAN using eventId_1, examines only this event's scores
ScoreStream/
├── src/ Backend (Node.js + Express)
│ ├── server.js Entry point: connect DB, start Change Stream, listen
│ ├── app.js Express app: middleware, static files, routes, errors
│ ├── config.js Reads .env in one place
│ ├── db.js One shared MongoClient (connectDb / getDb / closeDb)
│ ├── middleware/
│ │ ├── auth.js JWT tokens, requireAdmin/Judge/Team, event ownership, sha256, scrypt passwords
│ │ ├── validate.js httpError, parseId, rubric and members validation
│ │ └── errorHandler.js JSON 404 and the central error handler
│ ├── routes/
│ │ ├── index.js Registers every router under /api
│ │ └── auth, events, teams, judges, scores, leaderboard, feedback
│ └── services/
│ ├── scoring.js Validate marks + weighted total
│ ├── leaderboard.js Aggregation pipeline ending in $merge
│ ├── changeStream.js Watches scores -> recompute -> SSE push
│ ├── sse.js Connected browsers per event
│ ├── feedback.js Writes AI feedback for every team of a closed event
│ └── ai.js OpenRouter call + fallback model + mock (only AI-specific file)
├── public/ Frontend (plain HTML, CSS, JS)
│ ├── index.html Landing page
│ ├── login.html One login screen for Admin / Judge / Team (+ team and admin sign-up)
│ ├── admin.html judge.html team.html leaderboard.html
│ ├── css/style.css Shared design system
│ ├── img/ Hero image and free-licensed Pexels photos
│ └── js/common.js Shared helpers (api, auth tokens, toasts, stars)
│ js/landing.js login.js admin.js judge.js team.js leaderboard.js
├── scripts/ One-off database scripts
│ ├── setup-db.js Collections, $jsonSchema validators, indexes
│ └── explain.js COLLSCAN vs IXSCAN report
├── tests/
│ └── api.test.js The 6 required API tests (node --test)
├── docs/
│ ├── ScoreStream_Project_Report.pdf
│ └── report/ Report source (HTML) and screenshots
├── .env.example Config keys with no secret values
├── .editorconfig .gitattributes .gitignore .nvmrc
└── package.json
| Collection | Fields | Notes |
|---|---|---|
events | name, status (draft/live/closed), rubric [{key,label,max,weight}], createdAt | Rubric is embedded: it is small and always read together with its event |
teams | eventId, name, members[] | References its event |
judgeAccounts | name, code (JG…), passwordHash, createdAt | A registered judge; the code never changes |
judgeInvitations | eventId, accountId, invitedBy, status, createdAt | Pending until the judge accepts or declines |
judges | eventId, accountId, name, joinedAt | A judge's seat in one event; scores reference it |
scores | eventId, teamId, judgeId, criteria{}, total, comment, submittedAt | References, not embedded: written often by many judges and queried on its own |
leaderboard | eventId, teamId, teamName, avgScore, judgesCount, rank, updatedAt | Written only by $merge |
feedback | eventId, teamId, summary, strengths[], improvements[], sentiment{label,score}, source, generatedAt | source is ai or mock |
Scoring formula: total = 10 × Σ (mark_i / max_i) × weight_i (out of 10).
For example, marks 8, 7, 9 with weights 0.3, 0.4, 0.3 → 7.9. avgScore is the average of total across judges.
scripts/setup-db.js), set with validationLevel: strict and validationAction: error. criteria uses additionalProperties: { bsonType: 'number' }, which allows any keys but only numbers as values. Password hashes must match the scrypt salt:hash pattern, so a plain password can never be stored by mistake. Rules a schema can't express (weights add up to 1, criteria keys match the rubric) are checked in Node.scores UNIQUE {eventId, teamId, judgeId}: the database itself blocks a second score from a judge for the same team (error 11000 → HTTP 409).scores {eventId}, teams {eventId} and judges {eventId} make the per-event queries fast.leaderboard UNIQUE {eventId, teamId} is required by $merge.judgeAccounts UNIQUE {code}; judges UNIQUE {eventId, accountId} and judgeInvitations partial unique {eventId, accountId} where status: 'pending' stop duplicate judges and invitations.feedback UNIQUE {eventId, teamId} keeps one feedback document per team (used by an upsert).teams partial unique {eventId, accountId} stops the same team account joining an event twice.invitations partial unique {eventId, accountId} where status: 'pending' allows one open invitation per team per event.teamAccounts UNIQUE {code}, admins UNIQUE {usernameLower}, submissions UNIQUE {eventId, teamId}.$objectToArray -> $unwind -> $group on the free-form criteria object.services/leaderboard.js): $match → $group ($avg, $sum) → $set ($round) → $lookup teams → → with → → into .| Method | Path | Who |
|---|---|---|
| POST | /api/auth/admin-register, /api/auth/admin-login {username, password} | public |
| POST | /api/auth/judge-register {name, password}, /api/auth/judge-login {code, password} | public |
| POST | /api/auth/team-register {name, members, password}, /api/auth/team-login {code, password} | public |
| GET, POST | /api/events | admin |
| GET, PATCH, DELETE | /api/events/:id (PATCH: status and/or rubric) | admin |
| GET | /api/events/:eventId/teams (with team code, repo link and slides info) | admin |
| DELETE | /api/teams/:id (removes the team from the event) | admin |
| GET, POST | /api/events/:eventId/invitations {code} or {codes: [...]} | admin |
| DELETE | /api/invitations/:id (withdraw a pending invitation) | admin |
| GET, POST | /api/events/:eventId/judges (GET: judges with code and progress; POST {code}: invite) | admin |
| GET | /api/events/:eventId/judge-invitations; DELETE /api/judge-invitations/:id | admin |
| DELETE | /api/judges/:id (removes the judge from the event, with their scores) | admin |
| GET | /api/teams/:id/slides (download) | admin |
| GET | /api/team/me (account, events, pending invitations) | team |
| POST | /api/team/invitations/:id {accept} | team |
| GET | /api/team/events/:eventId (rank, per-criterion averages, judges' scores, submission, feedback once closed) | team |
Errors are always { "error": "message" } with a proper status code:
Stack traces are never sent to the client.
crypto.timingSafeEqual on SHA-256 digests (equal length).JWT_SECRET and expire after 12 hours. A judge or team token also stops working if the account is deleted.live.textContent only, so user-entered names can't inject HTML..env is git-ignored. Only .env.example (keys, no secrets) is tracked.services/ai.js is the only file that knows about OpenRouter:
AI_FALLBACK_MODEL (also with one retry), then falls back to a MOCK result. Mock results have source: 'mock' and a summary starting with [MOCK - reason].npm test covers:
The tests use the MONGODB_URI from .env, but work in a separate <DB_NAME>_test database that they drop afterwards.
$unwind$setWindowFields$rank$project$mergeleaderboardservices/changeStream.js): scores.watch() filtered to insert, update, replace and delete, with fullDocument: 'updateLookup'.
live event.scores, which avoids an infinite loop.scripts/explain.js): shows COLLSCAN vs IXSCAN, keys examined and documents examined.| PUT | /api/team/events/:eventId/submission {githubUrl} | team |
| PUT, GET, DELETE | /api/team/events/:eventId/slides?name=deck.pptx (PUT body = the raw file) | team |
| GET | /api/judge/me (account, events with progress, pending invitations) | judge |
| POST | /api/judge/invitations/:id {accept} | judge |
| GET | /api/judge/events/:eventId (rubric, teams with submissions, own scores) | judge |
| GET | /api/judge/teams/:teamId/slides (download) | judge |
| POST | /api/scores {teamId, criteria, comment} | judge |
| PUT | /api/scores/:id {criteria, comment} | judge (own scores only) |
| GET | /api/events/:eventId/scores | admin |
| GET | /api/leaderboard/events (live and closed events) | public |
| GET | /api/events/:eventId/leaderboard | public |
| GET | /api/events/:eventId/stream (SSE) | public |
| POST | /api/events/:eventId/feedback/generate (event must be closed) | admin |
| GET | /api/events/:eventId/feedback | public |