Feat v0.7.0 shell contract + surface audit + rebrand

Two-slot shell topbar (home, left, center, bell, user menu) with SDK API
(setLeft/setSlot/setTitle/hide/show). All 4 primary surfaces migrated:
Settings and Team Admin to Pattern B (flat tabs), Admin to Pattern C
(category tabs + sidebar), Docs to Pattern A (default).

Backend WS events: package.changed (broadcast), auth.changed (targeted),
notification.all_read. User menu re-fetches on package/auth changes.
Bell syncs read state across tabs.

Error handling pass with .sw-inline-error CSS primitive. Empty state
guidance for Admin Workflows/Groups. Announcement global dismiss via
localStorage. Rebrand assets deployed (both b/e icon variants, wordmarks,
full icon library). Docs outline scroll-to-heading fix.

Bug fixes: ICD security assertion tightened, workflow-demo error surfacing,
signoff display names, hello-dashboard + team-admin/groups.js deleted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:08:57 +00:00
parent 1236220302
commit 1fbd836c5c
150 changed files with 3250 additions and 533 deletions

209
docs/AUDIT-surfaces.md Normal file
View File

@@ -0,0 +1,209 @@
# Surface Audit — Settings, Admin, Team Admin, Docs
## Audit Methodology
For each surface: read the index.js (tab/section structure), read every
section module, check the HTML template, trace the topbar/navigation
pattern, identify bugs, missing features, dead ends, and legacy baggage.
---
## 1. Settings Surface
**Files:** `src/js/sw/surfaces/settings/` (7 files, ~868 lines)
**Template:** `surfaces/settings.html` — mounts into `#settings-mount`
**Topbar:** Custom — back arrow + user icon + "Settings" title. No bell. No user menu.
### Sections
| Section | Lines | Status | Notes |
|---------|-------|--------|-------|
| General | 62 | ✅ OK | Default surface picker. Clean. |
| Appearance | 78 | ✅ OK | Theme toggle (light/dark/system) + UI scale slider. |
| Profile | 180 | ✅ OK | Display name, email, avatar upload, password change. |
| Teams | 47 | ⚠️ Thin | Read-only list of your teams. No actions. No link to team admin. |
| Connections | 222 | ✅ OK | Personal BYOK connection CRUD. Functional. |
| Notifications | 96 | ✅ OK | Toggle notification types on/off. |
### Issues Found
| # | Severity | Issue |
|---|----------|-------|
| S1 | **P1** | **No notification bell.** Custom topbar renders back arrow + icon + "Settings" — no bell, no user menu dropdown. User can't see notifications or navigate to other surfaces without using the back button. |
| S2 | **P2** | **Teams section is a dead-end.** Lists your teams with no actions — can't leave team, can't navigate to team admin, can't see team details. Just names. Should either link to team admin or show useful info. |
| S3 | **P2** | **Back button uses sessionStorage return URL.** `sb_settings_return` stash means: open settings in a new tab → back goes to `/` (correct). But open settings from a deep link → back goes to referrer, which might be unexpected. Shell topbar with consistent home link would fix this. |
| S4 | **P3** | **Extension config sections.** `__CONFIG_SECTIONS__` injection works but has no documentation. Extension authors don't know they can add settings sections. Needs a docs entry. |
### Shell Topbar Migration
Settings renders its own `settings-topbar`. With shell topbar injection:
- **Option A (simple):** `sw.shell.topbar.hide()` and keep custom topbar. Works immediately.
- **Option B (ideal):** Remove custom topbar. Shell topbar provides back + title + bell + user menu. Settings nav stays in the sidebar.
- **Recommendation:** Option B. Settings topbar adds nothing the shell topbar doesn't. The back arrow just navigates to `/`.
---
## 2. Admin Surface
**Files:** `src/js/sw/surfaces/admin/` (13 files, ~2,522 lines)
**Template:** `surfaces/admin.html` — mounts into `#admin-mount`
**Topbar:** Custom — favicon + "Administration" + category tabs (People/Workflows/System/Monitoring) + UserMenu component. No notification bell.
### Sections
| Section | Category | Lines | Status | Notes |
|---------|----------|-------|--------|-------|
| Users | People | 152 | ✅ OK | User list, create, edit status/role. Functional. |
| Teams | People | 178 | ✅ OK | Team list, create, member management. |
| Groups | People | 207 | ✅ OK | Full CRUD — create, delete, permission toggles, member add/remove. Functional but **undocumented** (see issues). |
| Workflows | Workflows | 163 | ⚠️ | CRUD + stage editor. `sw.api.workflows.list()` — needs same error-surfacing treatment as workflow-demo. |
| Settings | System | 242 | ✅ OK | Comprehensive: default surface, registration, banner, message bar, footer, session TTLs, vault, package registry, email test. Actually solid. |
| Storage | System | 76 | ✅ OK | Status cards, orphan cleanup. Clean. |
| Packages | System | 391 | ⚠️ | Core feature. Large. Package list, install, uninstall, registry browse. **User menu doesn't update after install/uninstall** (main bug Jeff reported). |
| Connections | System | 210 | ✅ OK | Global connection CRUD. |
| Broadcast | System | 44 | ✅ OK | Send broadcast. Minimal. |
| Backup | System | 162 | ✅ OK | Create/restore/download/delete. Works. |
| Health | Monitoring | 209 | ✅ OK | Runtime, DB pool, cluster, extension metrics. |
| Audit | Monitoring | 88 | ✅ OK | Audit log viewer with pagination. |
### Issues Found
| # | Severity | Issue |
|---|----------|-------|
| A1 | **P1** | **User menu not reactive to package changes.** `UserMenu` fetches surface list once on mount (`useEffect([authenticated])`). Installing/uninstalling a package doesn't trigger re-fetch. User must refresh the page to see new surfaces in the menu. Same for role changes (adding as team-admin). |
| A2 | **P1** | **No notification bell.** Admin topbar has category tabs + UserMenu but no NotificationBell component. |
| A3 | **P2** | **Groups: no documentation or inline help.** Admin Groups has full CRUD but zero explanation of what groups are, what permissions mean, or how the RBAC model works. "No groups" → user creates one → sees a list of permission slugs like `surface.admin.access` with no description. Every permission should have a human-readable description. |
| A4 | **P2** | **Workflows: silent error potential.** `sw.api.workflows.list()` — if this fails, `catch (e) { sw.toast(e.message, 'error'); }` fires a toast but leaves the list empty. Better than workflow-demo's silent swallow, but the toast disappears and the user is left with an empty list + no context. Should show inline error state. |
| A5 | **P2** | **Packages: no post-install feedback.** After installing a package, the package list refreshes (good) but the user menu doesn't update (bad — A1). User installs Notes, doesn't see it in the menu, thinks it's broken. |
| A6 | **P3** | **Admin topbar favicon is hardcoded.** Line 142: `<img src="${BASE}/favicon.svg">`. Should respect light/dark theme favicon swap. |
| A7 | **P3** | **Category icon rendering is fragile.** Custom compact SVG format (`C12 12 3\|M19.4 15...`) in `CatIcon`. Works but is unmaintainable — any icon change requires understanding the custom format. Should use standard SVG paths or lucide/feather icons. |
### Shell Topbar Migration
Admin has the most complex custom topbar — category tabs are genuinely useful navigation. Options:
- **Option A (recommended):** `sw.shell.topbar.hide()`. Admin keeps its custom topbar but adds NotificationBell component to its existing right-side area next to UserMenu.
- **Option B:** Shell topbar with `sw.shell.topbar.setSlot()` for category tabs. Works but requires rethinking the layout since shell topbar has fixed structure (home | title | slot | bell | user).
- **Recommendation:** Option A for v0.7.0. Admin's custom topbar is bespoke enough to warrant keeping. Just wire in the bell.
---
## 3. Team Admin Surface
**Files:** `src/js/sw/surfaces/team-admin/` (7 files, ~1,119 lines)
**Template:** `surfaces/team-admin.html` — mounts into `#team-admin-mount`
**Topbar:** Custom — back arrow + "Team Admin: {team name}" title. No bell. No user menu.
### Sections
| Section | Lines | Status | Notes |
|---------|-------|--------|-------|
| Members | ~90 | ✅ OK | Member list, add, remove. Functional. |
| Groups | 37 | ❌ Dead-end | Read-only "No groups" display. No create, no docs, no link to admin. |
| Connections | ~120 | ✅ OK | Team-scoped connections. Same pattern as user/admin connections. |
| Workflows | 723 | ⚠️ Massive | Three tabs: Workflows (CRUD + inline stage editor), Assignments (claim/release/complete), Monitor (active instances + signoff). This is 65% of the surface's code. |
| Settings | 72 | ✅ OK | Team name + description. Clean. |
| Activity | ~80 | ✅ OK | Audit log. Works. |
### Issues Found
| # | Severity | Issue |
|---|----------|-------|
| T1 | **P1** | **Groups is a dead-end.** 37 lines. Read-only list of team groups. No "Create Group" button. No explanation of what groups are. No link to Admin > Groups where creation actually happens. A team admin user who isn't a platform admin literally cannot create team groups. The Admin groups page supports `scope: team` but that creates a global group with team scope — it's unclear if team-admin should even see groups at all. |
| T2 | **P1** | **Workflows "Adopt Global" — same silent-error class.** `sw.api.teams.availableWorkflows(teamId)` — if this fails, the catch fires a toast but the adopt panel shows "No global workflows available" — indistinguishable from "there genuinely aren't any" vs "the API errored." |
| T3 | **P1** | **Workflows is disproportionately complex.** 723 lines — inline stage editor with mode/type selectors, SLA fields, stage reordering, team role assignment per stage. This is a full workflow designer embedded in a tab. It works but it's a maintenance burden and the UX is dense. Question: should this complexity live here or be a separate workflow-designer surface? |
| T4 | **P1** | **No notification bell.** Same as Settings — custom topbar with no bell. |
| T5 | **P2** | **No user menu.** Unlike Admin (which renders UserMenu), Team Admin has no user menu in its topbar. User can't navigate to other surfaces except via the back button. |
| T6 | **P2** | **Signoff panel shows raw user_id.** Line 714: `<span>${s.user_id}</span>` — shows UUID instead of display name. Should use `sw.users.displayName(s.user_id)`. |
| T7 | **P3** | **Back button uses sessionStorage.** Same pattern as Settings (`sb_team_admin_return`). Shell topbar would fix. |
### Shell Topbar Migration
Team Admin has a simple topbar (back + title). Direct replacement:
- Shell topbar provides: home link + "Team Admin: {name}" title + bell + user menu.
- Team name from `sw.api.teams.get(teamId)``sw.shell.topbar.setTitle('Team Admin: ' + team.name)`.
- Delete the custom topbar entirely.
---
## 4. Docs Surface
**Files:** `src/js/sw/surfaces/docs/` (1 file, 313 lines)
**Template:** `surfaces/docs.html` — mounts into `#docs-mount`
**Topbar:** Imports and renders `shell/topbar.js` (the SDK Topbar component). **Only surface that uses the shell Topbar.**
### Features
| Feature | Status | Notes |
|---------|--------|-------|
| Document list sidebar | ✅ OK | Fetches from `/api/v1/docs`, renders nav links. |
| Markdown rendering | ✅ OK | Uses `sw.markdown.renderSync()` + post-renderers (mermaid, katex). |
| Document outline | ✅ OK | Parses H1-H4 from markdown, renders table of contents. |
| Search | ✅ OK | Filters documents in sidebar. |
| URL updates | ✅ OK | `history.replaceState` on doc change. |
| Topbar | ✅ OK | Uses shell `Topbar` component — has title, bell, user menu. |
### Issues Found
| # | Severity | Issue |
|---|----------|-------|
| D1 | **P2** | **Stale content.** The docs themselves may be outdated — GETTING-STARTED, EXTENSION-GUIDE, API-REFERENCE, DEPLOYMENT, PACKAGE-FORMAT were written in v0.6.1. 18 versions later, some content is likely stale. Needs a content review pass. |
| D2 | **P3** | **No docs for RBAC/Groups.** Admin Groups exists with full CRUD but there's no documentation explaining the permission model, what each permission slug means, how groups interact with teams, or how the settings cascade works. This directly causes the "groups WTF" reaction. |
| D3 | **P3** | **No docs for Workflows.** The workflow engine is complex (multi-stage, team roles, signoff gates, SLA, public entry) but has no user-facing documentation. `DESIGN-WORKFLOWS.md` exists but is a design doc, not a user guide. |
| D4 | **P3** | **Shell topbar migration.** Docs already imports `shell/topbar.js` — when shell topbar injection lands, Docs will get a double topbar. Needs migration: delete the import, let shell topbar handle it. Docs currently passes no custom slot content, so it's a pure delete. |
---
## Cross-Surface Issues
These affect multiple or all surfaces:
| # | Severity | Issue | Surfaces |
|---|----------|-------|----------|
| X1 | **P0** | **User menu not reactive.** Package install/uninstall, role changes, team membership changes — none trigger a menu refresh. User must reload the page. | All (via UserMenu component) |
| X2 | **P1** | **No notification bell on 3/4 surfaces.** Only Docs has a bell (via Topbar import). Settings, Admin, and Team Admin all lack it. | Settings, Admin, Team Admin |
| X3 | **P1** | **No user menu on 2/4 surfaces.** Settings and Team Admin have no user menu at all. Admin and Docs have one. | Settings, Team Admin |
| X4 | **P2** | **Every surface has its own topbar.** Four different topbar implementations. None use the (not-yet-existing) shell topbar injection. Shell topbar (v0.7.0) eliminates this duplication. | All |
| X5 | **P2** | **Silent error swallowing.** Multiple sections use `catch (e) { toast }` which fires a toast and leaves an empty/stale UI. Toast disappears after seconds; user is left confused. Every list endpoint needs an inline error state with retry. | Admin Workflows, Team Admin Workflows, Packages |
| X6 | **P2** | **Empty states provide no guidance.** "No groups", "No workflows", "No notifications" — no explanation of what the feature is, why it's empty, or what action to take. Every empty state should have a one-line explanation and a primary action (create, link to docs, etc.). | Admin Groups, Team Admin Groups, Workflows |
| X7 | **P3** | **Raw IDs in UI.** Team Admin signoff panel shows `user_id` UUIDs. Any surface showing IDs should resolve via `sw.users.displayName()`. | Team Admin Workflows |
---
## Recommendations
### Immediate (fold into v0.7.0)
1. **User menu reactivity** — emit `package.changed` and `auth.changed` events over WS + local custom events. UserMenu listens and re-fetches surface list. This is the single most impactful fix.
2. **Shell topbar migration for Settings + Team Admin** — both have simple topbars that the shell topbar directly replaces. Docs deletes its Topbar import. Admin keeps its custom topbar but adds NotificationBell.
3. **Remove Team Admin Groups tab** — it's 37 lines of dead-end. Team-scoped group management should either (a) be added properly with create/edit/delete or (b) removed until it's properly designed. Showing "No groups" with no path forward is worse than not showing the tab.
4. **Error states** — replace `catch { toast }` with inline error + retry UI on every list endpoint. Systematic pass across all four surfaces.
5. **Empty state copy** — every "No X" message gets a one-line explanation + primary action button or doc link.
### Deferred (v0.7.1+ / runner coverage)
6. **Admin Groups documentation** — write a "Permissions & Groups" doc for the docs surface. Explain the RBAC model, list all permission slugs with descriptions, explain group scoping.
7. **Workflow user guide** — write a "Workflows" doc. Entry modes, stage types, team roles, signoff gates, SLA.
8. **Team Admin Workflows simplification** — the 723-line inline stage editor is the most complex piece of UI in the entire application. Consider extracting to a dedicated workflow-designer surface or at minimum breaking into separate files.
9. **Docs content refresh** — review all 5 docs for accuracy at v0.6.18+.
10. **Settings Teams section** — either add useful actions (link to team admin, show team role, leave team) or remove the tab.
---
## Asset Inventory
| Surface | Lines (total) | Sections | Custom Topbar | Bell | UserMenu | Error Handling |
|---------|--------------|----------|---------------|------|----------|---------------|
| Settings | 868 | 6 | Yes (back+icon) | ❌ | ❌ | Toast only |
| Admin | 2,522 | 12 | Yes (tabs+menu) | ❌ | ✅ | Toast only |
| Team Admin | 1,119 | 6 | Yes (back+title) | ❌ | ❌ | Toast only |
| Docs | 313 | 1 | Shell Topbar ✅ | ✅ | ✅ | Inline error ✅ |
Docs is the gold standard. The other three need to converge toward its pattern.

