11 Commits
Author SHA1 Message Date
81a022dbe8 fix(web): resolve file upload config lookup for server-rendered forms (#279)
* fix(web): resolve file upload config lookup for server-rendered forms

The file upload widget's getUploadConfig() function failed to map
server-rendered field IDs (e.g., "static-image-images") back to schema
property keys ("images"), causing upload config (plugin_id, endpoint,
allowed_types) to be lost. This could prevent image uploads from
working correctly in the static-image plugin and others.

Changes:
- Add data-* attributes to the Jinja2 file-upload template so upload
  config is embedded directly on the file input element
- Update getUploadConfig() in both file-upload.js and plugins_manager.js
  to read config from data attributes first, falling back to schema lookup
- Remove duplicate handleFiles/handleFileDrop/handleFileSelect from
  plugins_manager.js that overwrote the more robust file-upload.js versions
- Bump cache-busting version strings so browsers fetch updated JS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): harden file upload functions against CodeRabbit patterns

- Add response.ok guard before response.json() in handleFiles,
  deleteUploadedFile, and handleCredentialsUpload to prevent
  SyntaxError on non-JSON error responses (PR #271 finding)
- Remove duplicate getUploadConfig() from plugins_manager.js;
  file-upload.js now owns this function exclusively
- Replace innerHTML with textContent/DOM methods in
  handleCredentialsUpload to prevent XSS (PR #271 finding)
- Fix redundant if-check in getUploadConfig data-attribute reader

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): address CodeRabbit findings on file upload widget

- Add data-multiple="true" discriminator on array file inputs so
  handleFileDrop routes multi-file drops to handleFiles() not
  handleSingleFileUpload()
- Duplicate upload config data attributes onto drop zone wrapper so
  getUploadConfig() survives progress-helper DOM re-renders that
  remove the file input element
- Clear file input in finally block after credentials upload to allow
  re-selecting the same file on retry
- Branch deleteUploadedFile on fileType: JSON deletes remove the DOM
  element directly instead of routing through updateImageList() which
  renders image-specific cards (thumbnails, scheduling controls)

Addresses CodeRabbit findings on PR #279:
- Major: drag-and-drop hits single-file path for array uploaders
- Major: config lookup fails after first upload (DOM node removed)
- Minor: same-file retry silently no-ops
- Major: JSON deletes re-render list as images

Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): address CodeRabbit round-2 findings on file upload widget

- Extract getConfigSourceElement() helper so handleFileDrop,
  handleSingleFileUpload, and getUploadConfig all share the same
  fallback logic: file input → drop zone wrapper
- Remove pluginId gate from getUploadConfig Strategy 1 — fields with
  uploadEndpoint or fileType but no pluginId now return config instead
  of falling through to generic defaults
- Fix JSON delete identifier mismatch: use file.id || file.category_name
  (matching the renderer at line 3202) instead of f.file_id; remove
  regex sanitization on DOM id lookup (renderer doesn't sanitize)

Addresses CodeRabbit round-2 findings on PR #279:
- Major: single-file uploads bypass drop-zone config fallback
- Major: getUploadConfig gated on data-plugin-id only
- Major: JSON delete file identifier mismatch vs renderer

Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): align delete handler file identifier with renderer logic

