---
name: mehant-planning
description: "Механт — новый проект, мини-CRM для автосервисов с голосовым ИИ-вводом. Фаза планирования, кода ещё нет. Все ключевые решения здесь."
metadata: 
  node_type: memory
  type: project
  originSessionId: 883032c2-9dd6-4b50-b864-2fb6bccb7836
---

# Механт — план нового проекта (2026-05-22, переименован с ОкМастер)

**Что это:** CRM для автосервисов в Telegram с голосовым вводом ремонтов через STT+LLM. Расширение в web/Android позже.

**История имени:** Первоначально проект назывался «ОкМастер», но имя оказалось занято — okmasterok.ru (сеть бытовых услуг, 10+ городов), okmaster.ru (ремонт), @okmaster_* в TG. 2026-05-22 переименован в «Механт» — маскот-мастер, ниша IT/CRM по проверке свободна. Главный бот **@mexunt_bot · id 8933316442 · токен в `.env` (gitignored)**.

**Why:** Юзер хочет SaaS-движок «Механт», на котором будут работать боты разных автосервисов, и продвигать сам движок через футер «Powered by Механт» в каждом deploy-е (как Shopify/Tilda). Есть пилот-сервис от знакомого.

**How to apply:** При работе над проектом сверяться с решениями ниже, не пересогласовывать.

## Зафиксированные решения

| Параметр | Решение |
|---|---|
| **Имя** | Механт (gaechkin). Бренд движка отдельно от per-tenant ботов |
| **Тенантность** | Свой бот для каждого автосервиса; multi-bot router в одном процессе aiogram 3 на webhook |
| **Онбординг** | Self-service через `@mexunt_bot` (главный движковый): юзер пишет → BotFather инструкция → присылает токен → автосоздание tenant |
| **БД** | PostgreSQL на mac mini (НЕ SQLite — мульти-тенант + concurrent + long LLM tx) |
| **STT/LLM/Vision** | OmniRoute (`127.0.0.1:20127`) — Whisper, Claude/Gemini extract, Vision для VIN |
| **VIN** | Ручной ввод + фото-распознавание Vision LLM. Опционально декодер из avtero (emex/exist). Платный API не закладываем |
| **Расписание** | Посты × часы (multiple lifts/posts × working hours) |
| **Walk-ins** | Ремонт можно создавать без booking — голосом, на ходу |
| **Роли** | owner / mechanic / client. Owner управляет per-mechanic permissions (can_delete_repair, can_edit_others, can_view_revenue, ...) |
| **Клиент** | Видит свои ремонты + автогенерация PDF-наряда после `repair.status=completed` |
| **PDF** | Шаблон с шапкой автосервиса + футер «Сделано в Механт» (white-label флаг `tenants.hide_engine_branding` под платный тариф позже) |
| **Кастомизация tenant** | Минимум: `branding_json = {name, logo_path, accent_color, address, phone}`. Тексты бота — единые для всех (i18n-overrides не делаем в MVP) |
| **Биллинг** | Отложен, но `tenants.plan` в схеме сразу |
| **Хостинг** | mac mini, launchd `local.gaechkin`, порт `:20150` для health/metrics (только localhost) |
| **Transport** | На старте — Telegram **polling** (multi-bot в одном aiogram Dispatcher). Без публичного домена. Webhook включим позже одним переключателем (`TRANSPORT=webhook`) когда появится URL. Лендинг `mehant.ru` отложен — пока всё через `@mexunt_bot` |
| **Бэкапы** | `pg_dump` + gzip + gpg → `rclone copy gdrive:Gaechkin-Backups/`. Passphrase в `~/.mehant/backup.pass` (mode 0600). Retention 30 дней. Отдельный launchd `local.mehant.backup` |
| **ops_dashboard** | Отдельная карточка |
| **Пилот** | Есть знакомый автосервис для беты |

## Бренд-стратегия «Powered by Механт»

Каждый per-tenant бот показывает футер в:
- `/start` приветствие
- `/about` команде
- PDF-наряде
- Уведомлениях о записи