View File

@@ -0,0 +1,448 @@
# DESIGN: Shell Contract — v0.7.0
## Status: Proposed
## Problem
Extension surfaces render into `#extension-mount` with no shell chrome.
A full audit (docs/AUDIT-surfaces.md) found: 3/4 primary surfaces lack a
notification bell, 2/4 lack a user menu, 4 different topbar implementations,
user menu never updates on package/role changes, toast-and-forget error
handling, and empty states that explain nothing.
## Solution
### 1. Shell Topbar — Two-Slot Model
The kernel injects a topbar for all extension surfaces. Two named slots
(left + center) let surfaces customize without replacing the entire bar.
**Layout:**
```
┌──────────────────────────────────────────────────────────────┐
│ ← │ [left] │ [center: flex-1] │ 🔔 │ 👤 │
└──────────────────────────────────────────────────────────────┘
```
- **← (home):** Always visible. Navigates to `__BASE__/`. Simple `<a>`.
- **Left slot:** Defaults to manifest title. Surfaces override with `setLeft()`.
- **Center slot:** Empty by default. Surfaces inject tabs, search, pickers.
`flex: 1` — expands to fill available space.
- **Bell + User Menu:** Always visible. Kernel-managed.
**Template change** (`surfaces/extension.html`):
```html
{{define "surface-extension"}}
<div id="extension-surface" class="extension-surface"
data-surface-id="{{.Surface}}">
<div id="shell-topbar" class="sw-topbar sw-topbar--shell"></div>
<div id="extension-mount" class="extension-mount" data-ext="{{.Surface}}"></div>
</div>
{{end}}
```
**SDK API:**
```js
sw.shell.topbar.setLeft(vnode) // Override left slot (default: title)
sw.shell.topbar.setSlot(vnode) // Set center slot content
sw.shell.topbar.setTitle(str) // Shorthand: setLeft with plain text
sw.shell.topbar.hide() // Remove topbar entirely
sw.shell.topbar.show() // Restore after hiding
```
### 2. Three Navigation Patterns
The shell topbar provides the top bar. What happens below it is the
surface's business. Three patterns emerge naturally:
#### Pattern A — Default (simple extensions, Docs)
```
┌──────────────────────────────────────────┐
│ ← │ Surface Title │ 🔔 │ 👤 │
├──────────────────────────────────────────┤
│ │
│ Content (full width) │
│ │
└──────────────────────────────────────────┘
```
Title only, no tabs, no sidebar. Content gets everything.
Zero code required — kernel defaults handle it.
**Used by:** Docs, Notes, Chat, simple extensions.
#### Pattern B — Flat Tabs (no sidebar, full width)
```
┌──────────────────────────────────────────────────────────┐
│ ← │ Title │ Tab1 │ Tab2 │ Tab3 │ Tab4 │ │ 🔔 │ 👤 │
├──────────────────────────────────────────────────────────┤
│ │
│ Content (full width, no sidebar) │
│ │
└──────────────────────────────────────────────────────────┘
```
Tabs in the topbar center slot. No sidebar — content fills the full
viewport width. For surfaces with 37 sections that don't have sub-items.
More real estate for content than a sidebar layout.
**Surface code:**
```js
sw.shell.topbar.setSlot(html`
<div class="sw-topbar__tabs">
${sections.map(s => html`
<a class="sw-topbar__tab ${active === s.key ? 'active' : ''}"
href=${s.href} onClick=${navigate}>${s.label}</a>
`)}
</div>
`);
```
**Used by:** Team Admin (Members / Connections / Workflows / Settings / Activity),
Settings, Schedules.
#### Pattern C — Category Tabs + Sidebar (two-level)
```
┌─────────────────────────────────────────────────────────────┐
│ ← │ Title │ People │ Workflows │ System │ Mon │ 🔔 │ 👤 │
├──────────┬──────────────────────────────────────────────────┤
│ Users │ │
│ Teams │ Content │
│ Groups │ │
│ │ │
└──────────┴──────────────────────────────────────────────────┘
```
Major categories in the topbar (via center slot). Surface renders its own
sidebar inside the content area for sub-navigation within the active
category. The shell topbar doesn't know about the sidebar — it's a
surface-level div below `#extension-mount`.
**Surface code:**
```js
// Topbar: major categories
sw.shell.topbar.setLeft(html`
<img src="${BASE}/favicon.svg" width="18" height="18" style="vertical-align:-3px" />
<span style="margin-left:6px">Administration</span>
`);
sw.shell.topbar.setSlot(html`
<div class="sw-topbar__tabs">
${categories.map(c => html`
<a class="sw-topbar__tab ${activeCat === c.key ? 'active' : ''}"
href=${c.href} onClick=${navigate}>
<${CatIcon} paths=${c.icon} /> ${c.label}
</a>
`)}
</div>
`);
// Content area: sidebar is surface-owned
return html`
<div class="admin-body">
<div class="admin-nav">
${sidebarSections.map(s => html`...`)}
</div>
<div class="admin-content">
<${SectionComponent} />
</div>
</div>
`;
```
**Used by:** Admin.
### 3. Kernel-Provided Tab CSS
The kernel provides `.sw-topbar__tabs` and `.sw-topbar__tab` CSS so
surfaces get consistent tab styling. Not required — surfaces can style
their own slot content however they want.
```css
.sw-topbar__tabs {
display: flex;
align-items: center;
gap: var(--sp-1);
height: 100%;
}
.sw-topbar__tab {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
font-size: 13px;
font-weight: 500;
color: var(--text-2);
text-decoration: none;
border-radius: var(--radius-sm);
transition: color var(--transition), background var(--transition);
white-space: nowrap;
}
.sw-topbar__tab:hover {
color: var(--text);
background: var(--bg-hover);
}
.sw-topbar__tab.active {
color: var(--text);
background: var(--bg-2);
}
```
### 4. Primary Surface Migrations
#### Settings → Pattern B (flat tabs)
Currently: custom topbar (back + icon + "Settings"), sidebar nav.
After: shell topbar with flat tabs, no sidebar. Content full width.
Settings has 6 sections (General / Appearance / Profile / Teams /
Connections / Notifications) — perfect for flat tabs. The sidebar was
thin (~140px) and ate width from the content area for no good reason.
**Migration:**
- Delete `settings-topbar` div and CSS.
- Delete sidebar nav. Move section links into `sw.shell.topbar.setSlot()`.
- Remove `sb_settings_return` sessionStorage — shell home link handles it.
- Content area becomes full-width.
- Fix Teams section: add "Open Team Admin →" link, show role, add leave action.
Extension config sections (`__CONFIG_SECTIONS__`) render as additional
tabs after the divider. Same as today, just in the topbar instead of sidebar.
#### Admin → Pattern C (category tabs + sidebar)
Currently: custom topbar (favicon + "Administration" + category tabs + UserMenu).
After: shell topbar with category tabs in center slot, surface-owned sidebar.
**Migration:**
- Delete custom `admin-topbar` div and CSS.
- `sw.shell.topbar.setLeft()` with favicon + "Administration".
- `sw.shell.topbar.setSlot()` with category tabs (People / Workflows / System / Monitoring).
- Bell and user menu come from the shell — delete the explicit `<${UserMenu}>`.
- Admin sidebar and content area unchanged — they're below the topbar.
- Delete custom `CatIcon` renderer if category tab icons use standard SVG.
- Fix hardcoded `favicon.svg` — left slot can use theme-aware image.
**Result:** Admin looks identical to today but its topbar is kernel-managed.
Bell added for free. User menu reactive for free.
#### Team Admin → Pattern B (flat tabs)
Currently: custom topbar (back + "Team Admin: {name}"), sidebar nav.
After: shell topbar with flat tabs, no sidebar.
With Groups removed, Team Admin has 5 sections: Members / Connections /
Workflows / Settings / Activity. Perfect for flat tabs.
**Migration:**
- Delete `team-admin-topbar` div and CSS.
- `sw.shell.topbar.setTitle('Team Admin: ' + team.name)` after team fetch.
- Section tabs into `sw.shell.topbar.setSlot()`.
- Delete sidebar nav. Content full-width.
- Remove `sb_team_admin_return` sessionStorage.
- Remove Groups tab entirely (37-line dead-end).
- Fix signoff display: `user_id``sw.users.displayName()`.
#### Docs → Pattern A (default)
Currently: imports `shell/topbar.js` and renders it explicitly.
After: shell topbar auto-renders. No surface code needed.
**Migration:**
- Delete `import { Topbar }` and `<${Topbar}>` render.
- Shell topbar provides title + bell + user menu automatically.
- Docs sidebar (document list) is in the content area, unaffected.
### 5. User Menu Reactivity
**The single most impactful fix.**
**Backend — new WS events:**
```go
// After package install/uninstall/enable/disable:
h.hub.BroadcastToUser(userID, "package.changed", map[string]string{
"action": "installed", "id": packageID,
})
// After team role/membership change:
h.hub.BroadcastToUser(userID, "auth.changed", map[string]string{
"reason": "team_role",
})
```
**Frontend** (`user-menu.js`):
```js
useEffect(() => {
if (!sw?.api?.surfaces?.list) return;
function fetchSurfaces() {
sw.api.surfaces.list().then(data => {
const raw = Array.isArray(data) ? data : data?.data || [];
setAllSurfaces(raw);
}).catch(() => {});
}
fetchSurfaces();
const off1 = sw.on?.('package.changed', fetchSurfaces);
const off2 = sw.on?.('auth.changed', fetchSurfaces);
return () => {
if (typeof off1 === 'function') off1();
if (typeof off2 === 'function') off2();
};
}, [authenticated]);
```
**Event inventory:**
| Event | Trigger | Payload |
|-------|---------|---------|
| `package.changed` | Install, uninstall, enable, disable | `{ action, id }` |
| `auth.changed` | Team role change, group membership change | `{ reason }` |
| `notification.read` | Mark notification read | `{ id }` |
| `notification.all_read` | Mark all read | `{}` |
### 6. Notification Read Broadcast
**Backend** — after `MarkRead()` / `MarkAllRead()`:
```go
h.hub.BroadcastToUser(userID, "notification.read", map[string]string{"id": id})
h.hub.BroadcastToUser(userID, "notification.all_read", nil)
```
**Frontend**`NotificationBell` listens for `.created`, `.read`, `.all_read`:
```js
const onRead = (e) => {
setNotifications(prev => prev.map(n =>
n.id === e.id ? { ...n, read_at: new Date().toISOString() } : n
));
};
const onAllRead = () => {
setNotifications(prev => prev.map(n => ({
...n, read_at: n.read_at || new Date().toISOString()
})));
};
```
### 7. Shell Announcement Global Dismiss
On dismiss, write: `localStorage.setItem('armature_dismissed_' + hash(text), '1')`.
On mount, check. New announcements (changed text) show again.
Implementation: inline `<script>` in `base.html` — checks localStorage on
DOMContentLoaded. Dismiss button writes the key. Works on every surface
including login.
### 8. Error Handling Pass
**Pattern** — inline error + retry replaces toast-and-forget:
```js
const [error, setError] = useState(null);
async function load() {
setError(null);
try {
const data = await sw.api.whatever.list();
setItems(data || []);
} catch (e) { setError(e.message); }
}
// In render:
${error && html`
<div class="sw-inline-error">
<span>${error}</span>
<button class="sw-btn sw-btn--secondary sw-btn--sm"
onClick=${load}>Retry</button>
</div>
`}
```
**New CSS class** (`sw-primitives.css`):
```css
.sw-inline-error {
display: flex; align-items: center; gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
background: var(--bg-2); border: 1px solid var(--danger);
border-radius: var(--radius); font-size: 13px; color: var(--danger);
}
```
**Sections requiring this pass:**
| Surface | Section | Current | After |
|---------|---------|---------|-------|
| Admin | Workflows | Toast + empty | Inline error + retry |
| Admin | Packages | Toast + empty | Inline error + retry |
| Admin | Groups | Toast + empty | Inline error + retry |
| Team Admin | Workflows (adopt) | Toast + "No global workflows" | Inline error + retry |
| Team Admin | Members | Toast + empty | Inline error + retry |
| Settings | General | Console warn | Inline error + retry |
| Settings | Teams | Toast + empty | Inline error + retry |
| Workflow Demo (pkg) | Main | Silent swallow | Inline error + retry |
### 9. Empty State Guidance
Every "No X" empty state gets: one-line explanation, primary action or doc link.
| Surface | Section | Current | After |
|---------|---------|---------|-------|
| Admin | Groups | "No groups" | "Groups control access to surfaces and features via permissions." + Create button |
| Admin | Workflows | "No workflows" | "Workflows define multi-stage approval processes with team roles and SLA tracking." + Create button |
| Admin | Teams | "No teams" | "Teams group users for shared connections, workflows, and access control." + Create button |
| Team Admin | Workflows | "No workflows — create one or adopt" | Add: "Adopt copies a global workflow for this team to customize." |
| Settings | Notifications | "No notification preferences" | "Preferences appear when notification types are configured by an administrator." |
## Rebrand Asset Inventory
### Current → Target
| File | Current | Target |
|------|---------|--------|
| `favicon.svg` | Dark icon ✅ | Unchanged |
| `favicon-light.svg` | **MISNAMED** (520×80 wordmark) | Square icon, light mode **(NEW)** |
| `favicon-32.png` | Dark raster ✅ | Unchanged |
| `favicon-256.png` | Dark raster ✅ | Unchanged |
| `favicon-light-32.png` | Missing | Light raster **(NEW)** |
| `favicon-light-256.png` | Missing | Light raster **(NEW)** |
| `favicon.ico` | Dark ✅ | Unchanged |
| `wordmark.svg` | Missing | **RENAMED** from current `favicon-light.svg` |
| `wordmark-dark.svg` | Missing | Light text #E5E5E5 on transparent **(NEW)** |
| `manifest.json` | "Self-hosted AI chat..." | "Self-hosted extension platform..." |
## Bug Fixes (bundled)
- **evil-chat:** `finally` cleanup + tighten `409` assertion.
- **Workflow demo:** Silent `catch` → inline error + retry.
- **Hello dashboard:** Delete `packages/hello-dashboard/`.
## Changeset Plan
| CS | Scope | Description |
|----|-------|-------------|
| CS-1 | Backend (Go) | WS events: `notification.read`, `notification.all_read`, `package.changed`, `auth.changed`. Tests. |
| CS-2 | Frontend (JS + HTML) | Shell topbar component, SDK API (`setLeft`/`setSlot`/`setTitle`/`hide`), extension.html template, SDK boot auto-mount. |
| CS-3 | Frontend (JS) | User menu reactivity: listen for `package.changed` + `auth.changed`. Notification bell: listen for `.read` + `.all_read`. |
| CS-4 | Frontend (JS + HTML) | Surface migrations: Settings → Pattern B, Admin → Pattern C, Team Admin → Pattern B, Docs → Pattern A. Delete custom topbars. |
| CS-5 | Frontend (JS + CSS) | Error handling pass + empty state guidance. `sw-inline-error` CSS. All sections from inventory. |
| CS-6 | Frontend (JS) | Announcement global dismiss (localStorage). |
| CS-7 | Static + docs | Rebrand assets, manifest.json, REBRAND-SPEC.md. |
| CS-8 | Frontend (JS) | Bug fixes: evil-chat cleanup, workflow demo error, hello-dashboard deletion. |
Each changeset independently CI-green.

