Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/features/01-in-app-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# In-App Notifications

## Summary

A notification inbox so users see streaks, new matches, achievements, and session events without having to poll every page.

## Why / Goal

The app already generates rich events (matches, streaks, sessions) via SSE but nothing surfaces them to the user. Notifications are the highest-leverage retention feature and the foundation for future push/mobile.

## Scope

- New `notification` table (recipient, type, actor, payload JSON, read flag, created_at)
- Notification creation hooks where events already fire (match create/remove, streak thresholds, session start/end, achievements)
- Notification router (list, mark-read, unread count, mark-all-read)
- Notification bell in the app header (sidebar) with unread badge
- Notification dropdown + full list page
- Live updates via the existing SSE/WebSocket infra

## Code map

- Events originate in: `apps/worker/src/trpc/router/match-router.ts`, `session-router.ts`
- SSE infra: `apps/worker/src/durable-objects/season-sse.ts`, `apps/worker/src/routes/sse-router.ts`
- Frontend hooks: `apps/web/src/hooks/use-session-sse.ts`
- Header/sidebar: `apps/web/src/routes/-components/layout/header.tsx`, `-components/sidebar/*`

## Acceptance criteria

- Bell shows unread count; badge updates live via SSE
- Clicking a notification navigates to the relevant entity (match/session/achievement)
- Mark single / mark all read
- Notifications generated for: new match, streak milestone, achievement earned, session started/ended

## Open questions / notes

- Scope of notifications per season vs per league vs global
- Should SSO/email notifications be added later (not in this story)
- Deletion/retention policy for notifications
36 changes: 36 additions & 0 deletions docs/features/02-achievements-showcase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Achievements Showcase & Celebration

## Summary

Surface the 13 achievement types already computed in the backend as visible, celebratory UI on profiles and league pages.

## Why / Goal

The achievement engine fully works but players never see it — profiles just say "No achievements yet". This makes progression tangible and drives engagement.

## Scope

- Achievement card grid on player profile (earned + locked states, date earned)
- League-wide achievements board (who has what, most decorated players)
- Toast/banner celebration when an achievement unlocks (leveraging `streak`/event infra)
- Achievement metadata (name, description, icon) centralized in one place
- Empty state improvements (show locked achievements so players know what to chase)

## Code map

- Engine: `apps/worker/src/services/achievement-calculation.ts`, `achievement-repository.ts`
- Router: `apps/worker/src/trpc/router/achievement-router.ts` (`getByPlayerId`)
- Profile UI: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/players/$leaguePlayerId/index.tsx`
- Achievement types list is in `achievement-calculation.ts` (5/10/15 win streak, clean sheets, redemptions, goals, season winner)

## Acceptance criteria

- Player profile shows all 13 achievement types with earned/locked state
- League achievements board renders from a single query
- Unlock fires an in-app celebration (can depend on 01-notifications)
- Achievement metadata is defined once and reused

## Open questions / notes

- `season_winner` achievement is declared but never computed — see 03-season-close-ceremony
- Iconography: use Hugeicons; need a per-type icon mapping
36 changes: 36 additions & 0 deletions docs/features/03-season-close-ceremony.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Season Close Ceremony & Champion Crowning

## Summary

A satisfying end-of-season flow: closing a season crowns the champion, awards the `season_winner` achievement (currently declared but never computed), and shows a podium/ceremony view.

## Why / Goal

Seasons can currently be closed ("Lock Season") but nothing celebrates the result. This completes the core product loop (start season → compete → crown champion → repeat) and gives players a reason to return.

## Scope

- Compute and award `season_winner` achievement to the top player/team when a season closes
- Season champion banner/podium view (leader, final standings, stats)
- Season summary page showing final placements, champion, biggest moments
- "Start next season" CTA from the ceremony view
- Ensure awards are idempotent (no duplicate achievements if closed twice)

## Code map

- Season close: `apps/worker/src/trpc/router/season-router.ts` (`updateClosedStatus`), `season-repository.ts`
- Achievement engine: `apps/worker/src/services/achievement-calculation.ts` (add `season_winner` path)
- Season detail UI: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/seasons/$seasonSlug/index.tsx`
- Close dialog: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/seasons/-components/seasons/close-season-dialog.tsx`

## Acceptance criteria

- Closing a season awards `season_winner` exactly once to the champion
- Champion/ceremony view renders with final standings
- Re-opening a closed season and re-closing does not duplicate achievements
- Pairs with 02-achievements-showcase for display

## Open questions / notes

- Champion for team-based (3-1-0) seasons: top player vs top team
- Should closing be irreversible (currently `closed` blocks match creation)? Keep existing behavior.
36 changes: 36 additions & 0 deletions docs/features/04-league-activity-feed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# League Activity Feed

## Summary

A "what happened" timeline per league/season showing recent matches, streaks, records, and milestones as they occur.

## Why / Goal

The app produces a huge amount of analytics but there is no single place to see league activity at a glance. An activity feed turns the stored match/streak data into a living story and gives the league dashboard a natural centerpiece.

## Scope

- Activity feed query aggregating recent events: matches recorded, streaks hit, achievements earned, sessions started/ended, records broken
- Feed timeline UI on the season dashboard (replacing/augmenting "Latest Match")
- Load more / pagination
- Optionally: activity feed page per league across all seasons
- Live updates via existing SSE

## Code map

- Match events: `apps/worker/src/trpc/router/match-router.ts` (+ `match-repository.ts` streak detection)
- Weekly/period stats already available: `seasonPlayer.getWeeklyStats`, `seasonTeam.getWeeklyStats`
- Season dashboard: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/seasons/$seasonSlug/index.tsx` and `-components/season/*`
- Records/upsets logic exists in MCP tools: `apps/worker/src/services/mcp-tools/tool-executors.ts`

## Acceptance criteria

- Season dashboard shows a chronological activity feed
- Feed updates live via SSE (new match/streak/achievement appears without refresh)
- Feed is paginated and performant (single aggregate query, no N+1)
- Each feed item links to the relevant match/player/achievement

## Open questions / notes

- Which event types to include in v1 (start with match + streak)
- Whether to build a dedicated activity log table or derive from existing match/streak data
37 changes: 37 additions & 0 deletions docs/features/05-team-management-crud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Team Management (Create / Edit / Roster)

## Summary

Full CRUD for teams. Today teams are only auto-created from match lineups; owners/editors cannot create named teams up front, manage rosters, or delete them.

## Why / Goal

Teams are a first-class concept (standings, profiles, rivalries) but management is invisible. Fixed teams (e.g. office pool pairs, real clubs) can't be pre-registered, and members can't be added/removed cleanly.

## Scope

- Create team with name + optional logo
- Edit team name/logo
- Add / remove players from a team roster (repo functions already exist)
- Delete team (with rules about teams that have season/match history)
- Optional: pre-register teams for a season before matches start
- Team list filters (already has "My teams" switch)

## Code map

- Router gap: `apps/worker/src/trpc/router/league-team-router.ts` (no create/delete/member procedures)
- Ready-to-use repo fns: `apps/worker/src/repositories/team-repository.ts` (`addPlayerToTeam`, `removePlayerFromTeam`)
- Teams page: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/teams/index.tsx`
- Team detail: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/teams/$teamId/index.tsx`

## Acceptance criteria

- Owner/editor can create, rename, and delete teams
- Roster add/remove works and reflects in team profile + standings
- Deleting a team with match history is blocked or shows a clear warning
- Permissions: only owner/editor (and team members for name) per existing `leagueTeam.edit` rule

## Open questions / notes

- Deleting a team that has matches: what happens to historical records (soft-delete vs block)?
- Should auto-created teams from lineups be mergeable/renamable into permanent teams?
34 changes: 34 additions & 0 deletions docs/features/06-manual-session-lineup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Manual Session Lineup Assist

## Summary

Make the `manual` rotation mode functional: it currently has no auto lineup — `computeManualLineup()` returns `null`, so the "Next Match" panel is empty.

## Why / Goal

The manual mode UI (team picker, live score sync) exists and works, but without a proposed lineup it's a weaker experience than winner-stays. This gives manual sessions a sensible default pairing while still allowing full manual override.

## Scope

- Implement lineup proposal for manual mode: pick next players who haven't played recently / haven't paired together (reuse diversity-shuffle heuristics)
- Respect team size, available (non-playing) players, queue state
- Keep the manual team picker override fully working
- Add tests for the strategy

## Code map

