One pipeline, two ways to work it. The Leads screen flips between the kanban board and a full data table in place — same header, same quick filters, same deals — so working a lead and scanning the book are one surface, not two features.
The board is the work view: untouched-first triage, effort stats, and an honest heat signal on every card, with Compact cards and in-place column collapse. The table is the scan view: every deal is a row, every field a column, sorted globally from one menu. A single two-state chip owns the flip; the incumbent board stays the default until the table earns it (ds_v264).
Listen to this page
0:00/11:21
Rulings ledger
Date
Ruling
07-08
Tiers are population dials — service-derived rank buckets, re-tuned each retrain; never hardcode thresholds client-side.
07-08
Funnel bar KILLED post-GSM call — measure EFFORT, not pipeline shape.
07-08
UNTOUCHED = deal created + zero outbound since; red-02 surface; a quick-filter chip; the stat renders red when >0.
07-08
One lead source per deal — the source IS the intent ("Credit Application", "Trade-In Form"); intent-vs-origin duplication is dead.
07-08
Multi-assignee cluster + qa footer per the customer-card conventions; hover-hidden buttons are dead.
07-09
Score = EMBER GRID top-right, ago beside the name; percentiles in the tooltip only.
07-29
Deals can start without a vehicle — a credit application or trade-in with no vehicle still opens a deal (an empty deal); the first vehicle the customer touches attaches to it, and a customer holds at most one empty deal at a time.
07-23
Incumbent-default (ds_v264) — until the table view is validated, the incumbent view is the default everywhere — Board for Leads, List for Contacts. Supersedes the v262 "default = Table" note; the files author Board as state-a.
07-23
Compact is board-only for now (ds_v260) — the chip hides in table view. The planned table Compact = a reduced column set (Name · Stage · Score · Vehicle · Assigned); wiring pending.
07-23
Bottom bar killed (ds_v260→v261) — .sb-dtfoot and the .lt-frame card are gone — the table fills edge-to-edge. Sort re-homed to the control band: desktop = labeled "⇅ Manual Order ⌄" dropdown right-aligned under Add; mobile = icon just left of Filters. Open: mobile lost the "28 leads" count — re-home or drop.
07-22
Board⇄Table is one screen, flipped in place (ds_v262) — a single two-state chip owns body.table-mode; the header band never changes, only content swaps. No separate table route.
07-22
Per-cell hover and click, not whole-row (ds_v259) — every populated cell is its own target; em-dash cells and the Stage dropdown are excluded. Click destinations are v1 read-and-open.
07-22
Honesty caret (ds_v256) — a quiet, non-interactive caret marks the ONE sorted column only. Defaults that aren't real sorts — Manual Order here, Recently Updated / Unseen Activity on Contacts — show no caret anywhere.
07-22
Sort is global and menu-driven — never a header-click (ds_v255) — one four-option menu (Manual Order default · Lead Score · Most Stale · Vehicle) sorts the whole table.
07-22
Stage is the one full-cell tint exception (ds_v253) — 14% status hue across the whole cell; everywhere else we color the value, never the cell. Tint base hardcodes white — dark-mode base unresolved.
07-09
Collapse rail hugs content — pad · letter · gap · eye · pad; full column height, in place.
07-09
Rename Desk → Leads (design-ahead; the shared topbar atom keeps Desk until the app ships it).
07-09
The card is ONE recipe — screens instance it, never re-implement it.
07-10
Lead score — the metric name — never "deal score"; tier words exactly Hot / Warm / Cool / Cold; directional labels rejected (direction is the grid's shape only).
07-10
Sort by Lead Score added to the column sort menu (high→low; unscored cards sink to the bottom).
07-10
Tier-banded opacity — per-tier alpha bands (Hot .90–1.00 · Warm .65–.85 · Cool .40–.60 · Cold .20–.35); raw probability never maps to pixels.
07-10
Stat info icons — circled-i on every effort-stat label; hover shows the definition tooltip, click pins it — no navigation.
07-10
Stat thresholds — Untouched red when >0; Avg First Touch: <15m neutral · 15–60m orange · >60m red — calibrated on live first-touch data.
07-10
Score explorations page removed — the decision trail lives in the repo plan doc.
07-10
Score tooltip designed — replaces the non-native gradient line-chart hover; contained bars on the shared band table, calibrated % headline with adaptive precision, SHAP why-rows, no gradients / no uncontained lines.
PRESCRIPTIVE — CARD v5: (Taylor ruling: score TOP-RIGHT, time NEXT TO NAME): R1 = name + ago inline (left) · EMBER GRID + tier word pinned TOP-RIGHT. Stack: [user] status DROPDOWN chip · [car] Vehicle of Interest (or "None" + plus) · [source icon] the SINGLE lead source · [tags] COLORED label chips + bare add · footer: cluster + always-exposed .qa actions. Tiers are service-derived buckets off within-pipeline rank (model team’s D1) — scale-coupled, re-tuned each retrain; never hardcode thresholds client-side. The grid = the last 7 daily trend points, one hue per current tier, opacity per day; empty-left = young deal, empty-right = going quiet (scoring stops after 45d silence). All absolute numbers live in the tooltip. Flag-gated (#1827); needs ≥2 trend points, else tier word only. Rule: the status chip ALWAYS carries a caret; label chips NEVER do.
Ember Grid — .sb-embergrid · component-level engineering spec (PRESCRIPTIVE — design-ahead of the app · flag #1827)
What it is: the lead-score element on the Leads deal card (top-right of row 1): 7 daily squares + the current tier word. It answers two questions at a glance — WHERE the deal stands (the color) and WHERE IT CAME FROM (the opacity over days). The atom CSS in components.css is the source of truth; this page is the deep-link spec — features reference it, never copy it.Card context: comp-deal-card. Decision trail: the repo plan doc (autopilot/DESK_PLAN.md).
The metric's user-facing name is the LEAD SCORE. The page is Leads — "deal score" never appears in user-facing or spec copy. Tier vocabulary is EXACTLY four words: Hot / Warm / Cool / Cold. "New" is the unscored label (no tier claimed); the stale microtext ("scored Nd ago") replaces the tier word. Directional labels are REJECTED — Cooling / Heating / Rising / Going Quiet never appear as UI labels; direction is expressed by the grid's shape only. Directional words are allowed in spec NARRATIVE prose (like this page's state names), never in the UI.
Missing dates render as empty squares; the two empty squares at the RIGHT edge are the going-quiet warning.
Mapping rules: take the LAST 7 CALENDAR DAYS from trend (stores ≤30 daily points; dates are NOT contiguous). Each day arrives as {d, t, pos, p}: day present → alpha from THAT DAY's tier band interpolated by pos (intra-tier percentile — cuts stay service-side dials, never client math). Day absent → empty gray-06 square. p is tooltip-display-only — pixels never derive from p. Hue from the CURRENT tier, which is SERVICE-DERIVED (rank-based buckets per model-team decision D1) — NEVER hardcode tier cutoffs client-side; they are scale-coupled and re-tuned each retrain.
2b · Tier-banded opacity (PM ruling 2026-07-10 — replaces the single quartile ladder)
WHY: raw probability must NEVER map linearly to alpha. The calibrated median pClose is under 1%, so a linear mapping renders nearly everything invisible — and even a rare 30% pClose (one of the hottest leads in the pipeline) would draw at 30% alpha and read cold. Probability space and perceptual alpha space are different axes.THE SYSTEM: each trend day arrives from the service with its own tier t + intra-tier position pos (0–1, percentile within that tier's population — cuts stay service-side dials, never client math). Grid HUE = current tier (unchanged law). Each day square's alpha = linear interpolation inside ITS OWN tier's fixed band. Formula: alpha = band.min + pos × (band.max − band.min), rounded to 2dp.
Day tier
Alpha band
Hot
0.90 – 1.00
floor 0.90 — always unmistakably hot
Warm
0.65 – 0.85
Cool
0.40 – 0.60
Cold
0.20 – 0.35
floor 0.20 — stays above invisibility
No score
solid var(--gray-06)
absence is a DIFFERENT channel — never the hue at low alpha
aBands are disjoint with deliberate gaps — a hot day can never render dimmer than a warm day.
bHot floor 0.90: a 30% and a 60% pClose hot lead differ by only a few alpha points — both unmistakably hot.
cTier changes read as visible steps; intra-tier drift is subtle BY DESIGN — level first, micro-trend second.
dCold floor 0.20 stays above invisibility; empty gray-06 is visually distinct from any cold alpha even when the current tier is Cold (gray-02 hue is light — verified below).
eGrayscale-safe — the band ladder survives colorblindness.
ColdCold-tier specimen (enlarged): cold alphas on the gray-02 hue vs solid gray-06 empties — the two channels stay distinct.
2c · The score tooltip (PM ruling 2026-07-10 — replaces the non-native gradient line-chart hover)
HARD AESTHETIC RULES: NO gradients anywhere. NO uncontained line charts — if a chart, it lives inside a hairline-bordered box with a baseline. Flat token fills only. Utilitarian — every row earns its place. The panel is the rich-content instance of the locked tooltip pattern (~260px, r-5, gray-06 border, body-level engine; hover shows, click/tap pins).
2Headline: the calibrated close probability as plain fact — number 20px 700 gray-00, "chance of closing" 12.5px gray-02. Formatting rule: ≥10% integer ("33%") · 1–10% one decimal ("4.2%") · <1% shows "<1%" — honest but readable (the median calibrated pClose is under 1%).
3Context line: within-pipeline percentile (model-team D1) · middot · the tier word in its tier color. 12px.
4Trend, CONTAINED: a micro BAR chart — last 30 daily bars (2px wide, 1px gap), flat tier-hue fill, inside a hairline gray-06 box with a gray-07 baseline, ~36px tall, 8px padding. BAR HEIGHT RULE: heights reuse THE SAME BAND TABLE as square opacity — a day maps its (t, pos) to height%: Hot 90–100 · Warm 65–85 · Cool 40–60 · Cold 20–35 · missing day = no bar (gap). One perceptual mapping, two channels: alpha on the card grid, height in the tooltip chart. NEVER raw probability as height (the same anti-linear principle). Bars are full opacity — the chart encodes level by HEIGHT; opacity stays the card grid's channel.
5Delta line, plain facts: "7 days ago 21% → today 33%" 11.5px gray-02 (numbers gray-00). Terminology law: no directional WORDS — the numbers speak.
6WHY: up to 3 reason rows from the model's real per-deal reasons (SHAP): "+" green-01 or "−" red-01 (11px 700) + reason text 11.5px gray-01, under a "WHY" 9.5px uppercase gray-04 label.
STALE (>7d): headline dimmed gray-02, "scored 9d ago" orange-01 microtext + neutral note "Score may be outdated".
NEW / UNSCORED: "Not scored yet" gray-02 + "needs 2+ days of activity" — no chart, no headline.
Data note: everything shown already exists in the score payload — calibrated pClose, within-pipeline percentile, the 30d trend (as {d, t, pos}), SHAP reasons, updatedAt. No new backend fields. Flag-gated #1827 like the grid.
UNTOUCHED: deal created → ZERO outbound since; clears on the first outbound touch. Background red-02 = the same "needs attention" surface language as the showroom unconfirmed rows; all rows normal (fresh ago — usually brand-new; no score yet). AA note: green-01 fresh-ago and the gray-06 .qa buttons were contrast-checked against red-02 — both pass AA on the light red wash; no token change needed.
OtherUnmatched dealer free-text, shown verbatim(verbatim raw string)
LABEL RULE (Taylor): the icon carries the FAMILY, the text carries the SPECIFIC source — megaphone + "Facebook" (not "Social"), store + "CarGurus", comment + "Podium"; generic text only when no recognizable brand exists (globe "Website", door "Walk-in", phone "Phone Lead", database "DMS Import"). Three-pass mapping: exact "Space Auto: <flow>" map → brand/keyword match → "App:" prefix-strip. Display names are friendly ("Vehicle Inquiry" was Space Auto: Vehicle · "Trade-In Form" was Space Auto: Trade App · "Credit Application" was Space Auto: Credit App). The raw leadSource string always rides in the tooltip; trafficSource is 88% null so it never renders on the card. The icon-only import rule is RETIRED — imports read quietly as fa-database + "DMS Import" in the labeled source row.
Threshold states: Avg First Touch — gray-00 under 15m · orange-01 15–60m · red-01 over 60m; Untouched — neutral at 0, red-01 whenever >0. New Leads + Touchpoints are always neutral (volume gauges, no judgment). Data-grounded: calibrated against 60-day first-touch distributions at three live stores — medians 2–5 min, p75 5–25 min, so an average past an hour is unambiguous systemic trouble.Definitions (the info-icon tooltips, verbatim): New Leads = "Leads created in the current window (defaults to today)." · Untouched = "Leads created in the window with zero outbound messages since — email, SMS, or call. Red whenever the count is above zero." · Touchpoints = "Outbound messages (email, SMS, call) sent in the window." · Avg First Touch = "Average time from lead creation to the first outbound message, over leads created in the window. Under 15m healthy · 15–60m drifting · over 60m red." Each label carries a circled-i (gray-04, gray-01 on hover): hover shows the definition via the locked sb-tooltip engine; CLICK pins/unpins the same tooltip (touch devices have no hover). It navigates nowhere — definitions are self-contained.PRESCRIPTIVE — pipeline EFFORT, not shape. The board opens filtered to leads CREATED TODAY by default; this row answers the GSM triage loop — did we touch them all · how many untouched · how much outreach today · how fast do we touch. A shorter variant of the locked showroom stat-card recipe (bare white card, gray-06 border, r5; value t-paragraph-sb, not the big showroom number); one-line nowrap labels — the whole row is scoped to the board's default created-today view, so no per-card 'Today' sublines. The Untouched value renders red-01 whenever >0 (gray-00 at 0 — alert semantics). Data: Touchpoints ≈ outreachStats and Avg First Touch ≈ first-response stats already on POST /v2/dashboard/sales; the Untouched count needs a NEW backend aggregate (design-ahead).
2 · Mobile mini stats (~390px · PRESCRIPTIVE)
28New Leads
6Untouched
41Touchpoints
12mAvg First Touch
Mobile = a 2×2 grid per the app's established mobile stats pattern — all four metrics retained (they are the manager triage loop); equal card widths, no horizontal scroll, no clipping.
3 · Quick filters + the Untouched chip (PRESCRIPTIVE)
Definition: a lead is UNTOUCHED when the customer + deal were created and ZERO outbound messages have been sent since deal creation; it clears on the first outbound touch. The chip uses the locked quick-filter recipe + selected state, with its live count.
Board chrome
The controls wrapped around the columns: per-column sort and hide, the in-place collapse rail, and the compact toggle.
4 · Column header v2 — all five statuses (PRESCRIPTIVE)
Fresh1.9K
Working48
Finance3
Close21.1K
Lost1.1K
PRESCRIPTIVE. Gospel look kept (status-02 tinted container · status-01 title · gray-00 count) + the right icon cluster: sort + hide are per-column. The shipped app's + here was "create deal in this stage" — REMOVED per PM ruling (no adding columns; creation lives elsewhere). Icons are always visible — board chrome, not hover-revealed. Idiom rule: Sort + Hide are the DESKTOP column-header cluster. On the MOBILE pager (one status column per swipe screen) Sort persists (per-page sort) but Hide is ABSENT — a one-column-per-screen pager has no side-by-side columns to collapse. General principle: a desktop affordance that manages a side-by-side spatial condition does not exist in the pager idiom.
5 · Column sort menu — open (PRESCRIPTIVE)
Working48
PRESCRIPTIVE. Locked dropdown-menu recipe. Manual Order = default (the kanban drag order); sort is per-column and session-sticky. Lead Score (ruling 07-10) sorts high→low by the current score rank; unscored cards sink to the bottom.
Supersedes the restore pill — ONE in-place affordance: a hidden column collapses WHERE IT LIVES to a slim content-hugging full-height rail (8px · letter · 8px · eye · 8px) (same status-02 tint + radius as the column container): first letter in status-01 over a small eye; the whole rail is one click target (tooltip "Show Fresh"). Spatial continuity — no affordance teleports to the board edge. Shown beside an expanded column fragment.
PRESCRIPTIVE — no new atom: the locked quick-filter chip recipe (the All/Mine/Unassigned family) with its locked selected state. Lives in the page header next to the quick filters (Taylor ruling); toggles the whole board between full Stacklight cards and the 56px compact cards (see comp-deal-card).
The table view
The table is built bottom-up on the shared .sb-datatable primitive — these are the live atoms, from the empty frame to the shipped screens.
One screen, two views. A single #viewToggle chip (data-role="two-state" data-body-class="table-mode") flips the page between Board and Table IN PLACE: the pagehead and quick-filter band stay constant; only the content region swaps (body:not(.table-mode) shows the board and hides .sb-datatable; body.table-mode does the reverse and additionally hides the ministat effort strip). View-specific utilities swap with it: Sort is table-only, Compact is board-only. Default = BOARD (authored data-state="a", no body class): per the incumbent-default ruling the incumbent view ships as default everywhere until the table is validated — Board for Leads, List for Contacts. Authoring rule: the state-a (incumbent) content MUST exist statically in the file — the two-state chip only writes the body class on init/click, so a page whose default content waits on JS renders blank until first click.
The shell. Column widths are declared once on .sb-datatable (grid-template-columns via --dt-cols); each row is a subgrid spanning those tracks, so every cell + the header align to the same column in every row — no stagger, even with variable row heights. Cells have 16px horizontal padding and a 1px gray-06 vertical separator between columns (last cell in a row has none). Text cells wrap (rows grow to fit); atom + number cells (.sb-dtcell--nowrap, --num) stay single-line. Per-cell hover = gray-07 on populated cells; em-dash, Stage, and Status cells are excluded. Empty cells render a muted em-dash. Reused atoms drop into .sb-dtcell.
Typed cells — each cell is an existing atom in a .sb-dtcell
Cell type
Atom in the cell
Name
Devon Brooks
Assigned
KSTBJM+2
Score
Warm4d
Labels
VIPHotRepeat+1
Vehicle
2023 Honda Accord Sport#48213$28,750
Source
CarGurus
Last activity
3h ago
Price
$28,750
Each typed cell is an existing atom vertically centered in a .sb-dtcell — no new cell styling. Name = the .nm/.sub truncation stack (NO idx gutter, NO customer avatar — customers never have avatars), sticky-left. Assigned = .sb-avgroup s24 STAFF avatars + "+N" (staff, so avatars are fine). Score = the .sb-embergrid element dropped flex:none as-is. Labels = .sb-tag--* chips + "+N" overflow. Vehicle = .sb-dealrow (the short deal row — NOT the 80px .sb-veh thumb row). Source = .ibx family icon + .srcname (raw leadSource in the tooltip). Last activity = the .ago colored age. Price = right-aligned .sb-dtcell--num. Nullable cells → the em-dash.
View + sort controls. The table's controls live in the shared header band, never in the table itself. The Table⇄Board view switch is a two-state .sb-msel chip in the quick-filter .sb-mselbar (fa-table-columns/fa-table-cells; the chip always names the CURRENT view — blue .on only in table state). Sort is GLOBAL and menu-driven — there is no header-click sorting. Desktop: a labeled .sb-msel dropdown (⊕ ⌄, wrapped in span.sb-sortwrap) right-aligned in the band, under the pagehead Add. Mobile: the same control as an icon-only chip, just left of Filters. Sort renders in table view only (body:not(.table-mode) .sb-sortwrap{display:none}). The menu is a standard .sb-popover (min-width 184px, gray-06 bg, gray-05 hairlines and hover) with four .sb-menuitem entries, verbatim and in order: Manual Order (fa-arrow-down-arrow-up, the default, .is-selected) · Lead Score (fa-fire) · Most Stale (fa-hourglass-half) · Vehicle (A-Z) (fa-car). Every item carries a .ck check, hidden except on .is-selected. The active sort echoes as the quiet .sortcaret on that one column header; Manual Order is not a real sort, so it shows no caret anywhere.
A row is a deal — default order Name(sticky) · Stage · Score · Last activity · Assigned · Vehicle · Price · Source · Labels · Email · Phone (triage front-loaded, contact detail last). Every non-empty, non-Stage cell has its own hover (gray-07 + cursor:pointer) and is its own click target (v1 destinations = read-and-open); em-dash cells and the Stage cell (which keeps its .sb-statusdd dropdown) do not hover. Stage is the ONE full-cell tinted column (documented exception to "color the value, never the cell"): a full-bleed tint of the deal processingStatus hue at ~14% with the picker at full chroma; nullable → untinted em-dash. sort is global and menu-driven from the control band (see the controls nugget); the quiet .sortcaret marks the one sorted column, and the authored default (Manual Order) shows none. Text cells wrap; atom + number cells nowrap; Labels is the sole 1fr column; Assigned caps at 3 + "+N".
Compact currently lives on the board only — the chip hides in table view. The reduced-column table (Name · Stage · Score · Vehicle · Assigned) is spec’d here but not yet wired.
The Leads table screen. Every row is a DEAL — the same DealItem payload the board cards render, one row per deal. Eleven columns, widths declared once on .sb-datatable (rows are CSS subgrid — no stagger), in order: Name (sticky left, min 200px) · Stage (full-cell 14%-tinted .sb-statusdd, caret pushed to the right cell edge) · Score (.sb-embergrid with live scoretip) · Last activity · Assigned (overlapping avatars + watcher eye-chip) · Vehicle · Price (right-aligned tabular) · Source (normalized name; raw feed value in the tooltip) · Labels · Email · Phone. Absent values render an em-dash cell that takes no hover. The five stages map 1:1 to DealProcessingStatus with the shipped hues: New/orange, Working/indigo, Finance/blue, Close/green, Lost/red. The whole .sb-datatable is the two-axis scroll container; the header row is sticky-top, Name is sticky-left, and the header-Name corner cell wins at z-4. Per-cell hover (gray-07 + pointer) marks every clickable cell; empty cells and the Stage dropdown (which owns its own interaction) are excluded, and destinations are v1 read-and-open. Twin flags for engineering: Last-activity heat colors are currently scoped to card selectors so table cells render neutral — intended behavior is the card's exact recency mapping; the Stage cell label reads 'New' where the board lane reads 'Fresh' (same status — reconcile to one label); Working/Close cells reuse the generic --indigo/--sold statusdd variants.
Mobile table. The identical .sb-datatable organism ships on the phone — no reduced redesign. It runs edge-to-edge (no card frame) and the table itself is the two-axis scroll surface. Mobile adjustments: Name narrows to minmax(140,150) and truncates; there is no pagehead — Filters and a primary "+" Add chip join the compact quick-filter band (.sb-mselbar--compact), where every chip is icon-only with a data-tip tooltip and Sort sits just left of Filters; the view toggle is icon-only too. Everything else — sticky header, sticky Name, column order, Stage tint, per-cell hover — is identical to desktop.
Screens
The combined Table/Board pages (comp-leads-table-desktop / -mobile) are the shipped screens; the board-only pages remain the board's atomic source.
INTERACTIVE (PRESCRIPTIVE — Leads-board evolution). The Compact chip toggles the whole board between full Stacklight cards and 56px compact cards (each card carries both canonical instances; a board-level class flips visibility — no reload). Click a column's eye to collapse it in place to its .sb-leadrail; click the rail to bring it back — multiple columns can collapse. Columns scroll vertically inside their tint containers. Board opens filtered to leads CREATED TODAY; untouched red-02 cards sort first in Fresh (deal created → zero outbound since; clears on first outbound touch). The "Leads" tab is a design-ahead rename — the shared topbar atom keeps "Desk" until the app ships it.
INTERACTIVE (PRESCRIPTIVE). One board column per swipe page — a REAL scroll-snap pager with ~20px tinted PEEK slivers of the adjacent columns on both edges (Working's indigo peeking right). The Compact chip toggles the page between full and 56px compact cards — identical recipe + mechanism as desktop (both instances rendered; a pager-level class flips visibility). Fresh page: 7 varied cards, 2 untouched red-02 first + the credit-app "None +" card. Board opens on leads created today. "Leads" tab rename is design-ahead (app keeps Desk until shipped).
Deals without a vehicle
A deal doesn't always start with a vehicle. Credit applications and trade-in submissions routinely arrive before the shopper has picked anything out, and that person is a lead worth working the moment they raise their hand. So a submission with no vehicle still opens a deal — an empty deal: somewhere for the credit app or trade-in to live, and a card on the board straight away.
The empty deal is a placeholder that fills itself in. The first vehicle that customer interacts with attaches to it rather than spawning a second deal, and a customer holds at most one empty deal at a time — so the placeholder becomes an ordinary deal as soon as a vehicle enters the picture, and no one accumulates a drawer of vehicle-less duplicates.
On the board and in the table an empty deal renders like any other deal, with an explicit “no vehicle yet” state where the vehicle would be. That state has to read as deliberate and stay clearly distinct from a deal whose vehicle reference is broken or stale — two different problems that must never look alike. The vehicle-less card and cell states are not yet drawn in the twin.
Open questions. What counts as “interacts with a vehicle” — does browsing a vehicle page or favoriting count, or only a lead / retailing / credit-app submission that names a vehicle? Does an empty deal expire or auto-archive if the customer never engages a vehicle, which stage does it sit in, and does it count toward board totals? And can an empty deal enter desking, or is a vehicle required first?
Engineering scope
Design-ahead of the backend — each item below needs engineering work before the board ships.
[NEW] Deals currently require a vehicle, so a credit application or trade-in submitted without one creates no deal at all and that customer never reaches the Leads board — roughly 4 in 10 recent credit-app submitters. Supporting empty deals means the board gains a population it has never shown, and the “one empty deal per customer” limit has to be enforced in the database rather than in application code (the equivalent per-vehicle rule is enforced in code today and leaks duplicates).
[CANONICAL] A table row = a DealItem from POST /deals/search (pagination + filters only). The shipped board fans out one request per status (25/column); the table needs ONE flat query across statuses.
[NEW — the one tagged backend item] Server orderBy on /deals/search — the request has no sort param today. Until it lands, table sort is client-side within the loaded page; Manual Order (the server's position order, maintained by the move endpoint) is the only free sort. Contrast: Contacts gets real server sort via CustomersSearchRequest.sort.
[CANONICAL] Stage = DealProcessingStatus {fresh, desking, financing, closed, archived}; financing serializes as "appointment" (legacy, bidirectional — never fix). Hues: orange/indigo/blue/green/red. Open: the table cell says "New" where the board lane says "Fresh" — pick one label.
[CANONICAL, gated] Score = DealScore (pClose, backend-derived tier, trend); the column must handle the isLeadScoreOn gate (DAISI package AND generateLeadScoreEnabled) — hide the column or em-dash; null score = em-dash. dealScoreTiers is already a server filter.
[CANONICAL] Last activity = updatedAt time-ago, heat-colored only while hasChanges (lastSeenAt < updatedAt): <=5m green, <=29m orange, else red; seen = gray. (The twin currently renders table cells neutral — scoping bug; intended = the card mapping.)
[CANONICAL] Labels are ID lists resolved against the dealership taxonomy; Assigned = deal.dealer + subscriberIds (the eye-chip presentation is NEW); opt-out flags ride the row payload and must gate any comms actions.
[NEW] Sticky Name column — the app pins no column anywhere (Reports pins only the header via PinnedHeaderSliver). The 2-axis scroll itself is code-confirmed on all idioms.
[NEW] Untouched — zero code backing; needs a backend flag/filter (definition: customer + deal created, zero outbound since deal creation; clears on first outbound touch).
[NEW] Compact (row density AND the reduced column set), multi-select + batch bar, per-cell click destinations, group-by rows — no app precedent; destinations undefined in v1.
[NEW/deferred] Prescriptive extra columns (Customer Status, Next Appointment, Unread, Untouched, Deals Count) are not on the DealItem payload. Phase 2: column resize + persisted per-user widths/order.
Full decision ledger: autopilot/DESK_PLAN.md · autopilot/TABLEVIEW_PLAN.md (repo). This page is the first composed feature doc under the Atomic documentation LAW — the template every future feature copies: narrative + rulings ledger + anatomy map + @dsEmbed transclusions (build inlines them with provenance links) + eng scope notes.