document the navigation conventions in claude.md
the url-as-source-of-truth rules from docs/navigation-audit.md were only in the audit; anything new was as likely to reach for a selection channel as a link. also records the workspace-barrel requirement for shared route helpers, and adds officer-headscale to the pm2 list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ Long-running and privileged work lives in **sidecars**: separate processes that
|
||||
`/api/sidecar/register` and are tracked in `src/servers/sidecar-registry.ts`. PM2 runs them
|
||||
(`ecosystem.config.cjs`): `officer` (the server), `officer-anthropic-proxy`, `officer-agent`,
|
||||
`officer-opencode`, `officer-email`, `officer-pty`, `officer-vnc`, `officer-music`, `officer-vault`,
|
||||
`officer-slskd`.
|
||||
`officer-slskd`, `officer-headscale`.
|
||||
|
||||
**`officer-anthropic-proxy` and `officer-agent` are not the same thing.** The proxy holds the Anthropic
|
||||
credential and forwards API traffic; the agent is the process that spawns `claude`. They were one entry
|
||||
@@ -205,14 +205,49 @@ Worked examples: `/soulseek`, `/music`, `/chat`.
|
||||
that pins `appType`s to an allow-list. Panels are windowed apps under
|
||||
`src/workspaces/officerdev/src/apps/<Feature>/`, each exporting `appRegistryMetas`
|
||||
(`{ key, name, icon, component, availableOnPanel: false }`) and registered in `AppRegistry.tsx`.
|
||||
Panels coordinate via `usePanelChannel`.
|
||||
The Workspace/Panel framework is orthogonal to routing — it contains no route navigation, and panels
|
||||
live inside the Route element tree, so they can call `useParams`/`useSearchParams` directly.
|
||||
- **Page title by route.** Add a rule to `RULES` in `src/apps/officer-web/state/usePageTitle.ts`
|
||||
(`{ match: (p) => p.startsWith('/<name>'), title: '<Name>' }`, most-specific first);
|
||||
`usePageTitleSync` does the rest.
|
||||
`usePageTitleSync` does the rest. `startsWith` means nested routes are covered.
|
||||
|
||||
### The URL is the source of truth for selection
|
||||
|
||||
`docs/navigation-audit.md` is the authority here — read it before building a screen that selects
|
||||
things. It names the **"opaque click" anti-pattern**: an element that opens something addressable but
|
||||
keeps the id in an onClick closure instead of the DOM, leaves the URL unchanged, holds the selection
|
||||
in `usePanelChannel`/`useGlobal`, and has no anchor semantics (no cmd-click, no middle-click, not
|
||||
link-focusable). Half the app still does this; none of the new code should.
|
||||
|
||||
- **Addressable state goes in the URL** (`useParams` / `useSearchParams`), never in a channel.
|
||||
`usePanelChannel` is for genuine signals and refresh buses (`preview:refresh`,
|
||||
`files:refresh-signal`, `SLSKD_REFRESH_CHANNEL`, `MUSIC_RESYNC_CHANNEL`, `FILE_VIEWER_CHANNEL`).
|
||||
"Which thing is open" is a URL. Panels each read the URL rather than passing it between themselves.
|
||||
- **Rows and nav items are real links.** `<Link>` for rows (exemplar: the `/chat` session list,
|
||||
`f35c145`); **react-router's `<NavLink>`** for nav chrome, so active state comes from the router.
|
||||
The hand-rolled `isActive` in `Dock`/`Header` is scheduled for replacement (audit Phase 4) — don't
|
||||
copy it. A disabled entry renders as a `<span>`; a disabled `<a>` is not a thing. A control that
|
||||
*mutates* rather than navigates stays a `<button>`.
|
||||
- **Route pairs.** A bare screen route plus a param route rendering the same component: `/chat` +
|
||||
`/chat/:sessionId`, `/jobs` + `/jobs/:id`, `/email` + `/email/:emailId`, `/headscale` +
|
||||
`/headscale/:section`. One `<Navigate … replace />` guard in the screen, placed after all hooks,
|
||||
canonicalises both the bare route and a bogus param.
|
||||
- **Master list + live preview uses `?selected=<id>`**, not the detail route — linking rows straight
|
||||
to `/x/:id` destroys preview-on-list, because that route is the full page. Rows link to
|
||||
`/x?selected=id` on-page and `/x/:id` off-page; list, mobile panel and preview all read the param.
|
||||
Action buttons are **siblings** of the anchor, never nested inside it.
|
||||
- **`src/workspaces/components/NavLink.tsx` is not react-router's `NavLink`** — it's a `<Link>`
|
||||
wrapper that appends `useGlobalQueryString()` and provides no active state. Import react-router's
|
||||
when you want `isActive`.
|
||||
- Route helpers shared between an `officerdev` panel app and an `officer-web` screen must be
|
||||
re-exported from `src/workspaces/officerdev/src/index.ts` (named exports only — the barrel
|
||||
deliberately avoids `export *` for app modules to keep `appRegistryMetas` from colliding).
|
||||
|
||||
## Further Reading
|
||||
|
||||
- `CONVENTIONS.md` — component organisation, state management, React patterns, with rationale
|
||||
- `docs/navigation-audit.md` — **authoritative** on routing/navigation: the opaque-click anti-pattern,
|
||||
a severity-ranked findings table, the channel-selection map and the four-phase plan
|
||||
- `TODO.md` — current direction and deferred work; **takes precedence over this file where they disagree**
|
||||
- `src/apps/CLAUDE.md` — shared frontend patterns
|
||||
- `src/databases/CLAUDE.md` — database patterns
|
||||
|
||||
Reference in New Issue
Block a user