console: the story is the page; the reference is the rail

The audit trail is the record of work nobody watched happen — it is what this
product is for — and it sat in a 384px gutter with its own inner scrollbar
beside a full-width panel of form fields. That said, in layout, that the AI's
work was a footnote to the data entry. Exactly backwards.

Swapped. The trail takes the wide column and scrolls with the page under a
heading that says what it is: "What happened". The reference data — customer,
vehicle, documents, the call, the stage's own facts — moves to a 340px sticky
rail, where short key/value pairs belong. The crew who worked the lead sits at
the top of that rail as its own small panel.

The overview now opens on who holds the work rather than on money: four large
numbers on navy — with the AI, waiting on a person, with the customer,
renewals at risk. A live person-queue or a lapsed renewal warms its cell; the
rest stay cool. Money is the outcome; this is the state, and it is the sentence
the whole console exists to say.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-09 00:04:12 +05:30
parent 9c05e4b26b
commit a49ae3d86c
3 changed files with 197 additions and 71 deletions

View File

@ -677,6 +677,59 @@ export default function Lead() {
)
})()}
{/* THE STORY IS THE MAIN COLUMN NOW.
This sat in a 384px gutter with its own inner scrollbar, beside a
panel of reference fields that had the whole width. That was
exactly backwards. The trail is what this product is FOR it is
the record of work nobody watched happen and putting it in a
sidebar said it was a footnote to the form fields.
It gets the wide column and the page's own scroll. The reference
data moved to the rail, where short key/value pairs belong and
where a narrow measure costs nothing. */}
<div className="panel">
<div className="panel__head">
<div>
<h2 className="panel__title">What happened</h2>
<p className="panel__sub">
Every step on this lead, who took it, and what they wrote.
</p>
</div>
{chatTurns ? (
<button type="button" className="panel__act" onClick={() => setShowChat(true)}>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z" fill="none" stroke="currentColor"
strokeWidth="1.3" strokeLinejoin="round" />
</svg>
Conversation
<b>{chatTurns}</b>
</button>
) : null}
</div>
<Timeline
rows={audit}
onOpenAgent={setOpenAgent}
onOpenChat={() => setShowChat(true)}
onOpenCall={() => setShowCall(true)}
/>
</div>
</div>
{/* Reference, not narrative. Sticky, so the facts stay put while the
story is read against them. */}
<aside className="lead__side">
{/* Who worked this lead. Five agents carry it and their names appear
only inside individual entries, so the team was never visible at
a glance. */}
{worked.length ? (
<div className="panel panel--crew">
<span className="crew__l">Worked by</span>
<div className="crew">
{worked.map((k) => <AgentChip key={k} agentKey={k} onOpen={setOpenAgent} />)}
</div>
</div>
) : null}
{/* WHAT MATTERS HERE, NOT EVERYTHING THERE IS.
The whole record used to sit open on the page: fifty-seven
fields over thirteen groups, which pushed the audit trail the
@ -758,50 +811,6 @@ export default function Lead() {
) : null}
</div>
) : null}
</div>
{/* The story runs alongside the file rather than under it, so what was
done and what was captured can be read against each other. */}
<aside className="lead__side">
<div className="panel panel--rail">
<div className="panel__head">
<div>
<h2 className="panel__title">Audit trail</h2>
<p className="panel__sub">Every step, who took it, and what they wrote.</p>
</div>
{/* The WhatsApp exchange is spread across the trail one turn at a
time, so reading it meant scrolling a log and holding the order
in your head. This shows it as the thread it is. */}
{chatTurns ? (
<button type="button" className="panel__act" onClick={() => setShowChat(true)}>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M2.5 3.5h11v7h-6l-3 2.5v-2.5h-2z" fill="none" stroke="currentColor"
strokeWidth="1.3" strokeLinejoin="round" />
</svg>
Conversation
<b>{chatTurns}</b>
</button>
) : null}
</div>
{/* Who worked this lead. Five agents carry it and their names appear
only inside individual entries, so the team was never visible at
a glance. */}
{worked.length ? (
<div className="crew">
<span className="crew__l">Worked by</span>
{worked.map((k) => <AgentChip key={k} agentKey={k} onOpen={setOpenAgent} />)}
</div>
) : null}
<div className="lead__feed">
<Timeline
rows={audit}
onOpenAgent={setOpenAgent}
onOpenChat={() => setShowChat(true)}
onOpenCall={() => setShowCall(true)}
/>
</div>
</div>
</aside>
</div>

