Клиентская часть гонки (вариант B): Web Locks API (navigator.locks) —
кросс-вкладочный мьютекс iistwin-offline-sync-replay на replay-проход в
useOfflineSync: первая вкладка под локом реплеит и удаляет записи, остальные
видят пустую очередь. Fallback для сред без Web Locks — старое поведение
(дубли гасит серверная идемпотентность). Race-тест: без locks → 2 POST,
с locks → 1 POST для одной записи очереди.
Мессенджер (conv_messages) — та же защита, что у task_messages:
- conversation_messages.client_message_id + partial unique (миграция 0087);
- POST /api/messenger/conversations/:id/messages: повтор с тем же
clientMessageId → существующее сообщение без insert и без SSE/уведомлений,
гонка insert'ов ловится по 23505;
- useChatController шлёт crypto.randomUUID(), attachment-реплей мессенджера
тоже использует id записи очереди.
Первопричина дублей (3 одинаковых сообщения в задаче 1988): живой POST сорвался
→ сообщение попало в общую IndexedDB-очередь → background-sync разослал
TRIGGER_SYNC всем вкладкам → каждая вкладка реплейнула одну и ту же запись
(мьютекс _syncLock модульный, кросс-вкладочной координации нет). Гонка
воспроизведена тестом tests/offline-queue-multitab-race.test.tsx (1 запись →
2 POST для двух вкладок).
Вариант C — серверная идемпотентность:
- task_messages.client_message_id + partial unique index (миграция 0086);
- sendTaskMessage: повтор с тем же clientMessageId возвращает существующее
сообщение БЕЗ insert и БЕЗ side-эффектов (уведомления/SSE/вебхуки не дублируются);
гонка insert'ов ловится по 23505 → fallback на select;
- клиент TaskChat шлёт crypto.randomUUID() в каждом сообщении; при офлайн-постановке
тело с ключом сохраняется, все реплеи идут с одним ключом;
- attachment-реплей: clientMessageId = id записи очереди (стабилен между реплеями);
- юнит-тесты tests/task-message-idempotency.test.ts (3: быстрый путь, гонка 23505,
проброс прочих ошибок).
Инфраструктура автоматизаций:
- триггер 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) и переключателем периодов по клику на иконку у поля
- /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 + фильтр «Замещение» на главной
- блок «Замещение» в профиле пользователя, бейдж «за <имя>» и баннер ознакомления в карточке задачи
B: BatchCreateDialog режет список на чанки ≤100 (лимит сервера), каждый
чанк со своим idempotency-key (requestId-N) — повтор после ошибки не
создаёт дублей; 10-минутный лок армируется только при полном успехе,
а не до запроса (раньше блокировал даже после 400).
D: таблица error_logs (миграция 0066), захват ответов >=400 в request
middleware (без 401/health/events, дедуп 60с), admin API GET/DELETE
/api/error-logs и вкладка «Журнал ошибок» в Настройках.
- планировщик server/workers/automation-scheduler.ts (тик 60 сек, все организации)
- защита от дублей: automations.last_scheduled_run_at (миграция 0065), catch-up в пределах суток
- UI: выбор триггера 'По расписанию' и времени запуска в Automations.tsx
- MCP: create_automation/update_automation принимают trigger 'schedule'
- triggerData: { trigger: 'schedule', scheduledAt }
- organization_api_keys.scopes (jsonb, NULL = полный доступ для legacy-ключей)
- MCP: фильтрация инструментов по режиму и проверка formIds в handler'ах
- PATCH /api/mcp-keys/:id, валидация скоупов в server/utils/api-key.ts
- UI Settings: диалог создания/редактирования ключа с режимом и мультивыбором форм/справочников
- Добавлены active_chat_* поля в user_presence и endpoint /api/devices/active-chat
- Сервер не шлёт Web Push/Pushy, если получатель смотрит в тот же чат на видимой вкладке
- Клиент сообщает серверу и Service Worker об активном чате из Chat.tsx и TaskChat.tsx
- TaskChat: входящие сообщения автоматически отмечаются прочитанными при видимой вкладке
- SW: подавляет foreground/system push, если активный чат совпадает
- Добавлена миграция и поле forms.qr_generation_enabled.
- В редакторе формы (вкладка «Основное») добавлен переключатель «Показывать кнопку генерации QR в реестре форм».
- В реестре форм (/forms) рядом со статусом формы появляется кнопка QR, если toggle включён.
- Новый компонент BatchTaskQrDialog: генерирует QR для всех задач формы по всем qr.scan-шаблонам, позволяет скачать отдельный PNG или все сразу ZIP-архивом.
- Добавлена зависимость jszip.
- Добавлено поле data_tables.tree_display_column_index.
- Backend /api/directories/:id/rows/tree возвращает displayColumnIndex и
формирует label для row-узлов на основе выбранной колонки.
- В редакторе справочника добавлен селектор «Колонка для отображения в дереве».
- DirectoryRowTree, DirectoryColumnTree, DirectoryTreeSelect,
DirectoryTreeMultiSelect используют настроенную колонку.
- TableFieldSelect / TableFieldMultiSelect используют
treeDisplayColumnIndex справочника как fallback, если в поле формы
не задан displayColumn.
- Миграция 0057 + запись в _journal.json.
- Add client/src/themes/ with tokens, built-in skins (iistwin, twenty, omegaprom)
- Add ThemeProvider with data-skin/data-theme and runtime CSS for custom skins
- Add npm run skins:generate script for build-time CSS generation
- Add organization_skins table, migration and CRUD API
- Add activeSkinId to organizations and expose it in /api/auth/me
- Add skin switcher in /profile and admin skin editor in /settings/appearance
- Adapt index.css hardcoded colors to skin tokens
- Update AGENTS.md and Obsidian notes
- number-поля теперь рендерятся как text + inputMode=numeric,
чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage
Closes: искажение расчётного счёта и других длинных числовых полей