mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-07 07:36:37 +00:00
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>
102 lines
3.3 KiB
Python
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
|