refactor(plugins): the display publishes plugin runtime state; retire plugin_state.json (#690)

Stage 2 of the web plugin catalog, after #688.

- The display publishes a plugin runtime snapshot (plugin_runtime.py) to
  the shared cache: per plugin loaded, lifecycle state, a short redacted
  error summary, the version it loaded and when, plus published_at /
  stale_after / running. Written on change (throttled to 10 s; the
  RUNNING/ENABLED flip of an ordinary update is not a change) and once a
  minute otherwise; cleanup() publishes running: false.
- The web reads it back and restores loaded / state / error_info in
  /api/v3/plugins/installed (plus loaded_version, loaded_at and
  data.runtime). Only a live snapshot counts; stale, stopped or missing
  answers null and says which.
- data/plugin_state.json is retired: every reader and writer moved to
  config + disk (desired) or the snapshot (observed). Nothing in it was
  non-derivable, so nothing is migrated and an existing file is left
  unread. The web-side PluginStateManager (state_manager.py) is removed;
  the display's plugin_state.PluginStateManager is the only state machine.
- StateReconciliation compares config + disk with the snapshot, reporting
  enabled-but-not-loaded and older-version-loaded as no_action findings.
- Backups list installed manifests with enabled from config.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-30 10:48:14 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7ab6fb1aff
commit b09434a418
38 changed files with 1893 additions and 940 deletions
+3 -16
View File
@@ -1,8 +1,8 @@
"""Plugin-system JSON files are read as UTF-8, whatever the locale says.
store_manager (install_from_url's manifest, the secrets file) and
state_manager (plugin_state.json) opened text files without an encoding, so
the platform default applied. A manifest written in UTF-8 with a non-ASCII
store_manager (install_from_url's manifest, the secrets file) opened text
files without an encoding, so the platform default applied. (So did the
retired plugin_state.json reader.) A manifest written in UTF-8 with a non-ASCII
name then read as mojibake -- or raised UnicodeDecodeError -- on a host
whose locale encoding is not UTF-8 (Windows' cp1252; a Pi with LANG=C).
On a UTF-8 host these pass either way; they fail on old code where the
@@ -13,7 +13,6 @@ import json
import pytest
from src.plugin_system.state_manager import PluginStateManager
from src.plugin_system.store_manager import PluginStoreManager
NAME = "Météo Á" # "Á" is C3 81 in UTF-8; 0x81 is undefined in cp1252
@@ -48,15 +47,3 @@ def test_install_from_url_reads_a_utf8_manifest(store):
written = json.loads(
(store.plugins_dir / "meteo" / "manifest.json").read_bytes().decode("utf-8"))
assert written["name"] == NAME
def test_state_manager_loads_a_utf8_state_file(tmp_path):
state_file = tmp_path / "plugin_state.json"
state_file.write_bytes(json.dumps({
"version": 1,
"states": {"meteo": {"plugin_id": "meteo", "status": "installed",
"enabled": True, "metadata": {"label": NAME}}},
}, ensure_ascii=False).encode("utf-8"))
mgr = PluginStateManager(state_file=str(state_file))
assert mgr.get_plugin_state("meteo").metadata["label"] == NAME