Architecture · ClientsFlow Pipeline

The God-Module Split — Modules & Seams

How two giant files became many small ones without changing a single behaviour · live build v098 · 2026-06-24

One-sentence summary. We took the two biggest, most-tangled files in the system — the 4,780-line "engine" and the 3,098-line "control panel" — and cut them into clearly-labelled smaller pieces, while keeping the exact same "front doors" so every other part of the app (and every safety test) still works identically. Nothing the business does changed; the code is just far easier and safer to work on now.

In plain language

Imagine a workshop where one enormous binder held the instructions for everything: taking sales calls, booking meetings, sending invoices, replying to emails, drawing the proposal documents — all jammed into the same 4,780 pages. To change one thing you had to leaf through the whole binder, and one slip could affect anything. That's a "god-module."

We split that binder into labelled chapter-books — one for emails, one for money, one for calls, one for the documents — and did the same for the control-panel binder. Two things made this safe:

We kept the same front desk. Everyone who used to "ask the binder" still asks the exact same desk and gets the exact same answer — they never learn a chapter moved. ② The chapters keep an internal phone line back to the shared services (logging, saving, sending) so they work exactly as before, and our automated safety checks still pass untouched. Those two clean joins are what engineers call "seams."

The map — engine & control panel

Each core file kept its name and its "front door." The coloured boxes are the new chapter-files the work was lifted into. Read the two arrow styles with the key below each diagram.

Callers modal_app · webhooks crons · dashboard they call flows.X() flows.py core helpers + FRONT DOOR store · log · send · sync · ensure_ghl re-exports every moved name 2,053 lines was 4,780 lines one door flows_forms.py proposal / review HTML renderers 9 functions · 332 lines · pure presentation flows_billing.py Stripe · DocuSeal · Wise · Számlázz 17 functions · 603 lines · the money path flows_inbound.py Missive + Fireflies webhooks & sweeps 19 functions · 965 lines · email in flows_calls.py calendar · Meet · booking · no-show · audio 26 functions · 924 lines · the call layer live shims call shared helpers back into core
solid = front door / re-export (callers + core point one way) dashed = live shim (a chapter phones a shared service back)

The engine: flows.py 4,780 → 2,053 lines; four domain chapters lifted out behind one unchanged front door.

dash.py Control Panel + route registry 2,417 lines was 3,098 · still wires every /dash route dash_styles.py the whole look (CSS), 462 lines re-injected at render — byte-identical page dash_actions.py post-call wizard orchestration, 264 lines 8 endpoints · still wired via dash.register()

The control panel: dash.py 3,098 → 2,417 lines; the styling and the heaviest wizard lifted into siblings, same rendered page.

Module anatomy — what's inside each box 71 + 8 functions

Every function below was lifted out byte-for-byte and grouped by job. These are the real names in the code today — nothing was rewritten, only relocated. (Leading _ = internal helper.)

flows_forms.py

9 functions · 332 lines · pure presentation, no side-effects
Page renderers
render_proposal_pagerender_proposal_review_pagerender_proposal_previewrender_review_form
Building blocks
_version_select_prompt_versions_compose_appendix_esc_fmt_huf

flows_billing.py

17 functions · 603 lines · the money path
Stripe
handle_stripe_event_process_stripe_sessionstripe_backup_pollensure_deposit_stripe_link
DocuSeal
ensure_docuseal_submissionhandle_docuseal_event_docuseal_dealemail_signed_document
Wise · invoices
_parse_wisehandle_wise_paymenttrigger_invoice_prepconfirm_invoice_senttrigger_balance_payment
Payment chase
trigger_payment_sequencepayment_reminder_steps_payment_due_amount_stop_payment_sequence_on_paid

flows_inbound.py

19 functions · 965 lines · email/transcript in
Sender classifiers
_is_ours_is_cold_inbox_is_lead_inbox_is_personal_inbox_is_auto_reply_is_automated_is_paid_client_deal_should_auto_goodbye_external_party
Missive
handle_missive_incoming_persist_first_reply_metamissive_backup_pollmissive_reply_sweepwarmup_archive_sweep
Fireflies
handle_fireflieshandle_fireflies_summarized_ff_get_dealfireflies_backup_pollprocess_pasted_transcript

flows_calls.py

26 functions · 924 lines · the call layer
Calendar · Meet
gcal_watchschedule_meeting_create_meet_or_fallback
Booking
handle_own_bookinghandle_ghl_eventcancel_booking_booking_supersedes_prior_state_ghl_appt_fields_fmt_mmdd_hhmm
No-show / unreachable loops
noshow_definestart_noshow_loopnoshow_loop_sweepunreachable_define_noshow_steps_unreachable_steps
Call audio · post-call log
ingest_call_audiosave_call_transcriptlog_callprocess_call_logset_followupfollowup_draftmark_preppedhad_a_call_poll+3 more

dash_styles.py

