# ТЗ: ordo-desk — мультитенантная система ведения проектов Версия документа: 2026-08-14. Статус: черновик к реализации, дополняется по ходу. Прототип интерфейса (обязателен к просмотру перед чтением): https://ordo-proto.sites.meteor.group ## 1. Что строим Личная и командная система, где **проект — главная сущность**, а всё остальное цепляется к нему: задачи, документы, записи справочников, финансы, доступы. Заменяет Notion + Todoist + Apple Notes. Домен: **ordo.ins.is**. Ключевые свойства: мультитенантность (несколько аккаунтов-контуров), настраиваемые поля и виды баз, markdown-хранение заметок, очень быстрый интерфейс, работа через API (включая агента в телеграме), готовность к PWA и десктопу. ## 1.1 Планка качества **Это не MVP.** Система делается сразу как законченный продукт: выверенный дизайн (см. `03-design-system.md`), обе темы равного качества, мгновенный отклик, рабочая мобильная раскладка, горячие клавиши, поиск. «Потом допилим» не закладывается ни в один экран: если фича не готова — её нет в интерфейсе, а не сделана наполовину. Раздел «Финансы» в прототипе помечен черновиком именно поэтому: он выключен, а не выглядит недоделанным. Практические следствия: - Ни одного экрана без пустого состояния, состояния загрузки и состояния ошибки. - Ни одной формы без валидации и понятного текста ошибки. - Мобильная и десктопная сборки не откладываются «на потом»: архитектура выбирается так, чтобы они получались из того же кода (см. §9). ## 2. Продуктовые принципы 1. **Проект отвечает «про что»**, метка — «в каком состоянии / с кем связано». Метки общие на аккаунт. 2. **Одна запись живёт в одном месте.** Связь — это поле-ссылка, двусторонняя. Копий нет. 3. **Заметка — файл.** Markdown с фронтматтером, зеркало в git; БД хранит индекс и связи. 4. **Никакой линеровщины**: нет вех, спринтов, оценок в часах, story points. 5. **Ничего в обход API.** Интерфейс, агент и скрипты используют один контракт. 6. **Разделы включаются по аккаунту.** Финансы могут быть выключены в одном контуре и включены в другом. 7. **Нет справочника людей и контактов.** Люди — только участники; контрагенты — записи в базе «Юрлица». ## 3. Модель данных Postgres. Именование полей — английское, интерфейс — русский. ### 3.1 Тенанты и люди ``` accounts id, slug, name, icon, color, settings jsonb, created_at users id, email, name, avatar, tz, created_at memberships user_id, account_id, role: owner|member|guest, can_finance bool, created_at -- финансы отдельным флагом sessions id, user_id, expires_at, agent bool -- agent=true для токенов Hermes ``` Все прикладные таблицы несут `account_id`. Ни один запрос не выполняется без него — это дисциплина уровня репозитория, а не «фильтр на всякий случай». ### 3.2 Проекты ``` projects id, account_id, key (короткий код: NARYN), title, description, cover_file_id, color, stage_id, archived bool, owner_id, created_at, updated_at project_fields id, account_id, name, kind: select|multiselect|text|number|date|checkbox|relation|money, options jsonb, position, system bool -- набор полей задаётся на аккаунт project_values project_id, field_id, value jsonb -- страна, индустрия, доля, бюджет… project_members project_id, user_id, role: owner|member|guest ``` Стадия проекта (`stage_id`) — системное поле-выбор со своим набором значений на аккаунт (Подготовка / В работе / Выход / Архив). ### 3.3 Задачи и метки ``` tasks id, account_id, project_id NULL, parent_id NULL, title, description, status_id, due_at, due_all_day bool, remind_at, priority 1..4, assignee_id, position, done_at, created_by, created_at, updated_at task_statuses id, account_id, name, kind: todo|doing|done, position, color labels id, account_id, name, color, pinned bool, position task_labels task_id, label_id task_links task_id, target_type: document|record|project, target_id ``` `project_id NULL` = «Входящие». Подзадачи — `parent_id`, без отдельной сущности «группа задач». Метки закрепляются флагом `pinned` — закреплённые попадают в левое меню и в строку над списком. ### 3.4 Базы (конструктор справочников) Универсальная механика: цели, домены, юрлица, объекты, договоры, платежи — одна и та же схема. ``` databases id, account_id, name, icon, description, system_kind NULL fields id, database_id, name, kind, options jsonb, position, required bool records id, database_id, account_id, project_id NULL, title, created_at, updated_at values record_id, field_id, value jsonb relations id, account_id, from_field_id, to_database_id, symmetric_name record_links from_record_id, to_record_id, relation_id ``` Типы полей: `text | number | money | date | datetime | select | multiselect | checkbox | url | email | phone | file | image | progress | relation | formula(позже)`. Поле `project_id` на записи — системная связь с проектом; именно она наполняет вкладку «Связанные записи» карточки проекта. ### 3.5 Виды (то, что делает базы «как в Notion») ``` views id, database_id, name, kind: table|board|list|gallery|calendar, group_by field_id NULL, -- колонки доски = значения поля filters jsonb, -- [{field, op, value}] sort jsonb, -- [{field, dir}] fields jsonb, -- какие поля показывать в строке/карточке cover field_id NULL, -- поле-картинка под обложку scope: private|account, owner_id NULL, position ``` Требования к поведению: - Переключение вида не меняет данные — только представление. - Доска: колонки по значениям `group_by`, перетаскивание меняет значение поля. - Календарь: раскладка по выбранному полю даты. - Галерея: обложка из поля-картинки, подпись из title + выбранных полей. - Запись открывается **страницей** (свойства сверху, содержимое-документ снизу, связи внизу). - Тот же самый механизм используется блоком «База» внутри документа. ### 3.6 Документы ``` documents id, account_id, project_id NULL, parent_id NULL, title, icon, cover_file_id, path (для файла), kind: page|meeting|spec, created_by, created_at, updated_at doc_blocks id, document_id, position, type, content jsonb -- канон для редактора doc_links document_id, target_type, target_id -- обратные ссылки ``` - Редактор — **BlockNote поверх TipTap** (не пишем свой). - Блоки: заголовки, списки, чек-лист, выноска, код, таблица, картинка, подстраница, **блок «База»** (хранит запрос: database_id + filters + view kind), ссылка на запись. - Чек-лист умеет превращать пункт в задачу проекта (создаётся `tasks` со ссылкой на документ). - **Markdown-зеркало**: `brain///-.md`, фронтматтер с полями, блок «База» сериализуется директивой ```` ```db {json} ```` . Направление одно: система → файлы. ### 3.7 Финансы (заложить сразу, включить позже) Системная база `payments`: `date, direction: in|out, amount, currency, rate_at_date, project_id, category_id, counterparty_record_id, document_id NULL, planned bool`. Плюс `budgets`: `project_id, amount, currency, approved_at`. Ничего специального: та же механика записей, свои виды. Доступ — по флагу `can_finance`. ## 4. Права Два уровня, оба проверяются на сервере: 1. **Аккаунт**: `owner` видит всё, `member` — проекты, где состоит, `guest` — только выданные проекты. 2. **Проект**: `project_members`. Приглашение в проект открывает задачи, документы и связанные записи. Финансы — отдельный флаг `can_finance`, чтобы партнёр видел стройку, но не движение денег. Записи баз наследуют доступ проекта, к которому привязаны; записи без проекта видны всем участникам аккаунта. ## 5. Поиск Требование: «сильный поиск по содержимому». - **Этап 1 — Postgres FTS.** `tsvector` по задачам, документам (плоский текст блоков), записям баз и названиям проектов; словари `russian` + `simple`, расширения `unaccent` и `pg_trgm` для опечаток и подстрок. Одна вью `search_index(account_id, kind, id, title, body, tsv)`, обновляется триггером. - **Этап 2 — Meilisearch** (не Elasticsearch), если FTS перестанет хватать: он на порядок легче, умеет опечатки и мгновенную выдачу из коробки, ставится одним контейнером. Elasticsearch уже крутится на этом сервере ради Huly и жрёт 1,2 ГБ — второй такой не нужен. - UI: `⌘K` — поиск по всему аккаунту с группировкой по типу, стрелки + Enter, недавнее сверху. - Поиск обязан уважать права: индекс несёт `account_id` и `project_id`, фильтрация до выдачи. ## 6. Горячие клавиши | Клавиши | Действие | |---|---| | `⌘K` / `Ctrl+K` | Глобальный поиск и переход | | `Q` | Быстрая задача из любого места (композер) | | `A` | Добавить задачу в текущий список | | `G` затем `P` / `T` / `D` / `B` / `F` | Переход: Проекты / Задачи / Документы / Базы / Финансы | | `X` | Отметить задачу выполненной | | `E` | Редактировать выделенную задачу | | `1…4` | Приоритет выделенной задачи | | `⌘Enter` | Сохранить и закрыть | | `Esc` | Закрыть модалку / снять выделение | | `⌘\` | Свернуть боковую панель | | `/` | Меню блоков в редакторе | | `?` | Шпаргалка по клавишам | Реализация: один менеджер горячих клавиш со стеком контекстов (глобальный → экран → модалка), чтобы модалка перехватывала `Esc` и `⌘Enter`, не ломая остальное. ## 7. API и агент Hermes **API-first.** Интерфейс не ходит в базу напрямую. Контракт: JSON, `/api/v1/...`, авторизация сессией (веб) или токеном (агент). Токен несёт scope: `read`, `tasks:write`, `docs:write`, `finance:read`. Минимальный набор ресурсов: `accounts`, `projects`, `tasks`, `labels`, `documents`, `databases`, `records`, `views`, `search`. **MCP-сервер** — тонкая обёртка над тем же API, инструменты: ``` projects.list(account, filters) project.status(project) tasks.search(query|filters) task.create(title, project, due, labels) task.move(task, status|project) task.complete(task) note.append(document, text) search.everything(query) record.create/update(database, values) finance.summary(project) # если scope позволяет ``` Тогда Hermes в телеграме работает без отдельного бэкенда: «что по Нарыну» → `project.status`, «перекинь смету на бурение в работу» → `task.move`. Опыт есть — MCP-сервер Nurion уже в проде (`apps/nurion-projects`), включая OAuth; ловушка оттуда: **сообщения об ошибках только ASCII**. ## 8. Разбор строки задачи Один парсер обслуживает три входа: композер в интерфейсе, режим «разобрать текст», команды агента. Понимает: `завтра`, `в пн`, `15 авг 11:00`, `через 3 дня`, `каждый вторник` (позже), `@метка`, `!1..!4` или `!важно`, `#проект`, `+исполнитель`. Разобранное подсвечивается прямо в строке и показывается чипами под ней — как в прототипе. ## 9. Производительность: что берём у Huly Huly ощущается быстрым не случайно — я разобрал её архитектуру на нашей же инсталляции `ordo` (v0.7.426). Четыре приёма стоит скопировать, три — сознательно не копировать. **Берём:** 1. **Отсечение прав одним индексным условием.** В `foundations/server/packages/middleware/src/spaceSecurity.ts` транзактор держит карты пространств и участников в памяти и подставляет в каждый запрос `space: {$in: [...]}` — проверка доступа стоит один `IN` по индексу, а не построчный предикат. Наш аналог: любой запрос сужается по `(account_id, project_id)` с составным индексом, права считаются один раз при входе и живут в памяти процесса. 2. **Живые запросы вместо перезапросов.** Клиент Huly держит результат выборки в памяти и применяет к нему приходящие транзакции — список не перезагружается никогда. Наш аналог: WebSocket-канал с дельтами (`entity, op, payload`), клиент патчит нормализованный кэш. Именно отсюда ощущение мгновенности, а не из «быстрого бэкенда». 3. **Журнал изменений как основа синхронизации.** У Huly всё построено на append-only журнале транзакций. Нам не нужен event sourcing целиком, но нужна таблица `changes(seq, account_id, entity, id, op, ts)`: клиент хранит `last_seq` в IndexedDB и при открытии тянет только дельту. Это же бесплатно даёт оффлайн и холодный старт «из кэша за 100 мс». 4. **Справочная модель грузится один раз.** Поля, статусы, метки, базы, виды — небольшой словарь, который клиент получает при подключении и кэширует. Huly так грузит модель; их же грабли: модель раздувается — держим словарь в десятках сущностей, не в сотнях. 5. **Адресная рассылка.** События уходят только участникам проекта, получатели вычисляются по членству в памяти — без широковещания на весь аккаунт. **Не берём:** CRDT-совместное редактирование (Y.js + отдельный сервис), Kafka/Redpanda для асинхронной индексации, отдельный fulltext-сервис на Elasticsearch. Для трёх человек это пятнадцать контейнеров ради задачи, которую решают Postgres и один процесс. ## 9.1 PWA, мобильное и десктоп - **Клиентское приложение** на JSON API (не серверный рендер страниц). Это отличается от `apps/nurion-projects` и выбрано сознательно. - **Кэш в IndexedDB** + оптимистичные обновления: отметка задачи и перетаскивание карточки применяются мгновенно, синхронизация фоном, откат при ошибке. - Пагинация и виртуализация длинных списков; запрос отдаёт только поля вида. - HTTP-кэш с ETag, `stale-while-revalidate`; ассеты с версией в имени. - **PWA**: service worker, оффлайн-оболочка, установка на iPhone — мобильное приложение без отдельной разработки. - **Десктоп — Tauri**: тот же фронт, бинарник под `aarch64-apple-darwin` собирается на маке владельца, вес около 10 МБ против сотни у Electron. Electron — запасной вариант, если понадобятся ноутбучные API. - Бюджет отклика: открытие экрана из кэша < 100 мс, любое действие пользователя < 50 мс до отклика UI. **Что закладывается в код сразу, чтобы мобильное и десктопное приложения получились без переписывания:** | Решение | Зачем | |---|---| | Весь доступ к данным — через слой API-клиента, ни одного прямого запроса из компонента | Тот же клиент используют PWA, Tauri и агент | | Адрес сервера — параметр конфигурации, не константа | Десктоп-сборка подключается к своему серверу | | Авторизация токеном в защищённом хранилище, а не только cookie | В Tauri и в мобильном контексте cookie ведут себя иначе | | Роутинг на History API без серверных зависимостей | Оффлайн-навигация в оболочке | | Все размеры в относительных единицах, безопасные зоны (`env(safe-area-inset-*)`) | Айфон с чёлкой, полноэкранный десктоп | | Никаких `hover`-only взаимодействий | На тач-экране их не существует; ручка блока и меню доступны по нажатию | | Service worker с версионированным кэшем оболочки | Установка на домашний экран, запуск без сети | | Файлы и картинки — через абстракцию хранилища | На десктопе появится локальный кэш вложений | | Иконки приложения и splash в двух темах | Требование магазинов и macOS | ## 10. Стек TypeScript везде. Сервер: Node + Fastify, Postgres, drizzle или kysely. Клиент: React (BlockNote — React-компонент) + Vite, состояние — TanStack Query поверх собственного кэша. Редактор: BlockNote/TipTap. Поиск: Postgres FTS → Meilisearch. Десктоп: Tauri. MCP: официальный SDK. Деплой: Docker + Traefik на этом сервере, домен ordo.ins.is. ## 11. Экраны Все есть в прототипе, каждый со встроенными пояснениями: 1. **Проекты** — галерея / таблица / доска, фильтры по полям аккаунта, сохранённые виды. 2. **Проект** — шапка с полями, вкладки: Обзор · Задачи · Документы · Связанные записи · Финансы · Доступ. 3. **Задачи** — «Сегодня» со всеми проектами аккаунта, закреплённые метки, панель меток, композер. 4. **Документы** — дерево страниц, блочный редактор, slash-меню, блок «База», обратные ссылки. 5. **Базы** — список баз, таблица записей с группировкой, схема полей и связей. 6. **База с видами (Цели)** — доска по сферам, таблица, календарь дедлайнов, галерея, панель настройки вида. 7. **Финансы** — сводка аккаунта, бюджеты и освоение, движение денег в проекте. 8. **Настройки аккаунта** — поля проектов, участники, экспорт в markdown, включение разделов. ## 12. Порядок работ | Фаза | Содержание | Оценка | |---|---|---| | 0 | Каркас: репозиторий, Postgres, аутентификация, аккаунты и участники, деплой на ordo.ins.is | 3–4 дня | | 1 | Проекты: поля аккаунта, CRUD, галерея/таблица/доска, фильтры, сохранённые виды | 1 неделя | | 2 | Задачи: статусы, метки с закрепом, «Сегодня», доска проекта, композер + парсер строки, шорткаты | 1,5 недели | | 3 | Документы: BlockNote, дерево, markdown-зеркало в git, обратные ссылки | 1,5 недели | | 4 | Базы: поля, записи, связи, виды (таблица/доска/галерея/календарь), блок «База» в документе | 2 недели | | 5 | Поиск ⌘K, PWA, кэш и оптимистичные обновления | 1 неделя | | 6 | API-токены + MCP-сервер, подключение Hermes | 4–5 дней | | 7 | Финансы: бюджеты, платежи, сводка, флаг доступа | 1 неделя | | 8 | Tauri-сборка под Apple Silicon | 2–3 дня | Порядок фаз 0–2 даёт работающую замену Todoist примерно за три недели; после фазы 3 можно переносить заметки; после 4 — закрывать Notion. ## 13. Открытые вопросы 1. Валюты в финансах: хранить исходную + курс на дату или приводить всё к $? 2. Повторяющиеся задачи — нужны ли и в каком виде (`каждый вторник`)? 3. Нужен ли календарь встреч отдельным разделом или хватит вида «календарь» у баз? 4. Гостевые ссылки на отдельный документ наружу — нужны ли? 5. Двусторонняя синхронизация markdown (файл → система) — оставляем выключенной?