View File

@ -133,8 +133,24 @@ export default function Overview() {
// never appears to a partner agent who could not act on it anyway.
const approvals = actionable.filter((s) => s.approval && roles.includes(s.approval))
// THE HEADLINE OF AN AGENTIC CONSOLE IS WHO HOLDS THE WORK RIGHT NOW.
// Every open lead is in exactly one of three hands: a person's, the AI's,
// or the customer's. The page led with money, which is the outcome; this
// is the state, and it is the sentence the whole product exists to say.
const byStageDef = new Map(STAGES.map((s) => [s.name, s]))
let withAI = 0, withCustomer = 0, withPerson = 0
for (const r of open) {
const def = byStageDef.get(r.current_state_name)
if (!def) continue
const ph = phaseOf(def, r)
if (ph.kind === 'auto') withAI += 1
else if (ph.kind === 'customer') withCustomer += 1
else withPerson += 1
}
return {
byStage, open, won, lost, closed, gwp, commission, pipeline, exposure, attention, actionable, approvals,
withAI, withCustomer, withPerson,
conversion: closed ? Math.round((won.length / closed) * 100) : null,
maxStage: Math.max(1, ...STAGES.map((s) => byStage[s.name] || 0)),
}
@ -193,6 +209,33 @@ export default function Overview() {
</div>
</header>
{/* Who holds the work, right now. Three numbers, one sentence: this many
are being worked by the AI, this many are waiting on a person, this
many on a customer. It sits above the money because it is the claim
the money is evidence for. */}
<div className="now">
<div className="now__cell now__cell--ai">
<span className="now__l">With the AI</span>
<strong className="now__v">{m.withAI}</strong>
<span className="now__s">being qualified, rated, checked or chased no one waiting</span>
</div>
<div className={'now__cell now__cell--person' + (m.withPerson ? ' is-live' : '')}>
<span className="now__l">Waiting on a person</span>
<strong className="now__v">{m.withPerson}</strong>
<span className="now__s">{m.withPerson ? 'a decision or an upload is owed' : 'nothing owed by anyone'}</span>
</div>
<div className="now__cell now__cell--cust">
<span className="now__l">With the customer</span>
<strong className="now__v">{m.withCustomer}</strong>
<span className="now__s">a quote or a payment is theirs to answer</span>
</div>
<div className={'now__cell now__cell--risk' + (m.exposure.lapsed.length ? ' is-live' : '')}>
<span className="now__l">Renewals at risk</span>
<strong className="now__v">{m.exposure.lapsed.length + m.exposure.week.length}</strong>
<span className="now__s">{m.exposure.lapsed.length} lapsed · {m.exposure.week.length} within 7 days</span>
</div>
</div>
<div className="kpis">
<div className="kpi">
<span className="kpi__l">Open leads</span>

View File

@ -589,11 +589,14 @@
/* ---- lead: the file beside the story ---- */
.lead {
display: grid;
/* Wider than it was. At 384px the trail was a column of two-line entries
beside a full-width file, which read as a margin note rather than as the
other half of the page and it is the half that says what happened. */
grid-template-columns: minmax(0, 1fr) 440px;
gap: 20px;
/* THE STORY GETS THE WIDE COLUMN. The trail used to sit in a 384px gutter
with its own scrollbar while a panel of reference fields had the whole
width exactly backwards for a product whose point is the record of
work nobody watched. Now the narrative has the measure prose needs and
the reference rail is narrow, because short key/value pairs are the one
thing a narrow column is good for. */
grid-template-columns: minmax(0, 1fr) 340px;
gap: var(--sp-6);
align-items: start;
}
@ -604,10 +607,19 @@
/* The trail stays in view while the file is read against it. */
.lead__side {
position: sticky;
top: 90px;
top: 92px;
min-width: 0;
max-height: calc(100vh - 108px);
overflow-y: auto;
overscroll-behavior: contain;
}
/* Rail panels are tighter than main panels a 340px measure cannot afford
32px of padding on each side. */
.lead__side .panel { padding: var(--sp-5) 0 0; margin-bottom: var(--sp-4); }
.lead__side .panel > .panel__head { padding: 0 var(--sp-6); margin-bottom: var(--sp-4); }
.lead__side .panel--crew { padding: var(--sp-5) var(--sp-6); }
.panel--rail {
padding: 0;
margin-bottom: 0;
@ -619,25 +631,19 @@
margin-bottom: 14px;
}
.lead__feed {
max-height: calc(100vh - 240px);
overflow-y: auto;
overscroll-behavior: contain;
padding: 0 22px 20px;
}
/* .lead__feed is gone: the trail is page content and scrolls with the page.
An inner scroll region is a widget; the story is not a widget. */
/* One column below the width where a 384px rail stops paying for itself. */
@media (max-width: 1180px) {
/* One column below the width where a rail stops paying for itself. */
@media (max-width: 1100px) {
.lead {
grid-template-columns: minmax(0, 1fr);
}
.lead__side {
position: static;
}
.lead__feed {
max-height: 560px;
max-height: none;
overflow: visible;
}
}
@ -857,6 +863,72 @@
font-variant-numeric: tabular-nums;
}
/* Right now
The overview's first row. Four large numbers on a navy ground so the page
opens on a statement rather than a grid: this is who holds the work. Each
cell carries its own hue at the top edge blue for the machine, amber for
a person, teal for the customer, red for risk so the row can be read as
colour before it is read as text. */
.now {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--sp-3);
margin-bottom: var(--sp-5);
}
.now__cell {
position: relative;
padding: var(--sp-6) var(--sp-6) var(--sp-5);
border-radius: var(--r-lg);
background: var(--zk-navy);
color: var(--zk-white);
overflow: hidden;
box-shadow: var(--sh-md);
}
.now__cell::before {
content: '';
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: var(--zk-blue-light);
}
.now__cell--person::before { background: var(--zk-amber-line); }
.now__cell--cust::before { background: var(--zk-cyan); }
.now__cell--risk::before { background: var(--zk-danger-line); }
/* A live person-queue or a lapsed renewal is the one thing on this page that
is actually owed. It warms up; the rest stay navy. */
.now__cell--person.is-live { background: var(--zk-amber-ink); }
.now__cell--risk.is-live { background: var(--zk-danger-ink); }
.now__l {
display: block;
margin-bottom: var(--sp-3);
font-size: var(--fs-3xs);
font-weight: var(--fw-semi);
letter-spacing: .09em;
text-transform: uppercase;
opacity: .78;
}
.now__v {
display: block;
margin-bottom: var(--sp-2);
font-size: var(--fs-3xl);
font-weight: var(--fw-semi);
line-height: 1;
letter-spacing: -.03em;
font-variant-numeric: tabular-nums;
}
.now__s {
display: block;
font-size: var(--fs-2xs);
line-height: var(--lh-snug);
opacity: .82;
}
/* KPI strip. Whitespace rather than rules see .lmetrics for why the cage
went. Tabular figures so the row does not jitter as it refreshes. */
.kpis {
@ -1273,18 +1345,21 @@
/* ---- who worked this lead ----
Five agents carry a lead and their names appeared only inside individual
entries. The team is a fact about the lead, so it sits above the trail. */
.panel--crew { padding: var(--sp-5) var(--sp-6); }
.crew {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 10px 18px 12px;
border-bottom: 1px solid var(--zk-line-soft);
gap: var(--sp-2) var(--sp-4);
margin-top: var(--sp-3);
}
.crew__l {
display: block;
font-size: var(--fs-3xs);
font-weight: 500;
letter-spacing: 0.1em;
font-weight: var(--fw-semi);
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--zk-grey);
}
@ -1541,10 +1616,9 @@
row of its own the way the thirteen-group version did. */
.glance {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 14px;
padding: 4px 22px 18px;
align-items: start;
grid-template-columns: 1fr;
gap: var(--sp-3);
padding: 0 var(--sp-6) var(--sp-4);
}
.glance .props { display: block; padding: 0; }