v0-german: CJM и генератор документов для воронки продаж

Когда продукт живёт в голове у основателя, а инструменты для работы с клиентами — это разрозненные Google Docs, таблицы и WhatsApp-переписка, рано или поздно приходит момент «надо это всё собрать в одно место». Именно с этой точки начался проект v0-german — внутренний инструмент для школы Германа Юна, который в итоге вырос в полноценную платформу с CJM-модулем, AI-генерацией документов и автоматизированной воронкой продаж.
Расскажу, как мы шли от брейншторма к работающему продакшн-решению, через какие грабли прошли и что в итоге получилось.
Контекст: что такое v0-german и зачем он нужен
Бизнес Германа Юна — это образовательная программа для предпринимателей. Воронка выглядит примерно так: потенциальный клиент получает лид-магниты, проходит «три пайра», затем попадает на диагностику с менеджером, и только после этого ему предлагают войти в программу. Мягкий вход — от 50 тысяч рублей, дальше идут тарифы серьёзнее.
Диагностика — ключевой этап. Это живой звонок, где менеджер погружается в бизнес клиента: отдел продаж, дебиторка, найм, обучение, финансовый учёт. По итогам нужно подготовить три документа:
- Пост-диагностический отчёт — «Точка А» (текущая ситуация) + «Ключевые выводы»
- Коммерческое предложение — с описанием программы и тарифами (250К / 750К / 1М)
- Дорожная карта — персонализированный план по месяцам
Раньше всё это делалось вручную: менеджер слушал запись звонка, сам писал отчёт, сам собирал КП в Google Docs. Долго, неконсистентно, зависит от конкретного человека. Задача — автоматизировать.
Архитектура: что уже было и что надо добавить
Когда мы начали работу, в проекте уже существовала неплохая база: транскрибация аудио через Whisper, PDF-экспорт, интеграция с OpenAI, Supabase как база данных. Плюс CJM-модуль (Customer Journey Map) — визуальный конструктор воронки продаж.
Проблема была в том, что CJM работал с багами. Неправильные отображения, кривая логика каскадных расчётов, потенциальные дыры в безопасности. При этом сформулировать «что именно не так» было сложно — проблемы были размазаны по всему модулю.
Для генератора документов решили делать новую страницу /generator — пошаговый визард из 4 шагов:
- Шаг 1 — данные клиента (имя, бизнес, локация, дата встречи)
- Шаг 2 — загрузка аудио или вставка текста транскрипции
- Шаг 3 — выбор тарифа и параметров
- Шаг 4 — генерация и скачивание трёх PDF
Выбор PDF-движка
Один из ключевых технических вопросов — чем генерировать PDF. Рассматривали три варианта:
| Подход | Плюсы | Минусы |
|--------|-------|--------|
| @react-pdf/renderer | Настоящие векторные PDF, кастомные шрифты, лёгкий (~5MB), работает в API routes | Свой синтаксис, не Tailwind, layout надо писать заново |
| Puppeteer | Pixel-perfect из HTML, Tailwind работает | Тяжёлый (~400MB Chromium), медленный старт, проблемы в Docker Alpine |
| window.print() | Простейший, уже работает | Нет контроля над версткой, плохо для сложных документов |
Остановились на @react-pdf/renderer — он даёт нужный контроль над типографикой без overhead от Chromium. Брендинг берём с germanyun.online: бордовый #9b2c2c, золотой акцент #c9a959, тёплый белый фон, шрифты Inter + Noto Serif JP.
Аудит CJM: 40 проблем на 50 компонентах
Параллельно с разработкой генератора провели полный аудит CJM-модуля. Результат оказался неприятным — около 40 проблем в 50+ компонентах и 15 API-роутах.
Проблемы разбили по приоритету:
CRITICAL — потеря данных и безопасность:
- SQL Injection через string interpolation в запросах к Supabase (UUID'ы вставлялись напрямую в
.not("id", "in", ...)без параметризации) - Edge weights не сохранялись при записи в БД — поле просто не передавалось
- Race condition при одновременном сохранении нескольких изменений
HIGH — логические баги:
- Каскадный движок (
funnel-engine.ts) не обрабатывал weight=0 как валидное значение - Нормализация весов ломалась при определённых комбинациях
leads_manualлогика работала некорректно при определённых условияхimport/confirmвозвращал 200 даже при ошибках вместо правильных HTTP-кодов
MEDIUM — отображение и UX:
isLeadsCalculatedфлаг устанавливался некорректно- Dead code в нескольких компонентах мешал пониманию логики
- Plan pro-rate в дашборде считался неверно
- Валидация числовых диапазонов отсутствовала как на клиенте, так и на сервере
Стабилизация: план и выполнение
По результатам аудита написали план в docs/plans/2026-03-10-cjm-stabilization.md — 16 задач (14 фиксов + 1 скип + 1 верификация через сборку).
Чтобы не терять время на последовательное выполнение, разбили задачи по файлам и запустили параллельно четыре агента:
- Agent 1: Tasks 3, 8, 11, 14 —
cjm-context.tsx+funnel-engine.ts - Agent 2: Tasks 5, 9, 10 —
metrics/route.ts+table-view.tsx - Agent 3: Tasks 6, 7 —
dashboard-view.tsx - Agent 4: Tasks 2, 4, 13, 15 —
api.ts,import/confirm,what-if-panel,stages/route
Вот как выглядело исправление SQL-уязвимости в роуте stages — до и после:
// БЫЛО: небезопасная string interpolation
const { data } = await supabase
.from('stages')
.select('*')
.not('id', 'in', `(${stageIds.join(',')})`)
// СТАЛО: параметризованный запрос
const { data } = await supabase
.from('stages')
.select('*')
.not('id', 'in', stageIds)Исправление каскадного движка для weight=0:
// БЫЛО: weight=0 трактовался как falsy, связь игнорировалась
if (edge.weight) {
totalWeight += edge.weight
}
// СТАЛО: явная проверка на undefined/null
if (edge.weight !== undefined && edge.weight !== null) {
totalWeight += edge.weight
}Fix race condition при сохранении через простой lock:
const saveRef = useRef(false)
const handleSave = async () => {
if (saveRef.current) return // блокируем повторный вызов
saveRef.current = true
try {
await saveData()
} finally {
saveRef.current = false
}
}После всех исправлений — build-верификация:
npm run build
# ✓ 41/41 pages compiled successfullyTypeScript-ошибки, которые выявил tsc, оказались в основном pre-existing issues с типизацией React Flow, не связанными с нашими изменениями. Build прошёл чисто.
Генератор документов: AI в воронке продаж
Параллельно со стабилизацией CJM шла разработка генератора. Здесь ключевую роль играют промпты для GPT-4o.
Для пост-диагностического отчёта промпт задаёт строгую структуру:
- Блок «Точка А» — подробное описание по подразделам (Отдел продаж, Дебиторка, Найм, Учёт)
- Блок «Ключевые выводы» — корневые причины, ограничения роста, особенности мышления собственника
Важный момент в промпт-инжиниринге — задать минимальные требования к детализации. Без этого AI склонен давать короткие ответы:
Требования к выводам:
- Минимум 5 симптомов проблем
- Минимум 3 текущих ограничения
- Минимум 5 метрик успеха
- Максимум 7 симптомов (не перегружать)
Для слайдов и КП каждый слайд имеет свой промпт с конкретными требованиями. Например, для slide-03 (анализ симптомов) комбинируем три источника:
// Объединяем симптомы + ограничения + корневые проблемы
const items = [
...diagnosticData.symptoms,
...diagnosticData.current_constraints,
...diagnosticData.core_problems
].slice(0, 7) // не больше 7 для визуального fitОтдельно пришлось поработать с консистентностью данных между слайдами. Проблема была в том, что каждый слайд генерировался независимо, и AI мог дать разные формулировки одного и того же факта на разных слайдах. Решение — textContent поле в structuredData, которое синхронизирует ключевые тезисы.
Git-история: итерации и детали
Часть работы отлично видна в git-истории. За последнюю неделю — плотная итерация по слайдам презентации:
slide-01: фиксированный маркетинговый заголовок «Системный бизнес без ручного управления»slide-02: headline «Точка А: итоги диагностики» + синхронизация diagnostic contentslide-03: lead text изfinancial_risk / mainTaskInsight, поддержка до 7 itemsslide-04: передача массива рисков + summary в textContentslide-05: синхронизация textContent со structuredData, расширение источников для Point B
Отдельный коммит — логика тарифа:
feat: add noTariff checkbox, hide tariff slide + fix case page skip
feat: move tariff selection to step 1 (client form)
Тарифный выбор перенесли на первый шаг (форма клиента) — так менеджер сразу видит контекст при заполнении остальных данных. Добавили noTariff чекбокс для случаев, когда на диагностике тарифный разговор ещё не шёл.
Также важный фикс:
fix: include all DB cases in presentation, not just GPT-ranked ones
GPT ранжировал кейсы из базы по релевантности, но в итоговую презентацию попадали только топ-N. Это приводило к ситуации, когда менеджер знал про кейс, а в презентации его не было. Теперь все кейсы из БД включаются, GPT только определяет порядок.
Результат
После всех работ:
- 13 из 14 фиксов применены (1 задача была скипом по плану)
- Build 41/41 страниц — чистая сборка
- Деплой через Dokploy — автоматически по пушу в main
- germanyun.online/cjm отвечает 200, всё живое
Результаты по категориям фиксов:
| Категория | Что починено | |-----------|-------------| | Data integrity | Edge weights в типах, cascade engine (weight=0 + нормализация), import confirm (корректные HTTP-коды), leads_manual логика | | Validation | Type='plan'/'fact' check, числовые диапазоны (server + client) | | Logic | Dead code удалён, plan pro-rate в дашборде, save race condition lock | | Security | SQL injection через параметризованные запросы | | Display | isLeadsCalculated флаг, корректное отображение в дашборде |
Выводы
Первый урок, который этот проект подтвердил в очередной раз: «доработать до идеала» и «написать с нуля» — это разные задачи, и важно не перепутать их в начале. Когда пришла идея интегрировать AFFiNE (мощный open-source workspace), соблазн был велик. Но анализ показал: AFFiNE — general-purpose инструмент, он не умеет воронки продаж, каскадные расчёты, план/факт. Пришлось бы всё писать внутри чужой платформы с нуля. Правильное решение — чинить то, что есть, но делать это системно.
Второй урок — аудит перед рефакторингом. Сорок проблем в пятидесяти компонентах — это не «а вот тут что-то не работает». Это карта территории. Без этой карты ты тратишь время на симптомы, а не причины. Потратить несколько часов на полный аудит перед тем, как трогать код — не потеря времени, а экономия.
Третий урок касается промпт-инжиниринга для бизнес-документов. AI без ограничений даёт минимально приемлемый результат, а не максимально полезный. Задать минимум 5 симптомов, минимум 3 ограничения, максимум 7 пунктов для визуального fit — это не мелочи. Это разница между документом, который менеджер отправит клиенту, и документом, который он переделает вручную.
Четвёртый урок — про консистентность данных в multi-step генерации. Когда каждый слайд или секция генерируется отдельным вызовом к AI, неизбежно возникает drift: одни и те же факты описываются по-разному, цифры расходятся, формулировки противоречат друг другу. Решение через textContent/structuredData синхронизацию — рабочее, но требует дисциплины при проектировании API ответов от AI. Лучше закладывать это в архитектуру сразу, а не добавлять потом через серию hotfix-коммитов.
Ну и про параллельное выполнение задач: разбить 13 независимых фиксов на 4 параллельных агента по принципу «один файл — один агент» — это работает. Конфликты при мерже минимальны, если границы ответственности чёткие. Главное — убедиться, что агенты действительно не трогают одни и те же файлы.
Полезные ссылки

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.
Связанный проект
CJM Designer →