From e7c2dd58154e224dadf97c356e3c4272f02b496f Mon Sep 17 00:00:00 2001 From: dvcdsys Date: Tue, 11 Aug 2026 12:19:42 +0100 Subject: [PATCH 01/48] feat(dashboard): rewrite the frontend on the cream & ink design system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full replacement of the dashboard UI. Every screen is rebuilt on a small token layer instead of ad-hoc utility classes, so the look is enforced by the build rather than by review. The system, in five rules: cream surfaces and ink outlines (no greys, no blur shadows); cards round at 12px and controls stay square, with borderRadius overridden globally to 0 so a stray `rounded-md` cannot quietly drift; depth is a hard 4px offset shadow, one per screen region; mono for every machine value, right-aligned; status is a 9x9 square plus a word, never colour alone. Surfaces are a measured tonal ladder rather than a set of moods — field > surface > canvas > head, each a fixed CIE L* step apart, the device Material 3 uses for surface-container roles and Carbon for layers. A control the user types into gets its own `field` tone: sharing the card's fill made a form read as a grid of identical outlines with nothing for the eye to land on. Text is three tones that all clear WCAG AA on every surface they are allowed on, plus `faint`, which does not and is therefore limited to placeholders and disabled text. Also in here: - Palette moved to RGB channel vars so Tailwind opacity modifiers keep working while `.dark` swaps the whole thing. - JetBrains Mono self-hosted as subset woff2 with unicode-range; the icon library (lucide-react) and @radix-ui/react-scroll-area are dropped. - The status bar spans the full window as a sibling of the sidebar+main row, and pages publish their middle-slot fact through useStatusFact(). - Fixed invalid nesting that shipped before: buttons inside buttons in the search result header, and buttons inside an anchor in the project card. - Fixed card corners reading as cut off — a square filled header strip painting over a 12px arc needs overflow-hidden on the card. - Workspace projects are a table like every other list in the dashboard, not a stack of cards with the facts crammed into one wrapped line. - devmock/ (gitignored) boots the real App against a mock fetch, so every authenticated screen can be reviewed without a server or a login. ?at= picks the route, ?theme= pins light/dark. Co-Authored-By: Claude Opus 5 --- server/dashboard/.gitignore | 8 + server/dashboard/README.md | 149 +++- server/dashboard/index.html | 17 +- server/dashboard/package.json | 2 - server/dashboard/src/app/App.tsx | 63 +- server/dashboard/src/app/BrandMark.tsx | 32 + server/dashboard/src/app/Footer.tsx | 94 --- server/dashboard/src/app/Shell.tsx | 33 +- server/dashboard/src/app/Sidebar.tsx | 127 +-- server/dashboard/src/app/StatusBar.tsx | 130 +++ server/dashboard/src/app/UpdateBanner.tsx | 66 +- server/dashboard/src/auth/AuthShell.tsx | 45 + .../src/auth/BootstrapNeededPage.tsx | 62 +- .../dashboard/src/auth/ChangePasswordPage.tsx | 149 ++-- server/dashboard/src/auth/LoginPage.tsx | 114 +-- server/dashboard/src/fonts/fonts.css | 91 +++ .../jetbrains-mono-cyrillic-400-normal.woff2 | Bin 0 -> 5328 bytes .../jetbrains-mono-cyrillic-500-normal.woff2 | Bin 0 -> 5356 bytes .../jetbrains-mono-cyrillic-700-normal.woff2 | Bin 0 -> 5336 bytes .../jetbrains-mono-latin-400-normal.woff2 | Bin 0 -> 21168 bytes .../jetbrains-mono-latin-500-normal.woff2 | Bin 0 -> 21832 bytes .../jetbrains-mono-latin-700-normal.woff2 | Bin 0 -> 21908 bytes .../jetbrains-mono-latin-ext-400-normal.woff2 | Bin 0 -> 7336 bytes .../jetbrains-mono-latin-ext-500-normal.woff2 | Bin 0 -> 7528 bytes .../jetbrains-mono-latin-ext-700-normal.woff2 | Bin 0 -> 7472 bytes server/dashboard/src/index.css | 771 ++++++++++++++++-- server/dashboard/src/lib/theme.ts | 4 + .../src/modules/api-keys/ApiKeysPage.tsx | 148 ++-- .../api-keys/components/ApiKeyTable.tsx | 144 ++-- .../components/CreateApiKeyDialog.tsx | 300 +++---- .../components/RevokeApiKeyDialog.tsx | 33 +- .../dashboard/src/modules/api-keys/index.ts | 5 +- .../GithubIntegrationPage.tsx | 29 +- .../modules/github-integration/TokensTab.tsx | 271 +++--- .../github-integration/WebhooksTab.tsx | 224 +++-- .../src/modules/github-integration/index.ts | 5 +- .../src/modules/groups/GroupsPage.tsx | 153 ++-- .../groups/components/CreateGroupDialog.tsx | 51 +- .../groups/components/GroupMembersDialog.tsx | 111 ++- .../groups/components/ShareToGroupCard.tsx | 69 +- server/dashboard/src/modules/groups/index.ts | 7 +- .../src/modules/home/CommandBlock.tsx | 38 - .../src/modules/home/ConnectClaudeCode.tsx | 174 ++++ .../modules/home/ConnectClaudeCodeCard.tsx | 220 ----- .../dashboard/src/modules/home/HomePage.tsx | 222 ++--- server/dashboard/src/modules/home/index.ts | 2 - .../modules/login-locks/LoginLocksPage.tsx | 73 +- .../components/LoginLocksTable.tsx | 101 ++- .../components/ResetLockButton.tsx | 35 +- .../src/modules/login-locks/index.ts | 5 +- .../modules/managed-tunnels/BinarySection.tsx | 120 ++- .../managed-tunnels/ManagedTunnelsPage.tsx | 444 +++++----- .../managed-tunnels/TunnelStateBadge.tsx | 16 +- .../src/modules/managed-tunnels/index.ts | 5 +- .../modules/projects/ProjectDetailPage.tsx | 528 ++++++------ .../src/modules/projects/ProjectsListPage.tsx | 242 ++---- .../components/DeleteProjectDialog.tsx | 29 +- .../projects/components/ForceStopButton.tsx | 25 +- .../components/IndexingProgressCard.tsx | 75 +- .../projects/components/ProjectCard.tsx | 181 ++-- .../projects/components/ProjectInfoCard.tsx | 80 +- .../projects/components/ProjectsTable.tsx | 363 ++++----- .../components/ReassignOwnerDialog.tsx | 76 +- .../components/ReindexProjectButton.tsx | 45 +- .../projects/components/SyncProjectButton.tsx | 28 +- .../projects/components/SyncSettingsCard.tsx | 393 ++++----- .../dashboard/src/modules/projects/index.ts | 5 +- .../src/modules/projects/lib/projectList.ts | 16 +- server/dashboard/src/modules/registry.ts | 22 +- .../src/modules/search/SearchPage.tsx | 499 ++++++------ .../src/modules/search/components/Filters.tsx | 160 ++-- .../search/components/ResultFileCard.tsx | 90 +- .../search/components/ResultSnippet.tsx | 81 +- .../modules/search/components/SearchBar.tsx | 72 ++ .../modules/search/components/SearchInput.tsx | 60 -- server/dashboard/src/modules/search/index.ts | 5 +- .../src/modules/server/ServerPage.tsx | 285 +++---- .../modules/server/components/NumberField.tsx | 66 ++ .../components/SaveAndRestartDialog.tsx | 67 +- .../server/components/SidecarStateBadge.tsx | 19 +- .../modules/server/components/SourcePill.tsx | 32 +- server/dashboard/src/modules/server/index.ts | 5 +- .../server/sections/AdvancedSection.tsx | 219 ++--- .../server/sections/EmbeddingModelSection.tsx | 245 +++--- .../sections/EmbeddingProviderSection.tsx | 301 +++---- .../server/sections/RuntimeParamsSection.tsx | 76 +- .../modules/server/sections/SidecarRail.tsx | 119 +++ .../server/sections/SidecarSection.tsx | 133 --- .../sections/providers/OpenAIProviderForm.tsx | 150 ++-- .../sections/providers/VoyageProviderForm.tsx | 297 +++---- .../src/modules/settings/SettingsPage.tsx | 28 +- .../components/ChangePasswordForm.tsx | 62 +- .../settings/components/SessionRow.tsx | 79 +- .../dashboard/src/modules/settings/index.ts | 5 +- .../settings/sections/EditorSection.tsx | 68 +- .../settings/sections/ProfileSection.tsx | 31 +- .../settings/sections/SessionsSection.tsx | 108 ++- .../settings/sections/ThemeSection.tsx | 56 +- server/dashboard/src/modules/types.ts | 19 +- .../dashboard/src/modules/users/UsersPage.tsx | 63 +- .../users/components/DeleteUserDialog.tsx | 70 +- .../users/components/DisableUserButton.tsx | 20 +- .../users/components/InviteUserDialog.tsx | 105 ++- .../users/components/ResetPasswordDialog.tsx | 78 +- .../users/components/UserRoleSelect.tsx | 25 +- .../modules/users/components/UsersTable.tsx | 177 ++-- server/dashboard/src/modules/users/index.ts | 5 +- .../workspaces/WorkspaceDetailPage.tsx | 298 ++++--- .../modules/workspaces/WorkspacesListPage.tsx | 95 +-- .../components/AddExistingProjectDialog.tsx | 131 ++- .../workspaces/components/AddRepoDialog.tsx | 368 ++++----- .../components/CreateWorkspaceDialog.tsx | 52 +- .../workspaces/components/WorkspaceCard.tsx | 101 +-- .../components/WorkspaceProjectRow.tsx | 139 ---- .../components/WorkspaceProjectsTable.tsx | 186 +++++ .../components/WorkspaceSearchDialog.tsx | 151 ++-- .../dashboard/src/modules/workspaces/index.ts | 5 +- server/dashboard/src/ui/alert.tsx | 59 +- server/dashboard/src/ui/badge.tsx | 88 +- server/dashboard/src/ui/button.tsx | 80 +- server/dashboard/src/ui/card.tsx | 137 +++- server/dashboard/src/ui/checkbox.tsx | 58 ++ server/dashboard/src/ui/code.tsx | 44 + server/dashboard/src/ui/dialog.tsx | 46 +- server/dashboard/src/ui/dropdown-menu.tsx | 129 +-- server/dashboard/src/ui/input.tsx | 82 +- server/dashboard/src/ui/label.tsx | 11 +- server/dashboard/src/ui/page.tsx | 52 ++ server/dashboard/src/ui/progress.tsx | 56 ++ server/dashboard/src/ui/radio-group.tsx | 96 ++- server/dashboard/src/ui/scroll-area.tsx | 46 -- server/dashboard/src/ui/select.tsx | 161 ++-- server/dashboard/src/ui/skeleton.tsx | 29 +- server/dashboard/src/ui/slider.tsx | 73 +- server/dashboard/src/ui/sonner.tsx | 21 +- server/dashboard/src/ui/switch.tsx | 66 +- server/dashboard/src/ui/table.tsx | 166 ++-- server/dashboard/src/ui/tabs.tsx | 96 ++- server/dashboard/src/ui/tooltip.tsx | 50 +- server/dashboard/tailwind.config.ts | 140 ++-- 140 files changed, 7415 insertions(+), 6947 deletions(-) create mode 100644 server/dashboard/src/app/BrandMark.tsx delete mode 100644 server/dashboard/src/app/Footer.tsx create mode 100644 server/dashboard/src/app/StatusBar.tsx create mode 100644 server/dashboard/src/auth/AuthShell.tsx create mode 100644 server/dashboard/src/fonts/fonts.css create mode 100644 server/dashboard/src/fonts/jetbrains-mono-cyrillic-400-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-cyrillic-500-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-cyrillic-700-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-latin-400-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-latin-500-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-latin-700-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-latin-ext-400-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-latin-ext-500-normal.woff2 create mode 100644 server/dashboard/src/fonts/jetbrains-mono-latin-ext-700-normal.woff2 delete mode 100644 server/dashboard/src/modules/home/CommandBlock.tsx create mode 100644 server/dashboard/src/modules/home/ConnectClaudeCode.tsx delete mode 100644 server/dashboard/src/modules/home/ConnectClaudeCodeCard.tsx create mode 100644 server/dashboard/src/modules/search/components/SearchBar.tsx delete mode 100644 server/dashboard/src/modules/search/components/SearchInput.tsx create mode 100644 server/dashboard/src/modules/server/components/NumberField.tsx create mode 100644 server/dashboard/src/modules/server/sections/SidecarRail.tsx delete mode 100644 server/dashboard/src/modules/server/sections/SidecarSection.tsx delete mode 100644 server/dashboard/src/modules/workspaces/components/WorkspaceProjectRow.tsx create mode 100644 server/dashboard/src/modules/workspaces/components/WorkspaceProjectsTable.tsx create mode 100644 server/dashboard/src/ui/checkbox.tsx create mode 100644 server/dashboard/src/ui/code.tsx create mode 100644 server/dashboard/src/ui/page.tsx create mode 100644 server/dashboard/src/ui/progress.tsx delete mode 100644 server/dashboard/src/ui/scroll-area.tsx diff --git a/server/dashboard/.gitignore b/server/dashboard/.gitignore index ac7e93ec..7113e791 100644 --- a/server/dashboard/.gitignore +++ b/server/dashboard/.gitignore @@ -9,3 +9,11 @@ node_modules/ # Type-gen output is reproducible — never commit src/api/generated.ts + +# Digest of the last successful build's inputs — lets `make dashboard-build` +# skip a no-op rebuild. Machine-local state, meaningless in another checkout. +.build-stamp + +# Local-only visual harness (see devmock/main.tsx) — never shipped. +devmock/ +devmock.html diff --git a/server/dashboard/README.md b/server/dashboard/README.md index da33ac33..1255fec8 100644 --- a/server/dashboard/README.md +++ b/server/dashboard/README.md @@ -1,9 +1,14 @@ # cix-dashboard The embedded operator dashboard for `cix-server`. Vite + React + TypeScript + -Tailwind + shadcn/ui + TanStack Query, served by the Go binary at +Tailwind + Radix primitives + TanStack Query, served by the Go binary at `/dashboard` via `embed.FS`. +The UI follows the **cix design system** — cream & ink, described in full +under [Design](#design) below. Read that section before touching any markup; +it is the difference between a change that lands and one that has to be +redone. + ## Local development ```bash @@ -51,21 +56,24 @@ register, done**. ```ts // src/modules/projects/index.ts - import { Folder } from 'lucide-react'; import type { Module } from '../types'; import ProjectsPage from './ProjectsPage'; export const ProjectsModule: Module = { id: 'projects', label: 'Projects', - icon: Folder, path: '/projects', element: ProjectsPage, // requiredRole: 'admin', // omit for everyone, 'admin' to gate - weight: 10, // lower numbers come first in the sidebar + group: 'workspace', // or 'admin' — which sidebar block it sits in + weight: 10, // lower numbers come first within the group + blurb: 'One sentence for the home-page module grid.', }; ``` + There is no `icon` field. Nav rows carry a 7×7 square marker and the label + does the work — see the "no icon soup" rule below. + 3. **Register it**: ```ts @@ -98,14 +106,16 @@ register, done**. on success, throws an `ApiError` (with `.status` and `.detail`) on any non-2xx. The provider in `app/providers.tsx` already disables retries on 401/403. -- **UI primitives**: import from `@/ui/*` (button, card, input, dialog, - alert, sonner). All wrap shadcn-style Tailwind primitives. Add new - ones via `npx shadcn add ` when needed. -- **Icons**: `lucide-react` only. Named imports — no default re-exports - so the bundle tree-shakes. -- **Styling**: Tailwind tokens only (`bg-background`, `text-muted-foreground`, - …). Never inline `style={{ color: '#abc' }}` — colour drift is the - reason we have a token system. +- **UI primitives**: import from `@/ui/*` (button, badge, card, input, + checkbox, code, dialog, alert, page, progress, table, tabs, sonner…). + Compose those plus the `.cix-*` classes in `index.css`; reach for raw + Tailwind for layout (flex / grid / gap / spacing) and little else. +- **Icons**: there is no icon library. The chrome has exactly one mark + (`app/BrandMark.tsx`); everywhere else, state is a 9×9 square plus a word + and affordances are mono glyphs (`▼ ▾ ▸ ↗ ✕`). +- **Styling**: design tokens only — `bg-surface`, `text-dim`, `border-line-soft`, + `shadow-hard`, `rounded-card`. Never inline `style={{ color: '#abc' }}`, and + never a raw hex: colour drift is the reason the token system exists. - **Class strings**: use `cn()` from `@/lib/cn` for conditional classes. It de-duplicates conflicting Tailwind classes. - **Dates**: format via helpers in `@/lib/formatDate`. Don't sprinkle @@ -118,15 +128,18 @@ register, done**. ``` src/ main.tsx boot React + Router + providers - index.css Tailwind + shadcn CSS variables (light/dark tokens) + index.css design tokens (light/dark) + the .cix-* component layer + fonts/ self-hosted JetBrains Mono (400/500/700, no CDN) api/ client.ts fetch wrapper, ApiError, cookie-based auth types.ts stable re-exports of generated schemas generated.ts ← gitignored; produced by `npm run gen:api` app/ App.tsx auth-gate + module routing - Shell.tsx sidebar + main content layout + Shell.tsx [banner] [sidebar | main] [full-width status bar] Sidebar.tsx renders modules from the registry, role-filtered + StatusBar.tsx the bottom bar + the per-page fact context + BrandMark.tsx the one icon in the chrome providers.tsx QueryClient + AuthProvider + Toaster auth/ AuthProvider.tsx bootstrap-status + /auth/me + login/logout mutations @@ -137,8 +150,8 @@ src/ modules/ types.ts Module interface registry.ts array of all registered modules, sorted by weight - home/ PR-B placeholder home; replace/augment in PR-C - ui/ shadcn primitives — never edit unless adding a new one + home/ projects/ search/ server/ … one folder per feature + ui/ design-system primitives — the whole visual vocabulary lib/ cn.ts `cn()` className helper formatDate.ts date / relative-time helpers @@ -158,15 +171,103 @@ placeholder when `dist/index.html` is missing. ## Bundle-size budget -`npm run build` should land below ~500 KB gzipped total. Today (PR-B): +`npm run build` should land below ~500 KB gzipped total. Today: ``` -index.html 0.5 kB │ gzip: 0.3 kB -assets/index-*.css 18 kB │ gzip: 4.3 kB -assets/index-*.js 289 kB │ gzip: 90 kB +index.html 1.7 kB │ gzip: 0.8 kB +assets/index-*.css 53 kB │ gzip: 9.4 kB +assets/index-*.js 617 kB │ gzip: 186 kB +assets/jetbrains-*.woff2 104 kB (9 subset files, fetched on demand) +``` + +If a future PR pushes that significantly higher, audit imports — the usual +culprit is a Radix primitive that ships more code than the feature actually +uses. + +## Design + +Cream & ink. Five rules; everything else follows from them. + +1. **Cream, not grey.** The page is `canvas`, panels are `surface`. There is + no neutral grey — every "grey" is a warm brown (`dim`, `muted`, `faint`). +2. **Ink outlines, not shadows.** Every panel, input, button and badge is + bounded by a 1.5px ink line. Blur-based shadows do not exist. +3. **Cards are rounded (12px). Controls are square (0).** `borderRadius` is + overridden globally to `0`, so a stray `rounded-md` renders square instead + of quietly breaking the look. Only `rounded-card` rounds — and `rounded-full` + for the radio, the single circle in the system. +4. **Depth is a hard offset shadow.** `shadow-hard` (4px 4px 0) on the one + primary button per view, on the search bar, on overlays (`shadow-hard-lg`). + Hover moves the element 2px into its own shadow. Never more than one + shadowed element per screen region. +5. **Mono for every machine value** — ids, ports, paths, versions, counts, + timestamps, code, scores, key prefixes. UI sans for prose and buttons. + Numbers align right; the right edge is the reading axis. + +### The tonal ladder + +Surfaces are a measured ladder, not a set of moods — one role per step, each a +fixed CIE L\* distance from its neighbour, the same device Material 3 uses for +its `surface-container` roles and Carbon uses for layers. Light theme: + +| Role | L\* | Where | +|---|---|---| +| `field` | 97.4 | inputs, selects, the search bar | +| `surface` | 94.4 | card body, table body | +| `canvas` | 89.6 | the page | +| `surface-head` | 84.7 | card header, card footer, table header | + +Two rules fall out of it, and both were violated before it was written down: + +- **A control a user types into gets `bg-field`, never `bg-surface`.** When the + input shares its card's fill, a form reads as a grid of identical outlines + and the eye has nothing to land on; the fill is what says "type here". +- **A header strip must be one full step off the canvas.** `surface-head` used + to sit 2.0 L\* below the canvas while the body sat 4.8 above it, so card + headers blended into the page. It is now the canvas's mirror image. + +Text tones are a three-level ladder — `ink`, `dim`, `muted` — and **all three +clear WCAG AA (4.5:1) on every surface they are allowed on**. `faint` does not, +by design, and is therefore limited to non-content: placeholders, disabled +text, "never". Check with a contrast tool before introducing a fourth tone; +`muted` shipped at 3.88:1 and made every form label harder to read than it +looked in Figma. + +Rank by weight, not by count: three emphasis levels per component is the +budget. A field is *value → label → everything else*, so provenance chips and +recommendations sit at the bottom — a solid ink chip on the least important +fact is what makes a settings page look speckled. + +Corollaries worth stating because they are easy to get wrong: + +- **Red is a scalpel.** `accent` marks destructive actions, the active nav + marker, focus rings and the indexing state. Never a decorative fill. +- **No icon soup.** Status is a 9×9 square plus a word — colour never carries + meaning alone. Use ``/`` from `@/ui/badge`. +- **Disabled ≠ information.** Never render read-only facts as greyed-out + fields or menu items. They go in a key/value block (``) or a stat strip. +- **One primary action per page**, in the `` header. Sub-navigation + (tabs, segmented controls, filters) belongs in the content area. +- **The status bar spans the full window width**, under the sidebar — it is a + sibling of the sidebar+main row, never a child of `
`. Pages publish + their middle-slot fact with `useStatusFact()`. + +Colours, sizes and geometry live in `src/index.css` as RGB channel vars +(`--c-ink`, …) so the `.dark` class swaps the whole palette and Tailwind can +still apply opacity modifiers. `tailwind.config.ts` maps them to names — +`surface`, `field`, `ink`, `dim`, `muted`, `faint`, `line-soft`, `accent`, +`ok`, `warn`. + +### Reviewing a change + +`devmock/` (gitignored) is a local visual harness: it installs a mock `fetch` +and boots the real `App`, so every authenticated screen can be reviewed with +realistic data and no server or login. + +```bash +npm run dev # then open /dashboard/devmock.html ``` -If a future PR pushes that significantly higher, audit imports — the -usual culprits are accidentally pulling all of lucide-react instead of -named imports, or shadcn primitives that ship more Radix code than the -feature actually uses. +Before calling a UI change done, sweep for the four usual violations: a +`border-radius` on a control, a `box-shadow` with a blur, a raw grey hex, and +an icon used as the sole carrier of state. diff --git a/server/dashboard/index.html b/server/dashboard/index.html index b4487ed1..325cbac1 100644 --- a/server/dashboard/index.html +++ b/server/dashboard/index.html @@ -4,10 +4,12 @@ + + cix dashboard + - +
diff --git a/server/dashboard/package.json b/server/dashboard/package.json index 1a0649e4..c133fb91 100644 --- a/server/dashboard/package.json +++ b/server/dashboard/package.json @@ -17,7 +17,6 @@ "@radix-ui/react-dropdown-menu": "^2.1.24", "@radix-ui/react-label": "^2.1.15", "@radix-ui/react-radio-group": "^1.4.7", - "@radix-ui/react-scroll-area": "^1.2.18", "@radix-ui/react-select": "^2.3.7", "@radix-ui/react-slider": "^1.4.7", "@radix-ui/react-slot": "^1.3.3", @@ -27,7 +26,6 @@ "@tanstack/react-query": "^5.101.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^0.577.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.18.1", diff --git a/server/dashboard/src/app/App.tsx b/server/dashboard/src/app/App.tsx index b01bd9c7..5227e52b 100644 --- a/server/dashboard/src/app/App.tsx +++ b/server/dashboard/src/app/App.tsx @@ -1,29 +1,30 @@ -import { Loader2 } from 'lucide-react'; import { Navigate, Route, Routes } from 'react-router-dom'; import { useAuth } from '@/auth/useAuth'; import BootstrapNeededPage from '@/auth/BootstrapNeededPage'; import ChangePasswordPage from '@/auth/ChangePasswordPage'; import LoginPage from '@/auth/LoginPage'; -import { MODULES } from '@/modules/registry'; +import { visibleModules } from '@/modules/registry'; +import { Dots } from '@/ui/button'; import { Shell } from './Shell'; +import { StatusFactProvider } from './StatusBar'; -// Top-level auth + route gate. Three states branch off here: -// - bootstrap not done → BootstrapNeededPage (no other route works) -// - logged out → LoginPage (no Shell, no nav) -// - must change password → ChangePasswordPage (no Shell, no nav) -// - logged in & happy → Shell + module routes +// Top-level auth + route gate. Four states branch off here: +// - bootstrap not done → BootstrapNeededPage (no other route works) +// - logged out → LoginPage (no Shell, no nav) +// - must change password → ChangePasswordPage (no Shell, no nav) +// - logged in & happy → Shell + module routes // -// Module routes are derived from the registry — no manual entries -// per feature. Each module owns its `path` (relative to /dashboard) and -// renders whatever it likes inside. +// Module routes come from the registry — no manual per feature. A +// module whose role gate excludes the current user has no mounted at +// all, so a deep link to it falls through to "/". export default function App() { const { loading, needsBootstrap, user, mustChangePassword } = useAuth(); if (loading) { return ( -
- - Loading… +
+ + loading
); } @@ -48,28 +49,20 @@ export default function App() { ); } - // Authenticated + ready — render every registered module under the Shell. - // A module whose role gate excludes the current user simply has no - // mounted, so a deep link to it 404s back to /. - const visible = MODULES.filter((m) => { - if (!m.requiredRole) return true; - if (m.requiredRole === 'user') return true; - return user.role === 'admin'; - }); - return ( - - - {visible.map((m) => { - // Modules can own a sub-tree by defining their own routes inside - // their element; we mount with a trailing wildcard so they get - // them on `//*`. - const mountPath = m.path === '/' ? '/*' : `${m.path}/*`; - const Element = m.element; - return } />; - })} - } /> - - + + + + {visibleModules(user.role).map((m) => { + // Modules can own a sub-tree, so each mounts with a trailing + // wildcard and routes `//*` internally. + const mountPath = m.path === '/' ? '/*' : `${m.path}/*`; + const Element = m.element; + return } />; + })} + } /> + + + ); } diff --git a/server/dashboard/src/app/BrandMark.tsx b/server/dashboard/src/app/BrandMark.tsx new file mode 100644 index 00000000..c8f276bb --- /dev/null +++ b/server/dashboard/src/app/BrandMark.tsx @@ -0,0 +1,32 @@ +import { cn } from '@/lib/cn'; + +// The one icon in the chrome: the cix magnifier, drawn as squares and a +// straight handle so it matches the app icon and the menu-bar panel. Inline +// SVG (not a font glyph, not lucide) because it is brand, not iconography — +// currentColor drives the ring so it inverts cleanly on an ink surface. +export function BrandMark({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/server/dashboard/src/app/Footer.tsx b/server/dashboard/src/app/Footer.tsx deleted file mode 100644 index c35fe5a6..00000000 --- a/server/dashboard/src/app/Footer.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Link } from 'react-router-dom'; -import { useServerStatus } from '@/lib/useServerStatus'; -import { useAuth } from '@/auth/useAuth'; -import { cn } from '@/lib/cn'; -import { formatVersion } from '@/lib/version'; - -// Footer spans the full width below the sidebar + main pane. Reads -// from the shared /status query (polled every 30 s) — server version -// on the left, embedding-provider indicator on the right. -// -// The label is the active provider kind ("ollama" / "openai" / -// "voyage") and the dot logic depends on whether the provider -// manages an in-process child: -// ollama (manages_process=true): green when /health alive, red -// otherwise — real liveness signal. -// openai / voyage (manages_process=false): permanently green. -// We don't ping remote APIs on every footer poll; failures -// surface at search/embed time with diagnostics. -// -// The provider name links to /server (admin-only page); viewers see -// plain text since the route isn't mounted for them. -export function Footer() { - const { data, isLoading } = useServerStatus(); - const { user } = useAuth(); - const version = data?.server_version ?? 'dev'; - const providerKind = data?.embedding_provider ?? ''; - const managesProcess = data?.embedding_provider_manages_process === true; - const alive = data?.model_loaded === true; - const isAdmin = user?.role === 'admin'; - - const dotClass = isLoading - ? 'bg-muted-foreground/40' - : managesProcess - ? alive - ? 'bg-emerald-500' - : 'bg-red-500' - : 'bg-emerald-500'; - const dotTitle = isLoading - ? 'Checking embedding provider status…' - : managesProcess - ? alive - ? 'Ollama sidecar is alive' - : 'Ollama sidecar is not responding' - : providerKind - ? `${providerKind} backend (no managed process)` - : 'Embedding backend'; - - const label = providerKind || 'embeddings'; - - const indicator = ( - <> - - {label} - - ); - - return ( - - ); -} diff --git a/server/dashboard/src/app/Shell.tsx b/server/dashboard/src/app/Shell.tsx index ee66ee7c..76f90cff 100644 --- a/server/dashboard/src/app/Shell.tsx +++ b/server/dashboard/src/app/Shell.tsx @@ -1,31 +1,26 @@ import type { ReactNode } from 'react'; import { Sidebar } from './Sidebar'; -import { Footer } from './Footer'; +import { StatusBar } from './StatusBar'; import { UpdateBanner } from './UpdateBanner'; -// Three-row layout: sidebar + main on top, footer spanning the full -// width on the bottom. min-h-0 on the inner row is required so that -//
's overflow-y-auto honors the footer's height when content -// grows tall. UpdateBanner sits above the main row so it spans the -// full width when a newer server release is available. +// The app is a column: [banner] [sidebar | main] [status bar]. +// +// The status bar is a SIBLING of the sidebar+main row, not a child of main — +// it spans the full window width and runs under the sidebar. That is the one +// structural rule of the shell; nesting it inside
is the bug this +// layout exists to prevent. +// +// Pages render their own (header + scrolling content) into
, so +// the page header stays pinned while the content below it scrolls. export function Shell({ children }: { children: ReactNode }) { return ( -
+
-
+
-
- {/* Content width scales with the viewport: comfortable reading - width on laptops (max-w-5xl), more room on large monitors so - wide layouts like the Projects table aren't boxed into the - middle with the columns squeezed. Capped at the 2xl screen - width so it never sprawls edge-to-edge on ultrawides. */} -
- {children} -
-
+
{children}
-
+
); } diff --git a/server/dashboard/src/app/Sidebar.tsx b/server/dashboard/src/app/Sidebar.tsx index b8fa3740..1046f4f1 100644 --- a/server/dashboard/src/app/Sidebar.tsx +++ b/server/dashboard/src/app/Sidebar.tsx @@ -1,76 +1,85 @@ -import { LogOut } from 'lucide-react'; import { NavLink } from 'react-router-dom'; import { useAuth } from '@/auth/useAuth'; -import { Button } from '@/ui/button'; import { cn } from '@/lib/cn'; -import { MODULES } from '@/modules/registry'; +import { visibleModules } from '@/modules/registry'; +import { BrandMark } from './BrandMark'; -// Sidebar is rendered from the module registry, filtered by the current -// user's role. A module without `requiredRole` is always visible; a module -// requiring `admin` is hidden from viewers. +// Rendered straight from the module registry, filtered by role, split into +// two blocks: the day-to-day tools and an ADMIN group under a mono rule. // -// New features show up in the sidebar automatically once registered — no -// edits to this component are needed when a module is added. +// Rows carry a 7×7 square marker instead of an icon — accent on the active +// row, quiet otherwise. The label carries the meaning; the marker only says +// "you are here". export function Sidebar() { const { user, logout } = useAuth(); - const role = user?.role ?? 'user'; + const modules = visibleModules(user?.role); + const workspace = modules.filter((m) => m.group !== 'admin'); + const admin = modules.filter((m) => m.group === 'admin'); - const visible = MODULES.filter((m) => { - if (!m.requiredRole) return true; - if (m.requiredRole === 'user') return true; - return role === 'admin'; - }); + const initials = (user?.email ?? '??').slice(0, 2).toUpperCase(); return ( -