Feat v0.2.7 user settings audit (#11)

Audit all 6 user settings sections. Remove dead chat-era code,
rename localStorage namespace, clean up stale backend policies.

- Remove BYOK nav section (empty BYOK_ITEMS, byokEnabled state,
  "BYOK Enabled" badge) and personas gate filter from settings nav
- Remove auth.permissions.changed listener (existed solely for
  dead BYOK + personas state)
- Remove Message Font Size slider, msgFont state, and --msg-font
  CSS variable application from appearance section and base template
- Rename localStorage key cs-appearance → sb-appearance with
  one-time migration preserving existing user preferences
- Remove allow_user_byok and allow_user_personas from PolicyDefaults,
  profile bootstrap, permissions handler, PublicSettings, and test seeds
- Remove orphaned .settings-nav-footer CSS class
- Replace stale policy-gating test assertions with allow_registration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 15:38:42 +00:00
parent c9effb0285
commit 66063b08ed
13 changed files with 48 additions and 97 deletions

View File

@@ -2,6 +2,32 @@
All notable changes to Switchboard Core are documented here.
## [Unreleased] — v0.2.7
### Changed
- **localStorage namespace**: Renamed `cs-appearance` key to `sb-appearance`
across appearance settings, base template, and workflow template. One-time
migration preserves existing user preferences.
- **Policy-gating tests**: Replaced stale `allow_user_byok` / `allow_user_personas`
assertions with `allow_registration` check (the only policy still in admin UI).
### Removed
- **Dead BYOK nav section** in user settings: empty `BYOK_ITEMS` array,
`byokEnabled` state, "BYOK Enabled" footer badge, and the nav group
that rendered an empty section.
- **Dead personas gate**: `personasEnabled` state and `.filter()` on
NAV_ITEMS for a `gate` property no items have. `auth.permissions.changed`
listener removed (existed solely for BYOK + personas state).
- **Dead Message Font Size**: Slider, `msgFont` state, and `--msg-font`
CSS variable application from appearance section and base template.
- **Dead policy defaults**: `allow_user_byok` and `allow_user_personas`
removed from `PolicyDefaults`, profile bootstrap, permissions handler,
and `PublicSettings`. Test seed data cleaned.
---
## [Unreleased] — v0.2.6
### Changed

View File

@@ -100,8 +100,8 @@ SDK stabilization, and the first rebuilt extension (tasks).
| Step | Status | Description |
|------|--------|-------------|
| User settings E2E | | Verify all user settings sections: General (default surface), Appearance (theme, scale, font), Profile (display name, avatar, handle), Connections, Notifications. Remove dead features, fix broken states. |
| Visibility gating | | Ensure settings sections only show features that are actually available. Hide empty sections. Respect `user_overridable` flag from extension manifests. |
| User settings E2E | | All 6 user settings sections verified (General, Appearance, Profile, Teams, Connections, Notifications). Dead code removed: BYOK nav section + state, personas gate filter, Message Font Size slider, `auth.permissions.changed` listener. localStorage key renamed `cs-appearance``sb-appearance` with one-time migration. |
| Visibility gating | | Dead BYOK/personas policy lookups removed from bootstrap and permissions handlers. `allow_user_byok` removed from `PublicSettings`. `PolicyDefaults` cleaned of `allow_user_byok` and `allow_user_personas`. Dead `msgFont` early-apply removed from base template. Stale policy-gating test assertions replaced. No empty nav sections remain. |
### v0.2.8 — Team Admin Settings Audit (Pass 1)

View File

@@ -340,8 +340,6 @@ func TruncateAll(t *testing.T) {
`)
DB.Exec(`
INSERT INTO platform_policies (key, value) VALUES
('allow_user_byok', 'false'),
('allow_user_personas', 'false'),
('allow_registration', 'true'),
('default_user_active', 'false'),
('allow_team_providers', 'true')
@@ -357,8 +355,6 @@ func TruncateAll(t *testing.T) {
`)
DB.Exec(`
INSERT INTO platform_policies (key, value) VALUES
('allow_user_byok', 'false'),
('allow_user_personas', 'false'),
('allow_registration', 'true'),
('default_user_active', 'false'),
('allow_team_providers', 'true')

View File

@@ -369,7 +369,6 @@ func (h *AdminHandler) PublicSettings(c *gin.Context) {
"storage_configured": storageConfigured,
"policies": gin.H{
"allow_registration": policies["allow_registration"],
"allow_user_byok": policies["allow_user_byok"],
},
})
}

View File

@@ -83,10 +83,6 @@ func (h *ProfileBootstrapHandler) GetBootstrap(c *gin.Context) {
// ── Policies ────────────────────────────
policies := make(map[string]bool)
if ps := h.stores.Policies; ps != nil {
policies["allow_user_byok"], _ = ps.GetBool(ctx, "allow_user_byok")
policies["allow_user_personas"], _ = ps.GetBool(ctx, "allow_user_personas")
}
// ── Settings ────────────────────────────
settings := make(map[string]interface{})

View File

@@ -64,10 +64,6 @@ func (h *ProfilePermissionsHandler) GetMyPermissions(c *gin.Context) {
// Policies that affect UI gating
policies := make(map[string]bool)
if ps := h.stores.Policies; ps != nil {
policies["allow_user_byok"], _ = ps.GetBool(ctx, "allow_user_byok")
policies["allow_user_personas"], _ = ps.GetBool(ctx, "allow_user_personas")
}
c.JSON(http.StatusOK, gin.H{
"permissions": list,

View File

@@ -84,8 +84,6 @@ type TeamMember struct {
// PLATFORM POLICIES
var PolicyDefaults = map[string]string{
"allow_user_byok": "false",
"allow_user_personas": "false",
"allow_registration": "true",
"default_user_active": "false",
"allow_team_providers": "true",

View File

@@ -51,11 +51,11 @@
if (fav) fav.href = fav.href.replace(/favicon(-light)?\.svg/, resolved === 'light' ? 'favicon-light.svg' : 'favicon.svg');
} catch(e) {}
})();
// Early appearance — apply scale + msg font from localStorage on all surfaces.
// Early appearance — apply scale from localStorage on all surfaces.
// Scale uses transform on .surface-inner so shell stays fixed, content fills viewport.
(function() {
try {
var p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
var p = JSON.parse(localStorage.getItem('sb-appearance') || '{}');
if (p.scale && p.scale !== 100) {
var s = p.scale / 100;
document.addEventListener('DOMContentLoaded', function() {
@@ -63,7 +63,6 @@
if (el) { el.style.transform = 'scale('+s+')'; el.style.transformOrigin = 'top left'; el.style.width = (100/s)+'%'; el.style.height = (100/s)+'%'; }
});
}
if (p.msgFont && p.msgFont !== 14) document.documentElement.style.setProperty('--msg-font', p.msgFont + 'px');
} catch(e) {}
})();
</script>

View File

@@ -163,7 +163,7 @@
<script>
(function() {
try {
var p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
var p = JSON.parse(localStorage.getItem('sb-appearance') || '{}');
var mode = p.theme || 'system';
var resolved = mode;
if (mode === 'system') resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';

View File

@@ -56,9 +56,6 @@
.settings-nav-link:hover { background: var(--bg-hover); color: var(--text); text-decoration: none; }
.settings-nav-link.active { background: var(--accent-dim); color: var(--accent); }
.settings-nav-sep { margin: 8px 0; border-top: 1px solid var(--border); }
.settings-nav-footer {
margin-top: auto; padding: 10px 12px; border-top: 1px solid var(--border);
}
.settings-content { flex: 1; overflow-y: auto; padding: 28px; }
.settings-content h2 { font-size: 18px; font-weight: 600; margin: 0 0 20px; }
.settings-content .settings-section {

View File

@@ -164,14 +164,9 @@ describe('Admin Preact surface handles settings', () => {
if (fs.existsSync(settingsPath)) {
const settingsSrc = fs.readFileSync(settingsPath, 'utf-8');
it('admin settings component references allow_user_byok', () => {
assert.ok(settingsSrc.includes('allow_user_byok'),
'MISSING: allow_user_byok in admin settings component');
});
it('admin settings component references allow_user_personas', () => {
assert.ok(settingsSrc.includes('allow_user_personas'),
'MISSING: allow_user_personas in admin settings component');
it('admin settings component references allow_registration', () => {
assert.ok(settingsSrc.includes('allow_registration'),
'MISSING: allow_registration in admin settings component');
});
}
});

View File

@@ -1,9 +1,15 @@
/**
* AppearanceSection — theme, UI scale, message font size
* AppearanceSection — theme, UI scale
*/
const { html } = window;
const { useState, useCallback } = hooks;
// Migrate cs- prefix to sb- (one-time, preserves existing prefs)
if (!localStorage.getItem('sb-appearance') && localStorage.getItem('cs-appearance')) {
localStorage.setItem('sb-appearance', localStorage.getItem('cs-appearance'));
localStorage.removeItem('cs-appearance');
}
const THEMES = [
{ key: 'light', icon: '\u2600', label: 'Light' },
{ key: 'dark', icon: '\u263E', label: 'Dark' },
@@ -11,7 +17,7 @@ const THEMES = [
];
function getPrefs() {
try { return JSON.parse(localStorage.getItem('cs-appearance') || '{}'); }
try { return JSON.parse(localStorage.getItem('sb-appearance') || '{}'); }
catch { return {}; }
}
@@ -19,7 +25,6 @@ export function AppearanceSection() {
const prefs = getPrefs();
const [theme, setTheme] = useState(sw.theme?.current || 'system');
const [scale, setScale] = useState(prefs.scale || 100);
const [msgFont, setMsgFont] = useState(prefs.msgFont || 14);
const applyTheme = useCallback((mode) => {
setTheme(mode);
@@ -30,8 +35,7 @@ export function AppearanceSection() {
const save = useCallback(() => {
const p = getPrefs();
p.scale = scale;
p.msgFont = msgFont;
localStorage.setItem('cs-appearance', JSON.stringify(p));
localStorage.setItem('sb-appearance', JSON.stringify(p));
// Apply scale to content area only (not shell/nav)
const inner = document.getElementById('surfaceInner');
@@ -48,10 +52,9 @@ export function AppearanceSection() {
inner.style.height = '';
}
}
document.documentElement.style.setProperty('--msg-font', msgFont + 'px');
sw.emit('toast', { message: 'Appearance saved', variant: 'success' });
}, [scale, msgFont]);
}, [scale]);
return html`
<div class="settings-section">
@@ -78,18 +81,6 @@ export function AppearanceSection() {
</span>
</div>
</div>
<h3 style="margin-top:16px;">Message Font Size</h3>
<div class="form-group">
<div style="display:flex;align-items:center;gap:8px;">
<input type="range" min="12" max="20" step="1"
value=${msgFont}
onInput=${e => setMsgFont(parseInt(e.target.value))}
style="flex:1;" />
<span style="font-size:12px;color:var(--text-2);font-family:var(--mono);min-width:32px;">
${msgFont}px
</span>
</div>
</div>
</div>
<button class="btn-md btn-primary" onClick=${save}>Save</button>
`;

View File

@@ -6,14 +6,11 @@
* __BASE__ — base path
* __CONFIG_SECTIONS__ — v0.38.3: extension config sections (array|null)
*
* Policy flags (BYOK, personas) come from sw.auth.policies,
* populated at SDK boot via GET /api/v1/profile/permissions.
*
* Layout: topbar + left nav + content area (same CSS classes as before).
* All sections are native Preact components loaded lazily.
*/
const { html } = window;
const { useState, useEffect, useCallback, useMemo } = hooks;
const { useState, useEffect, useCallback } = hooks;
const { render } = preact;
import { ToastContainer } from '../../primitives/toast.js';
@@ -40,8 +37,6 @@ const NAV_ITEMS = [
{ key: 'notifications', label: 'Notifications' },
];
const BYOK_ITEMS = [];
// ── Section title map ───────────────────────
const SECTION_TITLES = {
general: 'General', appearance: 'Appearance',
@@ -67,22 +62,8 @@ function SettingsSurface() {
const BASE = window.__BASE__ || '';
const section = window.__SECTION__ || 'general';
const policies = sw.auth?.policies || {};
const [byokEnabled, setByokEnabled] = useState(policies.allow_user_byok === 'true');
const [personasEnabled, setPersonasEnabled] = useState(policies.allow_user_personas === 'true');
const [SectionComponent, setSectionComponent] = useState(null);
// Update when permissions refresh (e.g. after boot or policy change)
useEffect(() => {
function _onPermsChanged() {
const p = sw.auth?.policies || {};
setByokEnabled(p.allow_user_byok === 'true');
setPersonasEnabled(p.allow_user_personas === 'true');
}
sw.on('auth.permissions.changed', _onPermsChanged);
return () => sw.off('auth.permissions.changed', _onPermsChanged);
}, []);
// Load the section component
useEffect(() => {
setSectionComponent(null);
@@ -153,9 +134,7 @@ function SettingsSurface() {
<div style="display:flex;flex:1;min-height:0;">
${/* Left Nav */''}
<div class="settings-nav">
${NAV_ITEMS
.filter(item => !item.gate || (item.gate === 'personas' && personasEnabled))
.map(item => html`
${NAV_ITEMS.map(item => html`
<a href="${BASE}/settings/${item.key}"
class="settings-nav-link ${section === item.key ? 'active' : ''}"
onClick=${onNavClick}>
@@ -163,27 +142,6 @@ function SettingsSurface() {
</a>
`)}
${byokEnabled && html`
<div>
<div class="settings-nav-sep"></div>
${BYOK_ITEMS.map(item => html`
<a href="${BASE}/settings/${item.key}"
class="settings-nav-link ${section === item.key ? 'active' : ''}"
onClick=${onNavClick}>
${item.label}
</a>
`)}
</div>
<div class="settings-nav-footer">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="font-size:11px;font-weight:600;color:var(--success);">BYOK Enabled</span>
</div>
<p style="font-size:10px;color:var(--text-3);margin:0;line-height:1.5;">
Admin has enabled Bring Your Own Key
</p>
</div>
`}
${EXT_NAV_ITEMS.length > 0 && html`
<div>
<div class="settings-nav-sep"></div>