Remove f.file_id from JSON file delete filter to match the renderer's
identifier logic (file.id || file.category_name || idx). Prevents
deleted entries from persisting in the hidden input on next save.

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: 5ymb01 <noreply@github.com>
2026-03-25 12:57:04 -04:00
31ed854d4e fix(config): deduplicate uniqueItems arrays before schema validation (#292)
* fix(config): deduplicate uniqueItems arrays before schema validation

When saving plugin config via the web UI, the form data is merged with
the existing stored config. If a user adds an item that already exists
(e.g. adding stock symbol "FNMA" when it's already in the list), the
merged array contains duplicates. Schemas with `uniqueItems: true`
then reject the config, making it impossible to save.

Add a recursive dedup pass that runs after normalization/filtering but
before validation. It walks the schema tree, finds arrays with the
uniqueItems constraint, and removes duplicates while preserving order.

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: recurse into array items and add tests for uniqueItems dedup

Address CodeRabbit review: _dedup_unique_arrays now also recurses into
array elements whose items schema is an object, so nested uniqueItems
constraints inside arrays-of-objects are enforced.

Add 11 unit tests covering:
- flat arrays with/without duplicates
- order preservation
- arrays without uniqueItems left untouched
- nested objects (feeds.stock_symbols pattern)
- arrays of objects with inner uniqueItems arrays
- edge cases (empty array, missing keys, integers)
- real-world stock-news plugin config shape

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract dedup_unique_arrays to shared validators module

Move _dedup_unique_arrays from an inline closure in save_plugin_config
to src/web_interface/validators.dedup_unique_arrays so tests import
and exercise the production code path instead of a duplicated copy.

Addresses CodeRabbit review: tests now validate the real function,
preventing regressions from diverging copies.

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: 5ymb01 <noreply@github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:48:26 -04:00
c8737d1a6c fix(api): use sys.executable for plugin action subprocess calls (#277)
* fix(api): use sys.executable for plugin action subprocess calls

The execute_plugin_action endpoint hardcoded 'python3' when spawning
plugin scripts via subprocess. This can fail if the system Python is
named differently or if a virtualenv is active, since 'python3' may
not point to the correct interpreter.

Changes:
- Replace 'python3' with sys.executable in the non-OAuth script
  execution branch (uses the same interpreter running the web service)
- Remove redundant 'import sys' inside the oauth_flow conditional
  block (sys is already imported at module level; the local import
  shadows the top-level binding for the entire function scope, which
  would cause UnboundLocalError if sys were referenced in the else
  branch on Python 3.12+)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(api): replace all remaining hardcoded python3 with sys.executable

Fix 4 additional subprocess calls that still used 'python3' instead of
sys.executable: parameterized action wrapper (line 5150), stdin-param
wrapper (line 5211), no-param wrapper (line 5417), and OAuth auth
script (line 5524). Ensures plugin actions work in virtualenvs and
non-standard Python installations.

Addresses CodeRabbit findings on PR #277.

Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 12:21:18 -04:00
28a374485f fix(test): repair test infrastructure and mock fixtures (#281)
* fix(test): repair test infrastructure and mock fixtures

- Add test/__init__.py for proper test collection
- Fix ConfigManager instantiation to use config_path parameter
- Route schedule config through config_service mock
- Update mock to match get_raw_file_content endpoint change

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): correct get_main_config assertion per CodeRabbit review

The endpoint calls load_config(), not get_raw_file_content('main').
Also set up load_config mock return value in the fixture so the
test's data assertions pass correctly.

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): correct plugin config test mock structure and schema returns

- Plugin configs live at top-level keys, not under 'plugins' subkey
- Mock schema_manager.generate_default_config to return a dict
- Mock schema_manager.merge_with_defaults to merge dicts (not MagicMock)
- Fixes test_get_plugin_config returning 500 due to non-serializable MagicMock

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): use patch.object for config_service.get_config in schedule tests

config_service.get_config is a real method, not a mock — can't set
return_value on it directly. Use patch.object context manager instead.

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: 5ymb01 <noreply@github.com>
2026-03-20 15:06:58 -04:00
f3e7c639ba fix: narrow bare except blocks to specific exception types (#282)
Replace 6 bare `except:` blocks with targeted exception types:
- logo_downloader.py: OSError for file removal, (OSError, IOError) for font loading
- layout_manager.py: (ValueError, TypeError, KeyError, IndexError) for format string
- app.py: (OSError, ValueError) for CPU temp, (SubprocessError, OSError) for systemctl, (KeyError, TypeError, ValueError) for config parsing

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: 5ymb01 <noreply@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:00:12 -04:00
f718305886 fix(security): stop leaking Python tracebacks to HTTP clients (#283)
* fix(security): stop leaking Python tracebacks to HTTP clients

Replace 13 instances where traceback.format_exc() was sent in API
JSON responses (via `details=`, `traceback:`, or `details:` keys).

- 5 error_response(details=traceback.format_exc()) → generic message
- 6 jsonify({'traceback': traceback.format_exc()}) → removed key
- 2 jsonify({'details': error_details}) → logger.error() instead

Tracebacks in debug mode (app.py error handlers) are preserved as
they are guarded by app.debug and expected during development.

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): sanitize str(e) from client responses, add server-side logging

Address CodeRabbit review findings:
- Replace str(e) in error_response message fields with generic messages
- Replace import logging/traceback + manual format with logger.exception()
- Add logger.exception() to 6 jsonify handlers that were swallowing errors
- All exception details now logged server-side only, not sent to clients

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove duplicate traceback logging, sanitize secrets config error

Address CodeRabbit nitpicks:
- Remove manual import logging/traceback + logging.error() that duplicated
  the logger.exception() call in save_raw_main_config
- Apply same fix to save_raw_secrets_config: replace str(e) in client
  response with generic message, use logger.exception() for server-side

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: 5ymb01 <noreply@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 14:51:05 -04:00
f0dc094cd6 fix(security): use Path.relative_to() for path confinement (#284)
* fix(security): use Path.relative_to() for path confinement check

Replace str.startswith() path check with Path.relative_to() in the
plugin file viewer endpoint. startswith() can be bypassed when a
directory name is a prefix of another (e.g., /plugins/foo vs
/plugins/foobar). relative_to() correctly validates containment.

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trigger CodeRabbit review

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: 5ymb01 <noreply@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 10:04:49 -04:00
178dfb0c2a fix(perf): cache fonts in sport base classes to avoid disk I/O per frame (#285)
* fix(perf): cache fonts in sport base classes to avoid disk I/O per frame

Replace 7 ImageFont.truetype() calls in display methods with cached
self.fonts['detail'] lookups. The 4x6-font.ttf at size 6 is already
loaded once in _load_fonts() — loading it again on every display()
call causes unnecessary disk I/O on each render frame (~30-50 FPS).

Files: sports.py (2), football.py (1), hockey.py (2), basketball.py (1), baseball.py (1)

Co-Authored-By: 5ymb01 <noreply@github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: trigger CodeRabbit review

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: 5ymb01 <noreply@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 09:59:58 -04:00
76c5bf5781 fix(security): mask secret fields in API responses and extract helpers (#276)
* fix(security): mask secret fields in API responses and extract helpers

GET /config/secrets returned raw API keys in plaintext to the browser.
GET /plugins/config returned merged config including deep-merged secrets.
POST /plugins/config could overwrite existing secrets with empty strings
when the GET endpoint returned masked values that were sent back unchanged.

Changes:
- Add src/web_interface/secret_helpers.py with reusable functions:
  find_secret_fields, separate_secrets, mask_secret_fields,
  mask_all_secret_values, remove_empty_secrets
- GET /config/secrets: mask all values with '••••••••'
- GET /plugins/config: mask x-secret fields with ''
- POST /plugins/config: filter empty-string secrets before saving
- pages_v3: mask secrets before rendering plugin config templates
- Remove three duplicated inline find_secret_fields/separate_secrets
  definitions in api_v3.py (replaced by single imported module)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): harden secret masking against CodeRabbit findings

- Fail-closed: return 500 when schema unavailable instead of leaking secrets
- Fix falsey masking: use `is not None and != ''` instead of truthiness check
  so values like 0 or False are still redacted
- Add array-item secret support: recurse into `type: array` items schema
  to detect and mask secrets like accounts[].token
- pages_v3: fail-closed when schema properties missing

Addresses CodeRabbit findings on PR #276:
- Critical: fail-closed bypass when schema_mgr/schema missing
- Major: falsey values not masked (0, False leak through)
- Major: pages_v3 fail-open when schema absent
- Major: array-item secrets unsupported

Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:41:18 -04:00
feee1dffde fix(web): remove shadowed sys import in plugin action handler (#280)
* fix(web): remove shadowed sys import in plugin action handler

Two `import sys` statements inside execute_plugin_action() and
authenticate_spotify() shadowed the module-level import, causing
"cannot access local variable 'sys'" errors when sys.executable
was referenced in earlier branches of the same function.

Also fixes day number validation in the of-the-day upload endpoint
to accept 366 (leap year).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(api): correct validation message from 1-365 to 1-366

The JSON structure validation message still said '1-365' while the
actual range check accepts 1-366 for leap years. Make all three
validation messages consistent.

Addresses CodeRabbit finding on PR #280.

Co-Authored-By: 5ymb01 <5ymb01@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:38:04 -04:00
f05c357d57 fix(config): use correct plugin ID key in secrets template (#275)
The secrets template used "weather" as the key, but the weather plugin's
ID is "ledmatrix-weather". Since ConfigManager deep-merges secrets into
the main config by key, secrets under "weather" never reached the plugin
config at config["ledmatrix-weather"], making the API key invisible to
the plugin.

Co-authored-by: 5ymb01 <5ymb01@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:31:38 -05:00