Boards: private, scoped rankings over the global leaderboard (API/MCP-first)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 25/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- typescript
Research direction
Start by settling the six open questions in the issue, especially the per-owner cap and the privacy level, since they set the ranking behaviour. Then read server/src/services/ArenaMemberService.ts as the pattern for the new BoardService.ts, and LEADERBOARD_CANDIDATE_SQL plus rankWithOwnerCap in server/src/services/AppService.ts for getBoard. Done means the board endpoint and MCP tools return only member bots, with the existing LeaderboardEntry shape and the privacy rule kept.
Written by the indexing model from the issue text.
Description
Terminology
- Leaderboard — the single, global bot ladder ranking (
GET /api/leaderboard, MCPleaderboard). Unchanged by this issue. - Board — the new entity: a private, named collection of apps that renders its own scoped ranking. "Board" is the term used everywhere in the new APIs, MCP tools, tables, and UI, to keep it clearly distinct from the global leaderboard.
Context
The global bot ladder (GitHub #151) gives every eligible app a single, public Elo rating and one worldwide leaderboard. There's no way for a group — a classroom, a friend group, a company team — to run a private, scoped ranking among just their own bots.
This adds boards: a named collection of apps that renders its own ranking, scoped to just those bots. It's deliberately lightweight — no brackets, no rounds, no separate rating. A user creates a board (getting an unguessable UUID), shares that id, and anyone who has it can add their own bot(s). The board lives at GET /api/board/<uuid> — the same row shape as the global leaderboard, just filtered to members. Minting/adding is API/MCP-first (no heavy management UI); viewing gets a shareable link at /board/<uuid>.
Standings model: reuse the global rating. A board is just a filter over the existing global ladder — each app is ranked by the global rating it already earns from auto-play. No new matchmaking and no new prod load. Independent per-board Elo is explicitly deferred (see Phase 2).
Intended outcome: a teacher runs create_board, hands out the id, students add their bot, and everyone watches a private /board/<uuid> ranking that only shows their cohort.
Design (MVP)
Data model — two new tables, mirroring arena_member
Follow the lazy-DDL singleton service pattern (CREATE TABLE IF NOT EXISTS + idempotent ADD COLUMN IF NOT EXISTS backfill at module load), exactly like server/src/services/ArenaMemberService.ts.
board
id UUID PRIMARY KEY -- the shareable capability (the <uuid> in the URL)
ownerUserId UUID
name text
createdTimestamp timestamp default CURRENT_TIMESTAMP
board_member
boardId UUID
appId UUID
createdTimestamp timestamp default CURRENT_TIMESTAMP
PRIMARY KEY (boardId, appId) -- an app may belong to many boards
New service: server/src/services/BoardService.ts (create/get/getForOwner + membership add/remove/getForBoard/getForApp). Model structurally on ArenaMemberService. Exclude DEMO_USER_ID as everywhere else.
Ranking — reuse the global rating, no new state
The scoped ranking is LEADERBOARD_CANDIDATE_SQL (server/src/services/AppService.ts:32-40) filtered to members:
... FROM app JOIN account ON account.id = app.userId
WHERE NOT app.deleted
AND NOT COALESCE(app.broken,false)
AND COALESCE(app.ratingGames,0) > 0
AND app.userId <> $DEMO
AND app.id IN (SELECT appId FROM board_member WHERE boardId = $id)
ORDER BY app.rating DESC, app.ratingGames DESC
Add AppService.getBoard(boardId, limit, viewerUserId?) that reuses the existing rankWithOwnerCap (AppService.ts:50-63), colorForAppId, the 24h movement rewind (rankedMatchService.deltasSince), and returns the existing LeaderboardEntry[] DTO (AppService.ts:95-110, the shared row shape) — including the privacy convention (real appId only on the viewer's own rows; every other row is name + ownerName + color). No wire-shape changes; the UI already renders this DTO generically.
- Per-owner cap caveat: the global leaderboard caps each owner at
MAX_APPS_PER_OWNER_ON_BOARD = 3viarankWithOwnerCap. In a small board you probably want to see all of an owner's bots. Decide whether a board uses an uncapped ranker (see Open questions).
Membership / privacy — capability-based
"Private" = unguessable UUID capability, matching the existing add-by-reference / arena /watch share-link convention (spectating and add-by-reference stay open by design). Anyone holding the board id can add their own app (ownedApp gate) and can read the board. No invite/approval flow in the MVP.
MCP tools — server/src/api/mcp.ts (inside buildServer, before registerResources)
Mirror the existing registerTool(name, config, handler) pattern with ok()/fail() wrappers and zod inputSchema. Add a helper ownedBoard(user, boardId) next to ownedArena (mcp.ts:145-178). The existing global leaderboard tool stays unchanged.
| Tool | Annotation | Input | Models on | Notes |
|---|---|---|---|---|
create_board |
WRITE | { name } |
create_arena |
per-user cap; returns { boardId, name } |
add_app_to_board |
WRITE | { boardId, appId } |
add_app_to_arena |
ownedApp gate; inserts member row; awards the join badge |
remove_app_from_board |
WRITE | { boardId, appId } |
remove_app_from_arena |
ownedApp gate |
list_boards |
READ_ONLY | {} |
— | boards the user owns and/or has apps in |
board |
READ_ONLY | { boardId } |
leaderboard (mcp.ts:298-313) |
capability read; returns LeaderboardEntry[] (parallels the global leaderboard tool) |
Badge — one catalog entry + one call (system already exists, GitHub #121)
In server/src/util/achievements.ts: add export const ACCOUNT_BOARD = 'account-board' (near :120-124) and one account-scope entry in ACHIEVEMENTS[] (e.g. name "Ranked Up", icon 🏅, description "Add a bot to a board."). In the add_app_to_board handler, fire-and-forget void awardEdgeAchievement(user.getId(), ACCOUNT_BOARD) — same pattern as ACCOUNT_MCP_TOKEN at server/src/api/oauth.ts:102. No UI badge work: GET /api/profile ships the whole catalog and profilePage.tsx renders it generically. (Badge is optional — easy to drop.)
REST + UI — shareable read link
- REST: new router
server/src/api/board.ts, mounted inserver/src/index.tsalongside the leaderboard router.GET /api/board/:boardIdwithauth(false)(capability-based, mirroringGET /api/leaderboardinserver/src/api/leaderboard.ts) returning{ name, entries }. (Create/add/remove stay MCP-first; REST write routes are an optional follow-up.) - UI: route
GET /board/:boardIdinui/src/App.tsxreusing the genericui/src/page/leaderboard/leaderboardPage.tsxcomponent — parameterize its fetch URL and add a heading with the board's name.LeaderboardEntry(ui/src/types/leaderboardEntry.ts) is unchanged, so movement arrows / medals / own-row bolding all work as-is. - SEO:
/board/:boardIdis capability-private →noindex(extend the resolver inserver/src/util/seo.ts; do not add to the sitemap).
Files touched (summary)
- New:
server/src/services/BoardService.ts;server/src/api/board.ts. - Edit:
server/src/services/AppService.ts(addgetBoard);server/src/api/mcp.ts(5 tools +ownedBoard);server/src/util/achievements.ts(constant + catalog entry);server/src/index.ts(mount router);server/src/util/seo.ts(noindex the route);ui/src/App.tsx(route) + a thin wrapper aroundleaderboardPage.tsx.
Open questions (to revise before implementing)
- Per-owner cap on a board — keep
MAX_APPS_PER_OWNER_ON_BOARD = 3or show all of an owner's bots within a board? (Leaning: uncapped for boards.) - Caps — max boards per user; max members per board; minimum members before the ranking renders.
- Moderation — does the board owner need remove-member / rename / delete-board? (MVP has none beyond an app owner removing their own app.)
- Privacy level — capability-by-UUID only, or owner-controlled invite/allowlist? Owner names are visible on a board (as on the global leaderboard) — acceptable inside a "private" group?
- Empty-until-played — new bots created for a board sit at 1500 until the global ladder plays them, so a fresh board may look flat. Acceptable for MVP, or does this push us toward Phase 2 sooner?
- Keep the badge? — the add-to-board badge is optional; decide whether it's worth a catalog slot.
Phase 2 (deferred — not in this issue)
Independent per-board Elo + matchmaking (a true private ladder): a board_entry { boardId, appId, rating, ratingGames, ratingWins } table, board-scoped match recording, and a matchmaking loop that plays a board's members against each other, load-gated on the small prod box. util/elo.ts:updateRatings is already pure and reusable against any {rating, games} pair, so the rating math carries over. This is the heavy branch and is intentionally out of MVP scope.
Verification (when implemented)
- Unit: service tests mirroring
server/test/services.test.ts(membership CRUD;getBoardscoping — only members appear, ordering, per-owner-cap decision, viewer-own-appIdprivacy). MCP tool tests for create/add/remove/read + the badge award (idempotent, once per user). - End-to-end via MCP:
create_board→add_app_to_boardtwo owned apps →boardshows exactly those (once they haveratingGames > 0) → confirm the badge appears onGET /api/profile. - UI: load
/board/<uuid>, confirm it renders only member bots with the board's name and that the route isnoindex. cd server && npm run build && npm test;cd ui && npm run build && npm test.
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 0
- Avg merge
- 8m
- Merged PRs (30d)
- 8
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from LeeAdcock/robocodeJs.com
-
priority: medium tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
LeeAdcock/robocodeJs.com#411 ·
Maintainers usually reply within 1 day
-
enhancement priority: medium
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
LeeAdcock/robocodeJs.com#254 ·
Maintainers usually reply within 1 day
-
good first issue priority: low tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
LeeAdcock/robocodeJs.com#146 ·
Maintainers usually reply within 1 day
-
dependencies priority: low tech-debt
Difficulty 1/5 Under an hour Newbie friendliness 15/100
LeeAdcock/robocodeJs.com#413 ·
Maintainers usually reply within 1 day
-
priority: low tech-debt
Difficulty 3/5 Half a day Newbie friendliness 32/100
LeeAdcock/robocodeJs.com#412 ·
Maintainers usually reply within 1 day
All issues in LeeAdcock/robocodeJs.com
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
db-ux-design-system/core-web#8461 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Cannot run an integration on windows when power shell is selected as the default profile terminalOpenPriority/Blocker Type/Bug
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
wso2/product-integrator#2767 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
severity:low
Difficulty 1/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day