ClientsFlow · Email-System Overhaul · W2 · EBO

EBO — W2 · Touchpoint History

FINAL · owner comment round folded in · 2026-07-12. The target touchpoint-history experience after W2 ships — the freeze model (Send generates copy for the whole sequence; the rest is scheduled frozen exactly as previewed), orange scheduled rows with the exact frozen copy, the amber drift-HOLD, the full modal editor reachable straight from history (armed steps AND already-sent records), auto-vs-owner-sent distinction, complete direction labels, full reminder bodies, the compose-AI-reply content, and the missing banner moments. Built from the W2 handoff + ANSWERS_ROUND3 (Q1/Q2/Q4/Q7/Q9/Q10) + the owner's W1 + W2 comment rounds (which override both). Scenarios S1 … S10 — this doc is the acceptance answer key the overnight build is judged against.

What W2 owns
What the history says and what text goes out: freeze-at-Send copy snapshots, the drift check + amber HOLD, orange scheduled rows (full resolved copy + exact send time), the full modal editor opened from history rows, the "edited" note on sent records, the ⚙️ Auto marker on auto-sent rows, direction pills for every touchpoint type, full reminder bodies, the AI-reply draft content, and the new banner event kinds. Hard bar: ROCK-SOLID 100% parity — the copy shown in history is character-for-character what was (or will be) sent.
What W2 only reflects (owned elsewhere)
W1 owns whether/when a send actually fires (the auto-send gate, failure detection, the payment-chase cadence). W3 owns the Details panel where lead data gets edited (W2's drift check only reacts to those edits). W8 owns card visuals (where the unreplied email sits on the card, ring styling, animations). W0 owns systemic watchdog alarms. W2 never re-specs those — it records and displays their truth.
You do → click/action/hover You should see → on-screen result Element changes → copy · look · where What changes underneath → data/state Must NOT happen → bug guard 🕓 Touchpoint history → impact on the card's history (or —)
🌱 Seeded test identities (STABLE — re-created VERBATIM on every reseed, never minted fresh per replay) — one ZZ-sentinel lead per scenario, so the signed must_see checks stay findable across reverts and retries:
ZZ-S1-FREEZE Kft (S1) · ZZ-S2-ORANGE Kft (S2) · ZZ-S3-EDIT Kft (S3) · ZZ-S4-DRIFT Kft (S4) · ZZ-S5-RECORD Kft (S5) · ZZ-S6-AUTO Kft (S6) · ZZ-S7-DIR Kft (S7) · ZZ-S8-REMIND Kft (S8) · ZZ-S9-REPLY Kft (S9) · ZZ-S10-BANNER Kft (S10). Values that change per replay (send datetimes, message ids, ledger states) are asserted in the step's probe, never in the pixel checklist. The ZZ hard guard blocks any real send to these leads; all ZZ data is purged after the run. Probe environment: every probe runs in a shell with $BASE (the staging dash base, e.g. https://matyas-staging--clientsflow-pipeline-web.modal.run/dash) and $DASH_TOKEN (the live dash token — validated with one GET, never computed from a local form_token) exported. {lead_id} is the ONLY curly placeholder in any probe (bound by probe.py --sub lead_id=… to the seeded lead's email key — the lead query param of /dash/api/history); a probe carrying any other {…} is refused unbound at run time.
Owner rulings folded in (W2 comments > W1 comments > ANSWERS_ROUND3 > handoff)Q1 (freeze model, final): clicking Send Emails generates the copy for ALL emails of the sequence in the modal (most are fixed copies, not AI); hitting Send → first email sends immediately, the rest are scheduled FROZEN exactly as previewed. Q2=B (final, stronger than the handoff): if lead data drifts before send-day the email HOLDS — the send is blocked, the row turns amber, until the owner re-approves. Q7: history rows editable with a really nice UI; 100% parity history ↔ sent is a named hard acceptance bar. Q9: editing a SENT email = internal record fix + an "edited" note — never a resend. Q10: orange rows open the full modal editor (same editor as Send Emails), not a lighter inline edit. Q4: unreplied latest lead email always visible on the card + "Compose AI reply" → editable field with an AI draft written from touchpoint history, resolved greeting, canonical sign-off. W1 comments (newest, win everything): the Send-Emails modal's current UI is good — don't redesign it; the modal closes immediately on Send (reliability proven in the background, no success-toast ceremony); the failures list is dead (failure = red dashed ring + banner, nothing more); auto-continuations must look different from owner-sent emails in history ("Add this feature"); payment chase = automatic daily reminder emails (3–4 days, stops on payment or a logged phone call — no button); booking-confirmation and signed-contract flows already work — verify, don't rebuild.
W2 comment round (2026-07-12 — newest, wins everything above; all nine marked "apply"): the Send-Emails modal flags any step whose copy resolved from an empty/fallback field before he can send it; reliability of the instant-close send is proven by a live probe (the mail truly left AND the history row matches it byte-for-byte), not by a toast; orange rows render collapsed (subject + first line) with a small separate expander for the full body; Esc/Cancel closes any history-opened editor with zero side effects and no confirm dialog; a drift-HOLD fires a banner on the existing banner surface ("Scheduled email on hold — {lead}") — including for payment-chase steps, where an unnoticed hold stalls the money; the re-approve editor keeps the frozen text and shows an old→new diff line per changed field with an optional "Update to new value" link — it never silently pre-applies fresh data; a superseded "Reminders scheduled" row is greyed/struck when cancelled (annotate, never delete); "Compose AI reply" disables on click with a "Drafting…" state (no double-generate) and the draft is written by a high-quality fast model — cost is explicitly not a constraint here.
Invariants that hold everywhere — operator UI is English-only (Hungarian appears only inside client-facing email copy); a scheduled row always shows the FULL frozen body (greeting + sign-off, no truncation, no raw {{tokens}}) and the exact Budapest send datetime; a send NEVER goes out with text differing from what history displayed; an amber row NEVER sends until re-approved; W2 never invents a new send — it edits/holds/records sends that W1 fires; Notion destructive ops stay archive-only.

