/** * Admin > System > Packages * * Full package lifecycle: list, install (upload + registry), enable/disable, * settings, export, delete. Uses /api/v1/admin/packages endpoints. */ const { html } = window; const { useState, useEffect, useCallback } = hooks; import { Dropdown } from '../../primitives/dropdown.js'; const TYPE_OPTIONS = ['all', 'surface', 'extension', 'full', 'workflow', 'library']; const CORE_IDS = new Set(['admin']); function typeBadge(type) { const cls = type === 'surface' ? 'badge-active' : type === 'extension' ? 'badge-pending' : ''; return html`${type || '?'}`; } function tierBadge(tier) { const cls = tier === 'browser' ? 'badge-active' : tier === 'starlark' ? 'badge-pending' : ''; return html`${tier || 'unknown'}`; } function sourceBadge(source) { if (!source || source === 'extension') return null; const cls = source === 'core' ? 'badge-active' : source === 'registry' ? 'badge-pending' : ''; return html`${source}`; } function statusBadge(status) { if (status === 'dormant') return html`dormant`; if (status === 'pending_review') return html`review`; if (status === 'suspended') return html`suspended`; return null; } export default function PackagesSection() { const [packages, setPackages] = useState([]); const [loading, setLoading] = useState(true); const [typeFilter, setTypeFilter] = useState('all'); const [expandedId, setExpandedId] = useState(null); const [pkgSettings, setPkgSettings] = useState(null); const [settingsDraft, setSettingsDraft] = useState({}); const [registryOpen, setRegistryOpen] = useState(false); const [registryPkgs, setRegistryPkgs] = useState([]); const [registryLoading, setRegistryLoading] = useState(false); const [installing, setInstalling] = useState(false); const [permsId, setPermsId] = useState(null); // v0.5.0: permissions drawer const [perms, setPerms] = useState([]); const BASE = window.__BASE__ || ''; // ── Load packages ─────────────────────────── const load = useCallback(async () => { try { const opts = typeFilter !== 'all' ? { type: typeFilter } : undefined; const data = await sw.api.admin.packages.list(opts); setPackages(data || []); } catch (e) { sw.toast(e.message, 'error'); } finally { setLoading(false); } }, [typeFilter]); useEffect(() => { setLoading(true); load(); }, [typeFilter]); // ── Actions ───────────────────────────────── async function toggleEnabled(pkg) { if (CORE_IDS.has(pkg.id)) return; if (pkg.status === 'dormant') { sw.toast('This package requires chat — it cannot be enabled yet', 'error'); return; } try { if (pkg.enabled) { await sw.api.admin.packages.disable(pkg.id); sw.toast(`${pkg.title || pkg.id} disabled`, 'success'); } else { await sw.api.admin.packages.enable(pkg.id); sw.toast(`${pkg.title || pkg.id} enabled`, 'success'); } load(); } catch (e) { sw.toast(e.message, 'error'); } } async function deletePkg(pkg) { if (pkg.source === 'core') return; const ok = await sw.confirm(`Delete "${pkg.title || pkg.id}"? This cannot be undone.`, true); if (!ok) return; try { await sw.api.admin.packages.del(pkg.id); sw.toast('Package deleted', 'success'); if (expandedId === pkg.id) { setExpandedId(null); setPkgSettings(null); } load(); } catch (e) { sw.toast(e.message, 'error'); } } function uploadPackage() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.pkg,.surface,.zip'; input.onchange = async () => { const file = input.files[0]; if (!file) return; setInstalling(true); try { await sw.api.admin.packages.install(file); sw.toast('Package installed', 'success'); load(); } catch (e) { sw.toast(e.message, 'error'); } finally { setInstalling(false); } }; input.click(); } function exportPkg(id) { window.open(`${BASE}/api/v1/admin/packages/${id}/export`, '_blank'); } // ── Settings drawer ───────────────────────── async function toggleSettings(pkgId) { if (expandedId === pkgId) { setExpandedId(null); setPkgSettings(null); setSettingsDraft({}); return; } try { const data = await sw.api.admin.packages.settings(pkgId); const settings = data?.data || {}; setPkgSettings(settings); setSettingsDraft({ ...settings }); setExpandedId(pkgId); } catch (e) { sw.toast(e.message, 'error'); } } async function saveSettings(pkgId) { try { await sw.api.admin.packages.updateSettings(pkgId, settingsDraft); sw.toast('Settings saved', 'success'); setPkgSettings({ ...settingsDraft }); } catch (e) { sw.toast(e.message, 'error'); } } // ── Permissions drawer (v0.5.0) ──────────── async function togglePerms(pkgId) { if (permsId === pkgId) { setPermsId(null); setPerms([]); return; } try { const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); setPermsId(pkgId); // Close settings if open if (expandedId === pkgId) { setExpandedId(null); setPkgSettings(null); } } catch (e) { sw.toast(e.message, 'error'); } } async function grantPerm(pkgId, perm) { try { await sw.api.admin.packages.grantPerm(pkgId, perm); const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); load(); } catch (e) { sw.toast(e.message, 'error'); } } async function revokePerm(pkgId, perm) { try { await sw.api.admin.packages.revokePerm(pkgId, perm); const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); load(); } catch (e) { sw.toast(e.message, 'error'); } } async function grantAllPerms(pkgId) { try { await sw.api.admin.packages.grantAllPerms(pkgId); const data = await sw.api.admin.packages.permissions(pkgId); setPerms((data && data.data) || data || []); sw.toast('All permissions granted', 'success'); load(); } catch (e) { sw.toast(e.message, 'error'); } } // ── Registry ──────────────────────────────── async function toggleRegistry() { if (registryOpen) { setRegistryOpen(false); return; } setRegistryOpen(true); setRegistryLoading(true); try { const data = await sw.api.admin.packages.registry(); setRegistryPkgs(data?.data || []); } catch (e) { sw.toast('Registry unavailable: ' + e.message, 'error'); setRegistryPkgs([]); } finally { setRegistryLoading(false); } } async function installFromRegistry(url) { try { await sw.api.admin.packages.registryInstall(url); sw.toast('Package installed from registry', 'success'); load(); } catch (e) { sw.toast(e.message, 'error'); } } // ── Render ─────────────────────────────────── if (loading) return html`
${p.permission}
${p.granted && p.granted_by && html`
by ${p.granted_by}
`}