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>
190 lines
10 KiB
JavaScript
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>
|
|
`;
|
|
}
|