ClientsFlow · Email-System Overhaul · W2 · EBO
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.
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.{{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.| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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) |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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 |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must 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) |
Each build work item derived for W2, and the scenario steps that prove it works when clicked through on the staging board.
| Work item | What it delivers | Proven by (scenario · step) |
|---|---|---|
| WI-1 · Freeze at Send-click | Send 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 rows | Full 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 HOLD | Lead-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 history | Orange/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 marker | Every 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 completion | Every 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 bodies | Pre-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 coverage | Proposal-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) |
main before this signature and a green staging QA pass captured in SHIP_LEDGER.md.