Отдельная инфра под бренд движка (делаем к концу беты, не блокирует ядро):
- `mehant.ru` — лендинг (Next.js как у zemlya-web)
- `@mexunt_bot` — главный, регистрация tenant'ов + поддержка + демо
- `@mexunt_news` — канал с обновлениями/кейсами

## Архитектура (костяк)

```
launchd local.gaechkin (один процесс)
├─ FastAPI :20150 (REST API + admin, общий для всех тенантов)
├─ aiogram 3 Dispatcher (webhook-роутер по bot.id → tenant_id)
│  ├─ @mexunt_bot (главный движковый)
│  └─ @tenant_xxx_bot, @tenant_yyy_bot, ... (per-service)
└─ Postgres gaechkin

External:
- OmniRoute :20127 (STT, LLM, Vision)
- Cloudflared (webhook routes)
```

## Доменная модель (черновик)

`tenants, users, posts, working_hours, slot_overrides, bookings, cars, clients, client_cars, repairs, repair_works, repair_parts, repair_attachments, voice_logs, mechanic_permissions`

## Фазы (≈12-15 дней)

0. Фундамент (FastAPI+aiogram+Postgres+launchd+cloudflared+healthcheck)
1. Multi-tenant routing
2. Учёт ремонтов вручную (CRUD + история по машине)
3. Голосовой ввод (STT → LLM extract → карточка-превью → save)
4. VIN через фото (Vision LLM)
5. Расписание и слоты (посты×часы)
6. Клиентские заявки (запись, подтверждение, напоминания)
7. Owner-панель и роли + permissions + сводки + экспорт
8. Self-service регистрация tenant'а через @mexunt_bot
9. ops_dashboard + observability

## Что ещё нужно от юзера перед стартом Фазы 0

1. Подтверждение домена `mehant.ru` (или альтернатива) — проверить свободность
2. От пилота: число постов, часы работы, список типичных работ (для LLM-промта), образец заказ-наряда (для PDF), контакт владельца для feedback
3. Кастомизация per-tenant: насколько брендинг настраиваемый (название, логотип, цвет PDF, текстовые шаблоны?)

Связано: [[cebers_pulse_overview]] (паттерн L2-агента для уточняющих вопросов), [[omniroute_cloud_bus]] (STT/LLM), [[avtero_overview]] (VIN-декодер), [[zemlya_web_overview]] (паттерн FastAPI+Next.js+Cloudflared+launchd на mini)

## Спринт «запуск/продажа» (2026-06-10, main = 4e28096)

Деплой теперь через git: `git push mini main:incoming` → на mini `git merge --ff-only incoming` → kickstart (прямой push в main не работает — Apple Git игнорит updateInstead). mini доступен по mDNS `Cebers-mini.local` (IP уплыл .107→.109 по DHCP).

