Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Boards: private, scoped rankings over the global leaderboard (API/MCP-first)

Open
#314 0 comments 0 reactions 0 assignees View on GitHub

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
Domain
api, backend, frontend

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

ai-mcp enhancement

Terminology

  • Leaderboard — the single, global bot ladder ranking (GET /api/leaderboard, MCP leaderboard). 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 = 3 via rankWithOwnerCap. 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 in server/src/index.ts alongside the leaderboard router. GET /api/board/:boardId with auth(false) (capability-based, mirroring GET /api/leaderboard in server/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/:boardId in ui/src/App.tsx reusing the generic ui/src/page/leaderboard/leaderboardPage.tsx component — 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/:boardId is capability-private → noindex (extend the resolver in server/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 (add getBoard); 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 around leaderboardPage.tsx.

Open questions (to revise before implementing)

  1. Per-owner cap on a board — keep MAX_APPS_PER_OWNER_ON_BOARD = 3 or show all of an owner's bots within a board? (Leaning: uncapped for boards.)
  2. Caps — max boards per user; max members per board; minimum members before the ranking renders.
  3. Moderation — does the board owner need remove-member / rename / delete-board? (MVP has none beyond an app owner removing their own app.)
  4. 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?
  5. 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?
  6. 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; getBoard scoping — only members appear, ordering, per-owner-cap decision, viewer-own-appId privacy). 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_board two owned apps → board shows exactly those (once they have ratingGames > 0) → confirm the badge appears on GET /api/profile.
  • UI: load /board/<uuid>, confirm it renders only member bots with the board's name and that the route is noindex.
  • 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

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from LeeAdcock/robocodeJs.com

All issues in LeeAdcock/robocodeJs.com

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.