Verified each finding against current code.
- api_v3 save_main_config: both duration blocks (*_duration suffix fields
and the newer duration__<mode> fields) only READ from `data`, never
removed the keys. The generic "remaining keys" merge later in the same
function has no skip-list entry for either pattern, so every duration
field was ALSO written a second time as a bogus top-level config key
(e.g. "clock_duration": 30 and "duration__mlb_live": 42 sitting at
config root, alongside the correct nested
display.display_durations.<key>). Confirmed by tracing the full
function. Fixed by popping each handled key from `data` (same pattern
already used for plugin_rotation_order) and validating strictly: a
non-integer duration now returns 400 with a message naming the
offending field/mode instead of silently logging and moving on (for the
*_duration fields, which previously had zero validation at all).
- api_v3 dependency-install loops (git_pull's post-update sync and
install_base_requirements): _pip_install_requirements can raise
subprocess.TimeoutExpired or OSError (confirmed: install_requirements_file
in permission_utils.py never catches either internally, despite its
docstring's "never raises on non-zero exit" only covering return codes).
Both loops previously let one file's exception either abort the whole
try block (skipping the second requirements file entirely) or propagate
uncaught. Each file's install is now in its own try/except, so a timeout
or OSError on one file is recorded as a labeled failure and the loop
continues to the next file.
- test_web_smoke.py: the `client` fixture mutated the module-level
pages_v3 Blueprint singleton's config_manager/plugin_manager directly
with no teardown - since pages_v3 is shared across the whole pytest
process (test_web_settings_ui.py touches the same attributes), this
fixture's mocks could leak into whichever test ran next. Now saves the
originals, yields the client, and restores them in a finally block.
Validation: py_compile passes; all 40 web tests pass with the now-generator
fixture.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Guardrails so this branch's fix classes can't regress silently:
- test_web_smoke.py (24 tests): boots the pages blueprint with the same
dual registration app.py uses and asserts every page/partial returns 200
with its load-bearing markers (nav wiring, getting-started card, advanced
section, rotation order card, per-mode duration inputs), the /v3 legacy
alias serves everything, all critical static assets (incl. vendored
fontawesome/codemirror, PWA manifest/icons) are served, durations group
per plugin with the leftover bucket, and the advanced-hardware section
really contains the tuning fields. Would have caught this session's
unreachable-durations-page and orphaned-tab bugs instantly.
- test_web_static_audit.py (3 tests): (1) every responsive utility class
referenced in templates is actually defined in app.css - the
silently-no-op class bug that left the header search box invisible at
every width; (2) every url_for('static', ...) reference points to a real
file; (3) any JS file calling the debugLog global declares it in a
/* global */ header.
All 40 web tests pass (24 + 3 new, 13 existing) under pytest + Flask.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