Human overview · for understanding

Live-Test Bug Fixes — Tech Spec Overview

The 8 hand-found funnel bugs + the two pipeline columns + the label rename, turned into an executable red→green checklist. · 2026-06-22

The 8 hand-found funnel bugs + the two pipeline columns + the label rename, turned into an executable red→green checklist.

Master summary — the gist in 30 seconds

TL;DRMátyás + Gergely hand-tested the LIVE funnel and found 8 bugs the automated vision-QA missed — because they're non-events (a stage that didn't move, a sequence that didn't clear, a row that wasn't logged, a label misread). This spec traced each to its exact line, grouped them into 11 locality clusters, and wrote a browser-verifiable checklist (14 check-IDs) plus the QA stubs that prove each fix.

Inputs: the founders' bug report + the live codebase. Outputs: checklist.md (14 boxes) + 2 ADRs + 14 QA stubs in board-card-qa + the cold-start prompt for the builder. The two ⚠️ blockers are BUG-01 (6 duplicate 'Send' clicks → API spam, no loading state) and BUG-06 (card doesn't move stage after Send and after Sign).

Why this mattersThe deepest finding: three of the four hardest bugs were NOT broken backend logic — the stage writes were already correct. The card 'didn't move' because two different stages were collapsed into one board column, and because there's no client auto-reload. Tracing first (D1–D4) stopped us from 'fixing' code that already worked and pointed at the real seam: the board projection + a cache-bust convention.
flowchart LR
  A[Founders<br/>live test] --> B[8 bugs +<br/>FR-01/02]
  B --> C{Code trace<br/>D1-D4}
  C --> D[11 locality<br/>clusters]
  D --> E[checklist.md<br/>14 check-IDs]
  E --> F[board-card-qa<br/>14 stubs]
  E --> G[2 ADRs]
  F --> H[Instance 3<br/>red to green]
  G --> H

1 · FR-01 column remap — the shared root of BUG-06

TL;DRTwo new board columns split out of the existing projection: 'Proposal Signed / Fizetés FUP' (signed-unpaid, with an overdue red border) and 'Payment Arrived / Adatbekérő FUP' (deposit paid). A pure remap — NO new GHL stages.

Inputs: stage_key contract_signed / deposit_paid (already canonical GHL stages). Outputs: two new UI columns. The map lives in FOUR places that must change in lockstep — _STAGE_TO_COL (twice, board_view.py + dash.py), _COL_TO_STAGE, and the JS STAGES/MOVE_STAGES arrays.

Why it mattersBUG-06's 'signing produces no visible move' was never a missing stage write — it was contract_signed and proposal_sent both pointing at the SAME column. Splitting the column is the fix. The known duplication of _STAGE_TO_COL is the trap: miss one copy and a card silently vanishes.
flowchart TB
  S1[proposal_sent] --> C1[Sign FUP]
  S2[contract_signed] -->|was| C1
  S2 -->|now| C2[Proposal Signed<br/>Fizetes FUP]
  S3[deposit_paid] -->|was| C3[Ongoing build]
  S3 -->|now| C4[Payment Arrived<br/>Adatbekero FUP]
  C2 -.overdue >3d.-> R((red border))

2 · BUG-06 — the stage moves were correct; the SURFACING was missing

TL;DRSend → proposal_sent (flows.py:2653), Sign → contract_signed (flows.py:766), Pay → deposit_paid (flows.py:647) all already fire. The card looked stuck because of the collapsed column (Cluster 1) + the 120s board cache with no client auto-reload.

