Changeset 0.37.14 (#226)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-23 16:47:48 +00:00
committed by xcaliber
parent fcb998bff9
commit b7746c3004
164 changed files with 6972 additions and 3527 deletions

View File

@@ -18,8 +18,8 @@ export default function CapabilitiesSection() {
sw.api.admin.models.overrides(),
sw.api.admin.models.list().catch(() => []),
]);
setOverrides(Array.isArray(o) ? o : o.data || []);
setModels(Array.isArray(m) ? m : m.models || m.data || []);
setOverrides(o.data || []);
setModels(m || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -1,5 +1,5 @@
/**
* Admin > System > Channels (Archived)
* Admin > System > Channels (Archived + Retention)
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
@@ -7,21 +7,34 @@ const { useState, useEffect, useCallback } = hooks;
export default function ChannelsSection() {
const [channels, setChannels] = useState([]);
const [total, setTotal] = useState(0);
const [retentionMode, setRetentionMode] = useState('');
const [retentionTTL, setRetentionTTL] = useState(0);
const [ttlDraft, setTTLDraft] = useState('');
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
try {
const data = await sw.api.admin.channels.archived();
setChannels(data.channels || data.data || []);
setChannels(data.data || []);
setTotal(data.total || 0);
setRetentionMode(data.retention_mode || '');
const ttl = data.retention_ttl_days || 0;
setRetentionTTL(ttl);
setTTLDraft(String(ttl));
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, []);
async function saveTTL() {
const val = parseInt(ttlDraft, 10);
if (isNaN(val) || val < 0) { sw.toast('TTL must be 0 or more', 'error'); return; }
try {
await sw.api.admin.settings.update('retention_ttl_days', { value: { value: val } });
setRetentionTTL(val);
sw.toast('Retention TTL saved', 'success');
} catch (e) { sw.toast(e.message, 'error'); }
}
async function purge(id, title) {
const ok = await sw.confirm(`Permanently delete "${title || id}"? This cannot be undone.`, true);
if (!ok) return;
@@ -38,9 +51,27 @@ export default function ChannelsSection() {
<div>
<div class="stat-cards-grid" style="margin-bottom:16px;">
<div class="stat-card"><div class="stat-value">${total}</div><div class="stat-label">Archived</div></div>
<div class="stat-card"><div class="stat-value">${retentionMode || '\u2014'}</div><div class="stat-label">Retention Mode</div></div>
<div class="stat-card"><div class="stat-value">${retentionTTL || 'Off'}</div><div class="stat-label">Retention TTL (days)</div></div>
</div>
<div class="admin-section" style="margin-bottom:20px;">
<h4 style="margin:0 0 8px;">Retention Policy</h4>
<p class="text-muted" style="font-size:12px;margin:0 0 8px;">
Channels are archived on delete and purged after this many days.
Only Personal (BYOK) provider channels are exempt. Set to 0 to disable retention.
</p>
<div style="display:flex;gap:8px;align-items:center;">
<input type="number" min="0" value=${ttlDraft}
onInput=${e => setTTLDraft(e.target.value)}
style="width:80px;padding:6px 8px;background:var(--bg-2);color:var(--text);border:1px solid var(--border);border-radius:4px;"
placeholder="0" />
<span class="text-muted" style="font-size:12px;">days</span>
<button class="btn-small" onClick=${saveTTL}
disabled=${String(retentionTTL) === ttlDraft}>Save</button>
</div>
</div>
<h4 style="margin:0 0 8px;">Archived Channels</h4>
<div class="admin-list">
${channels.length === 0 && html`<div class="empty-hint">No archived channels</div>`}
${channels.map(c => html`

View File

@@ -19,7 +19,7 @@ export default function GroupsSection() {
const loadGroups = useCallback(async () => {
try {
const data = await sw.api.admin.groups.list();
setGroups(Array.isArray(data) ? data : data.data || []);
setGroups(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
@@ -41,10 +41,10 @@ export default function GroupsSection() {
sw.api.admin.users.list(),
sw.api.admin.models.list().catch(() => []),
]);
setMembers(Array.isArray(m) ? m : m.data || []);
setMembers(m || []);
setAllPerms(p.permissions || []);
setAllUsers(Array.isArray(u) ? u : u.users || []);
setAllModels(Array.isArray(models) ? models : models.models || models.data || []);
setAllUsers(u || []);
setAllModels(models || []);
} catch (e) { sw.toast(e.message, 'error'); }
}
@@ -102,7 +102,7 @@ export default function GroupsSection() {
sw.toast('Member added', 'success');
setShowAddMember(false);
const m = await sw.api.admin.groups.members(detail.id);
setMembers(Array.isArray(m) ? m : m.data || []);
setMembers(m || []);
} catch (e) { sw.toast(e.message, 'error'); }
}

View File

@@ -12,7 +12,7 @@ export default function HealthSection() {
const load = useCallback(async () => {
try {
const data = await sw.api.admin.providers.health();
setProviders(Array.isArray(data) ? data : data.data || []);
setProviders(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -46,30 +46,32 @@ const CATEGORY_META = {
};
// ── Lazy section imports ────────────────────
// Version query busts browser module cache on upgrades.
const _v = window.__VERSION__ ? `?v=${window.__VERSION__}` : '';
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'),
users: () => import(`./users.js${_v}`),
teams: () => import(`./teams.js${_v}`),
groups: () => import(`./groups.js${_v}`),
providers: () => import(`./providers.js${_v}`),
models: () => import(`./models.js${_v}`),
personas: () => import(`./personas.js${_v}`),
roles: () => import(`./roles.js${_v}`),
knowledgeBases: () => import(`./knowledge.js${_v}`),
memory: () => import(`./memory.js${_v}`),
workflows: () => import(`./workflows.js${_v}`),
tasks: () => import(`./tasks.js${_v}`),
health: () => import(`./health.js${_v}`),
routing: () => import(`./routing.js${_v}`),
capabilities: () => import(`./capabilities.js${_v}`),
settings: () => import(`./settings.js${_v}`),
storage: () => import(`./storage.js${_v}`),
packages: () => import(`./packages.js${_v}`),
channels: () => import(`./channels.js${_v}`),
broadcast: () => import(`./broadcast.js${_v}`),
dashboard: () => import(`./dashboard.js${_v}`),
usage: () => import(`./usage.js${_v}`),
audit: () => import(`./audit.js${_v}`),
stats: () => import(`./stats.js${_v}`),
};
// ── Derive category from section ────────────

View File

@@ -12,8 +12,8 @@ export default function KnowledgeSection() {
const load = useCallback(async () => {
try {
const data = await sw.api.knowledge.list();
setKbs(Array.isArray(data) ? data : data.data || []);
const resp = await sw.api.knowledge.list();
setKbs(resp.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
@@ -24,7 +24,7 @@ export default function KnowledgeSection() {
setDetail(kb);
try {
const d = await sw.api.knowledge.documents(kb.id);
setDocs(Array.isArray(d) ? d : d.data || []);
setDocs(d.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
}
@@ -39,7 +39,7 @@ export default function KnowledgeSection() {
await sw.api.knowledge.upload(detail.id, file);
sw.toast('Document uploaded', 'success');
const d = await sw.api.knowledge.documents(detail.id);
setDocs(Array.isArray(d) ? d : d.data || []);
setDocs(d.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
};
input.click();

View File

@@ -11,7 +11,7 @@ export default function MemorySection() {
const load = useCallback(async () => {
try {
const data = await sw.api.admin.memories.pending();
setPending(Array.isArray(data) ? data : data.data || []);
setPending(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -13,7 +13,7 @@ export default function ModelsSection() {
const load = useCallback(async () => {
try {
const data = await sw.api.admin.models.list();
setModels(Array.isArray(data) ? data : data.models || data.data || []);
setModels(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -1,69 +1,274 @@
/**
* Admin > System > Packages (Extensions)
* 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;
export default function PackagesSection() {
const [extensions, setExtensions] = useState([]);
const [loading, setLoading] = useState(true);
const TYPE_OPTIONS = ['all', 'surface', 'extension', 'full', 'workflow'];
const CORE_IDS = new Set(['chat', 'admin']);
function typeBadge(type) {
const cls = type === 'surface' ? 'badge-active'
: type === 'extension' ? 'badge-pending' : '';
return html`<span class="badge ${cls}">${type || '?'}</span>`;
}
function tierBadge(tier) {
const cls = tier === 'browser' ? 'badge-active' : tier === 'starlark' ? 'badge-pending' : '';
return html`<span class="badge ${cls}">${tier || 'unknown'}</span>`;
}
function sourceBadge(source) {
if (!source || source === 'extension') return null;
const cls = source === 'core' ? 'badge-active' : source === 'registry' ? 'badge-pending' : '';
return html`<span class="badge ${cls}">${source}</span>`;
}
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 BASE = window.__BASE__ || '';
// ── Load packages ───────────────────────────
const load = useCallback(async () => {
try {
const data = await sw.api.admin.extensions.list();
setExtensions(Array.isArray(data) ? data : data.data || []);
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(() => { load(); }, []);
useEffect(() => { setLoading(true); load(); }, [typeFilter]);
async function toggleEnabled(ext) {
// ── Actions ─────────────────────────────────
async function toggleEnabled(pkg) {
if (CORE_IDS.has(pkg.id)) return;
try {
await sw.api.admin.extensions.update(ext.id, { is_enabled: !ext.is_enabled });
sw.toast(`Extension ${ext.is_enabled ? 'disabled' : 'enabled'}`, 'success');
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 deleteExt(id) {
const ok = await sw.confirm('Delete this extension?', true);
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.extensions.del(id);
sw.toast('Extension deleted', 'success');
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 tierBadge(tier) {
const cls = tier === 'browser' ? 'badge-active' : tier === 'starlark' ? 'badge-pending' : '';
return html`<span class="badge ${cls}">${tier || 'unknown'}</span>`;
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'); }
}
// ── 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`<div class="settings-placeholder">Loading\u2026</div>`;
const enabled = packages.filter(p => p.enabled).length;
const hasManifestSettings = (pkg) =>
pkg.manifest?.settings || pkg.package_settings;
return html`
<div>
${/* ── Stat cards ─────────────── */``}
<div class="stat-cards-grid" style="margin-bottom:16px;">
<div class="stat-card"><div class="stat-value">${packages.length}</div><div class="stat-label">Total</div></div>
<div class="stat-card"><div class="stat-value">${enabled}</div><div class="stat-label">Enabled</div></div>
</div>
${/* ── Action bar ─────────────── */``}
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:14px;">
${TYPE_OPTIONS.map(t => html`
<button key=${t}
class="btn-small ${typeFilter === t ? 'btn-primary' : ''}"
onClick=${() => setTypeFilter(t)}
style="text-transform:capitalize;">${t}</button>
`)}
<span style="flex:1;" />
<button class="btn-small" onClick=${uploadPackage} disabled=${installing}>
${installing ? 'Installing\u2026' : 'Install Package'}
</button>
<button class="btn-small" onClick=${toggleRegistry}>
${registryOpen ? 'Close Registry' : 'Browse Registry'}
</button>
</div>
${/* ── Package list ────────────── */``}
<div class="admin-list">
${extensions.length === 0 && html`<div class="empty-hint">No extensions installed</div>`}
${extensions.map(ext => html`
<div class="admin-surface-row" key=${ext.id}>
<div style="flex:1;min-width:0;">
<strong>${ext.name}</strong>
<span class="text-muted" style="margin-left:6px;font-size:11px;">v${ext.version || '?'}</span>
${' '}${tierBadge(ext.tier)}
${ext.is_system && html`<span class="badge" style="margin-left:4px;">system</span>`}
</div>
<div class="admin-actions-cell">
<button class="btn-small" onClick=${() => toggleEnabled(ext)}>
${ext.is_enabled ? 'Disable' : 'Enable'}
</button>
${!ext.is_system && html`<button class="btn-small btn-danger" onClick=${() => deleteExt(ext.id)}>Delete</button>`}
${packages.length === 0 && html`<div class="empty-hint">No packages found</div>`}
${packages.map(pkg => html`
<div key=${pkg.id}>
<div class="admin-surface-row">
<div style="flex:1;min-width:0;">
<strong>${pkg.title || pkg.id}</strong>
<span class="text-muted" style="margin-left:6px;font-size:11px;">v${pkg.version || '?'}</span>
${' '}${typeBadge(pkg.type)}
${' '}${tierBadge(pkg.tier)}
${sourceBadge(pkg.source)}
${pkg.is_system && html`<span class="badge" style="margin-left:4px;">system</span>`}
</div>
<div class="admin-actions-cell">
<button class="btn-small"
onClick=${() => toggleEnabled(pkg)}
disabled=${CORE_IDS.has(pkg.id)}>
${pkg.enabled ? 'Disable' : 'Enable'}
</button>
${hasManifestSettings(pkg) && html`
<button class="btn-small" onClick=${() => toggleSettings(pkg.id)}>
${expandedId === pkg.id ? 'Close' : 'Settings'}
</button>
`}
<button class="btn-small" onClick=${() => exportPkg(pkg.id)}>Export</button>
${pkg.source !== 'core' && !pkg.is_system && html`
<button class="btn-small btn-danger" onClick=${() => deletePkg(pkg)}>Delete</button>
`}
</div>
</div>
${/* ── Inline settings drawer ── */``}
${expandedId === pkg.id && pkgSettings != null && html`
<div style="padding:8px 12px 12px 24px;background:var(--bg-2);border-bottom:1px solid var(--border);">
${Object.keys(pkgSettings).length === 0
? html`<span class="text-muted" style="font-size:12px;">No configurable settings</span>`
: html`
${Object.entries(settingsDraft).map(([k, v]) => html`
<div key=${k} style="display:flex;gap:8px;align-items:center;margin-bottom:6px;">
<label style="min-width:120px;font-size:12px;color:var(--text-muted);">${k}</label>
<input type="text"
value=${v ?? ''}
onInput=${e => setSettingsDraft(prev => ({ ...prev, [k]: e.target.value }))}
style="flex:1;padding:4px 8px;background:var(--bg-1,var(--bg));color:var(--text);border:1px solid var(--border);border-radius:4px;font-size:12px;" />
</div>
`)}
<button class="btn-small" style="margin-top:4px;" onClick=${() => saveSettings(pkg.id)}>Save</button>
`
}
</div>
`}
</div>
`)}
</div>
${/* ── Registry panel ──────────── */``}
${registryOpen && html`
<div class="admin-section" style="margin-top:20px;padding:12px;border:1px solid var(--border);border-radius:6px;">
<h4 style="margin:0 0 8px;">Package Registry</h4>
${registryLoading
? html`<div class="settings-placeholder">Loading registry\u2026</div>`
: registryPkgs.length === 0
? html`<div class="empty-hint">No packages available in registry</div>`
: html`
<div class="admin-list">
${registryPkgs.map(rp => html`
<div class="admin-surface-row" key=${rp.id}>
<div style="flex:1;min-width:0;">
<strong>${rp.title || rp.id}</strong>
<span class="text-muted" style="margin-left:6px;font-size:11px;">v${rp.version || '?'}</span>
${rp.description && html`
<div class="text-muted" style="font-size:11px;margin-top:2px;">${rp.description}</div>
`}
</div>
<div class="admin-actions-cell">
${typeBadge(rp.type)}
<button class="btn-small btn-primary"
onClick=${() => installFromRegistry(rp.download_url)}>Install</button>
</div>
</div>
`)}
</div>
`
}
</div>
`}
</div>
`;
}

View File

@@ -21,9 +21,9 @@ export default function PersonasSection() {
sw.api.admin.models.list().catch(() => []),
sw.api.knowledge.list().catch(() => []),
]);
setPersonas(Array.isArray(p) ? p : p.data || []);
setModels(Array.isArray(m) ? m : m.models || m.data || []);
setKbs(Array.isArray(k) ? k : k.data || []);
setPersonas(p || []);
setModels(m || []);
setKbs(k.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -16,8 +16,8 @@ export default function ProvidersSection() {
sw.api.admin.configs.list(),
sw.api.admin.providers.types(),
]);
setConfigs(Array.isArray(c) ? c : c.configs || c.data || []);
setProviderTypes(t.types || t.data || []);
setConfigs(c || []);
setProviderTypes(t || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
@@ -82,7 +82,7 @@ export default function ProvidersSection() {
</div>
<div class="form-group"><label>Type</label>
<select name="provider">
${providerTypes.map(t => html`<option key=${t.name} value=${t.name} selected=${editing !== 'new' && editing.provider === t.name}>${t.display_name || t.name}</option>`)}
${providerTypes.map(t => html`<option key=${t.id} value=${t.id} selected=${editing !== 'new' && editing.provider === t.id}>${t.display_name || t.name}</option>`)}
</select>
</div>
</div>

View File

@@ -5,7 +5,7 @@ const { html } = window;
const { useState, useEffect, useCallback } = hooks;
export default function RolesSection() {
const [roles, setRoles] = useState([]);
const [roles, setRoles] = useState({});
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true);
const [dirty, setDirty] = useState({});
@@ -16,8 +16,8 @@ export default function RolesSection() {
sw.api.admin.roles.list(),
sw.api.admin.models.list().catch(() => []),
]);
setRoles(r.roles || r.data || (Array.isArray(r) ? r : []));
setModels(Array.isArray(m) ? m : m.models || m.data || []);
setRoles(r || {});
setModels(m || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
@@ -49,10 +49,8 @@ export default function RolesSection() {
return html`
<div>
${roles.length === 0 && html`<div class="empty-hint">No roles configured</div>`}
${roles.map(r => {
const name = typeof r === 'string' ? r : r.name || r.role;
const config = typeof r === 'object' ? r : {};
${Object.keys(roles).length === 0 && html`<div class="empty-hint">No roles configured</div>`}
${Object.entries(roles).map(([name, config]) => {
const changes = dirty[name] || {};
const primary = changes.primary_model ?? config.primary_model ?? '';
const fallback = changes.fallback_model ?? config.fallback_model ?? '';

View File

@@ -17,7 +17,7 @@ export default function RoutingSection() {
const load = useCallback(async () => {
try {
const data = await sw.api.admin.routing.policies();
setPolicies(Array.isArray(data) ? data : data.data || []);
setPolicies(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -18,31 +18,38 @@ export default function SettingsSection() {
sw.api.admin.models.list().catch(() => []),
sw.api.admin.vault.status().catch(() => null),
]);
const settings = s || {};
const raw = s || {};
const pol = raw.policies || {};
const cfg_ = raw.settings || {};
setCfg({
allow_registration: settings.policies?.allow_registration === 'true',
registration_default_state: settings.registration_default_state || 'pending',
system_prompt: settings.system_prompt || '',
default_model: settings.default_model || '',
allow_user_byok: settings.policies?.allow_user_byok === 'true',
allow_user_personas: settings.policies?.allow_user_personas === 'true',
allow_kb_direct_access: settings.policies?.allow_kb_direct_access === 'true',
banner_enabled: !!settings.banner?.enabled,
banner_text: settings.banner?.text || '',
banner_bg: settings.banner?.bg || '#007a33',
banner_fg: settings.banner?.fg || '#ffffff',
banner_position: settings.banner?.position || 'both',
search_provider: settings.search_provider || 'duckduckgo',
search_endpoint: settings.search_endpoint || '',
search_api_key: settings.search_api_key || '',
search_max_results: settings.search_max_results || 5,
compaction_enabled: !!settings.auto_compaction_enabled,
compaction_threshold: settings.compaction_threshold || 70,
compaction_cooldown: settings.compaction_cooldown || 30,
memory_extraction_enabled: !!settings.memory_extraction_enabled,
memory_auto_approve: !!settings.memory_auto_approve,
allow_registration: pol.allow_registration === 'true',
registration_default_state: cfg_.registration_default_state || 'pending',
system_prompt: cfg_.system_prompt || '',
default_model: pol.default_model || '',
allow_user_byok: pol.allow_user_byok === 'true',
allow_user_personas: pol.allow_user_personas === 'true',
allow_kb_direct_access: pol.allow_kb_direct_access === 'true' || pol.kb_direct_access === 'true',
banner_enabled: !!cfg_.banner?.enabled,
banner_text: cfg_.banner?.text || '',
banner_bg: cfg_.banner?.bg || '#007a33',
banner_fg: cfg_.banner?.fg || '#ffffff',
banner_position: cfg_.banner?.position || 'both',
message_enabled: !!cfg_.message?.enabled,
message_text: cfg_.message?.text || '',
message_variant: cfg_.message?.variant || 'info',
footer_enabled: !!cfg_.footer?.enabled,
footer_text: cfg_.footer?.text || '',
search_provider: cfg_.search_provider || 'duckduckgo',
search_endpoint: cfg_.search_endpoint || '',
search_api_key: cfg_.search_api_key || '',
search_max_results: cfg_.search_max_results || 5,
compaction_enabled: !!cfg_.auto_compaction_enabled,
compaction_threshold: cfg_.compaction_threshold || 70,
compaction_cooldown: cfg_.compaction_cooldown || 30,
memory_extraction_enabled: !!cfg_.memory_extraction_enabled,
memory_auto_approve: !!cfg_.memory_auto_approve,
});
setModels(Array.isArray(m) ? m : m.models || m.data || []);
setModels(m || []);
setVault(v);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
@@ -75,6 +82,19 @@ export default function SettingsSection() {
position: cfg.banner_position,
}});
// Message bar
await sw.api.admin.settings.update('message', { value: {
enabled: cfg.message_enabled,
text: cfg.message_text,
variant: cfg.message_variant,
}});
// Footer
await sw.api.admin.settings.update('footer', { value: {
enabled: cfg.footer_enabled,
text: cfg.footer_text,
}});
// Search
await sw.api.admin.settings.update('search_provider', { value: cfg.search_provider });
if (cfg.search_provider === 'searxng') {
@@ -154,6 +174,32 @@ export default function SettingsSection() {
`}
</div>
<div class="settings-section"><h3>Message Bar</h3>
<label class="toggle-label"><input type="checkbox" checked=${cfg.message_enabled} onChange=${e => set('message_enabled', e.target.checked)} /><span class="toggle-track"></span><span>Show dismissible message bar</span></label>
${cfg.message_enabled && html`
<div style="margin-top:8px;">
<div class="form-group"><label>Text</label><input value=${cfg.message_text} onInput=${e => set('message_text', e.target.value)} placeholder="System maintenance scheduled..." /></div>
<div class="form-group"><label>Variant</label>
<select value=${cfg.message_variant} onChange=${e => set('message_variant', e.target.value)}>
<option value="info">Info</option>
<option value="warn">Warning</option>
<option value="error">Error</option>
<option value="success">Success</option>
</select>
</div>
</div>
`}
</div>
<div class="settings-section"><h3>Footer</h3>
<label class="toggle-label"><input type="checkbox" checked=${cfg.footer_enabled} onChange=${e => set('footer_enabled', e.target.checked)} /><span class="toggle-track"></span><span>Show footer bar</span></label>
${cfg.footer_enabled && html`
<div style="margin-top:8px;">
<div class="form-group"><label>Text</label><input value=${cfg.footer_text} onInput=${e => set('footer_text', e.target.value)} placeholder="Powered by Chat Switchboard" /></div>
</div>
`}
</div>
<div class="settings-section"><h3>Web Search</h3>
<div class="form-group"><label>Provider</label>
<select value=${cfg.search_provider} onChange=${e => set('search_provider', e.target.value)}>

View File

@@ -12,7 +12,7 @@ export default function TasksSection() {
const load = useCallback(async () => {
try {
const data = await sw.api.admin.tasks.list();
setTasks(Array.isArray(data) ? data : data.data || []);
setTasks(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
@@ -50,7 +50,7 @@ export default function TasksSection() {
}
try {
const data = await sw.api.tasks.runs(taskId);
const runs = Array.isArray(data) ? data : data.data || [];
const runs = data || [];
setExpandedRuns(prev => ({ ...prev, [taskId]: runs }));
} catch (e) { sw.toast(e.message, 'error'); }
}

View File

@@ -16,7 +16,7 @@ export default function TeamsSection() {
const loadTeams = useCallback(async () => {
try {
const data = await sw.api.admin.teams.list();
setTeams(Array.isArray(data) ? data : data.data || []);
setTeams(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
@@ -30,8 +30,8 @@ export default function TeamsSection() {
sw.api.admin.teams.members(team.id),
sw.api.admin.users.list(),
]);
setMembers(Array.isArray(m) ? m : m.data || []);
const uList = Array.isArray(u) ? u : u.users || [];
setMembers(m || []);
const uList = u || [];
setAllUsers(uList);
} catch (e) { sw.toast(e.message, 'error'); }
}
@@ -66,7 +66,7 @@ export default function TeamsSection() {
sw.toast('Member added', 'success');
setShowAddMember(false);
const m = await sw.api.admin.teams.members(detail.id);
setMembers(Array.isArray(m) ? m : m.data || []);
setMembers(m || []);
} catch (e) { sw.toast(e.message, 'error'); }
}

View File

@@ -16,7 +16,7 @@ export default function UsageSection() {
sw.api.admin.pricing.list().catch(() => []),
]);
setUsage(u);
setPricing(Array.isArray(p) ? p : p.data || []);
setPricing(p || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -4,8 +4,6 @@
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
export default function UsersSection() {
const [users, setUsers] = useState([]);
const [search, setSearch] = useState('');
@@ -14,8 +12,8 @@ export default function UsersSection() {
const load = useCallback(async () => {
try {
const data = await sw.api.admin.users.list();
setUsers(Array.isArray(data) ? data : data.users || []);
const resp = await sw.api.admin.users.list();
setUsers(resp.data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -17,7 +17,7 @@ export default function WorkflowsSection() {
const load = useCallback(async () => {
try {
const data = await sw.api.workflows.list();
setWorkflows(Array.isArray(data) ? data : data.data || []);
setWorkflows(data || []);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);