ClientsFlow · Live-test fixes · Signed-EBO acceptance oracle
One signed doc = the human review artifact AND the machine oracle for the ebo-live-build loop. 10 scenarios (S1–S10), 36 steps, grouped into 4 work items · owner decisions in DECISIONS.md folded in · root causes in notes/INV_*.md · lead of record: Kocsy Gergely (prod build 876f15b-dirty+v225).
booking_context() returns lead_booking_link(deal), plus the three template additions).
B4 = keep the 2h-before reminder (Q3=B); for a same-day booking drop any day-type reminder whose slot is already past at first observation.
B9 (Q4=A) = show the "Waiting for transcript" overlay on Booked/appt_booked cards too, without marking prepped.
B1 (Q5) = collapsible 🔔 notification bell + a one-line red summary pinned only on send failure, dismissible with an ✕.
B6 (Q6=A) = lower .booked-card .flag to ~1rem (amend frontend.md).
B8 = NO bot ever dispatched (native Fireflies join only); native-join reliability is owner-side Fireflies config (see the owner note under S8)./dash/api/deal/{lead_id} returns the RAW deal record — draft bodies live at ai_draft.body and armed-sequence step bodies at seq_steps[i].body; the deal's stage is stage_key (the board JS d.stage is a frontend projection — a probe grepping stage misses stage_key). ·
/dash/api/send_ledger?deal={lead_id} returns full ledger rows (category, status, step, frozen_subject/body, sent_subject/body) — reminders ride it as category="reminder", step∈{3d,1d,2h}; neg-FUP as category="neg_fup". ·
/dash/api/queue returns Review-needed rows (name, lead, draft) — nudge/chase drafts land here (their Notion "Missive account" is empty, so the cold-inbox account-gate keeps them). ·
/dash/api/history?lead=<lowercase ZZ email> returns touchpoint rows. ·
POST /dash/api/fireflies/dispatch?token=… with {"meeting_url":…}. The booking staging page is served by a DIFFERENT app (booking base, not $BASE) and needs a booking-minted token the walker can't forge — so the read-only-page steps (S1.7/S1.8) are pixel-only by necessity, not laziness./missive/incoming so process_missive generates the AI reply and stamps deal.ai_draft.body. (2) Arm the post-reply state (awaiting_reply, last_outbound_at back-dated) and run followup_nudge_sweep twice so _draft_nudge writes the "Follow-up #1" and "Follow-up #2" Review-needed rows (their booking link comes through booking_context). (3) Advance to the proposal step and call the post-call step-3 path so sequences.define("proposal_chase", …) stores the proposal cover (seq_steps[0]) + chase (seq_steps[1]) bodies on the deal. (4) Put it through a negative reply and click the +2-day rebook so send_negative_followup writes the neg_fup ledger row (send BLOCKED on the ZZ address, but the frozen body is recorded). Identity ZZ-S1 Booking Kft + zz-s1@zz-sentinel.test re-created verbatim on every reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Open the AI reply draft the app generated for the inbound reply | The draft body offers a booking CTA whose link contains the unique per-lead ?deal&token |
Copy: booking CTA (foglalás/időpont) · Where: AI reply draft body | PROBE (api_deal): ai_draft.body contains both deal= and token= (from lead_booking_link, after booking_context stops returning the generic link) |
Must NOT leak a raw {{booking_link}}; must NOT carry the generic (token-less) public link |
No new entry — draft only, not sent. |
| 2 | Open the AI nudge #1 draft (Review queue) | Nudge #1 body carries the same unique per-lead booking link | Copy: "foglalj időpontot" CTA · Where: Follow-up #1 queue draft | PROBE (api_queue): the "Follow-up #1" row for zz-s1@zz-sentinel.test has deal=+token= in its draft |
Must NOT ship nudge #1 with the generic link or no link | No new entry — draft only. |
| 3 | Open the AI nudge #2 draft (Review queue) | Nudge #2 body carries the same unique per-lead booking link | Copy: booking CTA · Where: Follow-up #2 queue draft | PROBE (api_queue): the "Follow-up #2" row for zz-s1@zz-sentinel.test has deal=+token= in its draft |
Must NOT ship nudge #2 with the generic link or no link | No new entry — draft only. |
| 4 | Open the negative-FUP rebook nudge (its recorded body) | The "want to rebook?" nudge now carries the unique per-lead booking link | Copy: rebook CTA · Where: neg-FUP body | PROBE (api_send_ledger): the neg_fup row's frozen_body/sent_body contains deal=+token= (NEG_FUP_BODY_HU now renders {booking_link}) |
Must NOT be a rebook nudge with no booking link at all | No new entry (send blocked — ZZ address). |
| 5 | Open the proposal cover draft (post-call step 3) | The proposal cover now includes the unique per-lead booking link | Copy: booking CTA line · Where: proposal cover (seq_steps[0]) |
PROBE (api_deal): seq_steps[0].body contains deal=+token= |
Must NOT ship a proposal cover with no booking link | No new entry — draft only. |
| 6 | Open the proposal chase draft (post-call step 3) | The proposal chase also includes the unique per-lead booking link | Copy: booking CTA line · Where: proposal chase (seq_steps[1]) |
PROBE (api_deal): seq_steps[1].body contains deal=+token= |
Must NOT ship a proposal chase with no booking link | No new entry — draft only. |
| 7 | Open the unique link itself (booking staging page) | A read-only page showing only the confirmation email — no editable fields, no name field | Copy: "Foglalás e-mail-címe: zz-s1@zz-sentinel.test" · Look: read-only, date/time picker only · Where: booking page | PIXEL-ONLY — the booking page is a separate app on the booking base (not $BASE) and needs a booking-minted token the walker can't forge; judged from the screenshot. |
Must NOT render the editable public form's "Neved" name input | No new entry — page view. |
| 8 | Confirm the same link does NOT render the empty editable public form | Still the read-only email page — none of the public form's editable inputs | Look: no "Neved"/"Telefonszám" inputs · Where: booking page | PIXEL-ONLY — negative guard on the same booking-app page (off $BASE). |
Must NOT show the "Telefonszám (opcionális)" editable field of the public form | No new entry — page view. |
reminder_sweep once (the product emitter) so the ledger reflects exactly what fired. (2) ZZ-S2b Normal Kft / zz-s2b@zz-sentinel.test — books a call several DAYS out (REGRESSION control) through the same flow, so log_reminder_schedule records the full 3d/1d/2h schedule but nothing has fired yet. Never plant a ledger row or a reminder marker; both identities re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Open ZZ-S2 SameDay Kft's history after booking | Exactly one booking-confirmation email was sent for the same-day booking | Copy: booking-confirmation touchpoint · Where: card history | PROBE (api_send_ledger): exactly ONE booking_confirmation row exists |
Must NOT send the confirmation twice | critical One booking-confirmation touchpoint. |
| 2 | Read the fired reminders in the history right after the confirmation | The 2-hours-before reminder fired; NO day-before reminder fired minutes after the confirmation | Copy: "Reminder sent (2 hours before the call)" · Where: card history | PROBE (api_send_ledger): NO reminder row with step=="1d" exists (the day-before was dropped, not sent) |
Must NOT show "Reminder sent (1 day before the call)" | critical A "2 hours before" reminder — no "1 day before" reminder. |
| 3 | Confirm the 2-hour reminder is the one that actually fired | The imminent (2h) reminder is present in the send record | Copy: "Reminder sent (2 hours before the call)" · Where: card history | PROBE (api_send_ledger): a reminder row with step=="2h" exists |
Must NOT show "Reminder sent (1 day before the call)" | The single fired "2 hours before" reminder. |
| 4 | Confirm the 3-days-before reminder was also dropped | No 3-days-before reminder fired for the same-day booking | Copy: absence of "3 days before" · Where: card history | PROBE (api_send_ledger): NO reminder row with step=="3d" exists |
Must NOT show "Reminder sent (3 days before the call)" | No "3 days before" reminder. |
| 5 | Open ZZ-S2b Normal Kft (booked several days out) — REGRESSION guard | The normal multi-day booking STILL schedules the day-before reminder | Copy: "Reminders scheduled: … 1 day before … Holnapi megbeszélés" · Where: card history | PROBE (api_history, zz-s2b): the scheduled-reminders touchpoint contains "Holnapi megbeszélés" AND no "Reminder sent (1 day before the call)" has fired yet | Must NOT drop the day-before reminder for a normal (multi-day-out) booking | A "Reminders scheduled" touchpoint listing all three. |
handle_own_booking → _finalize_own_booking → _create_booking_meet_event(intent="reschedule")), so the send-ledger + calendar state are whatever the product produced. Never plant a confirmation row or an event id; ZZ-S3 re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Open the send-ledger after reschedule #1 | A fresh confirmation email was sent for the first reschedule (beyond the original booking) | Copy: confirmation row w/ new time · Where: send-ledger drawer | PROBE (api_send_ledger): ≥2 rows whose category ∈ {booking_confirmation, booking_reschedule} — the reschedule did NOT collide on the original key |
Must NOT skip the email as a "duplicate"; must NOT leak {{time}}/{{meet_link}} |
critical A reschedule-confirmation touchpoint for reschedule #1. |
| 2 | Check the tracked calendar event after reschedule #1 | A single tracked live event; the prior event was deleted | Copy: "áthelyezve" reschedule note · Where: card + owner calendar | PROBE (api_deal): a non-empty gcal_event_id is tracked. OWNER-VERIFIED: that exactly one event exists and the prior was deleted (Google Calendar state — not readable by the walker). |
Must NOT leave the first-rescheduled event lingering | pixel-only A reschedule touchpoint. |
| 3 | Open the send-ledger after reschedule #2 | A second fresh confirmation email was sent for the second reschedule | Copy: confirmation row w/ newest time · Where: send-ledger drawer | PROBE (api_send_ledger): ≥3 rows whose category ∈ {booking_confirmation, booking_reschedule} (original + two fresh reschedule confirmations) |
Must NOT skip the second reschedule's email; must NOT leak {{time}} |
critical A reschedule-confirmation touchpoint for reschedule #2. |
| 4 | Check the card's meeting time + live event count after reschedule #2 | The card shows the new time; exactly one live event remains | Copy: new date-time + "Google Meet" · Where: card | PROBE (api_deal): sales_call_date is a real ISO date (updated to the newest slot). OWNER-VERIFIED: exactly one live event remains (calendar state). |
Must NOT leave the lead on the old time; must NOT leak {{time}} |
A reschedule touchpoint with the newest time. |
| 5 | Confirm the new Meet reaches the lead (delivery) | The fresh confirmation email carries the real Google Meet link (the lead's delivery channel) | Copy: "meet.google.com/…" · Where: confirmation email body | PROBE (api_send_ledger): a confirmation/reschedule row's body contains "meet.google.com". OWNER-VERIFIED: the invite actually lands on the LEAD's own calendar (their calendar can't be read by the walker). | Must NOT ship a reschedule confirmation with no Meet link; must NOT leak {{meet_link}} |
Confirmation touchpoint carrying the Meet link. |
send_ledger to a real FAILED row via the product's send path with a forced send failure (never a hand-written failed row), so the pinned red summary + the bell's failed-send item appear for real. ZZ-S4 re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Open the Pipeline tab | A collapsible 🔔 bell (top-right) holding the failed-send + health items — no full-width red bar across the board | Look: bell icon w/ count · Where: top-right, above the columns | PIXEL-ONLY — .sendfail-board + #healthbanner no longer prepend the board; they live in the panel. |
Must NOT render a red horizontal bar spanning the full page width above the board columns | pixel-only n/a. |
| 2 | Observe the top of the board with the failed send present | A single one-line red summary pinned at top, with an ✕ dismiss icon | Copy: "failed to send" one-liner + ✕ · Look: one line, red · Where: pinned top strip | PROBE (api_send_ledger): a real row with status=="failed" drives the summary |
Must NOT render a multi-line health-alert strip pinned above the tab row | critical n/a — UI surface. |
| 3 | Click the ✕ on the red summary | The Pipeline board with no pinned red summary | Look: summary gone · Where: top strip | PIXEL-ONLY — the pinned summary is dismissed client-side; the bell still holds the item. | Must NOT keep showing "failed to send" at the top after dismiss | pixel-only n/a. |
| 4 | Look at the 🔔 bell after dismissing the summary | The bell still carries the failed-send item (a count badge) | Look: bell w/ count · Where: top-right | PIXEL-ONLY — dismiss clears only the pinned strip, not the panel item. | Must NOT show a notification bell with no count badge | pixel-only n/a. |
| 5 | Open the 🔔 bell and look for health alerts | Health alerts live inside the bell — not on a strip above the tabs | Look: health items in panel · Where: bell panel | PIXEL-ONLY — renderHealthBanner's content moved into the panel. |
Must NOT render a colored alert strip pinned above the tab row | pixel-only n/a. |
/missive/incoming (the product emitter) FROM a MIXED-CASE address (e.g. ZZ-S5@ZZ-Sentinel.Test) whose body contains the marker phrase ZZ-REPLY-BODY, so the .lower() fix on the stored "Lead email" is exercised. Never plant last_msg.row_id or the history row — the product must create them (the fix stamps the created Notion row id onto last_msg.row_id). ZZ-S5 + the marker re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Open the ZZ-S5 Reply Kft card after the inbound reply lands | The incoming reply text (ZZ-REPLY-BODY) shows on the card face | Copy: reply preview on card · Where: ZZ-S5 Reply Kft card | PROBE (api_deal): last_msg.row_id is a non-empty id (the skip_notion reply now carries a stamped row id) |
Must NOT show a card face with no inbound-reply block (reply only as a purple side notification) | critical An "Incoming reply" touchpoint on the card. |
| 2 | Confirm the card is not blank (the purple side-notification is fine) | The card shows the reply block with ZZ-REPLY-BODY — the side notification is additive, not the only signal | Look: reply block on card · Where: ZZ-S5 Reply Kft card | PIXEL-ONLY — the purple side banner may also be present; the card face must carry the reply. | Must NOT show an empty card face for this reply | pixel-only n/a. |
| 3 | Open the card's 🕘 Full history panel | The history panel loads with rows (including the reply) — not "No prior events." | Copy: "Full history" + ZZ-REPLY-BODY row · Where: history panel | PROBE (api_history, zz-s5): returns ≥1 row (the lowercased "Lead email" makes the equals-filter match) | Must NOT render "No prior events." | critical The history list renders for zz-s5@zz-sentinel.test. |
| 4 | Confirm the mixed-case sender resolved (the .lower() fix) |
The inbound reply is present in the history despite the mixed-case From-address | Copy: ZZ-REPLY-BODY inbound row · Where: history panel | PROBE (api_history, zz-s5): at least one row has dir=="in" (the mixed-case reply resolved through the lowercased store) |
Must NOT render "No prior events." for the lowercased address | The inbound reply row resolves. |
| 5 | Confirm prior touchpoints are present too | Earlier touchpoints (the outbound cold email / prior sends) render alongside the reply | Copy: prior outbound row · Where: history panel | PROBE (api_history, zz-s5): at least one row has dir=="out" (prior touchpoints render, not just the reply) |
Must NOT render "No prior events." | The full timeline renders. |
| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Look at the ZZ-S6 Tag Kft card tag in the Booked / Prep column | A compact tag roughly the height of the card's body text — clearly smaller than the old 2× size | Look: ~1rem tag · Where: Booked / Sales-Call-Prep card (crop) | PIXEL-ONLY — .booked-card .flag font-size lowered to ~1rem. |
Must NOT render a flag tag taller than the card's company-name heading | pixel-only n/a. |
.anim-sequence-ring). Product state only; ZZ-S7 re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Look at the ZZ-S7 Dashed Kft card's ring (crop) | A widely-spaced dashed ring with clear gaps between segments | Look: bigger gaps between dashes · Where: scheduled-email card border (crop) | PIXEL-ONLY — wider transparent stops in the conic-gradient; higher --anim-ring-dur (rotation speed owner-verified live). |
Must NOT render a continuous solid ring with no gaps | pixel-only n/a. |
POST /dash/api/fireflies/dispatch) — the ONLY bot-dispatch path observable on staging — and asserts it no longer dispatches a bot, then calls it a SECOND time to prove no second bot. No planted state; ZZ-S8 re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Trigger the manual 🎙 "Send Fireflies to a meeting" action | A response/toast saying native Fireflies join only — no bot dispatched | Copy: "Native Fireflies join only — no bot dispatched" · Where: 🎙 action result | PROBE (POST fireflies/dispatch): the response marks native-only / not dispatched; addToLiveMeeting is never called |
Must NOT report "Fireflies bot elindítva" or dispatch a visible bot | critical No "bot dispatched" touchpoint. |
| 2 | Trigger the manual 🎙 action a SECOND time on the same Meet | Still native-only — a second call does not dispatch a second bot | Copy: native-only again · Where: 🎙 action result | PROBE (POST fireflies/dispatch ×2): both responses mark native-only / not dispatched (no bot on either call) | Must NOT report "Fireflies bot elindítva" on the repeat | No second "bot dispatched" event. |
_meet_autodispatch (fired by a Meet participant-join ping), the Slack #meetings force-start, and the participant-joined "bot" route — none of which is reachable on staging (the Apps-Script Meet ping / Workspace subscription is DORMANT, and no automated agent can join a Google Meet to trigger a join event). The fix neuters dispatch_notetaker at ALL four call sites and keeps the shared single-flight key so even a revived path can't double-fire — but only the manual 🎙 endpoint above is observable in this run. Native-join reliability is 100% owner-side Fireflies + Google-Calendar config, with no API to force it (see INV_fireflies.md): confirm the connected calendar is matyas@clientsflow.hu; set Auto-join = "All meetings with a web-conf link"; keep bot-free Meet capture enabled; check Recording Rules aren't excluding sales Meets; accept the ~5–10 min sync lead-time (admit the notetaker manually for truly last-minute calls). This paragraph is a note, not a walkable step.sales_call_date is a few minutes in the PAST, and which was NOT marked prepped (so it never moved to sales_call). The seed sets the booked call time via the real booking path; it must NOT set sales_call / prep state. ZZ-S9 re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Look at the ZZ-S9 Overlay Kft card just after the call time passed | The purple "Waiting for transcript" overlay on the card, still in the Booked column | Copy: "Waiting for transcript" · Look: pulsing purple overlay · Where: Booked / appt_booked card | PROBE (api_deal): sales_call_date is a real ISO date AND stage_key != "sales_call" (card stayed Booked, prep not marked) |
Must NOT require the card to be moved into the Sales Call column first | critical The card's call-lifecycle shows transcript_pending. |
sequences.define("proposal_chase", …) stores the finalized proposal cover (seq_steps[0]) on the deal — the pending-link token swapped for the real Meet link. It ALSO drives the /review (non-do_calendar) route rebuild, so every stored/sent body is covered. Never hand-edit a body; ZZ-S10 re-created verbatim on reseed.| # | You do | You should see | Element that changes copy · look · where | What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Open the post-call proposal cover body (do_calendar path) | The canonical sign-off ("… ClientsFlow Digital …") is the LAST content; the Meet block appears above it | Copy: "ClientsFlow Digital" close at the end · Where: proposal cover tail (seq_steps[0]) |
PROBE (api_deal): in seq_steps[0].body, index_of("ClientsFlow Digital") > index_of("egyeztet") — Meet block injected BEFORE the sign-off |
Must NOT append the "online egyeztetésünk" Meet block AFTER the signature | critical No new entry — draft. |
| 2 | Scan the proposal cover body for the meet link (do_calendar path) | A real Google Meet link is present; no raw token | Copy: "meet.google.com/…" resolved link · Where: Meet block (seq_steps[0]) |
PROBE (api_deal): seq_steps[0].body contains "meet.google.com" and NOT "{{PENDING_MEET_LINK}}" (finalize swapped the token) |
Must NOT contain the literal {{PENDING_MEET_LINK}} |
critical No new entry — draft. |
| 3 | Confirm the token is stripped across EVERY stored/sent body (route + finalize) | No raw token survives on any proposal step or ledger send record | Copy: no raw token anywhere · Where: all seq_steps + send-ledger rows |
PROBE (api_deal + api_send_ledger): NO seq_steps[*].body and NO ledger frozen_body/sent_body contains "{{PENDING_MEET_LINK}}" |
Must NOT leak {{PENDING_MEET_LINK}} on the /review route or finalize path |
critical No new entry — draft. |