S1 — Send Emails → copy for the whole sequence generated, Send freezes the rest (happy path / the freeze moment)

Who: Mátyás  ·  When: A new lead is on the board and he decides to start the follow-up sequence. This click is THE moment copy is born: everything he previews here is exactly — character for character — what will leave over the coming days. The modal itself already works and he likes it; W2 changes what happens to the copy after Send, not the modal's look.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás clicks "Send Emails" on a lead card The existing sequence modal opens (existing flow, verify unchanged — owner: "the current UI is nice, don't fuck it up") with the copy for every email of the sequence already generated and readable — real name in the greeting, real booking/sign links, canonical sign-off, no raw tokens — and each step shows the exact date + time it is planned to send. Any step whose copy resolved from an empty or fallback field (e.g. no first name on the lead yet) carries a small inline warning marker "Check: {first_name} was empty" right on that step, so he catches an awkward greeting before the lead does Copy: full subject + body per step, e.g. "Kedves János, …" with the real link; the fallback marker line where it applies · Look: the current modal layout, untouched, plus the inline marker · Where: the Send-Emails modal Every step's template is resolved against the lead's current data at modal-open; what is displayed IS the candidate frozen copy — no later re-resolution is planned. The resolver reports which variables fell back to an empty/default value; those steps get flagged in the preview Must NOT show a raw {first_name}/{{greeting}} token anywhere in the preview; must NOT restyle or restructure the modal; must NOT show a body cut off mid-sentence; must NOT silently resolve an empty field into an awkward greeting without the marker — (nothing sent or scheduled yet; a preview is not a touchpoint)
2 He tweaks step 2's wording in the modal, then clicks "Send" The modal closes immediately — he is free to do other work at once. No waiting screen, no confirmation step to click through Copy: — · Look: modal gone, board usable instantly · Where: back on the board In the background: email #1 dispatches now; steps 2+ are stored frozen exactly as previewed (a subject + body snapshot per step, including his tweak) with their send datetimes. The frozen snapshot is what will send — never a fresh re-render at send time. The instant close is safe because the guarantee moved to the test: every EBO run probes that the mail truly left AND that the history row matches it byte-for-byte — reliability proven by probe, not by a toast Must NOT keep the modal open waiting for the server; must NOT show a success toast as a required step; must NOT store the raw template instead of the previewed text; must NOT lose his step-2 tweak NEW ENTRIES
Within seconds: email #1 appears as an outbound sent row with the exact text that left; every remaining step appears as an orange "Scheduled" row (see S2) carrying its frozen copy + send datetime
3 Nothing — the background does its job (reliability probe) A minute later the lead's history already tells the whole truth: one sent row (email #1) + orange scheduled rows for the rest; the sent row's text is identical to what the mail server actually delivered Copy: sent row = the delivered email, character for character · Look: normal history · Where: Full-history drawer Parity check passes: stored history body == dispatched mail body (the 100% parity bar, Q7). If the first send FAILED, W1 raises the failure and the card gets the red dashed ring + a banner (W8 styles it) — never a silent gap Must NOT show a history row whose text differs in any character from the delivered email; must NOT let a failed first send leave history looking as if it sent The sent row + orange rows from step 2 are now the durable record — the single source of truth for "what did/will this lead get"
4 Failure mode: a template is malformed and copy generation errors when the modal opens The modal shows a clear plain-English error for the broken step ("This step's template could not be generated — fix it before sending") and Send is disabled until every step has clean copy Copy: the error line on the affected step · Look: Send button disabled · Where: the modal Nothing is armed, nothing is half-frozen — the sequence either freezes completely and cleanly, or not at all Must NOT arm a sequence with one broken/half-frozen step; must NOT send email #1 while later steps failed to freeze; must NOT crash the modal — (a failed generation writes nothing)

