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:
Chuck
2026-09-28 10:40:52 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 65d82580bc
commit 0e9e2cabba
24 changed files with 109 additions and 579 deletions
+26 -156
View File
@@ -45,7 +45,7 @@
* installFromCustomRegistry, removeSavedRepository, executePluginAction,
* openOnDemandModal, requestOnDemandStop, loadOnDemandStatus,
* installStarlarkApp, installPixlet, the GitHub token functions,
* window.installedPlugins and window.currentPluginConfig.
* and window.installedPlugins.
*/
// ─── LocalStorage Safety Wrappers ────────────────────────────────────────────
@@ -215,7 +215,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) {
if (plugin) {
plugin.enabled = !enabled;
}
window.pluginManager.loadInstalledPlugins();
window.pluginManager.loadInstalledPlugins().catch(() => {});
}
// Clear token and re-enable UI
@@ -243,7 +243,7 @@ window.togglePlugin = window.togglePlugin || function(pluginId, enabled) {
if (plugin) {
plugin.enabled = !enabled;
}
window.pluginManager.loadInstalledPlugins();
window.pluginManager.loadInstalledPlugins().catch(() => {});
// Clear token and re-enable UI
delete window._pluginToggleRequests[pluginId];
@@ -452,10 +452,9 @@ window.handleGitHubPluginInstall = function() {
}
urlInput.value = '';
// Show notification if available
showNotification(`Plugin ${data.plugin_id} installed successfully`, 'success');
setTimeout(() => window.pluginManager.loadInstalledPlugins(true), 1000);
setTimeout(() => window.pluginManager.loadInstalledPlugins(true).catch(() => {}), 1000);
} else {
if (statusDiv) {
statusDiv.innerHTML = `<span class="text-red-600"><i class="fas fa-times-circle mr-1"></i>${window.LEDEscape.html(data.message || 'Installation failed')}</span>`;
@@ -609,7 +608,6 @@ window.checkGitHubAuthStatus = function checkGitHubAuthStatus() {
// Local variables for this instance
let installedPlugins = [];
window.currentPluginConfig = null;
let pluginStoreCache = null; // Cache for plugin store to speed up subsequent loads
let cacheTimestamp = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
@@ -904,6 +902,9 @@ const pluginLoadCache = {
}
};
// Rejects after it has reported the failure itself (panel error or toast),
// so fire-and-forget callers add .catch(() => {}) rather than leave it to
// the global unhandledrejection handler, which would toast it again.
function loadInstalledPlugins(forceRefresh = false) {
// Return cached data if valid and not forcing refresh
if (!forceRefresh && pluginLoadCache.isValid()) {
@@ -1381,7 +1382,7 @@ function handlePluginAction(event) {
.then(data => {
if (data.status === 'success') {
showNotification('Starlark app uninstalled', 'success');
loadInstalledPlugins(true);
loadInstalledPlugins(true).catch(() => {});
} else {
alert('Uninstall failed: ' + (data.message || 'Unknown error'));
}
@@ -1544,19 +1545,8 @@ function runUpdateAllPlugins() {
return;
}
// Counted by install_manager.js from each answer's update_status:
// a no-op update is "already up to date", not "updated". A cached
// install_manager.js from before that helper gets a plain count.
const manager = window.PluginInstallManager;
const summary = (manager && typeof manager.summarizeUpdateResults === 'function')
? manager.summarizeUpdateResults(results)
: (() => {
const failed = results.filter(r => !r.success).length;
const checked = results.length - failed;
return {
text: `${checked} checked` + (failed ? `, ${failed} failed` : ''),
type: failed ? (checked ? 'warning' : 'error') : 'success'
};
})();
// a no-op update is "already up to date", not "updated".
const summary = window.PluginInstallManager.summarizeUpdateResults(results);
showNotification(summary.text, summary.type);
})
.catch(error => {
@@ -1887,158 +1877,38 @@ function closeOnDemandModalOnBackdrop(event) {
}
// Generic Plugin Action Handler
window.executePluginAction = function(actionId, actionIndex, pluginIdParam = null) {
debugLog('[DEBUG] executePluginAction called - actionId:', actionId, 'actionIndex:', actionIndex, 'pluginIdParam:', pluginIdParam);
// Construct button ID first (we have actionId and actionIndex)
window.executePluginAction = function(actionId, actionIndex, pluginId) {
const actionIdFull = `action-${actionId}-${actionIndex}`;
const statusId = `action-status-${actionId}-${actionIndex}`;
const btn = document.getElementById(actionIdFull);
const statusDiv = document.getElementById(statusId);
// Get plugin ID from multiple sources with comprehensive fallback logic
let pluginId = pluginIdParam;
// Fallback 1: Try to get from button's data-plugin-id attribute
if (!pluginId && btn) {
pluginId = btn.getAttribute('data-plugin-id');
if (pluginId) {
debugLog('[DEBUG] Got pluginId from button data attribute:', pluginId);
}
}
// Fallback 2: Try to get from closest parent with data-plugin-id
if (!pluginId && btn) {
const parentWithPluginId = btn.closest('[data-plugin-id]');
if (parentWithPluginId) {
pluginId = parentWithPluginId.getAttribute('data-plugin-id');
if (pluginId) {
debugLog('[DEBUG] Got pluginId from parent element:', pluginId);
}
}
}
// Fallback 3: Try to get from plugin-config-container or plugin-config-tab
if (!pluginId && btn) {
const container = btn.closest('.plugin-config-container, .plugin-config-tab, [id^="plugin-config-"]');
if (container) {
// Try data-plugin-id first
pluginId = container.getAttribute('data-plugin-id');
if (!pluginId) {
// Try to extract from ID like "plugin-config-{pluginId}"
const idMatch = container.id.match(/plugin-config-(.+)/);
if (idMatch) {
pluginId = idMatch[1];
}
}
if (pluginId) {
debugLog('[DEBUG] Got pluginId from container:', pluginId);
}
}
}
// Fallback 4: Try to get from currentPluginConfig
// The action button (plugin_config.html) always passes its plugin's id.
if (!pluginId) {
pluginId = currentPluginConfig?.pluginId;
if (pluginId) {
debugLog('[DEBUG] Got pluginId from currentPluginConfig:', pluginId);
}
}
// Fallback 5: the active tab, when it is a plugin's tab
if (!pluginId) {
const appData = window.getApp();
if (appData && appData.activeTab && appData.activeTab !== 'overview' && appData.activeTab !== 'plugins' && appData.activeTab !== 'wifi') {
pluginId = appData.activeTab;
debugLog('[DEBUG] Got pluginId from Alpine activeTab:', pluginId);
}
}
// Fallback 6: Try to find from plugin tab elements (scoped to button context)
if (!pluginId && btn) {
try {
// Search within the button's Alpine.js context (closest x-data element)
const buttonContext = btn.closest('[x-data]');
if (buttonContext) {
const pluginTab = buttonContext.querySelector('[x-show*="activeTab === plugin.id"]');
if (pluginTab && window.Alpine) {
try {
const pluginData = Alpine.$data(buttonContext);
if (pluginData && pluginData.plugin) {
pluginId = pluginData.plugin.id;
if (pluginId) {
debugLog('[DEBUG] Got pluginId from Alpine plugin data (scoped to button context):', pluginId);
}
}
} catch (e) {
console.warn('[DEBUG] Error accessing Alpine plugin data:', e);
}
}
}
// If not found in button context, try container element
if (!pluginId) {
const container = btn.closest('.plugin-config-container, .plugin-config-tab, [id^="plugin-config-"]');
if (container) {
const containerContext = container.querySelector('[x-show*="activeTab === plugin.id"]');
if (containerContext && window.Alpine) {
try {
const containerData = Alpine.$data(container.closest('[x-data]'));
if (containerData && containerData.plugin) {
pluginId = containerData.plugin.id;
if (pluginId) {
debugLog('[DEBUG] Got pluginId from Alpine plugin data (scoped to container):', pluginId);
}
}
} catch (e) {
console.warn('[DEBUG] Error accessing Alpine plugin data from container:', e);
}
}
}
}
} catch (e) {
console.warn('[DEBUG] Error in fallback 6 DOM lookup:', e);
}
}
// Final check - if still no pluginId, show error
if (!pluginId) {
console.error('No plugin ID available after all fallbacks. actionId:', actionId, 'actionIndex:', actionIndex);
console.error('[DEBUG] Button found:', !!btn);
console.error('[DEBUG] currentPluginConfig:', currentPluginConfig);
console.error('executePluginAction called without a plugin ID. actionId:', actionId, 'actionIndex:', actionIndex);
showNotification('Unable to determine plugin ID. Please refresh the page.', 'error');
return;
}
debugLog('[DEBUG] executePluginAction - Final pluginId:', pluginId, 'actionId:', actionId, 'actionIndex:', actionIndex);
if (!btn || !statusDiv) {
console.error(`Action elements not found: ${actionIdFull}`);
return;
}
// Get action definition - try currentPluginConfig first, then fetch from API
let action = currentPluginConfig?.webUiActions?.[actionIndex];
if (!action) {
// Try to get from installed plugins
if (window.installedPlugins) {
const plugin = window.installedPlugins.find(p => p.id === pluginId);
if (plugin && plugin.web_ui_actions) {
action = plugin.web_ui_actions[actionIndex];
}
let action = null;
if (window.installedPlugins) {
const plugin = window.installedPlugins.find(p => p.id === pluginId);
if (plugin && plugin.web_ui_actions) {
action = plugin.web_ui_actions[actionIndex];
}
}
if (!action) {
console.error(`Action not found: ${actionId} for plugin ${pluginId}`);
debugLog('[DEBUG] currentPluginConfig:', currentPluginConfig);
debugLog('[DEBUG] installedPlugins:', window.installedPlugins);
showNotification(`Action ${actionId} not found. Please refresh the page.`, 'error');
return;
}
debugLog('[DEBUG] Found action:', action);
// Check if we're in step 2 (completing OAuth flow)
if (btn.dataset.step === '2') {
const redirectUrl = prompt(action.step2_prompt || 'Please paste the full redirect URL:');
@@ -2210,11 +2080,11 @@ function pollOperationStatus(operationId, pluginId, pluginName, options = {}) {
const onComplete = options.onComplete || (() => handleUninstallSuccess(pluginId));
const onFailed = options.onFailed || ((errorMsg) => {
showNotification(errorMsg || `Operation failed for ${pluginName}`, 'error');
setTimeout(() => loadInstalledPlugins(), 1000);
setTimeout(() => loadInstalledPlugins().catch(() => {}), 1000);
});
const onTimeout = options.onTimeout || (() => {
showNotification(`Operation timed out for ${pluginName}`, 'error');
setTimeout(() => loadInstalledPlugins(), 1000);
setTimeout(() => loadInstalledPlugins().catch(() => {}), 1000);
});
if (attempt >= maxAttempts) {
@@ -2250,7 +2120,7 @@ function pollOperationStatus(operationId, pluginId, pluginName, options = {}) {
console.error('Error polling operation status:', error);
// On error, refresh plugin list to see actual state
setTimeout(() => {
loadInstalledPlugins();
loadInstalledPlugins().catch(() => {});
}, 1000);
});
}
@@ -2268,7 +2138,7 @@ function handleUninstallSuccess(pluginId) {
// refreshInstalledPlugins() bypasses them.
pluginLoadCache.invalidate();
setTimeout(() => {
refreshInstalledPlugins();
refreshInstalledPlugins().catch(() => {});
}, 1000);
}
@@ -2284,7 +2154,7 @@ function refreshPlugins() {
refreshInstalledPlugins().then(() => {
searchPluginStore(true);
showNotification('Plugins refreshed with latest metadata from GitHub', 'success');
});
}, () => {});
}
function restartDisplay() {
@@ -2637,7 +2507,7 @@ window.installPlugin = function(pluginId, branch = null) {
}
});
// Refresh installed plugins list, then re-render store to update badges
loadInstalledPlugins();
loadInstalledPlugins().catch(() => {});
setTimeout(() => applyStoreFiltersAndSort(true), 500);
}
@@ -2696,7 +2566,7 @@ window.installFromCustomRegistry = function(pluginId, registryUrl, pluginPath, b
if (data.status === 'success') {
showNotification(`Plugin ${data.plugin_id} installed successfully`, 'success');
// Refresh installed plugins and re-render custom registry
loadInstalledPlugins();
loadInstalledPlugins().catch(() => {});
// Re-render custom registry to update install buttons
const registryUrlInput = document.getElementById('github-registry-url');
if (registryUrlInput && registryUrlInput.value.trim()) {
@@ -2890,7 +2760,7 @@ function attachInstallButtonHandler() {
// Refresh installed plugins list
setTimeout(() => {
loadInstalledPlugins();
loadInstalledPlugins().catch(() => {});
}, 1000);
} else {
if (pluginStatusDiv) {