diff --git a/CHANGESET.md b/CHANGESET.md
new file mode 100644
index 0000000..f6902b2
--- /dev/null
+++ b/CHANGESET.md
@@ -0,0 +1,395 @@
+# CS-0.37.1 — Permission Audit & Enforcement
+
+**Date:** 2026-03-20
+**Scope:** Backend permission enforcement + new self-service permissions endpoint
+**CI Impact:** Adds new test file with 12 test functions. Existing tests unaffected.
+
+---
+
+## Summary
+
+### Problem
+- 8 protected routes enforced only `Auth()` (logged-in check) but not
+ `RequirePermission(perm)`. Any authenticated user could call them
+ regardless of group permissions.
+- No user-facing permissions endpoint — the frontend has zero ability to
+ know what the user is allowed to do.
+- The Everyone group seeds with `model.use`, `kb.read`, `channel.create`,
+ masking the enforcement gaps in most deployments.
+- All existing task tests run as admin, which bypasses RequirePermission
+ entirely — permission denial was never tested.
+
+### Fix
+1. Add `RequirePermission` middleware to 8 routes in `main.go`
+2. Mirror the same 8 fixes in the integration test harness
+3. Add `GET /profile/permissions` endpoint (new handler file)
+4. Add `GET /profile/permissions` to route registration + test harness
+5. Add 12 permission enforcement integration tests (deny → grant → allow)
+
+### New Files (drop-in)
+- `server/handlers/profile_permissions.go`
+- `server/handlers/perm_enforcement_test.go`
+
+### Edited Files (str_replace patches below)
+- `server/main.go` — 9 route edits
+- `server/handlers/integration_test.go` — 9 route edits (mirror)
+- `docs/ICD/profile.md` — add new endpoint documentation
+
+---
+
+## Patch Instructions
+
+Apply each str_replace in order. Each section shows the exact old string
+and the exact replacement.
+
+### 1. main.go — Completions (model.use)
+
+**Old:**
+```
+ protected.POST("/chat/completions", comp.Complete)
+```
+
+**New:**
+```
+ protected.POST("/chat/completions", middleware.RequirePermission(auth.PermModelUse, stores), comp.Complete)
+```
+
+### 2. main.go — Summarize (model.use)
+
+**Old:**
+```
+ protected.POST("/channels/:id/summarize", summarize.Summarize)
+```
+
+**New:**
+```
+ protected.POST("/channels/:id/summarize", middleware.RequirePermission(auth.PermModelUse, stores), summarize.Summarize)
+```
+
+### 3. main.go — Generate Title (model.use)
+
+**Old:**
+```
+ protected.POST("/channels/:id/generate-title", titleH.GenerateTitle)
+```
+
+**New:**
+```
+ protected.POST("/channels/:id/generate-title", middleware.RequirePermission(auth.PermModelUse, stores), titleH.GenerateTitle)
+```
+
+### 4. main.go — KB Search (kb.read)
+
+**Old:**
+```
+ protected.POST("/knowledge-bases/:id/search", kbH.SearchKB)
+```
+
+**New:**
+```
+ protected.POST("/knowledge-bases/:id/search", middleware.RequirePermission(auth.PermKBRead, stores), kbH.SearchKB)
+```
+
+### 5. main.go — KB Update (kb.write)
+
+**Old:**
+```
+ protected.PUT("/knowledge-bases/:id", kbH.UpdateKB)
+```
+
+**New:**
+```
+ protected.PUT("/knowledge-bases/:id", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.UpdateKB)
+```
+
+### 6. main.go — KB Delete (kb.write)
+
+**Old:**
+```
+ protected.DELETE("/knowledge-bases/:id", kbH.DeleteKB)
+```
+
+**New:**
+```
+ protected.DELETE("/knowledge-bases/:id", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.DeleteKB)
+```
+
+### 7. main.go — KB Document Delete (kb.write)
+
+**Old:**
+```
+ protected.DELETE("/knowledge-bases/:id/documents/:docId", kbH.DeleteDocument)
+```
+
+**New:**
+```
+ protected.DELETE("/knowledge-bases/:id/documents/:docId", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.DeleteDocument)
+```
+
+### 8. main.go — KB Rebuild (kb.write)
+
+**Old:**
+```
+ protected.POST("/knowledge-bases/:id/rebuild", kbH.RebuildKB)
+```
+
+**New:**
+```
+ protected.POST("/knowledge-bases/:id/rebuild", middleware.RequirePermission(auth.PermKBWrite, stores), kbH.RebuildKB)
+```
+
+### 9. main.go — New route: GET /profile/permissions
+
+Insert AFTER the existing profile routes block. Find:
+
+```
+ protected.GET("/settings", settings.GetSettings)
+ protected.PUT("/settings", settings.UpdateSettings)
+```
+
+Insert immediately after:
+
+```
+ // Permission bootstrap (v0.37.1) — self-service resolved permissions
+ permH := handlers.NewProfilePermissionsHandler(stores)
+ protected.GET("/profile/permissions", permH.GetMyPermissions)
+```
+
+---
+
+### 10. integration_test.go — Mirror: Completions (model.use)
+
+**Old:**
+```
+ protected.POST("/chat/completions", completions.Complete)
+```
+
+**New:**
+```
+ protected.POST("/chat/completions", middleware.RequirePermission(authpkg.PermModelUse, stores), completions.Complete)
+```
+
+### 11. integration_test.go — Mirror: KB Search (kb.read)
+
+**Old:**
+```
+ protected.POST("/knowledge-bases/:id/search", kbH.SearchKB)
+```
+
+**New:**
+```
+ protected.POST("/knowledge-bases/:id/search", middleware.RequirePermission(authpkg.PermKBRead, stores), kbH.SearchKB)
+```
+
+### 12. integration_test.go — Mirror: KB Update (kb.write)
+
+**Old:**
+```
+ protected.PUT("/knowledge-bases/:id", kbH.UpdateKB)
+```
+
+**New:**
+```
+ protected.PUT("/knowledge-bases/:id", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.UpdateKB)
+```
+
+### 13. integration_test.go — Mirror: KB Delete (kb.write)
+
+**Old:**
+```
+ protected.DELETE("/knowledge-bases/:id", kbH.DeleteKB)
+```
+
+**New:**
+```
+ protected.DELETE("/knowledge-bases/:id", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.DeleteKB)
+```
+
+### 14. integration_test.go — Mirror: KB Doc Delete (kb.write)
+
+**Old:**
+```
+ protected.DELETE("/knowledge-bases/:id/documents/:docId", kbH.DeleteDocument)
+```
+
+**New:**
+```
+ protected.DELETE("/knowledge-bases/:id/documents/:docId", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.DeleteDocument)
+```
+
+### 15. integration_test.go — Mirror: KB Rebuild (kb.write)
+
+**Old:**
+```
+ protected.POST("/knowledge-bases/:id/rebuild", kbH.RebuildKB)
+```
+
+**New:**
+```
+ protected.POST("/knowledge-bases/:id/rebuild", middleware.RequirePermission(authpkg.PermKBWrite, stores), kbH.RebuildKB)
+```
+
+### 16. integration_test.go — New route: GET /profile/permissions
+
+Find this block in setupHarness:
+
+```
+ // Settings / Profile
+ settings := NewSettingsHandler(stores, nil)
+```
+
+Insert BEFORE it:
+
+```
+ // Profile permissions (v0.37.1)
+ permH := NewProfilePermissionsHandler(stores)
+ protected.GET("/profile/permissions", permH.GetMyPermissions)
+
+```
+
+### 17. Existing KB Permission Test Update
+
+The existing `TestIntegration_KB_PermissionEnforcement` test (line ~2808)
+tests kb.create denial. It still passes because it relies on TruncateAll
+wiping the Everyone group. No changes needed.
+
+However, note that this test's comment says:
+```
+// (TruncateAll wipes the Everyone group, so no default permissions)
+```
+This is the only reason it works. On a live system with an intact Everyone
+group seeded with `kb.read`, the denial would not trigger for kb.read
+routes. The new `perm_enforcement_test.go` tests do NOT rely on Everyone
+group presence/absence — they work from a clean slate.
+
+---
+
+## ICD Update
+
+Add to `docs/ICD/profile.md` before the closing `---`:
+
+```markdown
+### Get My Permissions
+
+\```
+GET /profile/permissions
+\```
+
+**Auth:** Authenticated user
+
+Returns the current user's resolved permission set, contributing group
+IDs, team memberships, and UI-relevant policies. Admin users receive all
+permissions.
+
+**Response:**
+
+\```json
+{
+ "permissions": ["model.use", "kb.read", "channel.create"],
+ "groups": ["group-id-1", "00000000-0000-0000-0000-000000000001"],
+ "teams": [
+ { "id": "uuid", "name": "Engineering", "my_role": "member" }
+ ],
+ "policies": {
+ "allow_user_byok": false,
+ "allow_user_personas": false,
+ "allow_raw_model_access": true,
+ "kb_direct_access": true
+ }
+}
+\```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `permissions` | string[] | Resolved permission set (union of all groups) |
+| `groups` | string[] | Contributing group IDs (always includes Everyone) |
+| `teams` | object[] | User's active team memberships with role |
+| `policies` | object | Boolean policy flags relevant to UI feature gating |
+```
+
+---
+
+## Audit Summary
+
+### Routes Now Enforcing RequirePermission
+
+| Route | Permission | Was | Now |
+|-------|-----------|-----|-----|
+| `POST /chat/completions` | `model.use` | Auth() only | RequirePermission |
+| `POST /channels/:id/summarize` | `model.use` | Auth() only | RequirePermission |
+| `POST /channels/:id/generate-title` | `model.use` | Auth() only | RequirePermission |
+| `POST /knowledge-bases/:id/search` | `kb.read` | Auth() only | RequirePermission |
+| `PUT /knowledge-bases/:id` | `kb.write` | Auth() only | RequirePermission |
+| `DELETE /knowledge-bases/:id` | `kb.write` | Auth() only | RequirePermission |
+| `DELETE /knowledge-bases/:id/documents/:docId` | `kb.write` | Auth() only | RequirePermission |
+| `POST /knowledge-bases/:id/rebuild` | `kb.write` | Auth() only | RequirePermission |
+
+### Routes Already Correctly Enforced (no changes)
+
+| Route | Permission |
+|-------|-----------|
+| `POST /channels` | `channel.create` |
+| `POST /channels/:id/participants` | `channel.invite` |
+| `POST /personas` | `persona.create` |
+| `PUT /personas/:id` | `persona.manage` |
+| `DELETE /personas/:id` | `persona.manage` |
+| `POST /knowledge-bases` | `kb.create` |
+| `POST /knowledge-bases/:id/documents` | `kb.write` |
+| `POST /workflows` | `workflow.create` |
+| `PATCH /workflows/:id` | `workflow.create` |
+| `DELETE /workflows/:id` | `workflow.create` |
+| All workflow stage routes | `workflow.create` |
+| `POST /tasks` | `task.create` |
+| `PUT /tasks/:id` | `task.create` |
+| `DELETE /tasks/:id` | `task.create` |
+| `POST /tasks/:id/run` | `task.create` |
+| `POST /tasks/:id/kill` | `task.create` |
+| Team tasks (scoped) | `task.create` |
+| All admin routes | `RequireAdmin()` |
+| Team-scoped routes | `RequireTeamAdmin()` |
+
+### Routes Correctly Using Auth() Only (no permission needed)
+
+Read-only data for the authenticated user: channels list, messages, notes,
+projects, profile, settings, models/enabled, notifications, extensions,
+groups/mine, teams/mine, presence, folders, workspaces, files, memories,
+packages, surfaces, usage, search, export/me.
+
+### Everyone Group Default Permissions
+
+The migration seeds Everyone with: `model.use`, `kb.read`, `channel.create`.
+
+These three permissions mean that on a default deployment, the new
+enforcement on completions (`model.use`) and KB search (`kb.read`) will
+not change behavior — users already have these via Everyone.
+
+The enforcement matters when an admin removes permissions from Everyone
+or creates restricted groups. Without these route-level gates, removing
+`model.use` from Everyone did nothing — users could still call completions.
+
+### New Tests
+
+12 test functions in `perm_enforcement_test.go`:
+
+| Test | Permission | Pattern |
+|------|-----------|---------|
+| `TestPermEnforce_ModelUse_Completions` | `model.use` | deny → grant → allow |
+| `TestPermEnforce_ChannelCreate` | `channel.create` | deny → grant → allow |
+| `TestPermEnforce_ChannelInvite` | `channel.invite` | deny → grant → allow |
+| `TestPermEnforce_KBCreate` | `kb.create` | deny → grant → allow |
+| `TestPermEnforce_KBSearch` | `kb.read` | deny → grant → allow |
+| `TestPermEnforce_KBWrite` | `kb.write` | deny → grant → allow |
+| `TestPermEnforce_PersonaCreate` | `persona.create` | deny → grant → allow |
+| `TestPermEnforce_PersonaManage` | `persona.manage` | deny → grant → allow |
+| `TestPermEnforce_WorkflowCreate` | `workflow.create` | deny → grant → allow |
+| `TestPermEnforce_TaskCreate` | `task.create` | deny → grant → allow |
+| `TestPermEnforce_ProfilePermissions_NoGroups` | — | empty perms |
+| `TestPermEnforce_ProfilePermissions_WithGrant` | — | granted perms appear |
+| `TestPermEnforce_ProfilePermissions_Admin` | — | admin gets all |
+
+### Not Covered (test harness limitation)
+
+Summarize and GenerateTitle routes are not in the integration test harness
+(`setupHarness`). They share `model.use` with completions, which IS tested.
+Route-level wiring is applied in main.go. Full coverage comes when the
+test harness is extended to include these routes.
diff --git a/UI redesign.md b/UI redesign.md
new file mode 100644
index 0000000..c4a4d7e
--- /dev/null
+++ b/UI redesign.md
@@ -0,0 +1,685 @@
+# Chat Switchboard UI Rewrite — Design Document
+
+**Version:** 0.0.1 (draft)
+**Date:** 2026-03-20
+**Scope:** Scorched earth rebuild of frontend on Preact+htm
+
+---
+
+## 1. Survey Findings
+
+### What Exists
+
+| Metric | Value |
+|--------|-------|
+| JS files | 55 |
+| Total JS lines | ~29,350 |
+| CSS files | 22 (~203KB) |
+| Go templates | ~1,920 lines across 14 template files |
+| `innerHTML =` assignments | 294+ (91 in ui-admin.js alone) |
+| `querySelector` / `getElementById` calls | 600+ (130 in ui-admin.js) |
+| Direct `fetch()` outside api.js | ~24 calls across 10 files |
+| Vendor deps | marked, DOMPurify, mermaid, KaTeX, CM6 (esbuild bundle) |
+
+### The SDK (switchboard-sdk.js)
+
+- ~860 lines, version-tagged `v0.28.5` in code but `v0.30.2` in docs
+- Thin wrapper over globals: `API`, `Events`, `Theme`, `UserMenu`
+- Delegates to `API._get()`, `API._post()`, etc. — not its own fetch layer
+- Has pipe/filter pipeline (pre-send, stream, render) — this is good and reusable
+- Has event bus bridge — good, reusable
+- Has theme control — good, reusable
+- No RBAC. Only `sw.isAdmin` (boolean derived from `API.user.role`)
+- No namespaced domain methods (e.g., `sw.api.channels.list()`)
+
+### The Registry (sb.js)
+
+- Action registry: `sb.register(name, fn)` + `sb.ns(name, obj)`
+- Dual-write to `window[name]` for backward compat
+- Go templates call `sb.call('action', args)` via onclick handlers
+- 55 files all register into this flat global space
+
+### Server Templates
+
+- Go templates generate initial DOM with hardcoded IDs
+- JS hydrates by ID lookups (`getElementById`, `querySelector`)
+- Templates inject `window.__USER__`, `window.__PAGE_DATA__`, `window.__SURFACE__`
+- This server-rendering + client-hydration pattern is deeply coupled
+
+### What's Missing for RBAC
+
+- **No user-facing permissions endpoint.** `GET /admin/users/:id/permissions`
+ is admin-only. There is no `GET /profile/permissions` or equivalent. The
+ login/refresh response includes only `role: "user"|"admin"`, not the
+ resolved permission set.
+- The frontend has zero permission-level gating. Everything is gated by
+ `API.isAdmin` (binary) or by server-side template conditionals.
+
+### Dependency Pipeline
+
+- `Dockerfile.frontend`: Stage 1 `npm pack` pulls marked, DOMPurify,
+ mermaid, KaTeX. Stage 2 builds CM6 via esbuild. Stage 3 copies into
+ nginx image.
+- Preact + htm would be added to Stage 1 as additional `npm pack` targets.
+- No bundler for app code — scripts loaded sequentially via `
+```
+
+Application files continue to load as `