Files
iistwin/IMPLEMENTATION_LOG.md
Ильяс Султанов 87fe0f03f5 chore(compose): ресурсные лимиты для всех сервисов
Шаг 0.4 плана production-готовности (подтверждён пользователем):
- mem_limit/cpus/pids_limit по docker stats прода + запас
- все 4 compose-файла + серверный бэкап .bak.serverfix
- config валиден везде
2026-09-08 10:01:23 +03:00

51 KiB
Raw Blame History

IMPLEMENTATION_LOG — доведение iistwin до коммерческой готовности

Контекст для любого ИИ-агента / разработчика

Проект: iistwin (TaskTitan) — CRM/таск-трекер. Стек: React 18 + TypeScript + Vite + Wouter + TanStack Query + shadcn/ui (клиент client/src), Express + Drizzle ORM + PostgreSQL с мультитенантностью через organizationId (сервер server/), JWT (access + refresh), SSE real-time. Миграции — детерминированный SQL-раннер в migrations/ (scripts/migrate-docker.js, НЕ drizzle-kit). Деплой: push в main → GitHub Actions deploy.yml → Docker на VPS 82.202.130.22 (прод: https://iistwin.ru). Финансы — субапп /di2 (client-di2/ + server/finance-di2/).

Инвариант (жёсткое требование владельца): механика мгновенного глобального поиска — на главной (client/src/components/HomeGlobalSearch.tsx), в реестре задач (TaskRegistry.tsx) и в канбане (TaskKanban.tsx) — НЕ должна измениться ни в поведении, ни в скорости ощущений. API GET /api/tasks?search=&limit= и подстроковая семантика поиска (ilike '%…%' находит совпадения внутри слов, «омент» → «моментально») сохраняются. Ускорение поиска — только через pg_trgm GIN-индексы, НЕ через tsvector (он меняет семантику).

Источники: этот лог ведётся по плану «план улучшения до продакт.md» (аудит production-готовности) и исполнительному плану Kimi Code. Нумерация шагов ниже соответствует исходному плану (Фаза 0: 0.0–0.15, Фаза 1: 1.1–1.10, Фаза 2: 2.1–2.5). Дополнительный шаг A3 (подключение тестов) вынесен вперёд из 1.6.

Принятые решения:

  • Биллинг (0.13): только абстракция PaymentProvider + MockPaymentProvider; реальный эквайринг — позже, отдельным этапом.
  • Шаг 0.14: серверные кастомизации — в отдельный приватный инфра-репозиторий (основной репозиторий остаётся как есть).

Как читать файл: записи идут в порядке выполнения (не обязательно совпадает с нумерацией). Каждая запись — по шаблону: статус, зачем, что изменено, ключевые файлы, коммит, как проверялось, влияние на поиск/UX, подводные камни. Ничего не менять без записи в этот лог.


[0.0] Обновить локальный репозиторий

  • Статус: ✅ done
  • Зачем: рабочая копия должна соответствовать актуальному main на GitHub.
  • Что изменено: — (только проверка)
  • Как проверялось: git fetch origin + git status (чисто, синхронизировано с origin/main, HEAD a2b231d), npm run check — зелёный (оба tsc: основной и client-di2).
  • Влияние на поиск/UX: нет.

[0.15] Сквозная документация изменений (этот файл)

  • Статус: ✅ done (коммит 76a0d85 «chore(docs): add implementation log»)
  • Зачем: по одному файлу любой ИИ-агент понимает, что сделано, зачем, какие файлы тронуты и как проверялось.
  • Что изменено: создан IMPLEMENTATION_LOG.md в корне репозитория (шапка-контекст + шаблон записей).
  • Влияние на поиск/UX: нет.

[A3 / 1.6 частично] Подключение тестов + починка 5 падающих

  • Статус: ✅ done
  • Зачем: тесты существовали (3 файла, vitest), но не запускались ни локально (нет скрипта), ни в CI; 5 из 45 падали. Без зелёных тестов дальнейшие изменения безопасности/производительности — вслепую.
  • Что изменено:
    • package.json — скрипт "test": "vitest run".
    • .github/workflows/pr-check.yml — шаг «Tests» (npm test) после type check.
    • server/utils/userActivity.ts — фикс реального бага: trackUserActivity мог кинуть синхронно внутри authenticateToken, исключение попадало в catch аутентификации → ложный 403 Недействительный токен на первом запросе пользователя за минуту. Тело обёрнуто в try/catch, throttle сбрасывается для повторной попытки (соответствие контракту fire-and-forget).
    • tests/auto-transitions.test.ts — 3 устаревших теста обновлены под намеренное текущее поведение (forward-only по position; синк assignee в task_assignees; системное сообщение заменено аудит-записью «Автопереход»), добавлен мок delegation.service.
  • Как проверялось: npx vitest run — 45/45 зелёные; npm run check — чисто.
  • Влияние на поиск/UX: нет.
  • Подводные камни: баг в userActivity.ts проявлялся только на первом аутентифицированном запросе в минуту (throttle) — в проде мог давать sporadic 403.

[0.7] Обработчик ошибок без утечки деталей

  • Статус: ✅ done
  • Зачем: клиент не должен видеть SQL и внутренности БД в 5xx-ответах (аудит, Фаза 0).
  • Что изменено:
    • server/index.ts — глобальный error handler: при status ≥ 500 отдаёт «Внутренняя ошибка сервера (код XXXXXXXX)», оригинальная ошибка — в серверный лог с тем же кодом инцидента. 4xx не тронуты.
    • Route-уровень (12 файлов): зачищены все 5xx-ответы с err.message/String(err)/телом апстрима — auth.users.routes.ts, form-offline.routes.ts, sync.routes.ts, polls.routes.ts, reactions.routes.ts, mcp-rag.routes.ts, llm-providers.routes.ts, external.routes.ts, task-crud-write.routes.ts, task-fields.routes.ts, finance-di2/routes.ts, documents/worker/server.ts. Где не было — добавлен console.error с контекстом.
  • Как проверялось: повторный grep status(5xx) + err.message по server/ — 0 совпадений; npm run check чисто; npx vitest run 45/45.
  • Влияние на поиск/UX: нет (только тексты 5xx-ответов).
  • Подводные камни: остались ответы 200 + success:false с err?.message (llm-providers, rag, finance-di2) и 4xx с err.message (documents, data-tables) — сознательно вне скоупа шага, кандидаты для фазы 2.

[0.8] Убрать passwordHash из выдачи пользователей

  • Статус: ✅ done
  • Зачем: хэши паролей и токены не должны уходить клиенту (в т.ч. через offline-sync).
  • Что изменено:
    • shared/schema.ts — тип SafeUser (User без passwordHash/verificationToken/resetPasswordToken/resetPasswordExpires) + набор колонок safeUserColumns (единая точка правды).
    • server/storage/users.storage.ts — getUsersByOrganization и listUsers → SafeUser[].
    • server/storage/social.storage.ts (searchUsers), server/storage/task-meta.storage.ts (getUsersByRole, getUsersByOrgRoleId), server/documents/data-resolution.service.ts (getUser для шаблонов) — тоже на safeUserColumns.
    • Сигнатуры в server/storage.ts; типовые правки у caller'ов (логика не менялась). Методы аутентификации (где passwordHash нужен) не тронуты.
    • Побочный эффект: ctx.users.list() в автоматизациях и MCP list_users тоже sanitized.
  • Как проверялось: ни один из 43 caller'ов не использовал исключённые поля (tsc); sync (initial + delta) теперь отдаёт пользователей без хэшей; npm run check чисто; npx vitest run 45/45; grep passwordHash по server/ — только auth-флоу.
  • Влияние на поиск/UX: нет.
  • Подводные камни: при явном списке колонок drizzle возвращает плоские строки даже с join — мёртвый маппинг в listUsers убран, на это опираться нельзя в будущих правках.

[0.11] Миграция индексов (0079_performance_indexes.sql)

  • Статус: ✅ done
  • Зачем: убрать Seq Scan на горячих путях — поиск задач, дельта-синк, история полей, логин по email.
  • Что изменено: migrations/0079_performance_indexes.sql — CREATE EXTENSION IF NOT EXISTS pg_trgm; GIN (gin_trgm_ops) на tasks.title и tasks.description; (organization_id, updated_at DESC) и (form_id, updated_at) на tasks; field_history(task_id); users(email). Все с IF NOT EXISTS, без CONCURRENTLY (раннер в транзакции).
  • Как проверялось: миграция применена на scratch-контейнере postgres:16-alpine с 100k синтетических задач — все 7 операторов чисто. EXPLAIN ANALYZE поиска ilike '%омент%' (совпадение внутри слова — инвариант): полный проход 77.5 мс (Seq Scan) → 4.8 мс (Bitmap Index Scan по trgm + BitmapAnd с org-индексом), ускорение ~16×; с LIMIT 50 — 10.8 мс → 9.7 мс. Отдельно проверено: CREATE EXTENSION pg_trgm выполняется не-суперпользователем-владельцем БД (trusted extension, PG 13+) — на проде миграция пройдёт под appuser.
  • Влияние на поиск/UX: семантика НЕ меняется (тот же ilike, тот же API) — только скорость.
  • Подводные камни: на проде построение GIN-индексов без CONCURRENTLY кратковременно блокирует запись в tasks — при текущих объёмах (десятки тысяч задач) это секунды, приемлемо.

[0.5] Чистый production-образ (без devDependencies)

  • Статус: ✅ done (коммиты a54ec77 (Dockerfile), 9fc6ad1, 5426c70 (фиксы запуска))
  • Зачем: в прод-образ не должны попадать typescript/vite/vitest — поверхность атаки и размер.
  • Что изменено:
    • Dockerfile — production-стадия: npm ci --omit=dev вместо копирования node_modules из builder.
    • package.json — vitest/supertest/@types/* (16 пакетов) перенесены в devDependencies (именно vitest в dependencies тянул vite/esbuild/tsx в прод-образ).
    • server/vite.ts — vite и vite.config импортируются динамически (dev-only), vite.config — по вычисляемому URL.
  • Как проверялось: локальная сборка образа; бандл стартует без devDeps (проверка до подключения БД); grep — 0 статических импортов vite в dist/index.js.
  • Влияние на поиск/UX: нет.
  • Инцидент и подводные камни (важно для будущих агентов):
    1. Правки Dockerfile ушли в main досрочно (попали в коммит 0.11 через git add -A) — автодеплой поднял их до завершения smoke-теста. Правило: коммитить явным списком файлов, не git add -A.
    2. Первое падение: ERR_MODULE_NOT_FOUND @vitejs/plugin-react — server/vite.ts статически импортировал vite/vite.config; раньше спасало наличие всех devDeps в образе.
    3. Второе падение: esbuild без --splitting ИНЛАЙНИТ статический import("../vite.config") в бандл и поднимает его импорты на верхний уровень — динамический импорт должен быть по вычисляемому пути (new URL(...), esbuild не анализирует).
    4. Локальная проверка node dist/index.js НЕ ловит такие ошибки, если локальный node_modules содержит devDeps — резолвится молча. Проверять grep'ом по бандлу или в чистом окружении.
    5. Даунтайм продакшена ~25 минут (краш-луп до фикса 5426c70).

[0.2] Контейнер не от root

  • Статус: ✅ done (коммит a54ec77)
  • Зачем: компрометация приложения не даёт root в контейнере.
  • Что изменено: Dockerfile — adduser -D app (uid 1001), chown -R app:app /app, USER app, HOME=/tmp, npm_config_cache=/tmp/.npm, PYTHONDONTWRITEBYTECODE=1; docker/Dockerfile.documents — аналогично (там уже был npm ci --production).
  • Как проверялось: docker run --entrypoint id → uid=1001(app); /app/data и /tmp доступны на запись от app; document-worker на проде поднялся и healthy сразу.
  • Влияние на поиск/UX: нет.
  • Подводные камни: named volume crm_crm_data на проде был root-owned — на сервере заранее выполнен chown -R 1001:1001 на crm_crm_data и crm_crm_uploads (иначе app не смог бы писать в /app/data). При развёртывании на новых серверах — учитывать.

[0.10] Лимиты и пагинация в /api/tasks

  • Статус: ✅ done
  • Зачем: листинг не отдаёт до 10000 полных строк (аудит, Фаза 0).
  • Что изменено:
    • server/utils/task-pagination.ts — clampTasksLimit (дефолт 50, кап 200), курсор <updatedAt>_<id> (parse/build).
    • server/storage/tasks-core.storage.ts — дефолт limit 50/кап 200 на уровне storage; опция excludeDescription (списковые ответы без тяжёлой text-колонки); курсорная пагинация (updated_at, id) с тай-брейкером; новый getTasksCountByOrganization.
    • server/routes/task-crud-list.routes.ts — кап на роуте, параметр cursor, в ответе nextCursor; ключ minimal-кэша включает limit/курсор.
    • Caller'ы: MCP search_tasks limit 500→200; embedding.service — pre-count через getTasksCountByOrganization вместо выгрузки миллиона строк; ShareTarget.tsx — явный ?limit=200.
    • tests/task-pagination.test.ts — 11 юнит-тестов (кламп, курсор, round-trip).
  • Как проверялось: .toSQL() — курсор и тай-брейкер корректны, списковый SELECT без description; npm run check чисто; npx vitest run 56/56.
  • Влияние на поиск/UX: search-ветка не изменена (SQL, лимиты, формат) — инвариант сохранён. Дельта: список без параметров — 50 строк вместо 10000, без description; кастомные JS-виджеты (Bots.tsx, CustomPageView) получают задачи без description (при необходимости дотягивают деталь).
  • Подводные камни: /api/forms/:id/tasks и /with-fields НЕ тронуты (там description остаётся — реестр/канбан/Гантт используют их).

[0.9] Дельта-синхронизация: фильтр на уровне SQL

  • Статус: ✅ done
  • Зачем: sync не выгружает все задачи всех форм (аудит, Фаза 0).
  • Что изменено: getTasksWithFieldsByFormOptimized и getUsersByOrganization принимают since?: Date → WHERE updated_at >= since OR updated_at IS NULL (паритет со старым JS-фильтром); JS-фильтры в sync.routes.ts удалены; initial sync не тронут.
  • Как проверялось: .toSQL() — условие в SQL; индекс tasks_form_updated_idx (0079) покрывает; vitest 56/56.
  • Влияние на поиск/UX: нет.
  • Подводные камни: OR IS NULL обязателен — старые строки с NULL updated_at должны попадать в дельту (паритет с !t.updatedAt || в JS).

Регрессия поиска после 0.10/0.9 (инвариант) — ПРОЙДЕНА

  • Подстрока внутри слова на проде: «емон» → находит «Ремонт…», «отельн» → находит «Котельная…» (через API, тот же путь, что HomeGlobalSearch).
  • EXPLAIN ANALYZE на проде (1002 задачи): Index Scan по tasks_org_updated_idx, Execution Time 0.806 мс — мгновенность сохранена с запасом.
  • Формат search-ответа не изменён; списковые ответы без description — MCP list_tasks подтверждает.

[0.12] getAccessibleTaskIds: кэш + инвалидация

  • Статус: ✅ done
  • Зачем: самый горячий эндпоинт перестаёт деградировать линейно (аудит, Фаза 0). Переписывание EXISTS-цепочки на чистый SQL отложено до замеров после кэша (по плану).
  • Что изменено:
    • server/utils/cache.ts — accessibleTasksCache (TTL 45 сек), ключ accessible:<userId>:<orgId>, метод invalidateSuffix, хелперы точечной (per-user) и org-wide инвалидации.
    • server/storage/system.storage.ts — кэш внутри getAccessibleTaskIds (все caller'ы получают его автоматически, код роутов не тронут); null (admin/view_all) кэшируется отдельно от miss.
    • Инвалидация проставлена в storage-слое (покрывает роуты и MCP): assignees/roles/formAccess/delegation/form visibility/updateUser appRole/createTask/updateTask assignedTo.
  • Как проверялось: npm run check чисто; npx vitest run 65/65 (новый tests/accessible-tasks-cache.test.ts — TTL, null vs miss, invalidateSuffix, LRU, изоляция per-user).
  • Влияние на поиск/UX: нет (форматы ответов не тронуты).
  • Подводные камни: stale-доступ до 45 сек теоретически возможен только при обходе storage-методов (прямые UPDATE в БД) — все кодовые пути покрыты инвалидацией; кэш in-memory — при multi-instance нужен Redis (та же оговорка, что у существующих кэшей).

[0.13] Биллинг: PaymentProvider + MockPaymentProvider

  • Статус: ✅ done
  • Зачем: самообслуживание оплаты без superadmin; реальный эквайринг (ЮKassa/CloudPayments) позже без переписывания (решение пользователя — только mock + интерфейс).
  • Что изменено:
    • server/billing/payment-provider.ts — интерфейс PaymentProvider + фабрика по env PAYMENT_PROVIDER (дефолт mock).
    • server/billing/mock-provider.ts — mock: externalId mock_, webhook-подпись sha256 (timingSafeEqual), secret env MOCK_PAYMENT_SECRET.
    • server/billing/payments.service.ts — createBillingPayment (ON CONFLICT по idempotency_key), applySucceededPayment: транзакция с атомарным guard pending→succeeded (дубли webhook невозможны), credit в billing_transactions, инкремент balance, авто-снятие billingBlocked при балансе > 0 (та же логика, что superadmin unblock).
    • server/routes/billing-payments.routes.ts — POST/GET /api/billing/payments (billing.manage), POST /:id/confirm-test (только mock, иначе 404), публичный POST /api/billing/webhooks/:provider (401 невалидная подпись, идемпотентность).
    • migrations/0080_billing_payments.sql + таблица в shared/schema.ts (UNIQUE external_id/idempotency_key).
    • client/src/pages/Billing.tsx — фикс setLocation в теле рендера (useEffect), кнопка «Пополнить» с диалогом, секция «Платежи», «Подтвердить (тест)» у pending при mock (признак paymentProvider в /api/billing/summary), текст блокировки про авто-восстановление.
    • tests/billing-payments.test.ts — 13 тестов.
  • Как проверялось: vitest 78/78 (дедуп по Idempotency-Key, повторный webhook не зачисляет дважды, изоляция организаций, 404 confirm-test при не-mock, 401 по подписи); npm run check чисто.
  • Влияние на поиск/UX: нет.
  • Подводные камни: на проде PAYMENT_PROVIDER не задан → mock активен, кнопка «Подтвердить (тест)» видна админам организаций — это и есть поставка шага; при подключении реального провайдера кнопка скроется автоматически. Подключение реального провайдера: новый класс в server/billing/ + case в фабрике + env (см. отчёт в коде payment-provider.ts).

[1.1] Data-tables: фильтрация на уровне SQL

  • Статус: ✅ done
  • Зачем: справочники не грузят все строки/таблицы ради фильтрации в JS (Фаза 1).
  • Что изменено:
    • server/storage/data-tables-core.storage.ts — getDataTablesWithAccess без N+1 (2 запроса вместо 1+N); новый getDataTableRowsPaged (SQL: фильтры values->>N ILIKE ESCAPE, поиск values::text ILIKE, сортировка lower()+тай-брейкер position/id, LIMIT/OFFSET); getDataTableRowCounts (один GROUP BY); moveDataTableRow — siblings SQL-запросом.
    • server/utils/data-table-rows-query.ts — escapeIlikePattern, капы limit (дефолт 100, макс 1000)/offset.
    • server/routes/data-tables.routes.ts — GET /rows с limit/offset/search → SQL-путь {rows, total}; без параметров — прежний JS-путь (сортировка localeCompare('ru') сохранена 1:1 для текущего фронта).
    • server/mcp.ts — list_directory_rows (flat) на SQL-пагинацию; list_directories rowCount одним запросом.
  • Tree-режим сознательно не тронут: дерево строится из всех строк, пагинация неприменима.
  • Как проверялось: vitest 88/88 (10 новых тестов хелперов); npm run check чисто.
  • Влияние на поиск/UX: нет (TableEditor попадает в legacy-ветку).
  • Подводные камни: /api/directories/:id/column-values всё ещё грузит все строки (прерывается на 20) — кандидат на следующий заход (SELECT DISTINCT values->>N LIMIT); фронт TableEditor пока не использует SQL-пагинацию (эндпоинт готов).

[1.2] /column-values: лимиты и чистка горячего пути

  • Статус: ✅ done
  • Зачем: cap выборки, убрать console.log с горячего пути (Фаза 1).
  • Что изменено: server/routes/task-crud-list.routes.ts — убраны 7 debug-логов на запрос + per-task лог contract-number; параметр limit (дефолт 20, кап 1000).
  • Как проверялось: выборка покрыта индексом tasks_org_id_form_id_idx — новый индекс не нужен; vitest 88/88.
  • Влияние на поиск/UX: нет.

[1.4] SSE: индекс соединений

  • Статус: ✅ done
  • Зачем: рассылка событий не перебирает все соединения; один heartbeat вместо таймера на соединение (Фаза 1).
  • Что изменено: server/utils/sse-connection-index.ts (новый класс: byId/byOrg/byUser); server/routes/shared.ts — EventBus рассылает через индекс (таргетинг org/user/user+org/broadcast с прежней tenant-семантикой), один глобальный heartbeat-таймер (unref, стоп при отсутствии соединений), убраны per-event console.log.
  • Протокол НЕ тронут: id:/event:/data:, буферы 500, replay по lastEventId — клиенты совместимы.
  • Как проверялось: 8 новых юнит-тестов индекса; vitest 96/96; npm run check чисто.
  • Влияние на поиск/UX: нет.
  • Подводные камни: живой прогон SSE (чат в двух вкладках, reconnect по lastEventId) — проверить на проде после деплоя (юнит-тесты покрывают маршрутизацию, не HTTP-поток).

[1.5] N+1: recursive CTE, Promise.all, батчи

  • Статус: ✅ done
  • Зачем: убрать N+1 на горячих путях (Фаза 1).
  • Что изменено:
    • getTaskTree — один WITH RECURSIVE CTE + один IN-запрос (было 1+2N); защита от циклов (lvl<100, assembled-Set); формат результата прежний.
    • Новый getTaskParentChain (CTE вверх, max 10) — MCP get_task_tree без цикла getTask.
    • Батч-методы: getTasksByIds, getTaskFieldValuesByTaskIds, getTaskMessagesByIds/ByTaskIds.
    • sync.routes.ts — /initial и /delta: Promise.all по формам (порядок ответа сохранён).
    • embedding.service.ts — processEmbeddingQueue и reindexOrganization: батч-предзагрузки (3–4 запроса на org вместо 2–3 на элемент; 2 запроса на чанк 100 задач вместо ~200).
  • Как проверялось: vitest 96/96; npm run check чисто; caller'ы дерева (роут /tree, MCP) — формат не изменился.
  • Влияние на поиск/UX: нет.
  • Подводные камни: raw CTE под PostgreSQL — при переименовании колонок tasks/forms обновить вручную (drizzle не проверяет); getRelatedTasks (граф task_relations) сознательно оставлен — кандидат на отдельный шаг.

[1.3] Виртуализация списков (@tanstack/react-virtual)

  • Статус: ✅ done
  • Зачем: длинные реестры/чаты не рендерят тысячи DOM-узлов (Фаза 1).
  • Что изменено:
    • @tanstack/react-virtual@3.14.11 в dependencies (+6-7 kB gzip).
    • client/src/components/TaskRegistry.tsx — виртуализация строк таблицы (padding-строки в нативном tbody, динамическое измерение высоты через ResizeObserver, overscan 10, getItemKey=task.id). Порог 30 строк — меньше рендер 1:1 прежний. Массовое выделение (Shift+клик через anchorTaskId), сортировка, мгновенный клиентский поиск (инвариант), DnD колонок, inline-редакторы — работают по полному массиву, не по DOM.
    • client/src/components/TaskChat.tsx — виртуализация сообщений (плоский список: дата-разделители + сообщения, динамическая высота, overscan 10). Порог 100 элементов. Автоскролл к новому, scrollToMessage с fallback scrollToIndex, оптимистичная отправка, реакции/опросы/вложения сохранены.
  • Как проверялось: npm run check чисто; vitest 96/96; npm run build собирается.
  • Влияние на поиск/UX: поиск в реестре мгновенный как и был (фильтрация до виртуализации).
  • Подводные камни: в виртуальном чате дата-разделители не sticky; возможна «игра» ширин колонок реестра при table-layout:auto; уход редактируемой строки за overscan размонтирует несохранённый ввод (стандарт виртсписков). Ручная проверка в браузере: реестр с длинными значениями, чат >100 сообщений (автоскролл, скролл к ответу).

[1.6] CI: eslint + npm audit

  • Статус: ✅ done
  • Зачем: lint и аудит зависимостей в CI (Фаза 1).
  • Что изменено: eslint.config.js (flat-config eslint 9 + typescript-eslint + react-hooks; только баг-ловушки, легаси — warn); скрипт npm run lint; pr-check.yml — шаги Lint (блокирующий) и npm audit --audit-level=high (continue-on-error, только отчёт).
  • Результаты: старт — 703 проблемы; итог 0 errors / 576 warnings (exit 0). Попутно исправлен РЕАЛЬНЫЙ баг: условный useRef после early return в TaskTitleInline (client/src/pages/TaskDetail.tsx:1612) — react-hooks/rules-of-hooks.
  • Как проверялось: npm run lint exit 0; vitest 96/96; npm run check чисто.
  • Влияние на поиск/UX: нет.
  • Подводные камни: npm audit нашёл 60 уязвимостей (49 moderate, 10 high, 1 critical — XSS в DOMPurify GHSA-v2wj-7wpq-c8vv) — в бэклог, чинить отдельным шагом; eslint-plugin-react-hooks взят v5 (v7 требует zod-validation-error/v4, в проекте v3.5).

[1.7] Структурированные логи + retention error_logs

  • Статус: ✅ done
  • Зачем: единый logger с уровнями, автоочистка error_logs (Фаза 1).
  • Что изменено:
    • server/utils/logger.ts — уровни debug/info/warn/error (env LOG_LEVEL), LOG_FORMAT=json; подключён в 5 местах (error handler, SSE eventBus, gps worker, automation-scheduler, billing payments). Массовая замена console.* сознательно не делалась (инвазивно) — новые модули обязаны использовать logger.
    • server/workers/error-logs-retention.ts — DELETE error_logs старше 30 дней, раз в сутки + при старте, unref-таймер.
    • server/index.ts — redactSensitive() в captureErrorLog: маскировка password/token/secret/Bearer в записываемых сообщениях.
  • Как проверялось: auth-роуты возвращают статичные тексты без секретов; vitest 96/96.
  • Влияние на поиск/UX: нет.

[1.8] Бэкапы PostgreSQL: документирование + проверка восстановления

  • Статус: ✅ done
  • Зачем: задокументированная стратегия восстановления (RPO/RTO), подтверждённая практикой (Фаза 1).
  • Что изменено: DOCKER.md — раздел «Бэкапы PostgreSQL (production)»: расписание (3:00 pg_dump + 3:30 CSV, ротация 14/7), RPO 24ч/RTO 15–30 мин, команды восстановления, что НЕ входит в дамп (MinIO/Ollama/crm_data), риск «бэкапы на том же VPS» (выгрузка наружу — в бэклог).
  • Как проверялось: восстановление свежего дампа (2026-09-07 03:00, 11 МБ) в scratch-БД на сервере — 0 ошибок pg_restore, 125 таблиц, 990 задач/49 пользователей/14 форм/6024 значений полей, последняя миграция 0078 (дамп предшествует 0079/0080 — консистентно). Scratch-БД удалена.
  • Влияние на поиск/UX: нет.

[1.9] Onboarding

  • Статус: ✅ done
  • Зачем: новая организация не попадает в тупик (Фаза 1).
  • Что изменено: client/src/components/OnboardingChecklist.tsx — чеклист «Начало работы · N из 3» (форма → коллеги → задача), состояние из реальных данных, скрытие per org (localStorage); встроен в Home.tsx. client/src/pages/Tasks.tsx — при отсутствии форм EmptyState с кнопкой «Создать форму» вместо задизейбленной «Создать задачу».
  • Как проверялось: npm run check/lint/build чисто; vitest 96/96.
  • Влияние на поиск/UX: нет (новая плашка только для пустых организаций).

[1.10] Модули и мёртвый код

  • Статус: ✅ done
  • Зачем: MedSchedule не в ядре без флага; мёртвый Capacitor-стек удалён (Фаза 1).
  • Что изменено:
    • MedSchedule за org-флагом organizations.medschedule_enabled (миграция 0081, default FALSE, TRUE для slug='medschedule'; выбран org-флаг как наименее инвазивный и соответствующий прежнему org-ориентированному гейту, admin-override сохранён): /api/auth/me отдаёт флаг, серверный гейт requireMedOrganization, сайдбар и страница модуля по флагу. Worker оставлен (работает только с org medschedule).
    • Capacitor удалён полностью: capacitor.config.ts, client/src/lib/push-notifications.ts (единственный файл с импортами @capacitor/), вызов в App.tsx, 5 зависимостей @capacitor/ из package.json. Expo (mobile/) не тронут — рабочее. Web-Push через @/lib/pwa остался.
  • Как проверялось: grep — упоминаний Capacitor не осталось (и в lockfile); npm run check/lint/build чисто; vitest 96/96.
  • Влияние на поиск/UX: нет.
  • Подводные камни: включение MedSchedule для другой org — SQL UPDATE (UI-переключателя нет, как у GPS-флагов до матрицы).

npm audit fix (внеплановый шаг после 1.6)

  • Статус: ✅ done
  • Зачем: npm audit нашёл 60 уязвимостей (1 critical, 10 high) при подключении аудита в CI.
  • Что изменено: package-lock.json — npm audit fix (совместимые обновления): dompurify 3.4.11→3.4.15 (~20 XSS-advisory закрыты), tar (critical DoS), tiptap и др. Итог: 60 → 46 уязвимостей (0 critical, 2 high, 43 moderate — остаток требует breaking changes, разбор отдельно).
  • Как проверялось: npm run check чисто; vitest зелёные; lint 0 errors; npm run build собирается.
  • Влияние на поиск/UX: нет.
  • Подводные камни: dompurify используется в client/src/lib/htmlUtils.ts для rich-text — обновление патчевое, API совместим.

[2.1] Средние security-фиксы

  • Статус: ✅ done
  • Зачем: anti-enumeration, open redirect, брутфорс, явный SSL, доделка 0.7 (Фаза 2).
  • Что изменено:
    • Anti-enumeration: server/services/auth.service.ts — bcrypt-сравнение всегда (dummy-хэш), единый 401 «Неверный email или пароль» для всех причин отказа; forgot-password — идентичный 200 «Если аккаунт существует…» независимо от email.
    • returnTo: server/utils/return-to.ts + client/src/lib/return-to.ts — только относительные пути с одиночным / (отброс //host, /\host, ctrl-символов); применено в auth и /api/documents/generate-link (был open redirect через res.redirect).
    • Лимит логина: server/utils/login-attempts.ts — 10 неудачных → блок 15 мин (in-memory, сброс при успехе), аудит auth.login.locked; поверх authLimiter по IP.
    • SSL: server/db.ts — rejectUnauthorized через env DATABASE_SSL_REJECT_UNAUTHORIZED (дефолт true).
    • Доделка 0.7: 200+success:false с err.message зачищены в llm-providers/rag/finance-di2.
  • Как проверялось: 8 новых тестов (tests/auth-security.test.ts) — vitest 104/104; check/lint чисто.
  • Влияние на поиск/UX: нет; UX логина: деактивированный аккаунт видит «Неверный email или пароль» (цель anti-enumeration), после 10 попыток — сообщение о 15-минутной блокировке.
  • Подводные камни: блокировки in-memory (рестарт обнуляет; multi-instance делит лимит — как остальные кэши).

[2.3] Дедупликация

  • Статус: ✅ done
  • Зачем: единая точка форматирования сумм (Фаза 2).
  • Что изменено: создан client/src/lib/format.ts с formatAmount (ru-RU, 2 знака, пустое → «0.00» — покрывает обе семантики копий); локальные копии в Billing.tsx и SuperAdminOrgDetail.tsx заменены импортом. Дубля ActiveFilterBadges/ColumnFilter нет — finance/-дубль удалён ранее вместе со старым модулем, канонические в ui/.
  • Как проверялось: check/lint/build чисто; vitest 104/104.
  • Влияние на поиск/UX: нет (отображение сумм идентично).

[2.4] Гигиена репозитория

  • Статус: ✅ done
  • Зачем: мусор, пины образов, дефолты MinIO, доля any (Фаза 2).
  • Что изменено:
    • Удалён _tablefield_orig.txt.
    • Пины образов во всех compose (по фактически работающим версиям на проде, существование тегов подтверждено docker manifest inspect): minio RELEASE.2025-09-07T16-13-09Z, rclone 1.74.0, ollama 0.30.10, traccar 6.15.2, pgvector 0.8.4-pg16, osrm latest@sha256:af5d… (тега v6.0.0 не существует — digest, совпал с продовым).
    • Дефолты minioadmin убраны: ${MINIO_ACCESS_KEY:?...} + комментарии в .env.example/.env.local.example (dev-only дефолт).
    • 28 замен : any + 4 as any в server/utils/auto-transitions.ts (23→0, типы из @shared/schema) и server/routes/shared.ts (5) — без изменения логики. finance-di2/routes.ts (113 any) сознательно не тронут.
  • Как проверялось: docker compose config валиден для всех 4 файлов; check/lint/build чисто; vitest 104/104.
  • Влияние на поиск/UX: нет.
  • Подводные камни: пины синхронизированы в серверный бэкап .bak.serverfix (крит. правило №1) — только image-теги, креды бэкапа не тронуты; локальный .env.local требует MINIO_ACCESS_KEY (есть в .env.local.example).

[2.2a] Разбиение Chat.tsx (2696 → 639 строк)

  • Статус: ✅ done
  • Зачем: гигантские файлы нечитаемы и рискованны в правках (Фаза 2). БЕЗ смены поведения.
  • Что изменено: client/src/pages/Chat.tsx → 8 файлов в client/src/components/chat/: chat-types.ts, chat-utils.ts, MessageBubble.tsx (пузырь + кнопки ботов), NewConversationDialog.tsx, GroupMembersDialog.tsx, HighlightMatch.tsx, TypingIndicator.tsx, useChatController.ts (вся логика страницы дословно). В Chat.tsx — только деструктуризация хука + прежний JSX.
  • Как проверялось: ПРОГРАММНАЯ сверка с оригиналом (git show): тело логики == useChatController посимвольно, рендер == оригиналу (кроме замены inline-блока на ). Чеклист real-time фич дословно на месте (SSE typing, reportActiveChat в useLayoutEffect, polling fallback 10с, оптимистичная автоотметка прочтения с debounce, кнопки ботов с «нажато»). check/lint/build чисто; vitest 104/104.
  • Влияние на поиск/UX: нет (поведение идентично доказанно).

[2.2b] Разбиение TaskDetail.tsx (3278 → 804 строки)

  • Статус: ✅ done
  • Зачем: см. 2.2a. БЕЗ смены поведения.
  • Что изменено: client/src/pages/TaskDetail.tsx → 14 файлов в client/src/components/task-detail/: types.ts, systemTabs.ts, TabFieldsContent, TaskAccessTab (мёртвый код, сохранён для паритета — кандидат на удаление), InlineSystemFields (ответственные, «На себя», срок, замещение), SubtasksTabContent, TaskContent (роутер вкладок + слоты пресетов), TaskTitleInline (хуки в исходном порядке, фикс условного useRef сохранён), useTaskFieldConditions, ReminderDialog, DeleteTaskDialog, TaskDetailHeader, TaskTabsBar, TaskMobileAccordion. В TaskDetail — композиция + хуки верхнего уровня (SSE, переходы, просмотры).
  • Как проверялось: посимвольная сверка — 32 несовпавшие строки, все объяснены (22 — префикс export, 4 — разбитые импорты, 5 — адаптации пропсов, 1 — комментарий). Чеклист: per-field editing, «На себя», просмотры, geo/contact в потоке, пресеты, порядок хуков 1:1. check/lint/build чисто; vitest 104/104.
  • Влияние на поиск/UX: нет.

[2.2c] Разбиение Bots.tsx (3928 → 163 строки)

  • Статус: ✅ done
  • Зачем: см. 2.2a. БЕЗ смены поведения.
  • Что изменено: client/src/pages/Bots.tsx → 16 файлов в client/src/components/bots/: bots-types.ts (+zod-схемы), bots-utils.ts, CollapsibleSection, CreateBotUsersPicker, useBotsController.tsx (вся логика дословно, хуки 1:1), CreateBotDialog, BotListCard, BotSettingsTab (apiAccess/регенерация ключей), BotAiTab, BotWebhookTab, BotSubscriptionsTab, BotApiDocsCard, ServicesSection, JsPagesSection, TabModulesSection. В Bots.tsx — каркас композиции (публичный экспорт BotsContent сохранён — используется в Settings.tsx).
  • Как проверялось: программная сверка — 28/28 сегментов дословно; пропсы типизированы через Pick<ReturnType>. Чеклист: выдача/регенерация API-ключей, zod-валидации, вкладки — дословно. check/lint/build чисто; vitest 104/104.
  • Влияние на поиск/UX: нет.

[2.2d] Разбиение FormEditor.tsx (4975 → 337 строк) — шаг 2.2 завершён

  • Статус: ✅ done
  • Зачем: см. 2.2a. БЕЗ смены поведения.
  • Что изменено: client/src/pages/FormEditor.tsx → 14 файлов в client/src/components/form-editor/: types.ts, constants.ts, utils.ts, SortableWrappers, useFormEditorController.tsx (все хуки 1:1, 1440 строк), FieldCard (карточка поля: Гантт-роли, button-настройки, companyAutofill, contract-number, formula, document_template), MainTab, FieldsTab, StatusesTab, TransitionsTab (конструктор согласующих с resolveTableMode и обратной совместимостью), OfflineTab, LayoutTab (пресеты @preset), FormEditorDialogs. В FormEditor — композиция + шапка + Tabs.
  • Как проверялось: построчная программная сверка — 13/15 блоков байт-в-байт, расхождения объяснены (export-префиксы, перенос типов/констант, фрагменты вместо leading-комментариев JSX, удаление 24 неиспользуемых импортов оригинала). Чеклист: bulkSaveFormFields, правила согласующих, пресеты, QR-toggle, условия полей — дословно. check/lint/build чисто (warnings 566→542); vitest 104/104.
  • Влияние на поиск/UX: нет.
  • Итог 2.2: Chat 2696→639, TaskDetail 3278→804, Bots 3928→163, FormEditor 4975→337.

[2.5] Продуктовая середина: центр уведомлений + a11y

  • Статус: ✅ done (i18n-задел — ⏸ отклонено/отложено по решению пользователя: не-русские рынки не подтверждены)
  • Зачем: центр уведомлений страницей, skip-link и базовый axe-проход (Фаза 2).
  • Что изменено:
    • client/src/pages/NotificationsPage.tsx — страница /notifications: фильтр все/непрочитанные (кнопки с aria-pressed), клиентская «Загрузить ещё» по 30, отметка одного/всех, переход к задаче через общий хелпер getNotificationTarget. Маршрут в App.tsx.
    • client/src/lib/notification-utils.tsx — общие хелперы/хуки уведомлений (дедупликация: SystemNotificationsDropdown отрефакторен на них); ссылка «Все уведомления» в обоих dropdown + пункт в MobileBottomNav (на мобильном dropdown недоступен).
    • Skip-link «Перейти к содержимому» в Sidebar.tsx (первым фокусируемым элементом, sr-only до фокуса, оба main получили id="content") — размещён в Sidebar, т.к. Layout рендерит children внутрь него.
    • tests/a11y.test.tsx — axe smoke (jsdom) на Login/OnboardingChecklist/NotificationsPage: 0 critical/serious; инфра: axe-core, jsdom, @testing-library/react, alias @ в vitest.config + oxc jsx runtime (vitest 4/Vite 8).
    • Исправлены найденные axe-нарушения: aria-valid-attr-value (Tabs без панелей → кнопки), nested-interactive (строка уведомления перестроена).
  • Как проверялось: vitest 107/107; check/lint/build чисто.
  • Влияние на поиск/UX: нет.
  • Подводные камни: GET /api/notifications без серверной пагинации — при очень больших историях «Загрузить ещё» работает на клиенте; серверная пагинация — отдельная задача. Полный axe-проход по всему приложению — задел, не входил.

[0.4] Ресурсные лимиты во все compose-файлы [ПРОД]

  • Статус: ⏳ деплой
  • Зачем: утечка в одном сервисе не роняет хост (аудит, Фаза 0). Подтверждено пользователем 2026-09-08.
  • Что изменено: mem_limit/cpus/pids_limit во всех 4 compose-файлах + серверный бэкап .bak.serverfix (крит. правило №1; преддеплойная копия .bak2-20260908). Значения по docker stats прода 2026-09-07 + запас (сервер 6 ГБ/3 CPU, на хосте другие стеки): app 1g/1.5/256, document-worker 1g/1.0/256, db 768m/0.75/128, minio 256m/0.5/64, webdav 64m/0.25/32, ollama 2g/2.0/64 (модель в RAM на ночном батче), traccar 768m/0.5/128, osrm 512m/0.5/32. proxy (Traefik) сознательно без лимита — критичная инфраструктура хоста.
  • Как проверялось: docker compose config валиден для всех 4 файлов и для бэкапа на сервере.
  • Влияние на поиск/UX: нет.
  • Подводные камни: сумма лимитов ~6.3 ГБ > 5.9 ГБ RAM — лимиты это потолки, не резерв; ollama в простое 36 МБ. На проде лимиты применяются к app/document-worker при ближайшем деплое; остальные сервисы — при их пересоздании.