839 lines
30 KiB
JavaScript
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;
|
|
|
|
|