- 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)
- шапка: [&_th]:bg-background вместо полупрозрачного bg-muted/50 — иначе сквозь
sticky-шапку проступают строки и она нечитаема (в plain-варианте фона не было вовсе)
- ресайз: table-fixed + w-full таблицы + w-max wrapper давали циклическую зависимость
ширин — колонки без заданной ширины схлопывались до нуля при первом drag.
В auto-раскладке <colgroup> задаёт предпочтительную ширину без схлопывания
check/build чисто, vitest 129/129
- 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
На мобильном (<768px) main#content был блочным: flex-1 обёртки PageTransition
работал вхолостую, высота страницы росла по контенту, весь скролл уходил в main,
а скроллбар широкой таблицы реестра оказывался внизу недостижимой высоты.
Десктопный main уже был flex flex-col (Sidebar.tsx:1687).
PLAN-user-password-fixes.md (инцидент 2026-09-07, user 55/56):
- toServiceError в client auth.service: BadRequestError → {success:false, error}
- полная клиентская валидация пароля в ProfilePasswordRow
- опциональный пароль при создании пользователя (UserModal + POST /api/users)
- onError с toast в PollCard/ReactionBar (этап 4)
- 11 новых тестов (129/129)
Убраны git stash кастомизаций и восстановление compose из бэкапа вне репо.
Источник правды для прод-compose: iisultanov/iistwin-infra (приватный),
клонируется в /opt/crm-infra deploy-ключом при каждом деплое
Шаг 0.1 (подготовка) плана production-готовности:
pg-драйвер возвращает name[] как "{a,b,c}" (нет парсера OID 1003) —
проверка находила 0 таблиц и запуск с ENABLE_RLS=true всегда падал.
Поддержаны оба формата. Проверено на копии прод-БД: app стартует,
31 таблица ENABLE+FORCE, login/search/sync работают
Шаг 2.5 плана production-готовности:
- страница уведомлений (фильтр, догрузка, прочтение) + ссылки из dropdown/мобильного меню
- общие хелперы notification-utils (дедупликация dropdown)
- skip-link + id=content на main
- axe smoke-тесты (0 critical/serious), исправлены 2 нарушения в новой странице
- 3 новых теста (107/107), i18n отложен по решению пользователя
Шаг 2.1 плана production-готовности:
- единый 401 при любом отказе логина + constant-time bcrypt
- forgot-password: идентичный ответ независимо от существования email
- sanitizeReturnTo (open redirect fix в /api/documents/generate-link)
- 10 неудачных попыток → блок 15 мин (in-memory, аудит)
- DATABASE_SSL_REJECT_UNAUTHORIZED env (дефолт true)
- доделка 0.7: статичные тексты в llm-providers/rag/finance-di2
- 8 новых тестов (104/104)