From 0c91390e5632cbd1c57dd3c7582daed01d4fc6f2 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Tue, 12 May 2026 10:55:09 -0500 Subject: [PATCH] fix(frontend): workspace switcher (Phase 3 MVP) + SW network-first migration + platform_admin accessible_workspaces expansion + static render CSS cleanup. The switcher adds a sidebar dropdown for users who are members of multiple workspaces, renders as static text with a 'Workspace' label for single-workspace users, and muted 'No workspace' for zero. Uses existing /api/auth/me's accessible_workspaces and POST /api/auth/switch-workspace endpoints. Platform admin / superadmin users now see all workspaces in accessible_workspaces (closing the known regression from 88d91b1) via a LEFT JOIN that preserves workspace_role semantics (null = acting-as, role string = direct member). No cap on the list - deliberate for now, revisit at 50+ workspaces. SW fix bumps rd-admin-v1 -> rd-admin-v2 and switches fetch strategy from cache-first to network-first so the server's existing Cache-Control: no-cache + ETag headers actually get respected; preserves offline fallback. Static render CSS drops the bordered-box chrome that was making single-workspace users think the static text was clickable. Includes test fixture user switcher-test@local.test (credentials in fixture SQL header). Surfaced by semetra22 / Discord report about 'screens jumbled up' post-migration; root cause was the missing workspace switcher UI making devices in non-active workspaces appear missing. --- frontend/css/main.css | 59 +++++++++++++ frontend/index.html | 1 + frontend/js/api.js | 1 + frontend/js/app.js | 4 + frontend/js/components/workspace-switcher.js | 91 ++++++++++++++++++++ frontend/sw-admin.js | 21 ++++- server/routes/auth.js | 34 ++++++-- 7 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 frontend/js/components/workspace-switcher.js diff --git a/frontend/css/main.css b/frontend/css/main.css index bbc6c26..86429ce 100644 --- a/frontend/css/main.css +++ b/frontend/css/main.css @@ -32,6 +32,65 @@ body { font-size: 16px; } +/* Workspace switcher (Phase 3 MVP). Sits in sidebar-header below the logo. + Three render modes via JS: dropdown (>1 ws), static text (1 ws), + muted empty state (0 ws). */ +.workspace-switcher { position: relative; margin-top: 12px; } +.workspace-switcher-button { + display: flex; align-items: center; justify-content: space-between; + width: 100%; padding: 8px 10px; + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius); color: var(--text-primary); + font-size: 13px; cursor: pointer; transition: all var(--transition); +} +.workspace-switcher-button:hover { border-color: var(--accent); } +.workspace-switcher-static { + display: block; padding: 4px 2px; + color: var(--text-primary); font-size: 13px; font-weight: 500; +} +.workspace-switcher-static::before { + content: 'Workspace'; + display: block; + font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; + color: var(--text-muted); margin-bottom: 2px; +} +.workspace-switcher-empty { + display: block; padding: 8px 10px; + color: var(--text-muted); font-size: 12px; font-style: italic; +} +.workspace-switcher-button .chev { + flex-shrink: 0; margin-left: 8px; color: var(--text-muted); + transition: transform var(--transition); +} +.workspace-switcher.open .chev { transform: rotate(180deg); } +.workspace-switcher-menu { + display: none; + position: absolute; top: calc(100% + 4px); left: 0; right: 0; + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius); box-shadow: 0 4px 12px rgba(0,0,0,0.3); + max-height: 320px; overflow-y: auto; z-index: 100; +} +.workspace-switcher.open .workspace-switcher-menu { display: block; } +.workspace-switcher-item { + display: flex; align-items: center; gap: 8px; + padding: 10px 12px; cursor: pointer; + border-bottom: 1px solid var(--border); + color: var(--text-primary); font-size: 13px; +} +.workspace-switcher-item:last-child { border-bottom: none; } +.workspace-switcher-item:hover { background: var(--bg-input); } +.workspace-switcher-item.current { font-weight: 600; } +.workspace-switcher-item .check { + flex-shrink: 0; color: var(--accent); width: 14px; +} +.workspace-switcher-item .ws-meta { flex: 1; min-width: 0; } +.workspace-switcher-item .ws-name { + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.workspace-switcher-item .ws-org { + font-size: 11px; color: var(--text-muted); margin-top: 2px; +} + .nav-links { flex: 1; padding: 12px 8px; diff --git a/frontend/index.html b/frontend/index.html index 5be30e5..747051a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -31,6 +31,7 @@ ScreenTinker +