fix(plugins): Resolve plugin action button errors and config save permission issues (#162)

* fix(plugins): Resolve plugin ID determination error in action buttons

- Fix server-side template parameter order for executePluginAction
- Add data-plugin-id attributes to action buttons in all templates
- Enhance executePluginAction with comprehensive fallback logic
- Support retrieving pluginId from DOM, Alpine context, and config state
- Fixes 'Unable to determine plugin ID' error for Spotify/YouTube auth

* fix(plugins): Add missing button IDs and status divs in server-side action template

- Add action-{id}-{index} IDs to action buttons
- Add action-status-{id}-{index} status divs for each action
- Match client-side template structure for consistency
- Fixes 'Action elements not found' error

* fix(api): Fix indentation error in execute_plugin_action function

- Fix incorrect else block indentation that caused 500 errors
- Correct indentation for OAuth flow and simple script execution paths
- Resolves syntax error preventing plugin actions from executing

* fix(api): Improve error handling for plugin actions and config saves

- Add better JSON parsing error handling with request details
- Add detailed permission error messages for secrets file saves
- Include file path and permission status in error responses
- Helps diagnose 400 errors on action execution and 500 errors on config saves

* fix(api): Add detailed permission error handling for secrets config saves

- Add PermissionError-specific handling with permission checks
- Include directory and file permission status in error logs
- Provide more helpful error messages with file paths
- Helps diagnose permission issues when saving config_secrets.json

* fix(config): Add permission check and actionable error message for config saves

- Check file writability before attempting write
- Show file owner and current permissions in error message
- Provide exact command to fix permissions (chown + chmod)
- Helps diagnose and resolve permission issues with config_secrets.json

* fix(config): Preserve detailed permission error messages

- Handle PermissionError separately to preserve detailed error messages
- Ensure actionable permission fix commands are included in error response
- Prevents detailed error messages from being lost in exception chain

* fix(config): Remove overly strict pre-write permission check

- Remove pre-write file existence/writability check that was blocking valid writes
- Let actual file write operation determine success/failure
- Provide detailed error messages only when write actually fails
- Fixes regression where config_secrets.json saves were blocked unnecessarily

* fix(config): Use atomic writes for config_secrets.json to handle permission issues

- Write to temp file first, then atomically move to final location
- Works even when existing file isn't writable (as long as directory is writable)
- Matches pattern used elsewhere in codebase (disk_cache, atomic_manager)
- Fixes permission errors when saving secrets configuration

* chore: Update music plugin submodule to include live_priority fix

* fix(plugins): Improve plugin ID determination in dynamic button generation

- Update generateFormFromSchema to pass currentPluginConfig?.pluginId and add data attributes
- Update generateSimpleConfigForm to pass currentPluginConfig?.pluginId and add data attributes
- Scope fallback 6 DOM lookup to button context instead of document-wide search
- Ensures correct plugin tab selection when multiple plugins are present
- Maintains existing try/catch error handling and logging

---------

Co-authored-by: Chuck <chuck@example.com>
This commit is contained in:
Chuck
2025-12-29 22:17:11 -05:00
committed by GitHub
parent 1815a5b791
commit 24c34c5a40
6 changed files with 460 additions and 178 deletions

View File

@@ -2872,7 +2872,9 @@ function generateFormFromSchema(schema, config, webUiActions = []) {
</div>
<button type="button"
id="${actionId}"
onclick="executePluginAction('${action.id}', ${index})"
onclick="executePluginAction('${action.id}', ${index}, '${window.currentPluginConfig?.pluginId || ''}')"
data-plugin-id="${window.currentPluginConfig?.pluginId || ''}"
data-action-id="${action.id}"
class="btn ${colors.btn} text-white px-4 py-2 rounded-md whitespace-nowrap">
${action.icon ? `<i class="${action.icon} mr-2"></i>` : ''}${action.button_text || action.title || 'Execute'}
</button>
@@ -3196,7 +3198,9 @@ function generateSimpleConfigForm(config, webUiActions = []) {
</div>
<button type="button"
id="${actionId}"
onclick="executePluginAction('${action.id}', ${index})"
onclick="executePluginAction('${action.id}', ${index}, '${window.currentPluginConfig?.pluginId || ''}')"
data-plugin-id="${window.currentPluginConfig?.pluginId || ''}"
data-action-id="${action.id}"
class="btn ${colors.btn} text-white px-4 py-2 rounded-md">
${action.icon ? `<i class="${action.icon} mr-2"></i>` : ''}${action.button_text || action.title || 'Execute'}
</button>
@@ -3546,37 +3550,137 @@ window.closePluginConfigModal = function() {
// Generic Plugin Action Handler
window.executePluginAction = function(actionId, actionIndex, pluginIdParam = null) {
// Get plugin ID from parameter, currentPluginConfig, or try to find from context
let pluginId = pluginIdParam || currentPluginConfig?.pluginId;
console.log('[DEBUG] executePluginAction called - actionId:', actionId, 'actionIndex:', actionIndex, 'pluginIdParam:', pluginIdParam);
// If still no pluginId, try to find it from the button's context or Alpine.js
if (!pluginId) {
// Try to get from Alpine.js context if we're in a plugin tab
if (window.Alpine && document.querySelector('[x-data*="plugin"]')) {
const pluginTab = document.querySelector(`[x-show*="activeTab === plugin.id"]`);
if (pluginTab) {
const pluginData = Alpine.$data(pluginTab.closest('[x-data]'));
if (pluginData && pluginData.plugin) {
pluginId = pluginData.plugin.id;
}
// Construct button ID first (we have actionId and actionIndex)
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) {
console.log('[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) {
console.log('[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) {
console.log('[DEBUG] Got pluginId from container:', pluginId);
}
}
}
// Fallback 4: Try to get from currentPluginConfig
if (!pluginId) {
console.error('No plugin ID available. actionId:', actionId, 'actionIndex:', actionIndex);
pluginId = currentPluginConfig?.pluginId;
if (pluginId) {
console.log('[DEBUG] Got pluginId from currentPluginConfig:', pluginId);
}
}
// Fallback 5: Try to get from Alpine.js context (activeTab)
if (!pluginId && window.Alpine) {
try {
const appElement = document.querySelector('[x-data="app()"]');
if (appElement && appElement._x_dataStack && appElement._x_dataStack[0]) {
const appData = appElement._x_dataStack[0];
if (appData.activeTab && appData.activeTab !== 'overview' && appData.activeTab !== 'plugins' && appData.activeTab !== 'wifi') {
pluginId = appData.activeTab;
console.log('[DEBUG] Got pluginId from Alpine activeTab:', pluginId);
}
}
} catch (e) {
console.warn('[DEBUG] Error accessing Alpine context:', e);
}
}
// 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) {
console.log('[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) {
console.log('[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);
if (typeof showNotification === 'function') {
showNotification('Unable to determine plugin ID. Please refresh the page.', 'error');
}
return;
}
console.log('[DEBUG] executePluginAction - pluginId:', pluginId, 'actionId:', actionId, 'actionIndex:', actionIndex);
const actionIdFull = `action-${actionId}-${actionIndex}`;
const statusId = `action-status-${actionId}-${actionIndex}`;
const btn = document.getElementById(actionIdFull);
const statusDiv = document.getElementById(statusId);
console.log('[DEBUG] executePluginAction - Final pluginId:', pluginId, 'actionId:', actionId, 'actionIndex:', actionIndex);
if (!btn || !statusDiv) {
console.error(`Action elements not found: ${actionIdFull}`);