docs(spec): calendar_share co-editor/deputy and Time Arc alignment.

Back §2.1.3 API/ACL; Front §3.3 share busy/ghost. Refs EventHub/EventHubBack#73
This commit is contained in:
2026-08-15 23:20:39 +03:00
parent 1182568c9d
commit 4dd741ba48
7 changed files with 342 additions and 161 deletions
+130 -130
View File
@@ -1,130 +1,130 @@
--- ---
description: EventHub — команды только через WSL/bash; PowerShell для сложного запрещён description: EventHub — команды только через WSL/bash; PowerShell для сложного запрещён
alwaysApply: true alwaysApply: true
--- ---
# EventHub: как запускать команды (обязательно) # EventHub: как запускать команды (обязательно)
## Проблема ## Проблема
PowerShell в Cursor **ломает** кавычки, `$`, JSON, heredoc и вложенный `bash -lc "..."`. Из‑за этого агенты снова и снова получают `unexpected EOF`, `Invalid JSON`, пустой curl. PowerShell в Cursor **ломает** кавычки, `$`, JSON, heredoc и вложенный `bash -lc "..."`. Из‑за этого агенты снова и снова получают `unexpected EOF`, `Invalid JSON`, пустой curl.
## Правило ## Правило
1. **Нельзя** гонять сложные one-liner’ы в PowerShell (npm, curl+JSON, python -c, git commit heredoc, вложенный bash с кавычками). 1. **Нельзя** гонять сложные one-liner’ы в PowerShell (npm, curl+JSON, python -c, git commit heredoc, вложенный bash с кавычками).
2. **Можно** в PowerShell только простые вещи: `cd`, `git status`, `ls`, одиночный `wsl …` без вложенных кавычек в аргументе. 2. **Можно** в PowerShell только простые вещи: `cd`, `git status`, `ls`, одиночный `wsl …` без вложенных кавычек в аргументе.
3. Рабочий способ — **WSL bash** или **файл `.sh` + launcher** (см. ниже). 3. Рабочий способ — **WSL bash** или **файл `.sh` + launcher** (см. ниже).
## CRLF / «No such file or directory» (обязательно) ## CRLF / «No such file or directory» (обязательно)
Cursor `Write` на Windows часто сохраняет `.sh` с **CRLF**. Тогда: Cursor `Write` на Windows часто сохраняет `.sh` с **CRLF**. Тогда:
```text ```text
wsl -e bash /mnt/c/.../EventHubFront/.tmp-run.sh wsl -e bash /mnt/c/.../EventHubFront/.tmp-run.sh
# bash: .../.tmp-run.sh: No such file or directory # bash: .../.tmp-run.sh: No such file or directory
``` ```
Файл **есть** (`ls` его видит) — ломается shebang (`#!/usr/bin/env bash\r`). Это **не** «файл не записался». Файл **есть** (`ls` его видит) — ломается shebang (`#!/usr/bin/env bash\r`). Это **не** «файл не записался».
### Канон запуска любого `.sh` с `/mnt/c/...` ### Канон запуска любого `.sh` с `/mnt/c/...`
**Всегда** через хелпер (снимает `\r`, потом `bash`): **Всегда** через хелпер (снимает `\r`, потом `bash`):
```text ```text
wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront/.tmp-run.sh wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront/.tmp-run.sh
``` ```
**Запрещено** после `Write` голый: **Запрещено** после `Write` голый:
```text ```text
wsl -e bash /mnt/c/.../repo/.tmp-something.sh wsl -e bash /mnt/c/.../repo/.tmp-something.sh
``` ```
Альтернатива без хелпера (тот же смысл): Альтернатива без хелпера (тот же смысл):
```text ```text
wsl -e bash -lc 'sed -i "s/\r$//" /mnt/c/.../script.sh && bash /mnt/c/.../script.sh' wsl -e bash -lc 'sed -i "s/\r$//" /mnt/c/.../script.sh && bash /mnt/c/.../script.sh'
``` ```
Предпочтительно класть долгоживущие скрипты в `/mnt/c/Users/alexc/.cursor/eventhub/` и тоже гонять через `run-wsl-sh.sh`. Предпочтительно класть долгоживущие скрипты в `/mnt/c/Users/alexc/.cursor/eventhub/` и тоже гонять через `run-wsl-sh.sh`.
## Канон (копируй) ## Канон (копируй)
### Вариант A — скрипт (предпочтительно) ### Вариант A — скрипт (предпочтительно)
1. Запиши команды в файл, например `EventHubBack/.tmp-run.sh` или `~/.cursor/eventhub/….sh` (`#!/usr/bin/env bash`). 1. Запиши команды в файл, например `EventHubBack/.tmp-run.sh` или `~/.cursor/eventhub/….sh` (`#!/usr/bin/env bash`).
2. Запусти **только** так (из PowerShell, без вложенных кавычек): 2. Запусти **только** так (из PowerShell, без вложенных кавычек):
```text ```text
wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack/.tmp-run.sh wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack/.tmp-run.sh
``` ```
### Вариант B — один простой WSL вызов ### Вариант B — один простой WSL вызов
Только если внутри **одинарные** кавычки bash и нет `"`/`$` конфликтов с PS (и **нет** нового `.sh` с диска C): Только если внутри **одинарные** кавычки bash и нет `"`/`$` конфликтов с PS (и **нет** нового `.sh` с диска C):
```text ```text
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront && npm run lint' wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront && npm run lint'
``` ```
В PowerShell для `-lc` используй **одинарные** кавычки снаружи (`'...'`), внутри — обычный bash. В PowerShell для `-lc` используй **одинарные** кавычки снаружи (`'...'`), внутри — обычный bash.
### Запрещено ### Запрещено
```text ```text
wsl -e bash /mnt/c/.../.tmp-run.sh # без run-wsl-sh / sed — CRLF → No such file wsl -e bash /mnt/c/.../.tmp-run.sh # без run-wsl-sh / sed — CRLF → No such file
wsl -e bash -lc ".... python -c \"import...\" ...." # ломается wsl -e bash -lc ".... python -c \"import...\" ...." # ломается
curl ... -d "{\"email\":\"$x\"}" # в PS ломается curl ... -d "{\"email\":\"$x\"}" # в PS ломается
git commit -m "$(cat <<'EOF' ...)" # в PS не так git commit -m "$(cat <<'EOF' ...)" # в PS не так
wsl -e bash -lc 'git-commit.sh … fix(ci): subject' # ( ) ломают bash wsl -e bash -lc 'git-commit.sh … fix(ci): subject' # ( ) ломают bash
wsl -e bash -lc 'git commit -m "fix(ci): …"' # то же wsl -e bash -lc 'git commit -m "fix(ci): …"' # то же
npm / npx / node из Windows # сломан npm / npx / node из Windows # сломан
``` ```
### Git commit — канон (subject с `( )`, `#`, `$`) ### Git commit — канон (subject с `( )`, `#`, `$`)
1. Текст коммита — в файл репо, напр. `.tmp-commit-msg.txt` (без heredoc из PS). 1. Текст коммита — в файл репо, напр. `.tmp-commit-msg.txt` (без heredoc из PS).
2. Скрипт `.tmp-commit.sh`: 2. Скрипт `.tmp-commit.sh`:
```bash ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack
git add path1 path2 git add path1 path2
GIT_EDITOR=true git commit -F .tmp-commit-msg.txt GIT_EDITOR=true git commit -F .tmp-commit-msg.txt
``` ```
3. Из PowerShell: 3. Из PowerShell:
```text ```text
wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack/.tmp-commit.sh wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack/.tmp-commit.sh
``` ```
**Не** передавать сообщение аргументом в `git-commit.sh` / `-m` через `wsl -lc` — скобки в `fix(ci):` парсятся как subshell. **Не** передавать сообщение аргументом в `git-commit.sh` / `-m` через `wsl -lc` — скобки в `fix(ci):` парсятся как subshell.
## Стек → команда ## Стек → команда
| Задача | Как | | Задача | Как |
|-----|-----| |-----|-----|
| Front lint/build/e2e | `.sh` + `run-wsl-sh.sh` или `-lc` с PATH-fix + `npm …` | | Front lint/build/e2e | `.sh` + `run-wsl-sh.sh` или `-lc` с PATH-fix + `npm …` |
| Front перед push main | сначала `npm run test:e2e:ift` в WSL, потом `git-push-main.sh` | | Front перед push main | сначала `npm run test:e2e:ift` в WSL, потом `git-push-main.sh` |
| Back rebar/eunit | `source scripts/wsl-dev-env.sh` в WSL | | Back rebar/eunit | `source scripts/wsl-dev-env.sh` в WSL |
| Git commit/push | **commit:** `.tmp-commit-msg.txt` + `git commit -F` в `.sh` через `run-wsl-sh.sh`; **push:** `bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh …` | | Git commit/push | **commit:** `.tmp-commit-msg.txt` + `git commit -F` в `.sh` через `run-wsl-sh.sh`; **push:** `bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh …` |
| JSON/API/python | скрипт `.sh`/`.py` + `run-wsl-sh.sh` | | JSON/API/python | скрипт `.sh`/`.py` + `run-wsl-sh.sh` |
## Git push (обязательно) ## Git push (обязательно)
**Запрещено агентам:** `git push`, `git push origin`, `git push origin HEAD` / `master` напрямую — без токена зависают на HTTPS prompt. **Запрещено агентам:** `git push`, `git push origin`, `git push origin HEAD` / `master` напрямую — без токена зависают на HTTPS prompt.
**Канон:** **Канон:**
```text ```text
bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack
bash /mnt/c/Users/alexc/.cursor/eventhub/git-push-main.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront bash /mnt/c/Users/alexc/.cursor/eventhub/git-push-main.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront
``` ```
Путь WSL: `/mnt/c/Users/alexc/.cursor/eventhub/` (не `~/.cursor/…` — home в WSL может отличаться). Путь WSL: `/mnt/c/Users/alexc/.cursor/eventhub/` (не `~/.cursor/…` — home в WSL может отличаться).
Credential store в WSL уже настроен (`credential.helper=store` + `git.sabilin.com`); хелпер всё равно предпочтителен (явный URL с токеном из secrets). Credential store в WSL уже настроен (`credential.helper=store` + `git.sabilin.com`); хелпер всё равно предпочтителен (явный URL с токеном из secrets).
## Перед push Front ## Перед push Front
1. WSL: `npm run lint && npm run build` (ловит синтаксис/TS) 1. WSL: `npm run lint && npm run build` (ловит синтаксис/TS)
2. WSL: `npm run test:e2e:ift` 2. WSL: `npm run test:e2e:ift`
3. Только потом push через `git-push-main.sh`. **Не пушить**, если lint/build/IFT красные. 3. Только потом push через `git-push-main.sh`. **Не пушить**, если lint/build/IFT красные.
## Частые сбои (подробнее) ## Частые сбои (подробнее)
См. **`agent-pitfalls.mdc`**: CRLF/`run-wsl-sh.sh`, git `-F`, `&&` в PS, docker dev scale, `git clean` vs `.cursor/`. См. **`agent-pitfalls.mdc`**: CRLF/`run-wsl-sh.sh`, git `-F`, `&&` в PS, docker dev scale, `git clean` vs `.cursor/`.
+38 -12
View File
@@ -13,8 +13,9 @@ EventHub — платформа для управления событиями
### 2.1. Календари ### 2.1. Календари
- CRUD календаря (название, описание, теги, владелец) - CRUD календаря (название, описание, теги, владелец)
- Расшаривание по ссылке / приглашения с правами (`calendar_share`) — **фаза 2** (таблица есть; - Расшаривание по ссылке / приглашения с правами (`calendar_share`) — invite/accept/revoke +
user HTTP API и UI — Future, см. §2.1.3) ACL `read|write|admin` (Back#73); personal и commercial (заместитель); UI — Front;
не путать со `specialist_invite` / follow.
- Типы календарей: `personal` | `commercial` — семантика и коммерческий контур: **§2.1.2** - Типы календарей: `personal` | `commercial` — семантика и коммерческий контур: **§2.1.2**
- Гибкое подтверждение заявок: `auto` | `manual` | `{timeout, N}` (секунды) — детали §2.1.2 / §2.3 - Гибкое подтверждение заявок: `auto` | `manual` | `{timeout, N}` (секунды) — детали §2.1.2 / §2.3
- Теги календаря, рейтинг (средняя оценка, количество голосов) - Теги календаря, рейтинг (средняя оценка, количество голосов)
@@ -68,7 +69,7 @@ EventHub — платформа для управления событиями
| | `personal` | `commercial` | | | `personal` | `commercial` |
|---|---|---| |---|---|---|
| Подписка владельца | не требуется | нужна active (trial или paid), иначе **restricted** | | Подписка владельца | не требуется | нужна active (trial или paid), иначе **restricted** |
| Видимость чужим | только владелец (+ share — фаза 2) | публичный просмотр; в search/discover — только если **не** restricted | | Видимость чужим | только владелец или share grant | публичный просмотр; в search/discover — только если **не** restricted |
| Booking чужим | **запрещён** (бэкенд `403`, код `personal_calendar`) | разрешён при active подписке владельца | | Booking чужим | **запрещён** (бэкенд `403`, код `personal_calendar`) | разрешён при active подписке владельца |
| Follow | нет (нет доступа) | да | | Follow | нет (нет доступа) | да |
| Специалисты | не используются | CRUD владельца; `specialist_id` на событии | | Специалисты | не используются | CRUD владельца; `specialist_id` на событии |
@@ -256,16 +257,33 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
**сделано** (Back#75): `GET/PUT /v1/notifications/prefs`, **сделано** (Back#75): `GET/PUT /v1/notifications/prefs`,
`GET /v1/push/vapid-public-key`, `POST/DELETE /v1/push/subscriptions`; `GET /v1/push/vapid-public-key`, `POST/DELETE /v1/push/subscriptions`;
UI — Front#71. UI — Front#71.
- `calendar_share` (`read` | `write` | `admin`): низкий приоритет; не путать с follow, - `calendar_share` (`read` | `write` | `admin`): соредактор personal / заместитель commercial;
specialist_invite и платной subscription. User HTTP API и UI — до отдельной задачи invite/accept/revoke + grants; personal **не** в search/discovery. Не путать с follow,
(таблица в схеме есть, см. §2.1.3). specialist_invite и платной subscription.
- `client_type=mobile` — хвост, когда появится нативный клиент. - `client_type=mobile` — хвост, когда появится нативный клиент.
- **Не в фазе 2:** оплата услуги клиентом (B2C) — вне горизонта продукта. - **Не в фазе 2:** оплата услуги клиентом (B2C) — вне горизонта продукта.
### 2.1.3. Share / приглашения (фаза 2) ### 2.1.3. Share / приглашения (соредактор и заместитель)
Таблица `calendar_share` (`calendar_id`, `user_id`, `rights`: `read` | `write` | `admin`)
существует в схеме. User HTTP API и клиентский UI — не стартовать, пока нет явного Таблица `calendar_share` (`id`, `calendar_id`, `user_id`, `rights`: `read` | `write` | `admin`,
сценария соредактора personal (совместная работа сейчас: specialist_invite + booking inbox). `mirror_to_default`) и `calendar_share_invite` (зеркало specialist_invite + `rights`).
- Invite создаёт **owner** или share `admin` (personal **и** commercial).
- Accept → upsert grant; для personal `mirror_to_default` default `true` (выбор invitee);
commercial mirror игнорируется / false.
- ACL: `can_access` personal = owner | share; commercial public view + share edit;
`can_edit` = owner | share `write|admin`. DELETE calendar / смена type — только owner.
- `GET /v1/calendars` = owned shared (`role`, `share_rights`, `mirror_to_default`).
- Personal **никогда** не discoverable в `/v1/search`.
API:
- `GET/POST /v1/calendars/:id/share-invites`, `DELETE …/:invite_id`
- `GET /v1/calendars/:id/shares`, `DELETE/PUT …/shares/:user_id`
- `PUT /v1/user/shares/:calendar_id` `{mirror_to_default}`
- `GET /v1/user/share-invites`, `POST /v1/share-invites/:id/accept|decline`,
`POST /v1/share-invites/accept` `{token}`
Не путать со `specialist_invite` (booking team) и `calendar_follow` (публичный follow).
### 2.2. События (расширенная версия с повторяющимися событиями) ### 2.2. События (расширенная версия с повторяющимися событиями)
@@ -576,10 +594,12 @@ src/
- `GET /v1/user/reviews` — отзывы пользователя. - `GET /v1/user/reviews` — отзывы пользователя.
- `GET /v1/user/following` — календари, которые пользователь отслеживает (follow). - `GET /v1/user/following` — календари, которые пользователь отслеживает (follow).
- `GET /v1/search` — поиск; **без токена** (гость) — только commercial по `can_access`; - `GET /v1/search` — поиск; **без токена** (гость) — только commercial по `can_access`;
пустой запрос + пагинация/`type` — discovery tops. С Bearer — как раньше (включая свои personal). пустой запрос + пагинация/`type` — discovery tops. Personal **никогда** не в выдаче
(в т.ч. свои). С Bearer — commercial + доступные; busy для Time Arc берётся из personal отдельно.
В calendar-результатах — `image_url` (если задан на календаре). Upload/multipart В calendar-результатах — `image_url` (если задан на календаре). Upload/multipart
**не** входит в контракт: поле URL. **не** входит в контракт: поле URL.
- `GET /v1/calendars` — список календарей (auth). - `GET /v1/calendars` — список календарей (auth): owned shared (`role`, `share_rights`,
`mirror_to_default`).
- `POST /v1/calendars` — создать календарь (`commercial` → нужна уже active sub/trial; - `POST /v1/calendars` — создать календарь (`commercial` → нужна уже active sub/trial;
иначе `402`; auto-start trial нет). иначе `402`; auto-start trial нет).
- `GET /v1/calendars/:id` — календарь **или unique `short_name`**. **Без токена** для active commercial (`following: false`); - `GET /v1/calendars/:id` — календарь **или unique `short_name`**. **Без токена** для active commercial (`following: false`);
@@ -598,6 +618,12 @@ src/
- `GET /v1/user/specialist-invites` — входящие приглашения. - `GET /v1/user/specialist-invites` — входящие приглашения.
- `POST /v1/specialist-invites/:id/accept` | `…/decline` — ответ invitee. - `POST /v1/specialist-invites/:id/accept` | `…/decline` — ответ invitee.
- `POST /v1/specialist-invites/accept` `{ token }` — accept по email deep-link. - `POST /v1/specialist-invites/accept` `{ token }` — accept по email deep-link.
- `GET/POST /v1/calendars/:id/share-invites` — исходящие share invite / создать (owner|admin).
- `DELETE /v1/calendars/:id/share-invites/:invite_id` — отменить pending.
- `GET /v1/calendars/:id/shares` — принятые grants; `DELETE/PUT …/shares/:user_id`.
- `PUT /v1/user/shares/:calendar_id` — свой `mirror_to_default`.
- `GET /v1/user/share-invites` — входящие share invites.
- `POST /v1/share-invites/:id/accept` | `…/decline`; `POST /v1/share-invites/accept` `{token}`.
- `GET /v1/calendars/:calendar_id/events` — события календаря (гость: commercial). - `GET /v1/calendars/:calendar_id/events` — события календаря (гость: commercial).
В JSON каждого события commercial-календаря — опциональное поле В JSON каждого события commercial-календаря — опциональное поле
`booking_occupancy`: `"free"` | `"pending"` | `"confirmed"`. `booking_occupancy`: `"free"` | `"pending"` | `"confirmed"`.
+9 -3
View File
@@ -151,9 +151,9 @@ CONTROL→bookings. Контент: free gaps / события сегодня /
### 3.3. Time Arc (живая сетка, Front#50) ### 3.3. Time Arc (живая сетка, Front#50)
На commercial week/day в режиме Студия клиент считает предложенный час без LLM и без Back: свободные слоты студии ∩ занятость пользователя (personal + confirmed bookings) × mood (FLOW — воздух и середина дня, MOMENTUM — ближайший, CONTROL — доля свободных мест). Одна ячейка — `data-time-arc`; коллизии — `data-collided`. Тап — обычный Book. На commercial week/day в режиме Студия клиент считает предложенный час без LLM и без Back: свободные слоты студии ∩ занятость пользователя (personal + confirmed bookings + **mirror** shared-personal) × mood (FLOW — воздух и середина дня, MOMENTUM — ближайший, CONTROL — доля свободных мест). Одна ячейка — `data-time-arc`; коллизии — `data-collided`. Тап — обычный Book.
Владелец: черновики слотов из прошлой ISO-недели в дырах текущей; «Создать неделю» — Владелец **или** share `write|admin` (заместитель): черновики слотов из прошлой ISO-недели в дырах текущей; «Создать неделю» —
preview, затем существующий POST events. После создания черновик снимается сразу preview, затем существующий POST events. После создания черновик снимается сразу
(без F5), в том числе если время в форме сдвинули (прошлый час → +1 день / правка start). (без F5), в том числе если время в форме сдвинули (прошлый час → +1 день / правка start).
Фраза с часом уже в прошлом («11» в 14:00) ставит черновик на следующий день, тот же час. Фраза с часом уже в прошлом («11» в 14:00) ставит черновик на следующий день, тот же час.
@@ -169,6 +169,11 @@ preview, затем существующий POST events. После созда
**v4 (Front#53):** pending occupancy тянет demand-черновики на тот же час в свободные дни текущей ISO-недели (`data-time-arc-demand`). Long-press пустого часа week/day у владельца commercial — локальный разбор фразы → phrase-черновик (`data-time-arc-phrase`); клик и long-press месяца по-прежнему открывают create. Специалист на чужой студии: дуга на своём будущем pending-слоте. Без LLM / AiRouter. **v4 (Front#53):** pending occupancy тянет demand-черновики на тот же час в свободные дни текущей ISO-недели (`data-time-arc-demand`). Long-press пустого часа week/day у владельца commercial — локальный разбор фразы → phrase-черновик (`data-time-arc-phrase`); клик и long-press месяца по-прежнему открывают create. Специалист на чужой студии: дуга на своём будущем pending-слоте. Без LLM / AiRouter.
**Share / заместитель (Back#73, согласование Front#49):** `GET /v1/calendars` включает shared;
busy Time Arc += события personal с `mirror_to_default`; ghost/demand/phrase у share
`write|admin`; Following не смешивать с share. Confirmed foreign bookings overlay на
default personal — только `role=owner` ids в ownSet.
## 4. Маршруты ## 4. Маршруты
Публичные: Публичные:
@@ -379,7 +384,8 @@ legal stubs; позиция по подписке **владельца**; DNS/SP
- waitlist: API Back#72; включение — чекбокс «Лист ожидания» - waitlist: API Back#72; включение — чекбокс «Лист ожидания»
(`settings.waitlist_enabled`) в настройках commercial; join/leave на (`settings.waitlist_enabled`) в настройках commercial; join/leave на
карточке события при полном слоте — Front#70 карточке события при полном слоте — Front#70
- шаринг календаря с правами (`calendar_share`) — низкий приоритет, не путать со specialist_invite - шаринг календаря с правами (`calendar_share`) — Back#73 API + Front list/invites/mirror;
не путать со specialist_invite / Following
- явный `client_type=mobile` — хвост (фаза 3 бэка / нативный клиент) - явный `client_type=mobile` — хвост (фаза 3 бэка / нативный клиент)
**Вне горизонта (не фаза 2):** оплата услуги клиентом (B2C). **Вне горизонта (не фаза 2):** оплата услуги клиентом (B2C).
+1
View File
@@ -5,6 +5,7 @@
## 4. [Правила работы (workflow)](WORKFLOW.md) ## 4. [Правила работы (workflow)](WORKFLOW.md)
## 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)
# **Репозитории разработки EventHub** # **Репозитории разработки EventHub**
## 1. [EventHub Backend](https://git.sabilin.com/EventHub/EventHubBack) ## 1. [EventHub Backend](https://git.sabilin.com/EventHub/EventHubBack)
+2 -1
View File
@@ -7,8 +7,9 @@ E2E и smoke иногда оставляют commercial-календари с р
1. **Не** удалять «живые» smoke-календари пилота без явного фильтра. 1. **Не** удалять «живые» smoke-календари пилота без явного фильтра.
2. Чистить только по безопасным маркерам (название/тег/описание содержат `e2e`, `E2E`, `playwright`, `ift-litter`, или owner = известный e2e user). 2. Чистить только по безопасным маркерам (название/тег/описание содержат `e2e`, `E2E`, `playwright`, `ift-litter`, или owner = известный e2e user).
3. Soft-delete через API (`DELETE /v1/calendars/:id` владельцем / admin), не ручной wipe Mnesia. 3. Soft-delete через API (`DELETE /v1/calendars/:id` владельцем / admin) для точечного litter; **полный** сброс стенда — Back CI `workflow_dispatch` `wipe-mnesia-ift` / `wipe-mnesia-stage` (`scripts/ci-full-mnesia-wipe.sh`), только вручную, не prod.
4. Front already tracks calendars created in-session via `e2e/helpers/standCleanup.ts` — предпочитать это для новых тестов. 4. Front already tracks calendars created in-session via `e2e/helpers/standCleanup.ts` — предпочитать это для новых тестов.
5. Catalog BizCal (`short_name` `bizcal-*`, tag `BizCal`) и disposable LargeCal — не путать с smoke; seed: Front `seed-catalog-ift|stage` (нужны `BIZCAL_CLIENT_*` в `.env` стенда).
## Suggested filter (admin / ops) ## Suggested filter (admin / ops)
+144
View File
@@ -0,0 +1,144 @@
# EventHub — карта для AI-агентов (Zed / Cursor)
Кросс-репо ориентир: где какой модуль и куда класть правки. Детали API — в `EventHubBackSpec.md` / `EventHubFrontSpec.md` / `EventHubFrontAdminSpec.md`. Процесс — `WORKFLOW.md`.
## Репозитории
| Репо | Стек | Зона |
|------|------|------|
| `EventHubBack` | Erlang/OTP 28, Cowboy, Mnesia | Public + Admin API |
| `EventHubFront` | React + Vite + TanStack Query | Клиентский SPA (CalenTIQ) |
| `EventHubFrontAdmin` | React + Vite | Admin SPA |
| `EventHubSpec` | Markdown | Спеки, UI-PARITY, workflow |
| `EventHubAiRouter` | FastAPI + LiteLLM | Zed gateway / routing |
| `EventHubDevOps` | Docker Swarm | Стенды, deploy |
**Правило:** один агент / один PR — один репо. Не смешивать Front и Back в одной сессии правок.
Бренд UI: **CalenTIQ**. Внутренние имена репо `EventHub*` не менять.
## Домен (кратко)
- **User** — владелец календарей / запись на слоты
- **Calendar** — расписание, специалисты, follow
- **Event** — слоты (в т.ч. recurring)
- **Booking / booking-request** — запись / заявка
- **Ticket / Review / Report** — поддержка, отзывы, жалобы
- **Subscription** — подписка владельца
- **Specialist invite** — приглашение специалиста в календарь
- **Admin** — модерация, аудит, stats (отдельный JWT)
Стенды: **dev** локально, **IFT**, **stage** (`https://stage.calentiq.com`, admin `https://admin.stage.calentiq.com`).
---
## EventHubBack (`src/`)
Слои (сверху вниз):
| Слой | Путь | Ответственность |
|------|------|-----------------|
| HTTP | `handlers/`, `handlers/admin/` | Cowboy handlers, валидация входа/ответа |
| Logic | `logic/` | Бизнес-правила, оркестрация core |
| Core | `core/` | Mnesia-таблицы / CRUD сущности |
| Infra | `infra/` | Mnesia, auth helpers, workers, migrations |
| Middleware | `middlewares/` | Auth JWT и т.п. |
### Типичные пары handler → logic → core
| Домен | Handler(s) | Logic | Core |
|-------|------------|-------|------|
| 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` |
| Calendars | `handler_calendars`, `handler_calendar_by_id`, `handler_calendar_view` | `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` |
| 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` |
| Bookings | `handler_bookings`, `handler_booking_by_id`, `handler_user_bookings`, `handler_user_booking_requests` | `logic_booking` | `core_booking` |
| Reviews | `handler_reviews`, `handler_review_by_id`, `handler_user_reviews`, `handler_review_vote` | `logic_review` | `core_review`, `core_review_vote` |
| Tickets | `handler_tickets`, `handler_ticket_by_id` | `logic_ticket` | `core_ticket` |
| Reports | `handler_reports` | `logic_report` | `core_report` |
| Search / lookup | `handler_search`, `handler_users_lookup` | `logic_search`, `logic_user_lookup` | — |
| Subscription | `handler_subscription` | `logic_subscription` | `core_subscription` |
| Notifications | — | `logic_notification` | `core_notification` |
| Admin API | `handlers/admin/admin_handler_*` | `logic_admin`, `logic_moderation`, `logic_automoderation`, `logic_stats` | `core_admin`, `core_admin_*`, `core_banned_words`, `core_automod_*` |
Миграции: `src/migrations/`. Сборка/тесты: **только WSL** (`source scripts/wsl-dev-env.sh`, `rebar3 eunit`).
---
## EventHubFront (`src/`)
| Путь | Зачем |
|------|--------|
| `pages/` | Экраны маршрутов |
| `api/*Api.ts` | HTTP к Back |
| `components/calendar/` | UI календаря / workspace |
| `layouts/AppShell.tsx` | Оболочка |
| `store/` | Zustand (auth, mood, locale, calendar context) |
| `hooks/` | React hooks (API, WS, swipe) |
| `lib/` | Утилиты (payloads, schedule, booking display) |
| `i18n/` | Локали |
### Страницы → смысл
| Page | Маршрут (ориентир) | Тема |
|------|-------------------|------|
| `DiscoverPage` | discover | Поиск / лента |
| `CalendarsPage` | calendars | Список календарей |
| `CalendarWorkspacePage` | `/c/:id` | Основной workspace слотов |
| `BookingsPage` | bookings | Мои записи / inbox заявок |
| `ReviewsPage` | reviews | Отзывы |
| `TicketsPage` / `TicketDetailPage` | tickets | Тикеты |
| `SubscriptionPage` | subscription | Подписка |
| `FollowingPage` | following | Подписки на календари |
| `SpecialistInvitesPage` | invites | Инвайты специалиста |
| `ProfilePage` / `MorePage` | profile / more | Профиль / меню |
| `pages/auth/*` | login/register/… | Auth |
npm / Playwright — **только WSL** (см. `.cursor/rules/npm-wsl.mdc`).
---
## EventHubFrontAdmin (`src/`)
| Путь | Зачем |
|------|--------|
| `pages/dashboard` | Дашборд |
| `pages/users`, `calendars`, `events` | Справочники |
| `pages/tickets`, `reports`, `inbox` | Модерация / inbox |
| `pages/reviews`, `automod`, `banned-words` | Контент / автомод |
| `pages/subscriptions`, `admins`, `audit`, `monitoring` | Биллинг / админы / аудит / метрики |
| `api/*Api.ts` | Admin API client |
| `store/authStore.ts` | Admin session |
---
## EventHubAiRouter (`router/`, `config/`)
| Файл / папка | Зачем |
|--------------|--------|
| `router/router.py` | Zed `/v1/chat/completions`, ветка tools vs text |
| `router/hierarchical.py` | Plan (Qwen3.8-Max) / workers / verify (DeepSeek) / plan_confirm |
| `router/agent_hier.py` | При `tools`: Coder-30B executor + tool_calls (Zed Write) |
| `router/orchestrator.py` | Tiers / lanes ABC, Redis session |
| `config/orchestration.yaml` | Модели plan/verify/executor, таймауты, synthetic=never |
| `config/providers.yaml` | Novita / VPN (`novita-planner` = Max) |
| `scripts/gen-litellm-config.py` | Генерация LiteLLM YAML |
IFT: `https://ai-router.ift.calentiq.com/v1`, модель `smart-router`.
Клиент-канон: **Zed Agent** (openai-compatible) → AiRouter; не Claude Code/Codex ACP как primary (обход бюджета).
---
## Куда править (шпаргалка)
| Задача | Репо | Куда смотреть |
|--------|------|----------------|
| Баг API / Mnesia | Back | `handlers``logic``core` |
| UI клиент | Front | `pages` + `api` + `components/calendar` |
| UI админки | FrontAdmin | `pages` + `api` |
| Контракт / процесс | Spec | `*Spec.md`, `WORKFLOW.md`, `design/` |
| Zed routing / hierarchical | AiRouter | `router/*`, `config/orchestration.yaml` |
Перед крупным планом: **утвердить** шаги с пользователем; не раздувать scope (не трогать соседний репо «заодно»).
+18 -15
View File
@@ -20,17 +20,20 @@ SoT: [`boards/calentiq-ui-board-final.png`](boards/calentiq-ui-board-final.png).
| Bookings list §D | список заявок с Confirm/Decline на `/bookings` | `/bookings` = **grouped inbox**: (A) К подтверждению — pending owner/specialist + Confirm/Decline; (B) Мои записи — participant upcoming/past + Cancel; empty groups hidden. Event card Confirm/Decline **остаётся** обязательным | | Bookings list §D | список заявок с Confirm/Decline на `/bookings` | `/bookings` = **grouped inbox**: (A) К подтверждению — pending owner/specialist + Confirm/Decline; (B) Мои записи — participant upcoming/past + Cancel; empty groups hidden. Event card Confirm/Decline **остаётся** обязательным |
| Login §D phone chrome | три phone frames + (на борде) Google CTA | brand-first hero + mood `--auth-*`; email/password only; compact mood на auth; taglines — в mood sheet AppShell (не в `/more`); **без** Google OAuth | | Login §D phone chrome | три phone frames + (на борде) Google CTA | brand-first hero + mood `--auth-*`; email/password only; compact mood на auth; taglines — в mood sheet AppShell (не в `/more`); **без** Google OAuth |
| Mood placement | board §C switcher cards | **Front#40/#45:** mood chip в AppShell рядом с BrandWordmark (+ compact auth); mobile chip icon-only (полный CalenTIQ wordmark); **не** в `/more`, **не** в toolbar Month/Week/Day; 3 moods, без flip / CREATOR; switcher glyphs §C (sprout/bolt/briefcase), Time Arc appicon — только favicon | | Mood placement | board §C switcher cards | **Front#40/#45:** mood chip в AppShell рядом с BrandWordmark (+ compact auth); mobile chip icon-only (полный CalenTIQ wordmark); **не** в `/more`, **не** в toolbar Month/Week/Day; 3 moods, без flip / CREATOR; switcher glyphs §C (sprout/bolt/briefcase), Time Arc appicon — только favicon |
| Lens + strip | board Today/Upcoming rail only | **Front#39 + controls IA:** lens Обзор/На сегодня/Заявки в **шапке strip** (underline tabs / mobile sheet); view Месяц/Неделя/День рядом с периодом; owner schedule actions отдельным рядом; localStorage only; soft-default по mood | | Lens + strip | board Today/Upcoming rail only | **Front#46:** lens инфо-табло **под AppShell** (не в ViewMode toolbar); Обзор/Сегодня/Заявки + метрики; soft-default по mood без auto-pin; view Месяц/Неделя/День в calendar chrome; commercial owner: «+ событие» в toolbar, WeekFill во вкладке Студия |
| Week view headers | — | **Front#40:** одна строка day-headers (без дубля `WeekDayColumn`); scroll `.eh-cal-week-scroll` (mobile ~390) | | Week view headers | — | **Front#40:** одна строка day-headers (без дубля `WeekDayColumn`); scroll `.eh-cal-week-scroll` (mobile ~390) |
| Favicon set | 3 отдельных favicon файла на борде | mood appicon через `applyMood` | | Favicon set | 3 отдельных favicon файла на борде | mood appicon через `applyMood` |
| Desktop nav | phone bottom tabs на борде | desktop: **одна строка** chrome — logo + mood \| nav (Календарь / Найти / Записи / Ещё) \| nickname + logout; bottom nav — mobile | | Desktop nav | phone bottom tabs на борде | desktop: **одна строка** chrome — logo + mood \| nav (Календарь / Найти / Записи / Ещё) \| nickname + logout; bottom nav — mobile |
| Header identity | — | nickname primary (не raw email); email только в `title` tooltip | | Header identity | — | nickname primary (не raw email); email только в `title` tooltip |
| Bookings past/expired | — | `expired` бейдж + Cancel скрыт; empty-state с CTA Discover / календари; soft list time | | Bookings past/expired | — | `expired` бейдж + Cancel скрыт; empty-state с CTA Discover / календари; soft list time |
| Agenda empty copy | — | personal «Нет событий»; commercial «Нет слотов» | | Agenda empty copy | — | personal «Нет событий»; commercial «Свободных окон нет» |
| Default personal | — | **Front#46 / Back#64:** один personal title `Default` / UI «По умолчанию»; `/default`; overlay только на нём; create UI только Студия |
| Owner rail Team/Studio | — | **Front#46:** фиксированная высота panel; Команда invite свёрнут; Студия = hub (WeekFill + Edit); ScrollRegion стрелки |
| More invites | пункт меню | скрыт при pending=0; badge счётчика при pending > 0 | | More invites | пункт меню | скрыт при pending=0; badge счётчика при pending > 0 |
| D9 guest specialist schedule | — | **модель B (Front#35):** roster карточек → тап → фильтр слотов по `event.specialist_id`; «Все слоты студии»; book без specialist в body; **не** chip-filter | | D9 guest specialist schedule | — | **модель B (Front#35):** roster карточек → тап → фильтр слотов по `event.specialist_id`; «Все слоты студии»; book без specialist в body; **не** chip-filter; в «Все» — studio aggregate occupancy |
| Confirmed на personal | — | Front-only overlay confirmed чужих commercial на default personal; title `событие · студия · спец`; клик → `/c/:foreignCalId/e/:eventId` | | Confirmed на personal | — | Front-only overlay confirmed чужих commercial на default personal; title `событие · студия · спец`; клик → `/c/:foreignCalId/e/:eventId` |
| SpecialistsPanel density | — | mobile: имя/теги сверху, actions снизу full-width (Front#33/#35) | | SpecialistsPanel density | — | mobile: имя/теги сверху, actions снизу full-width (Front#33/#35); owner первым + бейдж «Владелец» + галочка «Я специалист» (Front#48) |
| Studio / Мастер commercial grid | board §E denser studio | Owner: Студия (агрегат `start_time`+`duration`, `K/N свободно`, popover) / Мастер (roster filter); месяц — density markers; rail Расписание\|Команда\|Студия; `booking_occupancy` из API; ★ 0.0 на free не показывать; personal без агрегата |
| Stage | полный smoke на stage | **done** 2026-07-27: Login×3 / Calendar×3 + agenda / Discover / CONTROL commercial + Confirm·Decline / More moods | | Stage | полный smoke на stage | **done** 2026-07-27: Login×3 / Calendar×3 + agenda / Discover / CONTROL commercial + Confirm·Decline / More moods |
| Домены calentiq.* | — | публичный stage: `stage.calentiq.com` / `admin.stage.calentiq.com` (канон STAGE-BROWSER.md) | | Домены calentiq.* | — | публичный stage: `stage.calentiq.com` / `admin.stage.calentiq.com` (канон STAGE-BROWSER.md) |
@@ -88,7 +91,7 @@ API: `GET /v1/user/booking-requests` (Back) + существующий `PUT /v1/
изменений. изменений.
3. **SpecialistsPanel density (mobile):** имя/теги сверху, actions снизу full-width. 3. **SpecialistsPanel density (mobile):** имя/теги сверху, actions снизу full-width.
### Mood + Lens (locked Front#39 / placement Front#40) ### Mood + Lens (locked Front#39/#40; dashboard Front#46)
1. **Mood** = визуальная атмосфера (`html[data-mood]`, FLOW/MOMENTUM/CONTROL). Primary UX — 1. **Mood** = визуальная атмосфера (`html[data-mood]`, FLOW/MOMENTUM/CONTROL). Primary UX —
chip → sheet в **AppShell** рядом с BrandWordmark (Front#40); compact на auth. chip → sheet в **AppShell** рядом с BrandWordmark (Front#40); compact на auth.
@@ -97,16 +100,15 @@ API: `GET /v1/user/booking-requests` (Back) + существующий `PUT /v1/
полный CalenTIQ wordmark; desktop — полный label; sheet — §C glyph + label + tagline. полный CalenTIQ wordmark; desktop — полный label; sheet — §C glyph + label + tagline.
Glyphs: FLOW sprout, MOMENTUM bolt, CONTROL briefcase (не Time Arc appicon). Glyphs: FLOW sprout, MOMENTUM bolt, CONTROL briefcase (не Time Arc appicon).
2. **Lens** = отдельный контентный режим (`overview` / `today` / `bookings`), не mood и не 2. **Lens** = отдельный контентный режим (`overview` / `today` / `bookings`), не mood и не
view month/week/day. UI: Обзор / На сегодня / Заявки. Переключатель — в шапке strip view month/week/day. UI: Обзор / Сегодня / Заявки. **Front#46:** инфо-табло **под
(underline/text tabs на desktop; chip → sheet на mobile), **не** рядом с ViewMode. AppShell** (surface + сегменты + метрики/chips), **не** рядом с ViewMode и не в
Soft-default при смене mood, если lens не pinned: FLOW→overview, MOMENTUM→today, calendar toolbar. Soft-default при смене mood **без auto-pin** при клике:
CONTROL→bookings. FLOW→overview, MOMENTUM→today, CONTROL→bookings.
3. **Strip** над сеткой (не на HTML-архиве месяца): Front-only derive — free gaps / 3. Контент lens (не на HTML-архиве): Front-only derive — free gaps / события сегодня /
события сегодня / upcoming bookings. Desktop раскрыт по умолчанию; mobile свёрнут upcoming bookings. Workspace toolbar: селектор / период+view / «+ Новое событие»
(tap раскрывает). Сетка всегда видна. Workspace toolbar: контекст (селектор) / (WeekFill — во вкладке Студия).
виджет (период + view) / owner schedule actions (отдельный ряд). 4. **Persistence:** mood → DB; language → `user.language`; lens — session + soft LS
4. **Persistence:** mood → DB; language → `user.language`; lens → `localStorage` (без вечного pin от клика). Без нового Back API.
(`eh.calendar.lens` + pinned). Без нового Back API.
5. **Week view (Front#40):** одна строка day-headers (без дубля `WeekDayColumn`); 5. **Week view (Front#40):** одна строка day-headers (без дубля `WeekDayColumn`);
горизонтальный скролл `.eh-cal-week-scroll` (mobile ~390). горизонтальный скролл `.eh-cal-week-scroll` (mobile ~390).
@@ -121,6 +123,7 @@ API: `GET /v1/user/booking-requests` (Back) + существующий `PUT /v1/
| D9 B + confirmed personal overlay | **done** Front#35 (`57cfcf3`) | | D9 B + confirmed personal overlay | **done** Front#35 (`57cfcf3`) |
| Mood chip + Lens strip | **done** Front#39 (`c2cb754` / `d38a927`); chip → AppShell Front#40; mobile icon-only Front#45 | | Mood chip + Lens strip | **done** Front#39 (`c2cb754` / `d38a927`); chip → AppShell Front#40; mobile icon-only Front#45 |
| Week view single header + scroll | **done** Front#40 (`db11c41`) | | Week view single header + scroll | **done** Front#40 (`db11c41`) |
| Studio / Мастер commercial grid + `booking_occupancy` | Spec: FrontSpec §3.1/§5.4 + BackSpec events; aggregate по `start_time`+`duration`, rail Расписание\|Команда\|Студия; follow-up visual polish board §E |
## Mood concept = product surface ## Mood concept = product surface