This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/surfaces/admin/settings.js
Jeffrey Smith 1fd9d1574e feat(admin): add Package Registry settings UI section
Adds a registry URL field to Admin > Settings so admins can configure
an external package registry without touching env vars or the API directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:13:11 +00:00

190 lines
10 KiB
JavaScript

/**
* Admin > System > Settings
*/
const { html } = window;
const { useState, useEffect, useCallback } = hooks;
export default function SettingsSection() {
const [cfg, setCfg] = useState({});
const [surfaces, setSurfaces] = useState([]);
const [vault, setVault] = useState(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
try {
const [s, v, surfList] = await Promise.all([
sw.api.admin.settings.get(),
sw.api.admin.vault.status().catch(() => null),
sw.api.admin.surfaces.list().catch(() => []),
]);
const raw = s || {};
const pol = raw.policies || {};
const cfg_ = raw.settings || {};
setSurfaces((surfList || []).filter(s => s.enabled));
setCfg({
default_surface: cfg_.default_surface?.id || '',
allow_registration: pol.allow_registration === 'true',
registration_default_state: cfg_.registration_default_state || 'pending',
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 || '',
package_registry_url: cfg_.package_registry?.url || '',
});
setVault(v);
} catch (e) { sw.toast(e.message, 'error'); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, []);
function set(key, val) { setCfg(prev => ({ ...prev, [key]: val })); }
async function save() {
setSaving(true);
try {
// Default surface
await sw.api.admin.settings.update('default_surface', { value: cfg.default_surface ? { id: cfg.default_surface } : null });
// Registration
await sw.api.admin.settings.update('allow_registration', { value: String(cfg.allow_registration) });
await sw.api.admin.settings.update('registration_default_state', { value: cfg.registration_default_state });
// Banner
await sw.api.admin.settings.update('banner', { value: {
enabled: cfg.banner_enabled,
text: cfg.banner_text,
bg: cfg.banner_bg,
fg: cfg.banner_fg,
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,
}});
// Package Registry
await sw.api.admin.settings.update('package_registry', { value: { url: cfg.package_registry_url } });
sw.toast('Settings saved', 'success');
} catch (e) { sw.toast(e.message, 'error'); }
finally { setSaving(false); }
}
async function testEmail() {
try {
await sw.api.admin.notifications.testEmail();
sw.toast('Test email sent', 'success');
} catch (e) { sw.toast(e.message, 'error'); }
}
if (loading) return html`<div class="settings-placeholder">Loading\u2026</div>`;
return html`
<div class="admin-settings-form">
<div class="settings-section"><h3>Default Surface</h3>
<div class="form-group"><label>Landing page for /</label>
<select value=${cfg.default_surface} onChange=${e => set('default_surface', e.target.value)}>
<option value="">-- Auto (first extension surface, then Admin) --</option>
${surfaces.filter(s => s.source !== 'core').map(s => html`<option key=${s.id} value=${s.id}>${s.title || s.id}</option>`)}
</select>
</div>
<span class="text-muted" style="font-size:12px;">Where users land when visiting /. Only extension surfaces are listed.</span>
</div>
<div class="settings-section"><h3>Registration</h3>
<label class="toggle-label"><input type="checkbox" checked=${cfg.allow_registration} onChange=${e => set('allow_registration', e.target.checked)} /><span class="toggle-track"></span><span>Allow new user registration</span></label>
<div class="form-group" style="margin-top:8px;"><label>Default state</label>
<select value=${cfg.registration_default_state} onChange=${e => set('registration_default_state', e.target.value)}>
<option value="pending">Pending (require approval)</option>
<option value="active">Active (auto-approve)</option>
</select>
</div>
</div>
<div class="settings-section"><h3>Environment Banner</h3>
<label class="toggle-label"><input type="checkbox" checked=${cfg.banner_enabled} onChange=${e => set('banner_enabled', e.target.checked)} /><span class="toggle-track"></span><span>Show environment banner</span></label>
${cfg.banner_enabled && html`
<div style="margin-top:8px;">
<div class="form-group"><label>Text</label><input value=${cfg.banner_text} onInput=${e => set('banner_text', e.target.value)} placeholder="DEVELOPMENT" /></div>
<div class="form-row">
<div class="form-group"><label>Background</label><input type="color" value=${cfg.banner_bg} onInput=${e => set('banner_bg', e.target.value)} /></div>
<div class="form-group"><label>Foreground</label><input type="color" value=${cfg.banner_fg} onInput=${e => set('banner_fg', e.target.value)} /></div>
<div class="form-group"><label>Position</label>
<select value=${cfg.banner_position} onChange=${e => set('banner_position', e.target.value)}>
<option value="both">Both</option><option value="top">Top</option><option value="bottom">Bottom</option>
</select>
</div>
</div>
<div class="banner-preview" style="margin-top:8px;padding:4px 12px;text-align:center;font-size:12px;font-weight:600;border-radius:4px;background:${cfg.banner_bg};color:${cfg.banner_fg};">${cfg.banner_text || 'PREVIEW'}</div>
</div>
`}
</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 Switchboard Core" /></div>
</div>
`}
</div>
<div class="settings-section"><h3>Encryption Vault</h3>
${vault
? html`<div class="text-muted" style="font-size:12px;">Key set: ${vault.encryption_key_set ? 'Yes' : 'No'} \u2022 User vaults: ${vault.user_vaults_count ?? '?'}</div>`
: html`<span class="text-muted" style="font-size:12px;">Could not load vault status</span>`
}
</div>
<div class="settings-section"><h3>Package Registry</h3>
<div class="form-group"><label>Registry URL</label>
<input value=${cfg.package_registry_url} onInput=${e => set('package_registry_url', e.target.value)} placeholder="https://registry.example.com/registry.json" />
</div>
<span class="text-muted" style="font-size:12px;">URL to a JSON registry index. Enables browsing and one-click install under Packages > Registry.</span>
</div>
<div class="settings-section"><h3>Email</h3>
<button class="btn-small" onClick=${testEmail}>Send Test Email</button>
</div>
<div style="display:flex;gap:8px;align-items:center;margin-top:16px;">
<button class="btn-md btn-primary" onClick=${save} disabled=${saving}>${saving ? 'Saving\u2026' : 'Save Settings'}</button>
</div>
</div>
`;
}