Commit Graph

300 Commits

Author SHA1 Message Date
5aebcd1308 MCP: validate_custom_page_code — статическая проверка кода страницы до сохранения
Read-скоуп, без мутаций: те же проверки, что в create/update (404-маршруты, Ant Design,
отсутствующий 'return function Widget', .data-баги, композиция Select), плюс отдаёт
канонически отформатированный код для сохранения. Даёт ИИ-агенту цикл
«правка → проверка → сохранение» без сохранения битых страниц.
2026-09-30 00:35:12 +03:00
67b71ffd0e Внешние БД: пустой список таблиц = доступ ко всем (allowlist опционален)
Основная отсечка прав — на уровне пользователя внешней БД (GRANT SELECT);
allowlist в подключении теперь опциональное дополнение (было обязательным).
2026-09-30 00:16:36 +03:00
98499ca9d4 MCP custom pages: show_in_sidebar + постоянная ссылка для закладок
- create_custom_page: параметр show_in_sidebar (default true); результат содержит
  url (/pages/<slug>) и bookmark_url (полный URL) — страницу можно сразу добавить в закладки
- update_custom_page: параметр show_in_sidebar — скрыть/показать в боковом меню без удаления;
  результат также отдаёт url/bookmark_url
- В UI (Боты → JS Страницы) переключатель «Показывать в боковом меню» и ссылка /pages/<slug>
  уже были; сайдбар фильтрует по showInSidebar
2026-09-30 00:04:10 +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
f6e391d588 НДС в DI2: порядок тултипа графика, компактный виджет контроля квартала
- Тултип «Динамики НДС»: порядок Начислено за месяц → Вычет за месяц → К уплате за месяц
  → Накоплено за квартал; линии переименованы с уточнением «за месяц»
- «Контроль квартала»: высота таблицы ~5 строк с вертикальной прокруткой (как у трекера),
  убран список «Оплачено, аванс не висячий»
2026-09-29 16:11:40 +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
c56ec26b49 НДС в DI2: линии начислено/вычет на графике, таблицы с прокруткой
- Динамика НДС (месяц): поверх столбцов «к уплате за месяц»/«накоплено за квартал»
  добавлены линии «Начислено» и «Вычет»
- Трекер авансовых УПД: без лимита 15 (все ждущие), настоящая table с фиксированными
  колонками (table-fixed, truncate+title), видимы первые ~5 строк, дальше скролл
  внутри блока, шапка прилипает (sticky)
- Контроль квартала: главная таблица и «Показать все» — то же оформление table + скролл
2026-09-29 12:02:58 +03:00
6ef0621d63 НДС в DI2: трекер авансовых УПД, доработка графика и виджета «Контроль квартала»
- Новый GET /api/di2/nds/advance-vat-tracker (nds_advance_vat_tracker): KPI «сколько можем
  сбавить НДС» (оценка сверху 22/122) + топ-15 поставщиков без авансовой УПД; карточка
  сразу после «Динамики НДС» с обязательными оговорками из спеки
- Новый GET /api/di2/nds/quarter-control: redHanging / redNoAdvance («проверить зачёт») /
  totals / byMarker / ?all=1 — по правкам виджета из спеки
- График «Динамика НДС» (месяц): два столбца «К уплате за месяц» и «Накоплено за квартал»,
  кварталы выделены фоновыми полосами (ReferenceArea)
- Виджет «Контроль квартала»: легенда с подписями маркеров, итог «N договоров, сумма
  авансов X», кнопка «Показать все» (полный список с маркерами), отдельный список
  «Оплачено, аванс не висячий», полный год в датах договоров, overflow-x для таблиц