View File

@@ -0,0 +1,407 @@
# DESIGN: Surface Runners — v0.7.1v0.7.3
## Status: Proposed
## Problem
Armature has two test tiers today:
1. **Go unit tests** — test store methods, handlers, middleware, sandbox.
Run in CI on every push. Coverage is good for kernel internals.
2. **ICD/SDK test runners** — browser-based test suites that validate API
endpoint contracts and SDK domain methods. Run manually by navigating
to `/s/icd-test-runner` or `/s/sdk-test-runner`.
Neither tier catches the class of bugs discovered during manual testing:
- **Cross-surface state:** Notification bell state not syncing, announcement
dismiss not persisting across surface navigations.
- **Package integration:** Workflow demo shows "not installed" because its
API call fails silently. The API works (unit tests pass); the surface's
integration with the API is broken.
- **Test side-effects:** ICD security tests install `evil.surface` with no
cleanup — it leaks into the menu.
- **Surface lifecycle:** Surfaces fail to load, mount into wrong containers,
miss SDK boot, or render without shell chrome.
These are integration bugs — they live at the boundary between kernel and
package, between surface and surface, between API and UI. They require a
new test tier.
## Solution Overview
Three deliverables across three versions:
| Version | Deliverable | What it catches |
|---------|-------------|-----------------|
| v0.7.1 | Runner framework (`sw.testing`) | Framework bugs, standardizes existing runners |
| v0.7.2 | Package runners + CI gate | Package integration bugs, regressions |
| v0.7.3 | Headless E2E (Playwright) | DOM rendering bugs, navigation flows, visual regressions |
## v0.7.1 — Runner Framework
### `sw.testing` SDK Module
New kernel SDK module at `src/js/sw/sdk/testing.js`. Provides structured
test authoring, lifecycle hooks, cleanup tracking, and machine-readable
results.
```js
// Extension runner registers suites during load
sw.testing.suite('notes-crud', async (s) => {
let folderId, noteId;
s.beforeAll(async () => {
// Setup: create a test folder
const r = await sw.api.post('/api/v1/ext/notes/folders', {
name: 'test-' + Date.now()
});
folderId = r.id;
s.track('folder', folderId); // auto-cleanup
});
s.test('create note', async (t) => {
const r = await sw.api.post('/api/v1/ext/notes/notes', {
title: 'Test Note', folder_id: folderId, content: '# Hello'
});
t.assert.ok(r.id, 'note has ID');
t.assert.eq(r.title, 'Test Note');
noteId = r.id;
t.track('note', noteId); // auto-cleanup
});
s.test('renderers fire', async (t) => {
// Test that mermaid block in note content triggers renderer
await sw.api.patch('/api/v1/ext/notes/notes/' + noteId, {
content: '```mermaid\ngraph LR; A-->B\n```'
});
// Renderer integration tested via DOM assertion
// (only meaningful in headless E2E — marked as browser-only)
t.browserOnly(() => {
const el = document.querySelector('.mermaid svg');
t.assert.ok(el, 'mermaid rendered to SVG');
});
});
s.afterAll(async () => {
// s.track() resources auto-cleaned here
// Manual cleanup for anything not tracked
});
});
```
### Core API
```js
sw.testing.suite(name, fn) // Register a test suite
sw.testing.run(name?) // Run one suite or all
sw.testing.results() // Get structured results (JSON)
sw.testing.on('complete', fn) // Event when run finishes
// Inside suite:
s.test(name, fn) // Register a test
s.beforeAll(fn) // Runs once before all tests
s.afterAll(fn) // Runs once after all tests (always, even on failure)
s.beforeEach(fn) // Runs before each test
s.afterEach(fn) // Runs after each test
s.track(type, id) // Register resource for auto-cleanup
s.skip(reason) // Skip entire suite
// Inside test:
t.assert.ok(val, msg) // Truthy
t.assert.eq(a, b, msg) // Deep equality
t.assert.neq(a, b, msg) // Not equal
t.assert.gt(a, b, msg) // Greater than
t.assert.match(str, re, msg) // Regex match
t.assert.throws(fn, msg) // Expects throw
t.assert.status(resp, code, msg) // HTTP status check
t.track(type, id) // Register resource for auto-cleanup
t.warn(msg) // Emit warning (non-fatal)
t.browserOnly(fn) // Only runs in headless E2E, skipped in API-only mode
t.skip(reason) // Skip this test
```
### `requires` Declarations
Runner packages declare dependencies in their manifest:
```json
{
"id": "chat-runner",
"type": "test-runner",
"title": "Chat Runner",
"requires": ["chat", "chat-core"],
"version": "0.1.0"
}
```
On load, the framework calls `GET /api/v1/surfaces` (or equivalent) to
check which packages are installed. If any `requires` entry is missing:
- Suite is marked `skipped` with reason: `"Missing required package: chat-core"`
- No tests execute — clean skip, not a failure
- The runner registry surface shows the skip reason prominently
This directly solves the "workflow demo shows not-installed" pattern:
the runner *knows* what should be installed and reports clearly when it isn't.
### Auto-Cleanup
The `track(type, id)` method registers resources for deletion in `afterAll`.
Supported resource types and their cleanup endpoints:
| Type | Cleanup Action |
|------|---------------|
| `channel` | `DELETE /api/v1/channels/:id` |
| `note` | `DELETE /api/v1/ext/notes/notes/:id` |
| `folder` | `DELETE /api/v1/ext/notes/folders/:id` |
| `workflow` | `DELETE /api/v1/workflows/:id` |
| `schedule` | `DELETE /api/v1/schedules/:id` |
| `package` | `DELETE /api/v1/admin/packages/:id` |
| `user` | `DELETE /api/v1/admin/users/:id` |
| `team` | `DELETE /api/v1/admin/teams/:id` |
Cleanup runs in reverse order (LIFO) in `afterAll`, regardless of
test pass/fail. Cleanup failures are reported as warnings, not failures.
The framework never swallows cleanup errors silently.
### Result Structure
```json
{
"runner": "notes-runner",
"timestamp": "2026-04-01T12:00:00Z",
"duration_ms": 1234,
"summary": { "total": 5, "passed": 4, "failed": 0, "warned": 1, "skipped": 0 },
"suites": [
{
"name": "notes-crud",
"status": "passed",
"duration_ms": 890,
"tests": [
{
"name": "create note",
"status": "passed",
"duration_ms": 120,
"warnings": [],
"cleanup": { "tracked": 1, "cleaned": 1, "failed": 0 }
}
]
}
],
"requires": { "met": ["notes"], "missing": [] }
}
```
### Warning Tier
Three result statuses:
- **`passed`** — assertions all passed, cleanup succeeded
- **`failed`** — at least one assertion failed
- **`warned`** — assertions passed but something non-fatal happened:
- API returned unexpected shape (extra/missing fields) but test doesn't depend on the exact field
- Cleanup failed for a tracked resource
- Timing exceeded a soft threshold
- `t.warn(msg)` called explicitly
Warnings are **never silent.** They appear in the UI and structured results.
The difference from the current `catch (e) { /* ignore */ }` pattern is
that warnings are *visible* — a human or CI system can decide whether
to investigate.
### ICD/SDK Runner Migration
The existing runners use a hand-rolled framework (`T.test()`, `T.assert()`,
`T.authFetch()`). Migration preserves all test logic:
| Current | New |
|---------|-----|
| `T.test(tier, group, name, fn)` | `s.test(name, fn)` inside `sw.testing.suite(tier + '/' + group, fn)` |
| `T.assert(cond, msg)` | `t.assert.ok(cond, msg)` |
| `T.authFetch(token, method, path, body)` | Kept as utility — not an assertion primitive |
| `T.apiPost(...)` | Kept as utility |
| Result rendering (`ui.js`) | Delegated to runner registry surface |
| No cleanup hooks | `s.track()` + `s.afterAll()` |
The ICD and SDK runners become packages with `"type": "test-runner"` in
their manifests. Their existing surfaces (`/s/icd-test-runner`,
`/s/sdk-test-runner`) are replaced by the unified runner registry at
`/s/test-runners`.
## v0.7.2 — Package Runners
### Runner Inventory
| Runner | `requires` | Key Assertions |
|--------|-----------|---------------|
| `notes-runner` | `["notes"]` | CRUD, folders, tags, backlinks, search, markdown rendering, SDK integration |
| `chat-runner` | `["chat", "chat-core"]` | Channel CRUD, messaging, participant display, renderer blocks in messages |
| `schedules-runner` | `["schedules"]` | Schedule CRUD, cron expression, toggle, Starlark exec |
| `workflow-runner` | `["content-approval"]` | Install detection, stage progression, form submission, signoff |
| `renderer-runner` | `["mermaid-renderer"]` | `sw.renderers.register` contract, post-render hooks, block rendering |
### Runner Result API
New kernel endpoints (no package required — kernel-provided):
```
POST /api/v1/test-runners/run → Run all installed runners
POST /api/v1/test-runners/run/:id → Run specific runner
GET /api/v1/test-runners/results → Last run results (JSON)
GET /api/v1/test-runners/results/:id → Last run results for specific runner
```
These endpoints enable CI to trigger and consume runner results via
`curl` without browser automation. The v0.7.3 Playwright harness is
additive — not required for CI gating.
**Auth:** Admin-only. Runners create/delete resources — they must run
with elevated permissions.
### CI Integration
New stage in `.gitea/workflows/ci.yaml`:
```yaml
test-runners:
needs: [unit-tests]
steps:
- name: Boot server
run: |
docker compose up -d
./ci/wait-for-healthy.sh
- name: Run surface runners
run: |
RESULT=$(curl -s -X POST http://localhost:8080/api/v1/test-runners/run \
-H "Authorization: Bearer $ADMIN_TOKEN")
FAILED=$(echo "$RESULT" | jq '.summary.failed')
if [ "$FAILED" != "0" ]; then
echo "$RESULT" | jq '.suites[] | select(.status == "failed")'
exit 1
fi
```
Runs in both PG and SQLite pipelines. Server boots with `BUNDLED_PACKAGES=*`
so all packages and their runners are installed.
## v0.7.3 — Headless E2E
### Playwright Harness
`ci/e2e-surface-test.sh`:
1. `docker compose up -d` (server + DB)
2. `npx playwright install chromium` (CI caches this)
3. Run `ci/e2e-surfaces.spec.ts`
4. Collect screenshots on failure
5. `docker compose down`
### Surface Navigation Smoke Test
```ts
test('all surfaces reachable', async ({ page }) => {
// Login
await page.goto('/');
await page.fill('#username', 'admin');
await page.fill('#password', 'admin');
await page.click('button[type="submit"]');
// Navigate through every installed surface
const surfaces = ['notes', 'chat', 'admin', 'settings', 'docs'];
for (const s of surfaces) {
await page.goto(`/s/${s}`);
// Assert: page loaded, no uncaught JS errors
await expect(page.locator('.sw-topbar')).toBeVisible();
// Assert: home link works
await page.click('.sw-topbar__home');
await expect(page).toHaveURL('/');
}
});
```
This is the automated version of "hello dashboard has no way out" — if
any surface fails to render a topbar or its home link doesn't work, CI
catches it.
### Screenshot on Failure
```ts
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== 'passed') {
await page.screenshot({
path: `ci/artifacts/failure-${testInfo.title}.png`,
fullPage: true
});
const logs = await page.evaluate(() =>
(window.__consoleErrors || []).join('\n')
);
fs.writeFileSync(
`ci/artifacts/console-${testInfo.title}.log`, logs
);
}
});
```
Artifacts saved to CI workspace. On failure, the developer gets a
screenshot + console log dump without needing to reproduce locally.
### Visual Regression (Optional)
Not a CI gate in v0.7.3 — produces a diff report for human review:
```ts
test('visual baseline - notes', async ({ page }) => {
await page.goto('/s/notes');
await expect(page.locator('.sw-topbar')).toBeVisible();
await expect(page).toHaveScreenshot('notes.png', {
maxDiffPixelRatio: 0.01
});
});
```
Playwright stores baseline screenshots in `ci/visual-baselines/`.
`toHaveScreenshot` compares against baseline and produces a diff image
on mismatch. Foundation for future visual regression gating.
## Sequencing
```
v0.7.0 Shell Contract ← prerequisite: surfaces need topbar before
│ runners can assert on it
v0.7.1 Runner Framework ← standardize test authoring
v0.7.2 Package Runners + CI ← write the actual tests, wire into CI
v0.7.3 Headless E2E ← automate browser-based runner execution
```
Each version is independently shippable. v0.7.2's API-based CI gate
works without v0.7.3's Playwright. v0.7.3 adds coverage for DOM-level
bugs that API-only runners can't catch.
## Open Questions
1. **Runner package type.** Should `"type": "test-runner"` be a new
manifest type, or should runners be `"type": "surface"` with a
`"tags": ["test-runner"]` convention? New type is cleaner but requires
a `ValidateManifest()` update.
2. **Runner discovery.** The registry surface needs to find all installed
runners. Options: (a) scan installed packages for `type: "test-runner"`,
(b) runners register themselves via `sw.testing.register()` during SDK
boot. Option (a) is declarative and doesn't require runner JS to load
before discovery.
3. **Parallel vs sequential.** Should runners execute in parallel?
Probably not initially — shared DB state means test isolation is hard.
Sequential is safer. Parallel can be a future optimization.
4. **SQLite limitations.** Some runners (cluster, multi-node) are
PG-only. The `requires` mechanism should support
`"requires_db": "postgres"` for these cases, or runners should
self-skip when `sw.config.db_driver === 'sqlite'`.