1 constant · 462 lines · the Control Panel's look
Exports
CSS
A single string holding the whole stylesheet, re-injected into the page shell at render via .replace("/*CSS*/", CSS) — proven byte-identical to the original inline block.

dash_actions.py

8 functions · 264 lines · post-call wizard commit
Wizard steps
api_postcall_previewapi_postcall_step3api_postcall_routeapi_postcall_finalize
Support
_postcall_persist_formapi_proposal_docapi_postcall_docuseal_archive_meet_block

What a "seam" actually is 2 kinds

A seam is a clean join where two pieces meet without leaking into each other. The split rests on exactly two.

① The front door  re-export facade
Plain: every other part of the app still talks to flows.py / dash.py by name. Those files now quietly forward each name to whichever chapter holds it — so no caller had to change a single line.
Technical: the core re-exports moved names at its tail (from app.flows_x import …), so flows.handle_missive_incoming still resolves even though the body lives in flows_inbound.py.
② The internal phone line  live shims
Plain: each chapter still needs shared services (saving, logging, sending). It reaches them through a tiny "call-back" line, so behaviour is identical and our safety tests that swap in fakes still work.
Technical: sub-modules call def log(*a,**k): return _f.log(*a,**k) — resolved at call-time, so test monkeypatches (flows.log = fake) and live state both propagate. Moved bodies stay byte-identical.

The interfaces — the contracts at each seam 3 surfaces

"Module" is what each file holds; "interface" is how they're allowed to touch. The split defines exactly three contracts — and every one of them is what keeps behaviour identical.

① The public surface — what the front door re-exports

Each chapter's functions become callable as flows.<name> again through a re-export list at the bottom of flows.py. That list is the public interface — 71 names across four chapters — so no outside caller can tell anything moved.

flows.<name> 71 re-exported names · one public surface forms · 9render_proposal_page… billing · 17handle_stripe_event… inbound · 19handle_missive_incoming… calls · 26schedule_meeting…

The chapters feed names up into one front door; callers only ever see flows.<name>.

② The shared-helper contract — what each chapter calls back

Going the other way, each chapter depends on a small set of core services through live shims. This matrix is that contract: the rows are the shared helpers in flows.py, the columns are the chapters that call them. A change to one of these helpers ripples to every ✓ — which is exactly why they stayed in the core rather than being duplicated.

Core helper (stays in flows.py)formsbillinginboundcallsdoes
store()read/write the deal store
_c()·get an API client
log()·activity-log a touchpoint
stamp()·timestamp a deal field
_sync_stage()·move a deal's pipeline stage
_send_missive_email()··send an email (human-gated)
ensure_ghl()··ensure the GHL contact exists
lead_booking_link()··build the booking URL
_deal_from_crm()··hydrate a deal from the CRM
check_token() · _text_to_html()···auth + email HTML for renderers
_create_booking_meet_event() · create_sales_prep_task()···kept in core (also patched by tests)

Abbreviated to the shared helpers; each chapter also forwards a few of its own. The full per-file shim lists live in each module's header docstring.

③ The wiring contract — how the dashboard composes itself

The Control Panel assembles at startup through one entry point. dash.register(api) builds the two security guards, then hands the same (api, guard, owner_guard) contract to every panel; the extracted wizard rides in via the re-export so its routes register unchanged.

dash.register(api) called once at boot build guards guard · owner_guard (Personal) panel.register(api,…) metrics · services · finance · journey @api.post("/dash/api/postcall/*") route wrappers → re-exported dash_actions.*

One contract — (api, guard, owner_guard) — composes every panel; the moved wizard registers through it unchanged.

Before & after, by the numbers 2 god-modules → 8 files

FileWhat lives thereLinesRole
flows.pyShared helpers + the front door2,053 (was 4,780)core
flows_forms.pyProposal / review HTML renderers332chapter
flows_billing.pyStripe · DocuSeal · Wise · Számlázz · payment chases603chapter
flows_inbound.pyMissive + Fireflies webhooks & sweeps965chapter
flows_calls.pyCalendar · Meet · booking · no-show · call audio · post-call924chapter
dash.pyControl Panel shell + route registry2,417 (was 3,098)core
dash_styles.pyThe Control Panel's entire CSS look462chapter
dash_actions.pyPost-call wizard's multi-step commit logic264chapter

Why it's safe — the proof

Same behaviour, demonstrated
531 automated tests green at every single step.
• Function bodies moved byte-for-byte (a script verified nothing was lost or duplicated).
• The live /dash page serves CSS that is byte-identical to the source (42,469 chars matched).
Verified on the real system
• Deployed as build v098 and re-checked live.
• Board-card QA A5 · C4 · D5 · G1 passed — layout intact and the human-approval gate (nothing sends without a person) untouched.
• Decisions recorded in ADR-0010.
What did not change: every button, email, invoice, calendar event, proposal document and approval step behaves exactly as before. This was pure reorganisation — the value is faster, safer future changes, not a new feature. The work is committed locally on main and has not been pushed to the shared remote yet.