// static/js/addons.js - Addon manager for manifest-based Odysseus addons import uiModule from './ui.js'; let initialized = false; let currentManifest = null; function el(id) { return document.getElementById(id); } function esc(value) { return uiModule && typeof uiModule.esc === 'function' ? uiModule.esc(String(value ?? '')) : String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } async function api(path, options = {}) { const res = await fetch(path, { credentials: 'same-origin', headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, ...options, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`); return data; } function contributionSummary(addon) { const items = Array.isArray(addon.contributions) ? addon.contributions : []; if (!items.length) return 'No contributions'; const counts = new Map(); items.forEach(item => counts.set(item.type || 'unknown', (counts.get(item.type || 'unknown') || 0) + 1)); return Array.from(counts.entries()) .map(([type, count]) => `${esc(type)}${count > 1 ? ` x${count}` : ''}`) .join(''); } function renderAddons(addons) { const list = el('addons-list'); if (!list) return; if (!addons.length) { list.innerHTML = '
No addons installed yet.
'; return; } list.innerHTML = addons.map(addon => `
${esc(addon.name || addon.id)} ${esc(addon.version || '0.1.0')}
${esc(addon.description || 'No description')}
${esc(addon.id)} ${addon.author ? `${esc(addon.author)}` : ''} ${addon.error ? `${esc(addon.error)}` : ''}
${contributionSummary(addon)}
`).join(''); } function setMessage(message, kind = '') { const node = el('addons-msg'); if (!node) return; node.textContent = message || ''; node.className = kind ? `addon-msg ${kind}` : 'addon-msg'; } function setEditorManifest(manifest) { currentManifest = manifest || null; const editor = el('addon-manifest-editor'); if (!editor) return; editor.value = JSON.stringify(manifest || {}, null, 2); } async function loadAddons() { const list = el('addons-list'); if (list) list.innerHTML = '
Loading addons...
'; try { const data = await api('/api/addons'); renderAddons(Array.isArray(data.addons) ? data.addons : []); setMessage(''); } catch (err) { if (list) list.innerHTML = `
${esc(err.message)}
`; } } async function loadTemplate() { const idInput = el('addon-template-id'); const addonId = (idInput && idInput.value.trim()) || 'my-addon'; const data = await api(`/api/addons/template?addon_id=${encodeURIComponent(addonId)}`); setEditorManifest(data.manifest); setMessage('Template loaded. Edit the manifest and save it.', 'ok'); } async function saveManifest() { const editor = el('addon-manifest-editor'); if (!editor) return; let manifest; try { manifest = JSON.parse(editor.value || '{}'); } catch (err) { setMessage(`Invalid JSON: ${err.message}`, 'error'); return; } try { const data = await api('/api/addons', { method: 'POST', body: JSON.stringify({ manifest }), }); setEditorManifest(data.addon); setMessage('Addon saved.', 'ok'); await loadAddons(); } catch (err) { setMessage(err.message, 'error'); } } async function editAddon(addonId) { try { const data = await api(`/api/addons/${encodeURIComponent(addonId)}`); const addon = { ...(data.addon || {}) }; delete addon.path; setEditorManifest(addon); setMessage(`Editing ${addon.id}.`, 'ok'); } catch (err) { setMessage(err.message, 'error'); } } async function toggleAddon(addonId, enabled) { try { await api(`/api/addons/${encodeURIComponent(addonId)}/enabled`, { method: 'POST', body: JSON.stringify({ enabled }), }); setMessage(enabled ? 'Addon enabled.' : 'Addon disabled.', 'ok'); await loadAddons(); } catch (err) { setMessage(err.message, 'error'); } } async function deleteAddon(addonId) { if (!confirm(`Delete addon "${addonId}"?`)) return; try { await api(`/api/addons/${encodeURIComponent(addonId)}`, { method: 'DELETE' }); if (currentManifest && currentManifest.id === addonId) setEditorManifest(null); setMessage('Addon deleted.', 'ok'); await loadAddons(); } catch (err) { setMessage(err.message, 'error'); } } function bindEvents() { const refresh = el('addons-refresh-btn'); if (refresh) refresh.addEventListener('click', loadAddons); const template = el('addon-template-btn'); if (template) template.addEventListener('click', () => loadTemplate().catch(err => setMessage(err.message, 'error'))); const save = el('addon-save-btn'); if (save) save.addEventListener('click', saveManifest); const clear = el('addon-clear-btn'); if (clear) clear.addEventListener('click', () => { setEditorManifest(null); setMessage(''); }); const list = el('addons-list'); if (list) { list.addEventListener('click', (event) => { const edit = event.target.closest('.addon-edit-btn'); const toggle = event.target.closest('.addon-toggle-btn'); const del = event.target.closest('.addon-delete-btn'); if (edit) editAddon(edit.dataset.addonId); if (toggle) toggleAddon(toggle.dataset.addonId, toggle.dataset.enabled === '1'); if (del) deleteAddon(del.dataset.addonId); }); } } export function init() { if (initialized) return; initialized = true; bindEvents(); } export function open() { init(); loadAddons(); } export default { init, open };