chore: prepare the 3.5.0 release (#601)

* chore: prepare the 3.5.0 release

Turns the CHANGELOG's Unreleased section into `## 3.5.0` and bumps
`src.__version__`, the value plugin `ledmatrix_min_version` floors compare
against. No behaviour change; nothing outside the CHANGELOG, `src/__init__.py`
and one docs line is touched.

The staged entries are reshaped into the `### ` subsections every released
section already uses, and the "new modules a plugin may import via `src.*`"
block moves to the top as the plugin-facing summary, the same shape as 3.4.0.
Its floor, written as "the release that ships this" while it was staged, is now
3.5.0, and `docs/SPORTS_UNIFICATION.md` says 3.5.0 for `sports_helpers.py`
instead of "(unreleased)".

Four merged changes had never been written down. They are added under the
subsection each belongs to, from the commits and their measurements:

- the idle back-off clamped to the next kickoff (#599)
- concurrent ESPN date chunks (#596)
- the three web routes that consulted plugin manifests before anything had
  discovered plugins, one of which wrote a plugin API key to config.json in
  plain text (#594)
- the cache permission fix and its systemd unit changes (#593), which get
  their own subsection

No tag and no release: `scripts/check_release_version.py v3.5.0` passes, so
tagging is a separate, deliberate step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqzd6Nz2bQJp5K7DD5dS4X

* ci: let Claude Code Review run on PRs the Claude app opens

The review action refuses a workflow whose actor is a GitHub App unless the
app is named in `allowed_bots`, which this workflow never set:

  Actor is a GitHub App: claude[bot]
  Actor type: Bot
  Action failed with error: Workflow initiated by non-human actor: claude
  (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.

It aborts about two seconds in, before the diff is read, so the check is red
on every such PR and re-running cannot help: the actor does not change. Until
now no PR here had a bot author, so nothing tripped it.

`'claude'` rather than `'*'`: the action lowercases each entry and strips a
trailing `[bot]` before comparing it to the actor
(`isAllowedBot` in `src/github/validation/actor.ts`), so this admits
`claude[bot]` and no other app. `'*'` would admit any app that can trigger a
workflow here, with a prompt it controls — the action's own docs warn about
that on public repositories, and this one is public.

The write-permission check already allowed the app; `checkHumanActor` was the
only gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqzd6Nz2bQJp5K7DD5dS4X

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
claude[bot]
2026-09-21 16:08:05 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent cf02538d2e
commit 21c8a54f68
4 changed files with 105 additions and 37 deletions
+6
View File
@@ -36,6 +36,12 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Review PRs opened by the Claude GitHub App. Without this the action
# aborts before reading the diff ("Workflow initiated by non-human
# actor"), so every such PR shows this check red. Named rather than
# '*': the allow-list is matched against the triggering actor, so
# this admits claude[bot] alone and no other app.
allowed_bots: 'claude'
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
+97 -35
View File
@@ -19,7 +19,32 @@ accepts both, but the store flags the old spelling as deprecated
## Unreleased
Config saves and plugin config preparation:
## 3.5.0
New modules a plugin may import via `src.*` (floor on 3.5.0):
- `src/common/sports_helpers.py` — the helpers the scoreboards' `sports.py`
carry byte-identical copies of: `clamp_window`, `clamp_seconds`,
`logo_needs_refresh`, `spread_weighted_order` (+ `MIN_WINDOW_DAYS`,
`MAX_WINDOW_DAYS`), and `SportsHelpersMixin` with `_mode_customization`,
`_setting_int`, `_reset_dwell_on_reentry`, `_next_switch_index`,
`_spread_weighted_order`, `_odds_color`, `_upcoming_date_and_time_text` under
the plugins' names and signatures, plus the `_favorite_key` override point.
Constructor-free; keeps lazy state on its host (see the module docstring,
which also gives the host contract).
A new module rather than more methods on `sports_shared`: a plugin that
deletes a copy and leans on an older module having grown the method fails at
runtime with `AttributeError`, which no load-time check sees, while a missing
module fails at load. Nothing in core uses it yet.
- `test/test_common_is_hardware_free.py` — `src/common` must import without
`rgbmatrix` and never import `src.base_classes`, `src.display_manager` or
`src.plugin_system` at module level.
- `src/common/espn_dates.py` — `fetch_espn_scoreboard`,
`fetch_espn_date_chunks`, `espn_date_chunks`, `clamp_espn_limit`,
`ESPN_MAX_LIMIT`: fetch an ESPN scoreboard date range now that ESPN rejects
ranges (see Sports data below). Plugins bundle a copy of it.
### Config saves and plugin config preparation
- A JSON `POST /api/v3/config/main` changes only the keys it sends. The MQTT
bridge's brightness slider used to turn off `disable_hardware_pulsing`,
@@ -65,31 +90,7 @@ Config saves and plugin config preparation:
with any core config section are flagged: the last private copies of the
core-key list now use `src/core_config_keys.py`.
New module a plugin may import via `src.*` (floor on the release that ships
this):
- `src/common/sports_helpers.py` — the helpers the scoreboards' `sports.py`
carry byte-identical copies of: `clamp_window`, `clamp_seconds`,
`logo_needs_refresh`, `spread_weighted_order` (+ `MIN_WINDOW_DAYS`,
`MAX_WINDOW_DAYS`), and `SportsHelpersMixin` with `_mode_customization`,
`_setting_int`, `_reset_dwell_on_reentry`, `_next_switch_index`,
`_spread_weighted_order`, `_odds_color`, `_upcoming_date_and_time_text` under
the plugins' names and signatures, plus the `_favorite_key` override point.
Constructor-free; keeps lazy state on its host (see the module docstring,
which also gives the host contract).
A new module rather than more methods on `sports_shared`: a plugin that
deletes a copy and leans on an older module having grown the method fails at
runtime with `AttributeError`, which no load-time check sees, while a missing
module fails at load. Nothing in core uses it yet.
- `test/test_common_is_hardware_free.py` — `src/common` must import without
`rgbmatrix` and never import `src.base_classes`, `src.display_manager` or
`src.plugin_system` at module level.
- `src/common/espn_dates.py` — `fetch_espn_scoreboard`,
`fetch_espn_date_chunks`, `espn_date_chunks`, `clamp_espn_limit`,
`ESPN_MAX_LIMIT`: fetch an ESPN scoreboard date range now that ESPN rejects
ranges (see Sports data below). Plugins bundle a copy of it.
Sports data:
### Sports data
- Since 2026-09-15 ESPN answers `dates=YYYYMMDD-YYYYMMDD` scoreboard queries
with `400 Bad Request` for every sport, so season schedules, the weeks window
@@ -104,8 +105,28 @@ Sports data:
- `BackgroundDataService.handles_espn_date_ranges` is `True`. Plugins check it
to decide whether to submit a season range to the service or fetch it
themselves on an older core.
- A league with no live games no longer backs its poll off past the next
kickoff. The escalation counted empty looks and nothing else, so a league
three hours before kickoff was indistinguishable from one out of season and
both reached `live_idle_max_interval`: measured gaps of up to 928 seconds,
and a rig that sat for a quarter of an hour with eight NFL games in progress
without noticing any of them. The wait is now clamped so it cannot run past
the earliest start still ahead, which the live fetch already downloads, so
it costs no extra request. Just after a kickoff the live cadence is held for
a grace window, because a provider that has not yet flipped the status would
otherwise read as another empty check and escalate the back-off again.
- ESPN date chunks are fetched six at a time (`ESPN_CHUNK_WORKERS`) in two
passes: months and edge days first, then the days of any month that came
back at the cap. A cold college-baseball season is about 130 requests, and
they went out one at a time; March and April measured on a Pi 4 (63
requests, 3101 events) went from 11.2s to 1.6s. Merged events still follow
`espn_date_chunks` order, so the payload does not depend on which request
won the race, and a capped month's payload is dropped before its days are
fetched, which keeps the peak memory of a four-capped-month fetch to about
16 MB over the sequential path rather than 43 MB — `docs/LOW_MEMORY_BOARDS.md`
puts a 1 GB Pi 3B+ at under 200 MB of headroom.
Scrolling:
### Scrolling
- **Scoreboard scroll speed no longer changes with the General tab's "Scroll
Frame Rate" (`target_fps`).** Scoreboards on `src.common.sports_scroll`
@@ -135,7 +156,7 @@ Scrolling:
`scroll_delay` are described as the speed clamp they are rather than frame
stepping. Scoreboard `scroll_delay` is documented as ignored for pacing.
Web interface:
### Web interface
- The plugin settings form honours `"x-display": "hidden"` in config schemas:
the property gets no control at any depth (top level, nested objects, array
@@ -187,8 +208,19 @@ Web interface:
request that gets no HTTP answer (e.g. the web service restarting mid-run) is
re-sent with backoff instead of being counted as failed and skipped — that is
how a disabled plugin with an update waiting was silently left out.
- Three routes consulted the web process's plugin manifests without
discovering plugins first, so they misbehaved from every `ledmatrix-web`
restart until something else ran a discovery — in practice until someone
opened the dashboard, measured at over three minutes on one rig.
`POST /display/on-demand/start` and `POST /plugins/toggle` answered 404
"Plugin not found", and `POST /config/main` did not recognise a plugin
section, so it skipped secret separation and wrote the plugin's API key to
`config.json` in plain text instead of `config_secrets.json`. The routes now
discover when nothing has been discovered yet, and rescan once when a
specific plugin id (or, for on-demand by mode, a mode) is not found, so a
plugin installed since the last scan is found too.
Security (request paths and inline handlers, siblings of #561):
### Security (request paths and inline handlers, siblings of #561)
- `POST /api/v3/plugins/assets/upload`, `GET .../assets/list` and
`POST .../assets/delete` validate `plugin_id` with `src/common/path_safety`
@@ -208,7 +240,7 @@ Security (request paths and inline handlers, siblings of #561):
- The uploaded-images list escapes each file's original name, path and ids; a
name like `<img src=x onerror=...>.png` was inserted as markup.
Display hardware settings the library refuses:
### Display hardware settings the library refuses
- The rgbmatrix library answers several settings with no matrix or `abort()`
rather than an error, on every board, so the display service crash-looped
@@ -239,7 +271,7 @@ Display hardware settings the library refuses:
values as the defaults; the "code default" values they listed never apply,
because config migration fills missing keys from the template.
Plugin system:
### Plugin system
- A plugin no longer starts with a schema warning and a degraded flag because
config.json still holds a boolean where its schema now has an object with an
@@ -250,7 +282,7 @@ Plugin system:
is written at load; the next save of that plugin's settings stores the object.
Other type mismatches still warn.
Core:
### Core
- `ConfigManager.load_config()` no longer raises on a host without the POSIX
ownership APIs. The self-heal that chgrp's `config_secrets.json` to the
@@ -268,7 +300,37 @@ Core:
ownership step is now skipped where `os.chown` is missing. No behaviour
change on the Pi.
Automatic updates and Update Code:
### Cache permissions
- The web interface can read what the display service caches again.
`ledmatrix-web.service` carried `CacheDirectory=ledmatrix`, and systemd
re-owns `/var/cache/ledmatrix` and its contents to the unit's `User=`
whenever the directory's owner differs, which erased the `root:ledmatrix`
setgid layout the installers set up: every file the root display service
wrote afterwards was `root:root` 0660 and unreadable by the web interface
(392 unreadable files on one rig, with display status, on-demand state and
plugin health empty). Since #547 the web unit is rendered from its template
on every install, so every fresh install hit this.
`DiskCache.set` now gives each file the directory's group (when that
directory is group-writable) and 0660 on the open descriptor before the
rename, independent of setgid, which also closes a window where a fresh
file was visible as mkstemp's 0600. `DiskCache.share_existing_files`
repairs files an older version left behind, once per process, through
`O_NOFOLLOW` descriptors, skipping hard links and other users' files.
Existing installs only ever receive `git pull`, so that repair is the fix
for them; new installs also drop `CacheDirectory=` and
`CacheDirectoryMode=` from the web unit.
- `install_web_service.sh` replaces an existing cache directory's group
whenever the installing user is not in it. It used to replace only root's,
so a `root:ledmatrix` directory belonging to a user outside that group was
left alone and everything root wrote there stayed unreadable.
- `/display/on-demand/status` and the current-display status read the display
service's keys with `memory_ttl=0`, as every other cross-process reader
already does. They served the first copy the web process had read for the
full 120s `max_age`, so on-demand reported "active" for over 100 seconds
after the file on disk said "idle".
### Automatic updates and Update Code
- An update that changes `web_interface/requirements.txt` is no longer rolled
back on every auto-updating device. `safe_pip_install.sh` allowed only the
@@ -292,7 +354,7 @@ Automatic updates and Update Code:
budget, so a rollback finishes inside the unit's 30-minute limit instead of
being killed mid-way.
Small fixes (update-all, plugin system settings, scripts):
### Small fixes (update-all, plugin system settings, scripts)
- **Check & Update All** counts a plugin that had nothing to update as
"already up to date" instead of "updated". ZIP-installed monorepo plugins
@@ -330,7 +392,7 @@ Small fixes (update-all, plugin system settings, scripts):
check `web_interface/blueprints/api_v3/`, which became a package, instead of
reporting `api_v3.py` as missing.
Docs and developer tools:
### Docs and developer tools
- `docs/REST_API_REFERENCE.md` rechecked against every handler: request
fields that made documented calls fail (`repo_url`, `action_id`/`params`,
+1 -1
View File
@@ -81,7 +81,7 @@ src/common/
sports_scroll.py SportsScrollDisplay / …Manager — scroll orchestration
(content building stays in the plugins)
sports_helpers.py clamp/logo/rotation free functions + SportsHelpersMixin
(unreleased) — the helpers byte-identical in the
(3.5.0) — the helpers byte-identical in the
plugins' sports.py, and the _favorite_key seam
```
+1 -1
View File
@@ -4,5 +4,5 @@ LEDMatrix Display System
Core source package for the LED Matrix Display project.
"""
__version__ = "3.4.0"
__version__ = "3.5.0"