From 32aa8a8ce63b23f77e152be38786652bad55c6fa Mon Sep 17 00:00:00 2001 From: Chuck Date: Wed, 28 Jan 2026 08:51:18 -0500 Subject: [PATCH 1/3] fix(web): ensure unchecked boolean checkboxes save as false HTML checkboxes don't submit values when unchecked. The plugin config save endpoint starts from existing config (for partial updates), so an unchecked checkbox's old `true` value persists. Additionally, merge_with_defaults fills in schema defaults for missing fields, causing booleans with `"default": true` to always re-enable. This affected the odds-ticker plugin where NFL/NBA leagues (default: true) could not be disabled via the checkbox UI, while NHL (default: false) appeared to work by coincidence. Changes: - Add _set_missing_booleans_to_false() that walks the schema after form processing and sets any boolean field absent from form data to false - Add value="true" to boolean checkboxes so checked state sends "true" instead of "on" (proper boolean parsing) - Handle "on"/"off" strings in _parse_form_value_with_schema for backwards compatibility with checkboxes lacking value="true" Co-Authored-By: Claude Opus 4.5 --- web_interface/blueprints/api_v3.py | 50 +++++++++++++++++-- .../templates/v3/partials/plugin_config.html | 3 +- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 381b0d627..594010851 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -3088,10 +3088,10 @@ def _parse_form_value_with_schema(value, key_path, schema): if isinstance(value, str): stripped = value.strip() - # Check for boolean strings - if stripped.lower() == 'true': + # Check for boolean strings (includes "on"/"off" from HTML checkboxes) + if stripped.lower() in ('true', 'on'): return True - if stripped.lower() == 'false': + if stripped.lower() in ('false', 'off'): return False # Handle arrays based on schema @@ -3194,6 +3194,43 @@ def _set_nested_value(config, key_path, value): current[parts[-1]] = value +def _set_missing_booleans_to_false(config, schema_props, form_keys, prefix=''): + """Walk schema and set missing boolean form fields to False. + + HTML checkboxes don't submit values when unchecked. When saving plugin config, + the backend starts from existing config (to support partial form updates), which + means an unchecked checkbox's old ``True`` value persists. This function detects + boolean schema properties not present in the form submission and explicitly sets + them to ``False``. + + The top-level ``enabled`` field is excluded because it has its own preservation + logic in the save endpoint. + + Args: + config: The plugin config dict being built + schema_props: Schema ``properties`` dict at the current nesting level + form_keys: Set of form field names that were submitted + prefix: Dot-notation prefix for the current nesting level + """ + for prop_name, prop_schema in schema_props.items(): + if not isinstance(prop_schema, dict): + continue + + full_path = f"{prefix}.{prop_name}" if prefix else prop_name + prop_type = prop_schema.get('type') + + if prop_type == 'boolean' and full_path != 'enabled': + # If this boolean wasn't submitted in the form, it's an unchecked checkbox + if full_path not in form_keys: + _set_nested_value(config, full_path, False) + + elif prop_type == 'object' and 'properties' in prop_schema: + # Recurse into nested objects + _set_missing_booleans_to_false( + config, prop_schema['properties'], form_keys, full_path + ) + + def _enhance_schema_with_core_properties(schema): """ Enhance schema with core plugin properties (enabled, display_duration, live_priority). @@ -3679,6 +3716,13 @@ def ensure_array_defaults(config_dict, schema_props, prefix=''): feeds_config['custom_feeds'] = [custom_feeds_dict[k] for k in sorted_keys] logger.info(f"Force-converted feeds.custom_feeds from dict to array: {len(feeds_config['custom_feeds'])} items") + # Fix unchecked boolean checkboxes: HTML checkboxes don't submit values + # when unchecked, so the existing config value (potentially True) persists. + # Walk the schema and set any boolean fields missing from form data to False. + if schema and 'properties' in schema: + form_keys = set(request.form.keys()) + _set_missing_booleans_to_false(plugin_config, schema['properties'], form_keys) + # Get schema manager instance (for JSON requests) schema_mgr = api_v3.schema_manager if not schema_mgr: diff --git a/web_interface/templates/v3/partials/plugin_config.html b/web_interface/templates/v3/partials/plugin_config.html index c34498f0f..013b37f87 100644 --- a/web_interface/templates/v3/partials/plugin_config.html +++ b/web_interface/templates/v3/partials/plugin_config.html @@ -112,11 +112,12 @@ })(); {% else %} - {# Default checkbox #} + {# Default checkbox - value="true" ensures checked sends "true" not "on" #}