- Stub: `apps/worker/src/services/session/strategies/manual.ts` (`computeManualLineup` returns `null`)
- Working reference: `apps/worker/src/services/session/strategies/winner-stays.ts` + session queue logic in `apps/worker/src/services/session/session-service.ts`
- Manual UI: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/seasons/$seasonSlug/session/$sessionId/-components/manual/manual-session.tsx` and `team-picker.tsx`
- Tests: `apps/worker/src/test/trpc/` (see session tests)

## Acceptance criteria

- Starting a manual session shows a proposed lineup when enough players are available
- Proposed lineup is editable before starting the match
- Lineup logic prefers diverse pairings (no repeat partners) when possible
- Existing manual team-picker behavior is unchanged

## Open questions / notes

- Should manual mode lineups auto-advance queue state, or purely be a suggestion? (Recommend: suggestion only, winner-stays keeps the queue rules)
36 changes: 36 additions & 0 deletions docs/features/07-fixtures-points-season-ux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Fixtures & Points-Season (3-1-0) UX

## Summary

Polish the round-robin / scheduled-match experience for 3-1-0 points seasons. Fixtures are auto-generated but have minimal UI and no easy way to enter results.

## Why / Goal

3-1-0 seasons (league-style, e.g. office football) generate a full fixture schedule on creation, but users can only see a basic fixtures list and enter results one-by-one. A proper fixture UI makes points seasons competitive and easy to run.

## Scope

- Fixtures overview page: rounds, date, home/away, result state (played/pending)
- Enter/edit result directly from a fixture row (`createFromFixture` exists)
- Per-round grouping + round navigation
- Standings that update with fixture results (3-1-0 standings already exist via `seasonPlayer.getStanding`)
- Fixture re-schedule/swap (optional, stretch)

## Code map

- Fixture generation: `apps/worker/src/repositories/season-repository.ts` (circle method), triggered in `season-router.ts` `create`
- Fixture result entry: `apps/worker/src/trpc/router/match-router.ts` (`createFromFixture`)
- Fixtures component: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/seasons/-components/season/fixtures.tsx`
- Season page tabs already switch views; add fixtures tab

## Acceptance criteria

