- Динамика НДС (месяц): поверх столбцов «к уплате за месяц»/«накоплено за квартал»
добавлены линии «Начислено» и «Вычет»
- Трекер авансовых УПД: без лимита 15 (все ждущие), настоящая table с фиксированными
колонками (table-fixed, truncate+title), видимы первые ~5 строк, дальше скролл
внутри блока, шапка прилипает (sticky)
- Контроль квартала: главная таблица и «Показать все» — то же оформление table + скролл
- Новый 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 для таблиц
Живой тест показал: 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 и локальном режимах.
Живой тест показал: 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.
substring(url FROM '^.*\/api/files/') в PG возвращает СОВПАВШУЮ часть (префикс
URL), а не остаток — backfill писал бы мусорный file_key. regexp_replace
проверен на проде: ключ извлекается корректно из абсолютных и относительных URL.
Добавлен DELETE file_uploads с file_key LIKE '%/%' (ключи слэшей не содержат).
Проблема: прямое скачивание /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).
- summary: без фильтров дат период = весь доступный диапазон nds_monthly (с 2025-01)
- «К уплате» теперь всегда квартальный: квартал выбранного периода, если он внутри одного
квартала, иначе квартал последнего месяца в данных (НДС не копится между кварталами)
- карточки KPI: «Начислено»/«Вычет» за период с подписью, «К уплате» с меткой квартала
- listUserSessions: + isReplaced = false — иначе каждый рефреш токена добавлял
в список устройств «новую сессию» (цепочка ротации видна только для детекта кражи);
- rotateFamilySession: userAgent/ipAddress переносятся из исходной сессии —
rotated-строки больше не «Неизвестное устройство»;
- новый worker session-cleanup (тик раз в сутки + прогон при старте): удаляет
replaced-сессии старше 72ч (grace-окно детекта кражи) и истёкшие.
useCallback для ChatSection стоял после раннего return (if (!task)) —
в рендерах без задачи хук не вызывался → «Rendered more hooks than during
the previous render». Правильное решение: компонент на module level с пропом
taskId (стабильный тип + нет зависимости от хуков).
- TaskDetail: ChatSection объявлен inline в теле рендера → новый тип компонента
на каждый рендер → React перемонтировал весь чат при ЛЮБОМ ре-рендере TaskDetail
(SSE-событие, refetch задачи, возврат на вкладку): скролл сбрасывался вверх,
пользователь видел «старые сообщения → прыжок к последним». Фикс: useCallback
со стабильной идентичностью (тот же паттерн, что у мобильного accordion).
- TaskChat/useChatController: скролл к низу при открытии чата — useLayoutEffect
СИНХРОННО ДО paint (первый кадр уже у низа) + докрутка первого кадра, когда
сообщения пришли после монтирования без кэша.
Раньше образ собирался на 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).
- SSE: серверный heartbeat теперь именованное событие ping (25с) — клиент может отличить живое соединение от мёртвого; клиентский watchdog (45с без активности → принудительный reconnect), reconnect 1с вместо 3с, polling-фолбэк 5с вместо 10с
- Скролл: на скрытой вкладке скролл не трогаем (раньше автоскролл «выбрасывал» пользователя вниз при возврате); возврат на вкладку — pin к низу если был у низа; при чтении истории плавающий индикатор «↓ N новых» (оба чата)
- Персистентный кэш последних 100 сообщений чата в localStorage → initialData: мгновенный рендер после перезагрузки страницы без «Загрузка сообщений...», свежие данные в фоне; gcTime: Infinity
- SW: reload на controllerchange только на скрытой вкладке (обновление незаметно), предыдущая версия кэша сохраняется для ленивых чанков старой страницы; кэш v15
- 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
- /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-бакет офиса)
Каждая точка ingest (~1/сек) broadcast'илась SSE всем клиентам орг., клиент
на каждое событие делал invalidateQueries без throttle → 2+ GET/сек → исчерпание
per-user бакета apiLimiter (1500/15мин) → все API устройства 429 (формы не грузились).
Модель теперь как у нормальных трекеров: точки пишутся в БД, карта читает по интервалу.
+4 unit-теста троттла (server/gps/sse-throttle.ts)