205 Commits

Author SHA1 Message Date
b3f4529376 fix(offline): presence-вызовы не попадают в офлайн-очередь и молча дропаются из конфликтов
Some checks are pending
Deploy to Server / Type Check & Build (push) Waiting to run
Deploy to Server / Build & Push App Image (push) Blocked by required conditions
Deploy to Server / Deploy to VPS (push) Blocked by required conditions
В PWA накапливались десятки конфликтов «/api/devices/active-chat»: эфемерные
presence-вызовы (активный чат, heartbeat) при сетевых сбоях ставились в очередь,
при реплее ловили 4xx и становились конфликтами, требующими ручной разборки.
Реплеить их бессмысленно — они устаревают мгновенно.
- isQueueable: /api/devices/active-chat и /api/devices/heartbeat больше не
  ставятся в очередь (как read receipts);
- isSafeToDrop: те же URL молча дропаются при 4xx;
- уже помеченные конфликтами presence-записи авто-синк дропает молча —
  накопленные десятки очистятся при следующей синхронизации.
2026-10-01 11:50:57 +03:00
49b7732a24 Custom pages: флажок «скрыть шапку страницы» (миграция 0089)
- custom_pages.hide_header: платформа не рендерит заголовок (название + описание)
  над виджетом — для полноэкранных отчётов/дашбордов, без CSS-хаков из кода страницы
- UI: переключатель «Скрыть шапку страницы» в диалоге создания/редактирования
- REST create/patch принимают hideHeader; MCP create_custom_page/update_custom_page —
  параметр hide_header (виден ИИ сразу, default false)
2026-09-30 15:42:52 +03:00
06a2794090 refactor(chat): общие ChatComposer и useChatScroll для чата задачи и мессенджера
Пункты 1–2 плана де-дубликации чатов — больше не нужно «чинить и там и там»:
- components/chat/ChatComposer.tsx: textarea + меню трёх точек (Прикрепить файл/
  Создать опрос) + кнопка отправки, Enter-отправка (в PWA — только кнопкой),
  paste/drag&drop файлов, спиннер отправки; специфика клавиш родителя
  (упоминания, Escape) через onKeyDown + defaultPrevented-протокол;
- components/chat/useChatScroll.ts: pin к низу при открытии (useLayoutEffect до
  paint), автоскролл у низа/своё, тихая доставка на скрытой вкладке, счётчик
  «↓ N новых», pin при возврате на вкладку;
- TaskChat и useChatController/Chat.tsx переведены на оба; удалены дубли
  (~200 строк): свои scrollToBottom/atBottomRef/lastSeenLengthRef/эффекты,
  свои формы композера, drag&drop-стейты.
2026-09-30 14:37:31 +03:00
d5d95e2f53 feat(chat): меню трёх точек в композере обычного чата — как в чате задачи
Кнопки «Прикрепить файл» и «Создать опрос» в мессенджере спрятаны за иконку
MoreVertical (DropdownMenu) — единый вид композера в обоих чатах (в чате задачи
сделано ранее, f6e3e19).
2026-09-30 14:10:40 +03:00
67b71ffd0e Внешние БД: пустой список таблиц = доступ ко всем (allowlist опционален)
Основная отсечка прав — на уровне пользователя внешней БД (GRANT SELECT);
allowlist в подключении теперь опциональное дополнение (было обязательным).
2026-09-30 00:16:36 +03:00
59bd802bf7 Песочница отчётов для ИИ: recharts в custom pages + внешние БД с read-only шлюзом
- JsComponentRenderer: ctx.libs.charts (recharts) — графики в кастомных страницах
  и JS-вкладках без redeploy; справка get_js_coding_reference обновлена
- Внешние БД (миграция 0088): вкладка «Настройки → Внешние БД» — реестр подключений
  (пароль AES-256-GCM, env EXTERNAL_DB_SECRET, по API не отдаётся), тест подключения,
  allowlist таблиц на подключение