Inputs: a correct backend stage write. Outputs: a visible card move. The fix is a CONVENTION: every board-visible transition emits push_dash_event + busts the WEB-container board cache, and the frontend calls loadBoard(true) on success. No client-side auto-poll (that's a hard invariant).

Why it mattersThis is why D1 mattered: had we 'fixed' the send path we'd have changed working code and still seen a stuck card. The real seam is the gap between a correct write and a stale cached read.
flowchart LR
  W[stage write<br/>_sync_stage] --> E[push_dash_event]
  W --> X[bust board cache]
  X --> R[loadBoard true<br/>fresh=1]
  E --> R
  R --> V((card moves<br/>no F5))

3 · BUG-01/05 — the blocking loading state (⚠️ blocker)

TL;DR'Send & schedule emails' had no disable-on-click and the arm endpoint re-stamped + re-logged on every call → 6 clicks = 6 arms. Fix: disable + spinner on click (front) AND make arm idempotent (back).

Inputs: N rapid clicks. Outputs: exactly 1 arm + 1 log row. Reuse the existing generate-button disable pattern (dash_js.py:1271-1279) for the arm/route/send buttons; make api_seq_arm a no-op if already armed.

Why it mattersBelt AND braces: the front-end guard stops the spam, the server-side idempotency makes it correct even if a click slips through. Critically, the guard must NOT auto-send — it only collapses duplicate arming. The human-send gate stays sacred.
flowchart LR
  C[6 clicks] --> B{button<br/>disabled?}
  B -->|yes| N[5 no-ops]
  B -->|1st| A[arm POST]
  A --> I{already<br/>armed?}
  I -->|yes| K[ok, no re-stamp]
  I -->|no| S[stamp once<br/>1 log row]

4 · BUG-07 — a self-sent email faking a lead reply (⚠️ cascade)

TL;DRThe outgoing proposal email, re-ingested by Missive, was classified as an 'Incoming reply' — which could cancel the chase sequence AND resurface the lead to New Lead. This is automation-breaking, not cosmetic.

Inputs: a re-delivered self-sent mail. Outputs: dropped as outbound, no reply row, sequence intact. Guard in handle_missive_incoming BEFORE classify: if the newest from-addr _is_ours → drop and return.

Why it mattersD2 confirmed the cascade is real (flows.py:1309 cancel + 1289/1325 resurface). The guard goes at the impure boundary where _is_ours is available — not inside the pure router. Logging the send at send-time also fixes the wrong-order timestamp.
flowchart TB
  M[self-sent mail<br/>re-ingested] --> G{from is ours?}
  G -->|yes| D[drop: outbound<br/>no reply row]
  G -->|no| C[classify +<br/>route_inbound]
  C -.bug.-> X[cancel seq +<br/>resurface lead]
  D --> OK((sequence +<br/>stage intact))

5 · BUG-03 — a booking that vanishes when Google Meet fails

TL;DRhandle_own_booking creates the Meet first; if that fails it early-returns 'calendar_failed' BEFORE logging the booking. So a GCal blip = no touchpoint, no stage move, nothing to recover.

Inputs: a booking + a failing Meet API. Outputs: the booking still logged + an ok-with-warning toast. Restructure the failure branch to log internally + sync stage + tell the rep to send a link by hand.

Why it mattersGraceful degrade over all-or-nothing: a calendar outage shouldn't erase a real customer booking. The happy path already logs 'Call booked' (flows.py:4060) — we just stop throwing the booking away on the unhappy path.
flowchart LR
  B[book ZZ lead] --> M{Meet created?}
  M -->|yes| L1[log Call booked<br/>+ appt_booked]
  M -->|no, was| Z[early-return<br/>nothing logged]
  M -->|no, now| L2[STILL log +<br/>warn toast]

6 · BUG-02 / BUG-04 / BUG-08 / FR-02 — the smaller, sharp fixes

TL;DRBUG-02: manual actions must tear the sequence down (clear the dashed border). BUG-04: the wizard never set price_after_expiry, so the strike-through never rendered. BUG-08: a measured board-perf trim. FR-02: rename 'Megbízó dátum' → 'Aláírás dátuma'.

Each is a one-seam fix: call sequences.stop in manual paths, set price_after_expiry = price × 1.25, baseline-then-trim the worst board offender, one approved copy edit.

Why it mattersBUG-04 is the clearest 'trace beats guess' win: the pricing block was fully implemented — it just had nothing to render because one field was never populated upstream. BUG-08 is the only one with no pre-set budget, so it's red→green on a baseline you capture first.
mindmap
  root((4 sharp fixes))
    BUG-02
      manual action<br/>clears dashed border
    BUG-04
      set price_after_expiry<br/>strike-through renders
    BUG-08
      baseline then<br/>trim worst offender
    FR-02
      Megbizo datum to<br/>Alairas datuma

7 · How it gets built — red→green→refactor per cluster

TL;DRInstance 3 flips each of the 14 QA stubs to a real assert, watches it FAIL red against the live build, makes the cluster's edits, deploys once, watches it go GREEN, refactors, ticks. Never tick on reasoning alone.

Inputs: checklist.md + 14 board-card-qa stubs. Outputs: 14 green boxes + a Run log. Everything runs ZZ-only, send-blocklisted, AUTOSEND off — the 8 real deals are never touched.

Why it mattersA check that's green before you change anything proves nothing. Seeing it red first is what makes the green trustworthy. The QA harness is the same one that proved the 53-box board build — extended, not reinvented.
flowchart LR
  ST[stub] --> WR[write assert]
  WR --> RED{fails red?}
  RED -->|no| WR
  RED -->|yes| ED[edit cluster]
  ED --> DP[deploy once]
  DP --> GR{PASS?}
  GR -->|no| ED
  GR -->|yes| RF[refactor<br/>stay green]
  RF --> TK((tick + log))
checklist.md (the worklist) →PROMPT_to_execute.md (cold-start for Instance 3) →ADR-0001 — FR-01 column remap →ADR-0002 — stage-surfacing + seq-teardown →HANDOFF.md (Instance 1 bundle) →