Feat v0.2.4: SDK Topbar, Schedules surface, manifest icons, UserMenu cleanup

Shell navigation:
- sw.shell.Topbar — composable nav bar (title + slot + bell + user menu)
- Topbar CSS in sw-shell.css, wired into SDK via dynamic import

Schedules surface (packages/schedules/):
- Wraps kernel /api/v1/schedules CRUD + run + logs
- Table view with cron badge, human-readable preview, enable toggle
- Create/edit dialog with live cron-to-english preview

Manifest icons:
- icon field in manifest.json (emoji string)
- Surfaces API returns icon from package manifest
- UserMenu renders per-surface icons

UserMenu cleanup:
- Removed dead Chat/Notes/Projects hardcoded links
- Menu now driven by /api/v1/surfaces API (installed surfaces only)
- Core surfaces filtered via CORE_IDS set

Bug fixes:
- isAdmin() in can.js now checks surface.admin.access RBAC grant
  instead of deprecated user.role column (v0.2.0 regression)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 10:16:37 +00:00
parent 57ccf19efb
commit 22c70034c6
25 changed files with 2212 additions and 37 deletions

View File

@@ -2,7 +2,10 @@ package auth
import (
"context"
"encoding/json"
"log"
"switchboard-core/database"
"switchboard-core/store"
)
@@ -65,8 +68,42 @@ func EnsureEveryoneGroup(ctx context.Context, stores store.Stores, userID string
_ = stores.Groups.AddMember(ctx, EveryoneGroupID, userID, userID)
}
// EnsureAdminsGroup creates the Admins system group if it does not exist.
// Handles the case where migration 002 was applied before the Admins INSERT
// was added (no new migrations pre-MVP — edits in place).
// Uses raw SQL because the store's Create() overwrites the ID.
func EnsureAdminsGroup(ctx context.Context, stores store.Stores) {
if _, err := stores.Groups.GetByID(ctx, AdminsGroupID); err == nil {
return // already exists
}
permsJSON, _ := json.Marshal(AllPermissions)
db := database.DB
if db == nil {
return
}
_, err := db.ExecContext(ctx, `
INSERT OR IGNORE INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES (?, 'Admins', 'Full platform access — replaces legacy admin role.',
'global', NULL, 'system', ?)`,
AdminsGroupID, string(permsJSON))
if err != nil {
// Postgres variant
_, err = db.ExecContext(ctx, `
INSERT INTO groups (id, name, description, scope, created_by, source, permissions)
VALUES ($1, 'Admins', 'Full platform access — replaces legacy admin role.',
'global', NULL, 'system', $2::jsonb)
ON CONFLICT (id) DO NOTHING`,
AdminsGroupID, string(permsJSON))
if err != nil {
log.Printf("⚠ EnsureAdminsGroup: %v", err)
}
}
}
// AddToAdminsGroup adds a user to the Admins group (idempotent).
// Ensures the Admins group exists before attempting membership.
func AddToAdminsGroup(ctx context.Context, stores store.Stores, userID string) {
EnsureAdminsGroup(ctx, stores)
_ = stores.Groups.AddMember(ctx, AdminsGroupID, userID, userID)
}

View File

@@ -726,6 +726,7 @@ func (h *PackageHandler) ListEnabledSurfaces(c *gin.Context) {
ID string `json:"id"`
Title string `json:"title"`
Route string `json:"route"`
Icon string `json:"icon,omitempty"`
}
var enabled []navSurface
@@ -738,10 +739,12 @@ func (h *PackageHandler) ListEnabledSurfaces(c *gin.Context) {
continue
}
route, _ := p.Manifest["route"].(string)
icon, _ := p.Manifest["icon"].(string)
enabled = append(enabled, navSurface{
ID: p.ID,
Title: p.Title,
Route: route,
Icon: icon,
})
}
if enabled == nil {