diff --git a/EventHubBackSpec.md b/EventHubBackSpec.md new file mode 100644 index 0000000..f45b0d9 --- /dev/null +++ b/EventHubBackSpec.md @@ -0,0 +1,288 @@ +ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB +Версия: 1.2 (расширенная гибридная модель + ролевая модель администрирования) + +1. ЦЕЛИ И НАЗНАЧЕНИЕ + EventHub — платформа для управления событиями с поддержкой календарей, записи участников (включая специалистов), гибкого подтверждения, рейтингов, отзывов, модерации, встроенного баг-трекера и платной подписки. Целевая аудитория: владельцы календарей (бизнес), участники (клиенты), администраторы. + +2. ФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ + +2.1. Календари +- CRUD календаря (название, описание, теги, владелец) +- Расшаривание по ссылке (публичная/приватная) +- Приглашение пользователей с правами "запись" или "администрирование" +- Типы календарей: personal (бесплатный, без записи), commercial (платный, запись клиентов, специалисты) +- Гибкое подтверждение заявок: auto (автоматически), manual (вручную), timeout (авто через N секунд) +- Теги календаря, рейтинг (средняя оценка, количество голосов) + +2.2. События (расширенная версия с повторяющимися событиями) + +2.2.1. Типы событий и модель хранения +События делятся на два типа: +- single — одиночное событие, имеет конкретные дату и время начала, длительность. +- recurring — повторяющееся событие (серия), определяется мастер-записью и правилом повторения. + +Для эффективного хранения и быстрого поиска применяется гибридная модель с отложенной материализацией вхождений: +- Каждая повторяющаяся серия представлена одной мастер-записью в таблице events с полем event_type = recurring и полем recurrence_rule, содержащим правило повторения в формате, совместимом с iCalendar RRULE (или упрощённый JSON-представление: freq, interval, byday, until и т.д.). +- Конкретные вхождения (экземпляры) серии материализуются в таблице events только при необходимости: + * когда на данное вхождение записался хотя бы один участник; + * когда администратор вручную редактирует это вхождение (перенос времени, отмена, смена специалиста); + * (опционально) при наступлении даты вхождения для архивных целей. +- Материализованное вхождение имеет: + * event_type = single (или специальный флаг is_instance = true), + * ссылку master_id на родительскую мастер-запись, + * собственные start_time и duration, которые могут отличаться от вычисленных по правилу (например, при переносе конкретного дня). +- Отменённые вхождения из серии (исключения) хранятся в отдельной таблице recurrence_exceptions с указанием master_id, original_start_time и action = cancel | reschedule. + +2.2.2. Правила генерации вхождений при поиске +При поиске событий на заданный интервал дат (например, /v1/events?from=...&to=...) система: +1. Выбирает все одиночные события (event_type = single), попадающие в интервал. +2. Выбирает все активные мастер-записи (event_type = recurring), у которых: + - дата первого вхождения ≤ конец интервала, + - правило повторения допускает вхождения внутри интервала (учёт until или ограничения по количеству повторений). +3. Для каждой мастер-записи генерирует список вхождений в пределах запрошенного интервала (используя встроенную функцию разбора RRULE или собственный алгоритм). +4. Из сгенерированного списка исключаются вхождения, присутствующие в таблице recurrence_exceptions с действием cancel. +5. Если для какого-либо вхождения уже существует материализованная запись в таблице events (по совпадению master_id и start_time), то используются данные материализованного экземпляра (специалист, длительность, статус) вместо вычисленных по шаблону. +6. Итоговый список сортируется по времени начала и возвращается с пагинацией. + +Ограничение: глубина генерации вхождений для одного запроса ограничена 1000 элементов; для длительных серий применяется пагинация по датам. + +2.2.3. Материализация при записи участника +При попытке записаться на вхождение повторяющегося события (POST /v1/events/:event_id/join): +- Если event_id ссылается на мастер-запись (event_type = recurring), то в теле запроса обязательно передаётся occurrence_start — конкретное время вхождения, на которое производится запись. +- Система в одной транзакции Mnesia: + * Проверяет, существует ли материализованное вхождение с master_id = EventId и start_time = OccurrenceStart. + * Если нет — создаёт новую запись в таблице events (тип single, master_id = EventId), копируя общие атрибуты мастер-записи (название, описание, календарь, специалист). + * Создаёт бронирование (booking), привязанное к идентификатору материализованного вхождения. +- Дальнейшие действия по подтверждению заявки выполняются уже с материализованным экземпляром. + +2.2.4. Изменение и удаление серий +- При редактировании мастер-записи (изменение названия, описания, правила повторения) обновляется только сама мастер-запись. Уже существующие материализованные вхождения остаются неизменными (сохраняют старые значения атрибутов). Это поведение может быть изменено администратором через специальный флаг «применить ко всем будущим вхождениям». +- При удалении мастер-записи все связанные материализованные вхождения также помечаются удалёнными (или удаляются каскадно), а бронирования на будущие вхождения аннулируются с уведомлением участников. + +2.2.5. Структура записей (records.hrl) +```erlang +%% Событие (может быть мастером или экземпляром) +-record(event, { + id :: binary(), + calendar_id :: binary(), + title :: binary(), + description :: binary(), + event_type :: single | recurring, + start_time :: calendar:datetime(), + duration :: integer(), %% минуты + recurrence_rule :: binary() | undefined, + master_id :: binary() | undefined, + is_instance :: boolean(), %% true для материализованных вхождений + specialist_id :: binary() | undefined, + location :: #location{} | undefined, + tags :: [binary()], + status :: active | cancelled | completed, + created_at :: calendar:datetime(), + updated_at :: calendar:datetime() +}). + +%% Исключение из повторений +-record(recurrence_exception, { + master_id :: binary(), + original_start :: calendar:datetime(), + action :: cancel | reschedule, + new_start :: calendar:datetime() | undefined +}). +``` + +## 2.2.6. Требования к реализации +- Разбор и генерация RRULE должны быть реализованы в отдельном модуле `logic_recurrence` без внешних зависимостей (чистый Erlang). +- Все операции с материализацией и записью участников должны выполняться в транзакциях Mnesia для обеспечения консистентности. +- Для ускорения поиска материализованных вхождений по мастеру и дате создаётся составной индекс в Mnesia: `{master_id, start_time}` (через `mnesia:add_table_index/2` или хранение в ets с ключом `{master_id, start_time}`). + +## 2.3. Запись участников и подтверждение +- Запись через календарь +- Подтверждение согласно политике календаря +- Уведомления участника и владельца +- Привязка события к специалисту (отдельная запись в календаре специалиста) + +## 2.4. Отзывы и рейтинги +- Только участники событий могут оставлять отзывы +- Оценка 1-5, текстовый комментарий +- Отзывы на событие или на календарь +- Модерация отзывов (администраторы могут скрывать) + +## 2.5. Поиск и фильтрация +- По тексту, по тегам, по гео-позиции (радиус), по дате/времени + +## 2.6. Расширенные возможности +- Экспорт в Google Calendar (через OAuth2) +- Экспорт в Apple Calendar (ICS-файл) +- Геокодирование (адрес -> координаты) через внешний API (заглушка / Nominatim) + +## 2.7. Модерация и безопасность +- Жалобы на календари, события, отзывы +- Автоматическая модерация по ключевым словам и по порогу жалоб +- Ручная заморозка / разморозка администратором +- Бан-лист слов +- Аудит действий администраторов (детальная информация: кто, что, когда, IP, причина) + +## 2.8. Баг-трекер (автоматический) +- При ошибке создаётся тикет, группировка по хэшу ошибки +- Учёт количества повторений +- Уведомление пользователя при создании и при закрытии тикета +- Доступ только у администраторов + +## 2.9. Платная подписка +- Личное использование бесплатно (personal) +- Коммерческое использование платно (commercial) +- Пробный период 30 дней +- Планы подписки: 1, 3, 6, 12 месяцев +- Заглушка платежного шлюза (для тестирования) +- Автоматическое истечение подписки + +## 2.10. Административная панель +- Отдельный HTTPS-сервер (порт 8445) и отдельный WSS (порт 8446) +- Управление календарями, событиями, отзывами, жалобами, тикетами, бан-словами +- Статистика, информация о нодах кластера +- WebSocket-уведомления администраторам + +### 2.10.1. Ролевая модель администраторов +Вводится три встроенные роли администраторов с различным объёмом прав: + +| Роль | Идентификатор | Права | +|--------------|---------------|-------------------------------------------------------------------------------------------| +| Суперадмин | `superadmin` | Полный доступ ко всем модулям, управление ролями других админов, просмотр аудита, системные метрики. | +| Модератор | `moderator` | Модерация контента (события, жалобы, отзывы), блокировка/разблокировка пользователей, работа с баг-трекером. | +| Поддержка | `support` | Read-only доступ к пользователям и событиям, обработка жалоб, создание и обновление багов. | + +**Примечание:** На этапе MVP поддерживается только роль `admin` (эквивалент `superadmin`). Полноценная ролевая модель будет внедрена в ближайших итерациях. + +### 2.10.2. Эндпоинты для управления ролями и аудитом +- `GET /admin/me` (или `GET /api/auth/me`) — получение текущей роли и разрешений администратора. +- `GET /admin/admins` — список всех администраторов с ролями (только для `superadmin`). +- `PUT /admin/admins/:id` — изменение роли администратора (только для `superadmin`). +- `POST /admin/admins` — приглашение нового администратора с назначением роли (только для `superadmin`). +- `GET /admin/audit` — журнал действий администраторов с фильтрацией по дате, пользователю, действию (только для `superadmin`). + +### 2.10.3. Статистика для дашборда с учётом ролей +- `superadmin` — системные метрики: все пользователи, события, жалобы, баги за период, графики регистраций/событий по дням, активность администраторов. +- `moderator` — собственные обработанные жалобы/события (количество, статусы, время реакции), общая статистика по модерации. +- `support` — количество открытых багов и жалоб, назначенных на текущего сотрудника, персональные задачи. + +Эндпоинт: `GET /admin/stats` (или `/api/stats` с авторизацией). Возвращает JSON, содержимое которого фильтруется в соответствии с ролью вызывающего. + +## 2.11. Real-time уведомления (WebSocket) +- Пользователи: подписка на календарь, получение обновлений +- Администраторы: подписка на глобальные уведомления (жалобы, авто-заморозки) + +## 3.1. Производительность и масштабирование +- 100 000+ пользователей +- Горизонтальное масштабирование (увеличение нод) +- Mnesia с дисковыми копиями на нескольких нодах +- Отдельные ноды для исторических данных (disc_only_copies) +- Пагинация всех списков + +## 3.2. Надёжность +- Супервизорное дерево OTP +- Let it crash – быстрый перезапуск процессов +- Автоматическое восстановление после падения ноды (Mnesia) + +## 3.3. Безопасность +- JWT с ролью (user, admin; в будущем — superadmin, moderator, support) +- HTTPS / WSS (самоподписанный сертификат для dev, реальный для prod) +- Argon2 для хеширования паролей (erlang-argon2) +- OAuth2 для Google (опционально) +- Refresh token (запланирован) +- При блокировке пользователя или отклонении сущности обязательно сохранять причину (поле `reason`) + +## 3.4. Наблюдаемость +- Healthcheck эндпоинт (`GET /health`) +- Логирование (JSON, ротация) +- Prometheus метрики (запланированы на уровне приложения, в настоящее время доступны через балансировщик) +- Аудит действий администраторов (обязателен, см. п. 2.10.2) + +## 3.5. CI/CD +- Drone CI (или GitLab CI / GitHub Actions) +- EUnit + Common Test + Tsung +- Сборка релиза +- Docker-образ +- Горячее обновление (rolling upgrade) кластера (3+ нод) +- Миграции Mnesia + +## 4. СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ) +- Бэкенд: Erlang/OTP 28.2 +- Сборка: rebar3 3.27.0 +- HTTP/WebSocket: Cowboy 2.10.0 +- JSON: jsx 3.1.0 +- JWT: jose (реализовано) +- Хеширование паролей: erlang-argon2 1.0.0 +- Тестирование: meck 0.9.2, gun 2.0.0 +- Нагрузочное тестирование: Tsung 1.8.0 +- CI/CD: Drone 2.0+ +- Контейнеризация: Docker 20.10+ +- База данных: Mnesia (встроенная, распределённая) + +## 5. ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА +``` +src/ +├── infra/ # инфраструктура (приложение, супервизоры, аутентификация, подписки) +├── core/ # слой доступа к данным (DAO) и модели +├── logic/ # бизнес-логика (независимая от HTTP/WS) +├── services/ # внешние сервисы (заглушки/интеграции) +└── handlers/ # обработчики HTTP/WebSocket +``` + +**Правила:** +- Модули `handlers/` не содержат бизнес-логики. +- Модули `logic/` не знают о HTTP/WS. +- Модули `core/` (DAO) работают только с Mnesia. +- Модули `services/` могут быть заменены на реальные реализации без изменения остального кода. +- Заголовочный файл `include/records.hrl` содержит все записи таблиц Mnesia. + +## 6. ОСНОВНЫЕ API (КРАТКО) + +### Пользовательские (порт 8080) +- `POST /v1/register` +- `POST /v1/login` +- `GET /v1/user/me` +- `POST /v1/calendars` + CRUD +- `POST /v1/calendars/:id/events` + CRUD +- `POST /v1/events/:id/join` +- `POST /v1/events/:id/confirm/:user_id` +- `POST /v1/reviews` +- `POST /v1/reports` +- `POST /v1/subscription/activate` +- `POST /v1/events/:id/export` (Google) +- `GET /v1/events/:id/ical` (Apple) +- `GET /health` + +**WebSocket (порт 8081):** `ws://localhost:8081/ws?token=...` + +### Административные (порт 8445) +- `GET /admin/stats/overview` (или `/admin/stats`) — статистика с учётом роли +- `GET /admin/nodes` +- `GET /admin/calendars`, `PUT /admin/calendars/:id/freeze`, `/unfreeze` +- `GET /admin/events`, `PUT /admin/events/:id/freeze`, `PATCH /admin/events/:id/approve` (модерация) +- `GET /admin/reviews`, `PUT /admin/reviews/:id/hide` +- `GET /admin/reports` (complaints), `PUT /admin/reports/:id/status` +- `GET /admin/tickets` (bugs), `POST /admin/tickets`, `PUT /admin/tickets/:id/status`, `DELETE /admin/tickets/:id` +- `GET /admin/banned_words`, `POST /admin/banned_words`, `DELETE /admin/banned_words` +- `GET /admin/admins` (только `superadmin`) +- `PUT /admin/admins/:id` (только `superadmin`) +- `POST /admin/admins` (только `superadmin`) +- `GET /admin/audit` (только `superadmin`) +- `GET /admin/me` (текущий админ, его роль и права) + +**Административный WebSocket (порт 8446):** `wss://localhost:8446/admin/ws?token=...` + +## 7. ВЕРСИОНИРОВАНИЕ И СТАТУС +Текущая версия: **1.2** (MVP, альфа, включает гибридную модель повторяющихся событий и базовую ролевую модель администрирования). + +## 8. ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ +- В разработке используются самоподписанные SSL-сертификаты. +- Для production требуется реальный сертификат и настройка OAuth2 для Google. +- Заглушки внешних сервисов должны быть заменены перед запуском. +- Полноценное разграничение прав администраторов (`superadmin`, `moderator`, `support`) ожидается в следующем релизе; пока используется универсальная роль `admin`. + +## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ +- Erlang/OTP 28.2 +- rebar3 3.27.0 +- openssl +- Docker (опционально) +- Drone (опционально, для CI/CD) \ No newline at end of file diff --git a/EventHubFrontAdminSpec.md b/EventHubFrontAdminSpec.md new file mode 100644 index 0000000..43560d2 --- /dev/null +++ b/EventHubFrontAdminSpec.md @@ -0,0 +1,183 @@ +# **EventHub Admin UI — Техническое задание** (MVP) + +## 1. Роли и права доступа + +| Роль | Идентификатор | Права | +|------|---------------|-------| +| **Суперадмин** | `superadmin` | Полный доступ ко всем модулям, включая изменение ролей других админов, просмотр аудита, системные настройки. | +| **Модератор** | `moderator` | Модерация событий и жалоб, блокировка/разблокировка пользователей, работа с баг-трекером. | +| **Поддержка** | `support` | Просмотр пользователей и событий (read-only), обработка жалоб, создание и обновление багов. | + +**Примечание:** До реализации backend-эндпоинтов для управления ролями подразумевается одна роль `admin` (синоним `superadmin`). + +--- + +## 2. Модули панели + +### 2.1. Дашборд (Dashboard) + +**Виджеты:** +- Статистика по пользователям: всего, новых за сегодня, активных за неделю. +- Статистика по событиям: всего, ожидают модерации, опубликовано, отклонено. +- Статистика по жалобам: новых, в обработке. +- График регистраций/событий по дням (за последние 30 дней) — если API предоставляет агрегации; иначе заглушка. +- Последние действия модераторов (аудит) — последние 10 записей. + +**Источник данных:** `GET /api/stats/*` (приоритетный) или агрегация из списков ресурсов через `X-Total-Count`. + +### 2.2. Управление пользователями + +**Эндпоинты:** `GET /api/users`, `GET /api/users/:id`, `PUT /api/users/:id`, `DELETE /api/users/:id` + +**Функциональность:** +- Список пользователей (ID, username, email, роль, дата регистрации, статус, рейтинг). +- Фильтрация: по роли, статусу, поиск по username/email. +- Детальная карточка: + - Основная информация, аватар. + - Список событий пользователя. + - История жалоб на пользователя. + - Блокировка/разблокировка, смена роли (только суперадмин). + - Причина блокировки (поле ввода). +- Массовые действия: блокировка/удаление выбранных пользователей. + +**Особые требования:** Подтверждение деструктивных действий через модальное окно. + +### 2.3. Управление событиями + +**Эндпоинты:** `GET /api/events`, `GET /api/events/:id`, `PUT /api/events/:id`, `DELETE /api/events/:id`, а также эндпоинт модерации (например, `PATCH /api/events/:id/approve`) + +**Функциональность:** +- Список событий (название, организатор, календарь, статус, количество записей). +- Фильтрация: по статусу, календарю, дате. +- Детальный просмотр: + - Полная информация о событии (описание, дата-время, локация, тип). + - Список записей участников с возможностью ручного подтверждения/отклонения. + - Кнопки «Одобрить» / «Отклонить» (изменение статуса с `pending` на `approved`/`rejected`). + - При отклонении — обязательное указание причины. +- Редактирование любого поля события. +- Удаление с подтверждением. + +### 2.4. Управление календарями + +**Эндпоинты:** `GET /api/calendars`, `POST /api/calendars`, `PUT /api/calendars/:id`, `DELETE /api/calendars/:id` + +**Функциональность:** +- Список календарей (название, владелец, тип, количество подписчиков). +- Детальный просмотр: информация, возможность деактивации календаря. +- Создание коммерческого календаря (опционально, если требуется админам). +- Просмотр списка подписчиков (только чтение). + +### 2.5. Модерация жалоб + +**Эндпоинты:** `GET /api/complaints`, `GET /api/complaints/:id`, `PUT /api/complaints/:id` + +**Функциональность:** +- Список жалоб (ID, тип, подавший, объект жалобы, статус). +- Фильтрация: по статусу, типу. +- Детальный просмотр: + - Текст жалобы, прикреплённые файлы/ссылки. + - Информация о нарушителе/событии. + - Кнопки «Рассмотрено» и «Отклонить». + - Возможность перехода к блокировке пользователя/события прямо из карточки жалобы. + +### 2.6. Баг-трекер + +**Эндпоинты:** `GET /api/bugs`, `POST /api/bugs`, `PUT /api/bugs/:id`, `DELETE /api/bugs/:id` + +**Функциональность:** +- Список багов (ID, заголовок, статус, приоритет, назначенный, дата создания). +- Детальный просмотр: описание, шаги воспроизведения, скриншоты. +- Смена статуса, назначение ответственного из числа админов. +- Комментарии к багу (если поддержано API). +- Создание новых багов (только внутренние, не от пользователей). + +--- + +## 3. Интеграция с API и аутентификация + +- JWT-аутентификация, аналогичная основному приложению. +- Эндпоинт входа: `/api/auth/login` (или `/api/admin/login`). +- Токен сохраняется в `localStorage` и добавляется ко всем запросам (`Authorization: Bearer ...`). +- При получении HTTP 401 — редирект на страницу входа. +- Для дашборда использовать специальные эндпоинты `/api/stats/*`; если их нет — собирать статистику через заголовок `X-Total-Count` и множественные запросы (временно). + +--- + +## 4. Требования к UX/UI + +- Material Design (React-Admin + MUI). +- Адаптивная верстка (планшеты). +- Пагинация (серверная предпочтительна, клиентская допустима для малых объемов). +- Сортировка по любому полю. +- Поиск с debounce 300 мс. +- Подтверждение деструктивных действий (удаление, блокировка). +- Уведомления об успехе/ошибке через Snackbar. +- Тёмная тема (опционально, переключатель). + +--- + +## 5. Технологический стек + +- **Язык:** TypeScript +- **Сборщик:** Vite +- **UI-фреймворк:** React-Admin v5 + Material UI v5 +- **Провайдер данных:** `ra-data-simple-rest` с кастомизацией для JWT +- **Аутентификация:** кастомный `authProvider` +- **Контейнеризация:** Docker, Nginx + +--- + +## 6. Этапы реализации (MVP) + +1. **Неделя 1:** Инициализация проекта (Vite + React-Admin), простой dataProvider, страница входа. +2. **Неделя 2:** Модуль пользователей (список, детали, блокировка). +3. **Неделя 3:** Модуль событий (список, модерация). +4. **Неделя 4:** Модули жалоб, баг-трекера, календарей (базовый просмотр/изменение статусов). +5. **Неделя 5:** Дашборд (если API готово), финальные штрихи, тестирование, деплой в staging. + +--- + +## 7. Что потребуется от бэкенда (EventHubBack) + +### 7.1 Роли и права доступа + +Описать три административные роли: `superadmin`, `moderator`, `support`. + +В JWT-токене после аутентификации передавать поле `role` с одним из этих значений. Оставить поддержку `"role": "admin"` как синоним `superadmin` для обратной совместимости. + +Middleware авторизации для админских эндпоинтов (`/admin/*` или порты 8445/8446) должен: +- Проверять валидность JWT. +- Извлекать роль. +- Сопоставлять с требуемыми правами для каждого действия. + +Эндпоинт получения текущей роли: +- `GET /admin/me` (или `GET /api/auth/me`) — возвращает `{ id, username, role, permissions }`. + +Эндпоинты управления ролями (только для `superadmin`): +- `GET /admin/admins` — список администраторов. +- `PUT /admin/admins/:id` — изменить роль. +- `POST /admin/admins` — пригласить нового администратора. + +Логирование действий (аудит): +- Таблица `admin_audit` (admin_id, username, роль, действие, тип объекта, ID объекта, timestamp, IP, причина). +- Эндпоинт `GET /admin/audit` с фильтрами (доступен только `superadmin`). + +При блокировке пользователя / отклонении события обязательно принимать поле `reason` в теле запроса и сохранять в БД. + +Разграничение прав на уровне API: +- `support`: read-only пользователи и события; может обновлять статусы жалоб и багов, создавать баги. +- `moderator`: всё, кроме управления админами и просмотра аудита. +- `superadmin`: полный доступ. + +Ответ при недостаточности прав: `403 Forbidden`, тело: +```json +{ "error": "insufficient_permissions", "message": "Требуется роль moderator или выше" } +## 7.2 Статистика для дашборда с учётом ролей + +| Роль | Предоставляемые метрики | +|------|--------------------------| +| `superadmin` | Системные метрики (все пользователи, события, жалобы, баги за период, графики регистраций/событий по дням, активность администраторов). | +| `moderator` | Собственные обработанные жалобы/события (количество, статусы, время реакции), общая статистика по модерации. | +| `support` | Количество открытых багов и жалоб, назначенных на текущего сотрудника, персональные задачи. | + +Эндпоинт `GET /api/stats` должен возвращать JSON с секциями, доступными согласно роли вызывающего. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d2f4ed --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# **Техническое задание EventHub** +## 1. [EventHub Backend — Техническое задание](EventHubBackSpec.md) +## 2. [EventHub Admin UI — Техническое задание](EventHubFrontAdminSpec.md) \ No newline at end of file