docs(archive): hot/warm/cold month snapshots; HTML /view removed.
Fixes EventHub/EventHubSpec#20 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
|||||||
|
# Архив календаря (Фаза 3, волна 1)
|
||||||
|
|
||||||
|
Канон хранения и чтения истории. Трекер: [Spec#20](https://git.sabilin.com/EventHub/EventHubSpec/issues/20), эпик [Spec#19](https://git.sabilin.com/EventHub/EventHubSpec/issues/19).
|
||||||
|
|
||||||
|
Реализация: Back#76, Front#73. Этот документ — контракт; код подтягивается после него.
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Горячая Mnesia и live-запросы **не растут с годами истории**. Прошлые месяцы остаются usable в том же React-grid, без iframe как основного UX.
|
||||||
|
|
||||||
|
Критерий выигрыша: размер и нагрузка **рабочей** БД (dump/checkpoint, репликация `disc_copies`, `index_read` календаря), не «ещё одна копия тех же строк в другой таблице/ноде».
|
||||||
|
|
||||||
|
## Почему не старый pipeline (#15)
|
||||||
|
|
||||||
|
Старый pipeline (`archive_controller` / `archive_manager` / extra BEAM / HTML `GET …/view`):
|
||||||
|
|
||||||
|
- не планировался и не переносил данные (типы дат, схема `#event{}` разъехалась);
|
||||||
|
- extra BEAM на день/месяц — **больше** схем и load, не меньше;
|
||||||
|
- HTML `/view` и renderer **удалены**: история только через JSON `GET …/events`.
|
||||||
|
|
||||||
|
Extra-node, `slave`/`peer` для архива и построчный `event_archive` **удалены** (не «мёртвый» код).
|
||||||
|
|
||||||
|
## Модель: три яруса, ключ месяц
|
||||||
|
|
||||||
|
Единица архива: **календарь × `YYYY-MM`**. Не строка события.
|
||||||
|
|
||||||
|
| Ярус | Где | Рост | Кто читает |
|
||||||
|
|------|-----|------|------------|
|
||||||
|
| **Hot** | `event`, `booking` (`disc_copies`) | **текущий календарный месяц + всё будущее** | живой grid; выборка по `from`/`to`, не весь календарь |
|
||||||
|
| **Warm** | `month_snapshot` (`disc_only_copies`), ключ `{calendar_id, year_month}` | **3 закрытых месяца** gzip JSON | частый flip на прошлый месяц — один lookup |
|
||||||
|
| **Cold** | `{UPLOAD_DIR}/archive/{calendar_id}/{YYYY-MM}.json.gz` | диск volume, не Mnesia | тот же JSON API |
|
||||||
|
|
||||||
|
### Оценка нагрузки (зачем не 90 дней)
|
||||||
|
|
||||||
|
Живые запросы Front:
|
||||||
|
|
||||||
|
| Путь | Окно |
|
||||||
|
|------|------|
|
||||||
|
| Month grid | ~42 дня viewport (хвосты соседних месяцев) |
|
||||||
|
| Week / ghost | 7–14 дней |
|
||||||
|
| Time Arc hints | сегодня + **21 день** вперёд |
|
||||||
|
| Day | 1 день |
|
||||||
|
|
||||||
|
90 суток lookback в hot **не обслуживает** ни один live-путь: прошлый календарный месяц в UI — уже «архив». Пока строки висят в `event`, `dirty_index_read(calendar_id)` (как сейчас) тащит их на каждый `GET /events`.
|
||||||
|
|
||||||
|
Оценка busy-студии (8 слотов × 8 мастеров ≈ 64 события/день), **только lookback** (будущее всё равно в hot):
|
||||||
|
|
||||||
|
| Lookback | Лишние строки в hot на календарь |
|
||||||
|
|----------|----------------------------------|
|
||||||
|
| 90 суток | ~5800 |
|
||||||
|
| текущий месяц (~17–31 день) | ~1100–2000 |
|
||||||
|
| закрытый месяц после grace | **0** исторических месяцев |
|
||||||
|
|
||||||
|
100 таких студий: 90д lookback ≈ **0.5M** лишних hot-строк в `disc_copies` на **каждый** узел кластера. Warm-blob 24 месяцев в Mnesia тоже тяжёлый (~1 MB gzip/мес × 24 × 100 ≈ 2 GB disc_only). Три месяца warm ≈ **300 MB** на ту же сотню.
|
||||||
|
|
||||||
|
### Дефолты (зафиксировано)
|
||||||
|
|
||||||
|
- **Hot:** все события с `start_time` в **текущем календарном месяце** (TZ календаря / UTC — в реализации, один канон) **или в будущем**.
|
||||||
|
- **Снимок месяца M:** когда наступило **7 суток** следующего месяца (`ARCHIVE_GRACE_DAYS=7`) — хвост week/month-grid и правки конца месяца; затем M **read-only**.
|
||||||
|
- **Warm:** последние **3 закрытых месяца** blob в Mnesia (`ARCHIVE_WARM_MONTHS=3`).
|
||||||
|
- **Cold:** старше 3 закрытых месяцев, **без TTL**.
|
||||||
|
- `GET …/events?from&to` **склеивает ярусы**: viewport августа 1-го числа читает хвост июля из warm/cold + август из hot.
|
||||||
|
- Recurring **master** с вхождениями в hot/будущем остаётся в hot; в snapshot — только вхождения закрытого месяца.
|
||||||
|
- **Booking** остаётся в hot (inbox `/bookings`); в snapshot — копия занятости на момент снимка (через `event_to_json`).
|
||||||
|
- Mutate в archived month → `409`. Reopen — не в волне 1.
|
||||||
|
|
||||||
|
Env (без смены контракта): `ARCHIVE_GRACE_DAYS`, `ARCHIVE_WARM_MONTHS`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Hot --(месяц закрыт + 7д grace)--> Warm snapshot --(>3 закрытых мес)--> Cold file
|
||||||
|
```
|
||||||
|
|
||||||
|
## API чтения (контракт Back#76)
|
||||||
|
|
||||||
|
Клиент **не** переключается на HTML для прошлого месяца.
|
||||||
|
|
||||||
|
- Живой диапазон: `GET /v1/calendars/:id/events?from&to` — hot по времени; если `from`
|
||||||
|
задевает закрытый месяц — **merge** warm/cold (хвост month-grid). Не отдавать весь
|
||||||
|
календарь без `from`/`to` как единственный путь live-grid.
|
||||||
|
- Прошлый месяц (и week/day, курсор в archived month): тот же path и shape JSON. Сервер сам берёт warm snapshot или cold file и отдаёт события, попадающие в `from`/`to`.
|
||||||
|
- Ответ может содержать флаг `archived: true` на уровне списка или заголовка/обёртки — чтобы Front показал бейдж «Архив». Точная форма — в реализации, без второго URL.
|
||||||
|
- `GET /v1/calendars/:id/view` **нет**: HTML-календарь снят.
|
||||||
|
|
||||||
|
ACL: как у живого календаря — owner **или** share `read|write|admin` (кто уже `can_access`). Не только owner. Guest / public commercial: только hot public events; deep archive чужой студии — **не** в волне 1.
|
||||||
|
|
||||||
|
## Запись
|
||||||
|
|
||||||
|
- Create/update/delete событий и booking — только hot.
|
||||||
|
- Scheduler (один узел, как `infra_cleanup`): закрыть месяц → собрать JSON → gzip → `month_snapshot` → удалить из hot строки этого месяца (кроме защищённых master). Затем по TTL — сбросить blob в файл, ужать/убрать запись Mnesia до pointer.
|
||||||
|
- Идемпотентность: повторный прогон того же `{calendar_id, YYYY-MM}` не дублирует и не теряет данные.
|
||||||
|
- Backup (DevOps): volume `eventhub-data` включает `UPLOAD_DIR/archive/`.
|
||||||
|
|
||||||
|
## Front (контракт Front#73)
|
||||||
|
|
||||||
|
- Owner/share, вид месяц/неделя/день, **прошедший** период: React-grid по JSON, не iframe.
|
||||||
|
- Бейдж «Архив».
|
||||||
|
- Lens overview на прошлом месяце — минимум (не «нет lens, потому что HTML»).
|
||||||
|
- Текущий и будущий месяц без регрессии.
|
||||||
|
- Time Arc / ghosts на истории — **не** в волне 1.
|
||||||
|
|
||||||
|
## Non-goals волны 1
|
||||||
|
|
||||||
|
- Write в warm/cold, reopen месяца.
|
||||||
|
- Гостевой архив чужой студии.
|
||||||
|
- Time Arc на прошлых месяцах.
|
||||||
|
- S3 / внешняя БД / extra Erlang node.
|
||||||
|
- Удаление истории по TTL (файлы храним).
|
||||||
|
- Починка `archive_controller`/`slave` как целевой путь.
|
||||||
|
|
||||||
|
## Критерии приёмки канона
|
||||||
|
|
||||||
|
- [x] Этот документ согласован (Spec#20).
|
||||||
|
- [ ] Back#76: scheduler + snapshot + JSON read + тесты; peer/slave не в hot path.
|
||||||
|
- [ ] Front#73: past period на React; e2e навигации на прошлый месяц.
|
||||||
|
- [ ] После тестов: BackSpec/FrontSpec сверены с кодом (не с этим черновиком API-флага).
|
||||||
+18
-12
@@ -266,6 +266,10 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
|
|||||||
UI списка — Front#72. Kick-on-login / нативный клиент — по-прежнему future.
|
UI списка — Front#72. Kick-on-login / нативный клиент — по-прежнему future.
|
||||||
- **Не в фазе 2:** оплата услуги клиентом (B2C) — вне горизонта продукта.
|
- **Не в фазе 2:** оплата услуги клиентом (B2C) — вне горизонта продукта.
|
||||||
|
|
||||||
|
#### Фаза 3 / архив (Spec#19, канон Spec#20)
|
||||||
|
- История календаря: hot = текущий месяц + будущее; warm = 3 закрытых месяца;
|
||||||
|
cold = gzip-файлы. Контракт: [`ARCHIVE.md`](ARCHIVE.md). Код: Back#76.
|
||||||
|
|
||||||
### 2.1.3. Share / приглашения (соредактор и заместитель)
|
### 2.1.3. Share / приглашения (соредактор и заместитель)
|
||||||
|
|
||||||
Таблица `calendar_share` (`id`, `calendar_id`, `user_id`, `rights`: `read` | `write` | `admin`,
|
Таблица `calendar_share` (`id`, `calendar_id`, `user_id`, `rights`: `read` | `write` | `admin`,
|
||||||
@@ -511,7 +515,9 @@ API:
|
|||||||
- Полная репликация горячих таблиц между всеми узлами кластера (задача #14).
|
- Полная репликация горячих таблиц между всеми узлами кластера (задача #14).
|
||||||
- Автоматическое обнаружение узлов через DNS-имя `eventhub-node` или статический список (задача #14).
|
- Автоматическое обнаружение узлов через DNS-имя `eventhub-node` или статический список (задача #14).
|
||||||
- Периодическая очистка «мёртвых» узлов из схемы Mnesia (каждые 30 секунд) (задача #14).
|
- Периодическая очистка «мёртвых» узлов из схемы Mnesia (каждые 30 секунд) (задача #14).
|
||||||
- Архивирование исторических данных (старше 30 дней) в отдельные Mnesia-узлы с `disc_only_copies` (задача #15).
|
- Архив календаря (Фаза 3, [ARCHIVE.md](ARCHIVE.md), Spec#20): hot `disc_copies`
|
||||||
|
(текущий месяц + будущее) + `month_snapshot` `disc_only` (3 закрытых месяца) +
|
||||||
|
gzip-файлы старше. Не extra-node и не построчный `event_archive` (#15 отвергнут).
|
||||||
- Пагинация всех списков.
|
- Пагинация всех списков.
|
||||||
|
|
||||||
### 3.2. Надёжность
|
### 3.2. Надёжность
|
||||||
@@ -526,7 +532,8 @@ API:
|
|||||||
- `EVENTHUB_ENV`: `dev` — допускаются documented fallbacks; `stage`/`prod` — fail-fast при слабых или отсутствующих `JWT_SECRET`, `ADMIN_JWT_SECRET` и паролях seed-админов (#25).
|
- `EVENTHUB_ENV`: `dev` — допускаются documented fallbacks; `stage`/`prod` — fail-fast при слабых или отсутствующих `JWT_SECRET`, `ADMIN_JWT_SECRET` и паролях seed-админов (#25).
|
||||||
- Проверка прав доступа к календарям и событиям.
|
- Проверка прав доступа к календарям и событиям.
|
||||||
- Пароли хэшируются с использованием Argon2.
|
- Пароли хэшируются с использованием Argon2.
|
||||||
- Защита от несанкционированного просмотра архивных данных (только владелец календаря) (задача #15).
|
- Архив месяца: тот же ACL, что у живого календаря (`can_access`: owner | share).
|
||||||
|
Guest/public deep-archive чужой студии — не в волне 1 ([ARCHIVE.md](ARCHIVE.md)).
|
||||||
|
|
||||||
### 3.4. Наблюдаемость
|
### 3.4. Наблюдаемость
|
||||||
- Логирование в JSON-формате.
|
- Логирование в JSON-формате.
|
||||||
@@ -555,12 +562,10 @@ API:
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── core/ — бизнес-логика (core_user, core_event, core_booking, core_review, ...)
|
├── core/ — бизнес-логика (core_user, core_event, core_booking, core_review, ...)
|
||||||
├── handlers/ — обработчики HTTP (handler_login, handler_calendar_view, ...)
|
├── handlers/ — обработчики HTTP (handler_login, handler_events, ...)
|
||||||
├── infra/ — инфраструктура (infra_mnesia, infra_sup, cluster_discovery,
|
├── infra/ — infra_mnesia, infra_sup, month_archive_worker, stats_collector,
|
||||||
│ archive_manager, archive_controller, stats_collector,
|
│ migration_engine, ...
|
||||||
│ migration_engine, ...)
|
├── logic/ — в т.ч. logic_month_archive (снимки месяца)
|
||||||
├── archive/ — архивирование и рендеринг (archive_controller, archive_manager,
|
|
||||||
│ calendar_html_renderer, archive_fetcher)
|
|
||||||
├── migrations/ — файлы миграций
|
├── migrations/ — файлы миграций
|
||||||
└── eventhub_app.erl — точка входа приложения
|
└── eventhub_app.erl — точка входа приложения
|
||||||
```
|
```
|
||||||
@@ -682,7 +687,8 @@ src/
|
|||||||
- `GET /v1/subscription` — подписка пользователя.
|
- `GET /v1/subscription` — подписка пользователя.
|
||||||
- `POST /v1/subscription` — `action=start_trial` | `activate` (+ `plan`, опционально
|
- `POST /v1/subscription` — `action=start_trial` | `activate` (+ `plan`, опционально
|
||||||
`payment_info`). Trial — только через `start_trial`, не через create commercial.
|
`payment_info`). Trial — только через `start_trial`, не через create commercial.
|
||||||
- `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — HTML-календарь (владелец), включая архив.
|
- `GET /v1/calendars/:calendar_id/events?from&to` — события диапазона: **hot** для живого окна;
|
||||||
|
archived month — из `month_snapshot` / файла, тот же JSON ([ARCHIVE.md](ARCHIVE.md), Back#76).
|
||||||
|
|
||||||
### WebSocket (порт 8081)
|
### WebSocket (порт 8081)
|
||||||
- `WS /ws` — пользовательский канал real-time (подписка на обновления событий календаря; детали — §2.11).
|
- `WS /ws` — пользовательский канал real-time (подписка на обновления событий календаря; детали — §2.11).
|
||||||
@@ -805,9 +811,9 @@ src/
|
|||||||
хранение local disk `UPLOAD_DIR` (default `/app/data/uploads` на volume
|
хранение local disk `UPLOAD_DIR` (default `/app/data/uploads` на volume
|
||||||
`eventhub-data`); отдача `GET /v1/media/:kind/:owner/:file` (public).
|
`eventhub-data`); отдача `GET /v1/media/:kind/:owner/:file` (public).
|
||||||
Вложения к событию — пока не реализованы.
|
Вложения к событию — пока не реализованы.
|
||||||
- Серверный рендеринг календаря работает только для владельца календаря.
|
- HTML `GET …/view` **удалён**; архив читается JSON-ом (Spec#20 / [ARCHIVE.md](ARCHIVE.md)).
|
||||||
- Автоматическое архивирование через `archive_controller` в локальном режиме использует
|
- `archive_controller` + extra-node (`slave`/`peer`) **удалены**; архив — `month_snapshot`
|
||||||
`slave:start`, который устарел; в production планируется `peer`.
|
на тех же нодах (Back#76, [ARCHIVE.md](ARCHIVE.md)).
|
||||||
|
|
||||||
## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ
|
## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ
|
||||||
- Erlang/OTP 28
|
- Erlang/OTP 28
|
||||||
|
|||||||
+10
-2
@@ -70,7 +70,8 @@ Deep-link на календарь, владельцем которого явл
|
|||||||
клике на вкладку; при смене mood — `DEFAULT_LENS_BY_MOOD`. Workspace chrome: (1) селектор;
|
клике на вкладку; при смене mood — `DEFAULT_LENS_BY_MOOD`. Workspace chrome: (1) селектор;
|
||||||
(2) период + вид Месяц/Неделя/День; (3) owner actions — для commercial только
|
(2) период + вид Месяц/Неделя/День; (3) owner actions — для commercial только
|
||||||
«+ Новое событие» («Заполнить расписание» — во вкладке Студия). Без flip / CREATOR.
|
«+ Новое событие» («Заполнить расписание» — во вкладке Студия). Без flip / CREATOR.
|
||||||
Нет lens на HTML-архиве месяца / не-calendar routes.
|
Нет lens на не-calendar routes. Прошлый месяц — React-grid (не HTML-iframe); lens overview
|
||||||
|
на истории — минимум ([ARCHIVE.md](ARCHIVE.md), Front#73).
|
||||||
- **Week view (Front#40):** одна строка day-headers (без дубля заголовков
|
- **Week view (Front#40):** одна строка day-headers (без дубля заголовков
|
||||||
`WeekDayColumn`); горизонтальный скролл через `.eh-cal-week-scroll` (mobile ~390).
|
`WeekDayColumn`); горизонтальный скролл через `.eh-cal-week-scroll` (mobile ~390).
|
||||||
- Контекст виджета: свой календарь (селектор) или browse чужого (после поиска). Чужой `personal` —
|
- Контекст виджета: свой календарь (селектор) или browse чужого (после поиска). Чужой `personal` —
|
||||||
@@ -226,7 +227,9 @@ cover (`.eh-discover-media--photo`); иначе mood wash + initials (`.eh-disco
|
|||||||
(UI: «Отслеживать» в about, страница `/following`; не путать с `/subscription`)
|
(UI: «Отслеживать» в about, страница `/following`; не путать с `/subscription`)
|
||||||
- Просмотр коммерческого чужого календаря по id; при `booking_open=false` — баннер
|
- Просмотр коммерческого чужого календаря по id; при `booking_open=false` — баннер
|
||||||
«Запись временно недоступна», кнопка записи скрыта
|
«Запись временно недоступна», кнопка записи скрыта
|
||||||
- HTML month view владельца: `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — при переключении виджета на **прошедший месяц** (owner + вид «Месяц») автоматически подменяет клиентский grid серверным HTML (live/архив, задача #15). Текущий и будущие месяцы — React-виджет по `GET …/events`.
|
- Прошедший месяц/неделя/день: тот же React-виджет по `GET …/events?from&to` (сервер отдаёт
|
||||||
|
snapshot/файл); бейдж «Архив». HTML `GET …/view` **нет** ([ARCHIVE.md](ARCHIVE.md),
|
||||||
|
Front#73). Текущий и будущие месяцы — без изменения, только hot.
|
||||||
- Создание / апгрейд `commercial` без **уже active** sub/trial → `402` → `/subscription`
|
- Создание / апгрейд `commercial` без **уже active** sub/trial → `402` → `/subscription`
|
||||||
(trial — явный `start_trial`, не auto при create); после оплаты —
|
(trial — явный `start_trial`, не auto при create); после оплаты —
|
||||||
возврат к созданию/редактированию
|
возврат к созданию/редактированию
|
||||||
@@ -398,6 +401,11 @@ legal stubs; позиция по подписке **владельца**; DNS/SP
|
|||||||
|
|
||||||
Лайки/дизлайки отзывов: `PUT/DELETE /v1/reviews/:id/vote`, поле `my_vote` в ответах отзывов (EventHubBack#47).
|
Лайки/дизлайки отзывов: `PUT/DELETE /v1/reviews/:id/vote`, поле `my_vote` в ответах отзывов (EventHubBack#47).
|
||||||
|
|
||||||
|
## 7.1. Фаза 3 — архив (Spec#19 / Spec#20)
|
||||||
|
|
||||||
|
Канон: [`ARCHIVE.md`](ARCHIVE.md). Hot = текущий месяц + будущее; warm = 3 мес;
|
||||||
|
cold = файлы. UI истории — React, не iframe. Код: Back#76, Front#73.
|
||||||
|
|
||||||
## 8. Источники истины
|
## 8. Источники истины
|
||||||
|
|
||||||
- Маршруты и поведение: `EventHubBack/src/eventhub_app.erl` + handlers
|
- Маршруты и поведение: `EventHubBack/src/eventhub_app.erl` + handlers
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
## 5. [Stage UI в Cursor Browser](STAGE-BROWSER.md) (HTTPS `*.calentiq.com`; proxy — fallback)
|
## 5. [Stage UI в Cursor Browser](STAGE-BROWSER.md) (HTTPS `*.calentiq.com`; proxy — fallback)
|
||||||
## 6. [UX backlog (Client UI)](UX-BACKLOG.md)
|
## 6. [UX backlog (Client UI)](UX-BACKLOG.md)
|
||||||
## 7. [Карта архитектуры для Zed/агентов](ZED-ARCHITECTURE.md)
|
## 7. [Карта архитектуры для Zed/агентов](ZED-ARCHITECTURE.md)
|
||||||
|
## 8. [Архив календаря (Фаза 3)](ARCHIVE.md)
|
||||||
|
|
||||||
# **Репозитории разработки EventHub**
|
# **Репозитории разработки EventHub**
|
||||||
## 1. [EventHub Backend](https://git.sabilin.com/EventHub/EventHubBack)
|
## 1. [EventHub Backend](https://git.sabilin.com/EventHub/EventHubBack)
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@
|
|||||||
|-------|------------|-------|------|
|
|-------|------------|-------|------|
|
||||||
| Auth / session | `handler_login`, `handler_register`, `handler_auth`, `handler_refresh`, `handler_verify` | `logic_auth`, `logic_auth_session`, `logic_user` | `core_user`, `core_session`, `core_auth_session`, `core_verification` |
|
| Auth / session | `handler_login`, `handler_register`, `handler_auth`, `handler_refresh`, `handler_verify` | `logic_auth`, `logic_auth_session`, `logic_user` | `core_user`, `core_session`, `core_auth_session`, `core_verification` |
|
||||||
| Password reset | `handler_forgot_password`, `handler_reset_password` | `logic_password_reset` | `core_password_reset` |
|
| Password reset | `handler_forgot_password`, `handler_reset_password` | `logic_password_reset` | `core_password_reset` |
|
||||||
| Calendars | `handler_calendars`, `handler_calendar_by_id`, `handler_calendar_view` | `logic_calendar` | `core_calendar` |
|
| Calendars | `handler_calendars`, `handler_calendar_by_id` | `logic_calendar` | `core_calendar` |
|
||||||
| Specialists | `handler_calendar_specialists`, `handler_specialist_invites`, `handler_calendar_specialist_invites` | `logic_calendar_specialist`, `logic_specialist_invite` | `core_calendar_specialist`, `core_specialist_invite` |
|
| Specialists | `handler_calendar_specialists`, `handler_specialist_invites`, `handler_calendar_specialist_invites` | `logic_calendar_specialist`, `logic_specialist_invite` | `core_calendar_specialist`, `core_specialist_invite` |
|
||||||
| Follow | `handler_calendar_follow`, `handler_user_following` | `logic_calendar_follow` | `core_calendar_follow` |
|
| Follow | `handler_calendar_follow`, `handler_user_following` | `logic_calendar_follow` | `core_calendar_follow` |
|
||||||
| Events | `handler_events`, `handler_event_by_id`, `handler_event_occurrences` | `logic_event`, `logic_recurrence` | `core_event` |
|
| Events | `handler_events`, `handler_event_by_id`, `handler_event_occurrences` | `logic_event`, `logic_recurrence` | `core_event` |
|
||||||
|
|||||||
Reference in New Issue
Block a user