ClientsFlow · Email-System Overhaul · W3 · EBO
FINAL · owner comment round folded in · 2026-07-12. The target board-card Details drawer after W3 ships — every piece of saved lead data shown as an editable field, sorted by how likely Mátyás is to need it, the {{greeting}} the AI opens every automated email with finally visible, editable and previewable, a human verification gate on genuinely ambiguous AI legal data (in the post-call modal, not the drawer), and a way to re-run the legal lookup when a lead entered without a website. Scenarios S1 … S9. Acceptance oracle for /ebo-live-build.
payment_plan, appendix_mode, price_expiry_date, price_after_expiry. They sit low in the Deal group (item 28: relevance ordering).config.PAYMENT_PLANS = 50% / 100% upfront; appendix = structured / freeform). No free text, no invalid states.Szia, (no name). This replaces the old "Szia <first name>" fallback everywhere it resolves. The empty Greeting field carries a quiet amber tint so the one state that most needs attention is the one that catches the eye.ZZ-S1-FIELDS Kft — every saved field populated, greeting Szia ZZ-S1 Bence ·
ZZ-S1-NOGREET Kft — no greeting captured ·
ZZ-S2-GREET Kft — greeting Szia ZZ-S2 Bence, edited to Kedves ZZ-S2 Bence ·
ZZ-S3-AMBIG Kft — legal lookup returned conflicting candidates; confirmed legal name ZZ-S3 Confirmed Legal Kft ·
ZZ-S3-CLEAN Kft — unambiguous lookup ·
ZZ-S4-LATEURL Kft — no website, legal enrichment never ran; late URL https://zz-s4-lateurl.example ·
ZZ-S5-DRY Kft — lookup ran, returned nothing; hand-typed legal name ZZ-S5 Handtyped Legal Kft ·
ZZ-S6-MANUAL Kft — hand-typed values ZZ-S6 Kezzel Irt ·
ZZ-S7-SAVEFAIL Kft — save path forced to fail; the never-saved value is ZZ-S7 Never Saved ·
ZZ-S8-ROUNDTRIP Kft — every field re-typed with a ZZ-S8-RT… value ·
ZZ-S9-WIDE Kft — all fields populated with long values.$BASE (the staging app root) and $DASH_TOKEN (the live staging dash token — validated with one GET, never computed from local env) exported. The ONLY curly placeholder the walker binds is {lead_id} (the seeded deal's id). No other {…} placeholder is substituted.| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás clicks "Details" on the ZZ-S1-FIELDS Kft card |
The drawer opens with the familiar three groups — Contact → Deal → Legal — and every field the system saves for this lead has a row, pre-filled with its current stored value. Inside each group the daily-touched fields sit full-size at the top; the rarely-touched tail sits below a hairline divider in smaller, muted labels | Copy: group headers unchanged (Contact · Deal · Legal) · Look: same drawer, fuller field grid, a hairline divider + smaller muted labels for each group's tail · Where: the Details drawer | The drawer renders the full saved-field set from the lead's stored record; the board payload's info projection delivers every field the drawer lists (nothing stripped before it reaches the screen) |
Must NOT show a field the backend would silently refuse to save (a visible field that can't round-trip is worse than a hidden one); must NOT drop any of the 18 existing fields while adding the new ones; must NOT place a rarely-edited field above a daily-edited one | — (opening the drawer reads state; it never writes history) |
| 2 | Mátyás scans the Contact group, then hovers the Greeting label | Directly under the lead's name sits a new "Greeting" field, pre-filled with the greeting captured from the lead's cold email (Szia ZZ-S1 Bence) — the exact line every automated email to this lead opens with. Hovering the label reveals the tooltip "Opens every automated email to this lead" |
Copy: label "Greeting"; hover tooltip "Opens every automated email to this lead" · Look: normal wide editable text field, second slot in Contact (right after the name) · Where: Contact group | The stored greeting value (captured at pipeline entry from the cold email) is projected into the board payload and rendered into the drawer |
Must NOT bake the stakes text into the visible label (owner ruling: it belongs in the hover tooltip); must NOT show a raw {{greeting}} token instead of the value; must NOT show a greeting belonging to a different lead; must NOT render it read-only |
— (the greeting was captured at intake by the inbound flow; showing it adds no entry) |
| 3 | Mátyás scans the Deal and Legal groups and opens the Payment-plan dropdown | Deal now also lists the four previously-hidden saved fields — Payment plan, Appendix mode, Price expiry date, Price after expiry — in the group's muted tail; Payment plan and Appendix mode are dropdowns (Payment plan offers exactly "50% upfront" / "100% upfront"; Appendix mode offers "structured" / "freeform"), the other two are a date and a number field. Legal gains an editable "Legal form" field alongside the existing four | Copy: labels "Payment plan" · "Appendix mode" · "Price expiry date" · "Price after expiry" · "Legal form" · Look: two selects + a date + a number + a text row, all in the muted tail of their group · Where: Deal group tail + Legal group | Each new row reads its saved value; the dropdown options are generated from the backend's existing option lists (config.PAYMENT_PLANS, the structured/freeform appendix modes) — never a hand-copied duplicate; the save whitelist and the Notion CRM mirror mapping are extended in lockstep so each new field round-trips (see S8) |
Must NOT hide any of these because they "seem unimportant" (explicit owner ruling — every saved field gets a row); must NOT render Payment plan or Appendix mode as free text; must NOT offer a Payment-plan option the backend rejects (e.g. a 70% option — it was dropped) | — |
| 4 | Mátyás opens Details on ZZ-S1-NOGREET Kft — a lead whose cold email had no usable greeting line |
The Greeting field is empty and quietly amber-tinted, with the placeholder: "empty — emails will open with 'Szia,'" — so he knows exactly what the fallback does before any email goes out | Copy: placeholder text as quoted · Look: empty field with a quiet amber tint (the one state that needs attention is the one that catches the eye) · Where: Greeting field, Contact group | No stored greeting → the field stays blank; the greeting-resolution fallback is the literal "Szia," (owner ruling 2026-07-12 — this replaces the previous "Szia <first name>" fallback in the shared resolver, so every send path inherits it) |
Must NOT invent/auto-fill a greeting into the stored record just to fill the box (an empty store stays empty until a human or the capture flow writes one); must NOT hide the field when empty; must NOT resolve to "Szia <first name>" anywhere after this change |
— |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás retypes the Greeting field (Szia ZZ-S2 Bence → Kedves ZZ-S2 Bence) and clicks the floating Save button |
While he types, a muted preview line under the field shows the exact first line the lead will read: "Kedves ZZ-S2 Bence," (the comma the template adds is visible, so double-punctuation is impossible to miss). On Save the drawer stays free instantly — the existing floating Save button does its usual brief "Saved" confirmation; no modal, no blocking spinner | Copy: the field holds his new text; the preview line reads "Kedves ZZ-S2 Bence," · Look: one muted line directly under the Greeting input + the existing Save-button confirmation · Where: Contact group + floating Save | The new greeting is written to the lead's record and stamped as a manual field (so no future auto-capture/enrichment can overwrite it); the Notion CRM's existing "Greeting" property is mirrored in the background, best-effort, never blocking the save. The preview renders through the same resolver the sender uses — it is not a second implementation | Must NOT be silently dropped by the save whitelist (today a greeting write is discarded server-side — that is the exact bug W3 closes); must NOT block the UI on the Notion mirror; must NOT touch any other field in the same save; must NOT show a preview line that disagrees with what the sender would render | — (a field edit is not a lead touchpoint; history stays owned by W2) |
| 2 | Later, the next automated email for this lead is generated (Send-Emails modal or any template render) — Mátyás just reads it | The email copy opens with his edited line ("Kedves ZZ-S2 Bence,") — not the originally captured greeting, not the fallback | Copy: the email's first line = the saved Greeting value · Look: unchanged email UI · Where: wherever email copy renders (Send-Emails modal, history preview) | The existing {{greeting}} resolution reads the stored value — W3 changed the stored value's visibility and editability plus the empty-case fallback, never the resolution mechanism (existing flow, verify unchanged) |
Must NOT render the stale pre-edit greeting anywhere copy is generated after the save; must NOT alter when/whether that email sends (W1's domain) | When that email actually sends, W2's history row must show the resolved edited greeting — parity between shown and sent is W2's hard bar; W3 just feeds it the right value. |
| 3 | Background proof (QA, not a click): probe the saved record and the CRM mirror a few seconds after step 1 | — (nothing on screen; this row is the reliability check behind the non-blocking save) | — | The lead record holds the new greeting + the manual-field stamp; within a short background window the Notion CRM page's "Greeting" property shows the same value (capped at the same 300-char limit used at intake) | Must NOT find the store and the CRM mirror permanently disagreeing; a failed mirror write must not un-save the store value | — |
| # | 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 post-call modal on ZZ-S3-AMBIG Kft (the 3-step post-call wizard on the card) |
Before he can move on to the proposal, a compact "Verify legal data" block sits in the modal: the conflicting candidates the AI found, side by side, each showing legal name / seat address / tax number / representative — plus editable fields so he can correct anything, and one "Confirm" button | Copy: block title "Verify legal data — the AI found conflicting records"; button "Confirm" · Look: compact amber-edged block inside the existing post-call modal, above the proposal step · Where: post-call modal only | The block renders off the stored ambiguity signal the legal lookup wrote (conflicting candidates + the "cannot decide" flag). Nothing is written by showing it | Must NOT appear for a lead whose lookup was unambiguous (even if it was merely low-confidence); must NOT appear for a lead with no legal data at all (nothing to verify — that is the Re-run case, S5); must NOT appear anywhere outside the post-call modal; must NOT block him from closing the modal entirely (he can leave; the gate returns on the next open) | — (verifying CRM data is not a lead touchpoint) |
| 2 | Mátyás picks the correct candidate (or corrects a field by hand) and clicks Confirm | The block collapses to a quiet one-line confirmation and the modal continues to the proposal step exactly as before; the confirmed legal values now appear in the Details drawer's Legal group | Copy: "Legal data confirmed" one-liner replacing the block · Look: block gone, wizard proceeds · Where: post-call modal → Details drawer Legal group | The four legal fields are written to the deal and stamped as manual fields (human-owned forever); the ambiguity flag is cleared, so the gate never asks again for this lead; the contract's Megbízó block fills from these confirmed values | Must NOT ask again on the next post-call-modal open for the same lead; must NOT let a later auto-lookup overwrite the confirmed values; must NOT send or schedule anything as a side effect of confirming | — |
| 3 | Mátyás opens the post-call modal on ZZ-S3-CLEAN Kft (unambiguous lookup), then its Details drawer |
No verification block in the modal — it goes straight to the proposal step. The Details drawer shows the legal values as plain editable fields with no badge of any kind: silence is the normal state | Copy: — · Look: clean modal, clean Legal group header · Where: post-call modal + Details drawer | No ambiguity flag stored → nothing renders; the drawer's Legal group never carries a confidence badge in any state | Must NOT show a confidence/"AI guess" badge in the Details drawer (owner ruling: the drawer stays clean, the gate lives in the post-call modal); must NOT show a stale verification block carried over from a previously-opened lead | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás types the lead's website into the red-badge inline URL form and saves | The form confirms instantly, exactly as today — he is free to keep working immediately; no new dialog, no wait. And the card immediately shows a small "looking up legal data…" hint (owner ruling D5 — the re-enrichment is NOT silent), so he knows the machine picked the URL up and is already working | Copy: unchanged existing form + the new hint text "looking up legal data…" · Look: the existing form untouched; the hint is a small muted line/chip on the card where the red website badge used to sit · Where: the card's website-missing badge/form area | The URL saves as before, plus: because this lead's legal enrichment never ran, a background enrichment pass (website scrape → AI legal lookup) is kicked off fire-and-forget — the save response never waits for it. The lead now carries an "enrichment in flight" marker that the board payload exposes; the card renders the hint straight off that marker | Must NOT block or slow the URL save on the multi-second AI lookup; must NOT run the lookup silently with no on-screen sign (the old spec — reversed); must NOT make the hint a modal/spinner that blocks the card or the board; must NOT re-fire enrichment for a lead whose legal enrichment already completed (only the never-ran case auto-triggers) | — (enrichment fills CRM fields; it is never a touchpoint — enrichment stays invisible to history) |
| 2 | Nothing — he watches the card while the background lookup runs, then finishes | The "looking up legal data…" hint sits on the card for the whole run, then disappears by itself when the lookup finishes — whether it found legal data or came back empty. Nothing else changes on the card; the board was usable the entire time | Copy: hint present → hint gone · Look: the hint clears with no toast, no banner, no click needed · Where: the card | The enrichment pass clears the in-flight marker when it completes (success, empty result, or error — all three clear it); the card stops rendering the hint on the next board refresh. A crashed/timed-out lookup must not strand the hint forever — the marker expires | Must NOT leave the hint stuck on the card after the lookup ends (including when it found nothing or errored); must NOT require a click or a reload to clear it; must NOT show the hint on a lead with no lookup in flight | — |
| 3 | A little later, Mátyás opens the lead's Details drawer | The Legal group is populated — Legal name, Seat address, Tax number, Representative and Legal form carry the lookup's results, all editable, with no badge | Copy: real legal values in the fields · Look: filled editable fields · Where: Legal group | The background lookup wrote the legal fields through the existing enrichment guard — any field Mátyás had already typed by hand was skipped, untouched. If the lookup came back conflicting, the ambiguity flag is stored for S3's post-call gate (nothing renders in the drawer) | Must NOT overwrite a hand-typed legal field (the human-wins invariant); must NOT leave the contract's client block blank when this data now exists; must NOT surface the ambiguity in the drawer | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás opens Details on ZZ-S5-DRY Kft — a lead whose lookup ran but produced nothing |
Empty Legal fields and a small "↻ Re-run legal lookup" button on the Legal group header — bare: no "last lookup: … — nothing found" line under it (owner ruling D6, settled: that line is not built) | Copy: button "↻ Re-run legal lookup" and nothing else · Look: compact secondary button beside the group header · Where: Legal group, Details drawer | Nothing yet — the button only renders; the empty-result state is stored so the drawer knows the lookup ran and came back dry, but that state is never surfaced as a line of text | Must NOT hide the button (it is the only retry affordance); must NOT show any badge over the empty fields (nothing was guessed, nothing is conflicting); must NOT render a "last lookup …" / "nothing found" line anywhere near the button (explicitly ruled out) | — |
| 2 | Mátyás clicks "↻ Re-run legal lookup" (and immediately clicks it a second time) | The button acknowledges instantly (brief "running…" state) and goes inert; he is free to close the drawer and move on — the lookup runs in the background; on his next drawer open the results (or the still-blank fields + button) tell him the outcome | Copy: button flips to "running…" then back · Look: button-only state change, drawer never locks · Where: Legal group header | The lead's enrichment-done marker is cleared and the existing enrichment machinery re-runs in the background (reused, not reimplemented); results write through the human-wins guard as always; the second click is swallowed — exactly one lookup runs | Must NOT block the drawer while the AI call runs; must NOT stack duplicate lookups from double-clicks (the button is inert while one is in flight); must NOT overwrite any hand-typed field with the retry's results | — |
| 3 | The AI still finds nothing — Mátyás types the four legal fields by hand and saves | The values persist like any field edit; from now on those fields are human-owned and no future lookup (auto or manual re-run) touches them | Copy: his typed legal data · Look: normal field save · Where: Legal group | Each typed field gets the manual-field stamp — the existing, tested invariant (existing flow, verify unchanged) | Must NOT let the re-run button's next click clobber the hand-typed values; must NOT regress the existing manual-edit-wins test | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás hand-edits fields on ZZ-S6-MANUAL Kft — including the new Greeting and Legal form — saves, and a background enrichment pass completes afterwards |
Reopening the drawer shows his values, exactly as typed — the enrichment filled only the fields he never touched | Copy: his values intact; other blanks filled by enrichment · Look: unchanged drawer · Where: Details drawer | Enrichment writes through the guard that skips every manually-stamped field — existing tested behavior, now also covering greeting and legal_form |
Must NOT overwrite any human-typed value, ever — including the new W3 fields; must NOT strip the manual stamp during any save/mirror path | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás edits the Greeting on ZZ-S7-SAVEFAIL Kft, clicks Save while the save path is broken, then closes the drawer |
The existing "save failed" warning appears on the floating Save button, his typed text is still sitting in the field (not reverted) — and after he closes the drawer, the card's Details button stays in a warning state until the value actually saves. The failure outlives the toast | Copy: the existing failure toast wording (unchanged) · Look: existing warning state on the Save button + a persistent warning mark on the card's Details button · Where: floating Save button · edited field · the card's Details button | The store write did not happen; the drawer keeps the unsaved text client-side so a retry click can resend it (existing flow, verify unchanged — extended to the new fields); the card remembers it holds an unsaved failed field until a save succeeds | Must NOT show "Saved" when the write failed; must NOT wipe his typed text back to the stored value; must NOT let the warning disappear when the drawer closes; must NOT leave the new fields (Greeting etc.) outside this failure path while the old 18 are inside it | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | QA sweep (no owner click): edit-and-save every single field the drawer shows on ZZ-S8-ROUNDTRIP Kft, then reopen the drawer |
Every value comes back exactly as typed — all groups, old fields and new alike; zero fields accept input on screen but lose it on save | Copy: — · Look: — · Where: whole Details drawer, field by field | Every drawer field has a matching entry in the save whitelist and (where the CRM has the property) the Notion mirror mapping; a test fails the build if any of the three lists drifts from the others | Must NOT ship any field that renders but doesn't persist; must NOT let a future field addition touch one list without the guard test catching the other two | — |
| # | 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 Details drawer on ZZ-S9-WIDE Kft at desktop widths (1280 and 1440) |
All three groups, the Greeting field + its preview line, both dropdowns, the muted tail rows and the Re-run button render cleanly inside the drawer; long values wrap or scroll inside their own field; no horizontal page scroll | Copy: full · Look: full drawer layout, nothing clipped · Where: Details drawer | Standard desktop layout; the drawer scrolls vertically when the field list is long | Must NOT clip the Re-run button or a dropdown off the drawer edge; must NOT overlap labels and inputs; must NOT push the page into horizontal scroll | — |
Each build work item for W3 and the scenario steps that prove it works when clicked through live on staging. The /ebo-live-build walker drives exactly the listed steps after each item ships, then the full set before merge. Ruling notes mark where the 2026-07-12 owner comment round reshaped an item.
| Work item | What it delivers | Proven by (scenario · step) | Ruling note |
|---|---|---|---|
| WI-1 · Greeting field + preview + fallback | Editable "Greeting" field in Contact (second slot) with a hover tooltip and a live rendered-first-line preview, wired end-to-end: board projection → drawer → save whitelist → manual stamp → Notion "Greeting" mirror → template resolution. Empty-greeting fallback becomes the literal "Szia,"; the empty field carries a quiet amber tint. | S1·2 · S1·4 · S2·1 · S2·2 · S2·3 | RESHAPED by owner comments — stakes text moved from the label into a hover tooltip; preview line applied; fallback changed from "Szia <first name>" to "Szia,". D4 SETTLED — the field's place is the SECOND slot of the Contact group, under the lead's name; anywhere else is a defect. Cross-workstream: the fallback change is in the shared resolver — W1/W2 send copy inherits it. |
| WI-2 · Full field census + tail de-emphasis | Every saved lead field gets an editable row (adds Payment plan, Appendix mode, Price expiry date, Price after expiry, Legal form), sorted by relevance; each group's rarely-touched tail sits below a hairline divider in smaller muted labels; Payment plan + Appendix mode render as dropdowns fed by the backend's own option lists. | S1·1 · S1·3 · S8·1 | D1 SETTLED — the four deferred Deal fields are IN (item 27 + the owner's dropdown ruling presupposes them). Dropdowns + tail de-emphasis applied per comments. |
| WI-3 · Ambiguous-legal verification gate | The legal lookup records a conflicting-candidates / cannot-decide signal; the post-call modal shows a "Verify legal data" block with the candidates + editable fields + Confirm, only for that state. Confirming stamps the four fields manual and clears the flag. | S3·1 · S3·2 · S3·3 · S4·3 | REWRITTEN by owner comment — the Details-drawer confidence badge is DEAD. Verification only when truly ambiguous/conflicting, and only inside the post-call modal. A merely low-confidence lookup raises nothing. |
| WI-4 · Auto re-enrichment on late URL + the "looking up legal data…" hint | Saving a website URL for a never-legally-enriched lead fires the existing enrichment machinery in the background without blocking the save — and the card shows a small "looking up legal data…" hint for as long as the lookup runs, clearing itself on completion (found, empty, or error), with no click and no reload. | S4·1 · S4·2 · S4·3 | ⚠️ REVERSED by owner ruling D5 (settled) — the run is no longer silent: the in-flight hint is now REQUIRED visible behavior (an in-flight marker on the deal + the card hint + its self-clearing). Everything else about the item is unchanged. |
| WI-5 · Manual "Re-run legal lookup" | Retry button on the Legal group header — instant acknowledgment, background run, double-click-safe, human-typed fields untouched. The button stays bare. | S5·1 · S5·2 · S5·3 | D6 SETTLED — the muted "last lookup … — nothing found" line is NOT built. Do not add it. |
| WI-6 · Human-wins invariant extended | The existing manual-edit-wins guard covers the new fields (greeting, legal form, the four Deal-tail fields) on every auto-fill path; existing tests stay green. | S6·1 | Unchanged existing invariant — verified, not redesigned. |
| WI-7 · Save-failure integrity | New fields ride the existing failure toast + keep-typed-text behavior, and the failure now persists on the card's Details button after the drawer closes. | S7·1 | EXTENDED by owner comment — the failure must outlive the transient toast. |
| WI-8 · Field-list lockstep guard | A test that fails the build if the drawer field list, the save whitelist, the board projection, and the CRM mirror mapping ever drift apart again. | S8·1 | Unchanged. |
| WI-9 · Desktop drawer layout | The grown drawer holds at 1280 and 1440 — nothing clipped, overlapped, or pushed into horizontal scroll. | S9·1 | NARROWED by owner comment — tablet/mobile dropped entirely (global desktop-only rule for the pipeline dashboard). |