- Read-only шлюз POST /api/external-db/:id/query (finance.manage): только SELECT,
  без мульти-выражений, обёртка с LIMIT 5000, statement_timeout 10с, проверка таблиц
  по allowlist, аудит в external_db_query_log (кто/SQL/строки/мс)
- Назначение: ИИ-агенты строят отчёты и виджеты поверх витрин (напр. НДС 1С) без
  новых серверных эндпоинтов и пересборки
2026-09-29 22:50:40 +03:00
9827b5df33 fix(chat): cross-tab lock реплея офлайн-очереди + идемпотентность мессенджера (conv_messages)
Клиентская часть гонки (вариант 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 записи очереди.
2026-09-29 20:31:59 +03:00
af4850bd6d fix(chat): идемпотентность сообщений по clientMessageId — защита от дублей при повторной отправке
Первопричина дублей (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,
  проброс прочих ошибок).
2026-09-29 17:40:47 +03:00
4d1dc2b6f9 fix(users): fieldValues в реальном обработчике /api/users/list + горизонтальный скролл таблицы
Причина «поля профиля не отображаются»: /api/users/list обслуживает СТАРЫЙ
обработчик в auth.users.routes.ts (зарегистрирован раньше дубля handleListUsers
в user.routes.ts — проверено: в ответе нет total/pageSize). includeFieldValues
добавлен в оба, но реальный — теперь тоже отдаёт fieldValues.
Плюс overflow-x-auto на контейнере таблицы — прокрутка при переполнении колонок.
2026-09-29 15:32:47 +03:00
c0011b49a3 feat(users): выбор колонок в реестре пользователей + кликабельная Оценка и доработанный график истории
Реестр /users:
- кнопка «Колонки» — чекбоксы базовых колонок и кастомных полей профиля
  (Оценка, Средняя оценка и др.), скрытые ключи в localStorage;
- /api/users/list?includeFieldValues=1 — значения полей текущей страницы;
- поля типа history-number (Оценка) кликабельны — открывают график истории.

График истории (FieldHistoryDialog):
- точки красные и крупные (на тёмном фоне дефолтные не видны), линия primary;
- клик по точке — под графиком список оценок за бакет со ссылками на задачи
  (название задачи → /forms/<formId>/tasks/<id>);
- API field-history возвращает entries (value, taskId, taskTitle, formId, bucket).
2026-09-29 15:09:22 +03:00
c0b8910094 fix(gps): плавное движение маркеров при зуме — позиции обновляются напрямую в DOM на actiontick (без React-рендера), кластеры перестраиваются на actionend/zoomchange 2026-09-24 21:36:48 +03:00
69039e14a1 fix(gps): маркеры Яндекса — собственный DOM-слой поверх карты (клики React, пиксельная кластеризация); отказ от Clusterer/ObjectManager с их багами кастомных макетов 2026-09-24 16:36:34 +03:00
0409a547fc diag(gps): тестовый объект со стандартной иконкой в ObjectManager — разделение рендер vs layout 2026-09-24 08:37:12 +03:00
d54d2db351 diag(gps): перехват ошибки загрузки маркеров в ObjectManager 2026-09-23 15:09:39 +03:00
4b532540be fix(gps): формат объектов ObjectManager — GeoJSON Feature с coordinates [lng,lat] 2026-09-23 14:49:27 +03:00
39c8d61a04 fix(gps): типизация geoObjects кластера ObjectManager 2026-09-23 14:34:44 +03:00
e9f072c0c3 fix(gps): слой маркеров Яндекса переведён с Clusterer на ObjectManager — кастомные макеты в Clusterer теряли кликабельность (известный баг), ObjectManager лишён этой болезни 2026-09-23 14:33:49 +03:00
a231f64837 fix(gps): iconShape задан от точки привязки маркера (anchor), а не от угла — активная область перекрывает маркер 2026-09-23 13:41:07 +03:00
345c34730f fix(gps): iconShape у кастомных маркеров Яндекса — без него hit-test не находил метку, клики уходили в events-pane 2026-09-23 13:07:13 +03:00
1353b23c7d feat(gps): совпадающие точки задач раскидываются веером (~30 м) — кластер из одной координаты раскрывается зумом 2026-09-23 12:52:18 +03:00
a1e05c4cdb diag(gps): временные логи доставки клика на Яндексе (container/placemark) для точной диагностики 2026-09-23 12:48:34 +03:00
3964d861ae fix(gps): yandexMapDisablePoiInteractivity — отключены POI-хотспоты Яндекса (403 search/v2, вмешательство в клики по маркерам) 2026-09-23 10:58:19 +03:00
d238b005f4 fix(gps): двойной путь доставки клика по маркеру — data-gps-action на корне (capture-делегирование) + placemark.events 2026-09-23 10:04:06 +03:00
dadd49c15f fix(gps): свои панели вместо попапов карт — клик по точке показывает карточку задачи, клик по кластеру — вертикальный список (устранена ненадёжность balloon/popup) 2026-09-23 09:16:55 +03:00
6c1cea39b4 fix(gps): клик по точке открывает попап с названием (делегирование только по data-gps-action); спайдерфай кластеров в одной координате на max zoom 2026-09-23 08:49:15 +03:00
27a14ec513 feat(gps): клик по точке объекта показывает название (попап + «Открыть задачу»); точки объектов кластеризуются, список кластера вертикальный 2026-09-22 22:11:38 +03:00
dfedb3ebfa feat(gps): настройки слоя «Объекты» сохраняются (форма/поле/названия), подписи в две строки: объект / заказчик 2026-09-22 21:39:27 +03:00
fe174535b7 feat(gps): кнопка «Названия» для слоя объектов (по умолчанию скрыты); подписи сокращены до «объект · заказчик» (≤40 символов) 2026-09-22 21:15:31 +03:00
42894aaf8f feat(gps): на вкладке «Карта» слой задач формы (селектор формы+geo-поля) + создание объекта правым кликом с координатами 2026-09-22 20:59:54 +03:00
fe57e368da fix(gps): подложка OSM-режима на Esri World Street Map (CARTO теперь требует API-ключ — водяные знаки) 2026-09-22 20:08:19 +03:00
db6b18f679 fix(gps): OSM-подложка переведена с tile.openstreetmap.org (403 Access blocked в проде) на CARTO Basemaps 2026-09-22 19:54:13 +03:00
13db5be030 fix(chat): React #310 — ChatSection вынесен на module level вместо useCallback
useCallback для ChatSection стоял после раннего return (if (!task)) —
в рендерах без задачи хук не вызывался → «Rendered more hooks than during
the previous render». Правильное решение: компонент на module level с пропом
taskId (стабильный тип + нет зависимости от хуков).
2026-09-21 22:10:57 +03:00
693834ac27 fix(chat): настоящая первопричина дёрганья — remount чата из-за inline-компонента ChatSection + первый кадр не у низа
- TaskDetail: ChatSection объявлен inline в теле рендера → новый тип компонента
  на каждый рендер → React перемонтировал весь чат при ЛЮБОМ ре-рендере TaskDetail
  (SSE-событие, refetch задачи, возврат на вкладку): скролл сбрасывался вверх,
  пользователь видел «старые сообщения → прыжок к последним». Фикс: useCallback
  со стабильной идентичностью (тот же паттерн, что у мобильного accordion).
- TaskChat/useChatController: скролл к низу при открытии чата — useLayoutEffect
  СИНХРОННО ДО paint (первый кадр уже у низа) + докрутка первого кадра, когда
  сообщения пришли после монтирования без кэша.
2026-09-21 21:33:26 +03:00
34838a302d feat(chat): онлайн-доставка как в Telegram — watchdog SSE, тихий скролл, персистентный кэш сообщений, невидимое обновление SW
- SSE: серверный heartbeat теперь именованное событие ping (25с) — клиент может отличить живое соединение от мёртвого; клиентский watchdog (45с без активности → принудительный reconnect), reconnect 1с вместо 3с, polling-фолбэк 5с вместо 10с
- Скролл: на скрытой вкладке скролл не трогаем (раньше автоскролл «выбрасывал» пользователя вниз при возврате); возврат на вкладку — pin к низу если был у низа; при чтении истории плавающий индикатор «↓ N новых» (оба чата)
- Персистентный кэш последних 100 сообщений чата в localStorage → initialData: мгновенный рендер после перезагрузки страницы без «Загрузка сообщений...», свежие данные в фоне; gcTime: Infinity
- SW: reload на controllerchange только на скрытой вкладке (обновление незаметно), предыдущая версия кэша сохраняется для ленивых чанков старой страницы; кэш v15
2026-09-21 20:29:13 +03:00
f8d78afd2e feat(auth): retry рефреша при 401 + вкладка «Сессии» в профиле с отзывом устройств 2026-09-21 18:13:47 +03:00
2f9d049718 fix(chat): catch-up сообщений при возврате на вкладку; fix(sw): фокус на открытой вкладке вместо навигации всех; fix(history): числа не резолвятся в имена пользователей 2026-09-21 17:03:01 +03:00
1f878b61a1 feat(chat): персистентные черновики сообщений — текст переживает перезагрузку страницы 2026-09-21 15:14:30 +03:00
e3f0ebfb89 feat(fields): мягкое удаление полей формы — архив удалённых с восстановлением и блок «Удалённые поля» в задаче 2026-09-21 14:35:45 +03:00
d5f2925886 feat(tags): полный тинт строк/карточек, цветное значение поля вместо чипа в шапке; защита от дубль-клика при открытии задач 2026-09-21 14:35:45 +03:00
c1463c71a1 feat(fields): цветные опции select — глобальная подсветка задач (главная, реестр, канбан, шапка) 2026-09-21 11:13:44 +03:00
aa316141be feat(reminders): получатели в отложенных/входящих, приватные сообщения чата, календарь, бейдж уведомлений 2026-09-21 10:26:53 +03:00
17e6ebf412 fix(ui): удалён FAB закладок — перекрывал поле ввода чата в PWA
FAB (z-40, fixed right-bottom) ложился поверх композера чата и кнопок отправки.
Добавление закладки уже доступно: меню значка «Закладки» в MobileBottomNav
(мобильный) и сайдбар (десктоп) — функционал дублировался.
2026-09-21 08:24:08 +03:00
267af046a3 fix(chat): автоскролл задач только у низа, картинки с фикс. размером, упоминания по @ и 3+ буквам имени/фамилии
- TaskChat: atBottomRef-гвард (scrollHeight-scrollTop-clientHeight<80) — чужие
  сообщения больше не дёргают чат вниз, когда пользователь читает историю;
  свои сообщения и смена задачи скроллят как раньше
- ChatAttachments: img w-180 h-180 object-cover вместо max-* — высота строки
  не меняется при догрузке, measureElement виртуализатора не дёргает список
- упоминания: голый @ открывает список сразу; текстовый триггер от 3 букв;
  сервер ищет ILIKE по имени/отчеству/фамилии/email и отдаёт всех при пустом q;
  починен мёртвый fallback-резолв (Response без .json())
- groupByDate/flattenGroupedMessages обёрнуты в useMemo
2026-09-17 22:00:10 +03:00
d0112657b8 fix(auth): живая сессия на устройстве — рефреш на холодном старте, grace-окно ротации, без логаута при временных ошибках
- /api/auth/me при 401 делает одну попытку refresh (раньше refresh-retry был
  отключён для всех /api/auth/* — холодный старт после 15 мин простоя = логаут)
- ротация: повтор старого refresh-токена в пределах RACE_TOLERANCE_MS (5 мин)
  выдаёт новую пару от successor вместо deny; cookie больше не стираются при
  stale-токенах (разлогинивались все вкладки устройства); reuse после окна —
  revoke family как раньше (тесты обновлены)
- refreshSession различает 401/403/400 (сессия мертва) vs 429/5xx/сеть
  (transient — нет logout, отложенный retry)
- единый рефреш: useAuth keep-alive и useOfflineSync переведены на refreshSession;
  межвкладочная дедупликация через BroadcastChannel('auth-refresh')
- refreshLimiter: keyGenerator buildRateLimitKey (per-user, не общий IP-бакет офиса)
2026-09-17 21:59:51 +03:00
c1d5b1cbee fix(gps): остановлен 429-шторм — throttle SSE gps_asset_moved (3 сек/asset) и клиентских инвалидаций (60 сек), polling позиций 60 сек, статусов 5 мин
Каждая точка ingest (~1/сек) broadcast'илась SSE всем клиентам орг., клиент
на каждое событие делал invalidateQueries без throttle → 2+ GET/сек → исчерпание
per-user бакета apiLimiter (1500/15мин) → все API устройства 429 (формы не грузились).
Модель теперь как у нормальных трекеров: точки пишутся в БД, карта читает по интервалу.
+4 unit-теста троттла (server/gps/sse-throttle.ts)
2026-09-17 21:59:34 +03:00
e4f9a1992a feat(automations): ctx.tasks.scheduleStatusChange — отложенная смена статуса (воркер 5с, отмена при ручном переходе) 2026-09-17 16:38:56 +03:00
c1fe1c4e02 fix(registry): непрозрачный фон sticky-шапки, отказ от table-fixed
- шапка: [&_th]:bg-background вместо полупрозрачного bg-muted/50 — иначе сквозь
  sticky-шапку проступают строки и она нечитаема (в plain-варианте фона не было вовсе)
- ресайз: table-fixed + w-full таблицы + w-max wrapper давали циклическую зависимость
  ширин — колонки без заданной ширины схлопывались до нуля при первом drag.
  В auto-раскладке <colgroup> задаёт предпочтительную ширину без схлопывания

check/build чисто, vitest 129/129
2026-09-14 23:54:10 +03:00
e32def495d feat(registry): горизонтальная прокрутка внизу области, sticky-шапка, ресайз колонок, лимит строк в ячейке
- ui/table: пропс wrapperClassName (twMerge), в реестре wrapper = w-max min-w-full
  overflow-visible — переполнение по ширине всплывает в контейнер реестра,
  скроллбар оказывается внизу видимой области, а не под всей таблицей
- TableHeader sticky top-0 (фон tr для plain-варианта скина)
- drag правого края шапки меняет ширину колонки (table-fixed + colgroup,
  min 80 / max 1200 px), двойной клик — сброс; ширины хранятся в localStorage
  per-form (registry-colwidths-<formId>) — у каждого пользователя свои
- настройка «Строк в ячейке» (Авто / 1..8) в диалоге настроек реестра:
  CSS line-clamp на текстовых ячейках, хранится в registry-rowlines-<formId>;
  оценка высоты виртуализатора ниже в однострочном режиме

check/build чисто, vitest 129/129
2026-09-14 23:21:02 +03:00
c8b7741b11 fix(ui): мобильный main стал flex-колонкой — flex-1 PageTransition больше не игнорируется, h-full-цепочки страниц восстановлены
На мобильном (<768px) main#content был блочным: flex-1 обёртки PageTransition
работал вхолостую, высота страницы росла по контенту, весь скролл уходил в main,
а скроллбар широкой таблицы реестра оказывался внизу недостижимой высоты.
Десктопный main уже был flex flex-col (Sidebar.tsx:1687).
2026-09-14 23:08:30 +03:00
5af274cd0f fix(ui): двойной Sidebar на реестре задач и карточке задачи — TaskView/TaskDetail рендерили свой Sidebar поверх AppShell 2026-09-09 08:22:41 +03:00