Docs: sync booking-requests + Bookings IA + image_url after Back#59/Front#30. Fixes EventHub/EventHubSpec#12

This commit is contained in:
2026-07-27 15:21:12 +03:00
parent cc469d5698
commit 7c21fa2aef
8 changed files with 157 additions and 9 deletions
+33
View File
@@ -0,0 +1,33 @@
---
description: EventHub — локальные сборки/тесты только через WSL (Windows toolchain сломан)
alwaysApply: true
---
# EventHub: рабочее окружение агента
На этой машине **не использовать** нативный Windows для:
| Стек | Нельзя (Windows) | Нужно |
|------|------------------|--------|
| Front / FrontAdmin | `npm`, `npx`, Playwright | **WSL** `npm` |
| Back (Erlang) | Windows `erl` / `rebar3` | **WSL** + `scripts/wsl-dev-env.sh` |
## Front (EventHubFront)
```bash
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront && npm run lint && npm run build && npm run test:e2e'
```
## FrontAdmin
```bash
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFrontAdmin && npm run lint && npm run build'
```
## Back
```bash
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack && source scripts/wsl-dev-env.sh && rebar3 eunit'
```
Подробности: `EventHubFront/.cursor/rules/npm-wsl.mdc`, `EventHubBack/.cursor/rules/otp-rebar-wsl.mdc`.
+29
View File
@@ -0,0 +1,29 @@
---
description: Один агент — один репозиторий; не смешивать Front и Back
alwaysApply: true
---
# EventHub: отдельные агенты по репозиториям
## Правило
При работе с **несколькими репозиториями** EventHub запускай **отдельного агента на каждый репо**. Не смешивай Front и Back (и Spec/FrontAdmin) в одном агенте.
| Репо | Зона |
|------|------|
| `EventHubFront` | клиентский SPA |
| `EventHubBack` | Erlang API |
| `EventHubSpec` | спеки, UI-PARITY, контракты |
| `EventHubFrontAdmin` | admin SPA |
## Запрещено
- Один агент правит и Front, и Back в одной сессии
- Коммиты/`Refs`/`Fixes` не в тот репо (Front-код → issue Back, и наоборот)
- Закрывать/комментировать issue чужого репо «заодно»
## Как делать
1. Спека/issue → агент **Spec** (или короткий foreground), если только доки.
2. API → отдельный агент **Back** (`Back#N`, eunit, push Back).
3. UI → отдельный агент **Front** (`Front#N`, lint/IFT, push Front).
4. Родитель-координатор только маршрутизирует; не пишет код сразу в два репо.
Кросс-репо фича (например Bookings inbox): **сначала Back**, потом **Front** (или параллельно двумя агентами), Spec — третьим или после стабилизации API.
@@ -0,0 +1,13 @@
---
description: Shell — только WSL/bash; PowerShell-экранирование запрещено для сложного
alwaysApply: true
---
# Shell: WSL, не PowerShell
Агенты **не должны** гонять сложные one-liner’ы в PowerShell (ломается экранирование).
Канон: файл `.sh` + `wsl -e bash /mnt/c/.../script.sh`, либо
`wsl -e bash -lc 'простая команда без вложенных двойных кавычек'`.
Полный текст: `EventHubBack/.cursor/rules/shell-wsl-not-powershell.mdc`.
+13 -2
View File
@@ -171,6 +171,8 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
(иначе `400`). (иначе `400`).
- **Confirm/decline booking:** владелец — любые booking календаря; `active` specialist — только - **Confirm/decline booking:** владелец — любые booking календаря; `active` specialist — только
booking на событиях, где `event.specialist_id` = его `user_id`. booking на событиях, где `event.specialist_id` = его `user_id`.
- **Inbox к подтверждению:** `GET /v1/user/booking-requests` агрегирует pending по тем же
правилам (owner — все события своих календарей; specialist — только свои слоты).
#### Фаза 2 (вне текущего контракта реализации) #### Фаза 2 (вне текущего контракта реализации)
- `calendar_share` (`read` | `write` | `admin`): не путать с follow, specialist_invite и платной subscription. - `calendar_share` (`read` | `write` | `admin`): не путать с follow, specialist_invite и платной subscription.
@@ -453,10 +455,18 @@ src/
`nickname`, `timezone`, `phone`, `avatar_url`, `preferences`; смена пароля — `nickname`, `timezone`, `phone`, `avatar_url`, `preferences`; смена пароля —
пара `current_password` + `password` (неверный текущий → `403`). Нельзя менять пара `current_password` + `password` (неверный текущий → `403`). Нельзя менять
email/role/status; неизвестные поля → `400`. Ответ — полный профиль как GET. email/role/status; неизвестные поля → `400`. Ответ — полный профиль как GET.
- `GET /v1/user/bookings` — бронирования пользователя. - `GET /v1/user/bookings` — бронирования пользователя **как участника**.
- `GET /v1/user/booking-requests` — pending-заявки к подтверждению, где текущий
пользователь — **owner** календаря события или **assigned specialist**
(`event.specialist_id` = user и specialist active). Ответ — массив объектов
booking + `role` (`owner`|`specialist`) + вложенный `event`
(`id`, `calendar_id`, `calendar_title`, `title`, `start_time`, `duration`,
`specialist_id`). Confirm/decline — существующий `PUT /v1/bookings/:id`.
- `GET /v1/user/reviews` — отзывы пользователя. - `GET /v1/user/reviews` — отзывы пользователя.
- `GET /v1/user/following` — календари, которые пользователь отслеживает (follow). - `GET /v1/user/following` — календари, которые пользователь отслеживает (follow).
- `GET /v1/search` — поиск; пустой запрос (только auth + пагинация/`type`) — discovery tops. - `GET /v1/search` — поиск; пустой запрос (только auth + пагинация/`type`) — discovery tops.
В calendar-результатах — `image_url` (если задан на календаре). Upload/multipart
**не** входит в контракт: поле URL.
- `GET /v1/calendars` — список календарей. - `GET /v1/calendars` — список календарей.
- `POST /v1/calendars` — создать календарь (`commercial` → подписка/trial; иначе `402`). - `POST /v1/calendars` — создать календарь (`commercial` → подписка/trial; иначе `402`).
- `GET /v1/calendars/:id` — календарь (`following`, `booking_open` для текущего контекста). - `GET /v1/calendars/:id` — календарь (`following`, `booking_open` для текущего контекста).
@@ -482,7 +492,8 @@ src/
- `POST /v1/events/:id/bookings` — запись на событие. - `POST /v1/events/:id/bookings` — запись на событие.
- `GET /v1/events/:id/bookings` — список бронирований события (владелец). - `GET /v1/events/:id/bookings` — список бронирований события (владелец).
- `GET /v1/bookings/:id` — статус бронирования. - `GET /v1/bookings/:id` — статус бронирования.
- `PUT /v1/bookings/:id` — подтвердить/отклонить бронирование (владелец). - `PUT /v1/bookings/:id` — подтвердить/отклонить (`confirm`|`decline`); владелец
любые booking календаря; active specialist — только события со своим `specialist_id`.
- `DELETE /v1/bookings/:id` — отменить бронирование (участник). - `DELETE /v1/bookings/:id` — отменить бронирование (участник).
- `POST /v1/reviews` — создать отзыв. - `POST /v1/reviews` — создать отзыв.
- `GET /v1/reviews` — список отзывов (поле `my_vote` для текущего пользователя). - `GET /v1/reviews` — список отзывов (поле `my_vote` для текущего пользователя).
+19 -5
View File
@@ -51,6 +51,9 @@ Deep-link на календарь, владельцем которого явл
- Главная сущность — **календарь**. Центр UI — виджет с видами **месяц** (default) / **неделя** / **день**. - Главная сущность — **календарь**. Центр UI — виджет с видами **месяц** (default) / **неделя** / **день**.
- Главные вкладки: **Календарь** (`/`, `/c/:id`), **Найти** (`/search`), **Записи** (`/bookings`), **Ещё** (`/more`). - Главные вкладки: **Календарь** (`/`, `/c/:id`), **Найти** (`/search`), **Записи** (`/bookings`), **Ещё** (`/more`).
- **Записи** (`/bookings`) — grouped inbox: «К подтверждению» (owner/specialist pending + Confirm/Decline)
и «Мои записи» (participant); пустые группы скрывать. Confirm на карточке события остаётся.
- В **Ещё**: пункт входящих specialist invites скрыт при `pending=0` (deep-link `/invites` работает).
- Контекст виджета: свой календарь (селектор) или чужой (browse после поиска). Чужой `personal` - Контекст виджета: свой календарь (селектор) или чужой (browse после поиска). Чужой `personal`
только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false` только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false`
(restricted / нет подписки владельца) — просмотр + сообщение «Запись временно недоступна». (restricted / нет подписки владельца) — просмотр + сообщение «Запись временно недоступна».
@@ -96,9 +99,12 @@ Deep-link на календарь, владельцем которого явл
- `/c/:calendarId` — workspace с календарём - `/c/:calendarId` — workspace с календарём
- `/c/:calendarId/e/:eventId` — workspace + карточка события - `/c/:calendarId/e/:eventId` — workspace + карточка события
- `/search` — поиск календарей/событий - `/search` — поиск календарей/событий
- `/bookings`мои бронирования (deep-link в `/c/.../e/...`) - `/bookings`grouped inbox: (A) к подтверждению (owner/specialist pending) +
(B) мои записи участника; deep-link в `/c/.../e/...`
- `/calendars` — управление своими календарями (CRUD) - `/calendars` — управление своими календарями (CRUD)
- `/more` — вторичное меню + mood switcher - `/more` — вторичное меню + mood switcher; пункт specialist invites **скрыт**,
если входящих pending = 0 (deep-link `/invites?token=` работает)
- `/invites` — inbox входящих specialist invites (не смешивать с `/bookings`)
- `/following` — отслеживаемые чужие календари (follow; не платная Подписка) - `/following` — отслеживаемые чужие календари (follow; не платная Подписка)
- `/reviews`, `/subscription`, `/tickets`, `/profile` - `/reviews`, `/subscription`, `/tickets`, `/profile`
@@ -116,6 +122,7 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
### 5.3. Поиск (участник) ### 5.3. Поиск (участник)
`GET /v1/search` — фильтры (`type`, `q`, `tags`, даты, `lat`/`lon`/`radius`, sort, order, пагинация). Без `q`/дат/tags/geo/sort — топы календарей и событий; иначе — поиск. В списке бейдж типа — `calendar`|`event` (не `personal`/`commercial`). Даты, теги, geo и сортировка свёрнуты по умолчанию. Переход к `/c/:id` или `/c/:id/e/:eventId`. `GET /v1/search` — фильтры (`type`, `q`, `tags`, даты, `lat`/`lon`/`radius`, sort, order, пагинация). Без `q`/дат/tags/geo/sort — топы календарей и событий; иначе — поиск. В списке бейдж типа — `calendar`|`event` (не `personal`/`commercial`). Даты, теги, geo и сортировка свёрнуты по умолчанию. Переход к `/c/:id` или `/c/:id/e/:eventId`.
В результатах calendar (и event, если Back отдаёт) может быть `image_url` — Discover показывает cover; иначе mood wash + initials. **Upload API нет** — поле URL (seed / ручная установка).
### 5.4. Календари ### 5.4. Календари
- Список своих: `GET /v1/calendars` - Список своих: `GET /v1/calendars`
@@ -132,8 +139,10 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
список active + исходящие pending; deactivate/remove принятых. список active + исходящие pending; deactivate/remove принятых.
Выбор `specialist_id` в форме события (только `active`). Выбор `specialist_id` в форме события (только `active`).
Баннер «подписка истекла / истекает» при `booking_open=false`. Баннер «подписка истекла / истекает» при `booking_open=false`.
- Invitee: inbox входящих `GET /v1/user/specialist-invites` (More / уведомления) - Invitee: inbox входящих `GET /v1/user/specialist-invites` на `/invites`
принять / отклонить; deep-link из email → accept по `token` после логина. принять / отклонить; deep-link из email → accept по `token` после логина.
В More пункт «Приглашения» показывается **только** при `pending > 0`
(не путать с booking-заявками на `/bookings`).
### 5.5. События ### 5.5. События
- Список: `GET /v1/calendars/:calendar_id/events` - Список: `GET /v1/calendars/:calendar_id/events`
@@ -145,8 +154,13 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
### 5.6. Запись (bookings) ### 5.6. Запись (bookings)
- Участник: `POST /v1/events/:id/bookings` только при `booking_open`; иначе UI-сообщение / - Участник: `POST /v1/events/:id/bookings` только при `booking_open`; иначе UI-сообщение /
ответ `403`; `GET /v1/user/bookings`, `DELETE /v1/bookings/:id`, `GET /v1/bookings/:id` ответ `403`; `GET /v1/user/bookings`, `DELETE /v1/bookings/:id`, `GET /v1/bookings/:id`
- Владелец и specialist: `GET /v1/events/:id/bookings`, `PUT /v1/bookings/:id` - Владелец и specialist:
`{ action: confirm | decline }` (specialist — только свои события, см. BackSpec §2.1.2) - на карточке события: `GET /v1/events/:id/bookings` + Confirm/Decline (**обязательно**)
- inbox `/bookings` группа «К подтверждению»: `GET /v1/user/booking-requests`
(pending, где user — owner календаря или `event.specialist_id`); те же
`PUT /v1/bookings/:id` `{ action: confirm | decline }`
- UI `/bookings`: две группы (пустые скрывать): **К подтверждению** + **Мои записи**
(upcoming/past participant). `specialist_invite` сюда **не** попадает.
- Статусы: `pending` | `confirmed` | `cancelled`; confirmation: `auto` | `manual` | timeout - Статусы: `pending` | `confirmed` | `cancelled`; confirmation: `auto` | `manual` | timeout
- Pending резервирует capacity (как на бэке) - Pending резервирует capacity (как на бэке)
+23
View File
@@ -0,0 +1,23 @@
# Stage Popular cleanup notes
Popular / Discover empty-search feed = ratings tops (`GET /v1/search` without `q`/filters).
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.
4. Front already tracks calendars created in-session via `e2e/helpers/standCleanup.ts` — предпочитать это для новых тестов.
## Suggested filter (admin / ops)
- title/tags match: `(?i)e2e|playwright|ift-litter|tmp-`
- created recently + zero real bookings, if policy allows
- exclude calendars used by documented stage smoke accounts
## Upload / cover images
`image_url` на календаре отдаётся в search. **Upload API нет** — seed URL вручную или через admin/update поля. Discover показывает cover если URL непустой, иначе mood wash + initials.
Refs: EventHubSpec#12, EventHubBack#59, EventHubFront#30.
+2
View File
@@ -362,6 +362,7 @@ error rate > 1% **или** mean request > 500 ms (hold ≥2 мин в фазе)
- После успешных тестов — обновить спеку, если менялся внешний контракт. - После успешных тестов — обновить спеку, если менялся внешний контракт.
- Одна задача — один логический объём работы. - Одна задача — один логический объём работы.
- Код фичи — в репозитории-владельце; `EventHubSpec` — для документации и процесса. - Код фичи — в репозитории-владельце; `EventHubSpec` — для документации и процесса.
- **Один агент — один репо** (не смешивать Front/Back/Spec/FrontAdmin в одной сессии): `.cursor/rules/one-repo-per-agent.mdc`.
- Метки во всех репозиториях держать одинаковыми (`sync-labels.ps1`). - Метки во всех репозиториях держать одинаковыми (`sync-labels.ps1`).
- Секреты и токены не коммитить; Gitea token — только в env / `~/.cursor/secrets/`. - Секреты и токены не коммитить; Gitea token — только в env / `~/.cursor/secrets/`.
- **Git commit/push — через WSL** (`~/.cursor/eventhub/git-commit.sh`, `git-push.sh`); **push — только после подтверждения пользователя** (см. §2.1). - **Git commit/push — через WSL** (`~/.cursor/eventhub/git-commit.sh`, `git-push.sh`); **push — только после подтверждения пользователя** (см. §2.1).
@@ -377,6 +378,7 @@ error rate > 1% **или** mean request > 500 ms (hold ≥2 мин в фазе)
| Дата | Изменение | | Дата | Изменение |
|------|-----------| |------|-----------|
| 2026-07-27 | Spec#12: booking-requests + Bookings IA + image_url; правило one-repo-per-agent |
| 2026-07-27 | §6 / STAGE-BROWSER: канон — прямой HTTPS `*.calentiq.com`; proxy `:8787` только DNS-fallback | | 2026-07-27 | §6 / STAGE-BROWSER: канон — прямой HTTPS `*.calentiq.com`; proxy `:8787` только DNS-fallback |
| 2026-07-22 | §6: Stage UI в Cursor Browser — `STAGE-BROWSER.md` + alias proxy | | 2026-07-22 | §6: Stage UI в Cursor Browser — `STAGE-BROWSER.md` + alias proxy |
| 2026-07-22 | §2.1: Agent shell — не PowerShell для сложного; `.sh` + WSL; Front push только после lint/build/IFT | | 2026-07-22 | §2.1: Agent shell — не PowerShell для сложного; `.sh` + WSL; Front push только после lint/build/IFT |
+25 -2
View File
@@ -17,7 +17,7 @@ SoT: [`boards/calentiq-ui-board-final.png`](boards/calentiq-ui-board-final.png).
| Pixel-perfect calendar | denser board mock + Today/Upcoming rail | Agenda rail (§B): title-case Сегодня/Предстоящие + count, time-first rows; month chips/cell wash (§B/D) по mood; не 1:1 Figma | | Pixel-perfect calendar | denser board mock + Today/Upcoming rail | Agenda rail (§B): title-case Сегодня/Предстоящие + count, time-first rows; month chips/cell wash (§B/D) по mood; не 1:1 Figma |
| Discover cards | soft cards + media thumb + CTA | mood media plane (wash/initials + `image_url` when present) + soft-card rhythm + CTA; без full photo-hero grid | | Discover cards | soft cards + media thumb + CTA | mood media plane (wash/initials + `image_url` when present) + soft-card rhythm + CTA; без full photo-hero grid |
| CONTROL booking §E | отдельные mock-карточки «слот/специалист» | `EventActionCard` / specialists / bookings: steel rail (усилен), avatar, available label, Confirm/Decline; не pixel-perfect board mocks | | CONTROL booking §E | отдельные mock-карточки «слот/специалист» | `EventActionCard` / specialists / bookings: steel rail (усилен), avatar, available label, Confirm/Decline; не pixel-perfect board mocks |
| Bookings list §D | список заявок с Confirm/Decline на `/bookings` | `/bookings` = заявки **участника** (Отменить); owner Confirm/Decline — в карточке события (`EventActionCard` / Заявки) | | 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; mood switcher на login (иконки); taglines mood cards — на `/more`; **без** Google OAuth | | Login §D phone chrome | три phone frames + (на борде) Google CTA | brand-first hero + mood `--auth-*`; email/password only; mood switcher на login (иконки); taglines mood cards — на `/more`; **без** Google OAuth |
| Favicon set | 3 отдельных favicon файла на борде | mood appicon через `applyMood` | | Favicon set | 3 отдельных favicon файла на борде | mood appicon через `applyMood` |
| Desktop nav | phone bottom tabs на борде | desktop: top tabs (Календарь / Найти / Записи / Ещё); bottom nav — mobile | | Desktop nav | phone bottom tabs на борде | desktop: top tabs (Календарь / Найти / Записи / Ещё); bottom nav — mobile |
@@ -32,7 +32,30 @@ IA без sidebar / без Creator mood — совпадает с non-goals (п
**Polish in this pass:** agenda copy/casing/counts + time-first rows; CONTROL steel rail emphasis; neutral Discover cover in mock (не flow-appicon во всех moods). **Polish in this pass:** agenda copy/casing/counts + time-first rows; CONTROL steel rail emphasis; neutral Discover cover in mock (не flow-appicon во всех moods).
**Remaining intentional:** no pixel-perfect phone chrome; Discover без photo-hero grid; owner Confirm/Decline не на `/bookings` list. **Remaining intentional:** no pixel-perfect phone chrome; Discover без photo-hero grid (cover via `image_url` when API returns it); §E specialist schedule card vs panel — не pixel-perfect board mocks (follow-up).
### Bookings inbox + More invites (locked 2026-07-27)
Продуктовые решения (не deviation — канон):
1. **Event card Confirm/Decline** остаётся обязательным (quick approve на слоте).
2. **`/bookings`** — grouped inbox, не только participant:
- **Группа A — К подтверждению:** pending bookings, где пользователь calendar **owner** или **assigned specialist** (`event.specialist_id` = user). Confirm/Decline те же, что на `EventActionCard`.
- **Группа B — Мои записи:** participant bookings (upcoming/past + Cancel) как раньше.
- Пустые группы скрывать (`count=0`).
- Опционально подпись owner vs specialist внутри A; иначе одна секция «К подтверждению».
3. **`specialist_invite` в More:** скрывать пункт меню, если **0 pending** входящих. Deep-link `/invites?token=` работает. Исходящие invite остаются в `SpecialistsPanel`.
4. **Не** класть строки `specialist_invite` в `/bookings`.
API: `GET /v1/user/booking-requests` (Back) + существующий `PUT /v1/bookings/:id`.
### Follow-ups (вне этого fix-pass или частично)
| Тема | Статус |
|------|--------|
| Discover `image_url` в search serialization | Back отдаёт поле; upload API **нет** — только URL field / seed |
| Stage Popular clutter (e2e calendars) | filter notes: см. `EventHubSpec/STAGE-POPULAR-CLEANUP.md`; не трогать реальные smoke данные без фильтра по e2e-паттернам |
| §E specialist schedule card vs panel | deviation: EventActionCard + SpecialistsPanel; отдельный pixel mock — Future |
## Mood concept = product surface ## Mood concept = product surface