S2 — Orange scheduled rows: the exact frozen copy + exact send time, in history (happy path / core display)

Who: Mátyás  ·  When: A sequence is running on a lead. Days later he opens the lead's Full-history drawer to see where things stand — what already went out and, just as importantly, exactly what is about to go out and when. Today those future rows show truncated raw templates in amber; after W2 they show the real thing in a genuine orange.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás opens the lead's Full-history drawer Each not-yet-sent step is an ORANGE row labeled "Scheduled" showing the frozen subject, the first line of the frozen body, and the exact Budapest send datetime ("Scheduled · Sends Jul 15, 09:00"). A small "Show full email" expander on the row (a separate control from the row itself — the row-click opens the editor, S3) unfolds the FULL frozen body: greeting with the real name, complete text, sign-off, nothing truncated. Orange is a genuinely distinct colour from the amber used for warnings/drift — at a glance he can tell "armed and fine" from "needs my re-approval" Copy: "Scheduled · Sends Jul 15, 09:00" + subject + first line; expanded = the full frozen body · Look: orange row styling (new orange token — not the existing amber), collapsed by default, small expander control · Where: top of the history timeline, one row per pending step The row renders the stored frozen snapshot verbatim — never a live re-resolution of the template; the full body is served with the row (no 600-char cut) and merely collapsed in the DOM, so expanding costs no round-trip Must NOT show a raw {first_name}/{booking_link} token; must NOT truncate the body (collapsed ≠ truncated — expanding must reveal the whole thing); must NOT open the editor when the expander is clicked; must NOT reuse the amber warning colour for a healthy scheduled row; must NOT show a send time in the wrong timezone Orange rows ARE the forward half of the history — the timeline reads as one continuous story: what happened, then what will happen
2 Nothing — a scheduled step's send time arrives and no data drifted Next time he looks, that orange row has become a normal sent outbound row — same position in time order, same subject, and the body is character-for-character identical to what the orange preview showed Copy: unchanged text, label flips "Scheduled" → sent · Look: orange → the normal sent-row styling + the ⚙️ Auto marker (S6) · Where: the same event, now in the past section W1's sender dispatched the FROZEN text verbatim (no fresh re-resolve at send time); the history row and the delivered mail are the same bytes — the parity bar, again Must NOT send re-resolved text that differs from the frozen preview; must NOT leave a ghost orange row after the send; must NOT double-show the event (one orange + one sent for the same step) The orange row converts in place into the sent record — history never contradicts itself about what a step said

S3 — Click an orange row → full modal editor; saving = pre-approved to auto-send (happy path / Q10)

Who: Mátyás  ·  When: Reading a lead's history he spots that tomorrow's scheduled email could be sharper — maybe the lead mentioned something on a call that the frozen copy doesn't reflect. He wants to fix that one email right there, from the history row, without re-doing the whole sequence.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás clicks an orange "Scheduled" row in the history drawer The full modal editor opens — the same rich editor UI as the Send-Emails modal, scoped to this ONE step: subject and body editable, the send datetime shown, the lead's name/links already resolved in the text. Esc or Cancel closes it with zero side effects — no confirm dialog, no save, nothing changed: a look costs nothing, because row-click-to-edit is easy to trigger while merely reading Copy: the step's frozen subject + body, editable; a Cancel button · Look: the familiar full modal (not a cramped inline field) · Where: opens over the drawer The editor loads the stored frozen snapshot for exactly this step; other steps and the schedule are not loaded for editing. Cancel/Esc writes nothing at all — no draft, no flag, no owner_edited Must NOT open a lighter inline-edit or a read-only preview (Q10 is explicit: the FULL modal editor); must NOT open the whole-sequence editor when one row was clicked; must NOT ask "are you sure?" on Cancel/Esc; must NOT persist anything when the editor is closed without Save — (opening an editor is not an event)
2 He edits the body, clicks "Save" The modal closes immediately and the orange row in the drawer already shows the new text — what he reads there is what will send Copy: the row's body = his edited text · Look: row stays orange · Where: the same history row, updated in place The step's frozen snapshot is overwritten with his edit; the step is marked owner-edited — which is itself the "approved to auto-send" signal (pre-edited scheduled email = whitelisted, per the locked W1 contract); seq_armed_at/seq_next untouched — send time unchanged; any pending drift flag on this step clears (his fresh edit IS the re-approval) Must NOT change the send time or any other step; must NOT require redefining/re-arming the whole sequence; must NOT lose the edit if the drawer refreshes; must NOT leave the row showing the old text ROW UPDATED
The orange row now carries the edited frozen copy — still the single truth of what will send
3 Nothing — that step's send time arrives The email goes out with his edited text exactly, automatically (owner-edited = approved); the orange row converts to a sent row with the identical text Copy: delivered email == edited frozen copy · Look: orange → sent · Where: history timeline W1's gate reads the owner-edited flag and lets the send fire without further clicks; the frozen (edited) text is dispatched verbatim Must NOT send the pre-edit text; must NOT hold an owner-edited step waiting for another approval; must NOT clear the owner-edited flag automatically Sent row appears with the edited copy — parity holds through the edit path too

