mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-16 03:49:13 +00:00
Merge origin/main into chore/remove-ipc-mailboxes
Conflicts were additive only: CHANGELOG.md (this PR's Unreleased section now sits above main's 3.8.x release notes) and test/js/run_all.js (both new unit suites kept). display.py and api_v3/__init__.py merged cleanly with the stack-trace-exposure fix (#778). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+126
-26
@@ -94,6 +94,8 @@ more. Shared sports code lives in `src/common`:
|
||||
| `sports_game_over.py` | 3.8.1 | `SportsGameOverMixin` — `_is_game_really_over`, with the `FINAL_PERIOD` seam (family 5) |
|
||||
| `sports_favorites.py` | 3.8.2 | `SportsFavoritesMixin`, `SportsUpcomingFavoritesMixin`, `SportsRecentFavoritesMixin` — `_is_favorite_game` and the favourites-only picks, on the `_favorite_key` seam (family 6) |
|
||||
| `sports_rotation.py` | 3.8.4 | `SportsRotationMixin` — the other-games rotation (importance order, the window, odds on rotated-in games), on the `_rankings_loaded` seam (family 7) |
|
||||
| `sports_rankings.py` | 3.8.5 | `SportsRankingsMixin` — which teams are ranked (by id and abbreviation), the poll choice, stale polls hidden, the "ranked" filter; `choose_top_division_poll()` (family 8) |
|
||||
| `sports_live_fetch.py` | 3.8.6 | `SportsLiveFetchMixin` — the live poll's scoreboard request (always fresh, shared key) and odds fetched off the update path by one worker per manager (family 9) |
|
||||
|
||||
Each is described in [src/common/README.md](../src/common/README.md).
|
||||
|
||||
@@ -350,7 +352,7 @@ compare abbreviations exactly, and
|
||||
abbreviation (or by `_team_in` where a plugin has one) instead of through
|
||||
`_favorite_key`. The result-colour helpers also wait (decision above).
|
||||
|
||||
### Family 7: the other-games rotation (core done; adoption waits for a release)
|
||||
### Family 7: the other-games rotation (done: core 3.8.4, adopted)
|
||||
|
||||
ledmatrix-plugins `scripts/test_other_games_rotation.py` (#640) pinned 96 rows
|
||||
across the nine plugins first: `_by_importance` per rankings table, core's
|
||||
@@ -380,7 +382,78 @@ builds the pools, so nothing rotates on a board with no favourites; football
|
||||
routes it through `_favorites_first(games, 0, N)` (decision below, family 13).
|
||||
ufc's MMA managers override `update()` and never build the pools, so the
|
||||
rotation is dormant there. `_best_rank`, `_is_ranked_game` and
|
||||
`_passes_other_filters` are family 8.
|
||||
`_passes_other_filters` were family 8.
|
||||
|
||||
### Family 8: rankings (done: core 3.8.5, adopted)
|
||||
|
||||
ledmatrix-plugins `scripts/test_rankings.py` (#644) pinned 79 rows across the
|
||||
nine plugins first, every ESPN request answered offline from recorded-shape
|
||||
payloads: which endpoints `_fetch_team_rankings` asks per league and the
|
||||
tables it keeps, `_choose_poll` over nine poll sets, the division rosters'
|
||||
season year across six dates, the rankings and roster caches, a poll from a
|
||||
finished season, malformed payloads, and `_is_ranked_game`, `_best_rank` and
|
||||
`_passes_other_filters` over games whose abbreviations collide. The reconcile
|
||||
(ledmatrix-plugins #645) made `_fetch_team_rankings`,
|
||||
`_choose_poll` (three bodies each), `_load_division_team_ids`,
|
||||
`_passes_other_filters`, `_best_rank` and `_is_ranked_game` (two each) one
|
||||
body each on `SportsCore`, with football's `_ranked_positions` and a new
|
||||
`_poll_season_ended` beside them, and dropped baseball's per-league
|
||||
class-level rankings copy. Every changed cell is one of the decisions below:
|
||||
the id table in all nine (and the games it re-ranks), /rankings first for
|
||||
every college or NCAA league in afl, basketball, nrl and soccer, the season
|
||||
year in the eight that asked for the calendar year, the stale-poll rows, and
|
||||
baseball's shared copy. The harness renders were byte-identical.
|
||||
`src/common/sports_rankings.py` holds the bodies in one mixin,
|
||||
`SportsRankingsMixin`, plus `choose_top_division_poll()` for the dynamic team
|
||||
resolvers; `test/test_sports_rankings_parity.py` compares them, and the
|
||||
constants, with the plugin copies.
|
||||
|
||||
Left for later families: the rank badge (switch scorebug, scroll card and the
|
||||
plugins' `manager.py`, families 11, 12 and 14) still reads the abbreviation
|
||||
table; `manager.py`'s `_get_rankings_cache` merges every league's table for
|
||||
the scroll cards. `_rankings_loaded` stays the family 7 seam (football counts
|
||||
its id table too); the two differ only for a poll whose blocks carry ids and
|
||||
no abbreviations. The data sources' `fetch_standings` (held with
|
||||
`data_sources.py`) is still four bodies; only its endpoint order changed.
|
||||
|
||||
### Family 9: live fetch and odds (reconciled; promoted, core 3.8.6)
|
||||
|
||||
ledmatrix-plugins `scripts/test_live_fetch_odds.py` (#649) pinned 44 rows
|
||||
across the nine plugins first, every ESPN request answered offline the way
|
||||
ESPN answered on 2026-10-12 (a single date 200, a range 400): the requests,
|
||||
cache keys and TTLs of `_fetch_todays_games` per league and Eastern time,
|
||||
the live cache over time, empty, null and failed answers, `_fetch_odds` per
|
||||
game kind and odds endpoint on a virtual clock (which thread asks, how long
|
||||
the call waits, when the line lands), the real `update()` of every screen
|
||||
with a 3 s odds endpoint, and the league overrides (MiLB, HockeyTech, ufc's
|
||||
`MMALive`, which stay per-sport). Measuring found seven bodies of
|
||||
`_fetch_todays_games`, not five: the fetch service's shared key had split
|
||||
the old groups. The reconcile (ledmatrix-plugins #650)
|
||||
made `_fetch_todays_games` and `_fetch_odds` one body each on `SportsCore`,
|
||||
with the odds worker `_drain_odds_queue` beside them, and added a test that
|
||||
runs `update()` and `display()` on two threads against a slow endpoint.
|
||||
Every changed cell is one of the decisions below; the harness renders were
|
||||
byte-identical once the four fixtures that seeded the old live key seed the
|
||||
shared one. `src/common/sports_live_fetch.py` holds the bodies in one mixin,
|
||||
`SportsLiveFetchMixin`; `test/test_sports_live_fetch_parity.py` compares them
|
||||
with the plugin copies.
|
||||
|
||||
Measured `update()` time with a 3 s odds endpoint and four games, before and
|
||||
after: Upcoming 6 s (afl, baseball, football, nrl, soccer, ufc), 12 s
|
||||
(hockey, lacrosse), 0 s (basketball) to 0 s everywhere; Recent 6 s / 12 s to
|
||||
0 s; Live's first pass 8 s / 12 s and next poll 4 s / 6 s to 0 s. The line
|
||||
now lands when ESPN answers (3 s here) instead of at the next `update()`
|
||||
(15 s for a live game, an hour for an upcoming or recent one) in the six.
|
||||
|
||||
Left for later: ufc's MMA Recent manager never asks for odds and its Upcoming
|
||||
manager asks for every fight in the window before trimming to the headliners
|
||||
(both in `mma.py`'s `update()` overrides, family 13); basketball's NCAA
|
||||
Recent/Upcoming fallback still caches the undated scoreboard under
|
||||
`<sport_key>_scoreboard_current` with `limit=1000`, which ESPN truncates (held
|
||||
with the data fetching); `_attach_odds_to_rotated_games` still starts its own
|
||||
thread per rotation and can route through `_fetch_odds` once every plugin
|
||||
inherits this mixin; and ESPN lists only some college basketball games
|
||||
without `groups=50` (7 of 21 on 2026-03-14), dated or not.
|
||||
|
||||
### Why the method changes
|
||||
|
||||
@@ -468,9 +541,9 @@ release.
|
||||
| 4 | Identical sweep | `manager.py`: `_dispatch_switch_refresh`, `_favorite_team_is_live`, `get_vegas_priority_weight`, `_game_involves`, `_favorite_scan_targets`, `_favorite_scan_games`, `_get_total_games_for_manager` (all nine, 1); the live-scroll helpers `_preserving_scroll_position`, `_refresh_live_scroll_managers`, `_live_scroll_managers`, `_note_live_scroll_built`, `_live_scroll_needs_rebuild`, `_live_scroll_fields` (eight, 1). `sports.py`: `_card_option`, `_filtered_or_all`, `_effective_live_duration`, `_recent_date_text` (eight, 1). 58 identical families in all | Nothing to decide; brings `manager.py` into core as a `SportsPluginHostMixin`. `_resolve_font_path` (identical in nine `sports.py` and eight renderers) becomes `sports_font_path.resolve_font_path`, not `font_layout.resolve_asset_path`, which skips the cwd. Done: core 3.8.0, adopted (ledmatrix-plugins #594); see [Stage 4](#stage-4-the-identical-sweep-done-core-380-adopted) |
|
||||
| 5 | Game-over check | `SportsLive._is_game_really_over` (5) | Pure logic, no pixels; one seam, `FINAL_PERIOD`. The pilot for the procedure. Reconciled to one body and promoted as `sports_game_over`; shipped in 3.8.1 and adopted. See [Family 5](#family-5-the-game-over-check-done-core-381-adopted) |
|
||||
| 6 | Favourite matching | `_is_favorite_game` (7 across three classes), `_select_games_for_display` (2: nrl), `_select_recent_games_for_display` (3) | Everything that asks "is this a favourite" goes through the 3.5.0 `_favorite_key` seam. Reconciled to one body each and promoted as `sports_favorites`; shipped in 3.8.2 and adopted. See [Family 6](#family-6-favourite-matching-done-core-382-adopted) |
|
||||
| 7 | Other-games rotation | `_by_importance`, `_other_games_window`, `_advance_other_games_if_due` (2 each: football), `_rotate_other_games_on_display` (2: ufc), with `_attach_odds_to_rotated_games` (3; ufc had none) | One outlier each; football carried two fixes the other eight lacked. Reconciled to one body each, on a `_rankings_loaded` seam, and promoted as `sports_rotation`; adoption waits for the release that ships it. See [Family 7](#family-7-the-other-games-rotation-core-done-adoption-waits-for-a-release) |
|
||||
| 8 | Rankings | `_fetch_team_rankings` (3), `_choose_poll` (3), `_load_division_team_ids`, `_passes_other_filters`, `_best_rank`, `_is_ranked_game` (2 each: football) | Needs 7; the rank badge and the "ranked only" filter read it |
|
||||
| 9 | Live fetch and odds | `_fetch_todays_games` (5), `_fetch_odds` (3), `_attach_odds_to_rotated_games` (3) | The prerequisite for one shared ESPN poller across plugins |
|
||||
| 7 | Other-games rotation | `_by_importance`, `_other_games_window`, `_advance_other_games_if_due` (2 each: football), `_rotate_other_games_on_display` (2: ufc), with `_attach_odds_to_rotated_games` (3; ufc had none) | One outlier each; football carried two fixes the other eight lacked. Reconciled to one body each, on a `_rankings_loaded` seam, and promoted as `sports_rotation`; done: core 3.8.4, adopted (ledmatrix-plugins #643). See [Family 7](#family-7-the-other-games-rotation-done-core-384-adopted) |
|
||||
| 8 | Rankings | `_fetch_team_rankings` (3), `_choose_poll` (3), `_load_division_team_ids`, `_passes_other_filters`, `_best_rank`, `_is_ranked_game` (2 each: football) | Needs 7; the rank badge and the "ranked only" filter read it. Reconciled to one body each and promoted as `sports_rankings`; done: core 3.8.5, adopted (ledmatrix-plugins #648). See [Family 8](#family-8-rankings-done-core-385-adopted) |
|
||||
| 9 | Live fetch and odds | `_fetch_todays_games` (7 when measured), `_fetch_odds` (3); `_attach_odds_to_rotated_games` went with 7 | The prerequisite for one shared ESPN poller across plugins. Reconciled to one body each and promoted as `sports_live_fetch` (core 3.8.6). See [Family 9](#family-9-live-fetch-and-odds-reconciled-promoted-core-386) |
|
||||
| 10 | View model | `_extract_game_details_common` (9 of 9) | Every renderer reads it; its keys are additive-only, so reconcile to the superset and leave sport extras in `_extract_game_details` |
|
||||
| 11 | Switch scorebug | `_load_fonts` (4), `_load_custom_font_from_element_config` (6), `_get_layout_offset` (5), `_fit_score_font` (2: football), `_load_and_resize_logo` (9), `_draw_dynamic_odds` (9), then `_draw_scorebug_layout` (20: Upcoming 9, Recent 8, Live 2, ufc's Core 1) | Needs 10 and the twin decisions below. Several releases: fonts and offsets, logos, odds, then one mode's layout per release |
|
||||
| 12 | Scroll/Vegas card | `game_renderer.py`: `render_game_card` (6), `_load_and_resize_logo` (8), `_load_custom_font`, `preload_logos`, `__init__` (7 each), `_draw_records_or_rankings` (6), `_load_fonts`, `_draw_dynamic_odds`, `_draw_live_game_status`, `_draw_recent_game_status`, `_get_team_display_text` (5 each), `_draw_text_with_outline` (2: football) | Same decisions as 11; sequence after the scroll-performance work on pre-rendered strips lands |
|
||||
@@ -537,27 +610,54 @@ suspected behaviour that needs a payload or a rig to confirm first.
|
||||
non-empty; football counts its by-id table too). Kept for family 13: the
|
||||
no-favourites branch of `update()` keeps its fixed "next N" in the eight
|
||||
plugins that have it, rather than football's rotating pools.
|
||||
- **8, rankings.** (a) afl, basketball, nrl and soccer turn a *standings*
|
||||
payload into ranks (a pro league's standings position becomes the rank
|
||||
badge); baseball, hockey, lacrosse, ufc and football do not. Which is
|
||||
right is visible on every pro-league card with "show ranking" on.
|
||||
(b) football also keys ranks by team id, so two schools sharing an
|
||||
abbreviation across divisions cannot be confused: adopt for all.
|
||||
(c) football asks for the division roster of the *season* year (July
|
||||
onward is this year's season), which is right for football and wrong for
|
||||
college basketball, hockey and lacrosse, whose ESPN season is the year it
|
||||
ends: a per-sport seam, not football's constant. (d) baseball's
|
||||
`_choose_poll` calls `dynamic_team_resolver.choose_top_division_poll`, the
|
||||
others inline it: one home, in core.
|
||||
- **9, live fetch and odds.** (a) afl, basketball, nrl and soccer cache the
|
||||
live scoreboard for 30 s under `<sport_key>_scoreboard_current`; the others
|
||||
do not (the harness fixtures seed that key, so the change shows up there).
|
||||
(b) basketball fetches college games with no `dates` parameter, citing a
|
||||
404 (*verify* now that `espn_dates` handles ranges). (c) odds are fetched
|
||||
three ways: blocking (hockey, lacrosse), a thread waited on for 1.5–2 s
|
||||
(six plugins), or fire-and-forget for upcoming games (basketball). This
|
||||
sets how long `update()` takes and when an odds line appears. (d) nrl
|
||||
guards on a missing odds manager; port it.
|
||||
- **8, rankings. Decided 2026-10-12, reconciled.** Measured first, two of
|
||||
the questions as first written did not hold. (a) afl, basketball, nrl and
|
||||
soccer could turn a *standings* payload into ranks, but the branch never
|
||||
ran: rankings are fetched only where `_league_has_rankings()` (a college or
|
||||
NCAA league), and ESPN's site-API /standings answers `{}` or a link stub,
|
||||
never a table. Deleted. The same four data sources asked /standings first
|
||||
for any college league outside a fixed list, so a custom NCAA soccer
|
||||
league (`usa.ncaa.m.1`) never showed its coaches poll; every college or
|
||||
NCAA league now asks /rankings first, as in the other five (fix, visible
|
||||
only there). (b) Ranks are kept by ESPN team id too, in all nine, and a
|
||||
game with ids is ranked by id: ESPN lists Ohio State and Ohio State Newark
|
||||
both as OSU. Changes the "ranked" filter and the other-games order only
|
||||
where abbreviations collide; the badge still reads the abbreviation table.
|
||||
(c) Only college football has division rosters (all nine carry the same
|
||||
one-league table; college basketball, hockey and lacrosse publish none), so
|
||||
football's July rule is promoted as is and there is no seam. In the other
|
||||
eight the calendar year was unreachable. (d) One poll rule, in core:
|
||||
`_choose_poll`, with `choose_top_division_poll()` for the resolvers that
|
||||
expand `AP_TOP_n` (baseball's helper and the others' inline copies call it
|
||||
once they adopt). (e) A poll from a season that has ended loads nothing:
|
||||
ESPN keeps serving the last poll between seasons (on 2026-10-12, college
|
||||
basketball's April final and college lacrosse's May poll), and each block
|
||||
carries its season's `endDate`, so no calendar and no per-sport seam. No
|
||||
badge shows and the "ranked" filter fails open until the new season's
|
||||
first poll; visible on college basketball and lacrosse boards between
|
||||
seasons. A block without a readable `endDate` counts as current. (f)
|
||||
baseball's MLB, MiLB and NCAA Baseball managers drop their class-level
|
||||
shared copy of the table (none of those leagues publishes a poll).
|
||||
- **9, live fetch and odds. Decided 2026-10-12, reconciled.** (a) Live
|
||||
scores are always fresh: every poll asks ESPN and leaves the answer under
|
||||
the shared `espn_scoreboard_cache_key` for its dates (`max_age=1`: only a
|
||||
copy another reader fetched within the last second, the managers of one
|
||||
display tick, is reused). afl, basketball, nrl and soccer had held it 30 s
|
||||
under `<sport_key>_scoreboard_current`, so with the 15 s default poll every
|
||||
other poll repeated the last one; lacrosse and ufc now share their copy.
|
||||
(b) College basketball is asked by date like every other league: ESPN
|
||||
answered single dates for both college leagues on 2026-10-12 (the 404 no
|
||||
longer happens), so the lookback now reaches last night's late games there
|
||||
too. `groups=50` is a separate question (left above). (c) Odds never block
|
||||
`update()`: one daemon worker per manager answers a queue and attaches each
|
||||
line in place under `_games_lock`; one batch in flight per manager, games
|
||||
queued meanwhile join it. A card can draw once without odds and gain them;
|
||||
a pre-rendered scroll strip gains them when it is next rebuilt. (d) The
|
||||
missing-odds-manager guard (nrl's, already in `_attach_odds_to_rotated_games`
|
||||
since family 7) is in `_fetch_odds` too, logged at debug, not ERROR. Fix
|
||||
ported: an answer with `"events": null` is no games instead of a
|
||||
`TypeError` out of the fetch (all nine), and `get_espn_scoreboard` no longer
|
||||
caches it under the shared key.
|
||||
- **10, view model.** Per key, whether every sport emits it. Additive only:
|
||||
no key is renamed or removed.
|
||||
- **11 and 12, the scorebug and the card.** The pinned divergences in
|
||||
|
||||
Reference in New Issue
Block a user