mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
fix(web): widget cache-busting, dead frontend code, and dependency pins (#656)
- Plugin-supplied widgets load as /static/plugin-widgets/...js?v=<plugin version>, so an update isn't hidden behind the year-long immutable cache. - Fire-and-forget loadInstalledPlugins() calls catch the rejection it has already reported, so the global handler no longer adds a second toast. - Timezone picker renders again when the General partial is re-injected. - Remove dead code: executePluginAction's six plugin-id fallbacks and [DEBUG] logging, window.currentPluginConfig and every read of it, the file-upload JSON delete branch, unused PluginAPI / PluginInstallManager / PluginStateManager helpers, loadPluginWidgetsFromManifest, the stale install_manager.js and LEDVisibility fallbacks, error_handler.js's global escapeHtml, 13 unused CSS rules, and stale comments/no-op returns. - pytz < 2027, psutil < 7 in requirements-test.txt, pytest-cov < 8. - Pin anthropics/claude-code-action to the commit v1 resolves to. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -651,12 +651,7 @@ function markPanelLoadFailed(event) {
|
||||
// Also update window.installedPlugins for consistency
|
||||
window.installedPlugins = this.installedPlugins;
|
||||
debugLog(`[FULL] Loaded ${this.installedPlugins.length} plugins:`, this.installedPlugins.map(p => p.id));
|
||||
|
||||
// Debug: Log enabled status for each plugin
|
||||
this.installedPlugins.forEach(plugin => {
|
||||
debugLog(`[DEBUG Alpine] Plugin ${plugin.id}: enabled=${plugin.enabled} (type: ${typeof plugin.enabled})`);
|
||||
});
|
||||
|
||||
|
||||
this.updatePluginTabs();
|
||||
} else {
|
||||
console.error('[FULL] Failed to load plugins:', data.message);
|
||||
|
||||
@@ -12,44 +12,39 @@
|
||||
if (!target) {
|
||||
console.warn('[HTMX] Target is null, skipping swap');
|
||||
event.detail.shouldSwap = false;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target is a valid DOM element
|
||||
if (!(target instanceof Element)) {
|
||||
console.warn('[HTMX] Target is not a valid Element, skipping swap');
|
||||
event.detail.shouldSwap = false;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target has a parent node (required for insertBefore)
|
||||
if (!target.parentNode) {
|
||||
console.warn('[HTMX] Target has no parent node, skipping swap');
|
||||
event.detail.shouldSwap = false;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure target is in the DOM
|
||||
if (!document.body.contains(target) && !document.head.contains(target)) {
|
||||
console.warn('[HTMX] Target is not in DOM, skipping swap');
|
||||
event.detail.shouldSwap = false;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Additional check: ensure parent is also in DOM
|
||||
if (target.parentNode && !document.body.contains(target.parentNode) && !document.head.contains(target.parentNode)) {
|
||||
console.warn('[HTMX] Target parent is not in DOM, skipping swap');
|
||||
event.detail.shouldSwap = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// All checks passed, allow swap
|
||||
return true;
|
||||
} catch (e) {
|
||||
// If validation fails, cancel swap
|
||||
console.warn('[HTMX] Error validating target:', e);
|
||||
event.detail.shouldSwap = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ const RequestThrottler = {
|
||||
|
||||
debugLog('[RequestThrottler] Creating new request for:', key);
|
||||
|
||||
// Create throttled request with abort support
|
||||
let abortController = null;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
@@ -54,22 +52,8 @@ const RequestThrottler = {
|
||||
this.pending.delete(key);
|
||||
}
|
||||
}, delay);
|
||||
|
||||
// Store abort controller if available
|
||||
if (fn.abort && typeof fn.abort === 'function') {
|
||||
abortController = fn.abort;
|
||||
}
|
||||
});
|
||||
|
||||
// Add abort method if available
|
||||
if (abortController) {
|
||||
promise.abort = () => {
|
||||
debugLog('[RequestThrottler] Aborting request for:', key);
|
||||
abortController.abort();
|
||||
this.pending.delete(key);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
this.pending.set(key, promise);
|
||||
return promise;
|
||||
},
|
||||
@@ -83,18 +67,6 @@ const RequestThrottler = {
|
||||
} else {
|
||||
this.cache.clear();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get statistics about pending requests and cache
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
pendingCount: this.pending.size,
|
||||
cacheSize: this.cache.size,
|
||||
pendingKeys: Array.from(this.pending.keys()),
|
||||
cacheKeys: Array.from(this.cache.keys())
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -225,18 +197,6 @@ const PluginAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Batch multiple requests together for better performance
|
||||
*
|
||||
* @param {Array} requests - Array of {endpoint, method, data} objects
|
||||
* @returns {Promise<Array>} Array of response data
|
||||
*/
|
||||
async batch(requests) {
|
||||
return Promise.all(requests.map(req =>
|
||||
this.request(req.endpoint, req.method || 'GET', req.data || null, false)
|
||||
));
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear API cache
|
||||
*/
|
||||
@@ -308,34 +268,6 @@ const PluginAPI = {
|
||||
return await this.request(`/plugins/config/reset?plugin_id=${encodeURIComponent(pluginId)}`, 'POST');
|
||||
},
|
||||
|
||||
/**
|
||||
* Get plugin schema.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @returns {Promise<Object>} Plugin schema
|
||||
*/
|
||||
async getPluginSchema(pluginId) {
|
||||
const response = await this.request(`/plugins/schema?plugin_id=${encodeURIComponent(pluginId)}`);
|
||||
return response.data?.schema || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Install plugin from store.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {string} branch - Optional branch name to install from
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async installPlugin(pluginId, branch = null) {
|
||||
const data = {
|
||||
plugin_id: pluginId
|
||||
};
|
||||
if (branch) {
|
||||
data.branch = branch;
|
||||
}
|
||||
return await this.request('/plugins/install', 'POST', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update plugin.
|
||||
*
|
||||
@@ -348,28 +280,6 @@ const PluginAPI = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Uninstall plugin.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @returns {Promise<Object>} Response data
|
||||
*/
|
||||
async uninstallPlugin(pluginId) {
|
||||
return await this.request('/plugins/uninstall', 'POST', {
|
||||
plugin_id: pluginId
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get plugin store.
|
||||
*
|
||||
* @returns {Promise<Array>} List of available plugins
|
||||
*/
|
||||
async getPluginStore() {
|
||||
const response = await this.request('/plugins/store/list');
|
||||
return response.data || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get plugin health.
|
||||
*
|
||||
|
||||
@@ -1,83 +1,11 @@
|
||||
/**
|
||||
* Plugin installation and update management.
|
||||
*
|
||||
* Handles plugin installation, updates, and uninstallation operations.
|
||||
* Handles Check & Update All: which installed plugins it sends, retrying
|
||||
* across a web-service restart, and summarising the results.
|
||||
*/
|
||||
|
||||
const PluginInstallManager = {
|
||||
/**
|
||||
* Install a plugin.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {string} branch - Optional branch name to install from
|
||||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
async install(pluginId, branch = null) {
|
||||
try {
|
||||
const result = await window.PluginAPI.installPlugin(pluginId, branch);
|
||||
|
||||
// Refresh installed plugins list
|
||||
if (window.PluginStateManager) {
|
||||
await window.PluginStateManager.loadInstalledPlugins();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (window.errorHandler) {
|
||||
window.errorHandler.displayError(error, `Failed to install plugin ${pluginId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a plugin.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @returns {Promise<Object>} Update result
|
||||
*/
|
||||
async update(pluginId) {
|
||||
try {
|
||||
const result = await window.PluginAPI.updatePlugin(pluginId);
|
||||
|
||||
// Refresh installed plugins list
|
||||
if (window.PluginStateManager) {
|
||||
await window.PluginStateManager.loadInstalledPlugins();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (window.errorHandler) {
|
||||
window.errorHandler.displayError(error, `Failed to update plugin ${pluginId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uninstall a plugin.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @returns {Promise<Object>} Uninstall result
|
||||
*/
|
||||
async uninstall(pluginId) {
|
||||
try {
|
||||
const result = await window.PluginAPI.uninstallPlugin(pluginId);
|
||||
|
||||
// Refresh installed plugins list
|
||||
if (window.PluginStateManager) {
|
||||
await window.PluginStateManager.loadInstalledPlugins();
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (window.errorHandler) {
|
||||
window.errorHandler.displayError(error, `Failed to uninstall plugin ${pluginId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether POST /plugins/update can update this installed-list entry.
|
||||
*
|
||||
|
||||
@@ -9,12 +9,7 @@ const PluginStateManager = {
|
||||
* Installed plugins state.
|
||||
*/
|
||||
installedPlugins: [],
|
||||
|
||||
/**
|
||||
* Current plugin configuration state.
|
||||
*/
|
||||
currentConfig: null,
|
||||
|
||||
|
||||
/**
|
||||
* Load installed plugins.
|
||||
*
|
||||
@@ -32,83 +27,6 @@ const PluginStateManager = {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get plugin by ID.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @returns {Object|null} Plugin object or null
|
||||
*/
|
||||
getPlugin(pluginId) {
|
||||
return this.installedPlugins.find(p => p.id === pluginId) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update plugin state.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {Object} updates - State updates
|
||||
*/
|
||||
updatePlugin(pluginId, updates) {
|
||||
const plugin = this.getPlugin(pluginId);
|
||||
if (plugin) {
|
||||
Object.assign(plugin, updates);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set plugin enabled state.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {boolean} enabled - Whether plugin is enabled
|
||||
*/
|
||||
setPluginEnabled(pluginId, enabled) {
|
||||
this.updatePlugin(pluginId, { enabled });
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current plugin configuration.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @returns {Promise<Object>} Plugin configuration
|
||||
*/
|
||||
async getPluginConfig(pluginId) {
|
||||
try {
|
||||
const config = await window.PluginAPI.getPluginConfig(pluginId);
|
||||
this.currentConfig = { pluginId, config };
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (window.errorHandler) {
|
||||
window.errorHandler.displayError(error, `Failed to load config for ${pluginId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save plugin configuration.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {Object} config - Configuration data
|
||||
* @returns {Promise<Object>} Save result
|
||||
*/
|
||||
async savePluginConfig(pluginId, config) {
|
||||
try {
|
||||
const result = await window.PluginAPI.savePluginConfig(pluginId, config);
|
||||
|
||||
// Update local state
|
||||
if (this.currentConfig && this.currentConfig.pluginId === pluginId) {
|
||||
this.currentConfig.config = config;
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (window.errorHandler) {
|
||||
window.errorHandler.displayError(error, `Failed to save config for ${pluginId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -176,19 +176,19 @@ function showErrorModal(error, context, message, suggestions, docLink) {
|
||||
}
|
||||
|
||||
// Build modal content
|
||||
const contextText = context ? `<div class="text-sm text-gray-600 mb-2">${escapeHtml(context)}</div>` : '';
|
||||
const contextText = context ? `<div class="text-sm text-gray-600 mb-2">${window.LEDEscape.html(context)}</div>` : '';
|
||||
const suggestionsHtml = suggestions.length > 0 ? `
|
||||
<div class="mt-4">
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-2">Suggested fixes:</h4>
|
||||
<ul class="list-disc list-inside space-y-1 text-sm text-gray-700">
|
||||
${suggestions.map(s => `<li>${escapeHtml(s)}</li>`).join('')}
|
||||
${suggestions.map(s => `<li>${window.LEDEscape.html(s)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const docLinkHtml = docLink ? `
|
||||
<div class="mt-4">
|
||||
<a href="${escapeHtml(docLink)}" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-800 text-sm underline">
|
||||
<a href="${window.LEDEscape.html(docLink)}" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:text-blue-800 text-sm underline">
|
||||
<i class="fas fa-book mr-1" aria-hidden="true"></i>View troubleshooting guide<span style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;"> (opens in a new tab)</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -198,14 +198,14 @@ function showErrorModal(error, context, message, suggestions, docLink) {
|
||||
<div class="mt-4">
|
||||
<details class="cursor-pointer">
|
||||
<summary class="text-sm font-medium text-gray-700 hover:text-gray-900">Technical details</summary>
|
||||
<pre class="mt-2 text-xs bg-gray-100 p-3 rounded overflow-auto max-h-48 text-gray-800">${escapeHtml(error.details)}</pre>
|
||||
<pre class="mt-2 text-xs bg-gray-100 p-3 rounded overflow-auto max-h-48 text-gray-800">${window.LEDEscape.html(error.details)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
const errorCodeHtml = error.error_code ? `
|
||||
<div class="mt-2 text-xs text-gray-500">
|
||||
Error code: <code class="bg-gray-100 px-1 py-0.5 rounded">${escapeHtml(error.error_code)}</code>
|
||||
Error code: <code class="bg-gray-100 px-1 py-0.5 rounded">${window.LEDEscape.html(error.error_code)}</code>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
@@ -223,7 +223,7 @@ function showErrorModal(error, context, message, suggestions, docLink) {
|
||||
<h3 id="error-modal-title" class="text-lg leading-6 font-medium text-gray-900">Something went wrong</h3>
|
||||
<div class="mt-2" id="error-modal-description">
|
||||
${contextText}
|
||||
<p class="text-sm text-gray-500">${escapeHtml(message)}</p>
|
||||
<p class="text-sm text-gray-500">${window.LEDEscape.html(message)}</p>
|
||||
${errorCodeHtml}
|
||||
${suggestionsHtml}
|
||||
${docLinkHtml}
|
||||
@@ -284,8 +284,6 @@ function closeErrorModal() {
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) { return window.LEDEscape.html(text); }
|
||||
|
||||
/**
|
||||
* Copy error details to clipboard.
|
||||
*
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
*/
|
||||
window.handleFiles = async function(fieldId, files) {
|
||||
const uploadConfig = window.getUploadConfig ? window.getUploadConfig(fieldId) : {};
|
||||
const pluginId = uploadConfig.plugin_id || window.currentPluginConfig?.pluginId || 'static-image';
|
||||
const pluginId = uploadConfig.plugin_id || 'static-image';
|
||||
const maxFiles = uploadConfig.max_files || 10;
|
||||
const maxSizeMB = uploadConfig.max_size_mb || 5;
|
||||
const fileType = uploadConfig.file_type || 'image';
|
||||
@@ -333,30 +333,24 @@
|
||||
* @param {string} pluginId - Plugin ID
|
||||
*/
|
||||
window.deleteUploadedImage = async function(fieldId, imageId, pluginId) {
|
||||
return window.deleteUploadedFile(fieldId, imageId, pluginId, 'image', null);
|
||||
return window.deleteUploadedFile(fieldId, imageId, pluginId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete uploaded file (generic)
|
||||
* Delete an uploaded plugin asset
|
||||
* @param {string} fieldId - Field ID
|
||||
* @param {string} fileId - File ID
|
||||
* @param {string} pluginId - Plugin ID
|
||||
* @param {string} fileType - File type ('image' or 'json')
|
||||
* @param {string|null} customDeleteEndpoint - Custom delete endpoint
|
||||
*/
|
||||
window.deleteUploadedFile = async function(fieldId, fileId, pluginId, fileType, customDeleteEndpoint) {
|
||||
const fileTypeLabel = fileType === 'json' ? 'file' : 'image';
|
||||
if (!confirm(`Are you sure you want to delete this ${fileTypeLabel}?`)) {
|
||||
window.deleteUploadedFile = async function(fieldId, fileId, pluginId) {
|
||||
if (!confirm('Are you sure you want to delete this image?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const deleteEndpoint = customDeleteEndpoint || (fileType === 'json' ? '/api/v3/plugins/of-the-day/json/delete' : '/api/v3/plugins/assets/delete');
|
||||
const requestBody = fileType === 'json'
|
||||
? { file_id: fileId }
|
||||
: { plugin_id: pluginId, image_id: fileId };
|
||||
|
||||
const response = await fetch(deleteEndpoint, {
|
||||
const requestBody = { plugin_id: pluginId, image_id: fileId };
|
||||
|
||||
const response = await fetch('/api/v3/plugins/assets/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
@@ -381,7 +375,7 @@
|
||||
window.updateImageList(fieldId, newFiles);
|
||||
}
|
||||
|
||||
window.showNotification(`${fileType === 'json' ? 'File' : 'Image'} deleted successfully`, 'success');
|
||||
window.showNotification('Image deleted successfully', 'success');
|
||||
} else {
|
||||
window.showNotification(`Delete failed: ${data.message}`, 'error');
|
||||
}
|
||||
@@ -392,14 +386,13 @@
|
||||
};
|
||||
|
||||
/**
|
||||
* Get upload configuration for a file upload field.
|
||||
* Priority: 1) data attributes on the file input element (server-rendered),
|
||||
* 2) schema lookup via window.currentPluginConfig (client-rendered).
|
||||
* Get upload configuration for a file upload field, read from the data
|
||||
* attributes the server renders on the file input or its drop zone.
|
||||
* @param {string} fieldId - Field ID
|
||||
* @returns {Object} Upload configuration
|
||||
*/
|
||||
window.getUploadConfig = function(fieldId) {
|
||||
// Strategy 1: Read from data attributes on the file input element or
|
||||
// Read from data attributes on the file input element or
|
||||
// the drop zone wrapper (which survives progress-helper re-renders).
|
||||
// Accept any upload-related data attribute — not just pluginId.
|
||||
const configSource = getConfigSourceElement(fieldId);
|
||||
@@ -417,45 +410,6 @@
|
||||
return config;
|
||||
}
|
||||
|
||||
// Strategy 2: Extract config from schema (client-side rendered forms)
|
||||
const schema = window.currentPluginConfig?.schema;
|
||||
if (!schema || !schema.properties) return {};
|
||||
|
||||
// Find the property that matches this fieldId
|
||||
// FieldId is like "image_config_images" for "image_config.images" (client-side)
|
||||
// or "static-image-images" for plugin "static-image", field "images" (server-side)
|
||||
const key = fieldId.replace(/_/g, '.');
|
||||
const keys = key.split('.');
|
||||
let prop = schema.properties;
|
||||
|
||||
for (const k of keys) {
|
||||
if (prop && prop[k]) {
|
||||
prop = prop[k];
|
||||
if (prop.properties && prop.type === 'object') {
|
||||
prop = prop.properties;
|
||||
} else if (prop.type === 'array' && prop['x-widget'] === 'file-upload') {
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found an array with x-widget, get its config
|
||||
if (prop && prop.type === 'array' && prop['x-widget'] === 'file-upload') {
|
||||
return prop['x-upload-config'] || {};
|
||||
}
|
||||
|
||||
// Try to find nested images array (legacy fallback)
|
||||
if (schema.properties && schema.properties.image_config &&
|
||||
schema.properties.image_config.properties &&
|
||||
schema.properties.image_config.properties.images) {
|
||||
const imagesProp = schema.properties.image_config.properties.images;
|
||||
if (imagesProp['x-widget'] === 'file-upload') {
|
||||
return imagesProp['x-upload-config'] || {};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -501,7 +455,7 @@
|
||||
if (!imageList) return;
|
||||
|
||||
const uploadConfig = window.getUploadConfig(fieldId);
|
||||
const pluginId = uploadConfig.plugin_id || window.currentPluginConfig?.pluginId || 'static-image';
|
||||
const pluginId = uploadConfig.plugin_id || 'static-image';
|
||||
|
||||
const openEditor = imageList.querySelector('[id^="schedule_"]:not(.hidden)');
|
||||
const openScheduleId = openEditor ? openEditor.id.slice('schedule_'.length) : null;
|
||||
|
||||
@@ -20,9 +20,12 @@
|
||||
* Load a plugin-specific widget
|
||||
* @param {string} pluginId - Plugin ID
|
||||
* @param {string} widgetName - Widget name
|
||||
* @param {string} [version] - Plugin version, appended as ?v= so an
|
||||
* updated plugin's widget is not served from the year-long
|
||||
* immutable cache /static/ responses get (app.py)
|
||||
* @returns {Promise<void>} Promise that resolves when widget is loaded
|
||||
*/
|
||||
window.LEDMatrixWidgets.loadPluginWidget = async function(pluginId, widgetName) {
|
||||
window.LEDMatrixWidgets.loadPluginWidget = async function(pluginId, widgetName, version) {
|
||||
if (!pluginId || !widgetName) {
|
||||
throw new Error('Plugin ID and widget name are required');
|
||||
}
|
||||
@@ -36,8 +39,9 @@
|
||||
// The one route that serves plugin widgets (serve_plugin_widget in
|
||||
// blueprints/pages_v3.py); nothing is served under /plugins/<id>/ or
|
||||
// /static/plugins/<id>/, so trying those only added two failed imports.
|
||||
const versionQuery = version ? `?v=${encodeURIComponent(version)}` : '';
|
||||
const possiblePaths = [
|
||||
`/static/plugin-widgets/${pluginId}/${widgetName}.js`
|
||||
`/static/plugin-widgets/${pluginId}/${widgetName}.js${versionQuery}`
|
||||
];
|
||||
|
||||
let lastError = null;
|
||||
@@ -69,9 +73,10 @@
|
||||
* Called automatically when a widget is referenced in a plugin's config schema
|
||||
* @param {string} widgetName - Widget name
|
||||
* @param {string} pluginId - Plugin ID (optional, for plugin-specific widgets)
|
||||
* @param {string} [version] - Plugin version (optional, see loadPluginWidget)
|
||||
* @returns {Promise<boolean>} True if widget is available (either already registered or successfully loaded)
|
||||
*/
|
||||
window.LEDMatrixWidgets.ensureWidget = async function(widgetName, pluginId) {
|
||||
window.LEDMatrixWidgets.ensureWidget = async function(widgetName, pluginId, version) {
|
||||
// Check if widget is already registered
|
||||
if (this.has(widgetName)) {
|
||||
return true;
|
||||
@@ -80,7 +85,7 @@
|
||||
// If plugin ID provided, try to load as plugin widget
|
||||
if (pluginId) {
|
||||
try {
|
||||
await this.loadPluginWidget(pluginId, widgetName);
|
||||
await this.loadPluginWidget(pluginId, widgetName, version);
|
||||
return this.has(widgetName);
|
||||
} catch (error) {
|
||||
console.warn(`[PluginWidgetLoader] Could not load widget ${widgetName} from plugin ${pluginId}:`, error);
|
||||
@@ -91,37 +96,4 @@
|
||||
// Widget not found
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load all widgets specified in plugin manifest
|
||||
* @param {string} pluginId - Plugin ID
|
||||
* @param {Object} manifest - Plugin manifest object
|
||||
* @returns {Promise<Array<string>>} Array of successfully loaded widget names
|
||||
*/
|
||||
window.LEDMatrixWidgets.loadPluginWidgetsFromManifest = async function(pluginId, manifest) {
|
||||
if (!manifest || !manifest.widgets || !Array.isArray(manifest.widgets)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const loadedWidgets = [];
|
||||
|
||||
for (const widgetDef of manifest.widgets) {
|
||||
const widgetName = widgetDef.name || widgetDef.script?.replace(/\.js$/, '');
|
||||
if (!widgetName) {
|
||||
console.warn(`[PluginWidgetLoader] Invalid widget definition in manifest:`, widgetDef);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.loadPluginWidget(pluginId, widgetName);
|
||||
if (this.has(widgetName)) {
|
||||
loadedWidgets.push(widgetName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[PluginWidgetLoader] Failed to load widget ${widgetName} from plugin ${pluginId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return loadedWidgets;
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user