/** * Scripts Manager UI * * Three-panel layout: * - Left: Scripts list * - Center: Editor * - Right: Preview/Test */ import { scriptExecutor } from './script-executor.js'; const api = window.app; // ===== BroadcastChannel for intra-extension messaging ===== // Falls back to IPC pubsub if BroadcastChannel is unavailable (e.g., custom protocol origins) let scriptsChannel = null; const scriptsChannelHandlers = {}; try { scriptsChannel = new BroadcastChannel('scripts'); scriptsChannel.onmessage = (e) => { const { topic, data } = e.data; if (scriptsChannelHandlers[topic]) { for (const handler of scriptsChannelHandlers[topic]) { handler(data); } } }; } catch (err) { console.warn('[scripts] BroadcastChannel unavailable, using IPC fallback:', err.message); } function onChannel(topic, handler) { if (!scriptsChannelHandlers[topic]) scriptsChannelHandlers[topic] = []; scriptsChannelHandlers[topic].push(handler); if (!scriptsChannel) api.subscribe(topic, handler, api.scopes.GLOBAL); } function emitChannel(topic, data) { if (scriptsChannel) { scriptsChannel.postMessage({ topic, data }); } else { api.publish(topic, data, api.scopes.GLOBAL); } } // UI Elements let scriptsList; let editorForm; let scriptName; let scriptDescription; let matchPattern; let runAt; let codeEditor; let testUrl; let previewContent; let saveBtn; let revertBtn; let deleteBtn; let testBtn; let newScriptBtn; // State let scripts = []; let currentScript = null; let originalScript = null; // For revert /** * Initialize UI */ async function init() { console.log('[scripts-manager] Initializing...'); // Get DOM elements scriptsList = document.getElementById('scriptsList'); editorForm = document.getElementById('editorForm'); scriptName = document.getElementById('scriptName'); scriptDescription = document.getElementById('scriptDescription'); matchPattern = document.getElementById('matchPattern'); runAt = document.getElementById('runAt'); codeEditor = document.getElementById('codeEditor'); testUrl = document.getElementById('testUrl'); previewContent = document.getElementById('previewContent'); saveBtn = document.getElementById('saveBtn'); revertBtn = document.getElementById('revertBtn'); deleteBtn = document.getElementById('deleteBtn'); testBtn = document.getElementById('testBtn'); newScriptBtn = document.getElementById('newScriptBtn'); // Attach event listeners newScriptBtn.addEventListener('click', handleNewScript); saveBtn.addEventListener('click', handleSave); revertBtn.addEventListener('click', handleRevert); deleteBtn.addEventListener('click', handleDelete); testBtn.addEventListener('click', handleTest); // Load scripts from backend await loadScripts(); // Subscribe to updates via BroadcastChannel onChannel('scripts:created', () => loadScripts()); onChannel('scripts:updated', () => loadScripts()); onChannel('scripts:deleted', () => loadScripts()); // Check URL params for scriptId const params = new URLSearchParams(window.location.search); const scriptId = params.get('scriptId'); if (scriptId) { const script = scripts.find(s => s.id === scriptId); if (script) { selectScript(script); } } console.log('[scripts-manager] Initialized with', scripts.length, 'scripts'); } /** * Load all scripts from backend */ async function loadScripts() { return new Promise((resolve) => { onChannel('scripts:get-all:response', (msg) => { if (msg.success) { scripts = msg.data || []; renderScriptsList(); resolve(); } }); emitChannel('scripts:get-all', {}); }); } /** * Render scripts list in left panel */ function renderScriptsList() { if (scripts.length === 0) { scriptsList.innerHTML = '
No scripts yet. Click "+ New Script" to create one.
'; return; } scriptsList.innerHTML = scripts.map(script => { const status = script.lastExecutedAt ? (script.enabled ? 'success' : 'disabled') : 'never'; const lastRun = script.lastExecutedAt ? formatTime(script.lastExecutedAt) : 'Never run'; const isActive = currentScript && currentScript.id === script.id; return `
${escapeHtml(script.name)}
${lastRun}
`; }).join(''); // Attach click handlers scriptsList.querySelectorAll('.script-item').forEach(item => { item.addEventListener('click', (e) => { if (e.target.classList.contains('script-checkbox')) { return; // Handle checkbox separately } const scriptId = item.dataset.scriptId; const script = scripts.find(s => s.id === scriptId); if (script) { selectScript(script); } }); }); // Attach checkbox handlers scriptsList.querySelectorAll('.script-checkbox').forEach(checkbox => { checkbox.addEventListener('change', async (e) => { e.stopPropagation(); const scriptId = checkbox.dataset.scriptId; const enabled = checkbox.checked; await updateScriptField(scriptId, { enabled }); }); }); } /** * Select a script to edit */ function selectScript(script) { currentScript = script; originalScript = JSON.parse(JSON.stringify(script)); // Deep clone for revert // Populate form scriptName.value = script.name; scriptDescription.value = script.description || ''; matchPattern.value = script.matchPatterns[0] || ''; runAt.value = script.runAt; codeEditor.value = script.code; // Enable actions deleteBtn.disabled = false; // Re-render to show active state renderScriptsList(); } /** * Handle new script creation */ async function handleNewScript() { return new Promise((resolve) => { onChannel('scripts:create:response', async (msg) => { if (msg.success) { await loadScripts(); selectScript(msg.data); resolve(); } }); emitChannel('scripts:create', { name: 'New Script', code: '// Your script here\nconst h1 = document.querySelector(\'h1\');\nreturn { title: h1?.textContent || \'No h1 found\' };' }); }); } /** * Handle save */ async function handleSave() { if (!currentScript) return; const updates = { name: scriptName.value, description: scriptDescription.value, matchPatterns: [matchPattern.value], runAt: runAt.value, code: codeEditor.value }; await updateScriptField(currentScript.id, updates); } /** * Handle revert */ function handleRevert() { if (!originalScript) return; selectScript(originalScript); } /** * Handle delete */ async function handleDelete() { if (!currentScript) return; if (!confirm(`Delete script "${currentScript.name}"?`)) { return; } return new Promise((resolve) => { onChannel('scripts:delete:response', async (msg) => { if (msg.success) { currentScript = null; originalScript = null; await loadScripts(); // Clear form scriptName.value = ''; scriptDescription.value = ''; matchPattern.value = ''; codeEditor.value = ''; deleteBtn.disabled = true; resolve(); } }); emitChannel('scripts:delete', { scriptId: currentScript.id }); }); } /** * Handle test execution */ async function handleTest() { if (!currentScript) { showPreviewError('No script selected'); return; } const url = testUrl.value.trim(); if (!url) { showPreviewError('Please enter a test URL'); return; } testBtn.disabled = true; testBtn.loading = true; try { // Use current form values (not saved script) const testScript = { ...currentScript, name: scriptName.value, code: codeEditor.value, matchPatterns: [matchPattern.value], runAt: runAt.value }; const result = await scriptExecutor.executeScript(testScript, { url: url, pageDOM: document, pageWindow: window }); showPreviewResult(result); } catch (error) { showPreviewError(error.message); } finally { testBtn.disabled = false; testBtn.loading = false; } } /** * Update script field */ async function updateScriptField(scriptId, updates) { return new Promise((resolve) => { onChannel('scripts:update:response', async (msg) => { if (msg.success) { await loadScripts(); if (currentScript && currentScript.id === scriptId) { const updatedScript = scripts.find(s => s.id === scriptId); if (updatedScript) { currentScript = updatedScript; originalScript = JSON.parse(JSON.stringify(updatedScript)); } } resolve(); } }); emitChannel('scripts:update', { scriptId, updates }); }); } /** * Show preview result */ function showPreviewResult(result) { let html = ''; if (result.status === 'success') { html = `
Success
Execution time: ${result.executionTime}ms
${result.result !== undefined ? `
Result:
${JSON.stringify(result.result, null, 2)}
` : ''} ${result.output && result.output.length > 0 ? `

Console Output:

${result.output.map(log => `
${escapeHtml(log.message)}
`).join('')}
` : ''}
`; } else if (result.status === 'error') { html = `
Error
${escapeHtml(result.error)}
${result.stack ? `
Stack:
${escapeHtml(result.stack)}
` : ''}
`; } else if (result.status === 'skipped') { html = `
Skipped
${escapeHtml(result.reason)}
`; } previewContent.innerHTML = html; } /** * Show preview error */ function showPreviewError(message) { previewContent.innerHTML = `
Error
${escapeHtml(message)}
`; } /** * Format timestamp */ function formatTime(timestamp) { const now = Date.now(); const diff = now - timestamp; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ago`; if (hours > 0) return `${hours}h ago`; if (minutes > 0) return `${minutes}m ago`; if (seconds > 0) return `${seconds}s ago`; return 'Just now'; } /** * Escape HTML */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Initialize on load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }