ADR-156: sitemap UX shape + role/auth handling
Status: Proposed Date: 2026-05-06 Related: warp#1551 (parent EPIC, fleet sitemap-as-source-of-truth), ADR-155 (S0.1 sister: sitemap.json schema), warp#1442 (sister: warp mobile-first auth UX, ADR-141), warp#1486 (sister structural pattern: docs-validate workflow)
Context
Sub-ticket S0.2 of EPIC warp#1551 (fleet sitemap-as-source-of-truth). Phase 0 ships three sister ADRs that lock contracts before Phase 1 substrate ships: S0.1 (ADR-155, shipped 2026-05-06 PR#664) names the JSON shape and per-stack discovery contracts; S0.2 (this ADR) names the /sitemap page UX; S0.3 names the CI gate semantics. ADR-155 tells consumers WHAT data exists; this ADR tells them HOW it renders. Without locking the UX before Phase 1 substrate ships, the discovery scripts could emit fields the renderer can't use OR omit fields the renderer needs.
Operator (jon, 2026-05-06) framed the parent EPIC as "all routes need to go on a sitemap page like Windows Explorer view, and this is for all sites. The system structurally checks and if a route is missing from it it doesn't pass." Phase 2's anchor consumer is warp's /sitemap page; Phase 3 rolls the same UX out fleet-wide. Without an ADR locking tree-vs-flat, role-indicator visual style, dynamic-route display, and 404 integration, every per-product /sitemap page re-debates the same surface. Sister to warp#1442 (warp mobile-first auth UX, ADR-141) — same mobile-first responsive split applied to the sitemap surface.
Decision
The /sitemap page renders the per-product sitemap.json (ADR-155) as a tree on desktop and an accordion on mobile, sorted alphabetically by path within each group with common prefixes folded into folders, with role-gated routes carrying a lock-icon indicator, dynamic segments rendered verbatim with template markers, and 404 pages reading the same sitemap.json to surface "did you mean?" suggestions. The shape has five parts: viewport-split layout, group-and-prefix sorting, role indicator, dynamic-segment display, and 404 integration.
1. Tree (desktop ≥ 768px) + accordion (mobile < 768px)
Desktop: collapsible Windows-Explorer-style tree. Top-level nodes are the four group values from ADR-155 (primary, user, admin, custom); each node expands to show the routes within that group. Mobile: same data, collapsed to one accordion panel per group; only one panel open at a time on first paint (the primary group). The breakpoint at 768px is the same threshold ADR-141 fixed for warp's auth UX — fleet-wide consistency reduces the cognitive cost of crossing surfaces.
DESKTOP (≥768px) — tree
┌─ primary ──────────────────────────────────┐
│ ▼ / Home │
│ ▼ /board │
│ /board/[id] Item Detail │
│ /board/new Create Item │
│ ▼ /sitemap Sitemap │
└────────────────────────────────────────────┘
┌─ user ─────────────────────────────────────┐
│ ▼ /account │
│ /account/profile 🔒 Profile │
│ /account/keys 🔒 API Keys │
└────────────────────────────────────────────┘
MOBILE (<768px) — accordion (one panel open)
┌────────────────────────────────────────────┐
│ ▼ primary (4) │
│ / Home │
│ /board Kanban │
│ /board/[id] Item Detail │
│ /sitemap Sitemap │
├────────────────────────────────────────────┤
│ ▶ user (2) │
├────────────────────────────────────────────┤
│ ▶ admin (3) 🔒 │
└────────────────────────────────────────────┘
2. Group sorting + common-prefix folding
Within a group, routes sort alphabetically by path ascending. When 3 or more routes share a leading path segment (e.g. /account/profile, /account/keys, /account/billing), the renderer folds them under a synthesised /account/* folder node — the folder is a visual grouping, not a route. The "3+ shared" threshold matches warp's existing nav-folding behaviour and avoids single-child folders that add noise without aiding scanning. Folder nodes are alphabetised among their sibling routes by their shared prefix.
3. Role-gated routes — lock-icon indicator
Routes with role: "user" or role: "admin" (per ADR-155 schema) render with a lock icon next to the title. The icon is the SAME lock glyph regardless of role; tooltip on hover names the required role (Requires user role / Requires admin role). Operator-facing surface (this ADR's scope) shows ALL routes regardless of role, with the indicator. User-facing variant (DEFERRED to Phase 3 — see Out-of-scope) hides admin-role routes entirely.
TREE NODE WITH ROLE INDICATOR
/account/profile 🔒 Profile ← role: "user"
/account/keys 🔒 API Keys ← role: "admin" (operator-facing)
/ Home ← role: null, no indicator
4. Dynamic routes — verbatim template markers + italic indicator
Dynamic segments render verbatim using the template markers ADR-155 locked at emit time (/board/[id], NOT /board/123 or /board/<id>). The dynamic: true flag drives a visual distinction: the path renders in italics. Italics over a trailing badge avoids horizontal-bandwidth contention with the lock icon and does not require a separate badge component; the italic style alone is unambiguous against the static-route default-weight rendering.
DYNAMIC-SEGMENT RENDERING
/board Kanban
/board/[id] Item Detail ← dynamic: true → italics
/board/[id]/edit Edit Item ← dynamic: true → italics
/account/profile Profile ← dynamic: false → upright
5. 404 integration — path-similarity suggestions
The 404 page reads the same sitemap.json and surfaces "did you mean?" links to the top 3 closest matches. Two parallel similarity checks: Levenshtein distance normalised to a 0-1 score (1.0 = identical), and longest-common-prefix as a percentage of the requested path's length. A route qualifies for suggestion if Levenshtein ≥ 0.6 OR shared-prefix ≥ 50%. If no route qualifies, the 404 falls back to a generic "this page doesn't exist" message with no suggestions. Two thresholds (rather than one) catch the two failure modes: typos (/boad/123 → /board/[id], Levenshtein-strong) and over-specification (/account/profile/settings/advanced → /account/profile, prefix-strong).
Consequences
What becomes easier. The Phase 2 anchor consumer (warp's /sitemap) ships against a fixed UX contract; per-product /sitemap pages in Phase 3 inherit the same shape without re-debating tree-vs-flat, role-indicator style, or 404 integration. The 404 page becomes a structurally-grounded surface — it reads the same committed JSON the gate validates, so suggestions are never stale and never invent routes. Mobile and desktop renderers share the same data and the same component tree, differing only in the layout primitive (tree vs accordion).
What becomes harder. Every fleet product's /sitemap page now ships with a renderer that handles tree + accordion + folding + role-indicator + dynamic-segment styling — non-trivial component code, with regression-test surface across viewport sizes. The 404 page acquires a runtime dependency on sitemap.json (load on render, with a cached fallback if the file is unreachable). The "3+ shared prefix" folding rule is a UX heuristic; edge cases (a group with 50 routes all sharing a 2-segment prefix) need design follow-ups in the Phase 2 implementation.
What we sign up to operate. The renderer component lives in fleet-shared component territory (likely gyrum-ui per ADR-139) and ships under that package's release cadence. The 404 page's similarity thresholds (0.6 Levenshtein, 50% prefix) are tunable in code; bumping them is a follow-up ADR to keep cross-product consistency. The user-facing variant (Phase 3) is explicitly out-of-scope here; when Phase 3 lands it must NOT change the operator-facing variant's shape — only filter the input data.
Alternatives considered
Flat-list rendering (no tree, no accordion). Lost: at fleet scale a flat list of every route on every product is unscannable; the
groupfield in ADR-155 exists precisely to give the renderer a partition. A flat-list renderer ignores the schema's grouping intent.Trailing badge for
dynamic: trueroutes (instead of italics). Lost: badges contend with the lock icon for trailing horizontal real estate; on mobile this crowds the row. Italics over the path text is unambiguous and viewport-independent. Italics also degrade gracefully if the renderer's typography is constrained — the path is still readable.Accordion on desktop too (no tree). Lost: at desktop widths a nested tree is the dominant pattern for hierarchy navigation (file explorers, IDE project panes); collapsing to accordion on desktop loses the recognisable affordance and forces users to expand-collapse panels rather than scan a tree.
Render dynamic segments with concrete examples (
/board/123). Lost: the example value is arbitrary and misleading — users may type123literally and 404. ADR-155 fixes the verbatim-template rule at the schema level; this ADR mirrors it at the renderer level.404 page uses only Levenshtein OR only longest-common-prefix. Lost: each metric catches a failure mode the other misses (typos vs over-specification, see §5). One metric leaves a class of legitimate suggestions un-surfaced.
404 page generates suggestions from server-side route enumeration (not
sitemap.json). Lost: forks the source of truth — the gate validatessitemap.json, so the 404 page suggesting routes from a separate enumeration could surface routes the gate considers stale. Single-source-of-truth keeps 404 honest by construction.
Out-of-scope
- The actual
/sitemappage implementation — Phase 2 ships it; this ADR locks the UX-spec only. - The 404 page's CURRENT shape (Phase 2 substrate) — this ADR locks the integration contract only.
- User-facing sitemap variant (hides admin routes entirely) — Phase 3 fleet rollout, separate sub-ticket.
- S0.3 gate semantics — separate phase-0 child ADR.
Supersedes: none — extends EPIC warp#1551; sister to S0.1 (ADR-155, shipped) and S0.3 (gate semantics ADR, separate child). Superseded by: