# Changelog Notable changes to the LEDMatrix core. The version below is the value of `src.__version__`, which the plugin loader reports to compatibility checks and which plugin manifests reference via `ledmatrix_min_version`. **Why this file exists:** the plugin monorepo bundles fallback copies of several core modules (see `docs/plugin-development/08-shared-sports-code.md` in [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins)). A plugin may delete its bundled copy only when its manifest floors on the first core release that ships the module — which requires module additions to be recorded here, against a version number. When you add a module plugins will import via `src.*`, note it in the Unreleased section and bump `src/__init__.py` in the release that ships it. **Use `ledmatrix_min_version` in manifests, not `ledmatrix_min`.** The loader accepts both, but the store flags the old spelling as deprecated (`store_manager.py`) and only the new one is in `schema/manifest_schema.json`. ## Unreleased ### Removed: the cache-key mailboxes (control socket stage 5) -- breaking The control socket (`docs/IPC_CONTROL_SOCKET.md`) is now the only way the web interface sends the display a command. The file mailboxes it fell back to for one release are gone. - **`display_on_demand_request` and `plugin_error_clear_request` are no longer written or read.** The display stops polling the on-demand mailbox (`MailboxWatch`, `MAILBOX_POLL_INTERVAL_WITH_SOCKET`, `_consume_on_demand_request` and the persisted `display_on_demand_processed_id` guard are removed), and the error publisher stops reading the clear request. `CacheManager.file_signature`, `src.cache_manager.MailboxWatch`, `src.ipc.client.should_fall_back` and `src.error_aggregator.ERROR_CLEAR_REQUEST_KEY` are removed. - **A write to either key is dropped, with one warning per writer.** `CacheManager.save_cache` (and so `set`) refuses `RETIRED_MAILBOX_KEYS` and logs `Ignored a write to the retired '' cache key by plugin ''`, naming the plugin from the call stack or the request. Plugins must use `BasePlugin.request_on_demand()` / `end_on_demand()` (3.8.1). In the official monorepo, birdnet-go, mqtt-notifications, on-air and pomodoro-timer still write the mailbox, but only as their fallback when those methods are missing or answer `None`. - **On-demand routes without a listening display.** `POST /api/v3/display/on-demand/start` with no display listening starts the service (when `start_service`, the default) and answers **`202`** with `status: "starting"` at once; a single background worker in the web process (`web_interface/on_demand_dispatch.py`) sends the request until the display acknowledges it, for up to 45 s (10 s for a service that is running but has no socket yet). `GET /display/on-demand/status` reports it (`starting`, then the display's state, or `error` / `start-timeout`), and `/display/current-status` adds `on_demand_pending`. A newer start replaces a pending one and a stop cancels it (`cancelled_request_id`). The web UI and the MQTT bridge treat `202` as taken. With the service stopped and `start_service` false it answers `400`. Every other socket failure (`unknown_command` from an older display, `disabled`/`unsupported`, `busy`, a timeout) is a `503`. `/stop` answers `503` when no display is listening, unless `stop_service` stops the service. `transport` is always `"socket"`; the `"mailbox"` value is gone. - **`POST /api/v3/errors/clear` without the socket answers `503`** (with `context.socket_error` and a message saying why) instead of recording a request. `clear_pending` in the error routes is now always `false`; `src.error_aggregator.read_error_report()` returns only the snapshot, and `error_summary_from_report()` / `plugin_health_from_report()` / `request_error_clear()` lose their clear-request arguments. - **Kept:** the display still writes `display_current_state`, `display_on_demand_state`, `plugin_runtime_snapshot` and the heartbeat file, because the web interface reads them whenever the socket cannot answer (a stopped or starting display, a web user not yet in the socket's group, Windows), and `display_on_demand_config`, its own record for resuming a session after a restart. - **Windows and `LEDMATRIX_CONTROL_SOCKET=off`:** with no socket, the web interface can no longer start or stop on-demand sessions or clear errors on a running display (the mailbox used to carry them). ### Web UI: the Display tab is an ES-module page, with a page-visibility service (stage 4) - New `static/v3/js/core/visibility.js`: each page module gets `ctx.visibility` with `whileVisible(start, stop)`, `every(ms, fn)` and `isVisible()`. Work registered there runs only while the page's tab is the active tab and the browser tab is visible, and ends when the page is swapped out, with no teardown code in the page. It reads the active tab from `window.LEDVisibility`, so it agrees with the classic partials that still use that directly. The page registry gained a `mountContext` option for services bound to one mounted page. - The Display tab's inline scripts are now `static/v3/js/pages/display.js`. The partial has no inline script, `onclick` or `onchange` any more. The multi-display sync status poll (every 5 s) runs through `ctx.visibility.every`; the status and scroll-speed hint requests go through `core/api.js` with the page's abort signal, and so does the Vegas order widget's plugin-list request. - Behaviour differences: with sync on, opening the tab asks for the status once instead of twice, and a Display tab loaded while not on screen waits until it is. A login redirect during a poll no longer flashes "Sync status unavailable". The `window.syncStatusInterval` timer id is gone (nothing read it). A pending scroll-hint request or widget retry is dropped when the partial is swapped out. - `window.updateSyncUI` keeps working as a deprecated alias through `window.LEDMatrix` (one console warning). - New suites `test/js/dom/test_visibility_service.js` and `dom/test_display_page.js`; `unit/test_display_partial_ids.js` imports the module instead of slicing the template. ### Plugins ask for the screen in-process: `request_on_demand()` / `end_on_demand()` The in-process way in that stage 5 of the control socket needed (`docs/IPC_CONTROL_SOCKET.md`, "Plugins in the display process"). - **`BasePlugin.request_on_demand(mode=None, duration=None, pinned=False)`** shows the plugin now, and **`BasePlugin.end_on_demand()`** gives the screen back. Both are safe from any thread (an MQTT callback, a timer thread): `PluginManager.request_on_demand()` / `end_on_demand()` hand the request to `DisplayController.submit_plugin_on_demand()`, which only queues it (at most 32) and wakes the render thread through the control socket's flag (`ControlServer.wake()`). The render thread applies it with the socket's commands, through the same handler as a web on-demand request, so it lands within a frame rather than on the mailbox's once-a-second look. Both return the request id, or `None` when no display runs in the process (the web interface, `scripts/check_plugin.py`) or the queue is full. - **A plugin's stop ends only its own session.** A mailbox stop still ends any session, whoever started it. - **Older cores.** Plugins detect the methods with `hasattr` and write the `display_on_demand_request` mailbox when they are missing or answer `None`; the pattern is in `docs/PLUGIN_API_REFERENCE.md` ("On-demand display"). The display still reads the mailbox for plugins that write it. ### Web UI: Schedule and General are ES-module pages (stage 3) - The Schedule and General tabs follow stage 2 (#727): their inline `