feat/team capability rankings p1 - #248
Merged
Merged
Conversation
tfield
commented
Apr 20, 2026
Contributor
- feat(capability): frontend data layer (Unit 6)
- feat(capability): Tournament Teams card (Unit 7)
- feat(capability): Match Teams table on strategy page (Unit 8)
- docs(capability): team-capability-p1 design doc (Unit 9)
- fix(capability): on-demand fetch when IndexedDB is empty
- fix(db): handle IndexedDB upgrade-blocked and cross-tab version change
- feat(capability): add Rank column to Tournament Teams card
Wires the new GET /api/team-capability/{id} endpoint into the
network-comm-refinement infrastructure landed earlier this week.
- TeamCapability.ts types mirror the backend response 1:1 (camelCase,
number | null for nullable numeric fields).
- getTeamCapability wrapper in rb.ts uses cacheFetch (conditional GET
with If-None-Match via the apiEtags store) — no raw fetch or rbfetch
introduced.
- IndexedDB DB_VERSION bumped 15 → 16; new teamCapability object store
keyed by tournamentId; putTeamCapability / getTeamCapability helpers.
- cacheClear adds teamCapability to READ_ONLY_CACHE_STORES so logout /
username-change wipes it (apiEtags already wiped globally).
- sync.ts JOBS array gets one new entry (id: team-capability, cadence
30_000, precondition: session + network + ≥1 active tournament). The
runner iterates every active tournament, matching the syncMatchSchedule
/ syncRobotAlertList pattern so Units 7 and 8 can read by tournamentId
without plumbing a "current tournament" through the scheduler.
Tournament-window precondition uses serverNow() via the existing
active-tournament helper, inheriting the pre-tournament lead-in time
without adding a new heuristic.
Typecheck clean.
Renders below the existing schedule/bracket/rankings stack on the team-
schedule page. Reads from IndexedDB (populated by Unit 6's JOBS sync);
no network dependency on this path.
- 12-column wide view: Team (# + Name), Overall/Auto/Teleop/Endgame EPA,
OPR, Auto Accuracy, Teleop Success, Pickup Avg, Comments, Alerts,
Coverage. Portrait-iPhone collapses to 3 columns (Team / Overall EPA /
OPR) via useIsNarrow matching the schedule rotate-hint breakpoint.
- Default sort OPR desc; nulls last and withdrawn rows after nulls
regardless of direction. Numeric columns flip desc-first (higher =
better); Team flips asc-first.
- Owner team row reuses the existing rankings-row-owner highlight.
- Withdrawn rows render with text-decoration: line-through via
.tournament-teams-row-withdrawn.
- Coverage column renders .badge-scouting-{full,thin,none} pills in the
Paul Tol vibrant palette with text labels — CB-safe (colour + label
both carry the signal); dark-mode overrides included.
- Staleness banner (.banner-info) shown once when any row has oprStale
or epaStale. Skeleton row on first-load. Error falls back to a
.banner-warning.
- Row click -> /report/summary/:teamId.
CSS additions live in a labeled "/* Team Capability — Unit 7 */" block
at the end of components.css so Unit 8 can append cleanly.
Alliance-grouped 6-team capability table above the strategy diagrams on
/strategy/:id/:level/:match, reading from the IndexedDB store populated
by Unit 6's JOBS sync.
- Red alliance first, blue alliance second, visually separated via
CB-safe background tints plus a bold "Alliance" label column — colour
is redundant to the text label.
- Within-alliance ranks (1, 2, 2, 4 standard competition ranking) beside
every numeric value, rendered via the existing .schedule-team-rank
span. Suppression rules: (1) <2 non-null values in the alliance for a
column -> no rank, (2) spread == 0 across non-null values -> no rank.
- Em dash for missing data; strikethrough on withdrawn rows; owner-team
highlight; row click -> /report/summary/:teamNumber.
- Staleness .banner-info when any team has oprStale or epaStale; skeleton
row on first load; .banner-warning on fetch error.
- Skips rendering for Practice level and when both alliances are empty
(e.g., before schedule data syncs).
- Coverage column dropped for this surface — 6 rows at a glance doesn't
need the pill.
New sibling helper matchTeamsByAlliance() returns { red, blue } with
slot 4 included when non-zero (4-team playoff alliances + surrogates).
teamNumbersForMatch() is unchanged — widening its RobotSlot-keyed return
type to include slot 4 would cascade through StrategyStroke, canvas
colors, and the slot palette. Separate helper is cleaner.
CSS additions live in a labeled "/* Team Capability — Unit 8 */" block
after Unit 7's block. .schedule-team-rank is not redefined.
Short companion to the P1 plan — overview, data sources, component graph, response shape, key decisions, operational notes, what's not in scope, and the thundering-herd observability bookmark. Mirrors the tba-data-foundation.md / match-video-tba-sync.md pattern established for earlier TBA work. ~2 pages, skimmable.
The JOBS sync iterates active tournaments only, so post-season reviews (and any navigation to a tournament outside its activeFrom/activeUntil window) find an empty teamCapability store and render "No team capability data yet." indefinitely. Hooks now issue a one-off fetchTeamCapability + putTeamCapability when the first IndexedDB read is empty. Bounded (once per hook mount), cheap (cacheFetch short-circuits to 304 on subsequent mounts), and leaves the regular sync loop untouched so live-event bandwidth doesn't grow.
Unit 6's DB_VERSION bump (15 → 16) could hang forever for any user who already had a stale tab (or service-worker connection) holding the old version open. The open request fired onblocked, but we weren't listening, so getDB() never resolved and hooks that awaited it stayed "Loading" indefinitely. Symptom disappeared only after a browser restart closed the stale connection. - request.onblocked now rejects with an actionable error message. - The opened db.onversionchange closes our connection voluntarily when another tab tries to upgrade, so the other tab's upgrade can proceed. getDB() reopens lazily on the next call. Standard IndexedDB multi-tab defence; makes future schema bumps non-hazardous.
Prepends a non-sortable Rank column (1, 2, 3…) showing each row's position in the current sort order. Updates with every click-to-sort, so strat can see at a glance "who is 3rd by OPR" or "who is 5th by teleop success." Visible in both wide and narrow iPhone views. Header is non-sortable (it's a derived sort-position, not a field). Width kept tight with width:1% + tabular-nums; bolded in the body, muted in the header to signal "position indicator, not data."
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.