/** * Plugin Service - Manages frontend plugins for DSS Admin UI * * Supports loading, enabling/disabling, and configuring plugins. * Plugins can extend UI, add commands, and integrate with services. */ import logger from '../core/logger.js'; class PluginService { constructor() { this.plugins = new Map(); this.hooks = new Map(); this.loadedPlugins = []; this.storageKey = 'dss_plugins_config'; // Load saved plugin states this.config = this._loadConfig(); } /** * Load plugin configuration from localStorage */ _loadConfig() { try { const saved = localStorage.getItem(this.storageKey); return saved ? JSON.parse(saved) : { enabled: {}, settings: {} }; } catch (e) { logger.warn('PluginService', 'Failed to load plugin config', e); return { enabled: {}, settings: {} }; } } /** * Save plugin configuration to localStorage */ _saveConfig() { try { localStorage.setItem(this.storageKey, JSON.stringify(this.config)); } catch (e) { logger.warn('PluginService', 'Failed to save plugin config', e); } } /** * Register a plugin * @param {Object} plugin - Plugin definition */ register(plugin) { if (!plugin.id || !plugin.name) { throw new Error('Plugin must have id and name'); } if (this.plugins.has(plugin.id)) { logger.warn('PluginService', `Plugin ${plugin.id} already registered`); return; } const pluginDef = { id: plugin.id, name: plugin.name, version: plugin.version || '1.0.0', description: plugin.description || '', author: plugin.author || 'Unknown', icon: plugin.icon || '🔌', category: plugin.category || 'general', // Lifecycle hooks onInit: plugin.onInit || (() => {}), onEnable: plugin.onEnable || (() => {}), onDisable: plugin.onDisable || (() => {}), onDestroy: plugin.onDestroy || (() => {}), // Extension points commands: plugin.commands || [], panels: plugin.panels || [], settings: plugin.settings || [], // State enabled: this.config.enabled[plugin.id] ?? plugin.enabledByDefault ?? false, initialized: false }; this.plugins.set(plugin.id, pluginDef); logger.info('PluginService', `Registered plugin: ${plugin.name} v${pluginDef.version}`); // Auto-init if enabled if (pluginDef.enabled) { this._initPlugin(pluginDef); } return pluginDef; } /** * Initialize a plugin */ async _initPlugin(plugin) { if (plugin.initialized) return; try { await plugin.onInit(this._createPluginContext(plugin)); plugin.initialized = true; this.loadedPlugins.push(plugin.id); logger.info('PluginService', `Initialized plugin: ${plugin.name}`); } catch (e) { logger.error('PluginService', `Failed to init plugin ${plugin.name}`, e); } } /** * Create context object passed to plugin hooks */ _createPluginContext(plugin) { return { pluginId: plugin.id, settings: this.config.settings[plugin.id] || {}, // API for plugins registerHook: (hookName, callback) => this.registerHook(plugin.id, hookName, callback), unregisterHook: (hookName) => this.unregisterHook(plugin.id, hookName), getSettings: () => this.config.settings[plugin.id] || {}, setSetting: (key, value) => this.setPluginSetting(plugin.id, key, value), // Access to app APIs emit: (event, data) => this._emitEvent(plugin.id, event, data), log: (msg, data) => logger.info(`Plugin:${plugin.name}`, msg, data) }; } /** * Enable a plugin */ async enable(pluginId) { const plugin = this.plugins.get(pluginId); if (!plugin) { throw new Error(`Plugin ${pluginId} not found`); } if (plugin.enabled) return; plugin.enabled = true; this.config.enabled[pluginId] = true; this._saveConfig(); if (!plugin.initialized) { await this._initPlugin(plugin); } try { await plugin.onEnable(this._createPluginContext(plugin)); logger.info('PluginService', `Enabled plugin: ${plugin.name}`); this._emitEvent('system', 'plugin:enabled', { pluginId }); } catch (e) { logger.error('PluginService', `Failed to enable plugin ${plugin.name}`, e); plugin.enabled = false; this.config.enabled[pluginId] = false; this._saveConfig(); throw e; } } /** * Disable a plugin */ async disable(pluginId) { const plugin = this.plugins.get(pluginId); if (!plugin) { throw new Error(`Plugin ${pluginId} not found`); } if (!plugin.enabled) return; try { await plugin.onDisable(this._createPluginContext(plugin)); plugin.enabled = false; this.config.enabled[pluginId] = false; this._saveConfig(); logger.info('PluginService', `Disabled plugin: ${plugin.name}`); this._emitEvent('system', 'plugin:disabled', { pluginId }); } catch (e) { logger.error('PluginService', `Failed to disable plugin ${plugin.name}`, e); throw e; } } /** * Toggle plugin enabled state */ async toggle(pluginId) { const plugin = this.plugins.get(pluginId); if (!plugin) { throw new Error(`Plugin ${pluginId} not found`); } if (plugin.enabled) { await this.disable(pluginId); } else { await this.enable(pluginId); } } /** * Register a hook callback */ registerHook(pluginId, hookName, callback) { const key = `${pluginId}:${hookName}`; if (!this.hooks.has(hookName)) { this.hooks.set(hookName, new Map()); } this.hooks.get(hookName).set(pluginId, callback); logger.debug('PluginService', `Registered hook ${hookName} for ${pluginId}`); } /** * Unregister a hook callback */ unregisterHook(pluginId, hookName) { const hookMap = this.hooks.get(hookName); if (hookMap) { hookMap.delete(pluginId); } } /** * Execute all callbacks for a hook */ async executeHook(hookName, data = {}) { const hookMap = this.hooks.get(hookName); if (!hookMap) return []; const results = []; for (const [pluginId, callback] of hookMap) { const plugin = this.plugins.get(pluginId); if (plugin?.enabled) { try { const result = await callback(data); results.push({ pluginId, result }); } catch (e) { logger.error('PluginService', `Hook ${hookName} failed for ${pluginId}`, e); } } } return results; } /** * Set a plugin setting */ setPluginSetting(pluginId, key, value) { if (!this.config.settings[pluginId]) { this.config.settings[pluginId] = {}; } this.config.settings[pluginId][key] = value; this._saveConfig(); this._emitEvent(pluginId, 'settings:changed', { key, value }); } /** * Get plugin settings */ getPluginSettings(pluginId) { return this.config.settings[pluginId] || {}; } /** * Emit event to listeners */ _emitEvent(source, event, data) { window.dispatchEvent(new CustomEvent('dss:plugin:event', { detail: { source, event, data } })); } /** * Get all registered plugins */ getAll() { return Array.from(this.plugins.values()); } /** * Get enabled plugins */ getEnabled() { return this.getAll().filter(p => p.enabled); } /** * Get plugin by ID */ get(pluginId) { return this.plugins.get(pluginId); } /** * Get all commands from enabled plugins */ getAllCommands() { const commands = []; for (const plugin of this.getEnabled()) { for (const cmd of plugin.commands) { commands.push({ ...cmd, pluginId: plugin.id, pluginName: plugin.name }); } } return commands; } /** * Get all panels from enabled plugins */ getAllPanels() { const panels = []; for (const plugin of this.getEnabled()) { for (const panel of plugin.panels) { panels.push({ ...panel, pluginId: plugin.id, pluginName: plugin.name }); } } return panels; } } // Singleton instance const pluginService = new PluginService(); export default pluginService; export { PluginService };