Deal detail page redesign — Implementation Plan

15 TDD tasks. One commit per task. New full-page deal detail view at /admin/queue/$dealId replaces the slide-out modal. Real Postgres + MSW + respx for tests.

Plan 2026-05-23 · companion to docs/superpowers/specs/2026-05-23-deal-detail-page-redesign-design.md

TL;DRSummary

What this plan ships

Replaces the RunDetailSheet slide-out with a full-page detail view at /admin/queue/$dealId. Adds one VCC client method (get_appraisal_contents), one backend route (GET /admin/queue/deals/{deal_uuid}/appraisal), nine nullable per-item appraisal fields on DealItemBrief, seven new frontend components under web/src/components/deal-detail/, and a new file route.

Architecture: No DB migrations. New appraisal fields stay null until sub-project B builds the appraisal stage. The *IO components grow a temporary hideItems prop so they can be reused in the new StageDetailPanel without their items grid; the prop and items-rendering code are removed entirely in the final cleanup task. Auto-poll 3 s while any stage is running.

§0Working agreements

§1File map

Created

PathResponsibility
backend/tests/test_vcc_client_appraisal_contents.pyrespx-mocked unit tests for the new VCC client method
backend/tests/test_appraisal_route.pyRoute tests for the new appraisal endpoint
web/src/components/deal-detail/DealHeader.tsx (+ test)Identity / pipeline / VCC state card
web/src/components/deal-detail/DealTimeline.tsx (+ test)Horizontal pipeline stage strip; unifies PipelineTrail + TimelineTab
web/src/components/deal-detail/ItemCard.tsx (+ test)Read-only per-item card
web/src/components/deal-detail/ItemsGrid.tsx (+ test)Page-level always-visible items grid
web/src/components/deal-detail/StageDetailPanel.tsx (+ test)Wraps trimmed *IO components + inline error block
web/src/components/deal-detail/VccAppraisalCard.tsx (+ test)VCC human-appraisal display
web/src/components/deal-detail/LogsDrawer.tsx (+ test)Slide-up unified-log drawer with stage-boundary markers
web/src/routes/admin.queue.$dealId.tsx (+ test)New file route assembling the page

Modified

