Root Causes & Plan
Ошибка 1: column bot_users_1.patronymic does not exist
Причина: _add_column_if_not_exists() — синхронная функция (def), но conn — AsyncConnection. conn.execute() возвращает корутину, которая никогда не awaits:

max_bot/app/main.py:91: conn.execute(sa_text(...)) — нет await
Лог при запуске: RuntimeWarning: coroutine 'AsyncConnection.execute' was never awaited
Миграции молча не выполняются, колонки не создаются.

План исправления:

main.py:88 — сделать async def _add_column_if_not_exists
main.py:91 — добавить await conn.execute(...)
main.py:75-85 — добавить await перед каждым вызовом _add_column_if_not_exists
После деплоя колонки создадутся при следующем рестарте
Ошибка 2: Тёмная тема в тестовом прогоне
Причина: bot_test.html — это полноценный HTML-документ (не наследует base.html), цвета жёстко закодированы в <style>:

body { background:#1B2838; color:#E8EDF2; ... }
.test-chat { border:1px solid #253248; ... }
Если портал в светлой теме, iframe остаётся тёмным.

План исправления:

Добавить в <head> скрипт инициализации темы (как в base.html:7)
Заменить хардкод на CSS-переменные (var(--bg-card), var(--text-primary), var(--border), var(--accent)) с запасными цветами
Импортировать service-theme.css или определить :root { } блок с reference-переменными
Что затронет:
Файл	Изменение
max_bot/app/main.py:88-102	def → async def, await conn.execute()
max_bot/app/main.py:75-85	Добавить await перед каждым вызовом _add_column_if_not_exists
py_service/app/templates/pages/bot_test.html	Добавить тему + CSS variables


Аудит — все баги и проблемы
🔴 P1 — CRITICAL (крашит бота)
#	Файл	Строка	Проблема
1	main.py	88-102	_add_column_if_not_exists — def вместо async def, нет await — миграции никогда не выполнялись. Все 11 новых колонок не созданы → column patronymic does not exist
2	main.py	75-85	11 вызовов _add_column_if_not_exists без await — корутина возвращается, SQL не отправляется
🟠 P2 — CSS (внешний вид, тема)
#	Файл	Проблема
3	bot_test.html	Полностью standalone HTML — 0 CSS-переменных, все цвета хардкодом. Не наследует тему
4	bot_consent.html	3 хардкод цвета (#155724, #721c24, rgba(0,0,0,0.5)). var(--light-bg) не определён — <th> прозрачные
5	bot_tickets.html	6 хардкод цветов статусов баджей. Тот же var(--light-bg) не определён
6	bot_kb.html	6 хардкод цветов (активные/неактивные, модалка, кнопка удаления). Тот же var(--light-bg) не определён
7	bot_settings.html	1 хардкод rgba(0,150,200,0.1) для фокуса
🟡 P3 — Блокировка event loop
#	Файл	Строка	Проблема
8	inquiry.py	96-103	email_sender.send_ticket_escalation() — синхронный subprocess.run() + файловый I/O внутри async-функции. Блокирует event loop
🟢 P4 — SQL injection (низкий риск, но паттерн опасен)
#	Файл	Строка	Проблема
9	main.py	91	f-string в sa_text() — {table}, {column}, {col_type} интерполируются напрямую. Сейчас вызывается с константами, но если переиспользовать — дыра
План исправления
P1 (2 файла):

main.py:88 — def → async def _add_column_if_not_exists
main.py:91 — conn.execute( → await conn.execute(
main.py:75-85 — добавить await перед каждым _add_column_if_not_exists(
P2 (5 файлов): 4. bot_test.html — скрипт темы (как в base.html:7) + заменить хардкод на var(--*) + <link> на CSS 5. bot_consent.html — заменить хардкод на CSS-переменные + var(--light-bg) → var(--bg-card) 6. bot_tickets.html — статус-баджи на CSS-переменные + var(--light-bg) → var(--bg-card) 7. bot_kb.html — хардкод на CSS-переменные + var(--light-bg) → var(--bg-card) 8. bot_settings.html — rgba(0,150,200,0.1) → var(--accent) с opacity

P3 (1 файл): 9. email_sender.py — subprocess.run обернуть в asyncio.to_thread() или loop.run_in_executor() для неблокирующего вызова

P4 (1 файл, профилактика): 10. main.py:91 — убрать f-string, использовать sa_text() с аргументами через bindparams() или встроить валидацию


Повторный аудит — после исправления P1-P4
Все P1-P4 (миграция, CSS, email, SQL-injection) считаю исправленными. Вот что остаётся:

🔴 CRITICAL — краши/NameError
#	Файл	Строка	Ошибка
C1	main.py	179, 197	BotConversation не импортирован в main.py. Импорт: from app.models import Base (строка 12), без BotConversation. При контакте или dialog_cleared → NameError: name 'BotConversation' is not defined → HTTP 500
C2	main.py	516	VCF N-field regex сломан: r"N(?:;[^:]*)?:([^;\r\n]+)" — останавливается на ;, захватывает только фамилию. Для N:Doe;John;Michael;; вернёт только "Doe", имя и отчество теряются
C3	contact.py	23	Email пишется в user.phone: elif EMAIL_PATTERN.match(text): user.phone = text — должно быть user.email = text
C4	inquiry.py	40	user is None → AttributeError: _finalize_ticket(..., user, ...) → user.first_name упадёт, если BotUser не найден
C5	inquiry.py	53-54	Преждевременный commit: ticket создаётся, BotTicketMessage и BotTicketStatus — нет. Если второй commit упадёт — висячий тикет без описания и статуса
C6	docker-compose.yml	11-15	Секреты в VCS: пароль БД, токен MAX, секрет вебхука — хардкодом. Должны быть ${BOT_DATABASE_URL} c .env
🟠 HIGH — логические, prompt injection
#	Файл	Строка	Ошибка
H1	yandex_gpt.py	65-104	Prompt injection: текст пользователя вставляется напрямую в system_prompt без экранирования или instruction boundary
H2	contact.py	42-50	Дубликат BotMessage: handle_message() уже сохранил сообщение (greeting.py:94-101), а handle_manual_contact() сохраняет то же самое повторно
H3	main.py	476	_extract_phone_from_vcf захватывает только код страны: regex r"\+?\d+" → для +7 (861) 203-33-30 возвращает +7
H4	greeting.py	33-59	Race condition на BotUser: два одновременных bot_started для нового пользователя → оба проходят if not user → второй падает с IntegrityError
H5	email_sender.py	35	Silent error: except Exception: без as e и без logger.error(). Ошибки sendmail глотаются
H6	inquiry.py	88	Блокировка event loop: email_sender.send_ticket_escalation() — синхронный subprocess.run() внутри async-функции
🟡 MEDIUM — состояние, атомарность, TOCTOU
#	Файл	Строка	Ошибка
M1	main.py	452-469	TOCTOU на статус тикета: old_status читается, затем пишется — между ними другой запрос может изменить статус. Audit-log врёт
M2	inquiry.py	14-31,71-77	Двухсессионный дизайн: если _finalize_ticket упадёт между commits, conversation зависнет в "escalated" без тикета
M3	greeting.py	81-105	Stale state: conv.state читается в одной сессии, а используется после закрытия. Callback мог изменить состояние между чтением и маршрутизацией
M4	settings_cache.py	19-22	Double-load race: два одновременных get() оба увидят _loaded == False и оба загрузят кэш
M5	main.py	538	ORG regex обрезает отдел: [^\r\n;]+ не захватывает ;, теряется Engineering Dept из ORG:Acme Corp;Engineering Dept
M6	consent.html, tickets.html, kb.html		var(--light-bg) undefined — CSS-переменная не определена нигде,	прозрачные
M7	bot_test.html	весь	Standalone, 0 CSS-переменных — всегда тёмная тема, не наследует портал
🟢 LOW — мусор
#	Файл	Строка	Ошибка
L1	main.py	482-487	Функция _extract_name_from_vcf никогда не вызывается (dead code)
L2	Несколько файлов		Неиспользуемые импорты: Optional, json
L3	.gitignore		.opencode/ не в gitignore
📊 Покрытие тестами — 15% (21 тест)
Критические пробелы — 0 тестов на:

FastAPI endpoints (webhook, health, users, tickets, KB CRUD, broadcast, settings)
VCF парсинг и handle_contact_share
handle_greeting, handle_message, analyze_and_respond
Обработка ошибок (MAX API down, DB down, GPT unavailable)
Webhook secret validation (401)
Concurrent requests
