From 4dd741ba48aa5e90c4a5d5294d83dc8297a75ddd Mon Sep 17 00:00:00 2001 From: Aleksey Sabilin Date: Sat, 15 Aug 2026 23:20:39 +0300 Subject: [PATCH] docs(spec): calendar_share co-editor/deputy and Time Arc alignment. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Back §2.1.3 API/ACL; Front §3.3 share busy/ghost. Refs EventHub/EventHubBack#73 --- .cursor/rules/shell-wsl-not-powershell.mdc | 260 ++++++++++----------- EventHubBackSpec.md | 50 +++- EventHubFrontSpec.md | 12 +- README.md | 1 + STAGE-POPULAR-CLEANUP.md | 3 +- ZED-ARCHITECTURE.md | 144 ++++++++++++ design/front/UI-PARITY.md | 33 +-- 7 files changed, 342 insertions(+), 161 deletions(-) create mode 100644 ZED-ARCHITECTURE.md diff --git a/.cursor/rules/shell-wsl-not-powershell.mdc b/.cursor/rules/shell-wsl-not-powershell.mdc index 8b54896..e6a4459 100644 --- a/.cursor/rules/shell-wsl-not-powershell.mdc +++ b/.cursor/rules/shell-wsl-not-powershell.mdc @@ -1,130 +1,130 @@ ---- -description: EventHub — команды только через WSL/bash; PowerShell для сложного запрещён -alwaysApply: true ---- - -# EventHub: как запускать команды (обязательно) - -## Проблема -PowerShell в Cursor **ломает** кавычки, `$`, JSON, heredoc и вложенный `bash -lc "..."`. Из‑за этого агенты снова и снова получают `unexpected EOF`, `Invalid JSON`, пустой curl. - -## Правило -1. **Нельзя** гонять сложные one-liner’ы в PowerShell (npm, curl+JSON, python -c, git commit heredoc, вложенный bash с кавычками). -2. **Можно** в PowerShell только простые вещи: `cd`, `git status`, `ls`, одиночный `wsl …` без вложенных кавычек в аргументе. -3. Рабочий способ — **WSL bash** или **файл `.sh` + launcher** (см. ниже). - -## CRLF / «No such file or directory» (обязательно) - -Cursor `Write` на Windows часто сохраняет `.sh` с **CRLF**. Тогда: - -```text -wsl -e bash /mnt/c/.../EventHubFront/.tmp-run.sh -# bash: .../.tmp-run.sh: No such file or directory -``` - -Файл **есть** (`ls` его видит) — ломается shebang (`#!/usr/bin/env bash\r`). Это **не** «файл не записался». - -### Канон запуска любого `.sh` с `/mnt/c/...` - -**Всегда** через хелпер (снимает `\r`, потом `bash`): - -```text -wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront/.tmp-run.sh -``` - -**Запрещено** после `Write` голый: - -```text -wsl -e bash /mnt/c/.../repo/.tmp-something.sh -``` - -Альтернатива без хелпера (тот же смысл): - -```text -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`. - -## Канон (копируй) - -### Вариант A — скрипт (предпочтительно) -1. Запиши команды в файл, например `EventHubBack/.tmp-run.sh` или `~/.cursor/eventhub/….sh` (`#!/usr/bin/env bash`). -2. Запусти **только** так (из PowerShell, без вложенных кавычек): - -```text -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 вызов -Только если внутри **одинарные** кавычки bash и нет `"`/`$` конфликтов с PS (и **нет** нового `.sh` с диска C): - -```text -wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront && npm run lint' -``` - -В PowerShell для `-lc` используй **одинарные** кавычки снаружи (`'...'`), внутри — обычный bash. - -### Запрещено -```text -wsl -e bash /mnt/c/.../.tmp-run.sh # без run-wsl-sh / sed — CRLF → No such file -wsl -e bash -lc ".... python -c \"import...\" ...." # ломается -curl ... -d "{\"email\":\"$x\"}" # в PS ломается -git commit -m "$(cat <<'EOF' ...)" # в PS не так -wsl -e bash -lc 'git-commit.sh … fix(ci): subject' # ( ) ломают bash -wsl -e bash -lc 'git commit -m "fix(ci): …"' # то же -npm / npx / node из Windows # сломан -``` - -### Git commit — канон (subject с `( )`, `#`, `$`) - -1. Текст коммита — в файл репо, напр. `.tmp-commit-msg.txt` (без heredoc из PS). -2. Скрипт `.tmp-commit.sh`: - -```bash -#!/usr/bin/env bash -set -euo pipefail -cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack -git add path1 path2 -GIT_EDITOR=true git commit -F .tmp-commit-msg.txt -``` - -3. Из PowerShell: - -```text -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. - -## Стек → команда - -| Задача | Как | -|-----|-----| -| 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` | -| 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 …` | -| JSON/API/python | скрипт `.sh`/`.py` + `run-wsl-sh.sh` | - -## Git push (обязательно) - -**Запрещено агентам:** `git push`, `git push origin`, `git push origin HEAD` / `master` напрямую — без токена зависают на HTTPS prompt. - -**Канон:** -```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-main.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront -``` - -Путь WSL: `/mnt/c/Users/alexc/.cursor/eventhub/` (не `~/.cursor/…` — home в WSL может отличаться). - -Credential store в WSL уже настроен (`credential.helper=store` + `git.sabilin.com`); хелпер всё равно предпочтителен (явный URL с токеном из secrets). - -## Перед push Front -1. WSL: `npm run lint && npm run build` (ловит синтаксис/TS) -2. WSL: `npm run test:e2e: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/`. +--- +description: EventHub — команды только через WSL/bash; PowerShell для сложного запрещён +alwaysApply: true +--- + +# EventHub: как запускать команды (обязательно) + +## Проблема +PowerShell в Cursor **ломает** кавычки, `$`, JSON, heredoc и вложенный `bash -lc "..."`. Из‑за этого агенты снова и снова получают `unexpected EOF`, `Invalid JSON`, пустой curl. + +## Правило +1. **Нельзя** гонять сложные one-liner’ы в PowerShell (npm, curl+JSON, python -c, git commit heredoc, вложенный bash с кавычками). +2. **Можно** в PowerShell только простые вещи: `cd`, `git status`, `ls`, одиночный `wsl …` без вложенных кавычек в аргументе. +3. Рабочий способ — **WSL bash** или **файл `.sh` + launcher** (см. ниже). + +## CRLF / «No such file or directory» (обязательно) + +Cursor `Write` на Windows часто сохраняет `.sh` с **CRLF**. Тогда: + +```text +wsl -e bash /mnt/c/.../EventHubFront/.tmp-run.sh +# bash: .../.tmp-run.sh: No such file or directory +``` + +Файл **есть** (`ls` его видит) — ломается shebang (`#!/usr/bin/env bash\r`). Это **не** «файл не записался». + +### Канон запуска любого `.sh` с `/mnt/c/...` + +**Всегда** через хелпер (снимает `\r`, потом `bash`): + +```text +wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront/.tmp-run.sh +``` + +**Запрещено** после `Write` голый: + +```text +wsl -e bash /mnt/c/.../repo/.tmp-something.sh +``` + +Альтернатива без хелпера (тот же смысл): + +```text +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`. + +## Канон (копируй) + +### Вариант A — скрипт (предпочтительно) +1. Запиши команды в файл, например `EventHubBack/.tmp-run.sh` или `~/.cursor/eventhub/….sh` (`#!/usr/bin/env bash`). +2. Запусти **только** так (из PowerShell, без вложенных кавычек): + +```text +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 вызов +Только если внутри **одинарные** кавычки bash и нет `"`/`$` конфликтов с PS (и **нет** нового `.sh` с диска C): + +```text +wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront && npm run lint' +``` + +В PowerShell для `-lc` используй **одинарные** кавычки снаружи (`'...'`), внутри — обычный bash. + +### Запрещено +```text +wsl -e bash /mnt/c/.../.tmp-run.sh # без run-wsl-sh / sed — CRLF → No such file +wsl -e bash -lc ".... python -c \"import...\" ...." # ломается +curl ... -d "{\"email\":\"$x\"}" # в PS ломается +git commit -m "$(cat <<'EOF' ...)" # в PS не так +wsl -e bash -lc 'git-commit.sh … fix(ci): subject' # ( ) ломают bash +wsl -e bash -lc 'git commit -m "fix(ci): …"' # то же +npm / npx / node из Windows # сломан +``` + +### Git commit — канон (subject с `( )`, `#`, `$`) + +1. Текст коммита — в файл репо, напр. `.tmp-commit-msg.txt` (без heredoc из PS). +2. Скрипт `.tmp-commit.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail +cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack +git add path1 path2 +GIT_EDITOR=true git commit -F .tmp-commit-msg.txt +``` + +3. Из PowerShell: + +```text +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. + +## Стек → команда + +| Задача | Как | +|-----|-----| +| 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` | +| 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 …` | +| JSON/API/python | скрипт `.sh`/`.py` + `run-wsl-sh.sh` | + +## Git push (обязательно) + +**Запрещено агентам:** `git push`, `git push origin`, `git push origin HEAD` / `master` напрямую — без токена зависают на HTTPS prompt. + +**Канон:** +```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-main.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront +``` + +Путь WSL: `/mnt/c/Users/alexc/.cursor/eventhub/` (не `~/.cursor/…` — home в WSL может отличаться). + +Credential store в WSL уже настроен (`credential.helper=store` + `git.sabilin.com`); хелпер всё равно предпочтителен (явный URL с токеном из secrets). + +## Перед push Front +1. WSL: `npm run lint && npm run build` (ловит синтаксис/TS) +2. WSL: `npm run test:e2e: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/`. diff --git a/EventHubBackSpec.md b/EventHubBackSpec.md index eee4451..3d0d3e9 100644 --- a/EventHubBackSpec.md +++ b/EventHubBackSpec.md @@ -13,8 +13,9 @@ EventHub — платформа для управления событиями ### 2.1. Календари - CRUD календаря (название, описание, теги, владелец) -- Расшаривание по ссылке / приглашения с правами (`calendar_share`) — **фаза 2** (таблица есть; - user HTTP API и UI — Future, см. §2.1.3) +- Расшаривание по ссылке / приглашения с правами (`calendar_share`) — invite/accept/revoke + + ACL `read|write|admin` (Back#73); personal и commercial (заместитель); UI — Front; + не путать со `specialist_invite` / follow. - Типы календарей: `personal` | `commercial` — семантика и коммерческий контур: **§2.1.2** - Гибкое подтверждение заявок: `auto` | `manual` | `{timeout, N}` (секунды) — детали §2.1.2 / §2.3 - Теги календаря, рейтинг (средняя оценка, количество голосов) @@ -68,7 +69,7 @@ EventHub — платформа для управления событиями | | `personal` | `commercial` | |---|---|---| | Подписка владельца | не требуется | нужна active (trial или paid), иначе **restricted** | -| Видимость чужим | только владелец (+ share — фаза 2) | публичный просмотр; в search/discover — только если **не** restricted | +| Видимость чужим | только владелец или share grant | публичный просмотр; в search/discover — только если **не** restricted | | Booking чужим | **запрещён** (бэкенд `403`, код `personal_calendar`) | разрешён при active подписке владельца | | Follow | нет (нет доступа) | да | | Специалисты | не используются | CRUD владельца; `specialist_id` на событии | @@ -256,16 +257,33 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ **сделано** (Back#75): `GET/PUT /v1/notifications/prefs`, `GET /v1/push/vapid-public-key`, `POST/DELETE /v1/push/subscriptions`; UI — Front#71. -- `calendar_share` (`read` | `write` | `admin`): низкий приоритет; не путать с follow, - specialist_invite и платной subscription. User HTTP API и UI — до отдельной задачи - (таблица в схеме есть, см. §2.1.3). +- `calendar_share` (`read` | `write` | `admin`): соредактор personal / заместитель commercial; + invite/accept/revoke + grants; personal **не** в search/discovery. Не путать с follow, + specialist_invite и платной subscription. - `client_type=mobile` — хвост, когда появится нативный клиент. - **Не в фазе 2:** оплата услуги клиентом (B2C) — вне горизонта продукта. -### 2.1.3. Share / приглашения (фаза 2) -Таблица `calendar_share` (`calendar_id`, `user_id`, `rights`: `read` | `write` | `admin`) -существует в схеме. User HTTP API и клиентский UI — не стартовать, пока нет явного -сценария соредактора personal (совместная работа сейчас: specialist_invite + booking inbox). +### 2.1.3. Share / приглашения (соредактор и заместитель) + +Таблица `calendar_share` (`id`, `calendar_id`, `user_id`, `rights`: `read` | `write` | `admin`, +`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. События (расширенная версия с повторяющимися событиями) @@ -576,10 +594,12 @@ src/ - `GET /v1/user/reviews` — отзывы пользователя. - `GET /v1/user/following` — календари, которые пользователь отслеживает (follow). - `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 **не** входит в контракт: поле URL. -- `GET /v1/calendars` — список календарей (auth). +- `GET /v1/calendars` — список календарей (auth): owned ∪ shared (`role`, `share_rights`, + `mirror_to_default`). - `POST /v1/calendars` — создать календарь (`commercial` → нужна уже active sub/trial; иначе `402`; auto-start trial нет). - `GET /v1/calendars/:id` — календарь **или unique `short_name`**. **Без токена** для active commercial (`following: false`); @@ -598,6 +618,12 @@ src/ - `GET /v1/user/specialist-invites` — входящие приглашения. - `POST /v1/specialist-invites/:id/accept` | `…/decline` — ответ invitee. - `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). В JSON каждого события commercial-календаря — опциональное поле `booking_occupancy`: `"free"` | `"pending"` | `"confirmed"`. diff --git a/EventHubFrontSpec.md b/EventHubFrontSpec.md index d691fa7..77c1f05 100644 --- a/EventHubFrontSpec.md +++ b/EventHubFrontSpec.md @@ -151,9 +151,9 @@ CONTROL→bookings. Контент: free gaps / события сегодня / ### 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. После создания черновик снимается сразу (без F5), в том числе если время в форме сдвинули (прошлый час → +1 день / правка start). Фраза с часом уже в прошлом («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. +**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. Маршруты Публичные: @@ -379,7 +384,8 @@ legal stubs; позиция по подписке **владельца**; DNS/SP - waitlist: API Back#72; включение — чекбокс «Лист ожидания» (`settings.waitlist_enabled`) в настройках commercial; join/leave на карточке события при полном слоте — Front#70 -- шаринг календаря с правами (`calendar_share`) — низкий приоритет, не путать со specialist_invite +- шаринг календаря с правами (`calendar_share`) — Back#73 API + Front list/invites/mirror; + не путать со specialist_invite / Following - явный `client_type=mobile` — хвост (фаза 3 бэка / нативный клиент) **Вне горизонта (не фаза 2):** оплата услуги клиентом (B2C). diff --git a/README.md b/README.md index 9134280..75b299a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ## 4. [Правила работы (workflow)](WORKFLOW.md) ## 5. [Stage UI в Cursor Browser](STAGE-BROWSER.md) (HTTPS `*.calentiq.com`; proxy — fallback) ## 6. [UX backlog (Client UI)](UX-BACKLOG.md) +## 7. [Карта архитектуры для Zed/агентов](ZED-ARCHITECTURE.md) # **Репозитории разработки EventHub** ## 1. [EventHub Backend](https://git.sabilin.com/EventHub/EventHubBack) diff --git a/STAGE-POPULAR-CLEANUP.md b/STAGE-POPULAR-CLEANUP.md index a147cbf..d9769cd 100644 --- a/STAGE-POPULAR-CLEANUP.md +++ b/STAGE-POPULAR-CLEANUP.md @@ -7,8 +7,9 @@ E2E и smoke иногда оставляют commercial-календари с р 1. **Не** удалять «живые» smoke-календари пилота без явного фильтра. 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` — предпочитать это для новых тестов. +5. Catalog BizCal (`short_name` `bizcal-*`, tag `BizCal`) и disposable LargeCal — не путать с smoke; seed: Front `seed-catalog-ift|stage` (нужны `BIZCAL_CLIENT_*` в `.env` стенда). ## Suggested filter (admin / ops) diff --git a/ZED-ARCHITECTURE.md b/ZED-ARCHITECTURE.md new file mode 100644 index 0000000..6b894e1 --- /dev/null +++ b/ZED-ARCHITECTURE.md @@ -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 A–B–C, 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 (не трогать соседний репо «заодно»). diff --git a/design/front/UI-PARITY.md b/design/front/UI-PARITY.md index 680e9f7..810121b 100644 --- a/design/front/UI-PARITY.md +++ b/design/front/UI-PARITY.md @@ -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 **остаётся** обязательным | | 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 | -| 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) | | Favicon set | 3 отдельных favicon файла на борде | mood appicon через `applyMood` | | Desktop nav | phone bottom tabs на борде | desktop: **одна строка** chrome — logo + mood \| nav (Календарь / Найти / Записи / Ещё) \| nickname + logout; bottom nav — mobile | | Header identity | — | nickname primary (не raw email); email только в `title` tooltip | | 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 | -| 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` | -| 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 | | Домены 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. -### 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 — 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. Glyphs: FLOW sprout, MOMENTUM bolt, CONTROL briefcase (не Time Arc appicon). 2. **Lens** = отдельный контентный режим (`overview` / `today` / `bookings`), не mood и не - view month/week/day. UI: Обзор / На сегодня / Заявки. Переключатель — в шапке strip - (underline/text tabs на desktop; chip → sheet на mobile), **не** рядом с ViewMode. - Soft-default при смене mood, если lens не pinned: FLOW→overview, MOMENTUM→today, - CONTROL→bookings. -3. **Strip** над сеткой (не на HTML-архиве месяца): Front-only derive — free gaps / - события сегодня / upcoming bookings. Desktop раскрыт по умолчанию; mobile свёрнут - (tap раскрывает). Сетка всегда видна. Workspace toolbar: контекст (селектор) / - виджет (период + view) / owner schedule actions (отдельный ряд). -4. **Persistence:** mood → DB; language → `user.language`; lens → `localStorage` - (`eh.calendar.lens` + pinned). Без нового Back API. + view month/week/day. UI: Обзор / Сегодня / Заявки. **Front#46:** инфо-табло **под + AppShell** (surface + сегменты + метрики/chips), **не** рядом с ViewMode и не в + calendar toolbar. Soft-default при смене mood **без auto-pin** при клике: + FLOW→overview, MOMENTUM→today, CONTROL→bookings. +3. Контент lens (не на HTML-архиве): Front-only derive — free gaps / события сегодня / + upcoming bookings. Workspace toolbar: селектор / период+view / «+ Новое событие» + (WeekFill — во вкладке Студия). +4. **Persistence:** mood → DB; language → `user.language`; lens — session + soft LS + (без вечного pin от клика). Без нового Back API. 5. **Week view (Front#40):** одна строка day-headers (без дубля `WeekDayColumn`); горизонтальный скролл `.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`) | | 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`) | +| 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