esbuild без --splitting инлайнит статический import('../vite.config') в бандл
и поднимает его импорты (vite, @vitejs/plugin-react) на верхний уровень —
prod падал с ERR_MODULE_NOT_FOUND при старте. Динамический import по
вычисляемому URL esbuild не анализирует и оставляет рантайму (dev-only).
Проверено: dist/index.js содержит 0 статических импортов vite, бандл стартует,
подключается к БД и доходит до инициализации таблиц
Шаги 0.5/0.2 плана production-готовности (досрочно ушли с a54ec77):
- Dockerfile: npm ci --omit=dev вместо копирования node_modules из builder,
пользователь app (USER app), HOME=/tmp, npm_config_cache=/tmp/.npm
- docker/Dockerfile.documents: USER app, HOME=/tmp
- server/vite.ts: vite и vite.config импортируются динамически внутри setupVite —
иначе prod-бандл падал с ERR_MODULE_NOT_FOUND (@vitejs/plugin-react)
- package.json: vitest/supertest/@types/* (16 пакетов) перенесены
в devDependencies — именно vitest в dependencies тянул vite/esbuild/tsx в прод-образ
Проверено локально: образ собирается, uid=1001(app), /app/data доступна на запись,
бандл стартует без devDeps (падает только на отсутствии JWT-секретов окружения)
Шаг 0.7 плана production-готовности:
- глобальный error handler: нейтральный текст + код инцидента, детали в лог
- route-уровень: зачистка err.message/String(err) в 5xx по 12 файлам,
оригинальные ошибки логируются с контекстом
Шаг A3 (из 1.6) плана production-готовности:
- package.json: скрипт "test": "vitest run"
- pr-check.yml: шаг Tests после type check
- server/utils/userActivity.ts: try/catch в trackUserActivity — синхронная
ошибка трекинга активности больше не срывает authenticateToken (ложный 403
на первом запросе пользователя за минуту)
- tests/auto-transitions.test.ts: 3 устаревших теста обновлены под намеренное
поведение (forward-only position, синк assignee, аудит вместо системного сообщения)
45/45 тестов зелёные, npm run check чисто.
- server/finance/ (19 файлов) удалён: пул финансовой БД перенесён в server/finance-di2/db-core.ts, db-client.ts теперь реэкспортирует из ./db-core
- Шедулер аудита перенесён в server/finance-di2/audit-scheduler.ts (03:00 МСК, setInterval, без node-cron), запуск из registerDi2Routes с защитой от повторного старта
- История аудита DI2 теперь персистентна: загрузка/запись data/finance-audit-history.json в audit-agent.ts (порт из удалённого server/finance/audit.ts)
- registerFinanceRoutes убран из server/routes/index.ts, registerDi2Routes остался на его месте
- Клиент: удалены pages/finance/, components/finance/, hooks/useFinance.ts, hooks/useColumnLabels.ts, lib/format.ts, pages/salary.tsx; роуты /finance*, /income*, /expenses*, /salary и lazy-импорты убраны из App.tsx
- Навигация: «Финансы» в Sidebar и MobileBottomNav ведёт на субапп /di2 (внешняя ссылка), гейт moduleAccess.finance + finance.view сохранён
- index.css: удалён блок .hover-elevate/.hover-elevate-2/.active-elevate (использовался только старым модульным DataTable)
- FinanceBottomNav.tsx сохранён как референс для этапа 3 (.superpowers/sdd/di2-restyle/FinanceBottomNav.reference.tsx)
- Не тронуто: client-di2/, сиды прав finance.view/manage, finance_access (module-access)
DI2-роутер регистрируется перед основным dashboard.routes.ts и перехватывал
GET /api/dashboard (403 без finance.manage и неверный JSON — live-регрессия
главного дашборда CRM). Роут переименован, в DI2_PREFIXES /api/dashboard
заменён на /api/di2, клиент client-di2 ходит на новый путь (fetch + queryKey
+ prefetch в app-sidebar). Форматы старого модуля финансов не тронуты.
- Копия DI2-сервера в server/finance-di2/ с правками: schema/db-client/cache/table-config/override-tables/google-auth/audit-agent/routes
- db-client — обёртка над существующим пулом server/finance/db-client (второй пул не создаётся, добавлен экспорт isConnectionError)
- ai-config.ts — чистое IO конфигов из ai-agent.ts без Telegram-поллинга; роуты /api/ai/toggle и /api/ai/status удалены, без compression/startAuditScheduler/autoStartIfEnabled
- Обёртка registerDi2Routes с auth-gate (authenticateToken + finance.manage), регистрация строго перед registerFinanceRoutes
- Статика /di2 из dist/public-di2 перед веткой vite/static (dev и prod)
- Фикс предсуществующего бага DI2: buildGlobalExclusionConditions без cats в /api/profitability
- Раскладка: шапка-профиль → Основное/Контакты → Роль и доступ/Поля+Замещение → вторичные разделы чипами
- ProfileFieldRow: двойной клик/карандаш, редактор в слоте, Esc, optimistic-кэш, все типы полей
- Замещение: надпись + ⊕ popover, список под строкой
- Удалены RHF-форма, zod-схема и кнопки «Сохранить»
- server: PATCH /api/users/:id (частичное обновление, admin-градация, аудит)
- server: history-number в REST PATCH пишет историю + пересчёт средней
- UI: read-only поля отображаются текстом (у history-number остаётся кнопка графика),
исключены из сохранения; кнопка «Сохранить» скрыта, если все поля вкладки read-only
- Сервер: POST/PATCH field-values профиля отклоняют запись в поле с options.readOnly (400);
автоматизации пишут через storage напрямую и не затрагиваются
- Данные: options.readOnly=true у полей «Оценка» и «Средняя оценка»
- automation-worker-code: getFormFields возвращает поле code (не fieldCode) — поиск поля всегда завершался ошибкой 'Field ... not found', ошибка терялась
- automation.routes: результаты runAutomationsByTrigger (логи, ошибки) теперь пишутся в консоль сервиса с префиксом [automation]
Раньше assigned_to писался только в legacy-колонку tasks.assigned_to, а запись
в task_assignees (модель исполнителей) не создавалась — из-за этого автоматизации,
читающие ctx.tasks.getAssignees (например «Ответственный в состав бригады»),
видели пустой список исполнителей для задач, созданных через MCP
- setUserProfileFieldValueFromAutomation: для history-number значением поля становится переданное число (последняя оценка), а не среднее
- recomputeUserFieldMonthlyAverage: среднее за текущий месяц пишется в поле, указанное в options.avgFieldCode history-number поля (если не задано — в само поле, обратная совместимость)
- Автоматизация «Оценка сотрудникам» (форма 11): сумма трёх числовых полей «Оценка 1/2/3» вместо одного select-поля
- Документация sandbox обновлена во всех четырёх местах (UI, MCP, Obsidian, AGENTS.md)
- setFieldValue для history-number: переданное число пишется в историю, значением поля становится среднее за текущий месяц (автопересчёт)
- ctx.users.recalcFieldAverage(userId, fieldCodeOrId): принудительный пересчёт среднего за месяц (для schedule-автоматизаций; нет записей — поле очищается)
- документация обновлена (Automations.tsx, mcp.ts)
Инфраструктура автоматизаций:
- триггер task.status_changed оживлён: вызов из executeTaskTransition (fire-and-forget) + фильтр triggerConfig.statusId
- ctx.users.setFieldValue(userId, fieldCodeOrId, value): запись user_profile_field_values с аудитом; для history-number — append в user_field_history (миграция 0078)
- документация sandbox обновлена (Automations.tsx, mcp.ts, AGENTS.md) по критическому правилу 2
Мультивыбор пользователей и ролей в user-поле:
- чекбокс «Множественный выбор (пользователи и роли)» в настройках поля (form_fields.allow_multiple)
- значение — массив ['user:ID','role:ID'], обратная совместимость с одиночными/legacy значениями
- UserRoleMultiSelect в карточке задачи, реестре и диалоге создания (роли добавлены и в одиночный сценарий создания)
- синк в task_assignees итерирует массив; role:ID раскрывается в членов роли; PATCH single — diff старого/нового значений
- отображение массивов: реестр, title-шаблоны, условия полей, MCP update_task_fields
- список орг-ролей (/api/roles) загружается всем пользователям (сервер и так был открыт) — роли доступны не только админам
График history-number в профиле:
- GET /api/users/:id/field-history/:fieldId?period=week|month|quarter|year (агрегация AVG по бакетам)
- диалог с LineChart (recharts) и переключателем периодов по клику на иконку у поля
- enqueueEmbedding: onConflictDoNothing без предиката не находил
частичный уникальный индекс embedding_queue_pending_entity_idx
(WHERE processed_at IS NULL) — каждая постановка в очередь индексации
падала. Добавлен where: isNull(processedAt).
- finance ensureViews: CREATE OR REPLACE VIEW не умеет менять тип столбца
(stavkands text↔varchar во внешней БД omegaprom) — заменено на
DROP VIEW IF EXISTS + CREATE VIEW (зависимых объектов нет).
- /profile теперь обёртка MyProfilePage над UserProfilePage (один экран для своего и чужого профиля)
- self-вкладки из ProfileSettings перенесены: Уведомления, Offline, Интерфейс, Безопасность (components/profile/*)
- ProfileSettings.tsx удалён
- админ видит вкладки Уведомления и Offline на чужой карточке с данными целевого пользователя
(userId-параметр в GET/POST /api/subscriptions, PATCH/DELETE по владельцу, GET /api/sync/config?userId=; проверка canManageUser)
- Роль/Активен/Статус disabled для не-админа (сервер их и раньше игнорировал)
- пункт меню «Настройки профиля» переименован в «Мой профиль»
- таблица user_delegations (миграция 0067) + колонки delegated_from_user_id/pending_review в task_assignees
- delegation.service: резолвер с цепочкой и защитой от циклов, bulk-до-назначение чанками, чистка истёкших замещений
- центральный хук в addTaskAssignee: заместитель добавляется при любом назначении (ручном, автопереходе, user-поле, MCP)
- контур ознакомления: после перехода отсутствующий остаётся в исполнителях с pending_review, снимается кнопкой «Ознакомлен»
- inbox reasons delegation/delegation_review + фильтр «Замещение» на главной
- блок «Замещение» в профиле пользователя, бейдж «за <имя>» и баннер ознакомления в карточке задачи
- server: rich-text/company/task разрешены в PATCH field-values (была 400, поля не сохранялись)
- user-поле: дропдаун открывается сразу при редактировании, текущее значение всегда в списке
- select.tsx: viewport max-h-60 вместо высоты триггера (список показывал 1 пункт)
- плотность строк: min-h 28/36px, leading-5, read-only системные строки — py-1 как раньше
- вход в редактирование: double-click/double-tap (одиночный клик — только карандаш)
- textarea/table/task/company: компактная ширина редакторов, h-7
- сброс pendingAutofillRef при отмене; нативный select не отменяется по blur