Feat v0.3.7 package audit (#20)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m36s
CI/CD / test-sqlite (push) Successful in 2m41s
CI/CD / build-and-deploy (push) Successful in 1m13s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #20.
This commit is contained in:
2026-03-28 21:22:39 +00:00
committed by xcaliber
parent d68451fe8e
commit d91ec02dd7
47 changed files with 2067 additions and 53 deletions

View File

@@ -27,6 +27,11 @@ function sourceBadge(source) {
return html`<span class="badge ${cls}">${source}</span>`;
}
function statusBadge(status) {
if (status === 'dormant') return html`<span class="badge" style="opacity:0.6;">dormant</span>`;
return null;
}
export default function PackagesSection() {
const [packages, setPackages] = useState([]);
const [loading, setLoading] = useState(true);
@@ -56,6 +61,10 @@ export default function PackagesSection() {
// ── Actions ─────────────────────────────────
async function toggleEnabled(pkg) {
if (CORE_IDS.has(pkg.id)) return;
if (pkg.status === 'dormant') {
sw.toast('This package requires chat — it cannot be enabled yet', 'error');
return;
}
try {
if (pkg.enabled) {
await sw.api.admin.packages.disable(pkg.id);
@@ -154,6 +163,7 @@ export default function PackagesSection() {
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
const enabled = packages.filter(p => p.enabled).length;
const dormant = packages.filter(p => p.status === 'dormant').length;
const hasManifestSettings = (pkg) =>
pkg.manifest?.settings || pkg.package_settings;
@@ -163,6 +173,7 @@ export default function PackagesSection() {
<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>
${dormant > 0 && html`<div class="stat-card"><div class="stat-value">${dormant}</div><div class="stat-label">Dormant</div></div>`}
</div>
${/* ── Action bar ─────────────── */``}
@@ -194,12 +205,14 @@ export default function PackagesSection() {
${' '}${typeBadge(pkg.type)}
${' '}${tierBadge(pkg.tier)}
${sourceBadge(pkg.source)}
${statusBadge(pkg.status)}
${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)}>
disabled=${CORE_IDS.has(pkg.id) || pkg.status === 'dormant'}
title=${pkg.status === 'dormant' ? 'Requires chat' : ''}>
${pkg.enabled ? 'Disable' : 'Enable'}
</button>
${hasManifestSettings(pkg) && html`

View File

@@ -15,8 +15,10 @@ export default function TeamsSection() {
const loadTeams = useCallback(async () => {
try {
const data = await sw.api.admin.teams.list();
setTeams(data || []);
const resp = await sw.api.admin.teams.list();
// Paginated response: { data: [...], total, page }
const list = Array.isArray(resp) ? resp : (resp && resp.data ? resp.data : []);
setTeams(list);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);

View File

@@ -59,16 +59,22 @@ export default function WorkflowsSection({ teamId }) {
// ── Tab 1: Workflows ────────────────────────
function _unwrapList(resp) {
return Array.isArray(resp) ? resp : (resp && resp.data ? resp.data : []);
}
function WorkflowsTab({ teamId }) {
const [workflows, setWorkflows] = useState([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(null);
const [showCreate, setShowCreate] = useState(false);
const [showAdopt, setShowAdopt] = useState(false);
const [available, setAvailable] = useState([]);
const load = useCallback(async () => {
try {
const data = await sw.api.teams.workflows(teamId);
setWorkflows(data || []);
setWorkflows(_unwrapList(data));
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, [teamId]);
@@ -91,6 +97,23 @@ function WorkflowsTab({ teamId }) {
} catch (e) { sw.toast(e.message, 'error'); }
}
async function openAdopt() {
setShowAdopt(true);
try {
const data = await sw.api.teams.availableWorkflows(teamId);
setAvailable(_unwrapList(data));
} catch (e) { sw.toast(e.message, 'error'); }
}
async function adoptWorkflow(wfId) {
try {
await sw.api.teams.adoptWorkflow(teamId, wfId);
sw.toast('Workflow adopted', 'success');
setShowAdopt(false);
load();
} catch (e) { sw.toast(e.message, 'error'); }
}
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
if (editing) return html`<${WorkflowEditor} teamId=${teamId} workflow=${editing}
@@ -100,9 +123,33 @@ function WorkflowsTab({ teamId }) {
<div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<div style="flex:1"></div>
<button class="btn-md" onClick=${openAdopt}>Adopt Global</button>
<button class="btn-md btn-primary" onClick=${() => setShowCreate(true)}>+ Add</button>
</div>
${showAdopt && html`
<div class="admin-inline-form" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<strong>Adopt a Global Workflow</strong>
<div style="flex:1"></div>
<button class="btn-small" onClick=${() => setShowAdopt(false)}>Cancel</button>
</div>
${available.length === 0 && html`<div class="empty-hint">No global workflows available</div>`}
<div class="admin-list">
${available.map(w => html`
<div class="admin-surface-row" key=${w.id}>
<div style="flex:1;min-width:0;">
<strong>${w.name}</strong>
<span class="text-muted" style="margin-left:8px;font-size:11px;">/${w.slug}</span>
</div>
<span class="badge">${w.entry_mode}</span>
<button class="btn-small btn-primary" onClick=${() => adoptWorkflow(w.id)}>Adopt</button>
</div>
`)}
</div>
</div>
`}
${showCreate && html`
<form class="admin-inline-form" style="margin-bottom:16px;" onSubmit=${createWorkflow}>
<div class="form-row">
@@ -123,7 +170,7 @@ function WorkflowsTab({ teamId }) {
`}
<div class="admin-list">
${workflows.length === 0 && html`<div class="empty-hint">No workflows</div>`}
${workflows.length === 0 && html`<div class="empty-hint">No workflows — create one or adopt from global</div>`}
${workflows.map(w => html`
<div class="admin-surface-row" key=${w.id} style="cursor:pointer;" onClick=${() => setEditing(w)}>
<div style="flex:1;min-width:0;">
@@ -245,6 +292,24 @@ function WorkflowEditor({ teamId, workflow, onBack }) {
<span class="toggle-track"></span><span>Active</span>
</label>
${workflow.entry_mode === 'public_link' && html`
<div style="margin:8px 0;">
<label style="font-size:11px;color:var(--text-2);display:block;margin-bottom:4px;">Public Entry URL</label>
<div style="display:flex;gap:6px;align-items:center;">
<input type="text" readOnly value=${window.location.origin + (window.__BASE__ || '') + '/api/v1/public/workflows/' + workflow.id + '/start'}
style="flex:1;font-size:11px;padding:4px 8px;background:var(--input-bg);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:var(--mono);"
onClick=${e => e.target.select()} />
<button type="button" class="btn-small" onClick=${(e) => {
const url = window.location.origin + (window.__BASE__ || '') + '/api/v1/public/workflows/' + workflow.id + '/start';
navigator.clipboard.writeText(url).then(() => {
e.target.textContent = 'Copied!';
setTimeout(() => { e.target.textContent = 'Copy'; }, 1500);
});
}}>Copy</button>
</div>
</div>
`}
<div class="form-row" style="margin-top:16px;gap:8px;">
<button type="submit" class="btn-small btn-primary">Save</button>
</div>