# Requests page redesign

**Date:** 2026-08-16  
**Status:** implementation-ready  
**Owner-visible evidence:** the installed `/requests` page reproduces the supplied four-column board, owner-action drawer, and deliveries band with live collector data; a browser screenshot records both the full board and an open drawer.

## Purpose

The page must answer, in under five seconds: what did I ask, where is it, what happened last, and do I need to act? It is a read-and-decide surface for an owner who may not remember making the request.

The supplied image is the visual source of truth. The owner has already chosen the layout, hierarchy, and light visual direction; no alternative mockup is required.

## Scope

Redesign the existing `/requests` route and drawer. Preserve the existing collector query and request mutation seams. Do not add drag-and-drop or a hand-maintained workflow. Do not fabricate data that the collector does not supply.

### Included

- One compact pulse strip containing title, text search, four status totals, project filters, and feed freshness.
- Four fixed lifecycle columns: Asked, In flight, Blocked — needs you, and Shipped.
- Visually distinct blocked column as the only owner-action area.
- Compact request cards whose first line is the recorded request title.
- In-flight progress vocabulary `spec → build → verify → land → deploy → live`; unknown current stage is explicitly labeled, never inferred.
- Shipped grouping by Today, Yesterday, This week, and Earlier, with recorded proof links only.
- Right-side `DetailDrawer` with original request, plain status, activity, recorded links, repeated-ask coverage state, and applicable actions.
- Collapsible deliveries band. It derives only from recorded landed/deployed receipts. If no authoritative delivery history is available, it says so.
- Responsive containment: board and drawer fit the viewport; each column scrolls independently; narrow view scrolls the board horizontally without causing body overflow.
- Both themes, token-backed values, keyboard focus, and semantic buttons/links.

### Deferred because the current API does not provide authoritative fields

- Numeric WIP limit.
- Per-request current build stage.
- Related-request links.
- Worker heartbeat/quiet threshold.
- Separate spec, branch, and landing URLs.
- Repeated-ask message linkage.
- Bump priority, cancel, and “not what I meant” mutations unless matching collector endpoints already exist.

Each missing value renders as a named coverage gap where the image reserves its place; it is never guessed from free-text receipts.

## Architecture

### `RequestsBoard`

`RequestsBoard({ requests, onSelect }): JSX`

Owns only local presentation state: search query, active project, and optimistic removal after an answer. It derives lifecycle columns, summary totals, freshness, shipped groups, and delivery events from `RequestRow[]` through pure helpers.

The top strip, columns, card variants, and deliveries disclosure remain internal presentation units because they share one filter/state model and have no second consumer. Generic controls come from Astryx; overdeck-specific utilities and the existing drawer come from deck-ui.

### Request-board derivations

Pure functions in `apps/web/src/lib/request-board.ts` remain the authoritative seam for sorting, filtering, grouping, relative labels, and honesty decisions.

Required additions:

- `latestRequestEvent(row): RequestEventView | null` chooses the newest authoritative receipt or transition and returns its recorded line and timestamp.
- `boardFreshness(rows, now): { state: 'fresh' | 'stale' | 'unknown'; updatedAt: number | null }` never treats an empty result as fresh.
- `deliveryEvents(rows): DeliveryEventView[]` includes only receipt kinds `landed` and `deployed`, newest first.
- Shipped groups sort newest first within each bucket.

No helper parses prose to infer stage, queue depth, duplication, or heartbeat.

### `RequestDrawer`

`RequestDrawer({ request, onClose }): JSX`

Reuses `DetailDrawer`. It presents:

1. the full recorded title and asked date;
2. one plain-English status paragraph;
3. the ordered receipt story, with routine items individually expandable only when structured detail exists;
4. available links;
5. repeated-ask coverage state;
6. applicable mutation controls.

`Answer` remains the only enabled mutation until other collector routes exist. Unsupported actions are not rendered as working controls.

### Styling

`apps/web/src/styles/requests-astryx.css` maps Astryx variables to deck tokens and owns page-specific composition classes. React components select semantic classes; they do not introduce raw colors, shadows, radii, or transition values.

The visual language follows the supplied image: quiet neutral surfaces, thin borders, dense cards, blue current-selection emphasis, green evidence dots, and red only for unresolved owner action.

## Data flow

```mermaid
flowchart LR
  Collector[Collector requests endpoint] --> Query[useRequests]
  Query --> Board[RequestsBoard]
  Board --> Derive[Pure board derivations]
  Derive --> Pulse[Pulse strip]
  Derive --> Columns[Lifecycle columns]
  Derive --> Deliveries[Deliveries band]
  Board -->|select request id| Drawer[RequestDrawer]
  Drawer -->|answer blocked request| Collector
```

The query boundary owns loading/error handling. The page never promotes fixture content into live UI.

## Error and stale states

- Query failure remains visible through `CollectorQueryBoundary`.
- No requests: “Nothing in flight. Ask for something.”
- Empty blocked column: “Nothing needs you.”
- Missing proof: “Proof not recorded.”
- Missing current stage: “Current step not recorded.”
- Missing related work, heartbeat, WIP limit, repeated asks, or delivery history: omit decorative markers and show one concise coverage message at the reserved surface where needed.
- A stale newest `updated_at` becomes an explicit warning in the pulse strip. The threshold is a presentation constant covered by tests and must not imply that individual workers are dead.

## Testing

### Focused

- Pure helper tests: search only matches owner-facing text, queue ordering, newest-first shipped groups, event selection, freshness states, and delivery receipt projection.
- Board component tests: pulse totals, project filtering, blocked empty state, honest unknown labels, owner-facing card language, drawer selection, and delivery disclosure.
- Drawer tests: full title/date, status paragraph, activity ordering, safe proof URL, absent-link states, and answer availability only for blocked work.

### Static and broad

Follow `/od-testing`: focused tests, typecheck/build and `git diff --check`, then the mandatory deck-ui/web suites once.

### Installed owner flow

After deploy from `origin/main`, enter `/requests` through normal navigation using live collector data, search request text, filter a project, open one card, inspect the activity and recorded links, expand Deliveries, and verify the Blocked column is the sole owner-action region. Capture the resulting viewport in both themes when live data supports the state.

## Architecture decisions

- **Collapsed separate card modules into `RequestsBoard`:** their variants share the same request contract and have no independent consumer; splitting files would be organizational rather than a deep seam.
- **Kept derivations outside React:** sorting, freshness, activity selection, and delivery projection are independently testable and stable across visual refactors.
- **Kept the existing collector contract:** a UI redesign must not invent a second status source.
- **Rejected free-text inference:** parsing receipt prose for stages, heartbeat, queue depth, or related work would violate honest-data requirements.
- **Rejected a new deck-ui board primitive:** this route is already Astryx-backed and no second overdeck consumer justifies a new shared primitive.
