Files
LEDMatrix/web_interface/widget_bundle.py
T
ChuckandClaude Opus 5.5 7e066174d9 chore: remove dead code, deprecate unused plugin APIs (over-engineering audit)
Whole-tree audit. Every symbol was checked against core, the plugin
monorepo and all eight third-party plugins in plugins.json first.

- Deprecate (removal 3.10.0) plugin-facing methods nothing calls:
  LogoDownloader bulk download, ConfigManager backup/secret wrappers,
  APIHelper extras, BackgroundDataService poll API, PluginManager /
  PluginStateManager info readers, and a few CacheManager, FontManager,
  BaseOddsManager, DynamicTeamResolver methods and PluginTestCase.
  plugin_api_usage.py learns their receiver names; DEPRECATIONS doc
  regenerated.
- Remove core-internal dead code: CacheMetrics, Vegas status/stats
  plumbing, sync "new cycle" message (followers ignore unknown types),
  unused operation types, test-only PluginCatalog readers, IPC to_dict
  and ping, _parse_form_value, CacheStrategyProtocol, ErrorAggregator
  callbacks, duplicate web response helpers.
- Web UI: drop never-mounted json-file-manager.js, the example widget,
  utils/error_handler.js, four uncalled PluginAPI methods, and 29
  escapeHtml shims (call window.LEDEscape directly). Public globals,
  BaseWidget and widget names unchanged.
- Remove six one-off scripts (owner decision) and the unused markupsafe
  and pytest-mock pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:27:40 -04:00

102 lines
3.3 KiB
Python

"""Single-request bundle for the config-form widget scripts.
Every page used to carry one <script> tag per widget (34 of them, ~700 KB
uncompressed). On a Pi Zero 2 W serving a phone over WiFi, each tag is a
separate request the Python server has to answer. The files are plain scripts
that register themselves on window/LEDMatrixWidgets at load time, so
concatenating them in the same order is exactly equivalent to loading them
one by one — no module wrapper, no scope change.
The individual files stay on disk and keep working as direct URLs, which is
what plugin-loader.js and any third-party page expect.
BUNDLE_ORDER is the authority for which widgets ship; test_widget_scripts.py
checks it against the directory so a new widget can't be forgotten.
"""
import hashlib
from pathlib import Path
from threading import Lock
WIDGETS_DIR = Path(__file__).parent / "static" / "v3" / "js" / "widgets"
# Load order matters: the registry and base class must exist before the
# widgets that call them, and notification.js owns window.showNotification.
BUNDLE_ORDER = [
"registry.js",
"base-widget.js",
"notification.js",
"plugin-order-list.js",
"file-upload.js",
"checkbox-group.js",
"custom-feeds.js",
"array-table.js",
"google-calendar-picker.js",
"google-oauth.js",
"day-selector.js",
"time-range.js",
"time-picker.js",
"file-upload-single.js",
"plugin-file-manager.js",
"schedule-picker.js",
# Basic input widgets
"text-input.js",
"number-input.js",
"textarea.js",
"select-dropdown.js",
"font-selector.js",
"style-editor.js",
"toggle-switch.js",
"radio-group.js",
"date-picker.js",
"slider.js",
"color-picker.js",
"email-input.js",
"url-input.js",
"password-input.js",
"timezone-selector.js",
"plugin-loader.js",
]
# Widget files that must NOT be bundled, with the reason.
EXCLUDED = {}
_lock = Lock()
_cache = {"version": None, "body": None}
def bundle_paths():
"""Widget files in load order (only those present on disk)."""
return [WIDGETS_DIR / name for name in BUNDLE_ORDER if (WIDGETS_DIR / name).is_file()]
def bundle_version():
"""Cache-busting token: a fingerprint of every bundled file.
Uses each file's name, nanosecond mtime and size, so an edit that lands
in the same second as another file's, or leaves mtime behind the newest
file, still changes the token (and the URL).
"""
digest = hashlib.sha256()
for path in bundle_paths():
stat = path.stat()
digest.update(f"{path.name}:{stat.st_mtime_ns}:{stat.st_size};".encode())
return digest.hexdigest()[:12]
def build_bundle():
"""Concatenated widget sources, rebuilt only when a file changes."""
version = bundle_version()
with _lock:
if _cache["version"] == version and _cache["body"] is not None:
return _cache["body"], version
parts = []
for path in bundle_paths():
source = path.read_text(encoding="utf-8")
# A file ending in a line comment would swallow the next file's
# first line, and a missing semicolon can join two statements.
parts.append("/* %s */\n%s\n;\n" % (path.name, source))
body = "".join(parts)
_cache["version"] = version
_cache["body"] = body
return body, version