Saved Views & Deep-Link Parameters — Proposal (Phase 3B)
Status: PROPOSAL — nothing here is built. Authored 2026-08-07 per the completion-sprint charter ("3B needs product decisions on view schemas — propose, don't build"). Every capability below is a design target.
Decision owner: program owner. Build starts only after the open questions in §5 are answered.
1. What exists today (measured, 2026-08-07)
- One nav SSOT (
config/navigation.ts, test-locked: 101 internal paths resolve against the App Router tree) and a command palette (components/CommandPalette.tsx, ⌘K/Ctrl+K) derived from it. - Live Command Center surfaces render tri-state (live | unavailable |
error) over
/api/bff/*reads; their query inputs (decisions status filter, evidence domain + as_of, audit lookups by id) are held in component state only — a reload or shared link loses them. - No route today reads filter state from
searchParams; deep links land on default state. No saved-view concept exists anywhere.
2. Deep-link parameter conventions (the smaller, safer half)
Proposal: make the existing query inputs URL-addressable, page by page, using one convention — no new storage, no schema, reversible per page.
| Surface | Params | Notes |
|---|---|---|
/command-center/decisions |
?status= (validated enum) |
mirrors the BFF status filter; invalid → ignored + honest chip |
/command-center/events |
?domain=&as_of= (ISO-8601) |
the point-in-time query the page already validates server-side |
/command-center/approvals |
?view=pending\|history |
tab selection only; never an authz input |
/command-center/audit |
?decision_id= (id-format guard reused) |
pre-fills the lookup, still requires explicit run (no auto-fire on load — C4) |
/cells |
?filter= (stage/status token) |
client-side filter of the static registry |
Rules: (a) params are presentation state only — tenant/identity/role
NEVER come from the URL (1B law: identity from middleware-stamped headers
only); (b) every param is validated exactly like the corresponding BFF
input, silently dropped when invalid; (c) params never trigger mutations
or auto-execute lookups that cost money; (d) router.replace keeps
history clean on filter changes.
3. Saved views (needs product decisions first)
A saved view = named bookmark of (route + validated params + UI toggles).
Proposed schema (v0, deliberately minimal):
interface SavedView {
id: string; // uuid
name: string; // 1–60 chars, user-supplied
route: string; // must resolve in the nav SSOT route test
params: Record<string, string>; // only keys the target page declares
createdAt: string; // ISO
schemaVersion: 1;
}
Two candidate storage tiers — this is the product decision:
- Tier 1 (local-only): localStorage per browser, no backend, ships in a day, zero tenancy questions. Loss on device switch.
- Tier 2 (server-side): per-user per-tenant persistence (Firestore or a governance-service read/write pair). Requires: verified identity on every write (1B), tenant scoping, an HD-4-class backend scope decision, and a migration story for Tier-1 views.
Palette integration: saved views surface in the ⌘K palette under a
"Views" group, navigating via toSafeUrl like every other entry.
4. Explicitly out of scope
- Sharing views between users (authorization surface — separate decision).
- Views that capture live DATA snapshots (truth-discipline hazard: a stored snapshot ages into an unlabeled stale claim).
- Any view that encodes tenant/identity/role.
5. Open questions for the owner
- Tier 1 (local-only) now, Tier 2 later — or wait and ship Tier 2 once?
- If Tier 2: Firestore under the UI's existing project, or a governance service (adds HD-4-class backend scope)?
- Should deep-link params (§2) ship independently ahead of saved views? (Recommended: yes — they are prerequisite plumbing either way.)
- Naming/limits: max views per user (proposed 50), rename/delete UX.
Companion artifacts: FRONTEND_MIGRATION_LEDGER.md (3B row), config/navigation.ts (route SSOT), CommandPalette.tsx.