Files
p2ns/includes/admin/admin-frontend/ui/plugins.js
T
2025-12-17 20:05:50 -05:00

839 lines
30 KiB
JavaScript

// Plugins UI functions
let pluginsData = [];
// Fetch plugins from API
async function fetchPlugins() {
try {
const res = await fetch('/api/plugins');
const data = await res.json();
pluginsData = data.plugins || [];
return pluginsData;
} catch (err) {
console.error('Failed to fetch plugins:', err);
if (window.showNotification) window.showNotification('Failed to load plugins', 'error');
return [];
}
}
// Initialize plugin log buffers and terminals (if not already initialized in state.js)
if (!window.pluginLogBuffers) {
window.pluginLogBuffers = new Map();
}
if (!window.pluginTerminals) {
window.pluginTerminals = new Map();
}
if (!window.pluginFitAddons) {
window.pluginFitAddons = new Map();
}
if (!window.pluginResizeObservers) {
window.pluginResizeObservers = new Map();
}
// Render plugins
async function renderPlugins() {
const container = document.getElementById('pluginsContainer');
if (!container) return;
const plugins = await fetchPlugins();
pluginsData = plugins;
if (plugins.length === 0) {
container.innerHTML = `
<div class="theme-card p-8 text-center">
<p class="theme-text-tertiary text-lg">No plugins found</p>
<p class="theme-text-tertiary text-sm mt-2">Plugins are loaded from the plugin-sites/ directory</p>
</div>
`;
return;
}
// Sort plugins: example.plugin always at the bottom
const sortedPlugins = [...plugins].sort((a, b) => {
if (a.domain === 'example.plugin') return 1;
if (b.domain === 'example.plugin') return -1;
return a.name.localeCompare(b.name);
});
// Update pluginsData to sorted order for filtering
pluginsData = sortedPlugins;
container.innerHTML = sortedPlugins.map(plugin => renderPluginCard(plugin)).join('');
// Don't initialize terminals on page load - they'll be initialized when logs section is shown
// Attach event listeners for action buttons
sortedPlugins.forEach(plugin => {
plugin.actions.forEach(action => {
const buttonId = `action-${plugin.domain}-${action.name}`;
const button = document.getElementById(buttonId);
if (button) {
button.addEventListener('click', () => executeAction(plugin.domain, action.name, action));
}
});
});
}
// Initialize terminal for a plugin
function initializePluginTerminal(domain) {
const terminalEl = document.getElementById(`plugin-terminal-${domain}`);
if (!terminalEl) {
// Terminal element doesn't exist yet, try again after a short delay
setTimeout(() => initializePluginTerminal(domain), 100);
return;
}
// Clean up existing terminal if any
cleanupPluginTerminal(domain);
// Check if Terminal is available
if (typeof Terminal === 'undefined' || typeof FitAddon === 'undefined') {
terminalEl.innerHTML = '<p class="theme-text-tertiary p-2">Terminal not available. Please refresh the page.</p>';
return;
}
try {
const term = new Terminal({
fontSize: 12,
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace',
theme: {
background: '#000000',
foreground: '#ffffff'
},
rows: 10,
cols: 80
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(terminalEl);
// Small delay to ensure DOM is ready before fitting
setTimeout(() => {
try {
fitAddon.fit();
} catch (err) {
// Ignore fit errors
}
}, 100);
// Store terminal first so logs can be written to it immediately
if (!window.pluginTerminals) {
window.pluginTerminals = new Map();
}
window.pluginTerminals.set(domain, term);
// Load existing log buffer if available
if (!window.pluginLogBuffers) {
window.pluginLogBuffers = new Map();
}
const buffer = window.pluginLogBuffers.get(domain) || [];
if (buffer.length > 0) {
buffer.forEach(line => term.writeln(line));
} else {
term.writeln('No logs yet. Logs will appear here as they are generated.');
}
// Store fitAddon for resize handling
if (!window.pluginFitAddons) {
window.pluginFitAddons = new Map();
}
window.pluginFitAddons.set(domain, fitAddon);
// Handle resize
const resizeObserver = new ResizeObserver(() => {
try {
if (window.pluginFitAddons && window.pluginFitAddons.has(domain)) {
const addon = window.pluginFitAddons.get(domain);
if (addon) {
addon.fit();
}
}
} catch (err) {
// Ignore resize errors
}
});
resizeObserver.observe(terminalEl);
// Store observer for cleanup
if (!window.pluginResizeObservers) {
window.pluginResizeObservers = new Map();
}
window.pluginResizeObservers.set(domain, resizeObserver);
} catch (err) {
console.error(`Error initializing terminal for plugin ${domain}:`, err);
terminalEl.innerHTML = `<p class="text-red-400 p-2">Error initializing terminal: ${err.message}</p>`;
}
}
// Render a single plugin card
function renderPluginCard(plugin) {
const actionsHtml = plugin.status === 'stopped'
? '<p class="text-sm theme-text-tertiary mt-4">Actions unavailable (plugin stopped)</p>'
: plugin.actions.length > 0
? `
<div class="mt-4">
<h4 class="text-sm font-semibold theme-text-primary mb-2">Actions</h4>
<div class="flex flex-wrap gap-2">
${plugin.actions.map(action => `
<button
id="action-${plugin.domain}-${action.name}"
class="px-3 py-1 text-sm rounded transition-colors"
style="background: rgba(59, 130, 246, 0.3); border: 1px solid rgba(59, 130, 246, 0.5); color: var(--text-primary); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);"
onmouseover="this.style.background='rgba(59, 130, 246, 0.5)'; this.style.borderColor='rgba(59, 130, 246, 0.7)'"
onmouseout="this.style.background='rgba(59, 130, 246, 0.3)'; this.style.borderColor='rgba(59, 130, 246, 0.5)'"
title="${action.description || action.label}"
>
${action.icon || '⚡'} ${action.label || action.name}
</button>
`).join('')}
</div>
</div>
`
: '<p class="text-sm theme-text-tertiary mt-4">No actions registered</p>';
const settingsHtml = plugin.status === 'stopped'
? '<p class="text-sm theme-text-tertiary mt-4">Settings unavailable (plugin stopped)</p>'
: Object.keys(plugin.settings).length > 0
? `
<div class="mt-4">
<h4 class="text-sm font-semibold theme-text-primary mb-2">Settings</h4>
<div class="space-y-2">
${Object.entries(plugin.settings).map(([key, setting]) => renderSettingInput(plugin.domain, key, setting)).join('')}
</div>
<button
onclick="savePluginSettings('${plugin.domain}')"
class="mt-3 px-4 py-2 text-sm rounded transition-colors"
style="background: rgba(34, 197, 94, 0.3); border: 1px solid rgba(34, 197, 94, 0.5); color: var(--text-primary); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);"
onmouseover="this.style.background='rgba(34, 197, 94, 0.5)'; this.style.borderColor='rgba(34, 197, 94, 0.7)'"
onmouseout="this.style.background='rgba(34, 197, 94, 0.3)'; this.style.borderColor='rgba(34, 197, 94, 0.5)'"
>
Save Settings
</button>
</div>
`
: '<p class="text-sm theme-text-tertiary mt-4">No settings registered</p>';
const statusBadge = plugin.status === 'loaded'
? '<span class="px-2 py-1 bg-green-500 rounded text-xs" style="color: var(--text-primary);">Loaded</span>'
: plugin.status === 'stopped'
? '<span class="px-2 py-1 bg-red-500 rounded text-xs" style="color: var(--text-primary);">Stopped</span>'
: '<span class="px-2 py-1 bg-primary rounded text-xs" style="color: var(--text-primary);">Static</span>';
const featuresHtml = [
plugin.hasHandler ? '<span class="text-xs bg-blue-500 px-2 py-1 rounded" style="color: var(--text-primary);">Handler</span>' : '',
plugin.hasWww ? '<span class="text-xs bg-purple-500 px-2 py-1 rounded" style="color: var(--text-primary);">Web UI</span>' : '',
plugin.hasDatabase ? '<span class="text-xs bg-orange-500 px-2 py-1 rounded" style="color: var(--text-primary);">Database</span>' : ''
].filter(Boolean).join('');
return `
<div class="theme-card p-6 plugin-card" data-plugin-domain="${plugin.domain}">
<div class="flex justify-between items-start mb-4">
<div class="flex-1">
<div class="flex items-center gap-4 mb-2">
<h3 class="text-xl font-bold theme-text-primary flex items-center gap-2">
${plugin.icon ? `<i class="fa-solid fa-${escapeHtml(plugin.icon)}"></i>` : ''}
${escapeHtml(plugin.name)}
</h3>
<div class="ml-2">
${statusBadge}
</div>
</div>
<p class="text-sm theme-text-secondary">${escapeHtml(plugin.description || 'No description')}</p>
<div class="flex items-center gap-4 mt-2 text-xs theme-text-tertiary">
<span>v${escapeHtml(plugin.version)}</span>
${plugin.author ? `<span>by ${escapeHtml(plugin.author)}</span>` : ''}
</div>
${featuresHtml ? `<div class="flex gap-2 mt-2">${featuresHtml}</div>` : ''}
</div>
<div class="flex flex-col gap-2 items-end">
<div class="flex items-center gap-2 theme-glass px-3 py-2 rounded-lg">
<span class="text-xs font-semibold theme-text-secondary uppercase tracking-wide">Status</span>
${(['p2ns.admin', 'global.profile'].includes(plugin.domain) ? `
<div class="flex items-center gap-2">
<div class="w-11 h-6 rounded-full flex items-center justify-end px-1" style="background: rgba(59, 130, 246, 0.3); border: 1px solid rgba(59, 130, 246, 0.5); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);">
<div class="w-5 h-5 rounded-full" style="background: var(--text-primary); border: 1px solid var(--border-color);"></div>
</div>
<span class="ml-3 text-sm font-medium theme-text-primary min-w-[70px]">
<span style="color: var(--success);">Enabled</span>
<span class="ml-2 text-xs theme-text-tertiary">(System)</span>
</span>
</div>
` : `
<label class="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
class="sr-only peer"
${plugin.enabled !== false ? 'checked' : ''}
onchange="togglePluginEnabled('${plugin.domain}', this.checked)"
id="toggle-${plugin.domain}"
>
<div class="w-11 h-6 rounded-full peer peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:rounded-full after:h-5 after:w-5 after:transition-all plugin-toggle-switch" style="background: var(--bg-glass); border: 1px solid var(--border-color); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);"></div>
<span class="ml-3 text-sm font-medium theme-text-primary min-w-[70px]">
${plugin.enabled !== false ? '<span style="color: var(--success);">Enabled</span>' : '<span style="color: var(--error);">Disabled</span>'}
</span>
</label>
`)}
</div>
<div class="flex gap-2">
${plugin.status === 'loaded' ? `
<button
onclick="reloadPlugin('${plugin.domain}')"
class="px-4 py-2 rounded transition-colors flex items-center gap-2"
style="background: rgba(234, 179, 8, 0.3); border: 1px solid rgba(234, 179, 8, 0.5); color: var(--text-primary); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);"
onmouseover="this.style.background='rgba(234, 179, 8, 0.5)'; this.style.borderColor='rgba(234, 179, 8, 0.7)'"
onmouseout="this.style.background='rgba(234, 179, 8, 0.3)'; this.style.borderColor='rgba(234, 179, 8, 0.5)'"
title="Reload this plugin without restarting P2NS"
>
🔄 Restart
</button>
${(['p2ns.admin', 'global.profile'].includes(plugin.domain) ? '' : `
<button
onclick="stopPlugin('${plugin.domain}')"
class="px-4 py-2 theme-button-info rounded theme-glass-hover transition-colors flex items-center gap-2"
title="Stop this plugin (unload it from memory)"
>
⏹️ Stop
</button>
`)}
` : plugin.enabled !== false ? `
<button
onclick="startPlugin('${plugin.domain}')"
class="px-4 py-2 rounded transition-colors flex items-center gap-2"
style="background: rgba(34, 197, 94, 0.3); border: 1px solid rgba(34, 197, 94, 0.5); color: var(--text-primary); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);"
onmouseover="this.style.background='rgba(34, 197, 94, 0.5)'; this.style.borderColor='rgba(34, 197, 94, 0.7)'"
onmouseout="this.style.background='rgba(34, 197, 94, 0.3)'; this.style.borderColor='rgba(34, 197, 94, 0.5)'"
title="Start this plugin (load it into memory)"
>
▶️ Start
</button>
` : ''}
</div>
</div>
</div>
<div class="border-t pt-4 mt-4" style="border-color: var(--border-color);">
${actionsHtml}
${settingsHtml}
</div>
${plugin.status === 'stopped' ? `
<div class="mt-4 p-3 rounded theme-glass" style="background: rgba(234, 179, 8, 0.2); border: 1px solid rgba(234, 179, 8, 0.4); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%);">
<p class="text-sm" style="color: var(--text-primary);">
⚠️ This plugin is currently stopped. Actions and settings are not available until it is started.
</p>
</div>
` : ''}
<div id="plugin-logs-${plugin.domain}" class="mt-4 hidden">
<h4 class="text-sm font-semibold theme-text-primary mb-2">Logs</h4>
<div id="plugin-terminal-${plugin.domain}" class="bg-black rounded-lg overflow-hidden" style="height: 200px;"></div>
</div>
<div class="mt-4 text-xs theme-text-tertiary">
<span>Domain: <code class="theme-glass px-1 rounded">${escapeHtml(plugin.domain)}</code></span>
</div>
</div>
`;
}
// Render a setting input field
function renderSettingInput(domain, key, setting) {
const inputId = `setting-${domain}-${key}`;
// Use saved value if available, otherwise use default
const currentValue = setting.value !== undefined ? setting.value : (setting.default !== undefined ? setting.default : '');
switch (setting.type) {
case 'boolean':
return `
<div class="flex items-center gap-2">
<input
type="checkbox"
id="${inputId}"
data-plugin-domain="${domain}"
data-setting-key="${key}"
${currentValue ? 'checked' : ''}
class="w-4 h-4 text-primary theme-glass rounded focus:ring-primary"
/>
<label for="${inputId}" class="text-sm theme-text-primary">
${escapeHtml(setting.label || key)}
</label>
</div>
${setting.description ? `<p class="text-xs theme-text-tertiary ml-6">${escapeHtml(setting.description)}</p>` : ''}
`;
case 'number':
return `
<div>
<label for="${inputId}" class="block text-sm theme-text-primary mb-1">
${escapeHtml(setting.label || key)}
</label>
<input
type="number"
id="${inputId}"
data-plugin-domain="${domain}"
data-setting-key="${key}"
value="${currentValue}"
class="w-full p-2 theme-input rounded focus:outline-none focus:ring-2 focus:ring-primary"
/>
${setting.description ? `<p class="text-xs theme-text-tertiary mt-1">${escapeHtml(setting.description)}</p>` : ''}
</div>
`;
case 'select':
const optionsHtml = (setting.options || []).map(opt => {
const value = typeof opt === 'object' ? opt.value : opt;
const label = typeof opt === 'object' ? opt.label : opt;
return `<option value="${escapeHtml(value)}" ${value === currentValue ? 'selected' : ''}>${escapeHtml(label)}</option>`;
}).join('');
return `
<div>
<label for="${inputId}" class="block text-sm theme-text-primary mb-1">
${escapeHtml(setting.label || key)}
</label>
<select
id="${inputId}"
data-plugin-domain="${domain}"
data-setting-key="${key}"
class="w-full p-2 theme-input rounded focus:outline-none focus:ring-2 focus:ring-primary"
>
${optionsHtml}
</select>
${setting.description ? `<p class="text-xs theme-text-tertiary mt-1">${escapeHtml(setting.description)}</p>` : ''}
</div>
`;
case 'textarea':
return `
<div>
<label for="${inputId}" class="block text-sm theme-text-primary mb-1">
${escapeHtml(setting.label || key)}
</label>
<textarea
id="${inputId}"
data-plugin-domain="${domain}"
data-setting-key="${key}"
rows="3"
class="w-full p-2 theme-input rounded focus:outline-none focus:ring-2 focus:ring-primary"
>${escapeHtml(currentValue)}</textarea>
${setting.description ? `<p class="text-xs theme-text-tertiary mt-1">${escapeHtml(setting.description)}</p>` : ''}
</div>
`;
default: // string
return `
<div>
<label for="${inputId}" class="block text-sm theme-text-primary mb-1">
${escapeHtml(setting.label || key)}
</label>
<input
type="text"
id="${inputId}"
data-plugin-domain="${domain}"
data-setting-key="${key}"
value="${escapeHtml(currentValue)}"
class="w-full p-2 theme-input rounded focus:outline-none focus:ring-2 focus:ring-primary"
/>
${setting.description ? `<p class="text-xs theme-text-tertiary mt-1">${escapeHtml(setting.description)}</p>` : ''}
</div>
`;
}
}
// Execute a plugin action
async function executeAction(domain, actionName, action) {
if (!action) {
const plugin = pluginsData.find(p => p.domain === domain);
action = plugin?.actions?.find(a => a.name === actionName);
}
if (!action) {
if (window.showNotification) window.showNotification('Action not found', 'error');
return;
}
try {
const buttonId = `action-${domain}-${actionName}`;
const button = document.getElementById(buttonId);
if (button) {
button.disabled = true;
button.textContent = '⏳ Executing...';
}
// Collect parameters if any
const params = {};
if (action.params && action.params.length > 0) {
// TODO: Show modal to collect parameters
// For now, execute with empty params
}
const res = await fetch(`/api/plugins/${domain}/actions/${actionName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
});
const data = await res.json();
if (res.ok && data.success) {
if (window.showNotification) {
window.showNotification(`Action "${action.label || actionName}" executed successfully`, 'success');
}
// Refresh plugins to get updated state
await renderPlugins();
} else {
if (window.showNotification) {
window.showNotification(data.error || 'Action execution failed', 'error');
}
}
if (button) {
button.disabled = false;
button.innerHTML = `${action.icon || '⚡'} ${action.label || actionName}`;
}
} catch (err) {
console.error('Error executing action:', err);
if (window.showNotification) {
window.showNotification('Failed to execute action', 'error');
}
const buttonId = `action-${domain}-${actionName}`;
const button = document.getElementById(buttonId);
if (button && action) {
button.disabled = false;
button.innerHTML = `${action.icon || '⚡'} ${action.label || actionName}`;
}
}
}
// Show logs section for a plugin
function showPluginLogs(domain) {
const logsSection = document.getElementById(`plugin-logs-${domain}`);
if (logsSection) {
logsSection.classList.remove('hidden');
// Always re-initialize terminal to ensure it's set up correctly
// Use a small delay to ensure DOM is ready
setTimeout(() => {
initializePluginTerminal(domain);
}, 50);
}
}
// Stop a plugin
async function stopPlugin(domain) {
// Prevent stopping system plugins
const SYSTEM_PLUGINS = ['p2ns.admin', 'global.profile'];
if (SYSTEM_PLUGINS.includes(domain)) {
if (window.showNotification) {
window.showNotification(`Cannot stop system plugin: ${domain}. This plugin is required by the system.`, 'error');
}
return;
}
const confirmed = await window.ConfirmationModal.warning(
`Stop plugin "${domain}"? This will unload the plugin from memory. You can start it again later.`,
{
title: 'Stop Plugin',
confirmText: 'Stop',
cancelText: 'Cancel'
}
);
if (!confirmed) {
return;
}
// Show logs section before cleanup
showPluginLogs(domain);
// Clean up terminal for this plugin
cleanupPluginTerminal(domain);
// Re-initialize after cleanup
setTimeout(() => {
showPluginLogs(domain);
}, 100);
try {
const res = await fetch(`/api/plugins/${domain}/stop`, {
method: 'POST'
});
const data = await res.json();
if (res.ok && data.success) {
if (window.showNotification) {
window.showNotification(`Plugin "${domain}" stopped successfully`, 'success');
}
// Refresh plugins list
await renderPlugins();
// Re-show logs section after refresh (with longer delay to ensure DOM is ready)
setTimeout(() => {
showPluginLogs(domain);
}, 200);
} else {
if (window.showNotification) {
window.showNotification(data.error || 'Failed to stop plugin', 'error');
}
}
} catch (err) {
console.error('Error stopping plugin:', err);
if (window.showNotification) {
window.showNotification('Failed to stop plugin', 'error');
}
}
}
// Clean up terminal for a plugin
function cleanupPluginTerminal(domain) {
try {
// Dispose terminal
if (window.pluginTerminals && window.pluginTerminals.has(domain)) {
const term = window.pluginTerminals.get(domain);
if (term) {
term.dispose();
}
window.pluginTerminals.delete(domain);
}
// Disconnect resize observer
if (window.pluginResizeObservers && window.pluginResizeObservers.has(domain)) {
const observer = window.pluginResizeObservers.get(domain);
if (observer) {
observer.disconnect();
}
window.pluginResizeObservers.delete(domain);
}
// Clean up fitAddon
if (window.pluginFitAddons && window.pluginFitAddons.has(domain)) {
window.pluginFitAddons.delete(domain);
}
} catch (err) {
// Ignore cleanup errors
}
}
// Start a plugin
async function startPlugin(domain) {
// Show logs section
showPluginLogs(domain);
try {
const res = await fetch(`/api/plugins/${domain}/start`, {
method: 'POST'
});
const data = await res.json();
if (res.ok && data.success) {
if (window.showNotification) {
window.showNotification(`Plugin "${domain}" started successfully`, 'success');
}
// Refresh plugins list
await renderPlugins();
// Re-show logs section after refresh (with longer delay to ensure DOM is ready)
setTimeout(() => {
showPluginLogs(domain);
}, 200);
} else {
if (window.showNotification) {
window.showNotification(data.error || 'Failed to start plugin', 'error');
}
}
} catch (err) {
console.error('Error starting plugin:', err);
if (window.showNotification) {
window.showNotification('Failed to start plugin', 'error');
}
}
}
// Toggle plugin enabled/disabled state
async function togglePluginEnabled(domain, enabled) {
// Prevent toggling system plugins
const SYSTEM_PLUGINS = ['p2ns.admin', 'global.profile'];
if (SYSTEM_PLUGINS.includes(domain) && !enabled) {
if (window.showNotification) {
window.showNotification(`Cannot disable system plugin: ${domain}. This plugin is required by the system.`, 'error');
}
// Refresh to reset toggle state
await renderPlugins();
return;
}
try {
const res = await fetch(`/api/plugins/${domain}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled })
});
const data = await res.json();
if (res.ok && data.success) {
if (window.showNotification) {
window.showNotification(`Plugin "${domain}" ${enabled ? 'enabled' : 'disabled'} successfully`, 'success');
}
// Refresh plugins list
await renderPlugins();
} else {
if (window.showNotification) {
window.showNotification(data.error || 'Failed to toggle plugin', 'error');
}
// Refresh to reset toggle state
await renderPlugins();
}
} catch (err) {
console.error('Error toggling plugin:', err);
if (window.showNotification) {
window.showNotification('Failed to toggle plugin', 'error');
}
// Refresh to reset toggle state
await renderPlugins();
}
}
// Reload a plugin
async function reloadPlugin(domain) {
const confirmed = await window.ConfirmationModal.warning(
`Reload plugin "${domain}"? This will restart the plugin without restarting P2NS.`,
{
title: 'Reload Plugin',
confirmText: 'Reload',
cancelText: 'Cancel'
}
);
if (!confirmed) {
return;
}
// Show logs section
showPluginLogs(domain);
try {
const res = await fetch(`/api/plugins/${domain}/reload`, {
method: 'POST'
});
const data = await res.json();
if (res.ok && data.success) {
if (window.showNotification) {
window.showNotification(`Plugin "${domain}" reloaded successfully`, 'success');
}
// Refresh plugins list
await renderPlugins();
// Re-show logs section after refresh (with longer delay to ensure DOM is ready)
setTimeout(() => {
showPluginLogs(domain);
}, 200);
} else {
if (window.showNotification) {
window.showNotification(data.error || 'Failed to reload plugin', 'error');
}
}
} catch (err) {
console.error('Error reloading plugin:', err);
if (window.showNotification) {
window.showNotification('Failed to reload plugin', 'error');
}
}
}
// Save plugin settings
async function savePluginSettings(domain) {
try {
const settings = {};
const inputs = document.querySelectorAll(`[data-plugin-domain="${domain}"][data-setting-key]`);
inputs.forEach(input => {
const key = input.dataset.settingKey;
let value;
if (input.type === 'checkbox') {
value = input.checked;
} else if (input.type === 'number') {
value = parseFloat(input.value);
} else {
value = input.value;
}
settings[key] = value;
});
const res = await fetch(`/api/plugins/${domain}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await res.json();
if (res.ok && data.success) {
if (window.showNotification) {
window.showNotification(`Settings saved for plugin "${domain}"`, 'success');
}
} else {
if (window.showNotification) {
window.showNotification(data.error || 'Failed to save settings', 'error');
}
}
} catch (err) {
console.error('Error saving plugin settings:', err);
if (window.showNotification) {
window.showNotification('Failed to save settings', 'error');
}
}
}
// Filter plugins
function filterPlugins() {
const query = document.getElementById('search-plugins')?.value.toLowerCase() || '';
const cards = document.querySelectorAll('.plugin-card');
cards.forEach(card => {
const domain = card.dataset.pluginDomain;
const plugin = pluginsData.find(p => p.domain === domain);
if (!plugin) {
card.style.display = 'none';
return;
}
const searchableText = [
plugin.name,
plugin.domain,
plugin.description,
plugin.version,
plugin.author
].join(' ').toLowerCase();
if (searchableText.includes(query)) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}
// Make functions globally available
window.renderPlugins = renderPlugins;
window.executeAction = executeAction;
window.stopPlugin = stopPlugin;
window.startPlugin = startPlugin;
window.reloadPlugin = reloadPlugin;
window.savePluginSettings = savePluginSettings;
window.filterPlugins = filterPlugins;
window.initializePluginTerminal = initializePluginTerminal;
window.cleanupPluginTerminal = cleanupPluginTerminal;
window.showPluginLogs = showPluginLogs;