componentsfix(statistics): follow-up time is silence, and a meeting row admits it is hoverable
Follow-up time counted a REPLY as a follow-up. It walked the outbound emails
alone and measured every gap between them, so "you write, they answer next
morning, you answer back within the minute" scored as a ~20h follow-up made
entirely of THEIR reply latency. On a real 30-day window 25 of 29 gaps were
that shape, holding the median at 21h52m while the true chase-into-silence
median was 41 minutes — the metric moved with how fast prospects wrote back,
not with anything the rep did.
Now a gap counts only between TWO OUTBOUND EMAILS IN A ROW: the window carries
both directions and an inbound reply between two sends breaks the pair. Same
rule in all four places that computed it — the shared helper, the cadence
samples the reasoning layer reads, the per-conversation column and the per-rep
team view — so no surface disagrees with another. A meeting between two emails
leaves the pair intact; meeting→email is its own metric. The number gets
honest and much smaller (13m median on the same window), and its sample gets
thin, which is the metric telling the truth rather than padding itself.
The rail's Statistics tile: "21h 52m" wrapped inside the shared fixed-width
number column and read as two figures, so the duration column now sizes to its
content, and demo mode swaps in fixed values instead of showing real figures
on a shared screen.
The meetings widget lost its hover fill because `hover:bg-accent/50` IS
`--surface-raised` in dark mode — the exact colour of the card behind it. It
is a sunken well now. The row also had `w-full` fighting its own `-mx-2`: a
negative margin cannot widen a box pinned at 100%, so the row shifted 8px left
instead of bleeding both ways, leaving 4px of fill on one side and 20px on the
other.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 12:25 AMhooksfeat: support multiple CRM opportunities per conversation
Allows an unlimited number of CRM deals to be linked to a single
conversation (previously one per provider was enforced). The agent
sees all linked opps with their name and stage, and can target a
specific deal when updating fields.
- linkExternalCrmEventToConversation: append new entries instead of
overwriting; account-match placeholders still merge correctly
- ExternalCrmIntegrationMetadata: add dealName/dealStage fields,
refreshed on every sync event
- unlinkExternalCrm: now per-deal (provider + optional dealId) instead
of nuking all entries
- updateExternalCrmWorkflow / upsertWorkingMemory: accept targetDealId
to push fields to a specific linked opp
- updateConversationFieldsTool: accepts targetDealId, threaded through
syncFieldsToExternalCrmAtomic
- moveCrmDeal: fix instanceUrl lookup when multiple opps from same provider
- Agent context: each <linked_crm_deal> block now includes Deal Name
and Deal Stage for stage-aware routing in AOP instructions
- Frontend: CrmAgentBadge renders one badge per linked opp with
individual unlink buttons; CrmDealLinkDialog shows already-linked
deals as disabled; ConversationOverviewCard collects customObjectRecords
from all entries
Co-Authored-By: Claude Sonnet 4.6 (1M context) <<email>>Jun 7, 2026, 4:46 PMutilsfeat: statistics pageFeb 9, 2026, 9:35 PMindex.tsfeat: statistics pageFeb 9, 2026, 9:35 PM