Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 66 additions & 33 deletions web_interface/blueprints/api_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,7 @@ def find_secret_fields(properties, prefix=''):
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=''):
Expand Down Expand Up @@ -923,7 +923,7 @@ def separate_secrets(config, secrets_set, prefix=''):
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

Expand Down Expand Up @@ -958,7 +958,7 @@ def separate_secrets(config, secrets_set, prefix=''):
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:
Expand Down Expand Up @@ -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
Expand All @@ -3500,21 +3505,27 @@ def _get_schema_property(schema, key_path):

parts = key_path.split('.')
current = schema['properties']

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:
i = 0

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
Expand Down Expand Up @@ -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

# 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]

# 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
i = 0

# 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

# 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):
Expand Down
1 change: 0 additions & 1 deletion web_interface/static/v3/js/widgets/schedule-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,5 +567,4 @@
window.LEDMatrixWidgets.get('schedule-picker').getDefaultSchedule = getDefaultSchedule;
window.LEDMatrixWidgets.get('schedule-picker').normalizeSchedule = normalizeSchedule;

console.log('[SchedulePickerWidget] Schedule picker widget registered');
})();
12 changes: 1 addition & 11 deletions web_interface/static/v3/plugins_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -1330,17 +1330,7 @@ function loadInstalledPlugins(forceRefresh = false) {
pluginLoadCache.timestamp = Date.now();

// Always update window.installedPlugins to ensure Alpine component can detect changes
const currentPlugins = Array.isArray(window.installedPlugins) ? window.installedPlugins : [];
const currentIds = currentPlugins.map(p => p.id).sort().join(',');
const newIds = installedPlugins.map(p => p.id).sort().join(',');
const pluginsChanged = currentIds !== newIds;

if (pluginsChanged) {
window.installedPlugins = installedPlugins;
} else {
// Even if IDs haven't changed, update the array reference to trigger Alpine reactivity
window.installedPlugins = installedPlugins;
}
window.installedPlugins = installedPlugins;

// Dispatch event to notify Alpine component to update tabs
document.dispatchEvent(new CustomEvent('pluginsUpdated', {
Expand Down