/** * AdminSurface — root admin surface * * Reads globals: * __SECTION__ — active section name (string) * __BASE__ — base path * __CONFIG_SECTIONS__ * * Layout: topbar (back + category tabs) + body (sidebar nav + content area). * All 24+ sections are native Preact components loaded lazily. */ const { html } = window; const { useState, useEffect, useCallback, useMemo } = hooks; const { render } = preact; import { ToastContainer } from '../../primitives/toast.js'; import { DialogStack } from '../../shell/dialog-stack.js'; import { UserMenu } from '../../shell/user-menu.js'; // ── Category → Section mapping ────────────── const ADMIN_SECTIONS = { people: ['users', 'teams', 'groups'], workflows: ['workflows'], system: ['settings', 'storage', 'packages', 'connections', 'broadcast', 'backup'], monitoring: ['audit'], }; const ADMIN_LABELS = { users: 'Users', teams: 'Teams', groups: 'Groups', workflows: 'Workflows', settings: 'Settings', storage: 'Storage', packages: 'Packages', connections: 'Connections', broadcast: 'Broadcast', backup: 'Backup', audit: 'Audit', }; // ── Extension config sections ────── // Packages declare config_section in their manifest targeting "admin". // We merge them into the appropriate category and section module map. const _configSections = window.__CONFIG_SECTIONS__ || []; const _base = window.__BASE__ || ''; for (const cs of _configSections) { const pkgId = cs.package_id; const cat = cs.category || 'system'; // Add to category (create if new — unlikely but safe) if (!ADMIN_SECTIONS[cat]) ADMIN_SECTIONS[cat] = []; if (!ADMIN_SECTIONS[cat].includes(pkgId)) ADMIN_SECTIONS[cat].push(pkgId); ADMIN_LABELS[pkgId] = cs.label; } const CATEGORY_META = { people: { label: 'People', first: 'users', icon: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|C9 7 4' }, workflows: { label: 'Workflows', first: 'workflows', icon: 'P16 3 21 3 21 8|L4 20 21 3|P21 16 21 21 16 21|L15 15 21 21|L4 4 9 9' }, system: { label: 'System', first: 'settings', icon: 'C12 12 3|M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09' }, monitoring: { label: 'Monitoring', first: 'audit', icon: 'L18 20 18 10|L12 20 12 4|L6 20 6 14' }, }; // ── Lazy section imports ──────────────────── // Version query busts browser module cache on upgrades. const _v = window.__VERSION__ ? `?v=${window.__VERSION__}` : ''; const sectionModules = { users: () => import(`./users.js${_v}`), teams: () => import(`./teams.js${_v}`), groups: () => import(`./groups.js${_v}`), workflows: () => import(`./workflows.js${_v}`), settings: () => import(`./settings.js${_v}`), storage: () => import(`./storage.js${_v}`), packages: () => import(`./packages.js${_v}`), connections: () => import(`./connections.js${_v}`), broadcast: () => import(`./broadcast.js${_v}`), backup: () => import(`./backup.js${_v}`), audit: () => import(`./audit.js${_v}`), }; for (const cs of _configSections) { const pkgId = cs.package_id; const component = cs.component || 'js/config.js'; sectionModules[pkgId] = () => import(`${_base}/surfaces/${pkgId}/${component}`); } // ── Derive category from section ──────────── function catForSection(section) { for (const [cat, sections] of Object.entries(ADMIN_SECTIONS)) { if (sections.includes(section)) return cat; } return 'people'; } // ── Simple SVG icon renderer ──────────────── function CatIcon({ paths }) { // paths is a compact format: pipe-separated draw commands // C = circle, L = line, P = polyline, R = rect, G = polygon, M = path const els = paths.split('|').map((p, i) => { const t = p[0], d = p.slice(1).trim(); if (t === 'C') { const [cx, cy, r] = d.split(' '); return html``; } if (t === 'L') { const [x1, y1, x2, y2] = d.split(' '); return html``; } if (t === 'P') { return html``; } if (t === 'R') { const [x, y, w, h, rx] = d.split(' '); return html``; } if (t === 'G') { return html``; } if (t === 'M') { return html``; } return html``; }); return html`${els}`; } function AdminSurface() { const BASE = window.__BASE__ || ''; const section = window.__SECTION__ || 'users'; const activeCat = catForSection(section); const [SectionComponent, setSectionComponent] = useState(null); // Load section component useEffect(() => { setSectionComponent(null); const loader = sectionModules[section]; if (loader) { loader().then(mod => { const Comp = mod.default || Object.values(mod)[0]; setSectionComponent(() => Comp); }).catch(e => console.error('[admin] Failed to load section:', section, e)); } }, [section]); const navClick = useCallback((e) => { e.preventDefault(); const href = e.currentTarget.getAttribute('href'); if (href) location.replace(href); }, []); const title = ADMIN_LABELS[section] || 'Admin'; const sidebarSections = ADMIN_SECTIONS[activeCat] || []; return html`
${/* Top Bar */``}
Administration
${Object.entries(CATEGORY_META).map(([cat, meta]) => html` <${CatIcon} paths=${meta.icon} /> ${meta.label} `)}
<${UserMenu} placement="down-right" />
${/* Left Nav */``}
${sidebarSections.map(key => html` ${ADMIN_LABELS[key] || key} `)}
${/* Content */``}

${title}

${SectionComponent ? html`<${SectionComponent} />` : html`
Loading\u2026
` }
<${ToastContainer} /> <${DialogStack} /> `; } // ── Mount ──────────────────────────────────── const mount = document.getElementById('admin-mount'); if (mount) { render(html`<${AdminSurface} />`, mount); console.log('[admin] Surface mounted'); } export { AdminSurface };