The n8n-style Lab node redesign + reviewer rebuild, shipped on an isolated link. Clone, check out v3, run the tests, and pick up the two open fronts. v1 and v2 stay frozen — never deploy over them.
A Modal-hosted sales-prep tool. Its Lab turns a prospect's URL into a scrape (logo, brand palette, images, copy), SEO keyword research + Google-Ads volumes + current rankings, a brand-new website (one-shot Gemini 3.1 Pro from a proven template), competitor/copy research, a multi-page sitemap, a Figma asset bundle, and a Notion CRM write-up.
A second surface — the Reviewer — serves the generated site to the client via a tokenized link, collects pinned comments, and lets the rep apply AI section-edits and ship new versions. Hungarian UI, English code.
Studio runs as three fully isolated deployments that share nothing but the R2 bucket (namespaced by key prefix). This is a hard invariant — a startup guard aborts the app if the URL and the R2 prefix disagree.
| Version | Branch | Live link | Modal dashboard | Status |
|---|---|---|---|---|
| v1 | main |
studio-web | app ↗ | frozen |
| v2 | v2 |
studio-v2-web | app ↗ | frozen |
| v3 | v3 |
studio-v3-web | app ↗ | active — your work |
Each app redirects / →
/login and serves the dash after the ADMIN_PASSWORD
login (Matyás gives you the password + env + Modal access separately). The
v1/v2 links let you A/B against frozen behaviour while iterating on v3.
@modal.asgi_app()), server-rendered Jinja + vanilla JS — no Node build step.web function (max_containers=1) touches it.app/config.py..spawn() chains + an HMAC callback.browser_image workers only.app/db.py; they report to /internal/jobs/{id}/complete.app/core/ is pure, offline-testable logic; app/workers/ wraps it in the job harness.Built from one live test session (Dani × Matyás, 2026-06-15) plus Matyás's explicit asks, executed end-to-end in 10 phases. Full test suite: 124 passed / 1 skipped; deployed; all three apps healthy & isolated; the two-zone node UI verified responsive at 1280 / 768 / 390.
Every step is a node with an editable inputs zone (URL/mode + the prompt itself, save once/default/ named, + overrides/uploads) and an outputs zone (every artifact, download + copy). Horizontal/vertical layout; single-node re-run.
Per-card error boundary (a
throw can't wipe sibling cards), no-op skip, focus/scroll/<details>
preserved across the poll, responsive grid.
Logo cascade (JSON-LD → apple-touch-icon →
<svg> → <img>), color-probe palette,
consent-dismiss + lazy-force, manual override layer, sandboxed AI scraper.
Named AdsTokenExpiredError
(no silent empties), 1 QPS spacing, soft DataForSEO 404, Ads volume-screenshot
upload edge.
Intent clustering + a hard
≥3-page floor; SERP API competitors; GENERATE feeds Ads screenshots
(multimodal), thinking_level=low, finish-reason handling,
traceability manifest.
Preview-version gate + accept (no version explosion), durable comment re-anchoring + forward-carry, per-version client links, global comments, and the realtime-editor backend.
Because v3 is a separate app + volume + R2 prefix + branch, the nuclear rollback is trivial: just stop routing to v3 — v1 and v2 stay live and untouched no matter what you do on v3.
# A) redeploy the last-known-good v3 commit (e.g. one phase back)
git checkout v3 && git reset --hard d0c0f09 # = end of Phase 8
python3 -m modal deploy modal_app.py
# B) drop ONE phase, keep the rest (keeps history)
git revert 5b39b86 # undo Phase 6, then redeploy
# C) back to the pre-v3 baseline (v2's code on the v3 app)
git checkout v3 && git reset --hard 3cf097b
python3 -m modal deploy modal_app.py
backup-2026-06-11 on
GitHub + local DB/R2 snapshots under backups/ (see CLAUDE.md).
Tip: git tag v3-good-YYYYMMDD before risky work.git clone https://github.com/matt-clientsflow/clientsflow-studio.git
cd clientsflow-studio
git checkout v3
python3 -m pytest -q # expect: 124 passed, 1 skipped
python3 -m modal serve modal_app.py # local hot-reload (needs Modal auth + env)
python3 -m modal deploy modal_app.py # deploy v3 — ONLY from the v3 branch
~/.claude/.env
(Gemini, R2, ADMIN_PASSWORD, DataForSEO, Google Ads, …). Never print, commit
or paste secret values. Matyás hands you the env + Modal access directly.The backend is shipped & tested:
POST /projects/{id}/pages/{slug}/preview-edit takes edited
HTML, runs the same rewrite_for_viewer the client sees, stamps
nested data-section-ids, and returns a sanitized
srcdoc. To build: the CodeMirror 6 pane (debounced POST →
iframe) + a Sanity-style inspect overlay (click a section → jump to its
source). Rep editor iframe same-origin; client viewer SAMEORIGIN.
Deploy v3, log in, run two real audits end-to-end (e.g.
nyitrakert.hu, beridoor.hu) — once continuously,
then re-run single nodes. Screenshot inputs+outputs at 1280/768/390, hunt
visual defects, then exercise the reviewer (comment → apply → re-anchor →
accept → per-version client link). Iterate until clean. Chrome DevTools MCP
only, never headless. Clean up test data under v3/.
main) or v2. Work on v3.app/config.py; light theme only.old==new → the UPDATE is skipped. Build a fresh dict (helpers already do).db._migrate() does idempotent ALTER … ADD COLUMN; v3 added the preview/anchor/share-token columns.request.form() yields starlette UploadFile, not FastAPI's..tmp/blobs) ≠ prod store (R2); tests use LocalStorage.AdsTokenExpiredError.PROJECT_STATE.md, the 2026-06-16 CHANGELOG.md entry,
HANDOFF.md (the full text behind this page) and CLAUDE.md.