ClientsFlow · Email-System Overhaul · W0 · EBO

EBO — W0 · Observability & Watchdog

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.

What W0 owns
The system's alarms and nothing else: the health banner on the dash, the dedicated Slack alert channels, the external outside-the-app "app is dead" monitor, the ghost-app / cron-stall / auto-send-flag / duplicate-send / spend-cap checks, the ready-to-paste rollback prompt inside every prod-misbehavior alarm, and a log hygiene pass (no lead emails/phones ever printed raw).
What W0 never does
W0 detects and alarms — it never intervenes. It never blocks or delays a send (send gating is W1's), never rolls anything back on its own (Q25: NO auto-rollback — it hands you the exact prompt instead), never throttles AI spend, never touches a lead, a card, or the touchpoint history, and never adds a new always-on service that would break the $30/month total cap.
You do → click/action/hover You should see → on-screen result Element changes → copy · look · where What changes underneath → data/state Must NOT happen → bug guard 🕓 Touchpoint history → impact on the card's history (or —) 💡 UX critique + suggestion → genuine critique + concrete improvement (owner: keep/omit)
Owner rulings folded in (ANSWERS_ROUND3 + W1 comments, which win over the handoff)Q1/Q12: Slack is BACK — watchdog alarms go to in-app banner + dedicated Slack channels ("basically how it was set up"); the handoff's "Slack channel = pending owner decision" is therefore already decided: a dedicated alerts channel gets built, alerts stop sharing the lead-followup channel. Q13=A: ONE external monitor living outside the app fires the single "app is dead" alarm (Slack/email) — the in-app watchdog can't report its own death. Q23: the spend guard watches the TOTAL ≤ $30/month cap (Modal + Gemini together) — wider than the handoff's Gemini-only wording. Q25: prod misbehavior → NO auto-rollback; the alarm carries a notification + the exact prompt to paste into Claude Code to execute the rollback. Q26=B: once W0 is green, later ships go straight to full auto — W0 is what makes that trust rational. W1 comment (2026-07-12): the Q11 "failures list" is dead — a failure means a banner (and, for systemic failures only, Slack); no list UI, no popup queue. Per-send failures stay ring + banner (W1/W8 territory), no Slack for individual sends — Slack is reserved for systemic alarms.
Invariants that hold everywhere — English-only operator UI (banner, Slack messages, everything); alarms never block a click, a send, or a page (detection only); the banner is boring by default — invisible when everything is clean, and it always names the specific problem, never a generic "something's wrong"; every alarm de-duplicates (one Slack message per new problem, not one per polling cycle); no new scheduled job slot is consumed — every check rides inside an already-scheduled run (the platform's 5-schedule cap is full); the external monitor + all checks together stay inside the $30/month total; W0 never writes to a lead, a card, or the touchpoint history.

W0-1 — Clean day: everything healthy → the watchdog is invisible (happy path / baseline)

Who: Mátyás  ·  When: A normal working day. All schedules ran on time, no ghost app, no duplicate sends, spend well under cap, auto-send flag off. The correct behavior of the whole alarm layer on a good day is: you can't tell it exists.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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.

W0-2 — A ghost/decoy app comes back to life → alarm within 3 minutes (failure path — the July incident, never again)

Who: System (a stray deploy revives the old duplicate app) + Mátyás  ·  When: This is the exact 2026-07-06→07-11 incident: a second copy of the app woke up in the wrong workspace and, for 5 days, sent goodbye emails to real leads and hid cards — and nothing noticed, because no alarm watched for it. W0 makes this class of silence impossible.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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.

W0-3 — A background schedule stalls → alarm names exactly which one and for how long (failure path)

Who: System + Mátyás  ·  When: The app runs on 5 background schedules (the fast poll, the 10-minute poll, the CRM sync, the daily sweep, the morning report). Twice already (Jul 6, Jul 8) some of them silently died for days. Today, one of the five — the CRM sync — isn't even watched. W0 watches all five and rings when any goes quiet.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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:

W0-4 — Auto-send master flag ever turns ON in production → alarm within 10 minutes (failure path — the July 8 incident)

Who: System + Mátyás  ·  When: On 2026-07-08 a leaked setting flipped the global auto-send flag on, and sequences emailed real leads with zero review — discovered by luck, not by an alarm. The flag's correct production state is OFF; W0 treats it being ON as a fire.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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.

W0-5 — Monthly spend (Modal + Gemini together) approaches the $30 cap → one heads-up at 80%, one urgent at 100% (guardrail)

Who: System + Mátyás  ·  When: The whole system must run on ≤ $30/month total (Q23 — hosting + AI together, not AI alone). Today spend is tracked as a chart nobody watches and a past hosting-cap breach silently bounced real lead webhooks for hours. W0 turns the cap into an alarm.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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.

W0-6 — Duplicate sends to the same lead → detected within a day, alarm names the lead and the email (failure path / defense in depth)

Who: System + Mátyás  ·  When: The rogue ghost app's most damaging symptom was double-created drafts and repeated emails to the same leads, all week, undetected. Whatever the cause — a second app copy, a retry bug, a stuck schedule — the same email hitting the same lead repeatedly must ring on its own.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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.

W0-7 — The whole app dies → the ONE external, outside-the-app monitor raises the single "app is dead" alarm (failure path / the watchdog's watchdog — Q13)

Who: External monitor + Mátyás  ·  When: Every alarm above lives inside the app — so if the app itself dies (hosting outage, billing cut-off, fatal deploy), they all die with it in perfect silence. That was the July root cause in one sentence: "the watchdog lived inside the thing that died." Q13=A: one independent monitor, living entirely outside the app, whose only job is the single "app is dead" alarm.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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:

W0-8 — Every prod-misbehavior alarm carries the exact paste-prompt for Claude Code; nothing ever rolls back on its own (guardrail — Q25)

Who: Mátyás  ·  When: Any of the alarms above has fired about production misbehaving (ghost app, auto-send armed, a bad deploy's symptoms). His ruling: NO auto-rollback, ever — the system notifies and hands him the exact prompt to paste into Claude Code, and HE decides whether to fire it. This scenario is the shape of that payload, across every alarm.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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:

W0-9 — Logs stay clean: no lead email, phone, or name ever printed raw (guardrail / hygiene — nothing for you to click)

Who: System (verified by the build's audit)  ·  When: Always. The system's internal logs are read by hosting dashboards, debugging sessions, and AI agents — a lead's email address or phone number must never appear in them verbatim. A scrubber exists today but some log lines bypass it; W0 audits and closes every bypass. This scenario has no owner clicks — it is here so the guarantee is signed, not assumed.

#You doYou should see Element that changes
copy · look · where
What changes underneathMust 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.
🕓 Touchpoint-history note for W0:
W0 is a pure observer — no scenario in this document ever creates, edits, or deletes a touchpoint-history entry, a lead, or a card. Every history column above is "—" by design. The one place history appears at all is W0-6, where the duplicate rows the send path already wrote are the evidence the alarm points at. And W0-9 explicitly protects the split: touchpoint history and CRM keep showing real lead data (that's their job); only the system's internal logs get scrubbed.
💡 UX themes across all W0 scenarios:
(1) Boring by default — a healthy day shows nothing anywhere; the first pixel of watchdog UI you see means something is genuinely wrong, so it never trains you to ignore it. (2) Every alarm is specific and consequential — the name of the thing, the numbers, and one plain line of "what this means for you" — never "something's wrong". (3) Alarm once, banner persists, clear by measurement — one Slack message per episode, the banner carries the ongoing state, and nothing clears until the next real check measures healthy. (4) Notify, never intervene — no auto-rollback, no auto-flag-flip, no send-blocking, no throttling; every fix is a human decision armed with a ready paste-prompt. (5) The watchdog can't die with the app — the one external monitor covers the only failure the in-app checks can't: their own death.

Work-item → scenario-step mapping

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 itemWhat it deliversProven by (scenario · step)
WI-1 · Ghost-app probeEvery-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 watchAll 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 sentinelProduction 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 guardMonth-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 scanDaily 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 extensionThe 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 monitorONE 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 payloadsEvery 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 guardSweep 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
❓ Open design decisions I made myself — please comment (keep / change) in this round:
❓ D1 Two Slack channels, not one (W0-1·2): urgent (app dead, ghost, auto-send armed, duplicates) vs heads-up (spend 80%, slow-schedule staleness) — so phone notifications can stay ON for urgent only. One channel instead?
❓ D2 Health-tab pulse line (W0-1·1, W0-7·1): a single "Watchdog: all checks passed · last run 09:42 · external monitor seen 3 min ago" line inside the existing Health tab (in the hamburger — no new surface). Keep?
❓ D3 Staleness line = 3× each schedule's own interval (W0-3·1): fast checks alarm in ~9 minutes, the daily sweep only after ~3 days. Tighter or looser anywhere?
❓ D4 Duplicate threshold = 3+ of the same email type to the same lead in 24h (W0-6): chosen so the legitimate one-reminder-per-day payment chase never false-alarms. OK, or should 2 in 24h already ring?
❓ D5 Hosting spend fallback (W0-5·1): if the hosting bill isn't readable live, the cap guard uses a fixed monthly hosting estimate (stated in the alert) rather than watching AI spend alone. OK?
❓ D6 Dead-app alarm delivery (W0-7·2): Slack AND email from the external monitor, or Slack only? (Q13 says "Slack/email" — both is the safe reading; email survives a Slack outage.)
Sign-off — acceptance oracle. By signing, Sarudi Mátyás locks scenarios W0-1 … W0-9 and the invariants above as the acceptance answer key for the W0 Observability & Watchdog workstream — the rider that ships FIRST, before any other workstream's "shipped" claim is trusted — and records his keep/change verdict on the ❓ open decisions D1–D6. No W0 code is built or deployed before this signature.
Sarudi Mátyás  ✔ Draft · 2026-07-12 · awaiting sign-off