Skip to content

feat/team capability rankings p1 - #248

Merged
tfield merged 7 commits into
mainfrom
feat/team-capability-rankings-p1
Apr 20, 2026
Merged

feat/team capability rankings p1#248
tfield merged 7 commits into
mainfrom
feat/team-capability-rankings-p1

Conversation

@tfield

@tfield tfield commented Apr 20, 2026

Copy link
Copy Markdown
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

tfield added 7 commits April 20, 2026 06:53
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."
@tfield
tfield merged commit c908cfb into main Apr 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant