From e7d1b53ebf25f5caeca52671b86682894a956d10 Mon Sep 17 00:00:00 2001 From: Jeffrey Smith Date: Wed, 1 Apr 2026 16:36:43 +0000 Subject: [PATCH] Feat v0.6.17 bugfixes (#52) Co-authored-by: Jeffrey Smith Co-committed-by: Jeffrey Smith --- CHANGELOG.md | 36 +++++++++++++++++++++++++++++ VERSION | 2 +- packages/notes/js/main.js | 2 +- server/handlers/packages.go | 4 ++++ server/handlers/packages_bundled.go | 21 +++++++---------- server/pages/pages.go | 4 ++++ server/pages/pages_surfaces.go | 22 ++++++++++++++++++ src/css/surfaces.css | 2 +- src/css/sw-primitives.css | 5 ++-- src/js/sw/sdk/index.js | 19 ++++++++++++++- src/js/sw/shell/user-menu.js | 22 ++++++++++-------- src/js/sw/surfaces/admin/teams.js | 2 +- 12 files changed, 111 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89323dd..358bd9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to Armature are documented here. +## v0.6.17 — Bug Fixes & Welcome Logic + +Fixes broken UI interactions (folder creation, team member add), dropdown +overflow, welcome surface auto-disable, and bare-install default behavior. + +### Fixed + +- **Notes "Add folder" button** — `prompt()` replaced with `sw.prompt()` + so the dialog renders correctly in the extension iframe sandbox. +- **Admin "Add team members"** — user list API returns `{data:[…]}`; handler + now unwraps the envelope (`Array.isArray(u) ? u : u.data`) so the user + picker populates. +- **Package filter dropdown overflow** — removed `right:0` constraint on + `.sw-dropdown__list`, added `min-width:max-content` and `overflow-x:hidden` + so option labels ("Extension", "Workflow") render fully without a scrollbar. +- **Admin actions cell wrapping** — switched `.admin-actions-cell` from + `white-space:nowrap` to flexbox with `flex-wrap:wrap; gap:4px` so buttons + don't overflow on narrow viewports. + +### Changed + +- **Welcome surface auto-disable** — welcome page now redirects to `/` when + any non-core extension surface is installed. Removed from the topbar + navigation surface list so it never appears alongside real surfaces. +- **Zero default bundled packages** — `defaultBundledPackages` map is now + empty. Fresh installs start bare; use `BUNDLED_PACKAGES` env var to control + what gets auto-installed per environment (`*` for all, comma-separated list + for selective). +- **Bundled filter logic** — `nil` (from `*`) means install all; empty map + (default) means install nothing. Previous code treated both as "install all". +- **User menu conditional items** — Docs, Settings, and Team Admin menu + entries only appear when those surfaces are actually enabled, not assumed. +- **SDK imperative host mount** — `ToastContainer` and `DialogStack` are now + auto-mounted by the SDK boot sequence for extension surfaces that lack an + AppShell, preventing missing toast/dialog hosts. + ## v0.6.16 — Usability Survey Gate Machine-auditable UI quality gate. Four new audit scripts, a structured survey diff --git a/VERSION b/VERSION index 6769f67..fa20946 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.15 +0.6.17 diff --git a/packages/notes/js/main.js b/packages/notes/js/main.js index 2429d8e..86c0744 100644 --- a/packages/notes/js/main.js +++ b/packages/notes/js/main.js @@ -1622,7 +1622,7 @@ setShowUnfiled(true); } async function handleCreateFolder(parentId) { - var name = prompt('Folder name:'); + var name = await sw.prompt('Folder name:'); if (!name || !name.trim()) return; try { await api.post('/folders', { name: name.trim(), parent_id: parentId || '' }); diff --git a/server/handlers/packages.go b/server/handlers/packages.go index 3563e62..2fb5d25 100644 --- a/server/handlers/packages.go +++ b/server/handlers/packages.go @@ -747,6 +747,10 @@ func (h *PackageHandler) ListEnabledSurfaces(c *gin.Context) { if p.Type != "surface" && p.Type != "full" { continue } + // Welcome is a fallback surface, not a navigable destination + if p.ID == "welcome" { + continue + } route, _ := p.Manifest["route"].(string) icon, _ := p.Manifest["icon"].(string) enabled = append(enabled, navSurface{ diff --git a/server/handlers/packages_bundled.go b/server/handlers/packages_bundled.go index 7b342eb..63b447e 100644 --- a/server/handlers/packages_bundled.go +++ b/server/handlers/packages_bundled.go @@ -22,18 +22,12 @@ import ( "armature/triggers" ) -// defaultBundledPackages is the curated set of packages installed by default. -// Other packages still ship in the Docker image but require BUNDLED_PACKAGES -// to be set explicitly (or "*" for all). -// -// Recommended production override: BUNDLED_PACKAGES=notes,chat,chat-core,mermaid-renderer,schedules -var defaultBundledPackages = map[string]bool{ - "notes": true, - "chat": true, - "chat-core": true, - "mermaid-renderer": true, - "schedules": true, -} +// defaultBundledPackages is empty — fresh installs start bare. +// Use BUNDLED_PACKAGES env var to control what gets installed per environment: +// dev: BUNDLED_PACKAGES=* +// test: BUNDLED_PACKAGES=notes,chat,chat-core +// prod: BUNDLED_PACKAGES=notes,chat,chat-core,mermaid-renderer,schedules +var defaultBundledPackages = map[string]bool{} // InstallBundledPackages scans bundledDir for .pkg archives and installs // any that don't already exist in the database. Called once at startup @@ -74,7 +68,8 @@ func InstallBundledPackages(bundledDir, packagesDir, allowlist string, stores st } // Check allowlist by filename (strip .pkg extension = package ID) - if len(allowed) > 0 { + // nil = install all ("*"), empty map = install nothing (empty default set) + if allowed != nil { pkgName := strings.TrimSuffix(entry.Name(), ".pkg") if !allowed[pkgName] { filtered++ diff --git a/server/pages/pages.go b/server/pages/pages.go index 75641d2..ed6e332 100644 --- a/server/pages/pages.go +++ b/server/pages/pages.go @@ -494,6 +494,10 @@ func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddlewar continue } handler := e.RenderSurface(s.ID) + // Welcome auto-redirects to / when extension surfaces exist + if s.ID == "welcome" { + handler = e.welcomeRedirectIfSurfaces(handler) + } registerRoutes(group, s, handler) } diff --git a/server/pages/pages_surfaces.go b/server/pages/pages_surfaces.go index f156564..59ce71b 100644 --- a/server/pages/pages_surfaces.go +++ b/server/pages/pages_surfaces.go @@ -176,6 +176,28 @@ func (e *Engine) surfacePath(ctx context.Context, id string) string { return "/s/" + id } +// welcomeRedirectIfSurfaces wraps a welcome surface handler so that +// /welcome auto-redirects to / when any extension surface is installed. +// This means the welcome page only renders for truly empty deployments. +func (e *Engine) welcomeRedirectIfSurfaces(inner gin.HandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + if e.stores.Packages != nil { + if pkgs, err := e.stores.Packages.List(c.Request.Context()); err == nil { + for _, p := range pkgs { + if p.Source == "core" || !p.Enabled { + continue + } + if p.Type == "surface" || p.Type == "full" { + c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/") + return + } + } + } + } + inner(c) + } +} + // disabledRedirect returns a handler that redirects to /. // The DefaultSurfaceRedirect handler will resolve to the appropriate surface. func (e *Engine) disabledRedirect() gin.HandlerFunc { diff --git a/src/css/surfaces.css b/src/css/surfaces.css index fe55767..3c30eaa 100644 --- a/src/css/surfaces.css +++ b/src/css/surfaces.css @@ -288,7 +288,7 @@ .admin-user-name { font-weight: 600; font-size: 13px; } .admin-user-handle { font-size: 11px; color: var(--text-3); margin-top: 1px; } .admin-user-time { color: var(--text-3); font-size: 12px; font-family: var(--mono); } -.admin-actions-cell { white-space: nowrap; text-align: right; } +.admin-actions-cell { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; } /* ── Provider Cards ──────────────────────── */ .provider-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--sp-4); } diff --git a/src/css/sw-primitives.css b/src/css/sw-primitives.css index 7ccf6b2..5c07ea7 100644 --- a/src/css/sw-primitives.css +++ b/src/css/sw-primitives.css @@ -274,8 +274,9 @@ .sw-dropdown__value { flex: 1; text-align: left; } .sw-dropdown__chevron { color: var(--text-3); font-size: 0.7em; } .sw-dropdown__list { - position: absolute; top: calc(100% + 4px); left: 0; right: 0; - z-index: 6000; max-height: 240px; overflow-y: auto; + position: absolute; top: calc(100% + 4px); left: 0; + min-width: max-content; + z-index: 6000; max-height: 240px; overflow-y: auto; overflow-x: hidden; background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-lg); animation: sw-fade-in 0.1s ease; diff --git a/src/js/sw/sdk/index.js b/src/js/sw/sdk/index.js index 0ebefa5..bed8c81 100644 --- a/src/js/sw/sdk/index.js +++ b/src/js/sw/sdk/index.js @@ -193,7 +193,24 @@ export async function boot() { events.connect(BASE + '/ws'); } - // 10. Signal ready + // 10. Mount imperative hosts (Toast, DialogStack) if not already present. + // Core surfaces mount these via AppShell; extension surfaces need SDK to do it. + try { + const hostId = 'sw-imperative-hosts'; + if (!document.getElementById(hostId)) { + const mount = document.createElement('div'); + mount.id = hostId; + document.body.appendChild(mount); + const { ToastContainer } = await import('../primitives/toast.js'); + const { DialogStack } = await import('../shell/dialog-stack.js'); + const { render } = preact; + render(html`<${ToastContainer} /><${DialogStack} />`, mount); + } + } catch (e) { + console.warn('[sw] Imperative host mount failed:', e.message); + } + + // 11. Signal ready events.emit('sdk.ready', {}, { localOnly: true }); document.dispatchEvent(new CustomEvent('sw:ready', { detail: { sw } })); console.log(`[sw] SDK v${sw._sdk} ready`); diff --git a/src/js/sw/shell/user-menu.js b/src/js/sw/shell/user-menu.js index 4e0e99c..9ef31e7 100644 --- a/src/js/sw/shell/user-menu.js +++ b/src/js/sw/shell/user-menu.js @@ -27,7 +27,7 @@ function _currentSurface() { export function UserMenu({ placement = 'down-right', onAction, extraItems }) { const [open, setOpen] = useState(false); - const [extSurfaces, setExtSurfaces] = useState([]); + const [allSurfaces, setAllSurfaces] = useState([]); const btnRef = useRef(null); const sw = window.sw; @@ -35,17 +35,19 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) { const authenticated = sw?.auth?.isAuthenticated; // Fetch installed surfaces once on mount. - // Filter out core surfaces that have dedicated menu entries below. - const CORE_IDS = new Set(['admin', 'settings', 'team-admin', 'workflow', 'workflow-landing', 'docs']); + const CORE_IDS = new Set(['admin', 'settings', 'team-admin', 'workflow', 'workflow-landing', 'docs', 'welcome']); useEffect(() => { if (!sw?.api?.surfaces?.list) return; sw.api.surfaces.list().then(data => { const raw = Array.isArray(data) ? data : data?.data || []; - setExtSurfaces(raw.filter(s => !CORE_IDS.has(s.id))); + setAllSurfaces(raw); }).catch(() => {}); }, [authenticated]); + const extSurfaces = allSurfaces.filter(s => !CORE_IDS.has(s.id)); + const enabledIds = new Set(allSurfaces.map(s => s.id)); + const items = useMemo(() => { const current = _currentSurface(); const list = []; @@ -68,13 +70,13 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) { } // ── Standard items ───────────────────── - // Docs — skip if current surface IS docs - if (current !== 'docs') { + // Docs — only if enabled and not current + if (current !== 'docs' && enabledIds.has('docs')) { list.push({ label: 'Docs', action: 'docs', icon: '\ud83d\udcd6' }); } - // Settings — skip if current surface IS settings - if (current !== 'settings') { + // Settings — only if enabled and not current + if (current !== 'settings' && enabledIds.has('settings')) { list.push({ label: 'Settings', action: 'settings', icon: '\u2699\ufe0f' }); } @@ -85,7 +87,7 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) { // Team Admin — requires admin role on at least one team const hasTeamAdmin = sw?.auth?.teams?.some(t => t.my_role === 'admin'); - if (authenticated && hasTeamAdmin && current !== 'team-admin') { + if (authenticated && hasTeamAdmin && current !== 'team-admin' && enabledIds.has('team-admin')) { list.push({ label: 'Team Admin', action: 'team-admin', icon: '\ud83d\udc65' }); } @@ -98,7 +100,7 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) { list.push({ label: 'Sign Out', action: 'sign-out' }); return list; - }, [user, authenticated, extraItems, extSurfaces]); + }, [user, authenticated, extraItems, extSurfaces, enabledIds]); function handleSelect(action) { setOpen(false); diff --git a/src/js/sw/surfaces/admin/teams.js b/src/js/sw/surfaces/admin/teams.js index 8cf61d4..7774c69 100644 --- a/src/js/sw/surfaces/admin/teams.js +++ b/src/js/sw/surfaces/admin/teams.js @@ -33,7 +33,7 @@ export default function TeamsSection() { sw.api.admin.users.list(), ]); setMembers(m || []); - const uList = u || []; + const uList = Array.isArray(u) ? u : (u && u.data) || []; setAllUsers(uList); } catch (e) { sw.toast(e.message, 'error'); } }