ClientsFlow · Email-System Overhaul · W0 · EBO
DRAFT for owner comments · 2026-07-12. The alarm layer that ships FIRST, as the rider for the whole drop (Q24=A): from the moment W0 is live, a dead schedule, a revived ghost app, a duplicate send, an accidentally armed auto-send flag, spend nearing the $30/month cap — or the whole app dying — each rings a bell on its own, within minutes, with zero polling by you. Assembled from the W0 handoff + ANSWERS_ROUND3 (Q1/Q12/Q13/Q23/Q24/Q25/Q26) + your W1 comment killing the failures list. Scenarios W0-1 … W0-9.
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Mátyás opens the dashboard | The dash looks exactly as it does today — no health banner anywhere, nothing new to read (existing surface, verify unchanged when healthy) | Copy: — · Look: no banner rendered at all when every check is green · Where: top of the dash (the banner's slot stays empty) | Every 3–10 minutes the watchdog checks quietly re-ran in the background: ghost-app probe, all-5-schedule freshness, auto-send flag, spend total, duplicate-send scan — all green, so nothing is shown | Must NOT show a permanent "all systems OK" banner or badge (green noise trains him to ignore the slot); must NOT slow the dash load by even a beat — checks run on the server's own schedule, never on page-open | — (W0 never writes history) | Critique: A fully invisible watchdog leaves no way to confirm it's actually alive without forcing a failure. Suggestion: One small line inside the existing Health tab (already in the hamburger): "Watchdog: all checks passed · last run 09:42". Not a banner, not on the board — just an auditable pulse he can glance at when he wants proof. |
| 2 | Mátyás glances at the dedicated Slack alerts channel | The channel is quiet — days can pass with zero messages; when a message DOES appear, it is always a real systemic alarm worth reading | Copy: — · Look: empty/quiet channel · Where: the new dedicated Slack alerts channel (separate from the lead-followup channel) | Alerts now post to a dedicated channel; the lead-followup channel keeps only lead-followup traffic — the two never mix again | Must NOT post routine "check passed" messages to Slack; must NOT let any systemic alarm keep landing in the lead-followup channel where it drowns | — | Critique: One channel for every alarm type means the rare "app is dead" message looks identical to a "spend at 80%" heads-up. Suggestion: Since the ruling says dedicated channels ("basically how it was set up"): two — one urgent (app dead, ghost app, auto-send armed) and one heads-up (spend 80%, stale low-frequency schedule). Urgent gets his phone notification on; heads-up stays muted. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Nothing — a stray deploy silently revives the decoy app at its old address | Within 3 minutes: a Slack message in the urgent alerts channel — "🚨 Ghost app ALIVE: the old duplicate app at [address] is responding again. It can send real emails to real leads. Paste-prompt to kill it below." — and the dash shows a red health banner with the same message | Copy: banner "🚨 Ghost app detected — the old duplicate app is running again" + a "Details" link · Look: red banner strip at the top of the dash, above everything, on every tab · Where: dash top + urgent Slack channel | The every-3-minutes check probed the known decoy addresses; a decoy answering "I'm healthy" (instead of the expected dead/404) flips the ghost signal, posts one Slack alert, and turns the banner on | Must NOT wait for a human to remember to check the old addresses (the July failure); must NOT auto-stop or auto-rollback anything (Q25 — alarm only); must NOT re-post the same Slack alert every 3 minutes while it stays alive (one alert per episode + the persistent banner) | — (the ghost's own rogue sends would be caught separately by W0-6's duplicate-send scan — defense in depth) | Critique: The watchlist of decoy addresses is fixed — a future stray deploy could revive an app at an address nobody listed, and this check would stay silent. Suggestion: The alarm message should say exactly which addresses ARE being watched, so whenever a new environment is ever created, adding it to the watchlist is an obvious, visible step — not tribal knowledge. |
| 2 | Mátyás (or Claude Code, via the paste-prompt — see W0-8) stops the decoy app | Within one check cycle (≤3 min) the banner disappears on its own and a single all-clear line lands in Slack — "✅ Ghost app back to dead. Watching continues." | Copy: banner removed; one Slack recovery line · Look: dash returns to its normal clean state · Where: dash top + urgent Slack channel | The next probe finds the decoy addresses dead again; the ghost signal clears, the banner un-renders, one recovery message posts | Must NOT leave a stale banner up after the problem is fixed (a banner that lies once is ignored forever); must NOT require a manual "dismiss" click to clear a resolved alarm | — | Critique: After the July incident the real question isn't only "is it dead" but "what did it DO while alive". Suggestion: The recovery message includes how long the ghost was alive ("alive for 14 minutes, 09:02–09:16") so he immediately knows the damage window to check — minutes, not the 5 days of last time. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Nothing — one schedule (say the CRM sync) silently stops running | Once it has been quiet for 3× its normal interval, a Slack alert names it precisely — "⚠️ Schedule stalled: CRM sync last ran 22 minutes ago (normal: every 5). Leads' CRM data is going stale." — and the dash health banner shows the same | Copy: banner "⚠️ Schedule stalled: CRM sync — last ran 22 min ago" · Look: amber banner at the dash top · Where: dash + Slack alerts channel | Every schedule stamps a "last ran" mark when it completes; the 10-minute watchdog compares all five stamps (including the CRM sync, which today is stamped but never checked — that gap closes here) against 3× each one's own interval | Must NOT watch only 4 of the 5 (today's bug); must NOT alert on a single slightly-late run (3× the interval is the line, so a busy cycle never cries wolf); must NOT say a vague "a cron is stale" — always the name + minutes + what it means for him | — | Critique: "CRM sync stalled" states the fact; the July incidents showed the cost is downstream (reminders not sent, board stale) and that's what makes him act fast. Suggestion: Each schedule's alert carries one plain-language consequence line (as drafted above: "leads' CRM data is going stale" / "reminder emails are not going out") — the fact says what broke, the consequence says why to care now. |
| 2 | The schedule recovers (on its own, or after a fix via the paste-prompt) | The banner clears on its own within one watchdog cycle (≤10 min) and one recovery line lands in Slack — "✅ CRM sync running again (was quiet 41 min)." | Copy: banner removed; one Slack recovery line with the total quiet gap · Look: clean dash again · Where: dash + Slack | A fresh "last ran" stamp arrives; the stale signal clears; the alert de-dup marker resets so a FUTURE stall of the same schedule alarms again as a new episode | Must NOT keep alerting every 10 minutes while stalled (one alert per episode, banner persists instead); must NOT stay silent if it stalls AGAIN next week (each episode is new) | — | Critique: None needed — this row is deliberately boring. Suggestion: — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Nothing — a deploy or settings change flips the auto-send master flag ON in production | Within 10 minutes: an urgent Slack alert — "🚨 AUTO-SEND is ON in production. Emails can go out without your review. If this wasn't you, paste the prompt below into Claude Code." — plus the red dash banner | Copy: banner "🚨 Auto-send is ON in production" · Look: red banner, same slot as W0-2 · Where: dash top + urgent Slack channel | The 10-minute watchdog reads the flag's live production value each run; ON → one alert + banner. W0 does not flip the flag back (never intervenes) — it makes the state impossible to miss | Must NOT auto-flip the flag off (maybe one day he DOES mean it — the alarm asks, never decides); must NOT alert repeatedly while it stays on (one alert + persistent banner); must NOT take days to notice (the July 8 failure) | — | Critique: If a future phase legitimately turns auto-send on (Q26's "straight to full auto"), this alarm as written would cry wolf on day one of that phase. Suggestion: Keep the alarm exactly as specced now (ON = fire); when a future workstream legitimizes the flag, THAT workstream must also update this alarm's expected state in the same ship — the EBO for that phase should carry the line "W0-4 expectation flipped" explicitly. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Nothing — normal month, combined spend under 80% of $30 | Nothing. No banner, no Slack line about money, all month | Copy: — · Look: — · Where: — (spend detail stays where it already lives, on the Usage tab) | The 10-minute watchdog sums the month-to-date total (AI spend from the existing usage tracker + the hosting side's best available month-to-date figure) and compares against $30 — under 80%, no signal | Must NOT surface routine spend numbers as alarms; must NOT double-count or miss either half of the total (it's Modal + Gemini TOGETHER, per Q23 — not the AI bill alone) | — | Critique: The hosting side's month-to-date cost may not be readable in real time the way AI spend is — a cap guard that only sees half the bill gives false comfort. Suggestion: If live hosting spend proves unreadable, the guard uses a fixed monthly hosting estimate as its floor and says so in the alert ("incl. ~$X hosting estimate") — an honest approximation beats a precise half-truth. |
| 2 | Nothing — spend crosses 80% of the cap mid-month | One Slack heads-up — "💸 Spend at 82% of the $30/month cap ($24.60 so far, day 19). At this pace: ~$39 by month-end." — and an amber note on the dash banner | Copy: banner "💸 Spend at 82% of the $30 monthly cap" · Look: amber banner · Where: dash + heads-up Slack channel | The 80% threshold-crossing fires exactly once per month per threshold; the marker remembers it fired so later polls past 80% stay silent | Must NOT re-alert every 10 minutes above 80%; must NOT block, throttle, or degrade ANY AI call or hosting function (alert only — enforcement is explicitly out of W0's scope) | — | Critique: "82% on day 19" and "82% on day 28" are opposite situations — one is a blowout, the other is a normal month. Suggestion: The projection line drafted above ("at this pace: ~$39 by month-end") is the single most useful number in this alert — make it mandatory, not decorative. |
| 3 | Nothing — spend crosses 100% of the cap | One urgent Slack alert — "🚨 Monthly spend has passed the $30 cap ($30.40). Hosting may start rejecting work at its own billing limit — watch for missed leads." — and the dash banner turns red | Copy: banner "🚨 Over the $30 monthly cap" · Look: red banner · Where: dash + urgent Slack channel | The 100% crossing fires once; both threshold markers reset automatically on the 1st of the next month | Must NOT switch anything off (the day the hosting platform itself starts rejecting webhooks, W0-3/W0-7 will catch the symptoms — but W0 never pre-emptively cuts service); must NOT forget to reset thresholds at month roll-over (or month 2 gets no alarms) | — | Critique: The scariest cap failure isn't the number — it's the hosting platform silently rejecting real lead webhooks at ITS limit (this exactly happened once, for hours). Suggestion: The over-cap alert explicitly says what to watch for ("new leads may stop appearing — check the board") as drafted, tying the money alarm to the business symptom he'd actually notice. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Nothing — normal day: every lead gets each email type at most the intended number of times | Nothing — the daily scan runs silently and finds no repeats | Copy: — · Look: — · Where: — | Once a day the watchdog scans the sent-email record for the same email type going to the same lead 3+ times inside 24 hours — none found, no signal | Must NOT flag legitimate repetition (the payment-reminder chain legitimately sends one email per day for 3–4 days — a per-day cadence is NOT a duplicate; the scan's line is 3+ of the same type within one 24h window) | — (W0 reads the send record; it never writes it) | Critique: A daily scan means a duplicate storm at 09:00 is caught up to ~24h later — the ghost incident showed damage compounds by the hour. Suggestion: Acceptable for phase 0 (the every-3-min ghost probe in W0-2 catches the most likely cause much sooner) — but note the seam: if a faster send-record check becomes cheap once W1's send pipeline lands, tighten this window then. |
| 2 | Nothing — something sends the same email to the same lead 3+ times in 24h | A Slack alert naming both — "🚨 Duplicate sends: 'Follow-up #2' went to Kovács Béla 4× in the last 24h. Possible second app copy or retry bug — check the lead's history." — plus the red dash banner | Copy: banner "🚨 Duplicate sends detected — [lead] got the same email 4×" · Look: red banner · Where: dash + urgent Slack channel | The scan found the repeat pattern and raised the signal. Detection only — W0 does not retract, pause, or block anything (send gating is W1's territory) | Must NOT block or delay the send pipeline in any way; must NOT report the duplicate anonymously ("duplicates found") — always the lead name + which email + how many times, so he can assess the embarrassment and act | The duplicate rows are already in the lead's touchpoint history (written by the send path) — that history is exactly the evidence the alarm points him at. W0 adds nothing to it. | Critique: Knowing a lead got 4 copies is step one; his next move is damage control on that specific relationship. Suggestion: The Slack alert's lead name links straight to the lead's card (deep link), so alarm → open card → read history → decide on an apology is one continuous motion. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Nothing — the app is up and healthy | Nothing from the external monitor — it pings the app's public health address every few minutes from outside and stays silent while the app answers | Copy: — · Look: — · Where: — (the monitor is an external service, not part of the app's UI) | An independent uptime service (free tier — it must not dent the $30 cap) checks the production app's and the booking app's public health addresses on its own clock, from its own infrastructure | Must NOT run the external check from inside the app or its hosting account (that recreates the "watchdog dies with the app" flaw); must NOT add meaningful cost | — | Critique: An external monitor someone set up once and never sees again is itself a silent-failure risk (expired account, changed URL). Suggestion: The Health tab's watchdog line (W0-1's suggestion) also shows "external monitor: last seen checking X min ago" — the app confirming its own watchdog's pulse, closing the loop from both sides. |
| 2 | Nothing — the app stops answering entirely (outage, billing cut-off, fatal deploy) | One "app is dead" alarm arrives through a channel that does NOT depend on the app: a Slack message (and/or email, per how the monitor is set up) — "🚨 ClientsFlow app is DOWN — not answering since 14:32." No in-app banner, obviously — the app is down | Copy: the external monitor's down-alert, in English, naming which address and since when · Look: Slack/email notification · Where: urgent Slack channel and/or his email — never the dash | The external service's pings started failing; after its confirmation window (a couple of failed pings, so a single network blip doesn't page him) it fires its down-alert once | Must NOT depend on the app, its hosting, or its Slack-posting code to deliver this alarm (the monitor posts via its own integration); must NOT page him on one failed ping; must NOT fire a flood — one down-alert, one later recovery alert | — | Critique: "App is down" at 3am with no next step invites panicked guessing. Suggestion: The monitor's alert text is configurable once — bake the first-response step into it permanently: "Paste into Claude Code: 'The ClientsFlow prod app is down — investigate and restore it.'" The dead-app alarm then carries its own paste-prompt, same pattern as W0-8, even though the app can't help. |
| 3 | The app comes back up | One recovery message from the same external channel — "✅ ClientsFlow app is back UP (down 26 minutes)." — and, back inside the app, the regular watchdog banner may briefly show any collateral it found (e.g. a schedule that missed runs during the outage) | Copy: recovery line with the outage duration · Look: Slack/email; dash back to normal (or showing a truthful amber for missed schedules) · Where: external channel + dash | The external monitor's pings succeed again → its recovery alert; the in-app watchdog resumes on the next cycles and re-evaluates everything honestly (it does not pretend the gap didn't happen) | Must NOT have the in-app watchdog fire a backlog storm of alerts for everything that was stale during the outage — on recovery it evaluates the CURRENT state once and reports what is still wrong now | — | Critique: None — the outage-duration line already gives him the damage window. Suggestion: — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Mátyás reads any prod-misbehavior alarm (Slack message or the banner's "Details") | Below the plain-language description, a ready-to-copy block: "Paste this into Claude Code →" followed by a complete, self-contained prompt — e.g. for a ghost app: "The old duplicate ClientsFlow app at [address] is running again in production and can email real leads. Stop it, verify its health address is dead again, and check whether it sent anything in the last 24 hours." — specific to THIS alarm, naming the actual thing that broke | Copy: one copyable prompt block per alarm, in English, no placeholders left unfilled · Look: a quoted/code block in the Slack message; a copy button in the banner's detail view · Where: inside every systemic alarm from W0-2/3/4/6 (and baked into W0-7's external alert text) | Each check composes its paste-prompt from the live facts it just measured (which app, which schedule, which lead) at alarm time — it is not a generic template | Must NOT execute any rollback, stop, or fix automatically (Q25 — the human fires the prompt, always); must NOT hand him a vague prompt ("something is wrong, investigate") — it names the exact object and the exact desired end-state | — | Critique: A paste-prompt written at build time can rot — six months on, the named recovery steps may reference things that moved. Suggestion: Keep the prompts describing the PROBLEM and the desired END-STATE (as drafted — "stop it, verify dead, check what it sent") rather than step-by-step commands; Claude Code figures out the current steps fresh each time, so the prompt can't rot. |
| 2 | Mátyás pastes the prompt into Claude Code and lets it run | Claude Code performs the fix/rollback; within one watchdog cycle the relevant banner clears itself and the recovery line lands in Slack (per each scenario above) — confirmation comes from the watchdog re-measuring reality, not from anyone declaring success | Copy: the standard recovery lines from W0-2/3/4 · Look: banner clears on its own · Where: dash + Slack | The watchdog keeps polling on its normal schedule; the all-clear only appears when the next real measurement is actually green | Must NOT clear the banner because a fix was attempted — only because the next check measured healthy (the "deploy said success but wasn't" lesson from July 6) | — | Critique: None — measure-to-clear is the whole point. Suggestion: — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history | 💡 UX critique + suggestion |
|---|---|---|---|---|---|---|---|
| 1 | Nothing — the system runs a normal day and writes its internal logs | Nothing changes for you. Behind the scenes: any log line touching a lead shows scrubbed values (e.g. k***@gmail.com), never the raw address, phone, or full name — including the lines that today slip past the scrubber |
Copy: — · Look: — · Where: internal logs only; zero visible UI change | The build sweeps every place the code writes a log line, routes strays through the one scrubbing gate, and adds a regression test: a test lead's email must never appear verbatim in captured log output. A committed audit note records what was found and fixed | Must NOT change what is logged ABOUT events (the events themselves still get recorded fully — only personal identifiers are scrubbed); must NOT scrub the touchpoint history or CRM (those are supposed to show real lead data — this is about internal logs only) | — (touchpoint history is untouched — it correctly shows real lead data; only internal logs are scrubbed) | Critique: A hygiene guarantee with no visible surface tends to silently regress the first time someone adds a new log line. Suggestion: The regression test IS the surface — it runs in the normal test suite forever, so any future code that leaks a raw email into a log turns the suite red before it ships. No new UI needed. |
Each build work item derived for W0, and the scenario steps that prove it works when exercised on staging (failure modes are forced with ZZ-sentinel fixtures on staging only — never against real prod alarms).
| Work item | What it delivers | Proven by (scenario · step) |
|---|---|---|
| WI-1 · Ghost-app probe | Every-3-minutes probe of the known decoy addresses; alive → one urgent Slack alert + red banner; dead again → auto-clear + recovery line with the alive-window duration. | W0-2·1 · W0-2·2 |
| WI-2 · Full cron-staleness watch | All FIVE schedules watched (closing the unwatched-CRM-sync gap), stale = 3× own interval, alert names schedule + minutes + consequence; recovery auto-clears and re-arms per episode. | W0-3·1 · W0-3·2 · W0-1·1 (all-fresh baseline) |
| WI-3 · Auto-send flag sentinel | Production auto-send flag read every 10 minutes; ON → one urgent alert + red banner, never auto-flipped back. | W0-4·1 |
| WI-4 · $30 total-cap spend guard | Month-to-date Modal + Gemini total vs the $30 cap; one alert at the 80% crossing (with pace projection) and one at 100%; thresholds reset monthly; alert-only, never throttles. | W0-5·1 · W0-5·2 · W0-5·3 |
| WI-5 · Duplicate-send scan | Daily scan for the same email type to the same lead 3+ times in 24h; alert names lead + email + count; legitimate daily reminder cadence never flagged; detection only. | W0-6·1 · W0-6·2 |
| WI-6 · Health banner + health-data extension | The dash's health data gains the new signals (ghost, staleness incl. CRM sync, auto-send flag, spend %, duplicates); one banner slot renders red/amber with the specific reason, hidden when clean, cleared only by measurement. | W0-1·1 · W0-2·1/2 · W0-3·1/2 · W0-4·1 · W0-5·2/3 · W0-6·2 · W0-8·2 |
| WI-7 · Dedicated Slack alert channel(s) | Systemic alarms move out of the lead-followup channel into dedicated alert channel(s) (urgent vs heads-up split proposed in W0-1·2's 💡); Slack restored per Q1/Q12 — no pending decision. | W0-1·2 · every Slack line in W0-2…W0-6 |
| WI-8 · External dead-app monitor | ONE independent outside-the-app uptime monitor (free tier, inside the $30 cap) pinging the app's and booking app's public health addresses; single "app is dead" alarm + recovery with outage duration, delivered without depending on the app. | W0-7·1 · W0-7·2 · W0-7·3 |
| WI-9 · Paste-prompt alarm payloads | Every prod-misbehavior alarm composes a specific, self-contained, end-state-phrased Claude Code prompt from live facts; NO auto-rollback anywhere; all-clear only by re-measurement. | W0-8·1 · W0-8·2 · (baked into) W0-2·1 · W0-4·1 · W0-7·2 |
| WI-10 · Log-hygiene audit + regression guard | Sweep of every raw log call site; strays routed through the scrubbing gate; committed audit note + a permanent regression test that fails if a raw lead email ever reappears in log output. | W0-9·1 |