Docs / console/design-patterns.md · mirrored from the repo
BO Console — Object Presentation Patterns
Living catalog of system-design patterns for visualizing data objects in the back-office console. The premise: data objects fall into a small, easy-to-identify taxonomy, and each shape of object should be presented and behave the same way everywhere via reusable patterns — never designed bespoke per page. Identify the object’s shape first, then apply the matching contract below. New object types should slot into these patterns, not invent their own.
Already in practice: canonical routed object pages reused across surfaces (PR-E/PR-F). These patterns generalize that instinct to every object and every surface.
Object taxonomy
Most things on screen are entities with a lifecycle: brief, shot, asset,
run, batch, agent, … Each has: an identity, a status/lifecycle, relationships
(lineage), and actions. A few are collections (lists of entities) or events
(append-only run/audit log lines). Classify first; the contract follows from the class.
Patterns
1. Object presentation contract
Every entity kind declares, in one place: its list renderer, its detail renderer, its status vocabulary, and its actions (single + bulk). UI surfaces compose from these declarations rather than re-implementing per page.
2. One detail component, many hosts
An object’s detail view is a single component reused by every host — routed page,
drawer, modal, preview. A routed /briefs/:id page and a ?panel=brief~id drawer must
render the same component. (Current: BriefDetail, the *Detail renderers in
PipelineDrawer.)
3. Stack-based overlays (drawers/sheets)
Overlays are a push/pop navigation stack, not independent open flags. Opening from
within an overlay pushes; cancel/Escape/backdrop pops (reveals previous); close-all
clears. Stack serialized to one URL param, backed by browser history, deep-linkable,
refresh-safe. Render as a capped cascade. See the drawer-stack item in /TODO.md.
4. Lineage / relationship walking
Related objects (e.g. asset → shot → brief) are navigable without losing context — walking lineage pushes onto the overlay stack (pattern 3), never destroys where you came from.
5. Server-authoritative collections
All search / filter / sort / pagination for collections happens on the server/API.
The client never filters a fully-loaded list (counts will bloat fast). Query state
(filters, search, sort, page/cursor) lives in the URL (useSearchParams) so it
round-trips to the server and survives refresh.
6. Status & lifecycle as data
An object’s lifecycle state is data, rendered by one shared Badge with a consistent
status vocabulary and color mapping per object kind — never ad-hoc inline styling.
7. Loading taxonomy
Distinguish two async states and render them differently:
- Skeleton — data is being fetched (list/detail load). Show shape-matched skeleton placeholders.
- Progress / activity — work is in flight (generating, vectorizing, rendering,
batch running). Show an animated running-step indicator (see the pipeline
loaders/animation items in
/TODO.md).
8. Consistent actions
Per-object actions (and bulk actions on collections) use the same affordances, confirmation patterns, and permission gating (RBAC) wherever that object appears.
9. Data type → control / visual mapping
The type of a field determines the control and visual used to display and filter it — this is global, not a per-page decision. Identifying the mapping should be obvious from the data’s shape. Canonical mapping:
| Field shape | Display | Filter control |
|---|---|---|
| Categorical, multi-value (status set, language, geo, agent, tags) | Chips / badges | Multi-select (a row can match any of several selected values) |
| Categorical, single-value | Label / badge | Single-select |
| Boolean | Toggle / check | Toggle or tri-state (on / off / any) |
| Free text (title, caption) | Text | Search box (server-side, debounced) |
| Date / time | Formatted / relative | Date range picker |
| Number / count | Numeric | Range / comparator |
| Enum lifecycle status | Shared Badge (pattern 6) | Multi-select over the status vocabulary |
| Relationship (lineage ref) | Link that pushes onto the drawer stack (patterns 3–4) | Select by referenced object |
Default rule of thumb: if a field can legitimately hold or match more than one value, its filter is multi-select. Filter state still lives in the URL and is applied server-side (patterns 5). Build these as shared filter-control primitives so every collection composes the same controls rather than hand-rolling them.
10. Live updates via server push, not polling
Freshness comes from the server pushing events, not the client polling. One
long-lived stream (SSE GET /api/stream) per session replaces all per-page polling
loops. A Durable Object event hub in console-api fans out events emitted by producers
(agents/pipeline steps already POST run events to the /internal/run-events sink) to all
subscribers. Every event carries { type, object: {kind, id}, ts, ... } so the client
does targeted revalidation of the affected object (pattern 1/2) — not a blanket
refetch. Drives the running-step animation (pattern 7) live. On reconnect, replay via
Last-Event-ID or one reconciling refetch. Same stream is the notifications substrate
(a notification event type; later fans out to Web Push from the same hub). SSE now;
upgrade to WebSocket (Agents SDK/PartySocket, already a dep) only if bidirectional/
presence/low-latency is needed.
Extend this doc whenever a new presentation decision is made — capture it as a pattern so it applies to every object of that shape, not just the screen that prompted it.