Changeset 0.38.1 (#234)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-25 11:13:12 +00:00
committed by xcaliber
parent 10acadc9d0
commit 6943c91f40
30 changed files with 2410 additions and 10 deletions

View File

@@ -188,6 +188,12 @@ export function createDomains(restClient) {
fetchModels: (id) => rc.post(`/api/v1/api-configs/${id}/models/fetch`),
},
// ── 10b. Connections (v0.38.1) ────────
connections: {
...crud(rc, '/api/v1/connections'),
resolve: (type, name) => rc.get('/api/v1/connections/resolve' + _qs({ type, name })),
},
// ── 11. Notifications ──────────────────
notifications: {
list: (opts) => rc.get('/api/v1/notifications' + _qs(opts)),
@@ -241,6 +247,11 @@ export function createDomains(restClient) {
updateProvider:(id, pid, data) => rc.put(`/api/v1/teams/${id}/providers/${pid}`, data),
deleteProvider:(id, pid) => rc.del(`/api/v1/teams/${id}/providers/${pid}`),
providerModels:(id, pid) => rc.get(`/api/v1/teams/${id}/providers/${pid}/models`),
// Team connections (v0.38.1)
connections: (id) => rc.get(`/api/v1/teams/${id}/connections`),
createConnection: (id, data) => rc.post(`/api/v1/teams/${id}/connections`, data),
updateConnection: (id, cid, data) => rc.put(`/api/v1/teams/${id}/connections/${cid}`, data),
deleteConnection: (id, cid) => rc.del(`/api/v1/teams/${id}/connections/${cid}`),
roles: (id) => rc.get(`/api/v1/teams/${id}/roles`),
updateRole: (id, role, config) => rc.put(`/api/v1/teams/${id}/roles/${role}`, config),
deleteRole: (id, role) => rc.del(`/api/v1/teams/${id}/roles/${role}`),
@@ -477,6 +488,9 @@ export function createDomains(restClient) {
del: (id) => rc.del(`/api/v1/admin/extensions/${id}`),
},
// v0.38.1: Global connections
connections: crud(rc, '/api/v1/admin/connections'),
packages: {
list: (opts) => rc.get('/api/v1/admin/packages' + _qs(opts)),
get: (id) => rc.get(`/api/v1/admin/packages/${id}`),

View File

@@ -152,7 +152,7 @@ export async function boot() {
};
// Marker for idempotency
sw._sdk = '0.38.0';
sw._sdk = '0.38.1';
// 8. Expose globally
window.sw = sw;

View File

@@ -0,0 +1,219 @@
/**
* Admin > Connections — global extension connection CRUD
* v0.38.1: Scoped credential management for extensions
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
function esc(s) { return s == null ? '' : String(s); }
export default function ConnectionsSection() {
const [connections, setConnections] = useState(null);
const [connTypes, setConnTypes] = useState([]);
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState(null);
const [form, setForm] = useState({ type: '', package_id: '', name: '', config: {} });
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
try {
const resp = await sw.api.admin.connections.list();
setConnections(resp || []);
} catch (e) {
setConnections([]);
}
}, []);
const loadTypes = useCallback(async () => {
try {
const pkgs = await sw.api.admin.packages.list();
const types = [];
const seen = {};
for (const pkg of (pkgs || [])) {
const conns = pkg.manifest?.connections || [];
for (const cd of conns) {
if (!seen[cd.type]) {
seen[cd.type] = true;
types.push({ type: cd.type, label: cd.label || cd.type, packageId: pkg.id, fields: cd.fields || {} });
}
}
}
setConnTypes(types);
} catch (_) {}
}, []);
useEffect(() => { load(); loadTypes(); }, [load, loadTypes]);
const selectedType = connTypes.find(t => t.type === form.type);
const openAdd = useCallback(() => {
setEditId(null);
setForm({ type: connTypes[0]?.type || '', package_id: connTypes[0]?.packageId || '', name: '', config: {} });
setShowForm(true);
}, [connTypes]);
const openEdit = useCallback((conn) => {
setEditId(conn.id);
setForm({ type: conn.type, package_id: conn.package_id, name: conn.name, config: {} });
setShowForm(true);
}, []);
const cancel = useCallback(() => {
setShowForm(false);
setEditId(null);
}, []);
const save = useCallback(async () => {
if (!form.type || !form.name) {
sw.emit('toast', { message: 'Type and name are required', variant: 'error' });
return;
}
setSaving(true);
try {
if (editId) {
const patch = { name: form.name };
if (Object.keys(form.config).length > 0) patch.config = form.config;
await sw.api.admin.connections.update(editId, patch);
sw.emit('toast', { message: 'Connection updated', variant: 'success' });
} else {
await sw.api.admin.connections.create({
type: form.type, package_id: form.package_id,
name: form.name, config: form.config,
});
sw.emit('toast', { message: 'Connection created', variant: 'success' });
}
setShowForm(false);
setEditId(null);
load();
} catch (e) {
sw.emit('toast', { message: e.message, variant: 'error' });
} finally {
setSaving(false);
}
}, [form, editId, load]);
const del = useCallback(async (conn) => {
const ok = await sw.confirm(`Remove connection "${conn.name}"?`);
if (!ok) return;
try {
await sw.api.admin.connections.del(conn.id);
sw.emit('toast', { message: 'Connection removed', variant: 'success' });
load();
} catch (e) {
sw.emit('toast', { message: e.message, variant: 'error' });
}
}, [load]);
const setField = useCallback((key, val) => {
setForm(f => ({ ...f, [key]: val }));
}, []);
const setConfigField = useCallback((key, val) => {
setForm(f => ({ ...f, config: { ...f.config, [key]: val } }));
}, []);
const onTypeChange = useCallback((typeVal) => {
const ct = connTypes.find(t => t.type === typeVal);
setForm(f => ({ ...f, type: typeVal, package_id: ct?.packageId || f.package_id, config: {} }));
}, [connTypes]);
if (connections === null) {
return html`<div class="admin-placeholder">Loading connections\u2026</div>`;
}
return html`
<div style="margin-bottom:12px;display:flex;align-items:center;gap:8px;">
<button class="btn-md btn-primary" onClick=${openAdd}
disabled=${!connTypes.length}>+ Add Connection</button>
${!connTypes.length && html`
<span style="color:var(--text-3);font-size:12px;">
No installed packages declare connection types.
</span>
`}
</div>
${showForm && html`
<div class="admin-card" style="margin-bottom:16px;padding:16px;">
<h3 style="margin-top:0;">${editId ? 'Edit Connection' : 'Add Connection'}</h3>
${!editId && html`
<div class="form-group">
<label>Connection Type</label>
<select value=${form.type}
onChange=${e => onTypeChange(e.target.value)}>
${connTypes.map(ct => html`
<option value=${ct.type}>${esc(ct.label)}</option>
`)}
</select>
</div>
`}
<div class="form-group">
<label>Name</label>
<input type="text" placeholder="e.g. Production Gitea"
value=${form.name}
onInput=${e => setField('name', e.target.value)} />
</div>
${selectedType && Object.entries(selectedType.fields || {}).map(([fk, fd]) => html`
<div class="form-group" key=${fk}>
<label>${esc(fd.label || fk)}${fd.required ? ' *' : ''}${editId && fd.type === 'secret' ? ' (leave blank to keep)' : ''}</label>
${fd.type === 'secret' ? html`
<input type="password" placeholder="\u2022\u2022\u2022\u2022"
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
` : fd.type === 'url' ? html`
<input type="url" placeholder="https://..."
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
` : fd.type === 'boolean' ? html`
<input type="checkbox" checked=${!!form.config[fk]}
onChange=${e => setConfigField(fk, e.target.checked)} />
` : html`
<input type="text"
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
`}
</div>
`)}
<div style="display:flex;gap:8px;margin-top:12px;">
<button class="btn-md btn-primary" disabled=${saving}
onClick=${save}>
${saving ? 'Saving\u2026' : editId ? 'Update' : 'Add'}
</button>
<button class="btn-md btn-secondary" onClick=${cancel}>Cancel</button>
</div>
</div>
`}
${!connections.length && !showForm && html`
<div class="empty-hint">No global connections configured.</div>
`}
${connections.length > 0 && html`
<table class="admin-table">
<thead><tr><th>Name</th><th>Type</th><th>Package</th><th>Status</th><th></th></tr></thead>
<tbody>
${connections.map(c => html`
<tr>
<td style="font-weight:500;font-size:13px;">${esc(c.name)}</td>
<td style="font-size:12px;color:var(--text-2);">${esc(c.type)}</td>
<td style="font-size:12px;color:var(--text-3);">${esc(c.package_id)}</td>
<td style="font-size:12px;">
${c.is_active
? html`<span style="color:var(--green);">\u25CF Active</span>`
: html`<span style="color:var(--text-3);">\u25CB Inactive</span>`}
</td>
<td class="admin-actions-cell" style="white-space:nowrap;">
<button class="icon-btn" title="Edit" onClick=${() => openEdit(c)}>
\u{270F}\u{FE0F}
</button>
<button class="icon-btn icon-btn-danger" title="Delete"
onClick=${() => del(c)}>
\u{1F5D1}
</button>
</td>
</tr>
`)}
</tbody>
</table>
`}
`;
}

View File

@@ -21,7 +21,7 @@ const ADMIN_SECTIONS = {
ai: ['providers', 'models', 'personas', 'roles', 'knowledgeBases', 'memory'],
workflows: ['workflows', 'tasks'],
routing: ['health', 'routing', 'capabilities'],
system: ['settings', 'storage', 'packages', 'channels', 'broadcast'],
system: ['settings', 'storage', 'packages', 'connections', 'channels', 'broadcast'],
monitoring: ['dashboard', 'usage', 'audit', 'stats'],
};
@@ -32,6 +32,7 @@ const ADMIN_LABELS = {
health: 'Health', routing: 'Routing', capabilities: 'Capabilities',
workflows: 'Workflows', tasks: 'Tasks',
settings: 'Settings', storage: 'Storage', packages: 'Packages',
connections: 'Connections',
channels: 'Channels', broadcast: 'Broadcast',
dashboard: 'Dashboard', usage: 'Usage', audit: 'Audit', stats: 'Stats',
};
@@ -66,6 +67,7 @@ const sectionModules = {
settings: () => import(`./settings.js${_v}`),
storage: () => import(`./storage.js${_v}`),
packages: () => import(`./packages.js${_v}`),
connections: () => import(`./connections.js${_v}`),
channels: () => import(`./channels.js${_v}`),
broadcast: () => import(`./broadcast.js${_v}`),
dashboard: () => import(`./dashboard.js${_v}`),

View File

@@ -0,0 +1,222 @@
/**
* ConnectionsSection — personal extension connection CRUD
* v0.38.1: Scoped credential management for extensions
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
function esc(s) { return s == null ? '' : String(s); }
export function ConnectionsSection() {
const [connections, setConnections] = useState(null);
const [connTypes, setConnTypes] = useState([]);
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState(null);
const [form, setForm] = useState({ type: '', package_id: '', name: '', config: {} });
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
try {
const resp = await sw.api.connections.list();
setConnections(resp || []);
} catch (e) {
setConnections([]);
}
}, []);
const loadTypes = useCallback(async () => {
try {
const pkgs = await sw.api.admin.packages.list();
const types = [];
const seen = {};
for (const pkg of (pkgs || [])) {
const conns = pkg.manifest?.connections || [];
for (const cd of conns) {
if (!seen[cd.type]) {
seen[cd.type] = true;
types.push({ type: cd.type, label: cd.label || cd.type, packageId: pkg.id, fields: cd.fields || {} });
}
}
}
setConnTypes(types);
} catch (_) {}
}, []);
useEffect(() => { load(); loadTypes(); }, [load, loadTypes]);
const selectedType = connTypes.find(t => t.type === form.type);
const openAdd = useCallback(() => {
setEditId(null);
setForm({ type: connTypes[0]?.type || '', package_id: connTypes[0]?.packageId || '', name: '', config: {} });
setShowForm(true);
}, [connTypes]);
const openEdit = useCallback((conn) => {
setEditId(conn.id);
setForm({ type: conn.type, package_id: conn.package_id, name: conn.name, config: {} });
setShowForm(true);
}, []);
const cancel = useCallback(() => {
setShowForm(false);
setEditId(null);
}, []);
const save = useCallback(async () => {
if (!form.type || !form.name) {
sw.emit('toast', { message: 'Type and name are required', variant: 'error' });
return;
}
setSaving(true);
try {
if (editId) {
const patch = { name: form.name };
if (Object.keys(form.config).length > 0) patch.config = form.config;
await sw.api.connections.update(editId, patch);
sw.emit('toast', { message: 'Connection updated', variant: 'success' });
} else {
await sw.api.connections.create({
type: form.type, package_id: form.package_id,
name: form.name, config: form.config,
});
sw.emit('toast', { message: 'Connection created', variant: 'success' });
}
setShowForm(false);
setEditId(null);
load();
} catch (e) {
sw.emit('toast', { message: e.message, variant: 'error' });
} finally {
setSaving(false);
}
}, [form, editId, load]);
const del = useCallback(async (conn) => {
const ok = await sw.confirm(`Remove connection "${conn.name}"?`);
if (!ok) return;
try {
await sw.api.connections.del(conn.id);
sw.emit('toast', { message: 'Connection removed', variant: 'success' });
load();
} catch (e) {
sw.emit('toast', { message: e.message, variant: 'error' });
}
}, [load]);
const setField = useCallback((key, val) => {
setForm(f => ({ ...f, [key]: val }));
}, []);
const setConfigField = useCallback((key, val) => {
setForm(f => ({ ...f, config: { ...f.config, [key]: val } }));
}, []);
const onTypeChange = useCallback((typeVal) => {
const ct = connTypes.find(t => t.type === typeVal);
setForm(f => ({ ...f, type: typeVal, package_id: ct?.packageId || f.package_id, config: {} }));
}, [connTypes]);
if (connections === null) {
return html`<div class="settings-placeholder">Loading connections\u2026</div>`;
}
return html`
<div style="margin-bottom:12px;">
<button class="btn-md btn-primary" onClick=${openAdd}
disabled=${!connTypes.length}>+ Add Connection</button>
${!connTypes.length && html`
<span style="margin-left:8px;color:var(--text-3);font-size:12px;">
No installed packages declare connection types.
</span>
`}
</div>
${showForm && html`
<div class="settings-section" style="margin-bottom:16px;">
<h3>${editId ? 'Edit Connection' : 'Add Connection'}</h3>
${!editId && html`
<div class="form-group">
<label>Connection Type</label>
<select value=${form.type}
onChange=${e => onTypeChange(e.target.value)}>
${connTypes.map(ct => html`
<option value=${ct.type}>${esc(ct.label)}</option>
`)}
</select>
</div>
`}
<div class="form-group">
<label>Name</label>
<input type="text" placeholder="e.g. Work Gitea"
value=${form.name}
onInput=${e => setField('name', e.target.value)} />
</div>
${selectedType && Object.entries(selectedType.fields || {}).map(([fk, fd]) => html`
<div class="form-group" key=${fk}>
<label>${esc(fd.label || fk)}${fd.required ? ' *' : ''}${editId && fd.type === 'secret' ? ' (leave blank to keep current)' : ''}</label>
${fd.type === 'secret' ? html`
<input type="password" placeholder="\u2022\u2022\u2022\u2022"
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
` : fd.type === 'url' ? html`
<input type="url" placeholder="https://..."
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
` : fd.type === 'boolean' ? html`
<input type="checkbox" checked=${!!form.config[fk]}
onChange=${e => setConfigField(fk, e.target.checked)} />
` : fd.type === 'number' ? html`
<input type="number"
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
` : html`
<input type="text"
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
`}
</div>
`)}
<div style="display:flex;gap:8px;margin-top:12px;">
<button class="btn-md btn-primary" disabled=${saving}
onClick=${save}>
${saving ? 'Saving\u2026' : editId ? 'Update' : 'Add'}
</button>
<button class="btn-md btn-secondary" onClick=${cancel}>Cancel</button>
</div>
</div>
`}
${!connections.length && !showForm && html`
<div class="empty-hint">No connections configured.</div>
`}
${connections.length > 0 && html`
<table class="admin-table">
<thead><tr><th>Name</th><th>Type</th><th>Status</th><th></th></tr></thead>
<tbody>
${connections.map(c => html`
<tr>
<td style="font-weight:500;font-size:13px;">${esc(c.name)}</td>
<td style="font-size:12px;color:var(--text-2);">${esc(c.type)}</td>
<td style="font-size:12px;">
${c.is_active
? html`<span style="color:var(--green);">\u25CF Active</span>`
: html`<span style="color:var(--text-3);">\u25CB Inactive</span>`}
</td>
<td class="admin-actions-cell" style="white-space:nowrap;">
<button class="icon-btn" title="Edit" onClick=${() => openEdit(c)}>
\u{270F}\u{FE0F}
</button>
<button class="icon-btn icon-btn-danger" title="Delete"
onClick=${() => del(c)}>
\u{1F5D1}
</button>
</td>
</tr>
`)}
</tbody>
</table>
`}
`;
}

View File

@@ -27,6 +27,7 @@ const sectionModules = {
teams: () => import('./teams.js'),
models: () => import('./models.js'),
providers: () => import('./providers.js'),
connections: () => import('./connections.js'),
personas: () => import('./personas.js'),
roles: () => import('./roles.js'),
usage: () => import('./usage.js'),
@@ -49,6 +50,7 @@ const NAV_ITEMS = [
{ key: 'teams', label: 'Teams' },
{ key: 'workflows', label: 'Assignments' },
{ key: 'tasks', label: 'Tasks' },
{ key: 'connections', label: 'Connections' },
{ key: 'gitkeys', label: 'Git Keys' },
{ key: 'knowledge', label: 'Knowledge Bases' },
{ key: 'memory', label: 'Memory' },
@@ -67,6 +69,7 @@ const SECTION_TITLES = {
general: 'General', appearance: 'Appearance', models: 'Models',
personas: 'Personas', profile: 'Profile', teams: 'Teams',
workflows: 'Assignments', tasks: 'Tasks', gitkeys: 'Git Keys',
connections: 'Connections',
data: 'Data & Privacy', providers: 'My Providers', roles: 'Model Roles',
usage: 'My Usage', knowledge: 'Knowledge Bases', memory: 'Memory',
notifications: 'Notifications',

View File

@@ -0,0 +1,210 @@
/**
* Team Admin > Connections — team-scoped extension connection CRUD
* v0.38.1: Scoped credential management for extensions
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
function esc(s) { return s == null ? '' : String(s); }
export default function ConnectionsSection({ teamId }) {
const [connections, setConnections] = useState(null);
const [connTypes, setConnTypes] = useState([]);
const [showForm, setShowForm] = useState(false);
const [editId, setEditId] = useState(null);
const [form, setForm] = useState({ type: '', package_id: '', name: '', config: {} });
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
try {
const resp = await sw.api.teams.connections(teamId);
setConnections(resp || []);
} catch (e) {
setConnections([]);
}
}, [teamId]);
const loadTypes = useCallback(async () => {
try {
const pkgs = await sw.api.admin.packages.list();
const types = [];
const seen = {};
for (const pkg of (pkgs || [])) {
const conns = pkg.manifest?.connections || [];
for (const cd of conns) {
if (!seen[cd.type]) {
seen[cd.type] = true;
types.push({ type: cd.type, label: cd.label || cd.type, packageId: pkg.id, fields: cd.fields || {} });
}
}
}
setConnTypes(types);
} catch (_) {}
}, []);
useEffect(() => { load(); loadTypes(); }, [load, loadTypes]);
const selectedType = connTypes.find(t => t.type === form.type);
const openAdd = useCallback(() => {
setEditId(null);
setForm({ type: connTypes[0]?.type || '', package_id: connTypes[0]?.packageId || '', name: '', config: {} });
setShowForm(true);
}, [connTypes]);
const openEdit = useCallback((conn) => {
setEditId(conn.id);
setForm({ type: conn.type, package_id: conn.package_id, name: conn.name, config: {} });
setShowForm(true);
}, []);
const cancel = useCallback(() => {
setShowForm(false);
setEditId(null);
}, []);
const save = useCallback(async () => {
if (!form.type || !form.name) {
sw.emit('toast', { message: 'Type and name are required', variant: 'error' });
return;
}
setSaving(true);
try {
if (editId) {
const patch = { name: form.name };
if (Object.keys(form.config).length > 0) patch.config = form.config;
await sw.api.teams.updateConnection(teamId, editId, patch);
sw.emit('toast', { message: 'Connection updated', variant: 'success' });
} else {
await sw.api.teams.createConnection(teamId, {
type: form.type, package_id: form.package_id,
name: form.name, config: form.config,
});
sw.emit('toast', { message: 'Connection created', variant: 'success' });
}
setShowForm(false);
setEditId(null);
load();
} catch (e) {
sw.emit('toast', { message: e.message, variant: 'error' });
} finally {
setSaving(false);
}
}, [form, editId, teamId, load]);
const del = useCallback(async (conn) => {
const ok = await sw.confirm(`Remove connection "${conn.name}"?`);
if (!ok) return;
try {
await sw.api.teams.deleteConnection(teamId, conn.id);
sw.emit('toast', { message: 'Connection removed', variant: 'success' });
load();
} catch (e) {
sw.emit('toast', { message: e.message, variant: 'error' });
}
}, [teamId, load]);
const setField = useCallback((key, val) => {
setForm(f => ({ ...f, [key]: val }));
}, []);
const setConfigField = useCallback((key, val) => {
setForm(f => ({ ...f, config: { ...f.config, [key]: val } }));
}, []);
const onTypeChange = useCallback((typeVal) => {
const ct = connTypes.find(t => t.type === typeVal);
setForm(f => ({ ...f, type: typeVal, package_id: ct?.packageId || f.package_id, config: {} }));
}, [connTypes]);
if (connections === null) {
return html`<div class="admin-placeholder">Loading connections\u2026</div>`;
}
return html`
<div style="margin-bottom:12px;">
<button class="btn-md btn-primary" onClick=${openAdd}
disabled=${!connTypes.length}>+ Add Connection</button>
</div>
${showForm && html`
<div class="admin-card" style="margin-bottom:16px;padding:16px;">
<h3 style="margin-top:0;">${editId ? 'Edit Connection' : 'Add Connection'}</h3>
${!editId && html`
<div class="form-group">
<label>Connection Type</label>
<select value=${form.type}
onChange=${e => onTypeChange(e.target.value)}>
${connTypes.map(ct => html`
<option value=${ct.type}>${esc(ct.label)}</option>
`)}
</select>
</div>
`}
<div class="form-group">
<label>Name</label>
<input type="text" placeholder="e.g. Team Gitea"
value=${form.name}
onInput=${e => setField('name', e.target.value)} />
</div>
${selectedType && Object.entries(selectedType.fields || {}).map(([fk, fd]) => html`
<div class="form-group" key=${fk}>
<label>${esc(fd.label || fk)}${fd.required ? ' *' : ''}${editId && fd.type === 'secret' ? ' (leave blank to keep)' : ''}</label>
${fd.type === 'secret' ? html`
<input type="password" placeholder="\u2022\u2022\u2022\u2022"
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
` : fd.type === 'url' ? html`
<input type="url" placeholder="https://..."
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
` : html`
<input type="text"
value=${form.config[fk] || ''}
onInput=${e => setConfigField(fk, e.target.value)} />
`}
</div>
`)}
<div style="display:flex;gap:8px;margin-top:12px;">
<button class="btn-md btn-primary" disabled=${saving}
onClick=${save}>
${saving ? 'Saving\u2026' : editId ? 'Update' : 'Add'}
</button>
<button class="btn-md btn-secondary" onClick=${cancel}>Cancel</button>
</div>
</div>
`}
${!connections.length && !showForm && html`
<div class="empty-hint">No team connections configured.</div>
`}
${connections.length > 0 && html`
<table class="admin-table">
<thead><tr><th>Name</th><th>Type</th><th>Status</th><th></th></tr></thead>
<tbody>
${connections.map(c => html`
<tr>
<td style="font-weight:500;font-size:13px;">${esc(c.name)}</td>
<td style="font-size:12px;color:var(--text-2);">${esc(c.type)}</td>
<td style="font-size:12px;">
${c.is_active
? html`<span style="color:var(--green);">\u25CF Active</span>`
: html`<span style="color:var(--text-3);">\u25CB Inactive</span>`}
</td>
<td class="admin-actions-cell" style="white-space:nowrap;">
<button class="icon-btn" title="Edit" onClick=${() => openEdit(c)}>
\u{270F}\u{FE0F}
</button>
<button class="icon-btn icon-btn-danger" title="Delete"
onClick=${() => del(c)}>
\u{1F5D1}
</button>
</td>
</tr>
`)}
</tbody>
</table>
`}
`;
}

View File

@@ -19,6 +19,7 @@ import { DialogStack } from '../../shell/dialog-stack.js';
const SECTIONS = [
{ key: 'members', label: 'Members' },
{ key: 'providers', label: 'Providers' },
{ key: 'connections', label: 'Connections' },
{ key: 'personas', label: 'Personas' },
{ key: 'knowledge', label: 'Knowledge' },
{ key: 'groups', label: 'Groups' },
@@ -32,7 +33,8 @@ const SECTIONS = [
// ── Lazy section imports ────────────────────
const sectionModules = {
members: () => import('./members.js'),
providers: () => import('./providers.js'),
providers: () => import('./providers.js'),
connections: () => import('./connections.js'),
personas: () => import('./personas.js'),
knowledge: () => import('./knowledge.js'),
groups: () => import('./groups.js'),