PathChange
backend/.../vcc/client.pyAdd VccClient.get_appraisal_contents
backend/.../api/routers/admin.pyAdd GET /admin/queue/deals/{deal_uuid}/appraisal + AppraisalContentsResponse
backend/.../features/deals/schemas.py9 nullable fields on DealItemBrief
backend/.../features/deals/service.pyComment in get_deal_detail noting null defaults
backend/tests/test_deal_detail_schemas.py + test_deal_queue_service_detail.pyCover new fields
openapi.yaml + web/src/lib/api/generated/*Regenerated (T4)
web/src/lib/api/adapters/deal-detail.ts (+ test)ItemInfo grows 9 new fields
web/src/components/queue/tabs/io/*IO.tsxAdd hideItems prop (T10), remove items-rendering entirely (T15)
web/src/routes/admin.production.queue.tsxRow click navigates; backwards-compat redirect for ?run=

Deleted (T15)

PathReason
web/src/components/queue/RunDetailSheet.tsx (+ test)Slide-out replaced by full page
web/src/components/queue/PipelineTrail.tsxFolded into DealTimeline
web/src/components/queue/tabs/TimelineTab.tsxRedundant with timeline
web/src/components/queue/tabs/IOTab.tsx (+ test)Replaced by StageDetailPanel
web/src/components/queue/tabs/LogsTab.tsx (+ test)Replaced by LogsDrawer
web/src/components/queue/tabs/ErrorTab.tsx (+ test)Inlined into StageDetailPanel

§2Phase 0 · Backend foundation

T1VccClient.get_appraisal_contentstddmodify
integrations/vcc/client.py · tests/test_vcc_client_appraisal_contents.py
New async method on the existing VccClient. respx-mocked tests: happy path; 500 with "No Appraisal Found" → None; other 500/4xx/transport → VccError; x-api-key header set; non-dict 500 body raises. ~7 tests total.
T2Appraisal route + response schematddmodify
api/routers/admin.py · features/deals/schemas.py · tests/test_appraisal_route.py
GET /admin/queue/deals/{deal_uuid}/appraisal: 404 on unknown deal; 200 + payload from VccClient; 200 + null when VCC returns None; 502 on VccError. Response model has model_config = ConfigDict(extra="allow") so we accept whatever VCC returns.
T3Extend DealItemBrief with 9 nullable appraisal fieldstddmodify
features/deals/schemas.py · features/deals/service.py · tests/test_deal_detail_schemas.py · tests/test_deal_queue_service_detail.py
ai_item_name, condition, material, value_low, value_high, value_currency, triage_route (Literal of 5 routes), signals (dict[str, bool]), appraisal_status (Literal). All default None. Service's DealItemBrief construction picks up the defaults automatically; one new test asserts the fields are null until populated, one asserts round-trip when populated.
T4Regenerate openapi.yaml + frontend codegenmodify
openapi.yaml · web/src/lib/api/generated/*
make openapi then bun run codegen. Stage only the relevant openapi.yaml hunks — the user's TestSegmentationRequest hunk is unrelated WIP. Use git add -p to pick the appraisal-route and DealItemBrief hunks only.

§3Phase 1 · Adapter

T5Extend ItemInfo in deal-detail.ts adaptertddmodify
web/src/lib/api/adapters/deal-detail.ts (+ test)
Add aiItemName, condition, material, valueLow, valueHigh, valueCurrency, triageRoute, signals, appraisalStatus to ItemInfo (camelCased). Adapter copies them through. Two new tests cover null-default and populated round-trip.

§4Phase 2 · Components

T6ItemCard — read-only per-item summarytddcreate
web/src/components/deal-detail/ItemCard.tsx (+ test)
Thumbnail, title (AI item name or category/subcategory fallback), category line with confidence %, physical size, AI value range with currency symbol, condition/material line, triage badge with route-specific color, signal chips (only true-valued flags), status badge, source-label reference. ~10 tests covering field collapsing.
T7ItemsGrid — responsive grid of cardstddcreate
web/src/components/deal-detail/ItemsGrid.tsx (+ test)
2 → 5 columns by breakpoint. Empty state when items=[]. Clicking thumbnail opens existing Lightbox (no new lightbox component). 3 tests.
T8DealHeader — identity + pipeline + VCC statetddcreate
web/src/components/deal-detail/DealHeader.tsx (+ test)
Three rows: identity (hubspot id, internal UUID, country), our pipeline (status badge + current stage), VCC state (stage_label, is_terminal flag, amount, with "as of N min ago" freshness hint). 4 tests.
T9DealTimeline — horizontal clickable stage striptddcreate
web/src/components/deal-detail/DealTimeline.tsx (+ test)
6 nodes in a CSS grid. Status glyphs: ✓ (done + duration), ⟳ (running + live elapsed), ✗ (failed + em-dash), ○ (pending). Click → onSelect callback. Selected node has aria-current="step" + primary ring. 6 tests.
T10StageDetailPanel + hideItems prop on *IOtddcreate
queue/tabs/io/*IO.tsx · deal-detail/StageDetailPanel.tsx (+ test)
Add optional hideItems prop to the four *IO components (default false; preserves current RunDetailSheet behaviour). Conditionally skip crops grid when true. New StageDetailPanel dispatches by stage prop; prepends inline error block when last run failed. 3 tests for the panel; existing *IO tests stay green.
T11VccAppraisalCard — payload | placeholder | errortddcreate
web/src/components/deal-detail/VccAppraisalCard.tsx (+ test)
Three states: data present (items list + notes + box count + appraisalId); data null ("Not yet appraised by VCC"); error ("Couldn't reach VCC: …"). 4 tests.
T12LogsDrawer — slide-up panel with stage markerstddcreate
web/src/components/deal-detail/LogsDrawer.tsx (+ test)
50vh drawer fixed at bottom. Renders only when open=true. Inserts "── prev → next ──" markers between entries with different extra.stage. Empty-state, close button. 5 tests.

§5Phase 3 · Page wiring

T13Route /admin/queue/$dealId + auto-polltddcreate
web/src/routes/admin.queue.$dealId.tsx (+ test)
Zod search schema for ?stage= and ?logs=open. Two TanStack queries (detail + appraisal); refetchInterval=3000 when any run is "running", false otherwise. Logs query only enabled when drawer is open. Default-selects latest stage with a run when ?stage= is absent. MSW-backed integration test covers composition, deep-linking, polling cadence (vi.useFakeTimers).
T14Queue row click navigates + backwards-compat redirectmodify
web/src/routes/admin.production.queue.tsx · web/src/components/queue/QueueTable.tsx
Replace ?run= modal-trigger logic with navigate({ to: "/admin/queue/$dealId", params: { dealId } }). Add one-line backwards-compat shim at the top of the queue route: if search.run is present, <Navigate to="/admin/queue/$dealId" ... replace />. Remove the <RunDetailSheet/> render from the queue route.

§6Phase 4 · Cleanup

T15Delete dead components + drop hideItems propdelete
RunDetailSheet, PipelineTrail, TimelineTab, IOTab, LogsTab, ErrorTab + tests · *IO.tsx trim
Delete 6 files + their tests. Trim items-rendering and the hideItems prop from SegmentationIO + CategorizationIO entirely (no consumer renders items there anymore). Remove the unused hideItems prop from IntakeIO + AppraisalIO. Update any remaining tests that referenced the deleted components. Final smoke test in dev.

DoDDefinition of Done

One thing to verify during execution The exact generated TanStack Query option function names (from openapi-ts) depend on the OpenAPI operationIds. After T4's codegen, run grep "DealDetail\|DealAppraisal" web/src/lib/api/generated/@tanstack/react-query.gen.ts and adjust T13's imports if the names differ from the plan.