mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-16 10:23:31 +00:00
5dde1125e988f3d0f729e6e181e8455689c2473d
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
05b3fa56cb |
fix: Codacy security fixes, CVE dependency bumps, and code quality cleanup (#331)
* fix(deps): bump minimum versions to address CVEs Pillow 10.4.0 → 12.2.0: CVE-2026-40192 (DoS via FITS decompression bomb), CVE-2026-25990 (OOB write via PSD image), CVE-2026-42311/42308/42310 requests 2.32.0 → 2.33.0: CVE-2026-25645 (temp file security bypass), CVE-2024-47081 (.netrc credentials leak) werkzeug 3.0.0 → 3.1.6: CVE-2023-46136, CVE-2024-49766/49767, CVE-2025-66221, CVE-2026-21860/27199 (DoS, path traversal, safe_join bypass) Flask 3.0.0 → 3.1.3: CVE-2026-27205 (session data caching info disclosure) spotipy 2.24.0 → 2.25.2: CVE-2025-27154, CVE-2025-66040 python-socketio 5.11.0 → 5.14.0: CVE-2025-61765 pytest 7.4.0 → 9.0.3: CVE-2025-71176 (insecure temp dir handling) Updated in requirements.txt, web_interface/requirements.txt, plugin-repos/starlark-apps/requirements.txt, and plugin-repos/march-madness/requirements.txt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve Pylint errors in executor, data service, and odds call Rename TimeoutError to PluginTimeoutError in plugin_executor.py to avoid shadowing the built-in; no external callers affected. Remove dead try/except in BackgroundDataService.shutdown: executor.shutdown() never accepted a timeout kwarg so the try branch always raised TypeError. Simplify to a direct shutdown(wait=wait) call. Remove is_live kwarg from odds_manager.get_odds() call in sports.py; BaseOddsManager.get_odds() has no such parameter. The live update interval is already encoded in the update_interval_seconds argument passed alongside. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: MD5→SHA-256, shellcheck warnings, and broken doc links config_service.py: replace MD5 with SHA-256 for config change detection; same semantics (equality comparison), no stored hashes affected. Shell scripts — shellcheck warnings: - diagnose_web_interface.sh: remove useless cat (SC2002) - dev_plugin_setup.sh: restructure A&&B||C into if/then (SC2015) - fix_assets_permissions.sh: remove unused REAL_HOME block (SC2034) - install_web_service.sh: remove unused USER_HOME assignment (SC2034) - diagnose_web_ui.sh: remove unused SUDO assignments (SC2034) - diagnose_plugin_permissions.sh: remove unused BLUE color var (SC2034) - first_time_install.sh: remove unused CLEAR var, PACKAGE_NAME assignment, and replace loop variable with _ (SC2034) docs/PLUGIN_ARCHITECTURE_SPEC.md: fix 10 broken TOC anchor links to include section numbers matching the actual headings (MD051). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove unused imports and bare exception aliases (pyflakes F401/F841) Remove unused imports across 86 files in src/, web_interface/, test/, and scripts/ using autoflake. No logic changes — only dead import statements and unused names in from-imports are removed. Also remove bare exception aliases where the variable is never referenced in the handler body: - src/cache/disk_cache.py: except (IOError, OSError, PermissionError) as e - src/cache_manager.py: except (OSError, IOError, PermissionError) as perm_error - src/plugin_system/resource_monitor.py: except Exception as e - web_interface/app.py: except Exception as read_err 86 files changed, 205 lines removed, 18 pre-existing test failures unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove unused local variable assignments (pyflakes F841) Dead assignments removed across src/ and web_interface/: - background_data_service: drop future= on fire-and-forget executor.submit - base_classes/baseball: drop font= (all rendering uses self.fonts['time']) - base_classes/hockey: drop status_short= (never referenced after assignment) - common/cli: drop game_helper=/config_helper= bindings in import-test block; constructors called for instantiation-only validation - common/display_helper: drop text_width= (x_position uses display_width directly); drop draw= in create_error_image (uses _draw_centered_text) - config_manager: remove dead secrets_content loading block in migration path (comment already noted save_config_atomic handles secrets internally) - display_manager: drop setup_start= (timing was never completed or read) - font_manager: drop target_path= (catalog uses font_file_path directly); drop face=/font= bindings in validate_font (validation by construction — TypeError on failure is the signal, not the return value) - font_test_manager: drop width=/height= (draw_text uses display_manager directly) - plugin_system/state_reconciliation: drop manager= (only config/disk/state_mgr used) - plugin_system/store_manager: drop result= on pip install subprocess.run (check=True raises on failure; stdout unused) - web_interface/blueprints/pages_v3: drop main_config_path=""/secrets_config_path="" (render_template uses config_manager.get_*_path() inline) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(js): resolve ESLint no-undef warnings across 6 JS files Three distinct patterns: 1. Vendor library globals — htmx is injected by <script> before these extension files load; ESLint lints files in isolation and doesn't know. Fix: add /* global htmx */ to htmx-sse.js and htmx-json-enc.js. 2. Cross-file globals — showNotification is defined as window.showNotification in app.js/notification.js but called bare in app.js and error_handler.js. ESLint doesn't connect window.X = Y with a bare call to X. Fix: add /* global showNotification */ to app.js and error_handler.js. 3. Forward-reference window.* functions — in array-table.js, checkbox-group.js, and custom-feeds.js, functions like removeArrayTableRow are called early inside event-handler closures but assigned to window.* later in the file. At runtime this works (the handler fires after the assignment), but ESLint sees the bare name at the call site. Fix: change bare calls to window.removeArrayTableRow(this) etc. so the reference is explicit and ESLint-safe. Also guard the updateSystemStats call in app.js reconnectSSE: the function is called but defined nowhere in the codebase. Guard with typeof check so it won't throw ReferenceError if the reconnect path is hit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(js): resolve Biome lint warnings across 9 JS files noUnusedVariables (catch bindings → optional catch syntax): - app.js, file-upload.js, timezone-selector.js: } catch (e) { → } catch { ES2019 optional catch binding; e was unused in all three handlers noUnusedVariables (dead assignments): - app.js: remove const data= in display SSE stub (handler does nothing yet) - api_client.js: remove const timeoutId= (setTimeout ID never used to cancel) - custom-feeds.js: remove const oldIndex= (getAttribute result never read) - schedule-picker.js: remove const compactMode= (never used in HTML build) - select-dropdown.js: remove const icons= (icons not yet rendered in options) noPrototypeBuiltins: - day-selector.js: DAY_LABELS.hasOwnProperty(x) → Object.prototype.hasOwnProperty.call(DAY_LABELS, x) Safe form that works even on null-prototype objects useIterableCallbackReturn: - file-upload.js, notification.js: forEach(x => expr) → forEach(x => { expr; }) — forEach ignores return values; implicit return from arrow body was misleading htmx-sse.js is a vendor extension file with old-style var/== patterns that are correct for it; 18 Biome issues suppressed via Codacy API rather than modifying the vendor source. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(security): escape user input in raw HTML responses in pages_v3.py plugin_id comes directly from the URL path (/partials/plugin-config/<plugin_id>) and was interpolated into an HTML fragment without escaping. A crafted URL like /partials/plugin-config/<script>alert(1)</script> would inject that tag into the DOM via the HTMX partial response. Fix: wrap all user-controlled values in markupsafe.escape() before embedding in raw HTML strings. Affects the plugin-not-found 404 response and both error 500 responses in the plugin config partial. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address Bandit B108/B110 across production code B110 (try/except/pass): - display_controller.py: narrow 'except Exception' to 'except AttributeError' for get_offset_frame() — plugins not having this optional method is the expected case, not all exceptions - config_manager.py: B110 already resolved by the earlier removal of the dead secrets-loading block (the except/pass was inside it) - All other except/pass blocks in src/ and web_interface/ are intentional (last-resort recovery, best-effort fallbacks, non-critical startup probes). Annotated each with # nosec B110 and a brief inline reason so the decision is explicit for future reviewers. - Test files and plugin-repos B110 suppressed via Codacy API (not prod code). B108 (/tmp usage): - permission_utils.py: /tmp listed to PREVENT permission changes on it — not used as a temp path. Annotated # nosec B108. - display_manager.py: fixed snapshot path is intentional (web UI reads same path); path-check guard also annotated. - wifi_manager.py: named /tmp files match the sudoers allowlist installed with the system (the paths are hard-coded in both places by design). Annotated all six open/cp references # nosec B108. - scripts/render_plugin.py: dev script default overridable by user. Annotated. - web_interface/app.py: reads the same fixed path written by display_manager. Annotated # nosec B108. - Test files suppressed via Codacy API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address remaining Codacy security findings Flask debug=True (real fix): - web_interface/app.py: debug=True in __main__ block exposes the Werkzeug interactive debugger (arbitrary code execution). Changed to os.environ.get('FLASK_DEBUG', '0') == '1' — off by default, opt-in via environment variable for local development. nosec annotations (accepted risk with documented rationale): - disk_cache.py: os.chmod(0o660) is intentional — web UI and LED matrix service share a group, 660 gives group write while denying world access (B103 + Semgrep insecure-file-permissions suppressed in Codacy) - wifi_manager.py: urlopen to hardcoded connectivity-check.ubuntu.com URL (B310 — no user input involved) - font_manager.py: urlretrieve URL comes from user's own config file on their local device (B310) - start_web_conditionally.py: os.execvp with both sys.executable and a fixed PROJECT_DIR-relative constant (B606) Confirmed false positives suppressed via Codacy API (15 issues): - SSRF (3x): client-side JS fetch — SSRF is server-side; browser fetch is CORS-restricted to same origin - B105 (3x): test fixtures use dummy secrets by design; store_manager checks for the placeholder string, it is not itself a secret - PMD numeric literal (2x): 10000000 is within Number.MAX_SAFE_INTEGER - Prototype pollution (1x): read-only schema traversal, no writes - no-unsanitized_method (1x): dynamic import() is CORS-restricted - detect-unsafe-regex (1x): operates on server-controlled config values - plugin-repos B103 (1x): vendor code chmod on executable - Semgrep insecure-file-permissions (3x): same disk_cache 0o660 as above Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove unnecessary f prefix from f-strings without placeholders (F541) Pyflakes F541 flags f-strings that contain no {} interpolation — they are identical to plain strings but trigger unnecessary string formatting overhead. Fixed in production code: - src/base_classes/data_sources.py (2 debug log calls) - src/logo_downloader.py (1 error log) - src/plugin_system/store_manager.py (5 strings across 3 log calls) - src/web_interface/validators.py (1 return value) - src/wifi_manager.py (4 log/message strings) - web_interface/start.py (1 print) F541 issues in test/, scripts/, and plugin-repos/ suppressed via Codacy API as non-production code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(dev): add Pillow compatibility smoke test script Covers all Pillow APIs used in LEDMatrix — image creation, drawing, font metrics, LANCZOS resampling, paste/alpha_composite, and PNG I/O. Run after any Pillow version bump to catch regressions before deploy. python3 scripts/dev/test_pillow_compat.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve 8 new Codacy issues introduced by PR changes shellcheck SC2034: - first_time_install.sh: 'type' loop variable also unused in the wifi status loop (we previously fixed 'device' → '_' but left 'type'). Changed to '_ _ state' since neither device nor type is referenced. ESLint no-undef: - app.js: typeof guards don't satisfy no-undef; added updateSystemStats to the /* global */ declaration alongside showNotification. nosec annotation: - web_interface/app.py: app.run(host='0.0.0.0') line changed when we fixed debug=True, giving it a new issue ID. Re-added # nosec B104. pyflakes F401: - scripts/dev/test_pillow_compat.py: ImageFilter was imported but never used in the smoke test. Removed from the import. Codacy API suppressions (false positives on changed lines): - disk_cache.py 0o660 chmod (2x): lines changed when # nosec B103 was added, producing new Semgrep issue IDs. Re-suppressed. - pages_v3.py raw-html-concat: Semgrep does not recognise escape() as a sanitizer; the escape() call IS the correct fix. - app.py flask 0.0.0.0: same line as B104 above; Semgrep rule also re-suppressed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address PR review findings Fix (10 of 15 findings): plugin-repos/march-madness/requirements.txt: Add urllib3>=1.26.0 — manager.py directly imports from urllib3; it was an undeclared transitive dependency via requests. scripts/dev/dev_plugin_setup.sh: Restore subshell form (cd "$target_dir" && git pull --rebase) || true so the shell's working directory is not permanently changed after the if-cd block. Previous fix for SC2015 leaked cwd into the remainder of the script. src/base_classes/sports.py: Narrow 'except Exception' to 'except RuntimeError as e' and log via self.logger.debug — Path.home() raises only RuntimeError for service users; other exceptions should not be silently swallowed. src/config_service.py: Fix stale "MD5 checksum" in ConfigVersion.__init__ docstring (line 40); the implementation uses SHA-256 since the Codacy fix. src/wifi_manager.py: Log the last-resort AP enable failure with exc_info=True instead of silently passing — failure here means the device may be unreachable. web_interface/blueprints/pages_v3.py: Log the outer metadata pre-load exception at debug level instead of swallowing it silently; schema still loads fully below. src/background_data_service.py: Remove unused 'timeout' parameter from shutdown() — executor.shutdown() does not accept timeout; update __del__ caller accordingly. src/font_manager.py: Validate URL scheme before urlretrieve — reject non-http/https schemes (e.g. file://) to prevent reading local files from config-supplied URLs. src/plugin_system/plugin_executor.py: Simplify redundant except tuple: (PluginTimeoutError, PluginError, Exception) → Exception, which already covers the others. test/test_display_controller.py: Mark empty test_plugin_discovery_and_loading as @pytest.mark.skip with reason. Move duplicate 'from datetime import datetime' to module header and remove the stray mid-module copy. Skip (5 of 15 findings, with reasons): - pytest 9.0.3 concerns: full suite already verified (467 pass, 18 pre-existing) - Pillow 12.2.0 API concerns: no deprecated APIs in codebase; tests + Pi smoke test pass - diagnose_web_ui.sh sudo validation: set -e already ensures fail-fast on any sudo failure - app.py request-logging except: must stay silent (recursive logging risk); annotated - app.py SSE file-read except: genuinely transient I/O; annotated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
14b6a0c6a3 |
fix(web): handle dotted keys in schema/config path helpers (#260)
* fix(web): handle dotted keys in schema/config path helpers Schema property names containing dots (e.g. "eng.1" for Premier League in soccer-scoreboard) were being incorrectly split on the dot separator in two path-navigation helpers: - _get_schema_property: split "leagues.eng.1.favorite_teams" into 4 segments and looked for "eng" in leagues.properties, which doesn't exist (the key is literally "eng.1"). Returned None, so the field type was unknown and values were not parsed correctly. - _set_nested_value: split the same path into 4 segments and created config["leagues"]["eng"]["1"]["favorite_teams"] instead of the correct config["leagues"]["eng.1"]["favorite_teams"]. Both functions now use a greedy longest-match approach: at each level they try progressively longer dot-joined candidates first (e.g. "eng.1" before "eng"), so dotted property names are handled transparently. Fixes favorite_teams (and other per-league fields) not saving via the soccer-scoreboard plugin config UI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove debug artifacts from merged branches - Replace print() with logger.warning() for three error handlers in api_v3.py that bypassed the structured logging infrastructure - Simplify dead if/else in loadInstalledPlugins() — both branches did the same window.installedPlugins assignment; collapse to single line - Remove console.log registration line from schedule-picker widget that fired unconditionally on every page load Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
0d5510d8f7 |
Fix/plugin module namespace collision (#229)
* fix(web): handle string boolean values in schedule-picker widget The normalizeSchedule function used strict equality (===) to check the enabled field, which would fail if the config value was a string "true" instead of boolean true. This could cause the checkbox to always appear unchecked even when the setting was enabled. Added coerceToBoolean helper that properly handles: - Boolean true/false (returns as-is) - String "true", "1", "on" (case-insensitive) → true - String "false" or other values → false Applied to both main schedule enabled and per-day enabled fields. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: trim whitespace in coerceToBoolean string handling * fix: normalize mode value to handle per_day and per-day variants * fix(plugins): resolve module namespace collisions between plugins When multiple plugins have modules with the same name (e.g., data_fetcher.py), Python's sys.modules cache would return the wrong module. This caused plugins like ledmatrix-stocks to fail loading because it imported data_fetcher from ledmatrix-leaderboard instead of its own. Added _clear_conflicting_modules() to remove cached plugin modules from sys.modules before loading each plugin, ensuring correct module resolution. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
18fecd3cda |
fix(web): handle string boolean values in schedule-picker widget (#227)
* fix(web): handle string boolean values in schedule-picker widget The normalizeSchedule function used strict equality (===) to check the enabled field, which would fail if the config value was a string "true" instead of boolean true. This could cause the checkbox to always appear unchecked even when the setting was enabled. Added coerceToBoolean helper that properly handles: - Boolean true/false (returns as-is) - String "true", "1", "on" (case-insensitive) → true - String "false" or other values → false Applied to both main schedule enabled and per-day enabled fields. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: trim whitespace in coerceToBoolean string handling --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
a8c85dd015 |
feat(widgets): add modular widget system for schedule and common inputs (#213)
* feat(widgets): add modular widget system for schedule and common inputs Add 15 new reusable widgets following the widget registry pattern: - schedule-picker: composite widget for enable/mode/time configuration - day-selector: checkbox group for days of the week - time-range: paired start/end time inputs with validation - text-input, number-input, textarea: enhanced text inputs - toggle-switch, radio-group, select-dropdown: selection widgets - slider, color-picker, date-picker: specialized inputs - email-input, url-input, password-input: validated string inputs Refactor schedule.html to use the new schedule-picker widget instead of inline JavaScript. Add x-widget support in plugin_config.html for all new widgets so plugins can use them via schema configuration. Fix form submission for checkboxes by using hidden input pattern to ensure unchecked state is properly sent via JSON-encoded forms. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): improve security, validation, and form binding across widgets - Fix XSS vulnerability: escapeHtml now escapes quotes in all widget fallbacks - color-picker: validate presets with isValidHex(), use data attributes - date-picker: add placeholder attribute support - day-selector: use options.name for hidden input form binding - password-input: implement requireUppercase/Number/Special validation - radio-group: fix value injection using this.value instead of interpolation - schedule-picker: preserve day values when disabling (don't clear times) - select-dropdown: remove undocumented searchable/icons options - text-input: apply patternMessage via setCustomValidity - time-range: use options.name for hidden inputs - toggle-switch: preserve configured color from data attribute - url-input: combine browser and custom protocol validation - plugin_config: add widget support for boolean/number types, pass name to day-selector - schedule: handle null config gracefully, preserve explicit mode setting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): validate day-selector input, consistent minLength default, escape JSON quotes - day-selector: filter incoming selectedDays to only valid entries in DAYS array (prevents invalid persisted values from corrupting UI/state) - password-input: use default minLength of 8 when not explicitly set (fixes inconsistency between render() and onInput() strength meter baseline) - plugin_config.html: escape single quotes in JSON hidden input values (prevents broken attributes when JSON contains single quotes) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(widgets): add global notification widget, consolidate duplicated code - Create notification.js widget with toast-style notifications - Support for success, error, warning, info types - Auto-dismiss with configurable duration - Stacking support with max notifications limit - Accessible with aria-live and role="alert" - Update base.html to load notification widget early - Replace duplicate showNotification in raw_json.html - Simplify fonts.html fallback notification - Net reduction of ~66 lines of duplicated code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): escape options.name in all widgets, validate day-selector format Security fixes: - Escape options.name attribute in all 13 widgets to prevent injection - Affected: color-picker, date-picker, email-input, number-input, password-input, radio-group, select-dropdown, slider, text-input, textarea, toggle-switch, url-input Defensive coding: - day-selector: validate format option exists in DAY_LABELS before use - Falls back to 'long' format for unsupported/invalid format values Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(plugins): add type="button" to control buttons, add debug logging - Add type="button" attribute to refresh, update-all, and restart buttons to prevent potential form submission behavior - Add console logging to diagnose button click issues: - Log when event listeners are attached (and whether buttons found) - Log when handler functions are called Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): improve security and validation across widget inputs - color-picker.js: Add sanitizeHex() to validate hex values before HTML interpolation, ensuring only safe #rrggbb strings are used - day-selector.js: Escape inputName in hidden input name attribute - number-input.js: Sanitize and escape currentValue in input element - password-input.js: Validate minLength as non-negative integer, clamp invalid values to default of 8 - slider.js: Add null check for input element before accessing value - text-input.js: Clear custom validity before checkValidity() to avoid stale errors, re-check after setting pattern message - url-input.js: Normalize allowedProtocols to array, filter to valid protocol strings, and escape before HTML interpolation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): add defensive fallback for DAY_LABELS lookup in day-selector Extract labelMap with fallback before loop to ensure safe access even if format validation somehow fails. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(widgets): add timezone-selector widget with IANA timezone dropdown - Create timezone-selector.js widget with comprehensive IANA timezone list - Group timezones by region (US & Canada, Europe, Asia, etc.) - Show current UTC offset for each timezone - Display live time preview for selected timezone - Update general.html to use timezone-selector instead of text input - Add script tag to base.html for widget loading Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ui): suppress on-demand status notification on page load Change loadOnDemandStatus(true) to loadOnDemandStatus(false) during initPluginsPage() to prevent the "on-demand status refreshed" notification from appearing every time a tab is opened or the page is navigated. The notification should only appear on explicit user refresh. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * style(ui): soften notification close button appearance Replace blocky FontAwesome X icon with a cleaner SVG that has rounded stroke caps. Make the button circular, slightly transparent by default, and add smooth hover transitions for a more polished look. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): multiple security and validation improvements - color-picker.js: Ensure presets is always an array before map/filter - number-input.js: Guard against undefined options parameter - number-input.js: Sanitize and escape min/max/step HTML attributes - text-input.js: Clear custom validity in onInput to unblock form submit - timezone-selector.js: Replace legacy Europe/Belfast with Europe/London - url-input.js: Use RFC 3986 scheme pattern for protocol validation - general.html: Use |tojson filter to escape timezone value safely Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(url-input): centralize RFC 3986 protocol validation Extract protocol normalization into reusable normalizeProtocols() helper function that validates against RFC 3986 scheme pattern. Apply consistently in render, validate, and onInput to ensure protocols like "git+ssh", "android-app" are properly handled everywhere. Also lowercase protocol comparison in isValidUrl(). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(timezone-selector): use hidden input for form submission Replace direct select name attribute with a hidden input pattern to ensure timezone value is always properly serialized in form submissions. The hidden input is synced on change and setValue calls. This matches the pattern used by other widgets and ensures HTMX json-enc properly captures the value. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(general): preserve timezone dropdown value after save Add inline script to sync the timezone select with the hidden input value after form submission. This prevents the dropdown from visually resetting to the old value while the save has actually succeeded. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): preserve timezone selection across form submission Use before-request handler to capture the selected timezone value before HTMX processes the form, then restore it in after-request. This is more robust than reading from the hidden input which may also be affected by form state changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): add HTMX protection to timezone selector Add global HTMX event listeners in the timezone-selector widget that preserve the selected value across any form submissions. This is more robust than form-specific handlers as it protects the widget regardless of how/where forms are submitted. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug(widgets): add logging and prevent timezone widget re-init Add debug logging and guards to prevent the timezone widget from being re-initialized after it's already rendered. This should help diagnose why the dropdown is reverting after save. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * debug: add console logging to timezone HTMX protection * debug: add onChange logging to trace timezone selection * fix(widgets): use selectedIndex to force visual update in timezone dropdown The browser's select.value setter sometimes doesn't trigger a visual update when optgroup elements are present. Using selectedIndex instead forces the browser to correctly update the visible selection. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): force browser repaint on timezone dropdown restore Adding display:none/reflow/display:'' pattern to force browser to visually update the select element after changing selectedIndex. Increased timeout to 50ms for reliability. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(widgets): remove debug logging from timezone selector Clean up console.log statements that were used for debugging the timezone dropdown visual update issue. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ui): improve HTMX after-request handler in general settings - Parse xhr.responseText with JSON.parse in try/catch instead of using nonstandard responseJSON property - Check xhr.status for 2xx success range - Show error notification for non-2xx responses - Default to safe fallback values if JSON parsing fails Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): add input sanitization and timezone validation - Sanitize minLength/maxLength in text-input.js to prevent attribute injection (coerce to integers, validate range) - Update Europe/Kiev to Europe/Kyiv (canonical IANA identifier) - Validate timezone currentValue against TIMEZONE_GROUPS before rendering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ui): correct error message fallback in HTMX after-request handler Initialize message to empty string so error responses can use the fallback 'Failed to save settings' when no server message is provided. Previously, the truthy default 'Settings saved' would always be used. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): add constraint normalization and improve value validation - text-input: normalize minLength/maxLength so maxLength >= minLength - timezone-selector: validate setValue input against TIMEZONE_GROUPS - timezone-selector: sync hidden input to actual selected value - timezone-selector: preserve empty selections across HTMX requests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(widgets): simplify HTMX restore using select.value and dispatch change event Replace selectedIndex manipulation with direct value assignment for cleaner placeholder handling, and dispatch change event to refresh timezone preview. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |