mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
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>
This commit is contained in:
@@ -885,7 +885,7 @@ def save_main_config():
|
||||
if 'properties' in schema:
|
||||
secret_fields = find_secret_fields(schema['properties'])
|
||||
except Exception as e:
|
||||
print(f"Error reading schema for secret detection: {e}")
|
||||
logger.warning(f"Error reading schema for secret detection: {e}")
|
||||
|
||||
# Separate secrets from regular config (same logic as save_plugin_config)
|
||||
def separate_secrets(config, secrets_set, prefix=''):
|
||||
@@ -923,7 +923,7 @@ def save_main_config():
|
||||
if 'enabled' not in regular_config:
|
||||
regular_config['enabled'] = True
|
||||
except Exception as e:
|
||||
print(f"Error preserving enabled state for {plugin_id}: {e}")
|
||||
logger.warning(f"Error preserving enabled state for {plugin_id}: {e}")
|
||||
# Default to True on error to avoid disabling plugins
|
||||
regular_config['enabled'] = True
|
||||
|
||||
@@ -958,7 +958,7 @@ def save_main_config():
|
||||
plugin_instance.on_config_change(plugin_full_config)
|
||||
except Exception as hook_err:
|
||||
# Don't fail the save if hook fails
|
||||
print(f"Warning: on_config_change failed for {plugin_id}: {hook_err}")
|
||||
logger.warning(f"on_config_change failed for {plugin_id}: {hook_err}")
|
||||
|
||||
# Remove processed plugin keys from data (they're already in current_config)
|
||||
for key in plugin_keys_to_remove:
|
||||
@@ -3488,9 +3488,14 @@ def _get_schema_property(schema, key_path):
|
||||
"""
|
||||
Get the schema property for a given key path (supports dot notation).
|
||||
|
||||
Handles schema keys that themselves contain dots (e.g., "eng.1" in soccer
|
||||
league configs) by trying progressively longer segment combinations when an
|
||||
exact match for the current segment is not found.
|
||||
|
||||
Args:
|
||||
schema: The JSON schema dict
|
||||
key_path: Dot-separated path like "customization.time_text.font"
|
||||
or "leagues.eng.1.favorite_teams" where "eng.1" is one key.
|
||||
|
||||
Returns:
|
||||
The property schema dict or None if not found
|
||||
@@ -3500,21 +3505,27 @@ def _get_schema_property(schema, key_path):
|
||||
|
||||
parts = key_path.split('.')
|
||||
current = schema['properties']
|
||||
i = 0
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if part not in current:
|
||||
return None
|
||||
|
||||
prop = current[part]
|
||||
|
||||
# If this is the last part, return the property
|
||||
if i == len(parts) - 1:
|
||||
return prop
|
||||
|
||||
# If this is an object with properties, navigate deeper
|
||||
if isinstance(prop, dict) and 'properties' in prop:
|
||||
current = prop['properties']
|
||||
else:
|
||||
while i < len(parts):
|
||||
# Try progressively longer candidate keys starting at position i,
|
||||
# longest first, to greedily match dotted property names (e.g. "eng.1").
|
||||
matched = False
|
||||
for j in range(len(parts), i, -1):
|
||||
candidate = '.'.join(parts[i:j])
|
||||
if candidate in current:
|
||||
prop = current[candidate]
|
||||
if j == len(parts):
|
||||
return prop # Consumed all remaining parts — done
|
||||
# Navigate deeper if this is an object with properties
|
||||
if isinstance(prop, dict) and 'properties' in prop:
|
||||
current = prop['properties']
|
||||
i = j
|
||||
matched = True
|
||||
break
|
||||
else:
|
||||
return None # Can't navigate deeper
|
||||
if not matched:
|
||||
return None
|
||||
|
||||
return None
|
||||
@@ -3691,30 +3702,52 @@ def _set_nested_value(config, key_path, value):
|
||||
Set a value in a nested dict using dot notation path.
|
||||
Handles existing nested dicts correctly by merging instead of replacing.
|
||||
|
||||
Handles config keys that themselves contain dots (e.g., "eng.1" in soccer
|
||||
league configs) by trying progressively longer segment combinations against
|
||||
existing dict keys before falling back to single-segment creation.
|
||||
|
||||
Args:
|
||||
config: The config dict to modify
|
||||
key_path: Dot-separated path (e.g., "customization.period_text.font")
|
||||
key_path: Dot-separated path (e.g., "customization.period_text.font"
|
||||
or "leagues.eng.1.favorite_teams" where "eng.1" is one key)
|
||||
value: The value to set (or _SKIP_FIELD to skip setting)
|
||||
"""
|
||||
# Skip setting if value is the sentinel
|
||||
if value is _SKIP_FIELD:
|
||||
return
|
||||
|
||||
|
||||
parts = key_path.split('.')
|
||||
current = config
|
||||
i = 0
|
||||
|
||||
# Navigate/create intermediate dicts
|
||||
for i, part in enumerate(parts[:-1]):
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
elif not isinstance(current[part], dict):
|
||||
# If the existing value is not a dict, replace it with a dict
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
# Navigate/create intermediate dicts, greedily matching dotted keys.
|
||||
# We stop before the final part so we can set it as the leaf value.
|
||||
while i < len(parts) - 1:
|
||||
# Try progressively longer candidate keys (longest first) to match
|
||||
# dict keys that contain dots themselves (e.g. "eng.1").
|
||||
# Never consume the very last part (that's the leaf value key).
|
||||
matched = False
|
||||
for j in range(len(parts) - 1, i, -1):
|
||||
candidate = '.'.join(parts[i:j])
|
||||
if candidate in current and isinstance(current[candidate], dict):
|
||||
current = current[candidate]
|
||||
i = j
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
# No existing dotted key matched; use single segment and create if needed
|
||||
part = parts[i]
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
elif not isinstance(current[part], dict):
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
i += 1
|
||||
|
||||
# Set the final value (don't overwrite with empty dict if value is None and we want to preserve structure)
|
||||
if value is not None or parts[-1] not in current:
|
||||
current[parts[-1]] = value
|
||||
# The remaining parts form the final key (may itself be dotted, e.g. "eng.1")
|
||||
final_key = '.'.join(parts[i:])
|
||||
if value is not None or final_key not in current:
|
||||
current[final_key] = value
|
||||
|
||||
|
||||
def _set_missing_booleans_to_false(config, schema_props, form_keys, prefix='', config_node=None):
|
||||
|
||||
Reference in New Issue
Block a user