mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-22 10:58:15 +00:00
Merge main into feat/plugin-composer, and fix three review findings
The branch was 57 commits behind and conflicting. I had put the rebase
aside earlier as needing the author's eyes, on the grounds that the PR is
+5091 lines -- but that was the wrong measure. The actual conflict was a
single hunk in app.css, where this branch adds .md\:inline and main added
.md\:block and .md\:w-auto at the same place. All three are kept.
Merging rather than rebasing: the branch is public and 57 commits behind,
so a rebase would rewrite shared history for a force-push.
Three findings fixed on top:
A missing `text` or `format` was a 500. `p` is a copy of the raw element
and the defaults were applied to the locals t1/fmt1 only, so an element
omitting either key left it absent, manager.py.j2 rendered
`{{ el.text | tojson }}` over a jinja2.Undefined, and tojson raised
TypeError -- which no handler catches:
text without 'text': TypeError: Object of type Undefined is not
JSON serializable
clock without 'format': same
Both keys are now set explicitly. Verified: removing either assignment
fails 4 of the new tests.
E741 on my own injection-test file: two `for i, l in enumerate(...)`
loops, which ruff rejects and would fail a lint-gated build. Renamed.
Ruff now clean on all three files this PR touches.
Not done: registering composer_bp. This PR's own description gates it --
"Not yet wired up ... tracking as a follow-up", with an unchecked box for
"Register composer_bp in app.py before merging or exposing this route" --
so it is a deliberate decision, not an oversight. Confirmed the blueprint
appears in no register_blueprint call outside this branch's tests, which
also means the code-injection fixed earlier in this PR was never
reachable in a deployed instance. Worth fixing before the route is
exposed; not worth exposing the route to satisfy a review comment.
Verified on the merged tree: 3850 passed, 1 failed, 60 skipped. The
failure is test_install_lowmem's tmpfs assumption, which is fixed in #492
and not yet on main. The static audit now passes 3/3 -- the twelve
classes it flagged before were defined on main all along and only looked
missing because this branch was behind.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
This commit is contained in:
+1238
-264
File diff suppressed because it is too large
Load Diff
@@ -251,6 +251,12 @@ def _preprocess_elements(elements: list) -> list:
|
||||
if t == 'text':
|
||||
t1 = el.get('text', '') or ''
|
||||
t2 = el.get('text2', '') or ''
|
||||
# p is a copy of the raw element, so a payload omitting these
|
||||
# leaves the key absent and the template renders
|
||||
# {{ el.text | tojson }} over a jinja2.Undefined. tojson then
|
||||
# raises TypeError, which no handler catches -- so a missing
|
||||
# key came back as a 500 rather than a validation error.
|
||||
p['text'] = t1
|
||||
p['text2'] = t2
|
||||
# Detect {variable} tokens — generate format_map() call instead of literal
|
||||
_var_re = re.compile(r'\{([a-zA-Z_]\w*)\}')
|
||||
@@ -260,6 +266,7 @@ def _preprocess_elements(elements: list) -> list:
|
||||
p['x2_expr'] = p['x_expr'] # second line uses same x
|
||||
else: # clock
|
||||
fmt1 = el.get('format', '%H:%M') or '%H:%M'
|
||||
p['format'] = fmt1
|
||||
fmt2 = el.get('format2', '') or ''
|
||||
p['format2'] = fmt2
|
||||
ref_len = max(len(fmt1), len(fmt2)) if fmt2 else len(fmt1)
|
||||
|
||||
@@ -180,10 +180,6 @@ def load_partial(partial_name):
|
||||
return _load_durations_partial()
|
||||
elif partial_name == 'schedule':
|
||||
return _load_schedule_partial()
|
||||
elif partial_name == 'weather':
|
||||
return _load_weather_partial()
|
||||
elif partial_name == 'stocks':
|
||||
return _load_stocks_partial()
|
||||
elif partial_name == 'plugins':
|
||||
return _load_plugins_partial()
|
||||
elif partial_name == 'fonts':
|
||||
@@ -397,12 +393,51 @@ def _load_display_partial():
|
||||
return "Error loading partial", 500
|
||||
|
||||
def _load_durations_partial():
|
||||
"""Load display durations partial"""
|
||||
"""Load rotation & durations partial.
|
||||
|
||||
Builds one duration entry per display mode of every enabled plugin
|
||||
(falling back to the display controller's 30s default), overlaid with any
|
||||
values saved in display.display_durations. Historically the template only
|
||||
looped over saved keys, and nothing ever populated them, so the page
|
||||
rendered empty.
|
||||
"""
|
||||
try:
|
||||
if pages_v3.config_manager:
|
||||
main_config = pages_v3.config_manager.load_config()
|
||||
duration_groups = []
|
||||
covered_keys = set()
|
||||
if pages_v3.plugin_manager:
|
||||
try:
|
||||
pages_v3.plugin_manager.discover_plugins()
|
||||
saved = (main_config.get('display', {}) or {}).get('display_durations', {}) or {}
|
||||
infos = sorted(pages_v3.plugin_manager.get_all_plugin_info(),
|
||||
key=lambda i: (i.get('name') or i.get('id') or '').lower())
|
||||
for info in infos:
|
||||
pid = info.get('id')
|
||||
if not pid or not (main_config.get(pid, {}) or {}).get('enabled', False):
|
||||
continue
|
||||
modes = pages_v3.plugin_manager.get_plugin_display_modes(pid) or [pid]
|
||||
covered_keys.update(modes)
|
||||
duration_groups.append({
|
||||
'plugin_id': pid,
|
||||
'plugin_name': info.get('name') or pid,
|
||||
'modes': [{'key': m, 'value': saved.get(m, 30)} for m in modes],
|
||||
})
|
||||
# Saved keys not owned by any enabled plugin (disabled or
|
||||
# uninstalled plugins) stay visible rather than vanishing.
|
||||
leftovers = [{'key': k, 'value': v} for k, v in saved.items()
|
||||
if k not in covered_keys]
|
||||
if leftovers:
|
||||
duration_groups.append({
|
||||
'plugin_id': '',
|
||||
'plugin_name': 'Other saved entries',
|
||||
'modes': leftovers,
|
||||
})
|
||||
except Exception:
|
||||
logger.warning("durations: could not enumerate plugin modes", exc_info=True)
|
||||
return render_template('v3/partials/durations.html',
|
||||
main_config=main_config)
|
||||
main_config=main_config,
|
||||
duration_groups=duration_groups)
|
||||
except Exception as e:
|
||||
logger.error("Error loading partial", exc_info=True)
|
||||
return "Error loading partial", 500
|
||||
@@ -425,28 +460,6 @@ def _load_schedule_partial():
|
||||
return "Error loading partial", 500
|
||||
|
||||
|
||||
def _load_weather_partial():
|
||||
"""Load weather configuration partial"""
|
||||
try:
|
||||
if pages_v3.config_manager:
|
||||
main_config = pages_v3.config_manager.load_config()
|
||||
return render_template('v3/partials/weather.html',
|
||||
main_config=main_config)
|
||||
except Exception as e:
|
||||
logger.error("Error loading partial", exc_info=True)
|
||||
return "Error loading partial", 500
|
||||
|
||||
def _load_stocks_partial():
|
||||
"""Load stocks configuration partial"""
|
||||
try:
|
||||
if pages_v3.config_manager:
|
||||
main_config = pages_v3.config_manager.load_config()
|
||||
return render_template('v3/partials/stocks.html',
|
||||
main_config=main_config)
|
||||
except Exception as e:
|
||||
logger.error("Error loading partial", exc_info=True)
|
||||
return "Error loading partial", 500
|
||||
|
||||
def _load_plugins_partial():
|
||||
"""Load plugins management partial"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user