diff --git a/CHANGELOG.md b/CHANGELOG.md index bd23ba0..fd9c18c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## [0.37.19.0] — 2026-03-24 + +### Summary + +Tag release — "UI Complete." Closes all remaining code review items from +FE-REWRITE-REVIEW-0.37.14.md. Adds client-side RBAC gates (`sw.can()`) to +surface action buttons, migrates settings policy reads from `__PAGE_DATA__` +to `sw.auth.policies`, removes dead `__USER__` / `__PAGE_DATA__` template +injections, and moves shell-coupled `_getScale()` into the SDK. Light mode +CSS audit and dead code sweep confirm no outstanding issues. + +### Changed + +- **sw.can() RBAC gates (CR P2-1):** Channel/group creation buttons in chat + dashboard and sidebar gated behind `sw.can('channel.create')`. Persona + create/edit/delete gated behind `sw.can('persona.create')` / + `sw.can('persona.manage')`. Knowledge base create/upload/delete gated + behind `sw.can('kb.create')` / `sw.can('kb.write')`. + (`chat-workspace.js`, `sidebar.js`, `personas.js`, `knowledge.js`) +- **Settings → sw.auth.policies (CR P2-4):** Settings surface reads BYOK and + persona policies from `sw.auth.policies` (populated at SDK boot via + `/api/v1/profile/permissions`) instead of `window.__PAGE_DATA__`. Listens + for `auth.permissions.changed` event to react to policy updates. + (`settings/index.js`) +- **_getScale → sw.shell.getScale() (CR P3-3):** Scale detection for CSS zoom + compensation moved from sidebar-chats.js local function into SDK shell + namespace. Explicit coupling replaces implicit `#surfaceInner` DOM reach. + (`sdk/index.js`, `sidebar-chats.js`) +- **SDK version:** `sw._sdk` updated to `'0.37.19'`. + +### Removed + +- **__USER__ / __PAGE_DATA__ injection (CR P2-4):** Removed `window.__USER__` + and `window.__PAGE_DATA__` global injections from `base.html`. SDK boot + populates `sw.auth.user` and `sw.auth.policies` from API. Extension packages + updated to use SDK (`editor`, `hello-dashboard`, `icd-test-runner`). + (`base.html`, `loaders.go`, `EXTENSION-SURFACES.md`) +- **SettingsPageData policy fields:** `BYOKEnabled` and `UserPersonasEnabled` + removed from Go struct (no longer injected via template). (`loaders.go`) + +### Documentation + +- **CR P3-17:** Added comment documenting intentional raw DOM usage in notes + markdown wikilink processing (`markdown.js`). +- **CR P3-19:** Added comment documenting keyboard shortcut DOM presence check + pattern with future guidance (`use-notes.js`). +- **EXTENSION-SURFACES.md:** Updated `__USER__` references to `sw.auth.user`. + +--- + ## [0.37.18.0] — 2026-03-24 ### Summary diff --git a/FE-REWRITE-REVIEW-0.37.14.md b/FE-REWRITE-REVIEW-0.37.14.md index 72632c8..caefb32 100644 --- a/FE-REWRITE-REVIEW-0.37.14.md +++ b/FE-REWRITE-REVIEW-0.37.14.md @@ -122,9 +122,9 @@ The CI syntax check globs `src/js/*.js` (top-level only), missing all 115 files ## P2 — Track for Next Changeset -### 9. `sw.can()` is essentially unused +### 9. `sw.can()` is essentially unused — **RESOLVED v0.37.19** -Only 1 reference in all surface code (`team-admin/index.js` uses `sw.isAdmin`, not `sw.can()`). No surface gates create/edit/delete buttons with `sw.can('kb.write')`, `sw.can('channel.create')`, etc. The backend permission enforcement from CS-0.37.1 has no client-side counterpart. +~~Only 1 reference in all surface code (`team-admin/index.js` uses `sw.isAdmin`, not `sw.can()`). No surface gates create/edit/delete buttons with `sw.can('kb.write')`, `sw.can('channel.create')`, etc. The backend permission enforcement from CS-0.37.1 has no client-side counterpart.~~ The settings surface does its own policy resolution from `__PAGE_DATA__` + a separate API fallback, rather than reading `sw.auth.policies` which is already cached from `/profile/permissions`. @@ -148,11 +148,11 @@ The `useEffect` that sets `handleRef.current = {...}` has no dependency array, r **File:** `src/js/sw/components/chat-pane/index.js`, line 66 -### 12. `__USER__` / `__PAGE_DATA__` injection is mostly dead +### 12. `__USER__` / `__PAGE_DATA__` injection is mostly dead — **RESOLVED v0.37.19** -The Go template still injects `window.__USER__` and `window.__PAGE_DATA__` on every page load. The new SDK boots from API calls. Only `settings/index.js` reads `__PAGE_DATA__` (for BYOK/persona policies) and it already has an API fallback. `window.__USER__` is never read by new code. +~~The Go template still injects `window.__USER__` and `window.__PAGE_DATA__` on every page load. The new SDK boots from API calls. Only `settings/index.js` reads `__PAGE_DATA__` (for BYOK/persona policies) and it already has an API fallback. `window.__USER__` is never read by new code.~~ -**Recommendation:** Remove `__USER__` injection. Migrate settings to use `sw.auth.policies` (see #9), then remove `__PAGE_DATA__` injection. +~~**Recommendation:** Remove `__USER__` injection. Migrate settings to use `sw.auth.policies` (see #9), then remove `__PAGE_DATA__` injection.~~ ### 13. Debug modal is old-world code @@ -178,11 +178,11 @@ The debug modal in `base.html` (~50 lines of raw HTML with inline `onclick` hand **File:** `src/js/sw/surfaces/admin/users.js`, line 7 -### 16. `sidebar-chats.js` reaches into `#surfaceInner` +### 16. `sidebar-chats.js` reaches into `#surfaceInner` — **RESOLVED v0.37.19** -`_getScale()` reads `document.getElementById('surfaceInner')` to compensate for CSS zoom on `position: fixed` menus. Technically correct but creates an implicit coupling between a surface component and the shell's DOM structure. +~~`_getScale()` reads `document.getElementById('surfaceInner')` to compensate for CSS zoom on `position: fixed` menus. Technically correct but creates an implicit coupling between a surface component and the shell's DOM structure.~~ -**Recommendation:** Move scale detection into the SDK (e.g., `sw.shell.getScale()`) so the coupling is explicit. +~~**Recommendation:** Move scale detection into the SDK (e.g., `sw.shell.getScale()`) so the coupling is explicit.~~ ### 17. Notes markdown rendering uses raw DOM diff --git a/VERSION b/VERSION index fb9f0e5..5a0aaa6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.37.15.0 +0.37.19.0 diff --git a/docs/EXTENSION-SURFACES.md b/docs/EXTENSION-SURFACES.md index 8488f4f..539a71a 100644 --- a/docs/EXTENSION-SURFACES.md +++ b/docs/EXTENSION-SURFACES.md @@ -81,7 +81,7 @@ mounts into the `#extension-mount` container: if (!mount) return; var manifest = window.__MANIFEST__ || {}; - var user = window.__USER__ || {}; + var user = window.sw?.auth?.user || {}; mount.innerHTML = '

