This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/surfaces/admin/index.js
Jeffrey Smith 31548ce801 chore: strip remaining pre-fork version references
Second pass — removes chat-switchboard version numbers from config
field docs, section headers, file headers, and test comments. These
were pre-fork version tags that don't apply to switchboard-core.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:22:27 +00:00

200 lines
8.2 KiB
JavaScript

/**
* 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`<circle cx=${cx} cy=${cy} r=${r} key=${i} />`; }
if (t === 'L') { const [x1, y1, x2, y2] = d.split(' '); return html`<line x1=${x1} y1=${y1} x2=${x2} y2=${y2} key=${i} />`; }
if (t === 'P') { return html`<polyline points=${d} key=${i} />`; }
if (t === 'R') { const [x, y, w, h, rx] = d.split(' '); return html`<rect x=${x} y=${y} width=${w} height=${h} rx=${rx} key=${i} />`; }
if (t === 'G') { return html`<polygon points=${d} key=${i} />`; }
if (t === 'M') { return html`<path d=${d} key=${i} />`; }
return html`<path d=${p} key=${i} />`;
});
return html`<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${els}</svg>`;
}
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`
<div class="surface-admin">
${/* Top Bar */``}
<div class="admin-topbar">
<span class="admin-topbar-title">
<img src="${BASE}/favicon.svg" alt="" width="18" height="18" style="vertical-align:-3px;" />
Administration
</span>
<div class="admin-category-tabs">
${Object.entries(CATEGORY_META).map(([cat, meta]) => html`
<a key=${cat} href="${BASE}/admin/${meta.first}"
class="admin-cat-btn ${activeCat === cat ? 'active' : ''}"
onClick=${navClick}>
<${CatIcon} paths=${meta.icon} />
${meta.label}
</a>
`)}
</div>
<div style="margin-left:auto;">
<${UserMenu} placement="down-right" />
</div>
</div>
<div class="admin-body">
${/* Left Nav */``}
<div class="admin-nav">
${sidebarSections.map(key => html`
<a key=${key} href="${BASE}/admin/${key}"
class="admin-nav-link ${section === key ? 'active' : ''}"
onClick=${navClick}>
${ADMIN_LABELS[key] || key}
</a>
`)}
</div>
${/* Content */``}
<div class="admin-content">
<div class="admin-content-header">
<h2>${title}</h2>
</div>
<div class="admin-content-body" id="adminMain">
${SectionComponent
? html`<${SectionComponent} />`
: html`<div class="settings-placeholder">Loading\u2026</div>`
}
</div>
</div>
</div>
</div>
<${ToastContainer} />
<${DialogStack} />
`;
}
// ── Mount ────────────────────────────────────
const mount = document.getElementById('admin-mount');
if (mount) {
render(html`<${AdminSurface} />`, mount);
console.log('[admin] Surface mounted');
}
export { AdminSurface };