- **Фаза 7 PDF** (`99bff99`): weasyprint-наряд с брендингом tenant'а, автоотправка при закрытии ремонта + кнопка «📄 Наряд PDF». Дамп бэкапа БИНАРНЫЙ (-Fc) — restore только pg_restore.
- **Фаза 8 онбординг** (`4de712b`): /new_service + кнопка в /start engine-бота — название → токен BotFather → getMe → tenant + hot-reload. Токен-сообщение удаляется, лимит 3 tenant'а/владельца, уведомление админам. Happy-path с реальным свежим токеном НЕ прогнан (нужен юзер).
- **Фаза 6 клиентская** (`f1c8aea`): клиентское меню (Записаться/Мой гараж/О сервисе), wizard записи (свободные слоты, телефон request_contact → users.phone), заявка владельцу [✅/🚫], гараж по совпадению телефона (D16: users.phone == cars.owner_phone, последние 10 цифр), PDF своих нарядов, привязка машины через подтверждение владельца. Owner-кнопки ✅/🚫 живьём не кликнуты (живут в чате 1005954706).
- **«📊 Выручка» + ИИ-итоги** (`4e28096`): сегодня/7/30 дней, средний чек, топ-работы, LLM-дайджест (complete_text).
- **Launch.md** в Obsidian: пост @mexunt_news, сценарий демо-видео, прайс Free/Pro 990₽/WL +500₽, чек-лист пилота.
- Открыто: happy-path онбординга (юзер), DHCP-резервация, логотип (промпт выдан: зелёная гамма zemlya #3D6B2A/#0F4D2A, гайка+чат-пузырёк), демо-видео, «второй канал кроме TG» (обсудить после запуска).

## Логотип (2026-06-10, main = 776102d)

Выбран вариант «гайка + светящаяся галочка», Flux dev (NVIDIA NVCF, ключ NVIDIA_API_KEY из .env гаечкина), зелёная гамма zemlya #3D6B2A/#0F4D2A/#7CB342. Ассеты: `~/Desktop/gaechkin_logo/final/` (1024/512/256/64/32 + круговой превью) + в репо `assets/logo.png`. Грабли Flux: слово «avatar» в промпте → CONTENT_FILTERED (чёрный PNG 6428b, finishReason в artifacts[0]); 512px/4 steps на dev-эндпоинте → HTTP 422. Юзеру осталось: BotFather /setuserpic, happy-path /new_service, тап ✅ на тестовой заявке, DHCP-резервация, демо-видео, пост @mexunt_news (всё в Obsidian Launch.md).

## Фаза 9 (Observability) + фикс tz-напоминаний (2026-06-10, main = e3475f1)

Фазы 0–9 ROADMAP закрыты. Коммиты `a1ece26`/`afe372e`/`e3475f1`.
- **Фаза 9**: `/internal/metrics` обогащён (tenants_active, repairs_done_today/week, repairs_open, bookings_today, voice_inputs_today, clients_total; сутки по МСК; OmniRoute cost в БД НЕ логируется → voice_inputs как прокси). `/internal/health/html` — HTML живых метрик (`?auto=N` рефреш). Карточка «Механт» в ops_dashboard на mini (`~/Services/ops_dashboard/dashboard_config.json`, 22-й сервис) через `health_check` http + `health_details_url` — **0 правок в server.py дашборда** (паттерн как у zemlya `/healthz/html`, D17).
- **Фикс напоминаний (реальный баг)**: записи хранят московское настенное время как wall-clock-as-UTC, а `reminder_at` сравнивался с `now(real UTC)` → «за 2 часа» приходило через час ПОСЛЕ визита. Фикс `reminders.py` `_now_wall_as_utc()` (московское настенное «сейчас», помеченное UTC) — та же система координат. 0 миграций. Пер-tenant tz отложен (D-tz). Проверено SQL-предикатом на проде: new_due=t, old_due=f.
- Аудит хендлеров: client.py/onboarding.py крепкие (stale-fallback есть; токен удаляется до валидации; `wizard:cancel` ловится repair.py:316 без is_engine-гарда → работает и в engine). Багов кроме tz не найдено.
- Obsidian обновлён: Changelog (запись сверху + блок «Не сделано»), Decisions D-tz + D17.
- Не проверено headless (нужен юзер): клик owner-кнопок ✅/🚫, happy-path `/new_service` с реальным токеном, вид карточки в web-UI дашборда (auth + мой IP в login-rate-limit). Остаётся бэклог #42 (второй канал) — после запуска.

## Веб-канал /w/<slug> + /export XLSX — бэклог #42 закрыт (2026-06-11, main = b2fbbdc)

Решение юзера: «лёгкое внедрение всюду, кроме WhatsApp» → публичная страница записи + iframe-виджет + QR (D18). Домен — «пока без домена»: PUBLIC_BASE_URL в .env (сейчас 127.0.0.1:20150), при выходе наружу публиковать ТОЛЬКО /w/* (не /internal/*).
- Миграция 0005: tenants.public_slug + bookings.source ('tg'/'web'). Страница /w/{slug} (vanilla HTML/JS mobile-first, брендинг из tenant.branding), /slots, /book; антиспам IP rate-limit + honeypot (фейковый ok) + 409 slot_taken. Время по конвенции D-tz (wall-clock-as-UTC). Заявка → карточка «🌐 Запись с сайта» владельцу через bots_manager, кнопки те же cbk:ok/rej.
- Кнопка «🔗 Онлайн-запись» (+ /weblink) в меню владельца: lazy slug (translit), QR через segno, embed-код. «📤 Выгрузка»/(/export): XLSX openpyxl, 4 листа (Машины/Ремонты/Записи/Клиенты). Deps добавлены: openpyxl, segno (стоят в venv mini).
- e2e полный: TG (QR+ссылка+XLSX 12KB через tg-web-e2e, admin-bypass тест-аккаунта), curl API (заявка/409/honeypot), Playwright UI (формa→успех, скриншоты вычитаны). Заявка #36 «Браузер Тест» оставлена в БД — юзер тапнет ✅ на карточке в своём чате.
- Открыто: домен и публикация наружу; web-кабинет владельца отвергнут (D18) — вернуться при спросе.

## Двигатель возврата: статус→оценка→отзыв→ТО (2026-06-11, main = 342a072)

После deep-research (рынок плотный, инкумбенты имеют VIN+склад+Telegram-мост; дифференциатор Механта = Telegram-native + петли возврата, D19) построена цепочка после ремонта:
- Миграция 0006: reviews (rating 1-5 + comment) + service_reminders (ТО, due_at wall-clock-as-UTC, client_tg_id денормализован). branding.review_links — JSON.
- Статус клиенту: кнопки на карточке (🔧 В работе/⏳ Жду деталь/✅ Готова) → services/clients.resolve_client_for_car (телефон-матч D16). Воронка оценок при закрытии: ⭐1-5, 4-5→ссылки площадок (URL-кнопки), 1-3→текст→приватно владельцу. Callback-префикс `rvw:` (НЕ `rev:` — это revenue!). ТО-напоминание +6 мес → reminders.py _process_service_reminders шлёт «пора на ТО» с веб-ссылкой. «⭐ Отзывы» в меню владельца: сводка (кол-во/средняя) + настройка ссылок (парсинг Я.Карты/2ГИС/Авито по домену).
- e2e на проде (ремонт #59, клиент=тест-аккаунт): статус пришёл, 5★ записана+владелец уведомлён+ремонт completed, ТО-воркер sent=true (после фикса 342a072: get_settings не был импортирован в reminders.py — ронял ТО-воркер), настройка ссылок сохраняет в branding, парсер мультиссылок ок. Тестовые данные вычищены. Ветка 1-3★ отдельно не гонялась (код прямолинейный).
- e2e-грабли TG-web: эмодзи-звёзды НЕ матчатся has_text (кликать по индексу .nth); два callback-клика по одной клавиатуре подряд теряются (разделять/свежая карточка); send() с \n шлёт два сообщения (многострочную вставку реальный юзер шлёт одним).
- Стратегия продажи (из deep-research): не продавать сейчас (0 платящих=символическая цена); набрать 5-30 платящих+биллинг Robokassa→ARR-якорь. 152-ФЗ: Механт=оператор ПДн, уведомить РКН до обработки (штраф 100-300к с 30.05.2025). Полная справка в сессии.

## B2B-партнёрка + 152-ФЗ (2026-06-11, main = 62a02d9)

«Лояльность» юзер выбрал как B2B-рефералку МЕЖДУ сервисами (НЕ клиентскую, D20): привёл СТО → тот стал Pro → рефереру +990 баллов на свою подписку. Списание баллов на подписку — заглушка до биллинга (юзер: биллинг потом через **Platega**).
- Миграция 0007: tenants.referral_code (unique, ленивый) + referred_by_tenant_id + referral_points. services/referral.py: код/ссылка t.me/gaechkin_bot?start=ref_, stats(invited/paid/points), grant_pro_and_credit(+990). /start ref_<code>→FSM→referred_by при онбординге+уведомление реферера. «🎁 Пригласить сервис» в engine /start (ссылка+статистика). admin /grantpro <bot|id> → Pro + начисление (когда Platega — тот же хук из вебхука оплаты).
- 152-ФЗ (D21): Механт=обработчик, СТО=оператор. GET /privacy (политика), согласие на веб-записи, /privacy в ботах. РУЧНОЙ шаг юзера: уведомить РКН (rkn.gov.ru) — не автоматизируется; формулировки на юриста.
- Проверено: реф-сервис на боевой БД (код/ссылка/счётчики/+990/→Pro/cleanup), engine-дашборд (врем. swap владельца tenant1→тест→вернул), /grantpro usage, /privacy=200, согласие на /w/<slug>. Не гонялось: deep-link через реальный онбординг (регекс ок), списание (ждёт Platega).
- TODO когда Platega: подписки/оплата + списание referral_points на подписку + вебхук оплаты дёргает grant_pro_and_credit.

## Прайс-лист + витрина (2026-06-11, main = f05d38e)

Из трёх выбранных фич (Мастера/Прайс/Витрина) сделаны две; Мастера+роли — отдельно (изоляция данных между мастерами = security-поверхность, строить с тестами изоляции).
- Прайс-лист: миграция 0008 work_catalog (title/price_kopecks/hours/sort), WorkCatalogRepository. «🧾 Прайс» в меню владельца (список/добавить FSM/удалить). При «➕ Работа» непустой прайс → пикер услуг кнопками, wcat:pick добавляет работу одним тапом, wcat:manual → ручной ввод. catalog.py роутер.
- Витрина: /w/<slug> теперь мини-сайт — услуги+цены из прайса + отзывы 4-5★ (с comment) из reviews + форма записи. Грабли: select не был импортирован в web.py → 500 (фикс f05d38e).
- Проверено: витрина 200 + скриншот вычитан, бот «🧾 Прайс» + пикер + тап добавляет работу. Сид вычищен.

## Мастера + роли — все 3 фичи закрыты (2026-06-22, main = 9a66689)

Третья из выбранных фич; единственная с security-поверхностью. См. [[obsidian]] Decisions **D22**.
- Миграция 0009: `tenants.mechanic_invite_code` (unique). Изоляция в ОДНОЙ точке: `services/roles.py` — `resolve_actor`→(role,user_id)|None + `can_edit_repair` (owner: всё; mechanic: только свой `repair.mechanic_user_id`; client/None: нет). Все repair-scoped callback'и в repair.py через `_guard_repair_cb`.
- `services/mechanics.py`: коды-приглашения (ротация), список+счётчик открытых, `promote_to_mechanic`, `deactivate_mechanic` (открытые→в пул), `assign_mechanic` (проверяет что назначаемый — активный мастер tenant'а), `list_my_repairs`. Приглашение = ссылка в бот САМОГО сервиса `t.me/<tenant_bot>?start=mech_<code>` (НЕ engine).
- Мастер создаёт ремонт → авто-назначение на себя. Меню мастера минимально: Новый ремонт / Мои ремонты / О платформе (ни машин, ни выручки). start.py: deep-link mech_ + роле-зависимое /start меню. Выручка: блок «По мастерам». Карточка ремонта: «👷 Мастер» → пикер (owner).
- Проверено: **изоляция — скрипт против боевой БД** (mechB≠mechA, assign не-мастера отклонён, снятый→resolve=None+ремонт в пул, promote create+reactivate); **owner-side реальный TG** (Мастера/ссылка/ротация кода/Мои-ремонты-отбой/назначение Ивана #57/выручка «Иван 1/5000»). Сид вычищен, repair 57 снова свободен.
- **НЕ проверено в реальном TG:** mechanic-side UI — единственный e2e-аккаунт 8688355181 ∈ admin → всегда owner (admin-override). Логика мастера покрыта DB-скриптом.
- TODO когда Platega: списание referral_points; мастер-side TG e2e нужен второй не-admin аккаунт.

## Веб-кабинет: вкладки Мастера + Выручка (2026-06-25)

Часть [[kodoceh_web_overview]] (фронт `apps/gaechkin`). Достроены 2 вкладки поверх бот-логики `9a66689` без дублирования.
- API `gaechkin/api/cabinet.py`: `GET /api/cabinet/mechanics` (инвайт код+ссылка + список с числом открытых ремонтов, реюз `services/mechanics.py`), `POST .../mechanics/invite/regenerate`, `POST .../mechanics/{id}/deactivate`, `GET /api/cabinet/revenue?period=today|week|month` (выручка/средний/закрыто/в работе + топ-работ + по-мастерам; `_repair_revenue_kop` = зеркало `bot/handlers/revenue.py`). Всё tenant-scoped по owner_tg_id.
- Фронт `apps/gaechkin/app/cabinet/page.tsx`: `MechanicsView` (copy-ссылка+сброс кода, список с «Снять») + `RevenueView` (period-чипы, 4 KPI, бары по мастерам, топ-работ). Акцент `#D8902E`.
- **Проверено реально:** authorized curl (кованая `gk_session`, owner 1005954706) → revenue/month=2/8000₽/4000₽/«Без мастера ·2» совпало; Playwright-скриншоты обеих вкладок ✓; build зелёный; edge `mexunt.codoceh.ru/cabinet`=200. Деплой: scp на mini → import-check `~/.venvs/gaechkin/bin/python` → `launchctl kickstart -k gui/$(id -u)/local.gaechkin`; фронт build Node 22 + рестарт :20411.
- **Дальше по кабинету:** Витрина (`/w/<slug>` настройка) + админка; вход без TG (multi-identity, owner_tg_id→account_id) — после обкатки на Автерое.

## Кабинет: запись/перенос/расписание мастеров + QR витрины (2026-06-27, Gaechkin `c1c9399`, фронт kodoceh-web `8b750a8`, D23)
Закрыты product-гэпы кабинета (запрос «продолжай всё остальное»). **Где что:** backend Механта — `gaechkin/api/cabinet.py` (+миграция `alembic/versions/0010_booking_mechanic.py`); фронт — `apps/gaechkin/app/cabinet/page.tsx` в репо **kodoceh-web** (фронты kodoceh-web живут ТОЛЬКО на mini).
- **`bookings.mechanic_user_id`** (nullable FK, миграция 0010) — записи крепятся к мастеру. Эндпоинты: `POST /api/cabinet/bookings` (создать, опц. мастер), `POST .../{id}/reschedule` (перенос+переназначение), `GET /api/cabinet/mechanics/schedule?date=` (расписание дня по мастерам + `unassigned`).
- **QR витрины:** `qrcode` (npm, в воркспейсе apps/gaechkin), `toDataURL`→`<img>`+`<a download>`, оффлайн; кодирует `v.url` (прод `PUBLIC_BASE_URL=https://mexunt.codoceh.ru`).
- **Время записи:** хранится как москов. настенное, помеченное UTC (как `web_booking.wall_to_stored`); фронт форматирует время записи в UTC (`fmtBkTime`), иначе +3ч-сдвиг у владельца в МСК.
- **Гочи деплоя (важно cross-session):** прод-БД Механта = **PostgreSQL** (`postgresql+asyncpg://gaechkin:gaechkin@127.0.0.1:5432/gaechkin`), миграции = alembic на Postgres, бэкап `pg_dump` (`/opt/homebrew/opt/postgresql@16/bin`). Mini-чекаут Механта **впереди** локального MacBook-чекаута → править/коммитить на mini. Автеро демо-отчёт (гость до входа) = чистый фронт в `apps/avtero`, бэкенд не трогали.
- **Проверено реально:** backend E2E минтом cab-сессии (create 201 + round-trip времени, reschedule, schedule, негативы 400/403); Playwright Механт 10/10 + Автеро 8/8 (desktop+mobile, скриншоты вычитаны); прод 20411/20413=200, edge=200. Связано: [[kodoceh_web_overview]], [[kodoceh_web_build_gotcha]].