Hello, ' + esc(user.display_name || user.username) + '!

'; @@ -115,7 +115,7 @@ available when your script executes: | Global | Type | Description | |--------|------|-------------| | `window.__MANIFEST__` | Object | Your surface's manifest with route, layout, etc. Keys are **lowercase** (JSON serialization from Go). Access as `manifest.id`, `manifest.route`, `manifest.title`. | -| `window.__USER__` | Object | Authenticated user: `{ username, display_name, role, id, email }`. | +| `window.sw.auth.user` | Object | Authenticated user (via SDK): `{ username, display_name, role, id, email }`. Requires SDK boot. | | `window.__BASE__` | String | URL base path (e.g. `"/dev"` or `""`). Prepend to all internal links. | | `window.__VERSION__` | String | Platform version string. | | `window.__SURFACE__` | String | Your surface ID. | @@ -280,7 +280,7 @@ When your surface loads, the page DOM looks like this: - + ... platform scripts ... @@ -447,7 +447,7 @@ working example that demonstrates: - Manifest structure - Mounting into `#extension-mount` -- Reading `__MANIFEST__` and `__USER__` +- Reading `__MANIFEST__` and `sw.auth.user` - Using `UI.toast()` for notifications - Using `API._get()` for authenticated requests - Using CSS custom properties for theming diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index d865199..62d7639 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -52,7 +52,7 @@ v0.9.x–v0.28.7 Foundation through Platform Polish ✅ │ v0.37.4 Shell ✅ │ │ v0.37.5–16 Surfaces ✅ │ │ v0.37.17–18 Surfaces ✅ │ - │ v0.37.19 Tag │ + │ v0.37.19 Tag ✅ │ │ │ │ v0.38.0–.4 │ │ Extension │ │ @@ -95,11 +95,12 @@ v0.9.x–v0.28.7 Foundation through Platform Polish ✅ --- -## UI Rewrite Track +## UI Rewrite Track ✅ Scorched earth rebuild of the frontend on Preact+htm. Three-layer architecture (Primitives → SDK → Shell) replacing the imperative DOM manipulation codebase. See `UI redesign.md` for full design doc. +**Complete as of v0.37.19.** Depends on: v0.36.0 (OpenAPI spec), v0.31.2 (SDK composability). @@ -204,7 +205,7 @@ Each surface rebuilt as Preact component tree. Old JS deleted per surface. | v0.37.16 | Projects surface ✅ | Card grid + detail, inline ChatPane, context menu, sidebar pickers, deep-link | | v0.37.17 | Workspaces ✅ | Workspace-first project file storage, tree browser UI, archive upload/download | | v0.37.18 | Debug / model surface ✅ | Debug modal Preact rebuild (CR P2-5), default workspace, tool_output auto-save, save-to-workspace bridge, model/provider polish | -| v0.37.19 | Tag | Light mode CSS audit, dead code hunt, all features verified; `sw.can()` RBAC gates across surfaces (CR P2-1), `__USER__`/`__PAGE_DATA__` removal (CR P2-4), `_getScale` → `sw.shell.getScale()` (CR P3-3) | +| v0.37.19 | Tag ✅ | `sw.can()` RBAC gates (CR P2-1), `__USER__`/`__PAGE_DATA__` removal (CR P2-4), `_getScale` → `sw.shell.getScale()` (CR P3-3), light mode CSS audit, dead code sweep | **v0.37.14 — Scorched Earth IV + Pane Audit ✅** (10 sessions, v0.37.14.0–.22): @@ -255,10 +256,14 @@ auto-save (file refs linked to assistant messages). Save-to-workspace bridge (save code blocks to workspace). Model selector health dots. Provider test buttons (BYOK + admin). repl.js absorbed into Preact. sw-debug.css extracted. -**v0.37.19 — Tag ("UI Complete"):** +**v0.37.19 — Tag ("UI Complete") ✅:** -`sw.can()` RBAC gates, `__USER__`/`__PAGE_DATA__` removal, `_getScale` SDK, -light mode CSS audit, dead code hunt. Every capability has a UI. +`sw.can()` RBAC gates on chat, settings, and KB surfaces (CR P2-1). Settings +policies migrated from `__PAGE_DATA__` to `sw.auth.policies`; `__USER__` and +`__PAGE_DATA__` injections removed from `base.html` (CR P2-4). `_getScale()` +moved to `sw.shell.getScale()` SDK (CR P3-3). Light mode CSS audit clean. +Dead code sweep clean. P3-17/P3-19 documented as intentional. Every +capability has a UI. --- diff --git a/packages/editor/js/main.js b/packages/editor/js/main.js index 47876dd..948d9e9 100644 --- a/packages/editor/js/main.js +++ b/packages/editor/js/main.js @@ -193,7 +193,7 @@ const name = window.prompt('Workspace name:'); if (!name) return; try { - const userId = sw.user?.id || window.__USER__?.id; + const userId = sw.user?.id; if (!userId) throw new Error('Not authenticated'); const resp = await API.createWorkspace({ name: name.trim(), owner_type: 'user', owner_id: userId }); const newId = resp.id || resp.data?.id; @@ -264,7 +264,7 @@ btn.disabled = true; btn.textContent = 'Creating\u2026'; try { - const userId = sw.user?.id || window.__USER__?.id; + const userId = sw.user?.id; if (!userId) throw new Error('Not authenticated'); const resp = await API.createWorkspace({ name, owner_type: 'user', owner_id: userId }); const newId = resp.id || resp.data?.id; diff --git a/packages/hello-dashboard/js/main.js b/packages/hello-dashboard/js/main.js index 6abde2a..0f88eef 100644 --- a/packages/hello-dashboard/js/main.js +++ b/packages/hello-dashboard/js/main.js @@ -4,7 +4,7 @@ * Platform contract: * - Mounts into #extension-mount * - window.__MANIFEST__ — surface manifest (json, lowercase keys) - * - window.__USER__ — authenticated user {username, display_name, role} + * - sw.auth.user — authenticated user (from SDK boot) * - UI.toast(msg, type) — platform toast (success/error/info/warning) * - API._get(path) — authenticated fetch (returns parsed JSON) */ @@ -15,7 +15,7 @@ if (!mount) return; var manifest = window.__MANIFEST__ || {}; - var user = window.__USER__ || {}; + var user = window.sw?.auth?.user || {}; var isDark = document.documentElement.getAttribute('data-theme') === 'dark'; var name = user.display_name || user.username || 'World'; diff --git a/packages/icd-test-runner/js/main.js b/packages/icd-test-runner/js/main.js index 55d413d..3bfe7b3 100644 --- a/packages/icd-test-runner/js/main.js +++ b/packages/icd-test-runner/js/main.js @@ -53,7 +53,7 @@ window.ICD = { mount: mount, manifest: window.__MANIFEST__ || {}, - user: window.sw?.auth?.user || window.__USER__ || {}, + user: window.sw?.auth?.user || {}, base: window.__BASE__ || '', // State (populated by framework.js) diff --git a/server/handlers/channels.go b/server/handlers/channels.go index 02b951c..4e6214b 100644 --- a/server/handlers/channels.go +++ b/server/handlers/channels.go @@ -312,6 +312,7 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) { if err := h.stores.Channels.CreateFull(ctx, ch, req.Folder, req.Tags, aiMode, userID, dmPartners, req.Model, defaultConfigID); err != nil { + log.Printf("[channels] CreateFull error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create channel"}) return } diff --git a/server/pages/loaders.go b/server/pages/loaders.go index 6510266..d389e12 100644 --- a/server/pages/loaders.go +++ b/server/pages/loaders.go @@ -108,13 +108,9 @@ type NotesPageData struct { } // SettingsPageData is what the settings surface templates receive. +// Feature gates (BYOK, personas) moved to sw.auth.policies in v0.37.19. type SettingsPageData struct { Section string `json:"section"` - - // v0.22.7: Feature gates from admin config. - // These control which nav links/tabs are visible in the settings surface. - BYOKEnabled bool `json:"BYOKEnabled"` - UserPersonasEnabled bool `json:"UserPersonasEnabled"` } // ── Loader registration ────────────────────── @@ -422,20 +418,7 @@ func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) { section = "general" } - data := &SettingsPageData{Section: section} - - // Read feature gates from policies (where admin settings saves them). - // Keys: allow_user_byok, allow_user_personas - if s.Policies != nil { - ctx := context.Background() - policies, err := s.Policies.GetAll(ctx) - if err == nil { - data.BYOKEnabled = policies["allow_user_byok"] == "true" - data.UserPersonasEnabled = policies["allow_user_personas"] == "true" - } - } - - return data, nil + return &SettingsPageData{Section: section}, nil } func (e *Engine) projectsLoader(c *gin.Context, s store.Stores) (any, error) { diff --git a/server/pages/templates/base.html b/server/pages/templates/base.html index 9d4706a..b808b4c 100644 --- a/server/pages/templates/base.html +++ b/server/pages/templates/base.html @@ -121,8 +121,8 @@ window.__ENV__ = '{{.Environment}}'; window.__BRANDING__ = {}; window.__SURFACE__ = '{{.Surface}}'; - window.__PAGE_DATA__ = {{.Data | toJSON}}; - window.__USER__ = {{.User | toJSON}}; + {{/* __PAGE_DATA__ and __USER__ removed in v0.37.19 — SDK boot + populates sw.auth.user and sw.auth.policies from API. */}} {{if .Manifest}}window.__MANIFEST__ = {{.Manifest | toJSON}};{{end}} diff --git a/src/css/sw-notes-surface.css b/src/css/sw-notes-surface.css index 21d45d1..7a25927 100644 --- a/src/css/sw-notes-surface.css +++ b/src/css/sw-notes-surface.css @@ -95,7 +95,7 @@ display: flex; } -.sw-notes-surface__search { +input.sw-notes-surface__search { width: 100%; padding: 6px 8px 6px 30px; background: var(--bg, #0e0e10); @@ -108,7 +108,7 @@ box-sizing: border-box; } -.sw-notes-surface__search:focus { +input.sw-notes-surface__search:focus { border-color: var(--accent, #b38a4e); } diff --git a/src/js/sw/components/notes-pane/markdown.js b/src/js/sw/components/notes-pane/markdown.js index ca32507..5482eeb 100644 --- a/src/js/sw/components/notes-pane/markdown.js +++ b/src/js/sw/components/notes-pane/markdown.js @@ -5,6 +5,11 @@ // then post-processes wikilinks into clickable chips // and transclusion blocks. // Independently importable. +// +// NOTE (CR P3-17): Uses raw DOM (innerHTML, querySelector, addEventListener) +// for post-render wikilink processing. This is intentional — Preact renders +// the initial HTML, then this module post-processes specific elements for +// interactive behavior. The lifecycle is managed by the parent component. const WIKILINK_RE = /(!?)\[\[([^\[\]|]+?)(?:\|([^\]]+?))?\]\]/g; diff --git a/src/js/sw/components/notes-pane/use-notes.js b/src/js/sw/components/notes-pane/use-notes.js index 2a4db80..b7c664c 100644 --- a/src/js/sw/components/notes-pane/use-notes.js +++ b/src/js/sw/components/notes-pane/use-notes.js @@ -257,6 +257,8 @@ export function useNotes(opts = {}) { toast('Note updated', 'success'); setEditorMode('read'); _setView('reader'); + // Notify sidebar to refresh folders/tags (folder or tags may have changed) + sw.emit('note.updated', { id: editingId }, { localOnly: true }); // Reload backlinks try { const bl = await api().backlinks(editingId); @@ -269,6 +271,8 @@ export function useNotes(opts = {}) { toast('Note created', 'success'); setEditorMode('read'); _setView('reader'); + // Notify sidebar to refresh folders/tags + sw.emit('note.created', { id: created.id }, { localOnly: true }); } graphDirtyRef.current = true; return true; @@ -289,6 +293,7 @@ export function useNotes(opts = {}) { setView('list'); refreshList(); _loadFolders(); + sw.emit('note.deleted', { id: editingId }, { localOnly: true }); } catch (e) { toast(e.message, 'error'); } }, [editingId, setView, refreshList, _loadFolders]); @@ -417,7 +422,9 @@ export function useNotes(opts = {}) { // Ctrl/Cmd + N: new note if ((e.ctrlKey || e.metaKey) && e.key === 'n') { - // Only prevent when notes pane is focused context + // DOM presence check gates shortcut to notes surface (CR P3-19). + // If notes and chat are ever composed on the same page, revisit + // with a focus-based check or sw.shell.activeSurface flag. if (document.querySelector('.sw-notes-pane')) { e.preventDefault(); newNote(); diff --git a/src/js/sw/sdk/index.js b/src/js/sw/sdk/index.js index 37f3e13..2da059c 100644 --- a/src/js/sw/sdk/index.js +++ b/src/js/sw/sdk/index.js @@ -97,6 +97,19 @@ export async function boot() { sw.confirm = confirm; sw.prompt = prompt; + // Shell — layout utilities (decouples surface code from shell DOM) + sw.shell = Object.freeze({ + /** CSS transform scale on #surfaceInner (appearance zoom). */ + getScale() { + const el = document.getElementById('surfaceInner'); + if (!el) return 1; + const t = getComputedStyle(el).transform; + if (!t || t === 'none') return 1; + const m = t.match(/matrix\(([^,]+)/); + return m ? parseFloat(m[1]) || 1 : 1; + }, + }); + // Toast — dynamic import to avoid module resolution issues try { const toastMod = await import('../primitives/toast.js'); @@ -139,7 +152,7 @@ export async function boot() { }; // Marker for idempotency - sw._sdk = '0.37.14'; + sw._sdk = '0.37.19'; // 8. Expose globally window.sw = sw; diff --git a/src/js/sw/surfaces/chat/chat-workspace.js b/src/js/sw/surfaces/chat/chat-workspace.js index 64da4a9..caf5343 100644 --- a/src/js/sw/surfaces/chat/chat-workspace.js +++ b/src/js/sw/surfaces/chat/chat-workspace.js @@ -172,7 +172,7 @@ function ChatDashboard({ onNewChat, onCreateChannel, items, onNavigate }) { const _onDmSelect = useCallback((user) => { setShowDmPicker(false); const name = user.display_name || user.username; - onCreateChannel('dm', 'DM: ' + name, { participant: user.username || user.id }); + onCreateChannel('dm', 'DM: ' + name, { participant: user.id }); }, [onCreateChannel]); return html` @@ -186,14 +186,16 @@ function ChatDashboard({ onNewChat, onCreateChannel, items, onNavigate }) { \u{1F4AC} New AI Chat + ${sw.can('channel.create') && html` + `} + ${sw.can('channel.create') && html` + `} `; } -// Read the CSS scale transform on .surface-inner (set by appearance zoom). -// Context menus use position:fixed which breaks under transforms, -// so we divide getBoundingClientRect values by scale to compensate. -function _getScale() { - const el = document.getElementById('surfaceInner'); - if (!el) return 1; - const t = getComputedStyle(el).transform; - if (!t || t === 'none') return 1; - const m = t.match(/matrix\(([^,]+)/); - return m ? parseFloat(m[1]) || 1 : 1; -} +// Scale detection moved to sw.shell.getScale() in v0.37.19 (CR P3-3). // Folder count = direct children only (subfolders + channels in this folder). diff --git a/src/js/sw/surfaces/chat/sidebar.js b/src/js/sw/surfaces/chat/sidebar.js index 89d7f88..357a83f 100644 --- a/src/js/sw/surfaces/chat/sidebar.js +++ b/src/js/sw/surfaces/chat/sidebar.js @@ -6,6 +6,7 @@ import { SidebarItems } from './sidebar-chats.js'; import { UserMenu } from '../../shell/user-menu.js'; +import { UserPicker } from '../../primitives/user-picker.js'; const html = window.html; const { useState, useCallback, useRef, useEffect } = window.hooks; @@ -50,6 +51,7 @@ const FOLDER_PLUS_SVG = html` */ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChannel, onDeleteChat, onCollapse }) { const [newMenuOpen, setNewMenuOpen] = useState(false); + const [showDmPicker, setShowDmPicker] = useState(false); const newMenuRef = useRef(null); const _onSearch = useCallback((e) => { @@ -82,14 +84,19 @@ export function Sidebar({ sidebar, activeId, onSelect, onNewChat, onCreateChanne if (type === 'direct') { onNewChat(); } else if (type === 'dm') { - const who = await window.sw.prompt('Username to DM:', ''); - if (who && who.trim()) onCreateChannel('dm', 'DM: ' + who.trim(), { participant: who.trim() }); + setShowDmPicker(true); } else { const name = await window.sw.prompt((type === 'channel' ? 'Channel' : 'Group') + ' name:', ''); if (name && name.trim()) onCreateChannel(type, name.trim()); } }, [onNewChat, onCreateChannel]); + const _onDmSelect = useCallback((user) => { + setShowDmPicker(false); + const name = user.display_name || user.username; + onCreateChannel('dm', 'DM: ' + name, { participant: user.id }); + }, [onCreateChannel]); + return html` + ${showDmPicker && html` +
+ <${UserPicker} + onSelect=${_onDmSelect} + placeholder="Search for a user\u2026" + autoFocus=${true} /> + +
`} + ${/* Search */''}
${SEARCH_SVG} diff --git a/src/js/sw/surfaces/settings/index.js b/src/js/sw/surfaces/settings/index.js index 915f0b2..683ea65 100644 --- a/src/js/sw/surfaces/settings/index.js +++ b/src/js/sw/surfaces/settings/index.js @@ -4,7 +4,9 @@ * Reads globals: * __SECTION__ — active section name (string) * __BASE__ — base path - * __PAGE_DATA__ — { BYOKEnabled, ... } from Go template + * + * 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. @@ -73,21 +75,21 @@ const SECTION_TITLES = { function SettingsSurface() { const BASE = window.__BASE__ || ''; const section = window.__SECTION__ || 'general'; - const pageData = window.__PAGE_DATA__ || {}; - const [byokEnabled, setByokEnabled] = useState(!!pageData.BYOKEnabled); - const [personasEnabled, setPersonasEnabled] = useState(!!pageData.UserPersonasEnabled); + 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); - // Fetch policies if not provided by template + // Update when permissions refresh (e.g. after boot or policy change) useEffect(() => { - if (!pageData.BYOKEnabled || !pageData.UserPersonasEnabled) { - sw.api.admin?.settings?.public?.().then(data => { - const policies = data?.policies || {}; - if (policies.allow_user_byok === 'true') setByokEnabled(true); - if (policies.allow_user_personas === 'true') setPersonasEnabled(true); - }).catch(() => {}); + 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 diff --git a/src/js/sw/surfaces/settings/knowledge.js b/src/js/sw/surfaces/settings/knowledge.js index 5e6fcf7..1dd3db8 100644 --- a/src/js/sw/surfaces/settings/knowledge.js +++ b/src/js/sw/surfaces/settings/knowledge.js @@ -101,6 +101,9 @@ export default function KnowledgeSection() { return html`${status}`; } + const canCreate = sw.can('kb.create'); + const canWrite = sw.can('kb.write'); + if (loading) return html`
Loading\u2026
`; // Detail view @@ -111,8 +114,8 @@ export default function KnowledgeSection() {

${detail.name}

${statusBadge(detail.status || 'active')}
- - + ${canWrite && html``} + ${canWrite && html``}
${detail.description && html` @@ -132,7 +135,7 @@ export default function KnowledgeSection() { ${' '}${statusBadge(d.status || 'pending')} ${fmtDate(d.created_at)} - + ${canWrite && html``} `)} @@ -145,9 +148,10 @@ export default function KnowledgeSection() {
${kbs.length} knowledge base${kbs.length !== 1 ? 's' : ''}
+ ${canCreate && html` + `}
${showCreate && html` diff --git a/src/js/sw/surfaces/settings/personas.js b/src/js/sw/surfaces/settings/personas.js index 919098f..032908d 100644 --- a/src/js/sw/surfaces/settings/personas.js +++ b/src/js/sw/surfaces/settings/personas.js @@ -85,10 +85,14 @@ export function PersonasSection() { return html`
Loading personas\u2026
`; } + const canCreate = sw.can('persona.create'); + const canManage = sw.can('persona.manage'); + return html` + ${canCreate && html`
-
+ `} ${showForm && html`
@@ -136,6 +140,7 @@ export function PersonasSection() {
`} + ${canManage && html`
-
+ `} `)}