# ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB Версия: 1.5 (актуальная реализация: выполнены задачи #12–#17) ## 1. ЦЕЛИ И НАЗНАЧЕНИЕ EventHub — платформа для управления событиями с поддержкой календарей, записи участников (включая специалистов), гибкого подтверждения, рейтингов, отзывов, модерации, встроенного баг-трекера и платной подписки. Целевая аудитория: владельцы календарей (бизнес), участники (клиенты), администраторы. ## 2. ФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ ### 2.1. Календари - CRUD календаря (название, описание, теги, владелец) - Расшаривание по ссылке (публичная/приватная) - Приглашение пользователей с правами "запись" или "администрирование" - Типы календарей: personal (бесплатный, без записи), commercial (платный, запись клиентов, специалисты) - Гибкое подтверждение заявок: auto (автоматически), manual (вручную), timeout (авто через N секунд) - Теги календаря, рейтинг (средняя оценка, количество голосов) **Новые поля (задача #12):** - `short_name` — короткое уникальное имя для API и поиска - `category` — категория (enum) - `color` — цвет отображения - `image_url` — изображение календаря - `settings` — дополнительные настройки (map) ### 2.1.1. Специалисты календаря (задача #12) Реализована отдельная таблица `calendar_specialist`, связывающая пользователя (специалиста) с календарём. Специалист может иметь отображаемое имя (`name`) и список специализаций (`specialization`). Статус специалиста: `active` | `inactive`. ### 2.2. События (расширенная версия с повторяющимися событиями) #### 2.2.1. Типы событий и модель хранения События могут быть одиночными (`event_type = single`) или повторяющимися (`event_type = recurring`). Для повторяющихся событий: - Мастер-событие содержит правило повторения (`recurrence_rule`) и является шаблоном. - При создании повторяющегося события генерируются экземпляры (instances) на определённый период (например, на месяц вперёд), которые хранятся как отдельные записи с полем `is_instance = true` и ссылкой на мастер (`master_id`). - При изменении мастера можно выбрать обновление всех будущих экземпляров или только мастер-записи. - При удалении мастера удаляются все связанные экземпляры. - Для поддержки исключений (отмена отдельного вхождения) используется таблица `recurrence_exception`. #### 2.2.2. Правила генерации вхождений при поиске При поиске событий на заданный диапазон дат система должна: - Включать все одиночные события, попадающие в диапазон. - Для повторяющихся событий генерировать виртуальные вхождения на основе `recurrence_rule`, исключая те, что помечены как исключения. - Возвращать как одиночные, так и сгенерированные вхождения в едином списке. #### 2.2.3. Материализация при записи участника При записи участника на конкретное вхождение повторяющегося события: - Система материализует (создаёт) физическую запись события для этого вхождения, если оно ещё не было материализовано (например, для хранения количества записавшихся). - Материализованное событие имеет `is_instance = true` и ссылается на `master_id`. - Запись участника (`booking`) всегда привязывается к конкретному экземпляру (материализованному или одиночному событию). #### 2.2.4. Изменение и удаление серий - При редактировании мастера можно применить изменения ко всем будущим экземплярам или создать новый мастер с отдельной серией. - При удалении мастера удаляются все связанные экземпляры, если на них нет активных записей. Если есть активные записи, мастер-событие помечается как `cancelled`, а существующие записи остаются. #### 2.2.5. Структура записей (records.hrl) Актуальная структура записей включает дополнительные поля, добавленные в рамках задачи #12: - `event` — добавлены `attachments :: [binary()] | undefined`, `edit_history :: [map()] | undefined` - `booking` — добавлены `notes :: binary() | undefined`, `reminder_sent :: boolean()` - `review` — добавлены `likes :: non_neg_integer()`, `dislikes :: non_neg_integer()`, `edited_at :: calendar:datetime() | undefined` - `review_vote` — голос пользователя за отзыв: `id`, `review_id`, `user_id`, `value :: like | dislike`, `created_at`, `updated_at` (уникальность пары review+user) #### 2.2.6. Требования к реализации - Все операции с событиями должны быть транзакционными. - Генерация вхождений должна быть эффективной (использовать `calendar:datetime_to_gregorian_seconds` и кэширование). - При поиске событий для календаря учитывать права доступа пользователя. ### 2.3. Запись участников и подтверждение - Пользователь может отправить заявку на участие в событии. - В зависимости от `confirmation` календаря заявка либо подтверждается автоматически, либо ожидает ручного подтверждения владельцем, либо подтверждается по таймауту. - Бронирование имеет статусы: `pending`, `confirmed`, `cancelled`. - Пользователь может отменить свою запись. - Владелец календаря может подтвердить или отклонить заявку. - При подтверждении фиксируется время (`confirmed_at`). - Вместимость события (`capacity`) ограничивает количество подтверждённых записей. ### 2.4. Отзывы и рейтинги - Пользователи могут оставлять отзывы (рейтинг 1–5 и комментарий) на события или календари. - Отзыв можно редактировать (сохраняется `edited_at`). - Отзывы могут быть скрыты модератором. - При добавлении/изменении/удалении отзыва пересчитывается средний рейтинг события (`rating_avg`, `rating_count`). - Голосование за отзывы (user API, JWT): - таблица `review_vote` (один голос на пару `review_id` + `user_id`: `like` | `dislike`); - счётчики `likes` / `dislikes` на `review` обновляются в одной транзакции с голосом; - `PUT /v1/reviews/:id/vote` body `{"value":"like"|"dislike"}` — поставить/сменить (идемпотентно при том же value); - `DELETE /v1/reviews/:id/vote` — снять голос (идемпотентно); - нельзя голосовать за свой отзыв (`403`); скрытый/удалённый/отсутствующий → `404`; - в ответах `GET /v1/reviews`, `GET /v1/reviews/:id` (и при сериализации review) — поле `my_vote`: `"like"` | `"dislike"` | `null`. - Возможность пожаловаться на отзыв (создание `report`). ### 2.5. Поиск и фильтрация - Полнотекстовый поиск по названиям событий, календарей, тегам. - Фильтрация по дате, категории, местоположению, рейтингу. - Пагинация результатов. - Поиск должен учитывать права доступа (не показывать скрытые/заблокированные календари). ### 2.6. Расширенные возможности - Локация события (`location` запись с адресом, широтой, долготой). - Онлайн-ссылка (`online_link`). - Вложения к событию (`attachments`). - История изменений события (`edit_history`). - Заметки пользователя к бронированию (`notes`). - Напоминания о событии (поле `reminder_sent` в бронировании, логика отправки не реализована). ### 2.7. Модерация и безопасность - Пользователи могут отправлять жалобы (`report`) на календари, события, отзывы. - Модераторы могут просматривать жалобы и принимать меры (скрывать контент, блокировать). - Список запрещённых слов (`banned_word`) для фильтрации контента. - **Автомодерация** (`logic_automoderation`): - Настройки (`automod_settings`, singleton): `keyword_action` (`reject` | `flag` | `censor`, default `flag`), `match_mode` (`word_boundary` | `substring`, default `word_boundary`), `report_threshold` (integer ≥ 1, default `3`). - При create/update calendar (`title`, `description`), event (`title`, `description`), review (`comment`) выполняется scan бан-слов. - `reject` — сохранение отклоняется (`400`, content banned); `censor` — маскирование `***` + hit в очередь; `flag` — сохранение + soft-hide + hit (`review` → `hidden`, `event`/`calendar` → `frozen`, reason `auto:keyword`). - Порог жалоб: считаются только `pending`; при достижении порога — soft-hide (`auto:report_threshold`) + hit; dismissed/reviewed не учитываются. - Очередь `automod_hit`: `open` | `approved` | `rejected`; approve восстанавливает сущность, reject оставляет/усиливает блокировку. - WS админам: `automod_hit`; аудит от системного актора `system`. - Аудит действий администраторов (`admin_audit`). ### 2.8. Баг-трекер (автоматический) - Источники тикетов (`source`): `backend` (HTTP 500 / внутренние ошибки), `frontend` (клиентский crash), `manual` (ручной репорт). - При HTTP 500 сервер асинхронно регистрирует тикет через `logic_ticket:report_error` (ответы по путям `/tickets` не репортятся — защита от рекурсии). - Клиент и ручной репорт: `POST /v1/tickets` с `error_message`, опционально `stacktrace`, `context`, `source` (`frontend` | `manual`). Ручной `manual` — сценарий end-user клиентов, не Admin SPA. - Тикет содержит `error_hash` (sha256 от source + сообщение + fingerprint стека; для `manual` — source + user_id + сообщение), стектрейс, JSON-контекст, счётчик повторений. - Дедуп: открытый тикет (`open` / `in_progress`) с тем же `error_hash` — увеличивается `count` и `last_seen`; после `resolved`/`closed` повтор создаёт новый тикет (регрессия). - Новые тикеты лимитируются (~30/мин на узел); повторы по hash не ограничиваются. - При создании нового тикета администраторам уходит WS `ticket_created`. - Администраторы могут просматривать тикеты (фильтр `source`), назначать ответственных, менять статус. ### 2.9. Платная подписка - Пользователи могут оформить подписку (`subscription`) с разными планами: monthly, quarterly, biannual, annual. - Статус подписки: `active`, `expired`, `cancelled`. - Отслеживание использования пробного периода (`trial_used`). ### 2.10. Административная панель - Управление пользователями (просмотр, блокировка, изменение ролей). - Просмотр статистики платформы. - Управление жалобами и тикетами. - Управление подписками. - Аудит действий администраторов. #### 2.10.1. Ролевая модель администраторов - `superadmin` — полный доступ, может управлять другими администраторами. - `admin` — управление пользователями, контентом, подписками. - `moderator` — модерация контента (жалобы, отзывы, очередь автомодерации). - `support` — работа с тикетами. #### 2.10.2. Эндпоинты для управления ролями и аудитом - `GET /v1/admin/me` — информация о текущем администраторе. - `GET /v1/admin/admins` — список администраторов (только для superadmin). - `POST /v1/admin/admins/:id` — изменение роли администратора (superadmin). - `GET /v1/admin/audit` — просмотр аудита. #### 2.10.3. Статистика для дашборда с учётом ролей - `GET /v1/admin/stats` — агрегированная статистика дашборда (totals, by_day, breakdowns по роли). - Section endpoints: `GET /v1/admin/{users|events|calendars|reviews|reports|tickets|subscriptions}/stats`. - Источник агрегатов: `stats_collector` (Mnesia `detailed` subscribe) → ETS → upsert в `stats_counter` / `stats_daily` (#42, развитие #16). - Totals (`*_total`) — `mnesia:table_info(size)` где достаточно; by_status / by_role / by_type / by_day — из счётчиков. - Tops (все 9 полей section stats) — ETS `stats_tops` (#45, #46); avg resolution — running sum/count (#44); per-admin — индексы (#43). - Retention дневных бакетов: 730 дней. При старте — load из Mnesia или backfill. ### 2.11. Real-time уведомления (WebSocket) - Пользовательский WebSocket (порт 8081): подписка на обновления событий календаря. - Административный WebSocket (порт 8446): подписка на новые жалобы и тикеты. - Создана таблица `notification` для хранения уведомлений (тип, заголовок, тело, флаг прочитано). Полноценная логика доставки уведомлений будет реализована позже. ### 2.12. Инфраструктура развертывания (Docker Compose) - Docker Swarm с тремя репликами `eventhub-node{1..3}`. - Traefik в качестве reverse-прокси и балансировщика. - Prometheus для сбора метрик, Grafana для визуализации. - Observer Web для мониторинга Erlang-узлов. - Автоматическая ротация логов. ## 3. НЕФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ ### 3.1. Производительность и масштабирование - Поддержка 100 000+ пользователей. - Горизонтальное масштабирование добавлением новых узлов. - Все персистентные таблицы хранятся в `disc_copies` на каждом узле (задача #13). - Сессионные таблицы: `auth_session` (единая модель refresh JWT) в `disc_copies` для user и admin API. - Legacy-таблицы `session` и `admin_session` (`ram_copies`) сохранены в схеме, user/admin hot path используют `auth_session`. - Индексы созданы для часто запрашиваемых полей (calendar_id, start_time, event_type, status и др.) (задача #13). - Полная репликация горячих таблиц между всеми узлами кластера (задача #14). - Автоматическое обнаружение узлов через DNS-имя `eventhub-node` или статический список (задача #14). - Периодическая очистка «мёртвых» узлов из схемы Mnesia (каждые 30 секунд) (задача #14). - Архивирование исторических данных (старше 30 дней) в отдельные Mnesia-узлы с `disc_only_copies` (задача #15). - Пагинация всех списков. ### 3.2. Надёжность - Супервизорное дерево OTP. - Философия "let it crash" — быстрый перезапуск упавших процессов. - Автоматическое восстановление после падения ноды (Mnesia). - Валидация входных данных и единый формат ошибок. - Механизм миграций схемы данных: миграции в `src/migrations/`, реестр в `migration_engine`; при старте после init_tables вызывается `ensure_applied/0` с глобальной блокировкой в Mnesia — один узел применяет pending, join-узлы ждут синхронизации; поддерживается откат (задачи #17, #24). ### 3.3. Безопасность - JWT-аутентификация с разделением ролей (user, admin, superadmin, moderator, support). - `EVENTHUB_ENV`: `dev` — допускаются documented fallbacks; `stage`/`prod` — fail-fast при слабых или отсутствующих `JWT_SECRET`, `ADMIN_JWT_SECRET` и паролях seed-админов (#25). - Проверка прав доступа к календарям и событиям. - Пароли хэшируются с использованием Argon2. - Защита от несанкционированного просмотра архивных данных (только владелец календаря) (задача #15). ### 3.4. Наблюдаемость - Логирование в JSON-формате. - Экспорт метрик для Prometheus (HTTP-эндпоинт `/metrics`). - Встроенный Observer Web для мониторинга Erlang-системы. - Сбор admin-статистики через триггеры Mnesia (`detailed` subscribe): upsert-счётчики `stats_counter` и дневные бакеты `stats_daily` (#42; ранее пробный append-only `stats` в #16). ### 3.5. CI/CD - Контейнеризация (Docker). - Makefile для автоматизации задач. - Возможность развёртывания в Kubernetes (в будущем). ## 4. СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ) - Erlang/OTP 28 - Mnesia (встроенная БД) - Cowboy 2.12 (HTTP-сервер) - JWT (jose 1.11.10) - Prometheus (prometheus 4.11.0, prometheus_cowboy 2.1.0) - Docker Compose v3.8 - Traefik v3.1 - Grafana 11.2 - Prometheus 2.55 - Observer Web - Logrotate ## 5. ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА ``` src/ ├── core/ — бизнес-логика (core_user, core_event, core_booking, core_review, ...) ├── handlers/ — обработчики HTTP (handler_login, handler_calendar_view, ...) ├── infra/ — инфраструктура (infra_mnesia, infra_sup, cluster_discovery, │ archive_manager, archive_controller, stats_collector, │ migration_engine, ...) ├── archive/ — архивирование и рендеринг (archive_controller, archive_manager, │ calendar_html_renderer, archive_fetcher) ├── migrations/ — файлы миграций └── eventhub_app.erl — точка входа приложения ``` ## 6. ОСНОВНЫЕ API (КРАТКО) ### Пользовательские (порт 8080) - `GET /health` — health + build identity: `status`, `service`, `version`, `build`, `git_sha`, `built_at` (как admin health). - `POST /v1/register` — регистрация. - `POST /v1/login` — вход. - `POST /v1/refresh` — обновление токена. - `GET /v1/user/me` — профиль пользователя. - `PATCH /v1/user/me` — частичное обновление своего профиля: `language` (`ru`|`en`), `nickname`, `timezone`, `phone`, `avatar_url`, `preferences`; смена пароля — пара `current_password` + `password` (неверный текущий → `403`). Нельзя менять email/role/status; неизвестные поля → `400`. Ответ — полный профиль как GET. - `GET /v1/user/bookings` — бронирования пользователя. - `GET /v1/user/reviews` — отзывы пользователя. - `GET /v1/search` — поиск. - `GET /v1/calendars` — список календарей. - `GET /v1/calendars/:id` — календарь. - `GET /v1/calendars/:calendar_id/events` — события календаря. - `GET /v1/events/:id` — событие. - `GET /v1/events/:id/occurrences` — вхождения повторяющегося события. - `POST /v1/events/:id/bookings` — запись на событие. - `GET /v1/bookings/:id` — статус бронирования. - `POST /v1/reviews` — создать отзыв. - `GET /v1/reviews` — список отзывов (поле `my_vote` для текущего пользователя). - `GET /v1/reviews/:id` — отзыв по ID (`my_vote`). - `PUT /v1/reviews/:id` — обновить отзыв. - `PUT /v1/reviews/:id/vote` — поставить/сменить голос (`{"value":"like"|"dislike"}`). - `DELETE /v1/reviews/:id/vote` — снять голос. - `POST /v1/reports` — пожаловаться (`target_type`: `event` | `calendar` | `review`). - `GET /v1/tickets` — тикеты пользователя. - `POST /v1/tickets` — создать или обновить тикет (дедуп по `error_hash`): body `{ error_message, stacktrace?, context?, source?: frontend|manual }`; ответ 201 (или 429 при rate-limit новых). - `GET /v1/tickets/:id` — статус тикета. - `GET /v1/subscription` — подписка пользователя. - `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — HTML-календарь (владелец), включая архив. ### Административные (порт 8445) - `GET /admin/health` и `GET /v1/admin/health` — состояние сервера + build identity: `status`, `service`, `version` (`MAJOR.MINOR` из `EventHubSpec/VERSION`), `build` (CI `run_number`, per-repo), `git_sha`, `built_at`. `/admin/health` — CT/Traefik; `/v1/admin/health` — Admin UI. - `GET /v1/admin/stats` — агрегированная статистика дашборда. - `POST /v1/admin/login` — вход администратора. - `POST /v1/admin/refresh` — обновление пары access/refresh JWT администратора. - `GET /v1/admin/users`, `GET /v1/admin/users/:id` — пользователи. - `GET /v1/admin/reports`, `GET /v1/admin/reports/:id` — жалобы. - `DELETE /v1/admin/reviews/:id` — удалить отзыв. - `GET /v1/admin/banned-words`, `POST /v1/admin/banned-words` — запрещённые слова. - `GET /v1/admin/automod/settings`, `PUT /v1/admin/automod/settings` — настройки автомодерации (чтение: admin/moderator; запись: admin/superadmin). - `GET /v1/admin/automod/hits`, `PUT /v1/admin/automod/hits/:id` — очередь срабатываний (`status`: `approved` | `rejected`; фильтры `status`, `trigger`; доступ: admin/moderator). - `GET /v1/admin/tickets/stats` — статистика тикетов: `total_tickets`, `open`, `in_progress`, `resolved`, `closed`, `total_errors`. - `GET /v1/admin/tickets` — список с фильтрами `status`, `source`, `assigned_to`, `q`; `GET /v1/admin/tickets/:id` — управление тикетами. - `GET /v1/admin/subscriptions`, `POST /v1/admin/subscriptions/:id` — подписки. - `PUT /v1/admin/:target_type/:id` — модерация. - `GET /v1/admin/me` — профиль администратора. - `GET /v1/admin/admins`, `POST /v1/admin/admins/:id` — управление администраторами. - `GET /v1/admin/audit` — аудит. ## 6.1. Аутентификация и авторизация ### Общие принципы - Пользователи и администраторы используют разные эндпоинты и JWT-секреты. - Access-токен — короткоживущий stateless JWT (HS256), **TTL 86400 с (24 ч)** — `eventhub_auth:generate_token/4`. - Refresh-токен — 30 дней; подписанный JWT в единой session-модели (`auth_session`). - Все защищённые эндпоинты требуют заголовок `Authorization: Bearer `. - **Source of truth:** `EventHubSpec` (этот документ); Swagger в репозитории — вторичная справка, синхронизируется по факту кода. ### Единая session-модель (`auth_session`) Таблица Mnesia `auth_session` (`disc_copies`): | Поле | Описание | |------|----------| | `session_id` | Первичный ключ сессии устройства | | `family_id` | Семейство ротаций одной сессии | | `subject_id` | ID пользователя или администратора | | `subject_type` | `user` \| `admin` | | `client_type` | `admin` \| `web` \| `mobile` | | `current_jti` | Актуальный jti refresh JWT | | `expires_at` | Срок жизни сессии (30 дней) | | `revoked` | Флаг отзыва | **Refresh JWT (admin)** — claims: - `typ=refresh`, `aud=admin`, `sub=` - `sid=`, `fid=`, `jti=` - `client=admin`, `exp`, `iat` **Refresh JWT (user, фаза 2)** — claims: - `typ=refresh`, `aud=user`, `sub=` - `sid`, `fid`, `jti` — как у admin - `client=web` (фаза 3: `mobile`), `exp`, `iat` - Подпись: `JWT_SECRET` (user JWK), отдельно от admin **Поток login admin** (`POST /v1/admin/login`): 1. Проверка email/password. 2. Создание записи `auth_session`. 3. Ответ: `{ token, refresh_token, user }`. **Поток refresh admin** (`POST /v1/admin/refresh`): 1. Верификация подписи и срока refresh JWT. 2. Сверка `jti` из JWT с `current_jti` в Mnesia. 3. При совпадении — ротация: новый `jti`, новая пара токенов. 4. При несовпадении (reuse) — `revoke_family`, ответ 401. **Поток login user** (`POST /v1/login`): 1. Проверка email/password. 2. Создание записи `auth_session` (`subject_type=user`, `client_type=web`). 3. Ответ: `{ token, refresh_token, user }`. **Поток refresh user** (`POST /v1/refresh`): 1. Верификация refresh JWT (`aud=user`). 2. Сверка `jti` с `current_jti` в Mnesia, ротация при успехе. 3. Reuse — `revoke_family`, 401. **Фазы внедрения:** - Фаза 1 (#23): admin API — реализовано. - Фаза 2 (#26): user login + `/v1/refresh` — реализовано. - Фаза 3: client web + mobile (явный `client_type`, тот же контракт). ### Legacy (не используется login/refresh user) - Таблица `session` (`ram_copies`) и opaque refresh в `core_session` — оставлены в кодовой базе, hot path user API переведён на `auth_session`. ## 7. ВЕРСИОНИРОВАНИЕ И СТАТУС Текущая версия: 1.5 (MVP, альфа). Включает: - Гибридную модель повторяющихся событий - Раздельную JWT-аутентификацию (пользователи/администраторы) - Полноценную ролевую модель администрирования - Версионированное админ-API (/v1/admin/...) - Расширенную инфраструктуру (Traefik, WAF, мониторинг, failover) - Аудит действий администраторов - Расширенную статистику для дашборда - Улучшенную обработку ошибок и валидацию **Новое в версии 1.5:** - Расширенная структура данных (новые поля и таблицы, задача #12) - Дисковое хранение и индексы (задача #13) - Репликация между узлами кластера с автоочисткой (задача #14) - Архивирование исторических данных и серверный рендеринг календаря (задача #15) - Сбор статистики через триггеры Mnesia: upsert `stats_counter` / `stats_daily` (#42, развитие #16) - Механизм миграций схемы данных (задача #17) ## 8. ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ - Система не поддерживает транзакционную целостность между несколькими таблицами на уровне приложения (полагаемся на Mnesia). - В текущей версии отсутствует полноценная система уведомлений (только таблица). - Загрузка файлов (вложения) пока не реализована. - Серверный рендеринг календаря работает только для владельца календаря. - Автоматическое архивирование через `archive_controller` в локальном режиме использует `slave:start`, который устарел; в production планируется `peer`. ## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ - Erlang/OTP 28 - Docker Engine 27.3.1+ - Docker Compose v3.8 - Linux (продакшен) или WSL2 (разработка)