diff --git a/plugins/baseball-scoreboard b/plugins/baseball-scoreboard
index 13cfa603..67e5596e 160000
--- a/plugins/baseball-scoreboard
+++ b/plugins/baseball-scoreboard
@@ -1 +1 @@
-Subproject commit 13cfa6031c1398c894cd27d618feb7cb5cf86843
+Subproject commit 67e5596e52b91c97e30b23a26a27b17d60a0448f
diff --git a/plugins/basketball-scoreboard b/plugins/basketball-scoreboard
index a250ebe8..960a716e 160000
--- a/plugins/basketball-scoreboard
+++ b/plugins/basketball-scoreboard
@@ -1 +1 @@
-Subproject commit a250ebe8f1858b3350c13101b5d949838aa0e19a
+Subproject commit 960a716e1149c4be6897d64fff3195e9a486c353
diff --git a/plugins/clock-simple b/plugins/clock-simple
index 7316ba25..228e1f2f 160000
--- a/plugins/clock-simple
+++ b/plugins/clock-simple
@@ -1 +1 @@
-Subproject commit 7316ba256fd90e70e1ceb5555bde368a634f5597
+Subproject commit 228e1f2fcf876f15268261acc9e7a09b6d2dde60
diff --git a/plugins/football-scoreboard b/plugins/football-scoreboard
index 6c5cf171..8d36803e 160000
--- a/plugins/football-scoreboard
+++ b/plugins/football-scoreboard
@@ -1 +1 @@
-Subproject commit 6c5cf171c65aecbbc171a1ddaac0866d8068448d
+Subproject commit 8d36803e3b4288eaf3ba5feec8659e4d084ca024
diff --git a/plugins/hockey-scoreboard b/plugins/hockey-scoreboard
index 22c6cf75..9fef0cd2 160000
--- a/plugins/hockey-scoreboard
+++ b/plugins/hockey-scoreboard
@@ -1 +1 @@
-Subproject commit 22c6cf75aa08d227a4fae20024bac016f45514d1
+Subproject commit 9fef0cd2099c371b80a2637b0a648e3d1fa0de9c
diff --git a/plugins/ledmatrix-news b/plugins/ledmatrix-news
index 118b6292..cdc1b118 160000
--- a/plugins/ledmatrix-news
+++ b/plugins/ledmatrix-news
@@ -1 +1 @@
-Subproject commit 118b6292a231d75852ab5f6b0309ed95706cdbe9
+Subproject commit cdc1b118add3eee9e15966a2c146b7e9a8f77f3a
diff --git a/scripts/install/one-shot-install.sh b/scripts/install/one-shot-install.sh
index 29ebbd67..ddac1115 100755
--- a/scripts/install/one-shot-install.sh
+++ b/scripts/install/one-shot-install.sh
@@ -63,11 +63,18 @@ retry() {
local attempt=1
local max_attempts=3
local delay_seconds=5
+ local status
while true; do
- if "$@"; then
+ # Run command in a context that disables errexit so we can capture exit code
+ # This prevents errexit from triggering before status=$? runs
+ if ! "$@"; then
+ status=$?
+ else
+ status=0
+ fi
+ if [ $status -eq 0 ]; then
return 0
fi
- local status=$?
if [ $attempt -ge $max_attempts ]; then
print_error "Command failed after $attempt attempts: $*"
return $status
diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py
index e49a1418..10046742 100644
--- a/src/plugin_system/store_manager.py
+++ b/src/plugin_system/store_manager.py
@@ -838,7 +838,7 @@ def install_plugin(self, plugin_id: str, branch: Optional[str] = None) -> bool:
# Update plugin_id to match manifest for rest of function
plugin_id = manifest_plugin_id
- required_fields = ['id', 'name', 'class_name']
+ required_fields = ['id', 'name', 'class_name', 'display_modes']
missing = [field for field in required_fields if field not in manifest]
manifest_modified = False
@@ -976,7 +976,7 @@ def install_from_url(self, repo_url: str, plugin_id: str = None, plugin_path: st
}
# Validate manifest has required fields
- required_fields = ['id', 'name', 'class_name']
+ required_fields = ['id', 'name', 'class_name', 'display_modes']
missing_fields = [field for field in required_fields if field not in manifest]
if missing_fields:
return {
diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py
index 545dcb6a..381b0d62 100644
--- a/web_interface/blueprints/api_v3.py
+++ b/web_interface/blueprints/api_v3.py
@@ -606,6 +606,9 @@ def separate_secrets(config, secrets_set, prefix=''):
'auto_load_enabled', 'development_mode',
'plugins_directory']:
continue
+ # Skip display settings that are already handled above (they're in nested structure)
+ if key in display_fields:
+ continue
# For any remaining keys (including plugin keys), use deep merge to preserve existing settings
if key in current_config and isinstance(current_config[key], dict) and isinstance(data[key], dict):
# Deep merge to preserve existing settings
diff --git a/web_interface/static/v3/plugins_manager.js b/web_interface/static/v3/plugins_manager.js
index 808714fb..42d64ce1 100644
--- a/web_interface/static/v3/plugins_manager.js
+++ b/web_interface/static/v3/plugins_manager.js
@@ -2229,7 +2229,8 @@ function handlePluginConfigSubmit(e) {
const baseKey = key.replace(/_data$/, '');
const jsonValue = JSON.parse(value);
// Handle both objects (patternProperties) and arrays (array-of-objects)
- if (typeof jsonValue === 'object') {
+ // Only treat as JSON-backed when it's a non-null object (null is typeof 'object' in JavaScript)
+ if (jsonValue !== null && typeof jsonValue === 'object') {
flatConfig[baseKey] = jsonValue;
console.log(`JSON data field ${baseKey}: parsed ${Array.isArray(jsonValue) ? 'array' : 'object'}`, jsonValue);
continue; // Skip normal processing for JSON data fields
@@ -2476,7 +2477,10 @@ function flattenConfig(obj, prefix = '') {
function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, index, itemsSchema) {
const item = itemValue || {};
const itemId = `${escapeAttribute(fieldId)}_item_${index}`;
- let html = `
`;
+ // Store original item data in data attribute to preserve non-editable properties after reindexing
+ const itemDataJson = JSON.stringify(item);
+ const itemDataBase64 = btoa(unescape(encodeURIComponent(itemDataJson)));
+ let html = `
`;
// Render each property of the object
const propertyOrder = itemsSchema['x-propertyOrder'] || Object.keys(itemProperties);
@@ -2492,44 +2496,47 @@ function renderArrayObjectItem(fieldId, fullKey, itemProperties, itemValue, inde
html += `
`;
// Handle file-upload widget (for logo field)
- // NOTE: File upload for array-of-objects items is not yet implemented.
- // The widget is disabled to prevent silent failures when users try to upload files.
- // TODO: Implement handleArrayObjectFileUpload and removeArrayObjectFile with proper
- // endpoint support and [data-file-data] attribute updates before enabling this widget.
if (propSchema['x-widget'] === 'file-upload') {
html += ``;
if (propDescription) {
html += `
+
+
+ `;
- // Display existing logo if present, but disable upload functionality
- // Store file metadata in data-file-data attribute for serialization
if (logoValue.path) {
- // Use base64 encoding for JSON in data attributes to safely handle all characters
- const fileDataJson = JSON.stringify(logoValue);
- const fileDataBase64 = btoa(unescape(encodeURIComponent(fileDataJson)));
html += `
-
-
-
- File upload not yet available for array items
-
-
- `;
- } else {
- html += `
-
+
+
-
File upload functionality for array items is coming soon
`;
}
@@ -2887,27 +2894,59 @@ function generateFieldHtml(key, prop, value, prefix = '') {
`;
} else if (prop.type === 'array') {
- // Array - check for file upload widget first (to avoid breaking static-image plugin),
- // then checkbox-group, then custom-feeds, then array of objects
- const hasXWidget = prop.hasOwnProperty('x-widget');
- const xWidgetValue = prop['x-widget'];
- const xWidgetValue2 = prop['x-widget'] || prop['x_widget'] || prop.xWidget;
-
- console.log(`[DEBUG] Array field ${fullKey}:`, {
- type: prop.type,
- hasItems: !!prop.items,
- itemsType: prop.items?.type,
- itemsHasProperties: !!prop.items?.properties,
- hasXWidget: hasXWidget,
- 'x-widget': xWidgetValue,
- 'x-widget (alt)': xWidgetValue2,
- 'x-upload-config': prop['x-upload-config'],
- propKeys: Object.keys(prop),
- value: value
- });
+ // Check if this is an array of objects FIRST (before other checks)
+ if (prop.items && prop.items.type === 'object' && prop.items.properties) {
+ // Array of objects widget (like custom_feeds with name, url, enabled, logo)
+ console.log(`[DEBUG] ✅ Detected array-of-objects widget for ${fullKey}`);
+ const fieldId = fullKey.replace(/\./g, '_');
+ const itemsSchema = prop.items;
+ const itemProperties = itemsSchema.properties || {};
+ const maxItems = prop.maxItems || 50;
+ const currentItems = Array.isArray(value) ? value : [];
+
+ html += `
+