diff --git a/docs/navigation-audit.md b/docs/navigation-audit.md index 9720dd82..f68f1b3f 100644 --- a/docs/navigation-audit.md +++ b/docs/navigation-audit.md @@ -37,14 +37,14 @@ That fix is the template for the HIGH items below. **Caveat:** the fix only did **Existing entity routes:** `/chat/:sessionId`, `/jobs/:id`, `/dashboards/:id`, `/email/:emailId`, `/browser/:tabId`, `/tasks/:dirName`, `/skills/:dirName`, `/processes/:dirName`, `/task-logs/:id`, -`/activity/:id`. (`/projects/:id` +`/activity/:id`, `/plans/:name`. (`/projects/:id` was in this list; the Projects feature was removed end to end on 2026-07-30, so the route is gone with it. H3 and navigate-site 22 below still name it — they are the record of work that happened, not of code that exists.) **Section routes** (a param that names a view rather than an entity): `/settings/:page/:section`, `/soulseek/:section`, `/headscale/:section`, `/wallet/:section`, `/system-monitor/:scope`. **Query-param screens:** `/music?path=`, `/files?path=`, `/code-editor?file=`. -**Genuinely flat, still:** `/` `/plans` `/terminal` `/desktop` `/qr-transfer`. Of these only `/plans` selects -something (M9); the other four have nothing addressable to put in an address. +**Genuinely flat, and correctly so:** `/` `/terminal` `/desktop` `/qr-transfer` — none of them selects +anything, so there is nothing to put in an address. The five settings pages are route **pairs** now, not flat screens — `/settings/{profile,ai,system,integrations,user-management}` plus a `:section` each (M6). There has never been a `/settings/apps`; that entry was wrong when this list was written. @@ -85,7 +85,7 @@ are good building blocks. The **Workspace/Panel framework** contains **zero** ro | ~~M6~~ | `Settings/SettingsPanel.tsx` | a settings sub-section | `/settings/:page/:section` | **Done.** `` + `useParams`, five `*_SELECTED` globals gone, one `SettingsRoute` guard per page. The "one change covers all settings pages" claim was *almost* right: Integrations builds its own sidebar and did not go through `createSettingsPanelComponents`, and it also held the Enterprise/Personal tab in a second global — derived from the section key now, which is what fixes deep-linking a Personal section. | | ~~M7~~ | ~~`workspaces/components/Combobox.tsx:53`~~ | caller-supplied route | — | **Deleted, not fixed.** "Every caller inherits the opaque click" was the reason this ranked MEDIUM, and it is wrong: `Combobox` has **no callers**. Nothing has imported it since the initial commit, there is no barrel export, and nothing anywhere sets `href` on a `SelectOption` — so the navigate, the separator that only showed for `href` options, and the `href` field on both declarations of the type were all unreachable. Writing anchor semantics into a component that is never rendered is building, not fixing. Its `Command` primitives stay; `AIHarnessesSection` uses them. | | ~~M8~~ | `Layout/Header/UserMenu.tsx` | — | — | **Done.** Removed rather than routed: nothing had ever been built behind `/settings/resources`, so the item was a bounce to `/` dressed as navigation. Its `header.userMenu.resources` locale keys went with it. | -| M9 | `Screens/Dashboard/Plans/index.tsx:36` | a plan document | `/plans/:name` | native `` navigates instead of setting state; it stays a `` navigates instead of setting state. It stays a `` rather than becoming a link list. It is chrome for one document, + * not a master list, and a ` setSelectedPlan(ev.target.value)} - className="text-xs border border-duck-dark/20 rounded px-2 py-1 bg-background/80 text-duck-dark" - > - {plans.map((p) => ( - - ))} - - )} - + +
+ Plans + {plans.length > 0 && ( + + )} +
- {/* Markdown content */} -
+
+ {selected ? (
{content}
-
- + ) : ( +

+ {plans.length === 0 ? 'No plans yet.' : 'Pick a plan to read it.'} +

+ )} +
+
); }; diff --git a/src/servers/api/plans/plans.ts b/src/servers/api/plans/plans.ts index 16f3b178..0200068b 100644 --- a/src/servers/api/plans/plans.ts +++ b/src/servers/api/plans/plans.ts @@ -1,6 +1,6 @@ import { createRouter } from '../../create-router'; import { readdir } from 'node:fs/promises'; -import { join } from 'node:path'; +import { basename, join } from 'node:path'; const plansDir = join(process.cwd(), 'plans'); @@ -17,7 +17,11 @@ plansRouter.get('/', async (ctx) => { }); plansRouter.get('/:name', async (ctx) => { - const name = ctx.req.param('name'); + // A single path segment is not a single *name*: hono percent-decodes params, so `..%2F..%2Fsecret` + // arrives here as `../../secret` and `join` would happily walk out of plansDir. Verified against hono + // directly. Auth limits the blast radius to the owner's own token, and the `.md` suffix limits it to + // markdown, but "read any .md on the disk" is not what this endpoint is for. + const name = basename(ctx.req.param('name')); const filePath = join(plansDir, `${name}.md`); const file = Bun.file(filePath); if (!(await file.exists())) return ctx.text('Not found', 404);