perf(tasks): лимиты/пагинация /api/tasks и дельта-синк в SQL

Шаги 0.10 и 0.9 плана production-готовности:
- дефолт limit 50, кап 200; списковые ответы без description
- курсорная пагинация (updated_at, id) + nextCursor
- since-фильтр дельта-синка перенесён в SQL (updated_at >= since)
- embedding pre-count через COUNT(*) вместо выгрузки миллиона строк
- 11 новых юнит-тестов (56/56 зелёные)

Инвариант поиска сохранён: search-ветка /api/tasks?search= не изменена
This commit is contained in:
2026-09-07 22:01:25 +03:00
parent 0a60c6abf1
commit a27766a38b
11 changed files with 275 additions and 40 deletions

View File

@@ -115,3 +115,28 @@
- Как проверялось: `docker run --entrypoint id` → uid=1001(app); `/app/data` и `/tmp` доступны на запись от app; document-worker на проде поднялся и healthy сразу.
- Влияние на поиск/UX: нет.
- Подводные камни: named volume `crm_crm_data` на проде был root-owned — на сервере заранее выполнен `chown -R 1001:1001` на `crm_crm_data` и `crm_crm_uploads` (иначе app не смог бы писать в /app/data). При развёртывании на новых серверах — учитывать.
---
## [0.10] Лимиты и пагинация в /api/tasks
- Статус: ✅ done
- Зачем: листинг не отдаёт до 10000 полных строк (аудит, Фаза 0).
- Что изменено:
- `server/utils/task-pagination.ts` — `clampTasksLimit` (дефолт 50, кап 200), курсор `<updatedAt>_<id>` (parse/build).
- `server/storage/tasks-core.storage.ts` — дефолт limit 50/кап 200 на уровне storage; опция `excludeDescription` (списковые ответы без тяжёлой text-колонки); курсорная пагинация `(updated_at, id)` с тай-брейкером; новый `getTasksCountByOrganization`.
- `server/routes/task-crud-list.routes.ts` — кап на роуте, параметр `cursor`, в ответе `nextCursor`; ключ minimal-кэша включает limit/курсор.
- Caller'ы: MCP search_tasks limit 500→200; embedding.service — pre-count через `getTasksCountByOrganization` вместо выгрузки миллиона строк; ShareTarget.tsx — явный `?limit=200`.
- `tests/task-pagination.test.ts` — 11 юнит-тестов (кламп, курсор, round-trip).
- Как проверялось: `.toSQL()` — курсор и тай-брейкер корректны, списковый SELECT без description; `npm run check` чисто; `npx vitest run` 56/56.
- Влияние на поиск/UX: search-ветка не изменена (SQL, лимиты, формат) — инвариант сохранён. Дельта: список без параметров — 50 строк вместо 10000, без description; кастомные JS-виджеты (Bots.tsx, CustomPageView) получают задачи без description (при необходимости дотягивают деталь).
- Подводные камни: `/api/forms/:id/tasks` и `/with-fields` НЕ тронуты (там description остаётся — реестр/канбан/Гантт используют их).
## [0.9] Дельта-синхронизация: фильтр на уровне SQL
- Статус: ✅ done
- Зачем: sync не выгружает все задачи всех форм (аудит, Фаза 0).
- Что изменено: `getTasksWithFieldsByFormOptimized` и `getUsersByOrganization` принимают `since?: Date` → `WHERE updated_at >= since OR updated_at IS NULL` (паритет со старым JS-фильтром); JS-фильтры в `sync.routes.ts` удалены; initial sync не тронут.
- Как проверялось: `.toSQL()` — условие в SQL; индекс `tasks_form_updated_idx` (0079) покрывает; vitest 56/56.
- Влияние на поиск/UX: нет.
- Подводные камни: `OR IS NULL` обязателен — старые строки с NULL updated_at должны попадать в дельту (паритет с `!t.updatedAt ||` в JS).