- Fixtures render grouped by round with played/pending state
- Clicking an unplayed fixture opens the score entry for that fixture
- Entering a result updates standings immediately
- Works for single-player 3-1-0 seasons (that's the mode fixtures are generated for)

## Open questions / notes

- Should fixtures be supported for ELO seasons too, or keep 3-1-0 only for now?
- Re-scheduling fixtures is likely a later iteration
32 changes: 32 additions & 0 deletions docs/features/08-standings-realtime-sse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Real-Time Standings via SSE

## Summary

Emit the `standings:update` SSE event (declared in the event type but never sent) so standings refresh live without manual reload.

## Why / Goal

Match insert/delete already broadcasts via SSE, but standings are computed separately and don't push. Two users in a session or an office league watching a match expect the table to update in real time. Small change, big perceived responsiveness.

## Scope

- Emit `standings:update` after match create / remove / session recordResult (and match score updates)
- Frontend: subscribe to the event and invalidate the standings query
- Optionally include the computed standings payload in the event to avoid a refetch

## Code map

- Event type: `apps/worker/src/durable-objects/season-sse.ts` (`SeasonSSEEvent`, `standings:update` already in union, never emitted)
- Broadcast sites: `apps/worker/src/trpc/router/match-router.ts`, `session-router.ts`
- Frontend hooks: `apps/web/src/hooks/use-session-sse.ts` (+ a season-level SSE hook)

## Acceptance criteria

- Standings table on season dashboard + session page updates live when a match is recorded
- No page reload needed
- Match removal also refreshes standings
- No N+1: event is either lightweight or carries a single precomputed standings snapshot

## Open questions / notes

- Payload design: send computed standings vs just an invalidation signal (recommend signal + refetch for simplicity)
35 changes: 35 additions & 0 deletions docs/features/09-elo-individual-vs-team.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# elo-individual-vs-team Score Type

## Summary

Wire up the `elo-individual-vs-team` score type end-to-end. The ELO engine and DB schema already support it (`WEIGHTED_TEAMS` strategy); season creation only offers `elo` or `3-1-0`.

## Why / Goal

This is the mode for games where an individual player can face a team (e.g. king of the hill, singles vs pairs, fighting games with tag formats). The heavy lifting is done — exposing it is low effort and unlocks a new game format.

## Scope

- Accept `elo-individual-vs-team` in `season.create` validation
- Team-size validation for this mode (one side size 1, other side 1..n, or defined rule)
- Verify ELO weighting math via `WEIGHTED_TEAMS` strategy and add tests
- UI: allow selecting the mode in the create-season dialog with an explanatory description
- Standings/profiles already derive from `seasonPlayer`/`seasonTeam` so should work once matches can be created

## Code map

- Engine strategy: `packages/util/src/elo-util` (`WEIGHTED_TEAMS`), used in `apps/worker/src/repositories/match-repository.ts`
- Schema enum: `apps/worker/src/db/schema/league-schema.ts` (`scoreType`)
- Validation gate: `apps/worker/src/trpc/router/season-router.ts` `create` (currently `"elo" | "3-1-0"`)
- Create-season UI: `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/seasons/-components/seasons/create-season-form.tsx`

## Acceptance criteria

- A season of type `elo-individual-vs-team` can be created and matches recorded
- ELO math produces expected outcomes in tests
- Create-season dialog exposes the mode

## Open questions / notes

- Define the exact pairing rule (fixed 1vN? any?) before implementing
- Confirm `getById`/profile pages that are ELO-only gate correctly for this type
35 changes: 35 additions & 0 deletions docs/features/10-public-shareable-leaderboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Public Shareable Leaderboard & Results Pages

## Summary

Read-only, publicly shareable league/season pages (standings, latest results) that non-members can view without signing up.

## Why / Goal

Office and friend leagues are social — people share results in chat and want to brag. Today everything requires an account + membership. A public URL per league/season is the cheapest virality + bragging-rights feature.

## Scope

- Opt-in "Make this league public" toggle in league settings (default off)
- Public route rendering standings + recent results without auth
- Share link (copy-to-clipboard) from the league page
- No mutation capabilities on public pages; strip all editing UI
- Optional: read-only "public only" exposure of a subset of analytics

## Code map

- Routes are under `_authenticated` today; a new public route tree is needed (e.g. `_public/leagues/$slug`), see `apps/web/src/routes/_public/home.tsx` for the public layout pattern
- Data sources already exist and are auth-scoped: `seasonPlayer.getStanding`, `match.getAll`
- League metadata/logo: `apps/worker/src/db/schema/league-schema.ts`

## Acceptance criteria

- Public toggle per league; when off, public URLs 404/redirect
- Public page shows standings + latest results for the active season
- Public page has no auth requirement and no write actions
- Share-link button on league page

## Open questions / notes

- Privacy: default-off; consider what data leaks via player names/ELO
- Cache-friendliness for public pages (edge caching opportunity)
34 changes: 34 additions & 0 deletions docs/features/11-head-to-head-rivalries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Head-to-Head Rivalry Records on Player Profiles

## Summary

Show per-pair head-to-head records ("Emma vs Fatima: 5W–2L") on player profiles and a dedicated rivalry view.

## Why / Goal

Rivalries are the emotional core of a competition app. The data already exists (`comparePlayers`, per-opponent breakdown) but is only surfaced via the explicit compare page. Making it visible per-player turns passive stats into stories.

## Scope

- On player profile, list opponents ordered by matches played with W/D/L record
- Link each opponent row to the compare page
- Optional: "rival" highlight (most-played / most contentious opponent)
- Team equivalent (team vs team records already exist via `getRivalTeams`)

## Code map

- Per-opponent W/L breakdown: `apps/worker/src/trpc/router/player-router.ts` (`getPlayerStats`)
- Compare: `apps/worker/src/trpc/router/player-router.ts` (`comparePlayers`), UI at `apps/web/src/routes/_authenticated/_sidebar/leagues/$slug/players/compare.tsx`
- Team rivalries: `apps/worker/src/trpc/router/league-team-router.ts` (`getRivalTeams`)
- Player profile UI: `.../players/$leaguePlayerId/index.tsx`

## Acceptance criteria

- Player profile shows head-to-head list with W/D/L + link to compare
- Query is a single join (no N+1 per opponent)
- Works for ELO seasons (profile pages are ELO-gated today)

## Open questions / notes

- Where to place on profile (existing "Best/Worst Teammate" cards area or new tab)
- Pagination for opponents with many matches
Loading
Loading