|
1 | | -# UI Navigation Rework — Future Improvements (Draft) |
| 1 | +# UI Navigation Rework — Implementation Spec |
2 | 2 |
|
3 | 3 | - **Date captured:** 2026-06-02 |
4 | | -- **Status:** DRAFT — vision captured from discussion, **not yet brainstormed or scheduled**. |
5 | | - Needs a full `brainstorming` → `writing-plans` pass before any implementation. |
6 | | -- **Author intent:** reduce overlap between three navigation surfaces and make |
7 | | - drill-down navigation clearer. |
8 | | - |
9 | | -## Motivation |
10 | | - |
11 | | -Today three surfaces overlap: |
12 | | - |
13 | | -- **Instance selector** — pick the active Honcho connection. |
14 | | -- **Dashboard** (`packages/web/src/components/dashboard/Dashboard.tsx`) — workspace |
15 | | - count + queue status for the active instance. |
16 | | -- **Fleet** (`packages/web/src/components/fleet/`) — cross-instance observability: |
17 | | - workspaces / sessions / queue side-by-side per instance. |
18 | | - |
19 | | -Dashboard and Fleet are **nearly identical** in what they show (workspace/queue |
20 | | -metrics), differing mainly in single-instance vs all-instances scope. That overlap |
21 | | -is the thing to collapse. |
22 | | - |
23 | | -## Proposed information architecture |
24 | | - |
25 | | -A single hierarchical drill-down, navigable as an **outline / tree**: |
| 4 | +- **Status:** SPEC — agent-ready. Phases 2–3 carry open product questions; resolve them |
| 5 | + via `superpowers:brainstorming` against this doc before implementing those phases. |
| 6 | + Phase 1 is well-defined and can start directly. |
| 7 | +- **Branch/PR:** `docs/ui-navigation-rework` (PR #55). Implementation should branch |
| 8 | + separately, one concern per PR (see [[branch-and-pr-one-concern]]). |
| 9 | + |
| 10 | +## For the agent picking this up — start here |
| 11 | + |
| 12 | +1. Read this whole doc, then read the **Current architecture** map below and open the |
| 13 | + cited files to confirm they still match (the codebase moves; verify before acting). |
| 14 | +2. If you're doing **Phase 1** (merge Dashboard + Fleet), the design is settled — |
| 15 | + follow the action items. If **Phase 2/3** (the Server drill-down, Fleet |
| 16 | + repositioning), first run `superpowers:brainstorming` to resolve the **Open |
| 17 | + questions** section, then `superpowers:writing-plans`. |
| 18 | +3. Honor the repo conventions in **Conventions** below. Gate every change with |
| 19 | + `make ci-web`; tests live in `packages/web/src/test/`. |
| 20 | +4. This is a frontend-only change. No backend/data-layer rewrite is required — the |
| 21 | + multi-instance fan-out already exists (see **Data layer**). |
| 22 | + |
| 23 | +## Goals |
| 24 | + |
| 25 | +- **Eliminate the Dashboard/Fleet overlap.** Today `Dashboard` (active instance only) |
| 26 | + and `Fleet` (all instances) render nearly the same metrics. Merge into one |
| 27 | + server-aware Dashboard. |
| 28 | +- **Make the hierarchy navigable as Server → Workspace → Peers / Sessions / |
| 29 | + Conclusions / …**, surfaced as an outline/drill-down. The Workspace→children level |
| 30 | + already exists; the missing level is **Server** (today "server" = the active |
| 31 | + instance, switched globally, not navigated). |
| 32 | +- **Unify cross-instance browsing into the Dashboard**: show all workspaces across all |
| 33 | + servers labelled `<workspace> (<server>)`, **filterable by server**. Fleet stops |
| 34 | + being a separate browsing page. |
| 35 | +- Keep navigation clear via breadcrumbs + an outline view. |
| 36 | + |
| 37 | +## Non-goals |
| 38 | + |
| 39 | +- No backend or Honcho API changes. No data-layer rewrite — `compareQueries.ts` + |
| 40 | + `scopedClient.ts` already fan out across instances. |
| 41 | +- No new "real" server entity — a "Server" is an existing `Instance` |
| 42 | + (`packages/web/src/lib/config.ts:25` `instanceSchema`). This is an IA/UX change, not |
| 43 | + a data-model change. |
| 44 | +- Not changing the localStorage multi-instance store model. |
| 45 | + |
| 46 | +## Current architecture (where to look) |
| 47 | + |
| 48 | +> Verified 2026-06-02. Paths relative to repo root; `packages/web/` is the SPA. |
| 49 | +
|
| 50 | +### Routes (TanStack flat-route; `routeTree.gen.ts` is generated — never hand-edit) |
| 51 | +- Generator: `@tanstack/router-plugin/vite` in `packages/web/vite.config.ts` (regen on save). |
| 52 | +- Root layout: `src/routes/__root.tsx` → renders `Sidebar` + `<Outlet/>`; redirects to |
| 53 | + `/settings` when no instance configured. |
| 54 | +- Top-level: `src/routes/index.tsx` (`/` → `Dashboard`), `fleet.tsx` (`/fleet` → |
| 55 | + `FleetDashboard`), `workspaces.tsx` (`/workspaces` → `WorkspaceList`), |
| 56 | + `seed-kits.tsx`, `settings.tsx`, `explore.tsx` (redirect helper). |
| 57 | +- Workspace-scoped (the existing drill-down): `workspaces_.$workspaceId.tsx` |
| 58 | + (`WorkspaceDetail` hub) and |
| 59 | + `workspaces_.$workspaceId_.{peers,sessions,conclusions,dreams,queue,webhooks}.tsx`. |
| 60 | +- Detail/param routes: `workspaces_.$workspaceId_.peers_.$peerId.tsx`, |
| 61 | + `…peers_.$peerId_.{chat,playground}.tsx`, `…sessions_.$sessionId.tsx`. |
| 62 | + |
| 63 | +### Navigation |
| 64 | +- `src/components/layout/Sidebar.tsx`: |
| 65 | + - `TOP_NAV` array (~line 33) — top-level items (Dashboard, Fleet, Workspaces, Seed |
| 66 | + Kits, Settings). |
| 67 | + - `WORKSPACE_SECTIONS` array (~line 41) — the workspace sub-nav (Peers, Sessions, |
| 68 | + Conclusions, Dreams, Webhooks), conditionally rendered (~271–335) when a workspace |
| 69 | + route is active. |
| 70 | + - Active-instance switcher (~145–226): renders `active.name`/`baseUrl`, health dot, |
| 71 | + dropdown over `instances` via `useInstances()`; `activate(id)` switches. |
| 72 | + - Active-context detection via `matchRoute({ to: "/workspaces/$workspaceId", fuzzy: true })` (~108). |
| 73 | +- `src/components/layout/Breadcrumb.tsx` — used by list pages (e.g. `PeerList.tsx`). |
| 74 | + |
| 75 | +### Dashboard vs Fleet (the overlap to merge) |
| 76 | +- `src/components/dashboard/Dashboard.tsx` — **active instance only**. Uses |
| 77 | + `useWorkspaces(page, 50)`; per-workspace `useQueueStatus()` via `WorkspaceQueueRow`; |
| 78 | + `GlobalQueueBanner` aggregates totals. Polls 2.5s active / 10s idle. |
| 79 | +- `src/components/fleet/FleetDashboard.tsx` — **all instances**. Iterates |
| 80 | + `useInstances().instances` → one `<FleetRow instance=…>` each. |
| 81 | +- `src/components/fleet/FleetRow.tsx` — per-instance metrics via |
| 82 | + `createScopedClient(instance)` + `useScopedWorkspaces` + `useQueries` fan-out of |
| 83 | + `scopedQueueStatusOptions` / `scopedConclusionsCountOptions`. |
| 84 | +- `src/components/fleet/fleetAggregates.ts` — `computeFleetAggregates(rows)` sums |
| 85 | + workspaces/conclusions/queue/health across instances. **Pure + transport-agnostic — |
| 86 | + reuse as-is.** |
| 87 | + |
| 88 | +### Data layer (already multi-instance capable) |
| 89 | +- `src/api/queries.ts` — active-instance hooks: `useWorkspaces`, `useWorkspace`, |
| 90 | + `useQueueStatus`, `usePeers`, `usePeer`, `usePeerRepresentation`, `usePeerCard`, |
| 91 | + `useSessions`, `useSessionMessages`, `useConclusions`, `useChat`, `useScheduleDream`, … |
| 92 | +- `src/api/compareQueries.ts` — **scoped (per-instance)** hooks + `useQueries` option |
| 93 | + builders: `useScopedWorkspaces`, `useScopedPeers`, `useScopedQueueStatus`, |
| 94 | + `useScopedConclusionsCount`, `scopedQueueStatusOptions`, `scopedConclusionsCountOptions`. |
| 95 | + Query keys namespaced by `instance.id` (`CK` map) so caches never collide. |
| 96 | +- `src/api/client.ts` (`client.current`, active) vs `src/api/scopedClient.ts` |
| 97 | + (`createScopedClient(instance)`, per-instance). `src/api/keys.ts` — `QK` query keys. |
| 98 | +- **Conclusion:** fanning a query across all instances is already a solved pattern |
| 99 | + (FleetRow demonstrates it). Server-level aggregation = generalize that pattern. |
| 100 | + |
| 101 | +### Instance ("Server") store |
| 102 | +- `src/lib/config.ts` — `instanceSchema`/`Instance` (~25), `InstanceStore` |
| 103 | + (`{instances, activeId}`), CRUD (`addInstance`/`updateInstance`/`deleteInstance`/ |
| 104 | + `setActiveInstance`). Persisted to `localStorage` `openconcho:instances`. |
| 105 | +- `src/hooks/useInstances.ts` — `{ instances, active, activeId, add, update, remove, activate }` |
| 106 | + via `useSyncExternalStore`; `activate`/`remove` clear the query cache. |
| 107 | +- Tests: `src/test/instances.test.ts` (store CRUD), `src/test/fleet.test.tsx` |
| 108 | + (multi-instance fan-out). |
| 109 | + |
| 110 | +### Domain components to reuse for Server-scoped views |
| 111 | +- `workspaces/WorkspaceList.tsx`, `workspaces/WorkspaceDetail.tsx` |
| 112 | +- `peers/PeerList.tsx`, `peers/PeerDetail.tsx` |
| 113 | +- `sessions/SessionList.tsx`, `sessions/SessionDetail.tsx` |
| 114 | +- `conclusions/ConclusionBrowser.tsx` |
| 115 | +- Each takes a `workspaceId` and calls the active-instance `use*` hooks. |
| 116 | + |
| 117 | +## Target information architecture |
26 | 118 |
|
27 | 119 | ``` |
28 | | -Server (instance) |
29 | | - └─ Workspace |
30 | | - ├─ Peers |
| 120 | +Server (= Instance) ← NEW navigable level (today: global switcher only) |
| 121 | + └─ Workspace ← exists: /workspaces/$workspaceId |
| 122 | + ├─ Peers (+ Peer → chat/playground) |
31 | 123 | ├─ Sessions |
32 | 124 | ├─ Conclusions |
33 | | - └─ … (queue, dreams, webhooks, etc.) |
| 125 | + └─ Dreams / Queue / Webhooks |
34 | 126 | ``` |
35 | 127 |
|
36 | | -- Navigation drills **Server → Workspace → {Peers | Sessions | Conclusions | …}**. |
37 | | -- An **outline view** makes the hierarchy explicit and improves wayfinding (vs. the |
38 | | - current flat per-feature pages). |
39 | | - |
40 | | -## Fleet folds into Dashboard |
41 | | - |
42 | | -- The Dashboard becomes the **unified, all-servers view**: every workspace across |
43 | | - every server, each labelled **`<workspace> (<server>)`**. |
44 | | -- **Filterable by server** — selecting a server narrows the Dashboard to that |
45 | | - server's workspaces. Fleet stops being a separate page and becomes an **overlay / |
46 | | - filter state** on the Dashboard. |
47 | | - |
48 | | -## Open question: does standalone Fleet survive? |
49 | | - |
50 | | -Fleet "still has merit" — but possibly **repositioned as a settings/management-style |
51 | | -view** (fleet health, per-instance admin) rather than a primary nav destination. |
52 | | -To decide in brainstorming: what is Fleet's unique job once cross-instance browsing |
53 | | -lives in the Dashboard? (Likely: fleet *health/ops*, not fleet *browsing*.) |
54 | | - |
55 | | -## Open questions (resolve during brainstorming) |
56 | | - |
57 | | -- How does the **instance selector** relate to the new top-level "Server" node — is |
58 | | - it absorbed into the tree, or kept as a global switcher? |
59 | | -- **Routing**: how does `Server → Workspace → …` map onto TanStack Router flat-route |
60 | | - files, and how are deep links / breadcrumbs handled? |
61 | | -- What exactly lives in a **settings-like Fleet** (health, queue depth, version, auth |
62 | | - status)? |
63 | | -- Responsive/mobile behaviour of the outline/tree. |
64 | | -- Migration: does the current `/fleet` route redirect, or stay as the ops view? |
65 | | - |
66 | | -## Non-goals (for this capture) |
67 | | - |
68 | | -- This is a **vision record**, not an implementation plan. No code, no schedule. |
69 | | -- Does not change the data layer — `compareQueries.ts` / `fleetAggregates.ts` |
70 | | - already fan out across instances and can back a unified Dashboard unchanged. |
71 | | - |
72 | | -## Next step |
73 | | - |
74 | | -When prioritised: run `superpowers:brainstorming` against this doc to resolve the |
75 | | -open questions, then `writing-plans` for the implementation. Keep it a separate |
76 | | -concern (its own branch/PR) from the proxy work in #54. |
| 128 | +- **Dashboard** = the all-servers landing: every workspace across every server as |
| 129 | + `<workspace> (<server>)`, **filterable by server**; server filter = "all" by default. |
| 130 | + Selecting a single server narrows to that server (and is the entry to its drill-down). |
| 131 | +- **Outline/tree** view makes Server→Workspace→section explicit; breadcrumbs reflect it. |
| 132 | + |
| 133 | +## Phased action items |
| 134 | + |
| 135 | +### Phase 1 — Merge Fleet into a server-aware Dashboard (settled; start here) |
| 136 | +**Goal:** one Dashboard that shows all workspaces across all servers, server-filterable; |
| 137 | +remove the redundant standalone Fleet *browsing* page (see Phase 3 for Fleet's future). |
| 138 | + |
| 139 | +- **Where:** `src/components/dashboard/Dashboard.tsx`, `src/components/fleet/*`, |
| 140 | + `src/routes/index.tsx`, `src/routes/fleet.tsx`, `src/components/layout/Sidebar.tsx`. |
| 141 | +- **Steps:** |
| 142 | + 1. Generalize the Dashboard to iterate `useInstances().instances` instead of only the |
| 143 | + active instance. Reuse `FleetRow`'s scoped fan-out pattern |
| 144 | + (`createScopedClient` + `scopedQueueStatusOptions`/`scopedConclusionsCountOptions`) |
| 145 | + and `computeFleetAggregates` (reuse as-is) for the totals banner. |
| 146 | + 2. Render a unified workspace table: row per workspace across all servers, labelled |
| 147 | + `<workspace> (<server>)`. Add a **server filter** control (dropdown: "All servers" |
| 148 | + + each `instance.name`); default "All". When one server is selected, the table |
| 149 | + narrows and the row link enters that server's workspace drill-down. |
| 150 | + 3. Fold `fleetAggregates.ts` totals into the Dashboard header (workspaces, conclusions, |
| 151 | + healthy/unreachable counts) — this is the current Fleet metric-card content. |
| 152 | + 4. Update `Sidebar.tsx` `TOP_NAV`: remove the separate "Fleet" item (or repoint it — |
| 153 | + see Phase 3). Keep "Dashboard". |
| 154 | + 5. Redirect `/fleet` → `/` (keep the route file as a redirect for back-compat) OR |
| 155 | + remove it and add a redirect in `__root.tsx`. |
| 156 | +- **Reuse:** `fleetAggregates.ts`, `FleetRow` logic, `compareQueries.ts`, `useInstances`. |
| 157 | +- **Net-new:** server-filter control; the merged Dashboard table. |
| 158 | +- **Tests:** extend `src/test/fleet.test.tsx` (or new `dashboard.test.tsx`): asserts the |
| 159 | + unified table renders rows for ≥2 instances with `<workspace> (<server>)` labels, and |
| 160 | + the server filter narrows to one server. Keep `computeFleetAggregates` snapshot tests. |
| 161 | + |
| 162 | +### Phase 2 — Server as a navigable level + outline drill-down (needs brainstorming) |
| 163 | +**Goal:** drill Server → Workspace → section from the Dashboard, with an outline view. |
| 164 | + |
| 165 | +- **Where:** `src/routes/` (new server-scoped routes), `Sidebar.tsx` (server-context |
| 166 | + sub-nav analogous to the existing workspace sub-nav), `Breadcrumb.tsx`. |
| 167 | +- **Likely steps (confirm in brainstorming):** |
| 168 | + 1. Decide the URL scheme — e.g. `/servers/$serverId/workspaces/$workspaceId/...` vs |
| 169 | + keeping `/workspaces/...` scoped to the active server + server selection as state. |
| 170 | + (Resolve "instance selector vs Server node" open question first.) |
| 171 | + 2. Add server-context detection in `Sidebar.tsx` (mirror the `matchRoute` workspace |
| 172 | + pattern ~108) and a server-level sub-nav. |
| 173 | + 3. Add an outline/tree component for Server→Workspace→section wayfinding. |
| 174 | + 4. Extend breadcrumbs to include the Server segment. |
| 175 | +- **Reuse:** existing workspace-scoped routes/components; `createScopedClient` to query a |
| 176 | + non-active server without switching the global active instance. |
| 177 | +- **Net-new:** server-scoped routes/loaders; outline component; Sidebar server context. |
| 178 | + |
| 179 | +### Phase 3 — Reposition standalone Fleet as an ops/settings view (needs brainstorming) |
| 180 | +**Goal:** decide Fleet's residual job once browsing lives in the Dashboard. |
| 181 | + |
| 182 | +- **Open decision:** does Fleet survive as a *health/ops* surface (per-server health, |
| 183 | + queue depth, version, auth status, reachability) — likely under Settings — or is it |
| 184 | + fully absorbed? Resolve in brainstorming. |
| 185 | +- **Where:** `src/components/fleet/*`, `src/components/settings/SettingsPage.tsx`, |
| 186 | + `Sidebar.tsx`. |
| 187 | + |
| 188 | +## Validation |
| 189 | + |
| 190 | +- After every change: `make ci-web` (lint + typecheck + test + build) must pass. |
| 191 | +- Targeted tests: `pnpm --filter @openconcho/web exec vitest run src/test/<file>`. |
| 192 | +- Manual: `make up` (docker) or `make dev-web` (Vite) with ≥2 instances configured; |
| 193 | + confirm the unified Dashboard lists workspaces across servers, the server filter |
| 194 | + narrows correctly, drill-down navigates Server→Workspace→section, and breadcrumbs are |
| 195 | + correct. Toggle demo mode (masking) and light/dark — both must stay correct. |
| 196 | +- Acceptance (write as Gherkin per [[gherkin-acceptance-criteria]] when planning): |
| 197 | + e.g. *Given two configured servers, When I open the Dashboard, Then I see each |
| 198 | + server's workspaces labelled `<workspace> (<server>)`; When I filter to one server, |
| 199 | + Then only its workspaces show.* |
| 200 | + |
| 201 | +## Open questions (resolve in brainstorming before Phase 2/3) |
| 202 | + |
| 203 | +1. **Instance selector vs Server node** — does the global switcher stay, get absorbed |
| 204 | + into the Server level, or coexist (switcher = "default server", drill-down = browse |
| 205 | + any)? |
| 206 | +2. **Routing** — `/servers/$serverId/...` nested routes vs server-as-state on the |
| 207 | + existing `/workspaces/...` routes. Affects deep links + breadcrumbs. |
| 208 | +3. **Fleet's residual role** — ops/health view under Settings, or fully absorbed? |
| 209 | +4. **Server-scoped aggregation** — do Peers/Sessions/Conclusions ever aggregate |
| 210 | + *across workspaces within a server* (new `useServer*` hooks), or is drill-down always |
| 211 | + Server→one Workspace→section (no cross-workspace union)? The latter needs zero new |
| 212 | + queries; the former needs new aggregation hooks in `compareQueries.ts`. |
| 213 | +5. **Responsive/mobile** behaviour of the outline/tree. |
| 214 | + |
| 215 | +## Conventions to honor |
| 216 | + |
| 217 | +- TanStack flat-route params: cast `as never` at every `navigate()`/`<Link>` callsite |
| 218 | + (see `Sidebar.tsx`, `PeerList.tsx`). Never hand-edit `routeTree.gen.ts`. |
| 219 | +- framer-motion: `import { type Variants }` and annotate variant objects; never |
| 220 | + `as const`. |
| 221 | +- CSS variables only for theme colors (`var(--text-1)` etc.) — no Tailwind color |
| 222 | + utilities. |
| 223 | +- Tests in `packages/web/src/test/`, behavior-focused, mock only at the fetch boundary |
| 224 | + (`@/lib/http`). Gate with `make ci-web`. |
| 225 | +- Conventional commits; one concern per PR; push under `offendingcommit`. |
| 226 | +- No environment-specific values in code/docs — use `honcho.example.net` / `192.0.2.x` |
| 227 | + (enforced by the pre-commit secret scan). |
0 commit comments