2026-09-29 11:35:30 +03:00
5fe0e30ea0 fix(files): get_task_file отдаёт внешний presigned-URL через прокси приложения вместо внутреннего minio:9000
Живой тест показал: presigned-ссылка формировалась против внутреннего хоста
(http://minio:9000/...) — снаружи docker-сети недоступна, для ботов бесполезна.
- presigned-токены вынесены в server/utils/presigned-tokens.ts (общие для
  index.ts и mcp.ts);
- MCP get_task_file возвращает /api/files/<key>?presigned=<token> — работает
  через https://iistwin.ru без авторизации ~5 минут, в S3 и локальном режимах.
2026-09-29 11:04:02 +03:00
0b4324c23d fix(files): принадлежность файла задаче — дозаполнение task_id/field_id + MCP-проверка с сериализацией значений
Живой тест показал: 77 файлов отслеживаются, но get_task_file отвечал «не
относится к задаче» — старые backfill-строки без task_id/field_id (ON CONFLICT
DO NOTHING их не обновлял), а проверка по file-полям искала ключ только в
строковых значениях (file-поля хранятся объектом/массивом).
- MCP get_task_file: поиск ключа в JSON.stringify(value) для нестроковых значений;
- trackFileOnDemand: у существующих строк без task_id дозаполняет taskId/fieldId;
- startup-backfill: UPDATE task_id/field_id из task_field_values и task_messages
  для строк с task_id IS NULL.
2026-09-29 10:50:34 +03:00
102f5410b1 fix(files): backfill — regexp_replace вместо substring FROM (возвращал префикс, а не ключ) + удаление мусорных ключей со слэшами
substring(url FROM '^.*\/api/files/') в PG возвращает СОВПАВШУЮ часть (префикс
URL), а не остаток — backfill писал бы мусорный file_key. regexp_replace
проверен на проде: ключ извлекается корректно из абсолютных и относительных URL.
Добавлен DELETE file_uploads с file_key LIKE '%/%' (ключи слэшей не содержат).
2026-09-29 10:41:53 +03:00
4bac21ff71 fix(files): бот может скачивать файлы — API-ключ на /api/files и /uploads + трекинг абсолютных URL
Проблема: прямое скачивание /api/files/<key> с X-Api-Key давало 403 (файловые
эндпоинты принимали только JWT), а get_task_file отвечал «не отслеживается» —
77 файлов со старыми АБСОЛЮТНЫМИ URL (https://iistwin.ru/api/files/...) не
попадали в file_uploads: startup-backfill понимал только относительные ссылки.

- tryPresignedOrAuth: ветка X-Api-Key — resolve ключа, req.apiKey/organizationId,
  tenant-контекст; владение проверяет canAccessFile, скоупы форм — новый
  checkApiKeyFileScope (файл привязан к задаче → форма должна быть разрешена ключом);
- /api/files/:key/presigned теперь тоже через tryPresignedOrAuth (боты могут
  выпускать presigned-ссылки);
- server/utils/file-tracking.ts: trackFileOnDemand — догрузка по требованию из
  task_field_values/task_messages по ссылке любого вида; используется в
  canAccessFile и MCP get_task_file;
- startup-backfill: паттерны покрывают абсолютные URL (substring FROM regex).
2026-09-29 10:36:41 +03:00
2ac9da7220 НДС в DI2: KPI-карточки показывают текущий квартал (начислено/вычет/к уплате)
Все три карточки — состояние текущего квартала по состоянию на сегодня
(квартал последнего месяца в данных). Полная история остаётся в графиках.
2026-09-29 09:39:17 +03:00
b9ef0b2152 НДС в DI2: дефолтный период — вся история витрины, KPI «К уплате» — за квартал
- summary: без фильтров дат период = весь доступный диапазон nds_monthly (с 2025-01)
- «К уплате» теперь всегда квартальный: квартал выбранного периода, если он внутри одного
  квартала, иначе квартал последнего месяца в данных (НДС не копится между кварталами)
- карточки KPI: «Начислено»/«Вычет» за период с подписью, «К уплате» с меткой квартала
2026-09-29 09:15:43 +03:00
de9bbfe17a НДС в DI2: вкладка «Расчёты → НДС» переведена на декларационную витрину 1С
- Новый server/finance-di2/nds.routes.ts: GET /api/di2/nds/summary|quarters|input-balance|sync-status|contract-markers
  (только отображение nds_monthly/nds_input_balance/nds_sync_log/nds_contract_marker, без расчёта НДС)
- VatTab переписан: KPI, динамика (месяц с накопительным итогом квартала / квартал), по ставкам,
  stacked по типам операций, доля авансов, резерв вычетов (19 счёт), баннер свежести,
  таблица «деньги есть — отгрузки нет» (RED_NO_SHIPMENT > 1 млн)
- Старый GET /api/calculations/vat (вычисление 20/120 из платежей) удалён
2026-09-29 07:49:23 +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
5c68d84cff fix(csp): разрешён core-renderer-tiles.maps.yandex.net (script-src) — Яндекс.Карты 2.1 грузит JSONP hit-test объектов с него, блокировка CSP ломала клики по маркерам 2026-09-23 10:25:27 +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
8d11ad3b33 fix(auth): список сессий без мусора ротаций — фильтр isReplaced, перенос UA/IP, чистка старых
- listUserSessions: + isReplaced = false — иначе каждый рефреш токена добавлял
  в список устройств «новую сессию» (цепочка ротации видна только для детекта кражи);
- rotateFamilySession: userAgent/ipAddress переносятся из исходной сессии —
  rotated-строки больше не «Неизвестное устройство»;
- новый worker session-cleanup (тик раз в сутки + прогон при старте): удаляет
  replaced-сессии старше 72ч (grace-окно детекта кражи) и истёкшие.
2026-09-22 09:01:59 +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
d0d73a59f2 ci(deploy): app-образ собирается в GitHub Actions и пушится в GHCR — сервер только скачивает
Раньше образ собирался на VPS (3 ядра) при каждом деплое: 13–15 минут даунтайма.
Теперь: job build-image (buildx + кэш слоёв GHA) пушит ghcr.io/iisultanov/iistwin-app:latest
и :<sha>; SSH-скрипт скачивает образы ДО остановки контейнеров (даунтайм ~10 секунд
на подмену), fallback на сборку на сервере сохранён при недоступности GHCR.
Инфра-репо: сервису app добавлен image: (iistwin-infra@0960563).
2026-09-21 20:59:31 +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
c957a70260 feat(auth): access 24ч, jti против гонок ротации, транзакционная ротация, отзыв сессий по sid 2026-09-21 18:13:46 +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
006d020d15 fix(fields): миграция 0085 — верное имя unique-констрейнта form_fields_form_id_code_unique 2026-09-21 14:46:57 +03:00