Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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'); }
|
||||
}
|
||||
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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 ────────────
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?? '';
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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'); }
|
||||
}
|
||||
|
||||
@@ -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'); }
|
||||
}
|
||||
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
@@ -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); }
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user