Changeset 0.37.6 (#218)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-21 16:21:59 +00:00
committed by xcaliber
parent 74f3cb84e9
commit 5f1c733002
30 changed files with 2959 additions and 131 deletions

View File

@@ -0,0 +1,218 @@
/**
* AdminSurface — root admin surface
*
* Reads globals:
* __SECTION__ — active section name (string)
* __BASE__ — base path
*
* 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';
// ── Category → Section mapping ──────────────
const ADMIN_SECTIONS = {
people: ['users', 'teams', 'groups'],
ai: ['providers', 'models', 'personas', 'roles', 'knowledgeBases', 'memory'],
workflows: ['workflows', 'tasks'],
routing: ['health', 'routing', 'capabilities'],
system: ['settings', 'storage', 'packages', 'channels', 'broadcast'],
monitoring: ['dashboard', 'usage', 'audit', 'stats'],
};
const ADMIN_LABELS = {
users: 'Users', teams: 'Teams', groups: 'Groups',
providers: 'Providers', models: 'Models', personas: 'Personas',
roles: 'Roles', knowledgeBases: 'Knowledge', memory: 'Memory',
health: 'Health', routing: 'Routing', capabilities: 'Capabilities',
workflows: 'Workflows', tasks: 'Tasks',
settings: 'Settings', storage: 'Storage', packages: 'Packages',
channels: 'Channels', broadcast: 'Broadcast',
dashboard: 'Dashboard', usage: 'Usage', audit: 'Audit', stats: 'Stats',
};
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' },
ai: { label: 'AI', first: 'providers', icon: 'R4 4 16 16 2|L9 9 9.01 9|L15 9 15.01 9|M8 14s1.5 2 4 2 4-2 4-2' },
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' },
routing: { label: 'Routing', first: 'health', icon: 'G13 2 3 14 12 14 11 22 21 10 12 10 13 2' },
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: 'dashboard', icon: 'L18 20 18 10|L12 20 12 4|L6 20 6 14' },
};
// ── Lazy section imports ────────────────────
const sectionModules = {
users: () => import('./users.js'),
teams: () => import('./teams.js'),
groups: () => import('./groups.js'),
providers: () => import('./providers.js'),
models: () => import('./models.js'),
personas: () => import('./personas.js'),
roles: () => import('./roles.js'),
knowledgeBases: () => import('./knowledge.js'),
memory: () => import('./memory.js'),
workflows: () => import('./workflows.js'),
tasks: () => import('./tasks.js'),
health: () => import('./health.js'),
routing: () => import('./routing.js'),
capabilities: () => import('./capabilities.js'),
settings: () => import('./settings.js'),
storage: () => import('./storage.js'),
packages: () => import('./packages.js'),
channels: () => import('./channels.js'),
broadcast: () => import('./broadcast.js'),
dashboard: () => import('./dashboard.js'),
usage: () => import('./usage.js'),
audit: () => import('./audit.js'),
stats: () => import('./stats.js'),
};
// ── 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]);
// Back button with return URL stash
useEffect(() => {
const RETURN_KEY = 'sb_admin_return';
if (!sessionStorage.getItem(RETURN_KEY)) {
const ref = document.referrer;
if (ref && ref.startsWith(location.origin) && !ref.includes('/admin')) {
sessionStorage.setItem(RETURN_KEY, ref);
} else {
sessionStorage.setItem(RETURN_KEY, location.origin + BASE + '/');
}
}
}, []);
const backClick = useCallback((e) => {
e.preventDefault();
const RETURN_KEY = 'sb_admin_return';
const returnURL = sessionStorage.getItem(RETURN_KEY);
sessionStorage.removeItem(RETURN_KEY);
location.href = returnURL || BASE + '/';
}, []);
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">
<a href="${BASE}/" class="admin-topbar-back" onClick=${backClick}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Back
</a>
<div class="admin-topbar-sep"></div>
<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>
<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 };