S4 — Lead data drifts before send-day → the email HOLDS, row turns amber until re-approved (edge / Q2=B, the safety core)

Who: System (detects the drift) + Mátyás (re-approves)  ·  When: After a sequence was frozen, the lead's underlying data changes — he fixes a misspelled name in the Details panel, or a booking link is regenerated. The frozen copy may now be wrong. Per the owner's final ruling this is stronger than a warning: the affected email is BLOCKED from sending until he re-approves it. Nothing drifted ever goes out silently — in either the old or the new version.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás corrects the lead's name in the Details panel (a W3 surface) — a sequence step is still scheduled The affected scheduled row flips from orange to AMBER, labeled "On hold — lead data changed", with a one-line plain reason ("Lead name changed since this email was frozen") — and a banner appears on the existing banner surface: "Scheduled email on hold — {lead}", click-through to the card. He cannot walk away from a hold: a drawer he never opens would let the send-day pass unnoticed. Every OTHER scheduled step of the same sequence stays orange and keeps its send time — a drift holds the affected email only, never the sequence Copy: row "On hold — lead data changed" + reason line; banner "Scheduled email on hold — {lead}" · Look: orange → amber warning styling; the existing banner style with its own icon · Where: the affected row(s) in the history drawer + the existing top-right banner stack The drift check compares a fresh resolution of the raw template against the stored frozen text; on mismatch the step is flagged drifted and its send is blocked. Scope is per-step and ONLY per-step (owner ruling D3, settled): a hold holds the affected email, never the whole sequence. A step whose text doesn't use the changed field stays orange and keeps sending on schedule. Creating a hold pushes a new banner event kind through the already-built banner system — no new UI Must NOT keep the row orange when its copy is stale; must NOT hold the whole sequence (a step whose text doesn't use the changed field MUST stay orange and keep sending on schedule); must NOT silently re-resolve and send the NEW data either — nothing sends until he re-approves; must NOT create a hold without a banner; must NOT rebuild or restyle the banner system ROW STATE
The scheduled row itself carries the hold state — history is where he discovers AND fixes it
2 Nothing — the held step's send time passes while it is still amber Nothing is sent. The amber row stays put, its label now reading "On hold — send time passed"; the lead receives nothing Copy: label gains "send time passed" · Look: still amber · Where: the held row The sender skips held steps entirely — the HOLD is enforced at the send gate, not just cosmetically in the drawer Must NOT send the frozen (stale) text; must NOT send a freshly re-resolved text; must NOT quietly drop/delete the step — it waits for him The amber row remains the honest record: this step is overdue and waiting on a human
3 Mátyás clicks the amber row The full modal editor opens on the FROZEN text as it stands (never silently pre-applied with the new data), with what changed stated plainly at the top — one diff line per changed field: "Lead name: 'Janos' → 'János Kovács'" — each with an optional "Update to new value" link that visibly rewrites the text in the editor when he clicks it. He fixes the text (or accepts it as-is) and clicks "Save & approve" — the row turns orange again and the hold lifts. He always approves exactly what he can see Copy: the old→new diff line(s) + "Update to new value" link per field + editable frozen copy; button "Save & approve" · Look: modal over drawer; on save amber → orange · Where: the held row Saving re-freezes the step to exactly the text in the editor, marks it owner-edited (auto-send-approved), and clears the drift flag + its hold banner. "Update to new value" only rewrites the editor's textarea — it saves nothing by itself. If the send time already passed, the step sends at the next dispatch tick with the re-approved text; otherwise it sends on schedule Must NOT auto-apply the new data without showing him; must NOT lift the hold without an explicit save; must NOT save on clicking "Update to new value"; must NOT lose the original send order when a late step catches up ROW UPDATED
The row returns to orange with the re-approved copy — and later converts to a sent row with that exact text
4 Failure mode: the drift check itself errors (e.g. a deal field was deleted) The row shows as amber "On hold — needs review" rather than a crash or a false-orange "all good"; the drawer still opens and everything else renders normally Copy: "On hold — needs review" · Look: amber · Where: the affected row only An erroring drift check defaults the step to held (safe direction: block, never send unverified text); the error is logged for W0's observability Must NOT crash the history drawer; must NOT show orange (healthy) when the check couldn't run; must NOT send the step while the check is broken The row's amber state IS the record of the problem

S5 — Editing an already-SENT email: internal record fix + "edited" note (happy path / Q9)

Who: Mátyás  ·  When: Reviewing a lead's history he notices a sent email's record is wrong — maybe a manual reply was logged with a stray footer, or he wants the record to reflect what he actually said on a corrected thread. The email already reached the lead; only the internal record changes.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás clicks a past sent-email row in the history drawer The same full modal editor opens, loaded with the recorded subject + body — clearly framed as a record edit ("Edit record — this email was already sent; changes fix the internal record only, nothing is re-sent") Copy: the framing line + the recorded text, editable · Look: the familiar full modal · Where: over the drawer The editor loads the stored touchpoint body; no send machinery is involved anywhere in this path Must NOT present any button that could re-send; must NOT confuse this with the scheduled-row editor's "will send" semantics — (opening is not an event)
2 He corrects the text, clicks "Save" The modal closes immediately; the row now shows the corrected text and carries a small "edited" note with the edit date — visible whenever the row is read, so the record is honest about having been touched Copy: corrected body + "edited · Jul 12" note · Look: normal sent row + the note · Where: the same row The touchpoint record is overwritten with the correction and stamped edited; the pre-edit text is stored in the record (audit value, cheap) but no viewer UI is built for it — owner ruling D4, settled: no tap-to-reveal, no "show original" link, no diff, nothing on the row beyond the "edited" note. The lead's actual inbox is untouched — no network send of any kind fires Must NOT resend or alter what the lead received; must NOT hide that the record was edited (the note is mandatory); must NOT build any viewer/reveal/diff control for the pre-edit original (ruled out — storage only); must NOT discard the pre-edit text from the stored record either; must NOT change the row's timestamp/position in the timeline RECORD FIXED
The row = corrected text + "edited" note; the timeline order is unchanged

S6 — Auto-sent vs owner-sent: visibly different in history (incl. the automatic payment chase) (happy path / owner-requested feature)

Who: System (auto-sends) + Mátyás (reading)  ·  When: Weeks into a deal, the history holds a mix of emails: ones he wrote and sent by hand, and ones the machine continued on its own — sequence follow-ups and, after signing, the automatic daily payment-reminder emails (one per day for 3–4 days, stopping on payment or a logged phone call). He asked for these to be distinguishable at a glance ("Add this feature" — W1 comment round).

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás reads a history that contains both kinds of sent email Every email the system sent on its own carries a small ⚙️ Auto marker on its row; emails he sent himself (manual replies, the first email of a Send-click, an AI reply he approved and sent) carry none — one glance separates "the machine kept going" from "I did this" Copy: ⚙️ Auto marker on auto-sent rows · Look: a subtle but unmistakable marker, consistent everywhere · Where: every auto-sent outbound row, board-wide W1's send ledger stamps auto: true on every machine-fired send; the history row renders the ⚙️ marker straight from that recorded fact — never guessed later from the type name. (W1 writes auto; W2 writes owner_edited.) Must NOT mark an owner-clicked send as ⚙️ Auto; must NOT leave an auto-continuation looking identical to a hand-sent email; must NOT vary the marker's look between row types ROW METADATA
The sent-by fact becomes part of every outbound row — history now answers "who sent this, me or the machine"
2 Nothing — both parties signed yesterday and no payment has landed; the automatic payment chase runs The history shows today's payment-reminder email as a ⚙️ Auto sent row with its full real text, and the remaining chase days as orange Scheduled rows — same frozen-copy treatment as any sequence (this chase is fully automatic; there is no owner button — W1 owns the cadence and the stop conditions) Copy: the chase email's full body (Stripe link + transfer details block) · Look: ⚙️ Auto sent rows + orange future rows · Where: the deal's history The self-arming payment-reminder sequence freezes its copy at the moment it arms itself (no Send-click exists for it — the arm moment IS its freeze moment); each daily send dispatches the frozen text; payment or a logged phone call stops the chase and the remaining orange rows disappear with a plain "stopped — paid" style annotation from the stop event. If a chase step ever goes on hold (S4), the hold banner fires for it exactly like any other hold — an unnoticed amber row here is money left waiting, the worst version of the drift problem, so an unattended chase is never allowed to stall silently Must NOT require an owner click to run the chase; must NOT keep showing orange chase rows after payment/stop; must NOT let a chase email's history text differ from what was delivered (parity applies to auto emails identically); must NOT put a chase step on hold without raising the hold banner NEW ENTRIES
One ⚙️ Auto sent row per chase day + orange rows for the days ahead; the stop event (payment / logged call) closes the story in the same timeline

S7 — Every history row wears the right direction label (happy path / completeness fix)

Who: Mátyás  ·  When: Scanning a long history he relies on the little sent/received pills to parse the conversation's back-and-forth. Today only 5 of ~25 event types carry one — a reminder email, a calendar invite, an onboarding email all render with no direction at all, even though they are real outbound mail. After W2 the classification is complete and principled.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás scans a mature lead's full history Every row that represents a real email carries a direction pill: sent on everything outbound (sequence emails, proposal, reminder emails, calendar invites, onboarding email, manual replies) and received on everything inbound (lead replies). Rows that have no direction by nature — stage changes, payments, notes, call logs, document events — show none, by design Copy: "sent" / "received" pills · Look: the existing pill styling, now present wherever it belongs · Where: every directional row in the drawer Every touchpoint type has an explicit direction classification (in / out / none) — no type falls through to blank by accident; a test guards that no directional type is unclassified Must NOT leave a genuinely outbound email type with a blank direction; must NOT stick a direction on system/stage/payment rows; must NOT ever label an outgoing email as a received reply (the old bug class — its ingestion root cause is fixed; this closes the display side generally) The whole timeline becomes consistently parseable — direction is information, and now it's never missing where it exists
2 He looks at a booked call's rows specifically The booking-confirmation row and the pre-call-reminder rows are distinguishable at a glance — each with its own label and icon, not two identical generic system rows (owner approved this in the W1 comment round: "Apply") Copy: distinct labels, e.g. "Booking confirmation sent" vs "Reminder sent (1 day before)" · Look: own icon per kind · Where: the call-related rows Confirmation and reminder touchpoints render with their own type labels/icons instead of a shared generic treatment Must NOT render confirmation and reminder as visually identical rows; must NOT change WHEN these emails send (existing flow — verify unchanged, W1 owns sending) Call-related rows become self-explanatory without opening them

S8 — Pre-call reminder rows show the full email bodies; cancellation stays an honest annotation (happy path / cheap follow-on)

Who: Mátyás  ·  When: A call gets booked (the booking flow itself already works — owner: "the current setup for it is perfect", verify, don't rebuild). He opens the history to sanity-check what the 3-day / 1-day / 2-hour reminders will actually say — today he gets 90-character snippets; after W2 he gets the full resolved emails.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás opens the history of a lead with a freshly booked call The "Reminders scheduled" row lists all three reminders (3d / 1d / 2h) each with its send datetime, subject, and the FULL resolved body — real name, real Meet link, complete text — not a truncated snippet Copy: full reminder bodies · Look: same row, richer content · Where: the reminders-scheduled row The reminder touchpoint stores/renders complete resolved bodies (reusing the same resolution treatment the orange rows got); the booking flow that writes it is unchanged Must NOT show 90-char previews anymore; must NOT show raw tokens; must NOT alter when/whether reminders send (existing flow, verify unchanged — reminders are already auto-send-whitelisted) ROW ENRICHED
The reminders row becomes a complete answer to "what exactly will the lead get before the call"
2 The call happens (or is cancelled) — the pending reminders stop A "Reminders cancelled" row appears after the scheduled row, and the earlier "Reminders scheduled" row is greyed out and struck through — still there (honest audit trail), but instantly readable as no-longer-live rather than as clutter competing with the cancellation Copy: "Reminders cancelled" · Look: grey annotation row; the superseded scheduled row greyed + struck · Where: after the reminders-scheduled row The cancellation is appended as its own event and marks the earlier scheduled entry superseded (a render state — the original row is never deleted; existing annotate behaviour, kept deliberately) Must NOT delete the earlier scheduled row; must NOT leave the superseded row looking live/active; must NOT send a reminder after the cancellation row exists Planned → stopped, both visible — the timeline never rewrites its own past

S9 — Unreplied lead email on the card + "Compose AI reply" (happy path / new feature, Q4)

Who: Mátyás  ·  When: A lead wrote in and nobody has answered yet. The card itself shows the lead's message and offers an AI-drafted reply he can edit and fire — without opening any other surface. (W8 owns where/how this sits on the card visually; W2 owns the draft's content — history-aware, correctly greeted, canonically signed — and the history record of the send.)

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Mátyás looks at a card whose latest lead email is unreplied The lead's message text is visible on the card itself (not hidden behind a click) together with a "Compose AI reply" button; once a reply goes out, this block disappears from the card Copy: the lead's email text + button "Compose AI reply" · Look: per W8's card design · Where: the lead card, only while unreplied The card reads the latest-inbound + replied-status straight from the touchpoint history — the same record the drawer shows, one source of truth Must NOT show a stale/older email as "latest"; must NOT keep showing the block after a reply was sent (from ANY path — AI reply, Missive, manual); must NOT invent a second inbound record for displaying it The inbound email is already a received row in history — the card surfaces it, nothing new is written
2 He clicks "Compose AI reply" The button disables itself instantly and reads "Drafting…", then an editable field appears pre-filled with an AI-written reply that demonstrably knows this lead: it answers what the lead actually asked, references the real state of the deal (from the touchpoint history so far), opens with the proper resolved greeting ("Kedves János,") and closes with the canonical sign-off Copy: button → "Drafting…"; then the AI draft — greeting + on-topic body + canonical sign-off · Look: disabled button during generation, then editable field + Send/Cancel · Where: on the card The draft is generated from the lead's full touchpoint history (past emails both ways, calls, stage) — not from a generic template — by a high-quality fast model (owner: "price doesn't matter here"): Gemini 3.1 Pro, the house default, in preference to a cheap/flash tier. The greeting variable resolves to the real name; the button's single-flight state makes a double-click impossible; nothing sends yet Must NOT stay clickable while generating (a second click must never start a competing draft); must NOT look dead/frozen with no "Drafting…" state; must NOT downgrade to a cheap model to save cost; must NOT show a raw {{greeting}} token; must NOT produce a generic reply that ignores the lead's actual question; must NOT drop the canonical sign-off; must NOT auto-send the draft — (a draft is not a touchpoint; nothing is written until the send)
3 He edits the draft as needed and clicks "Send" The field closes immediately — he moves on; the reply goes out in the background; the card's unreplied block clears Copy: card returns to normal · Look: field gone, block gone · Where: the card The send fires through W1's send path (the click is the human gate); the exact text he sent — with his edits — is recorded. A background failure raises the red ring + banner (W1/W8), never a silent loss Must NOT send without the explicit click; must NOT block him waiting for the server; must NOT record a draft version that differs from the sent version NEW ENTRY
An outbound sent row appears with the exact reply text — no ⚙️ Auto marker (he gated it) — and the inbound row now counts as replied, which is what clears the card block

S10 — The missing banner moments: proposal, signature, payment, reminder, failed send (coverage fix / no new UI)

Who: System + Mátyás  ·  When: Big deal moments happen while he's looking elsewhere. The live banner system already exists and works (persists until dismissed, click-through to the card, per-kind mute) — it just doesn't fire for these events yet. W2 adds the missing moments to the existing surface. No failures-list screen is built — the owner killed it ("banner notification and that's it").

#You doYou should see Element that changes
copy · look · where
What changes underneathMust NOT happen 🕓 Touchpoint history
1 Nothing — a proposal goes out / a contract gets signed / a deposit or balance payment lands / a pre-call reminder sends A live banner appears top-right naming the event with its own icon + label ("Contract signed — {lead}", "Payment received — {lead}"…), persists until dismissed, and clicking it jumps to the card — exactly like the existing booking/new-lead banners Copy: event-specific label per kind · Look: the existing banner style with a proper icon per kind (not a generic bell) · Where: the existing banner stack The already-built banner system gains the missing event kinds at their source moments; the banner mechanism itself is untouched Must NOT rebuild/redesign the banner UI; must NOT fall back to a generic bell/raw-kind-name banner for the new kinds; must NOT fire duplicate banners for one event Each of these moments is already (or becomes) its own history row on the card — the banner is the live echo, the history row is the durable record
2 Nothing — a scheduled/automated send FAILS in the background A banner names the failure ("Email failed to send — {lead}") and the card wears the red dashed ring (W1 detects, W8 styles) — and that is the whole failure surface: no failures-list screen exists anywhere Copy: the failure banner text · Look: banner + red dashed ring on the card · Where: banner stack + the card The send failure raises a banner event; the failed step's history row shows a plain failed state (not orange "scheduled", not sent) so the drawer tells the truth too Must NOT stay silent on a failed automated send; must NOT build a failures list / review queue (owner explicitly killed it); must NOT show the step as sent or still-scheduled in history when it failed FAILED STATE
The step's row flips to a failed state with the frozen copy it tried to send — recovery (retry) is W1's behavior; W2 records the truth
3 Verify-only sweep (no build): book one test call end-to-end Exactly ONE booking-notification email arrives for the booking (no duplicates), and Mátyás receives the real Meet invite as a named attendee in his own inbox — both behaviours are already shipped in code; this is a live re-check, not a rebuild Copy: — · Look: — · Where: his inbox + calendar Existing flow, verify unchanged: the once-per-booking guard and the both-parties invite are already on main — the EBO run proves them live and touches nothing Must NOT receive two notification emails for one booking; must NOT "fix" or refactor these shipped paths as part of W2 The booking rows appear as they already do (plus S7·2's clearer labels)
🕓 Touchpoint-history note for W2:
W2 IS the history workstream — every scenario lands in the drawer. The state machine of a scheduled email row is: orange (frozen, will auto-send as shown) → amber (data drifted → send HELD until re-approved) → back to orange on save-&-approve → sent (converts in place, byte-identical text, ⚙️ Auto marker if the machine fired it) — or failed (red state + banner) if the send errored. Past rows are editable too: a sent record accepts a correction and wears a mandatory "edited" note, and never re-sends. The one bar over everything: the text a row shows is the text that was, or will be, delivered — 100%, character for character, in every path (Send-click, edited, re-approved, auto-chase, AI reply).

Work-item → scenario-step mapping

Each build work item derived for W2, and the scenario steps that prove it works when clicked through on the staging board.

Work itemWhat it deliversProven by (scenario · step)
WI-1 · Freeze at Send-clickSend generates + freezes the whole sequence's copy exactly as previewed (modal UI untouched, closes instantly); the frozen snapshot — not a re-resolve — is what sends; clean all-or-nothing failure if generation errors. Self-arming sequences (payment chase) freeze at their arm moment.S1·1 · S1·2 · S1·3 · S1·4 · S2·2 · S6·2
WI-2 · Orange scheduled rowsFull frozen subject+body (resolved, untruncated) + exact Budapest send datetime per pending step, in a genuine orange token distinct from amber; converts in place to a sent row with identical text.S2·1 · S2·2 · S6·2
WI-3 · Drift check + amber HOLDLead-data change before send-day flips affected rows amber and BLOCKS the send until re-approved; per-step, safe-by-default on check errors; overdue holds send on re-approval.S4·1 · S4·2 · S4·3 · S4·4
WI-4 · Full modal editor from historyOrange/amber rows open the full step editor (edit → save = owner-edited = auto-send-approved, schedule untouched); sent rows open the record editor (fix + mandatory "edited" note, never re-sends).S3·1 · S3·2 · S3·3 · S4·3 · S5·1 · S5·2
WI-5 · Auto vs owner-sent markerEvery outbound row records + shows who fired it — ⚙️ Auto marker on machine sends (sequence continuations, payment chase), nothing on human-gated sends.S6·1 · S6·2 · S9·3
WI-6 · Direction-label completionEvery touchpoint type explicitly classified in/out/none — sent/received pills wherever a real email direction exists, none where none exists; confirmation vs reminder rows distinguishable.S7·1 · S7·2
WI-7 · Full reminder bodiesPre-call reminder rows carry complete resolved email bodies (3d/1d/2h) instead of 90-char snippets; cancellation stays an annotation.S8·1 · S8·2
WI-8 · AI reply on card (content + record)History-aware AI draft with resolved greeting + canonical sign-off in an editable field; human-gated send recorded with exact parity; unreplied block clears on any reply path.S9·1 · S9·2 · S9·3
WI-9 · Banner coverageProposal-sent / contract-signed / payment / reminder-sent / send-failed + scheduled-email-on-hold banner kinds on the existing banner surface with proper icons — no failures list; B15/B16 verified live only.S10·1 · S10·2 · S10·3 · S4·1 (hold banner — owner: apply)
✔ Decisions settled — the register is CLOSED, nothing in this doc is openD1 hold banner: apply (fires for every hold, chase steps included). D2 re-approve editor: confirmed — frozen text + old→new diff line + optional "Update to new value" link, never a silent pre-apply. D3 hold scope (owner, 2026-07-12): a drift holds only the affected email — never the whole sequence; untouched steps stay orange and keep sending. D4 edited sent record (owner, 2026-07-12): the pre-edit text is stored in the record but no viewer UI is built — the mandatory "edited" note is the entire visible surface. D5 reminders-cancelled: keep annotate; the superseded row is greyed/struck, never deleted. D6 orange rows: collapsed (subject + first line) with a separate expander for the full body.
Sign-off — acceptance oracle. By signing, Sarudi Mátyás locks scenarios S1 … S10 and the invariants above as the acceptance answer key for W2 — Touchpoint History. Every claim the system makes afterwards about what a lead was (or will be) sent is verified against these steps. Every decision is ruled — no step may be reported AMBER for a missing owner answer. No W2 code merges to main before this signature and a green staging QA pass captured in SHIP_LEDGER.md.
Sarudi Mátyás  ✔ FINAL · 2026-07-12 · awaiting sign-off