Compare commits

...

45 Commits

Author SHA1 Message Date
aleksey d451753be2 docs(spec): slot drafts not ghosts; clear after create. Refs EventHub/EventHubFront#65 2026-08-14 12:12:21 +03:00
aleksey d7c5ae3ac4 docs(spec): nearest banner skips first-card duplicate. Refs EventHub/EventHubFront#64 2026-08-14 12:03:34 +03:00
aleksey 1bed282cfb docs(spec): create capacity 1 and WeekFill preview. Refs EventHub/EventHubFront#63 2026-08-14 11:52:09 +03:00
aleksey 5fa00a44bf docs(spec): tablet chrome and owner rail breakpoints. Refs EventHub/EventHubFront#62 2026-08-14 11:30:43 +03:00
aleksey 80e10caf3d docs(spec): studio-bookings and instance inbox. Refs EventHub/EventHubFront#60 2026-08-14 11:13:08 +03:00
aleksey e87236b981 docs(spec): guest public search and commercial week. Refs EventHub/EventHubFront#58 2026-08-14 11:03:05 +03:00
aleksey 8e8e361e77 docs(spec): POST bookings occurrence_start. Refs EventHub/EventHubBack#67 2026-08-14 10:52:30 +03:00
aleksey 73ff212020 docs(spec): Syne brand-only, Manrope for UI Cyrillic. Refs EventHub/EventHubFront#54 2026-08-13 19:58:56 +03:00
aleksey e611f6b92c docs: Time Arc v4 demand ghosts and phrase. Refs EventHub/EventHubFront#53 2026-08-13 18:50:43 +03:00
aleksey bc1d770f16 docs: Time Arc v3 following and discover when. Refs EventHub/EventHubFront#52 2026-08-13 18:16:07 +03:00
aleksey 951d070d63 docs(spec): Time Arc v2 memory and heat (Front#51).
Refs EventHub/EventHubFront#51
2026-08-13 17:47:48 +03:00
aleksey 94170ebd13 docs(spec): Time Arc living grid on client (Front#50).
Refs EventHub/EventHubFront#50
2026-08-13 14:44:03 +03:00
aleksey 2ec5d1b6d6 docs(spec): mood switcher §C glyphs; full wordmark on mobile
Clarify Front#45: sprout/bolt/briefcase in switcher; CalenTIQ wordmark not mark-only.

Refs EventHub/EventHubFront#45
2026-07-30 10:28:54 +03:00
aleksey 4d8f4db284 docs(spec): Front#45 mood chip mobile icon-only chrome
Sync EventHubFrontSpec and UI-PARITY with mark-only logo + icon-only chip on mobile.

Refs EventHub/EventHubFront#45
2026-07-29 23:46:07 +03:00
aleksey a93997b0bb docs: document calendar.settings org defaults
Cross-link EventHubBack#63 and EventHubFront#44 for known keys,
unknown passthrough, and 400 invalid_settings.
2026-07-29 23:16:47 +03:00
aleksey 9dfc4f0097 docs: note Front#43 mergeExpandedWithMasters for duty overlay
Refs EventHub/EventHubFront#43
2026-07-29 22:10:07 +03:00
aleksey 2fa4635a9f docs: sync Front#43 specialist duty overlay on personal
Accepted invites + active specialist check; personal owner does not
mirror foreign roster.

Refs EventHub/EventHubFront#43
2026-07-29 21:16:55 +03:00
aleksey 9d79babac5 docs: sync workspace controls IA — lens in strip, schedule actions row
Document separated calendar chrome zones and lens labels (Обзор / На сегодня / Заявки).
2026-07-29 19:28:11 +03:00
aleksey 89073c9e76 docs(rules): require run-wsl-sh for CRLF .sh scripts
Cursor Write on Windows often saves CRLF; bare wsl -e bash script.sh
then fails with No such file. Canon is run-wsl-sh.sh (strip CR).
2026-07-29 12:55:04 +03:00
aleksey 8451bb352d Docs: sync Front#40 Mood AppShell + week scroll. Fixes EventHub/EventHubSpec#17
Refs EventHub/EventHubFront#40 (7c092d1 / db11c41).
2026-07-28 22:09:59 +03:00
aleksey 86e53b5f0b Docs: sync Front#39 Mood chip + Lens strip. Fixes EventHub/EventHubSpec#16
Refs EventHub/EventHubFront#39 (c2cb754 / d38a927).
2026-07-28 21:22:03 +03:00
aleksey a7132b9b64 Docs: sync Front#35 D9 B guest schedule + confirmed personal overlay. Fixes EventHub/EventHubSpec#15
Refs EventHub/EventHubFront#35 (57cfcf3).
2026-07-28 13:26:15 +03:00
aleksey a81262b3e0 docs(rules): git commit via -F file when message has parentheses
Document fix(ci): and special chars breaking wsl -lc one-liners.
Use .tmp-commit-msg.txt + GIT_EDITOR=true git commit -F in .sh scripts.

[skip ci]
2026-07-28 10:20:57 +03:00
aleksey 8dae666119 docs: forbid bare git push; use git-push.sh helpers.
[skip ci]
2026-07-27 23:11:46 +03:00
aleksey 96f320cabc docs: agent pitfalls + token economy rules for Cursor [skip ci] 2026-07-27 21:50:59 +03:00
aleksey 644ded803c Docs: sync Back#61 specialist_id + trial/402 (no auto-start). Fixes EventHub/EventHubSpec#14 2026-07-27 21:24:09 +03:00
aleksey 546f888487 Docs: sync booking expired (Back#60) + Front#31 UX polish. Fixes EventHub/EventHubSpec#13 2026-07-27 17:53:23 +03:00
aleksey 7c21fa2aef Docs: sync booking-requests + Bookings IA + image_url after Back#59/Front#30. Fixes EventHub/EventHubSpec#12 2026-07-27 15:21:12 +03:00
aleksey cc469d5698 docs(design): stage smoke parity vs board — mark done, durable IA gaps. Refs EventHub/EventHubSpec#10 2026-07-27 13:43:14 +03:00
aleksey 7a6b1a84cc Docs: stage access is direct HTTPS (*.calentiq.com); :8787 proxy is DNS fallback only. [skip ci] 2026-07-27 13:25:00 +03:00
aleksey 01904e9873 Refs Spec#10/#11 — visual pass ×3 moods: update UI-PARITY Known deviations. 2026-07-27 12:57:04 +03:00
aleksey a60a939566 Refs Spec#10/#11 — note Login §D board composition 2026-07-27 12:38:15 +03:00
aleksey f62071b86b Refs Spec#10/#11 — note CONTROL booking §E polish 2026-07-27 11:53:33 +03:00
aleksey 3bc3bd4236 Refs Spec#10/#11 — note Discover media hierarchy board-parity. 2026-07-27 11:36:26 +03:00
aleksey d3a011b746 Refs Spec#10/#11 — note calendar month grid §B/D chips/cell wash. 2026-07-27 11:02:23 +03:00
aleksey 567090ebcc Note board-parity agenda rail and Discover thumb progress.
Refs EventHub/EventHubSpec#11
2026-07-27 10:05:14 +03:00
aleksey 862af12208 Close UI parity workstreams; document board deviations (Front#29).
Refs EventHub/EventHubFront#29 EventHub/EventHubSpec#11
2026-07-27 00:21:56 +03:00
aleksey 8825a24dde Mark CONTROL booking UI parity workstream done (Front#28).
Refs EventHub/EventHubFront#28 EventHub/EventHubSpec#11
2026-07-27 00:13:17 +03:00
aleksey 85201075c2 Mark Discover UI parity workstream done (Front#27).
Refs EventHub/EventHubFront#27 EventHub/EventHubSpec#11
2026-07-26 23:55:57 +03:00
aleksey f2b200a0f5 docs: mark calendar mood-chrome workstream done (Front#26). Refs EventHub/EventHubFront#26 Refs EventHub/EventHubSpec#11 2026-07-26 23:41:40 +03:00
aleksey ab1a1e6dba docs: mark UI parity mood system (Front#25) done. Refs EventHub/EventHubSpec#11 Refs EventHub/EventHubFront#25 2026-07-26 23:07:11 +03:00
aleksey 5e3c5cb691 docs: UI parity plan (mood-first) for CalenTIQ Front board. Refs EventHub/EventHubSpec#10 2026-07-26 22:48:43 +03:00
aleksey 1f6bc3decc Archive CalenTIQ client Front design SoT under design/front. Refs EventHub/EventHubSpec#10 2026-07-26 22:35:08 +03:00
aleksey 6f88205675 docs: domain/DNS checklist for CalenTIQ branding. Refs EventHub/EventHubSpec#10 2026-07-26 22:25:32 +03:00
aleksey 22360a66f7 docs: CalenTIQ branding + product VERSION 0.2. Refs EventHub/EventHubSpec#10 2026-07-26 22:04:23 +03:00
43 changed files with 1338 additions and 129 deletions
+119
View File
@@ -0,0 +1,119 @@
---
description: Типичные ошибки агентов EventHub — shell, git, docker, scope, токены (корень + субагенты)
alwaysApply: true
---
# EventHub: типичные ошибки агентов (избегать)
Канон. Субагентам: включать в промпт «следуй `agent-pitfalls.mdc` + `shell-wsl-not-powershell.mdc`».
## 1. Shell / PowerShell
| Симптом | Причина | Как правильно |
|---------|---------|---------------|
| `bash: /mnt/c/.../.tmp-*.sh: No such file or directory` при том что `ls` файл видит | **CRLF** от Cursor Write: shebang = `bash\r` | **Всегда** `wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/.../script.sh` (или `sed -i 's/\r$//' && bash`). **Не** голый `wsl -e bash /mnt/c/.../script.sh` |
| `unexpected EOF`, `ParserError`, пустой curl | PS ломает `"`, `$`, heredoc, `&&` | Файл `.sh` → `run-wsl-sh.sh` |
| `bash: syntax error near '('` | `grep -E ^(...)` в PS; **`fix(ci):` / `(…)` в `wsl -lc` one-liner** | Только внутри `.sh`; commit → `-F` файл (см. ниже) |
| `{{.Names}}: command not found` | `docker --format "{{…}}"` в PS | Формат в `.sh` или простой `docker ps` |
| `&&` is not valid | PowerShell не bash | Отдельные команды или `.sh` |
**Запрещено в PS:** голый `wsl -e bash /mnt/c/.../.tmp-*.sh` (без `run-wsl-sh.sh`), `wsl -e bash -lc "…\"…"`, `curl -d "{\"x\":…}"`, heredoc commit, `npm`/`rebar3`.
Канон запуска скриптов: см. `shell-wsl-not-powershell.mdc` § CRLF.
## 2. Git
| Симптом | Как правильно |
|---------|---------------|
| `#` в `-m` обрезает сообщение | `-F` файл; в тексте `Refs EventHub/EventHubBack#N` |
| `syntax error near '('` на commit | Сообщение `fix(ci): …` inline в `wsl -lc '… git-commit.sh … fix(ci): …'` | **Только** `.tmp-commit-msg.txt` + `git commit -F`; или `.sh` без inline `-m` |
| `Co-authored-by: Cursor <…>` / `<` ParserError | `git commit -F .tmp-msg.txt` из PS **или** `git-commit.sh` **из `.sh`**, не из PS one-liner |
| `editor` / not a terminal | Не `git commit` без `-F` в WSL; из Windows: `git commit -F file` |
| Сотни `M` после reset на `/mnt/c` | `git config core.filemode false` (локально); не «чинить» код |
| Удалились `.cursor/`, `.idea/` | **`git clean -fdx` только с backup** env + certs; IDE-директории **не трогать** |
| Push «висит» / ждёт login | **Не** `git push origin` — только `~/.cursor/eventhub/git-push.sh` / `git-push-main.sh` |
| Push заблокирован Auto-review | Push в `master` — только по явной просьбе; `request_smart_mode_approval` |
**`git clean` / reset к origin:** сначала сохранить `docker/.env*`, `.env.development`, `docker/traefik/certs/*`.
**Push:** запрещены голые `git push` / `git push origin …`. Канон — `bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh <repo-dir>` (токен из secrets).
**Commit (сообщение со скобками, `#`, `$`, пробелами):** не передавать subject/body аргументом через PowerShell или `wsl -e bash -lc '…'`. Пиши `.tmp-commit-msg.txt` в репо, затем в `.sh`:
```bash
cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack
git add path/to/file
GIT_EDITOR=true git commit -F .tmp-commit-msg.txt
```
Запуск из PS: `wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/.../EventHubBack/.tmp-commit.sh` — **без** голого `wsl -e bash …/.tmp-commit.sh`, **без** `-lc` и **без** `-m "fix(ci): …"`.
## 3. Docker dev (Swarm локально)
| Ошибка | Правильно |
|--------|-----------|
| «Балансировка» → 3× eventhub | **dev:** `traefik=1` + **`eventhub=1`** (см. `EventHubDevOps/docs/STANDS.md`) |
| `host-mode port already in use` | У eventhub опубликованы 8080/8445 — **нельзя** scale >1; LB = traefik |
| Собрали admin-ui/front без просьбы | «Только бэк» → eventhub (+ traefik если нужен HTTPS), **без** Front/Admin образов |
| Эмулятор не остановился | `docker service scale eventhub_bot-emulator-users=0` |
| `No such image: eventhub:latest` | Сначала `docker build -t eventhub:latest …` или `docker/build-images.sh` |
| Admin API dev не отвечает на :443 | Traefik без ports → **`http://127.0.0.1:8445`**; stage → SSH + `--resolve …:443:127.0.0.1` |
## 4. Scope (не додумывать)
- Делать **ровно** запрос; не раздувать (полный `build-images.sh`, 3 ноды, admin-ui, spec commit).
- Перед scale/deploy — прочитать compose и STANDS.md.
- Секреты: `gitea.ps1` / `.sh` + env; не inline `source secrets` в PS one-liner.
## 5. Субагенты (Task)
В промпт субагента добавлять:
```text
Shell: только WSL + .sh (см. shell-wsl-not-powershell.mdc, agent-pitfalls.mdc).
Один репо. Итог на русском. Не push без явного OK.
```
## 6. Экономия токенов / лимитов моделей
### Контекст чата
- **Один чат — одна задача**; после «готово / CI зелёный» — новый чат на следующий шаг (см. `WORKFLOW.md` §5).
- Workspace: для Back-задачи достаточно **`EventHubBack`** (+ Spec при необходимости); не держать все репо без нужды.
- **`@` точечно** — файл/папка, не `@Codebase` без причины.
- Не просить «проанализируй все transcripts» — они огромные.
### Дешёвые vs дорогие действия
| Дорого | Дешевле |
|--------|---------|
| Task explore «very thorough» | `grep` + 12 файла |
| Полный `build-images.sh` | Только нужный образ |
| Весь лог CI (80k строк) | API job + 20 строк вокруг ошибки |
| Несколько subagents «на всякий» | 1 subagent, узкий промпт |
| Browser deep-walk stage | Smoke-скрипт / один URL |
### Меньше итераций
- Сразу **`.sh`**, не 5 попыток PS one-liner (каждая = новый turn).
- Git перед commit: **один** `status` + `diff`, не три раза подряд.
- Тесты: `rebar3 eunit --module=…` если трогали один модуль; не полный suite без нужды.
- Docker: не rebuild, если `docker images` уже есть нужный tag.
### Модели и режимы
- **Ask / Plan** — вопросы и варианты без tool calls.
- **Agent** — только когда нужны команды и правки.
- Bugbot / Security Review — не на каждый мелкий diff.
### Scope в запросе (шаблон)
```text
Репо: EventHubBack
Issue: #N
Scope: только …
Не: front, docker, push, spec
Done: eunit / lint / …
```
### Subagents
- Редко; промпт = issue + файлы + done, **без** всей переписки.
- `one-repo-per-agent` — меньше контекста и ошибок.
### Повторяемое — в скрипты
Токены verify, smoke creds, stage deploy — **`~/.cursor/eventhub/*.sh`**, не заново через SSH в каждом чате.
+27
View File
@@ -0,0 +1,27 @@
---
description: Ответы субагентов и итоги работы — на русском
alwaysApply: true
---
# EventHub: язык ответов агентов
## Правило
Все **вызванные / фоновые агенты** (Task subagents) пишут пользователю и parent-агенту **на русском**:
- итоги, вердикты, чеклисты, UX-оценки;
- комментарии в Gitea (как и раньше — только русский);
- краткие status-апдейты в конце задачи.
## Допустимо на английском
- Идентификаторы кода, API paths, commit subject если в репо уже принят EN-стиль сообщений (можно и RU — главное единообразие в рамках задачи);
- цитаты из логов CI / stack traces без перевода;
- имена файлов, SHA, URL.
## Родитель-координатор
Тоже отвечает пользователю по-русски.
## Промпт субагенту (обязательно)
- «Return summary in Russian»
- «Shell: WSL + `.sh` only — `shell-wsl-not-powershell.mdc`, `agent-pitfalls.mdc`»
- «One repo per agent — `one-repo-per-agent.mdc`»
- «Экономия токенов: §6 agent-pitfalls — узкий scope, без лишних subagents/explore»
- Не push / не `git clean -fdx` без явного OK пользователя
+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.
+130
View File
@@ -0,0 +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/`.
+6 -6
View File
@@ -1,15 +1,15 @@
--- ---
description: Как открыть stage UI в Cursor Browser (alias + proxy) description: Stage UI в Cursor Browser — прямой HTTPS; proxy только DNS-fallback
alwaysApply: true alwaysApply: true
--- ---
# Stage browser (Cursor) # Stage browser (Cursor)
`*.eventhub.local` часто не резолвится в Chromium/Cursor Browser. **Канон:** прямой HTTPS с валидным TLS/DNS:
Канон: **`http://ui.stage.eventhub.test:8787`** через локальный proxy. - Client: `https://stage.calentiq.com/`
- Admin: `https://admin.stage.calentiq.com/`
Локальный proxy `http://ui.stage.eventhub.test:8787` (`scripts/stage-browser-alias-proxy.mjs`) — **только fallback**, если Cursor/Chromium не резолвит hostname. Не из‑за сертификата.
Полная инструкция: `EventHubSpec/STAGE-BROWSER.md` Полная инструкция: `EventHubSpec/STAGE-BROWSER.md`
Скрипт: `EventHubSpec/scripts/stage-browser-alias-proxy.mjs` (запуск **Windows Node** на `:8787`).
Hosts: `127.0.0.1 ui.stage.eventhub.test`
+50
View File
@@ -0,0 +1,50 @@
# CalenTIQ branding (серия 0.2)
Краткие зафиксированные решения для UI / публичного бренда. Внутренние имена репозиториев (`EventHub*`) не меняем.
## Публичный бренд
- **Имя:** CalenTIQ
- **Смысл:** calendar + TIQ (time intelligence)
- **Tagline (направление):** One calendar. Three modes for your time.
- **Логотип:** Time Arc (дуга часов + равные точки слотов); master vector в [`design/front/logo/`](design/front/logo/)
## Moods (UI-лейблы)
Внутренние ключи API/preferences пока: `calm` | `energetic` | `business`.
| Ключ (API) | Публичный лейбл | Акцент |
|------------|-----------------|--------|
| `calm` | FLOW | sage `#ABC5B5` / primary `#2F7A68` |
| `energetic` | MOMENTUM | coral `#FF6A3D` на ink |
| `business` | CONTROL | steel `#324A5F` |
IA без изменений: top nav + bottom nav mobile; без left sidebar; без 4-го Creator mood.
## Design / Brand pack SoT
Канон в Spec (клиентский фронт):
- [`design/front/`](design/front/) — UI-борды, Time Arc (mark/lockup), mood icons, tokens
- [`design/front/README.md`](design/front/README.md) — опись файлов и IA
- [`design/front/UI-PARITY.md`](design/front/UI-PARITY.md) — план visual parity (mood как product surface; workstreams + non-goals)
- [`design/admin/`](design/admin/) — зарезервировано под админку (пока пусто)
Runtime-копии в приложении: `EventHubFront/public/brand/` (mark, lockup, favicons, mood app icons).
Локальные ZIP (`calentiq-brand-pack-local.zip` и т.п.) — рабочие сборки; в git SoT не кладём.
## Domain / DNS (чеклист, без покупки)
Публичный бренд ≠ внутренние хосты `*.eventhub.local` / `*.eventhub.test` (DevOps). Перед продом:
- [ ] Выбрать домен (кандидат: `calentiq.*` / аналог) — **не покупать** без явного ок владельца
- [ ] RDAP/whois: свободен ли домен; нет ли конфликтующих TM
- [ ] DNS: A/AAAA или CNAME на edge (Traefik/CDN); отдельно `www` / apex
- [ ] TLS: ACME (Lets Encrypt) или managed cert; проверить SAN
- [ ] Почта: SPF/DKIM/DMARC когда будет SMTP (серия B2)
- [ ] Stage/IFT: либо оставить `eventhub.*` стенды, либо завести `*.calentiq.*` aliases в DevOps
- [ ] Обновить публичные URL в UI/письмах только после cutover
## Epic
https://git.sabilin.com/EventHub/EventHubSpec/issues/10
+130 -42
View File
@@ -19,17 +19,35 @@ EventHub — платформа для управления событиями
- Гибкое подтверждение заявок: `auto` | `manual` | `{timeout, N}` (секунды) — детали §2.1.2 / §2.3 - Гибкое подтверждение заявок: `auto` | `manual` | `{timeout, N}` (секунды) — детали §2.1.2 / §2.3
- Теги календаря, рейтинг (средняя оценка, количество голосов) - Теги календаря, рейтинг (средняя оценка, количество голосов)
- После успешной верификации email (`POST /v1/verify`, статус пользователя → `active`) система - После успешной верификации email (`POST /v1/verify`, статус пользователя → `active`) система
идемпотентно создаёт дефолтный **personal**-календарь владельцу (`logic_calendar:ensure_default_calendar/1`): идемпотентно нормализует **единственный personal** владельца
название — `nickname` или «Мой календарь», `confirmation=manual`. Повторный вызов не создаёт дубликат, (`logic_calendar:ensure_default_calendar/1``normalize_owner_personals/1`):
если у пользователя уже есть active personal-календарь. Существующим пользователям без календаря - title в БД — **`Default`** (EN); UI — i18n «По умолчанию»;
backfill не выполняется. - `confirmation=manual`;
- если personal нет — создаёт; если есть — оставляет **самый ранний** (`created_at`), title → `Default`;
- лишние personal: с событиями/специалистами → `type=commercial` (system path, без требования
subscription); пустые → soft-delete.
- Повторный вызов идемпотентен. Миграция `20260730200000_single_default_personal`
`backfill_single_personal/0` для всех владельцев.
- `POST /v1/calendars` с `type=personal` при уже существующем personal → **409**
`{error: "personal_exists"}`.
- `DELETE` единственного personal → **403** `{error: "default_calendar"}`.
- `PUT` `personal → commercial` на единственном personal → **403**
`{error: "default_calendar"}` (новый бизнес — отдельный create commercial).
**Новые поля (задача #12):** **Новые поля (задача #12):**
- `short_name` — короткое уникальное имя для API и поиска - `short_name` — короткое уникальное имя для API и поиска
- `category` — категория (enum) - `category` — категория (enum)
- `color` — цвет отображения - `color` — цвет отображения
- `image_url` — изображение календаря - `image_url` — изображение календаря
- `settings` — дополнительные настройки (map) - `settings` — дополнительные настройки (map). Известные org-default ключи
(валидируются на PUT/PATCH; неизвестные ключи — passthrough, напр. `week_patterns`):
- `default_location``{ "address": "…", "lat"?: number, "lon"?: number }`;
`address` — непустая строка; `lat`/`lon` — оба или ни одного (числа)
- `default_duration_minutes` — integer 1..1440
- `default_recurrence``null` или
`{ "enabled": boolean, "freq": "DAILY"|"WEEKLY"|"MONTHLY", "interval": integer ≥ 1 }`
- Невалидное значение известного ключа → **400** `{error: "invalid_settings", key: "…"}`
(см. EventHub/EventHubBack#63, UI: EventHub/EventHubFront#44)
### 2.1.1. Специалисты календаря ### 2.1.1. Специалисты календаря
Таблица `calendar_specialist`: связь пользователя-специалиста с **commercial**-календарём. Таблица `calendar_specialist`: связь пользователя-специалиста с **commercial**-календарём.
@@ -78,14 +96,17 @@ EventHub — платформа для управления событиями
слоты освобождаются; слоты освобождаются;
- уже `confirmed`**оставляем**; - уже `confirmed`**оставляем**;
- owner: просмотр/редактирование своих событий и настроек, CRUD specialists — **разрешены**; - owner: просмотр/редактирование своих событий и настроек, CRUD specialists — **разрешены**;
- create нового commercial / upgrade type → `402`. - create нового commercial / upgrade type → `402` (нужна **уже active** подписка
или trial; auto-start trial при create **нет** — см. ниже и §2.9).
Периодический job (например раз в 60 с): помечает просроченные подписки `expired`, отменяет Периодический job (например раз в 60 с): помечает просроченные подписки `expired`, отменяет
pending владельца, **не** меняет `calendar.type`. Legacy `downgrade_user_calendars/1` pending владельца, **не** меняет `calendar.type`. Legacy `downgrade_user_calendars/1`
(type → personal) — удалить/не использовать. (type → personal) — удалить/не использовать.
Первое создание commercial без подписки: **auto-start trial** (один раз, `trial_used`); Create / upgrade `commercial` без **уже active** подписки или trial → **`402`**.
повтор без подписки → `402`. Планы/цены — §2.9. Trial стартует **только** явным `POST /v1/subscription` с `action=start_trial`
(один раз, `trial_used`); при create commercial auto-start trial **нет**.
Планы/цены — §2.9.
В ответах календаря (user API): `booking_open` (boolean) — производное от В ответах календаря (user API): `booking_open` (boolean) — производное от
`type=commercial` ∧ calendar `active` ∧ подписка владельца active. `type=commercial` ∧ calendar `active` ∧ подписка владельца active.
@@ -94,24 +115,48 @@ pending владельца, **не** меняет `calendar.type`. Legacy `downg
- `POST /v1/events/:id/bookings` только если календарь события commercial, `booking_open=true`, - `POST /v1/events/:id/bookings` только если календарь события commercial, `booking_open=true`,
событие `active`, есть свободная вместимость. событие `active`, есть свободная вместимость.
- Для **recurring** master в теле обязательно `occurrence_start` (ISO8601). Без поля → `400`
(`occurrence_start required`). Невалидное или отменённое вхождение → `400`.
Back материализует instance (`is_instance=true`, `master_id`) и вешает booking на его `id`.
Для `single` / уже материализованного instance тело опционально, `occurrence_start` игнорируется.
- **Pending занимает capacity** наравне с confirmed (защита от overbook при auto/timeout). - **Pending занимает capacity** наравне с confirmed (защита от overbook при auto/timeout).
- Capacity: число booking со статусом `pending` | `confirmed`; `cancelled` не считаются. - Capacity: число booking со статусом `pending` | `confirmed`; `cancelled` и `expired` не считаются.
- Политика `confirmation` календаря при создании booking: - Политика `confirmation` календаря при создании booking:
- `auto` → сразу `confirmed` + `confirmed_at`; - `auto` → сразу `confirmed` + `confirmed_at`;
- `manual``pending`; confirm/decline — владелец или specialist (см. ниже); - `manual``pending`; confirm/decline — владелец или specialist (см. ниже);
- `{timeout, N}``pending`; через N секунд без решения: auto-confirm, если ещё есть - `{timeout, N}``pending`; через N секунд без решения: auto-confirm, если ещё есть
capacity, иначе `cancelled` (`timeout_full`). capacity, иначе `cancelled` (`timeout_full`).
- Участник: `DELETE /v1/bookings/:id` — отмена своей pending/confirmed. - **Past-pending → `expired`:** если событие уже началось (`now >= event.start_time`), а booking
ещё `pending`, статус переводится в `expired` (lazy при чтении списков/`GET` booking и в
`process_timeout_bookings`). `expired` не actionable: Confirm/Decline → `409` (`Booking expired`).
- **Inbox:** `GET /v1/user/booking-requests` возвращает только ещё actionable pending
(до старта события); past-pending помечает `expired` и **не** включает в ответ.
- Участник: `DELETE /v1/bookings/:id` — отмена своей pending/confirmed; для уже `expired`
no-op успех (как для `cancelled`).
- WS: `booking_update` участнику и владельцу (и specialist при confirm на «своём» событии). - WS: `booking_update` участнику и владельцу (и specialist при confirm на «своём» событии).
#### Специалисты #### Специалисты
- Специалист = существующий `user`, привязанный к commercial-календарю после **принятия приглашения**. - Специалист = существующий `user`, привязанный к commercial-календарю
(`calendar_specialist`, `status` active|inactive).
- **Не путать** с `calendar_share` (права read/write/admin — фаза 2). - **Не путать** с `calendar_share` (права read/write/admin — фаза 2).
##### Owner как specialist (одиночки)
- При **create** `commercial` (и при системном upgrade personal→commercial) бэкенд
**идемпотентно** создаёт строку `calendar_specialist` на `owner_id`:
`status=active`, `name` из nickname (иначе email), `specialization=[]`.
- Owner-строка **неудаляема**: `DELETE /v1/calendars/:id/specialists/:owner_id`
`403` (`Owner specialist cannot be removed` / `owner_specialist_protected`).
- «Убрать себя из специалистов» = `PUT` с `status=inactive` (продуктовая галочка);
снова включить — `status=active`. Редактируются также `name` / `specialization`.
- Invite себе **не** требуется. Миграций/backfill старых календарей нет (wipe БД
на стендах при деплое).
##### Приглашение (`specialist_invite`) ##### Приглашение (`specialist_invite`)
Владелец commercial **не** вводит сырой `user_id` в продуктовом UI. Добавление — через invite: Владелец commercial **не** вводит сырой `user_id` в продуктовом UI для **других**
специалистов. Добавление команды — через invite:
| Канал | Как | | Канал | Как |
|-------|-----| |-------|-----|
@@ -155,7 +200,10 @@ pending владельца, **не** меняет `calendar.type`. Legacy `downg
- `POST /v1/calendars/:id/specialist-invites` — тело: `{ user_id }` **или** `{ email }`, - `POST /v1/calendars/:id/specialist-invites` — тело: `{ user_id }` **или** `{ email }`,
опционально `name`, `specialization` опционально `name`, `specialization`
- `DELETE /v1/calendars/:id/specialist-invites/:invite_id` — отмена pending (`cancelled`) - `DELETE /v1/calendars/:id/specialist-invites/:invite_id` — отмена pending (`cancelled`)
- `PUT/DELETE /v1/calendars/:id/specialists/:user_id`deactivate / remove уже принятого - `PUT /v1/calendars/:id/specialists/:user_id`update `name` / `specialization` / `status`
(в т.ч. owner)
- `DELETE /v1/calendars/:id/specialists/:user_id` — remove принятого; для
`user_id = owner_id`**403** (см. Owner как specialist)
Invitee: Invitee:
@@ -167,10 +215,14 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
допускается только как внутренний/тестовый путь или удаляется после миграции на invite. допускается только как внутренний/тестовый путь или удаляется после миграции на invite.
Продуктовый путь: invite → accept → specialist. Продуктовый путь: invite → accept → specialist.
- `event.specialist_id` опционален; если задан — только `active` specialist этого календаря - `event.specialist_id` опционален на `POST`/`PUT` события; если задан — персистится
(иначе `400`). и валидируется: только `active` specialist этого календаря (иначе `400`
`Invalid specialist_id for this calendar`).
- **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` агрегирует **actionable** pending
по тем же правилам (owner — все события своих календарей; specialist — только свои слоты);
past-pending → `expired` и из ответа исключается.
#### Фаза 2 (вне текущего контракта реализации) #### Фаза 2 (вне текущего контракта реализации)
- `calendar_share` (`read` | `write` | `admin`): не путать с follow, specialist_invite и платной subscription. - `calendar_share` (`read` | `write` | `admin`): не путать с follow, specialist_invite и платной subscription.
@@ -202,12 +254,11 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
- Возвращать как одиночные, так и сгенерированные вхождения в едином списке. - Возвращать как одиночные, так и сгенерированные вхождения в едином списке.
#### 2.2.3. Материализация при записи участника #### 2.2.3. Материализация при записи участника
При записи участника на конкретное вхождение повторяющегося события: `POST /v1/events/:id/bookings` на мастер серии:
- Система материализует (создаёт) физическую запись события для этого вхождения, если оно ещё - в теле JSON: `occurrence_start` — время вхождения;
не было материализовано (например, для хранения количества записавшихся). - если instance с этим `start_time` ещё нет — создаётся (`is_instance=true`, `master_id`);
- Материализованное событие имеет `is_instance = true` и ссылается на `master_id`. - `booking.event_id` — id материализованного вхождения, не master.
- Запись участника (`booking`) всегда привязывается к конкретному экземпляру (материализованному Одиночные события бронируются как раньше (тело может быть пустым `{}`).
или одиночному событию).
#### 2.2.4. Изменение и удаление серий #### 2.2.4. Изменение и удаление серий
- При редактировании мастера можно применить изменения ко всем будущим экземплярам или создать - При редактировании мастера можно применить изменения ко всем будущим экземплярам или создать
@@ -245,12 +296,17 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
- Запись доступна только на событиях **commercial**-календаря с `booking_open=true` (§2.1.2). - Запись доступна только на событиях **commercial**-календаря с `booking_open=true` (§2.1.2).
- На personal → `403` (`personal_calendar`); при restricted commercial → `403` (`subscription_inactive`). - На personal → `403` (`personal_calendar`); при restricted commercial → `403` (`subscription_inactive`).
- В зависимости от `confirmation` календаря: auto / manual / timeout (§2.1.2). - В зависимости от `confirmation` календаря: auto / manual / timeout (§2.1.2).
- Статусы: `pending`, `confirmed`, `cancelled`. - Статусы: `pending`, `confirmed`, `cancelled`, `expired`.
- **Capacity:** лимит слотов считают `pending` + `confirmed` (pending резервирует место). - **Capacity:** лимит слотов считают `pending` + `confirmed` (pending резервирует место;
- Участник может отменить свою запись (`DELETE`). `cancelled` / `expired` не занимают).
- **Истечение:** past-pending (событие уже началось) → `expired` (lazy + timeout job); не
путать с `specialist_invite.status=expired`.
- Участник может отменить свою запись (`DELETE`) для pending/confirmed; `expired`/`cancelled`
идемпотентный успех.
- Confirm/decline: владелец календаря — любые заявки; active specialist — только на событиях - Confirm/decline: владелец календаря — любые заявки; active specialist — только на событиях
со своим `specialist_id` (§2.1.2). со своим `specialist_id` (§2.1.2). На `expired` (и после lazy-mark) → `409`.
- При подтверждении фиксируется `confirmed_at`. - При подтверждении фиксируется `confirmed_at`.
- Inbox `GET /v1/user/booking-requests` — только pending до старта события (§2.1.2).
### 2.4. Отзывы и рейтинги ### 2.4. Отзывы и рейтинги
- Пользователи могут оставлять отзывы (рейтинг 1–5 и комментарий) на события или календари. - Пользователи могут оставлять отзывы (рейтинг 1–5 и комментарий) на события или календари.
@@ -325,8 +381,9 @@ Legacy: прямой `POST /v1/calendars/:id/specialists` **не использ
- Цены (`logic_subscription:plan_price/1`, minor units): monthly **999**, quarterly **2499**, - Цены (`logic_subscription:plan_price/1`, minor units): monthly **999**, quarterly **2499**,
biannual **4499**, annual **7999**; trial — 0. Платёжный шлюз — заглушка (`process_payment` → ok). biannual **4499**, annual **7999**; trial — 0. Платёжный шлюз — заглушка (`process_payment` → ok).
- Статус подписки: `active`, `expired`, `cancelled`. - Статус подписки: `active`, `expired`, `cancelled`.
- Отслеживание использования пробного периода (`trial_used`); trial стартует автоматически при - Отслеживание использования пробного периода (`trial_used`); trial стартует **только**
первой попытке создать/апгрейднуть commercial (§2.1.2). явным `POST /v1/subscription` (`action=start_trial`), не при create/upgrade commercial
(§2.1.2). Create/upgrade commercial без уже active sub/trial → `402`.
- Истечение: периодический job → `expired`; commercial-календари **не** меняют type — - Истечение: периодический job → `expired`; commercial-календари **не** меняют type —
переходят в restricted; после `activate` / новой active подписки функционал восстанавливается переходят в restricted; после `activate` / новой active подписки функционал восстанавливается
(§2.1.2). Не использовать legacy downgrade type → personal. (§2.1.2). Не использовать legacy downgrade type → personal.
@@ -453,37 +510,67 @@ 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`**actionable** pending-заявки к подтверждению, где
текущий пользователь — **owner** календаря события или **assigned specialist**
(`event.specialist_id` = user и specialist active). Учитываются и материализованные
occurrence (`is_instance`). Past-pending помечается `expired` и **не** попадает в ответ.
- `GET /v1/user/studio-bookings` — pending **и confirmed** на тех же календарях (журнал студии).
Форма ответа как у booking-requests (`role` + вложенный `event`).
- `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` — поиск; **без токена** (гость) — только commercial по `can_access`;
- `GET /v1/calendars` — список календарей. пустой запрос + пагинация/`type` — discovery tops. С Bearer — как раньше (включая свои personal).
- `POST /v1/calendars` — создать календарь (`commercial` → подписка/trial; иначе `402`). В calendar-результатах — `image_url` (если задан на календаре). Upload/multipart
- `GET /v1/calendars/:id` — календарь (`following`, `booking_open` для текущего контекста). **не** входит в контракт: поле URL.
- `PUT /v1/calendars/:id`обновить календарь (`personal→commercial` → gate подписки). - `GET /v1/calendars`список календарей (auth).
- `POST /v1/calendars` — создать календарь (`commercial` → нужна уже active sub/trial;
иначе `402`; auto-start trial нет).
- `GET /v1/calendars/:id` — календарь. **Без токена** для active commercial (`following: false`);
personal без доступа → `403`. С сессией: `following`, `booking_open`.
- `PUT /v1/calendars/:id` — обновить календарь (`personal→commercial` → нужна уже
active sub/trial, иначе `402`).
- `DELETE /v1/calendars/:id` — удалить календарь. - `DELETE /v1/calendars/:id` — удалить календарь.
- `POST /v1/calendars/:id/follow` — отслеживать чужой календарь. - `POST /v1/calendars/:id/follow` — отслеживать чужой календарь.
- `DELETE /v1/calendars/:id/follow` — снять follow. - `DELETE /v1/calendars/:id/follow` — снять follow.
- `GET /v1/users/lookup?q=` — typeahead пользователей для invite (минимальный PII, rate-limit). - `GET /v1/users/lookup?q=` — typeahead пользователей для invite (минимальный PII, rate-limit).
- `GET /v1/calendars/:id/specialists` — список специалистов (владелец). - `GET /v1/calendars/:id/specialists` — список специалистов (гость на commercial — да;
personal — как `can_access`).
- `PUT/DELETE /v1/calendars/:id/specialists/:user_id` — deactivate / убрать специалиста (владелец). - `PUT/DELETE /v1/calendars/:id/specialists/:user_id` — deactivate / убрать специалиста (владелец).
- `GET/POST /v1/calendars/:id/specialist-invites` — исходящие invite / создать (владелец). - `GET/POST /v1/calendars/:id/specialist-invites` — исходящие invite / создать (владелец).
- `DELETE /v1/calendars/:id/specialist-invites/:invite_id` — отменить pending (владелец). - `DELETE /v1/calendars/:id/specialist-invites/:invite_id` — отменить pending (владелец).
- `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 /v1/calendars/:calendar_id/events` — события календаря. - `GET /v1/calendars/:calendar_id/events` — события календаря (гость: commercial).
- `POST /v1/calendars/:calendar_id/events` — создать событие. В JSON каждого события commercial-календаря — опциональное поле
- `GET /v1/events/:id` — событие. `booking_occupancy`: `"free"` | `"pending"` | `"confirmed"`.
- `PUT /v1/events/:id` — обновить событие. Считается по **active** bookings события (`pending` | `confirmed`);
`cancelled` / `expired` **не** дают занятость. Приоритет агрегата:
`confirmed` > `pending` > `free` (если есть хотя бы один confirmed →
`"confirmed"`; иначе если есть pending → `"pending"`; иначе `"free"`).
Для personal: поле можно omit или всегда `"free"`.
Virtual occurrences (expand списка): occupancy по **event id в ответе**
(материализованный instance, если он уже есть и его id отдан; иначе —
тот id, с которым Back отдаёт вхождение — обычно master / шаблон —
bookings смотрятся по этому id).
- `POST /v1/calendars/:calendar_id/events` — создать событие (тело может включать
опциональный `specialist_id`; invalid → `400`).
- `GET /v1/events/:id` — событие (тот же контракт `booking_occupancy`, что
у списка events выше).
- `PUT /v1/events/:id` — обновить событие (в т.ч. `specialist_id`; invalid → `400`).
- `DELETE /v1/events/:id` — удалить событие. - `DELETE /v1/events/:id` — удалить событие.
- `GET /v1/events/:id/occurrences` — вхождения повторяющегося события. - `GET /v1/events/:id/occurrences` — вхождения повторяющегося события.
- `DELETE /v1/events/:id/occurrences/:start_time` — отменить вхождение серии. - `DELETE /v1/events/:id/occurrences/:start_time` — отменить вхождение серии.
- `POST /v1/events/:id/bookings` — запись на событие. - `POST /v1/events/:id/bookings` — запись на событие. Recurring master: тело
`{ "occurrence_start": "<ISO8601>" }` обязательно; booking на материализованный instance.
- `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`); владелец
- `DELETE /v1/bookings/:id` — отменить бронирование (участник). любые booking календаря; active specialist — только события со своим `specialist_id`.
На past-pending / уже `expired``409` (`Booking expired`); при полной вместимости на
confirm → `409` (`Event is full`).
- `DELETE /v1/bookings/:id` — отменить бронирование (участник; `expired`/`cancelled` — no-op `200`).
- `POST /v1/reviews` — создать отзыв. - `POST /v1/reviews` — создать отзыв.
- `GET /v1/reviews` — список отзывов (поле `my_vote` для текущего пользователя). - `GET /v1/reviews` — список отзывов (поле `my_vote` для текущего пользователя).
- `GET /v1/reviews/:id` — отзыв по ID (`my_vote`). - `GET /v1/reviews/:id` — отзыв по ID (`my_vote`).
@@ -497,7 +584,8 @@ src/
- `POST /v1/tickets` — создать или обновить тикет (дедуп по `error_hash`): body `{ error_message, stacktrace?, context?, source?: frontend|manual }`; ответ 201 (или 429 при rate-limit новых). - `POST /v1/tickets` — создать или обновить тикет (дедуп по `error_hash`): body `{ error_message, stacktrace?, context?, source?: frontend|manual }`; ответ 201 (или 429 при rate-limit новых).
- `GET /v1/tickets/:id` — статус тикета. - `GET /v1/tickets/:id` — статус тикета.
- `GET /v1/subscription` — подписка пользователя. - `GET /v1/subscription` — подписка пользователя.
- `POST /v1/subscription`активировать подписку. - `POST /v1/subscription``action=start_trial` | `activate` (+ `plan`, опционально
`payment_info`). Trial — только через `start_trial`, не через create commercial.
- `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — HTML-календарь (владелец), включая архив. - `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — HTML-календарь (владелец), включая архив.
### WebSocket (порт 8081) ### WebSocket (порт 8081)
+190 -43
View File
@@ -37,8 +37,8 @@ Deep-link на календарь, владельцем которого явл
- Язык: TypeScript - Язык: TypeScript
- Сборщик: Vite - Сборщик: Vite
- UI: React 19 + Tailwind CSS 4 + shadcn/Radix - UI: React 19 + Tailwind CSS 4 + shadcn/Radix
- Типографика: Syne (display) + Manrope (UI) - Типографика: **Syne** только для логотипа CalenTIQ (латиница); UI, заголовки, даты и время — **Manrope** (кириллица)
- Состояние: Zustand (auth + mood + calendar context), TanStack React Query v5 (сервер) - Состояние: Zustand (auth + mood + lens + calendar context), TanStack React Query v5 (сервер)
- HTTP: Axios с интерсепторами - HTTP: Axios с интерсепторами
- Формы: React Hook Form + Zod - Формы: React Hook Form + Zod
- Дата/время: dayjs (ru locale) - Дата/время: dayjs (ru locale)
@@ -51,58 +51,148 @@ Deep-link на календарь, владельцем которого явл
- Главная сущность — **календарь**. Центр UI — виджет с видами **месяц** (default) / **неделя** / **день**. - Главная сущность — **календарь**. Центр UI — виджет с видами **месяц** (default) / **неделя** / **день**.
- Главные вкладки: **Календарь** (`/`, `/c/:id`), **Найти** (`/search`), **Записи** (`/bookings`), **Ещё** (`/more`). - Главные вкладки: **Календарь** (`/`, `/c/:id`), **Найти** (`/search`), **Записи** (`/bookings`), **Ещё** (`/more`).
- Контекст виджета: свой календарь (селектор) или чужой (browse после поиска). Чужой `personal` - **Записи** (`/bookings`) — grouped inbox: «К подтверждению» (owner/specialist pending + Confirm/Decline)
и «Мои записи» (participant); пустые группы скрывать. Confirm на карточке события остаётся.
Past/expired: бейдж `expired` («истекла»), Cancel скрыт; empty-state с CTA Discover / календари.
Время на list-карточках — вторичное к title (`.eh-book-list-time`).
- В **Ещё**: язык (`LanguageSwitcher`); пункт входящих specialist invites скрыт при
`pending=0`; при `pending > 0` — badge со счётчиком (deep-link `/invites` работает).
**Mood в Ещё нет** — chip в AppShell рядом с BrandWordmark (Front#40; ранее Front#39
в chrome workspace).
- **Mood + Lens (Front#39/#40 + Front#46):** mood chip (FLOW/MOMENTUM/CONTROL → sheet picker) —
в **AppShell** рядом с логотипом (**не** в toolbar Month/Week/Day). **Lens** — инфо-табло
**под строкой AppShell** (logo/mood/nav) на calendar workspace: сегменты Обзор / Сегодня /
Заявки + метрики/chips (не рядом с ViewMode). Soft-default по mood **без auto-pin** при
клике на вкладку; при смене mood — `DEFAULT_LENS_BY_MOOD`. Workspace chrome: (1) селектор;
(2) период + вид Месяц/Неделя/День; (3) owner actions — для commercial только
«+ Новое событие» («Заполнить расписание» — во вкладке Студия). Без flip / CREATOR.
Нет lens на HTML-архиве месяца / не-calendar routes.
- **Week view (Front#40):** одна строка day-headers (без дубля заголовков
`WeekDayColumn`); горизонтальный скролл через `.eh-cal-week-scroll` (mobile ~390).
- Контекст виджета: свой календарь (селектор) или browse чужого (после поиска). Чужой `personal`
только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false` только просмотр; чужой `commercial` с `booking_open=true` — запись; при `booking_open=false`
(restricted / нет подписки владельца) — просмотр + сообщение «Запись временно недоступна». — просмотр + «Запись временно недоступна». Бейдж «чужой» **не** показывать; CTA
- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель. Действия зависят от роли (owner / participant). **«Мой календарь»** + `ChevronLeft``/default`.
- Без выбранного события — панель «О календаре» (описание, title/meta, рейтинг, отзывы). - **Единственный personal** (Back: title `Default`, UI «По умолчанию»): overlay confirmed
Форма отзыва на календарь/событие — только при confirmed booking; жалоба доступна без записи. записей на чужие commercial + specialist duty (Front#35/#43) — **только** на нём, не на
На чужом календаре — CTA **«Отслеживать» / Follow** (не путать с платной «Подписка»); студиях. Create calendar UI — только тип **Студия** (`commercial`). Селектор: «По умолчанию»
follow не открывает форму отзыва. + студии.
- На чужом `/c/:id` — CTA «Мой календарь» и таб Calendar не `aria-current` (intent `browse`). - **Guest на чужом commercial (D9 модель B):** roster карточек активных специалистов →
тап → сетка только со слотами `event.specialist_id` = выбранный; «Все слоты студии»
снимает фильтр. Это **не** chip-filter на общей сетке. Book как обычно — без
`specialist` в body заявки (слот уже привязан к спецу на событии).
В режиме «Все» — тот же **studio-агрегат** слотов, что у owner «Студия» (см. ниже);
фильтр D9 **без** второго UI режима.
- **Owner commercial workspace — режимы Студия / Мастер:**
- **Студия** — агрегат слотов по `start_time` + `duration`: одна ячейка с заливкой
`booked/total`, label `HH:mm · K/N свободно`; клик → popover мастеров со статусами
(занятость по `booking_occupancy` / active bookings, BackSpec §6 events).
- **Мастер** — фильтр сетки по `specialist_id`; sticky roster как guest D9 B
(**owner тоже** использует roster-фильтр, не только guest).
- **Месяц + Студия:** density markers в ячейках дня, **не** список чипов слотов.
- **Personal:** без studio-агрегата и без вкладки «Команда» / Team.
-`0.0` на free-слотах **не** показывать.
- Agenda / rail desktop (commercial owner, `≥ 1024px`): вкладки **Расписание | Команда | Студия** в
общем `.eh-owner-rail-panel` **фиксированной высоты** (вкладки не прыгают). Команда:
invite свёрнут за «Пригласить». **Студия** = hub: about + «Заполнить расписание» +
«Редактировать» (+ link `/calendars`); Delete только на `/calendars`. Phone **и tablet**:
segmented **День | Команда | Студия** под grid (не боковой rail — иначе колонки недели
сжимаются). Карточка события: sheet `< 768px`, боковая панель `≥ 768px`. Списки —
`ScrollRegion` (полоса скрыта, стрелки по краям при overflow); dialogs — thin scrollbar
on hover.
- Agenda empty: personal «Нет событий»; commercial «Свободных окон нет». UI-тип commercial —
«Студия» (не «коммерческий»).
- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель.
- На browse `/c/:id` — CTA «Мой календарь» (+ иконка) → `/default`; tab Calendar не
`aria-current`. Follow на чужом commercial без изменений.
- Список отслеживаемых: `/following` (из «Ещё»). - Список отслеживаемых: `/following` (из «Ещё»).
- Открытие события — `navigate` push (не `replace`), чтобы Back возвращал к календарю. - Открытие события — `navigate` push (не `replace`), чтобы Back возвращал к календарю.
- Owner: WS subscribe на активный календарь; `PUT` события из карточки; `PUT` календаря на `/calendars`. - Owner: WS subscribe на активный календарь; `PUT` события из карточки; `PUT` календаря на `/calendars`.
- Поиск (`/search`): query/filters в URL params (восстановление при возврате); chip type фильтрует - Поиск (`/search`): query/filters в URL params (восстановление при возврате); chip type фильтрует
discovery tops без ухода из Popular; в строке результата — id snippet и `calendar_title` для event. discovery tops без ухода из Popular; в строке результата — id snippet и `calendar_title` для event.
- CRUD своих календарей — `/calendars` (из «Ещё»). Legacy `/discover`, `/calendars/:id` → redirects. - CRUD своих календарей — `/calendars` (из «Ещё»). Legacy `/discover`, `/calendars/:id` → redirects.
- На мобиле — bottom tab bar (+ safe-area); на desktop — sticky header + табы. - На **phone и tablet** (`< 1024px`, Tailwind `lg`) — одна primary nav: bottom tab bar
(+ safe-area). На **desktop** (`≥ 1024px`) — **одна строка** chrome:
logo + mood chip | nav tabs | nickname + logout (не два ряда header+nav, не bottom+top
одновременно). Сырой email в chrome не показывать — primary identity = `nickname`
(fallback без `@`). Mobile: logo mark-only + mood chip icon-only (Front#45);
desktop — полный wordmark + label.
Nav `aria-current` синхронизирован с `useLocation` (без remount `Outlet` по pathname). Nav `aria-current` синхронизирован с `useLocation` (без remount `Outlet` по pathname).
- Mood themes — см. §3.2. - Mood themes — см. §3.2.
## 3.2. Настроения (mood themes) ## 3.2. Настроения (mood themes) / режимы CalenTIQ
Пользователь переключает визуальное настроение; атрибут `html[data-mood]`. Для гостя и при отсутствии `preferences.mood` — дефолт `calm` (в профиль при логине не пишется). После входа смена mood сохраняется в `preferences.mood` через `PATCH /v1/user/me`. Публичный бренд UI: **CalenTIQ** (см. [BRANDING.md](BRANDING.md)). Внутренние ключи API/preferences без изменений. Три mood только — **без CREATOR**, без flip календаря.
| Mood | Идентификатор | Характер | Пользователь переключает визуальный режим; атрибут `html[data-mood]`. Для гостя и при отсутствии `preferences.mood` — дефолт `calm` (в профиль при логине не пишется). После входа смена mood сохраняется в `preferences.mood` через `PATCH /v1/user/me`.
|------|---------------|----------|
| Спокойный | `calm` | sage/mist, мягкий градиент (default) |
| Энергичный | `energetic` | тёмный ink + coral/amber |
| Деловой | `business` | charcoal + steel, более жёсткие радиусы |
Переключатели mood и языка: на экранах auth (`/login`, `/register`, `/verify`) и в `/more`. В шапке workspace — только бренд, email и выход (без mood/lang). | UI-лейбл | Идентификатор (API) | Характер |
|----------|---------------------|----------|
| FLOW | `calm` | sage/mist, мягкий градиент (default) |
| MOMENTUM | `energetic` | тёмный ink + coral `#FF6A3D` |
| CONTROL | `business` | charcoal + steel `#324A5F`, более жёсткие радиусы |
Язык UI (`ru`/`en`): до логина — `navigator.language`; после входа — поле `language` профиля (`PATCH /v1/user/me`). Если в профиле язык пуст — при логине записывается текущий (браузерный/выбранный на экране входа). Язык также можно сменить в форме `/profile`. **Где mood:**
- Auth (`/login`, `/register`, `/verify`, forgot/reset) — compact `MoodSwitcher` (+ language).
- AppShell — primary control: **mood chip** рядом с BrandWordmark → dialog/sheet
(**не** в `/more`, **не** в calendar toolbar Month/Week/Day). Desktop: logo + mood \| nav \| nickname + logout.
**Front#45:** mobile (`< sm`) — полный BrandWordmark CalenTIQ + mood chip
icon-only (`aria-label` = FLOW/MOMENTUM/CONTROL); с `sm+` — полный label chip
без truncate. Sheet: §C glyph (sprout/bolt/briefcase) + label + tagline.
Time Arc appicon — только favicon, не в switcher.
**Lens** (отдельно от mood и от view month/week/day): `overview` / `today` / `bookings`
(UI: Обзор / Сегодня / Заявки). Инфо-табло **под AppShell** на calendar routes (не в
toolbar ViewMode). Persist: session preference + soft LS; клик по вкладке **не** ставит
pin навсегда; при смене mood — soft-default FLOW→overview, MOMENTUM→today,
CONTROL→bookings. Контент: free gaps / события сегодня / upcoming bookings (Front-derive).
Язык UI (`ru`/`en`): до логина — `navigator.language`; после входа — поле `language` профиля (`PATCH /v1/user/me`). Если в профиле язык пуст — при логине записывается текущий (браузерный/выбранный на экране входа). Язык также в `/more` и в форме `/profile`.
### 3.3. Time Arc (живая сетка, Front#50)
На commercial week/day в режиме Студия клиент считает предложенный час без LLM и без Back: свободные слоты студии ∩ занятость пользователя (personal + confirmed bookings) × mood (FLOW — воздух и середина дня, MOMENTUM — ближайший, CONTROL — доля свободных мест). Одна ячейка — `data-time-arc`; коллизии — `data-collided`. Тап — обычный Book.
Владелец: черновики слотов из прошлой ISO-недели в дырах текущей; «Создать неделю» —
preview, затем существующий POST events. После создания черновик снимается сразу
(без F5). Без подтверждения в API не пишем. EventHubAiRouter не используется.
**v2 (Front#51):** отказ от дуги (тап другой свободной ячейки / свайп недели) пишет штраф часу и мастеру в `localStorage` (`eh.timeArc.skips`) — дуга переезжает. ≥2 confirmed записей к одному `specialist_id` бустят его слоты. Месяц: тепло `data-time-arc-heat` на днях со скоренным часом. Personal: черновик «тот же час +7д» после прошедшего confirmed визита; тап открывает студию.
**v3 (дуга между календарями):** `/following` сортирует студии по ближайшему Time Arc
(`start_time`); баннер «Ближайший час» — **min** среди подписок, без дубля `when`
на той же строке списка. `/search`: баннер nearest — другая студия, чем первая
карточка выдачи (если совпадает — следующий hint); Discover остаётся списком
с `data-time-arc-when` на остальных calendar-строках. Тап открывает неделю студии
с курсором на этом дне. Черновики владельца по активной студии. Без LLM / Back AI.
**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.
## 4. Маршруты ## 4. Маршруты
Публичные: Публичные:
- `/login`, `/register`, `/verify` - `/login`, `/register`, `/verify`, `/forgot-password`, `/reset-password`
- `/search`, `/discover` — каталог без сессии
- `/c/:calendarId`, `/c/:calendarId/e/:eventId` — read-only неделя commercial-студии;
запись / follow / «мой календарь» → `/login?next=`
Защищённые (`ProtectedRoute`): Защищённые (`ProtectedRoute`):
- `/` — Calendar Workspace (автовыбор своего календаря или empty-state) - `/default` — resolve единственного personal → `/c/:id` (post-login и tab «Календарь»)
- `/c/:calendarId` — workspace с календарём - `/default/e/:eventId` — то же + карточка события
- `/c/:calendarId/e/:eventId` — workspace + карточка события - `/` — alias → `/default`
- `/search` — поиск календарей/событий - `/bookings` — grouped inbox: (A) к подтверждению (owner/specialist pending) +
- `/bookings` — мои бронирования (deep-link в `/c/.../e/...`) (B) мои записи участника; deep-link в `/c/.../e/...`
- `/calendars` — управление своими календарями (CRUD) - `/calendars` — управление своими календарями (create только Студия; personal delete UI скрыт)
- `/more` — вторичное меню + mood switcher - `/more` — вторичное меню + language switcher (**без** mood); пункт specialist
invites **скрыт**, если входящих pending = 0; при pending > 0 — badge со счётчиком
(deep-link `/invites?token=` работает)
- `/invites` — inbox входящих specialist invites (не смешивать с `/bookings`)
- `/following` — отслеживаемые чужие календари (follow; не платная Подписка) - `/following` — отслеживаемые чужие календари (follow; не платная Подписка)
- `/reviews`, `/subscription`, `/tickets`, `/profile` - `/reviews`, `/subscription`, `/tickets`, `/profile`
Redirects: `/discover``/search`; `/calendars/:id``/c/:id`; `/calendars/:id/events/:eventId``/c/:id/e/:eventId`. Redirects: `/discover``/search`; `/calendars/:id``/c/:id`; `/calendars/:id/events/:eventId``/c/:id/e/:eventId`.
Состояние вида: Zustand `calendarContextStore` (`view`, `cursorDate`, last own `calendarId` в `localStorage`). Состояние вида: Zustand `calendarContextStore` (`view`, `cursorDate`, last own `calendarId` в `localStorage`); lens — `lensStore` + `localStorage` (см. §3.2).
## 5. Модули ## 5. Модули
@@ -110,10 +200,13 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
Регистрация, верификация, вход, восстановление сессии. Регистрация, верификация, вход, восстановление сессии.
### 5.2. Профиль ### 5.2. Профиль
`GET /v1/user/me` + форма на `/profile`. Редактируемые через `PATCH /v1/user/me`: `nickname`, `phone`, `timezone`, `avatar_url`, `language`; смена пароля — `current_password` + `password`. Read-only в UI: `id`, `email`, `role`, `status`. Mood — отдельно в `/more` (`preferences.mood`). Язык при первом визите — из браузера; при первом логине без `language` — пишется в профиль. Mood по умолчанию `calm`, при логине не сидится. `GET /v1/user/me` + форма на `/profile`. Редактируемые через `PATCH /v1/user/me`: `nickname`, `phone`, `timezone`, `avatar_url`, `language`; смена пароля — `current_password` + `password`. Read-only в UI: `id`, `email`, `role`, `status`. Mood — chip в AppShell рядом с BrandWordmark (+ compact на auth); persist `preferences.mood` через `PATCH /v1/user/me` (**не** в `/more`, **не** в calendar toolbar). Язык при первом визите — из браузера; при первом логине без `language` — пишется в профиль. Mood по умолчанию `calm`, при логине не сидится. Lens — только localStorage (не профиль).
### 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 (`.eh-discover-media--photo`); иначе mood wash + initials (`.eh-discover-media--fallback`,
не fake photo-hero). Битый `image_url` → fallback. **Upload API нет** — поле URL (seed / ручная установка).
### 5.4. Календари ### 5.4. Календари
- Список своих: `GET /v1/calendars` - Список своих: `GET /v1/calendars`
@@ -123,29 +216,81 @@ Redirects: `/discover` → `/search`; `/calendars/:id` → `/c/:id`; `/calendars
- Просмотр коммерческого чужого календаря по id; при `booking_open=false` — баннер - Просмотр коммерческого чужого календаря по id; при `booking_open=false` — баннер
«Запись временно недоступна», кнопка записи скрыта «Запись временно недоступна», кнопка записи скрыта
- HTML month view владельца: `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — при переключении виджета на **прошедший месяц** (owner + вид «Месяц») автоматически подменяет клиентский grid серверным HTML (live/архив, задача #15). Текущий и будущие месяцы — React-виджет по `GET …/events`. - HTML month view владельца: `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — при переключении виджета на **прошедший месяц** (owner + вид «Месяц») автоматически подменяет клиентский grid серверным HTML (live/архив, задача #15). Текущий и будущие месяцы — React-виджет по `GET …/events`.
- Создание / апгрейд `commercial` без подписки`402``/subscription`; после оплаты — - Создание / апгрейд `commercial` без **уже active** sub/trial`402``/subscription`
(trial — явный `start_trial`, не auto при create); после оплаты —
возврат к созданию/редактированию возврат к созданию/редактированию
- Владелец commercial: специалисты **через invite** (не сырой `user_id`): - Владелец commercial: команда — owner-строка specialist (создаётся бэком при create
commercial) + остальные **через invite** (не сырой `user_id`):
typeahead `GET /v1/users/lookup` и/или email → `POST …/specialist-invites`; typeahead `GET /v1/users/lookup` и/или email → `POST …/specialist-invites`;
список active + исходящие pending; deactivate/remove принятых. список + исходящие pending; deactivate/remove принятых (**не** owner).
Выбор `specialist_id` в форме события (только `active`). Выбор `specialist_id` в форме события (только `active`).
Баннер «подписка истекла / истекает» при `booking_open=false`. Баннер «подписка истекла / истекает» при `booking_open=false`.
- Invitee: inbox входящих `GET /v1/user/specialist-invites` (More / уведомления) — `SpecialistsPanel` (owner): во вкладке **Команда** rail (desktop) / segmented
(mobile) — **не** внутри About; **owner всегда первым**, бейдж «Владелец»,
галочка «Я специалист» ↔ `PUT status` active|inactive; edit name/specialization;
Delete для owner скрыт. Mobile density — имя/теги сверху, actions снизу
full-width (invite/deactivate не ломать); desktop — вертикальный rail + scroll.
Режимы сетки **Студия** (агрегат `start_time`+`duration`, заливка booked/total,
label `HH:mm · K/N свободно`, popover мастеров) и **Мастер** (фильтр
`specialist_id` + sticky roster как D9 B) — см. §3.1. Месяц в Студии —
density markers. Occupancy ячеек/popover — из API `booking_occupancy`
(`"free"` | `"pending"` | `"confirmed"`, BackSpec events); ★ `0.0` на free
не показывать. Personal — без агрегата / без вкладки Команда.
- Guest browse commercial: roster активных → фильтр расписания по `specialist_id`
(D9 B; см. §3.1); в «Все» — studio-агрегат как у owner; owner panel (invite/CRUD)
не смешивать с guest roster.
- Invitee: inbox входящих `GET /v1/user/specialist-invites` на `/invites`
принять / отклонить; deep-link из email → accept по `token` после логина. принять / отклонить; deep-link из email → accept по `token` после логина.
В More пункт «Приглашения» показывается **только** при `pending > 0` с badge
счётчика (не путать с booking-заявками на `/bookings`).
### 5.5. События ### 5.5. События
- Список: `GET /v1/calendars/:calendar_id/events` - Список: `GET /v1/calendars/:calendar_id/events` — для commercial в JSON события
- CRUD владельца: `POST/PUT/DELETE` (в т.ч. опциональный `specialist_id`) опционально `booking_occupancy` (`"free"` | `"pending"` | `"confirmed"`;
- Детали: `GET /v1/events/:id` (отображение специалиста, если задан) приоритет confirmed > pending > free; см. BackSpec §6). Studio-агрегат /
popover мастеров опираются на это поле (+ capacity/bookings при необходимости).
- CRUD владельца: `POST/PUT/DELETE` (в т.ч. опциональный `specialist_id`).
Create-форма: **capacity = 1** (или `settings.default_capacity` студии);
`specialist_id` подставляется из текущего roster-фильтра сетки; recurrence —
из org defaults календаря. **WeekFill:** горизонт по умолчанию **1 неделя**;
«Создать расписание» disabled, пока нет именованных слотов и выбранного
специалиста; превью считает, сколько событий будет создано; пустой apply —
один error toast, не success.
- Детали: `GET /v1/events/:id` (отображение специалиста, если задан;
тот же `booking_occupancy`)
- Вхождения: `GET /v1/events/:id/occurrences` - Вхождения: `GET /v1/events/:id/occurrences`
- Отмена вхождения: `DELETE /v1/events/:id/occurrences/:start_time` - Отмена вхождения: `DELETE /v1/events/:id/occurrences/:start_time`
### 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` Тело book **без** specialist (назначение — на `event.specialist_id`).
`{ action: confirm | decline }` (specialist — только свои события, см. BackSpec §2.1.2) - Владелец и specialist:
- Статусы: `pending` | `confirmed` | `cancelled`; confirmation: `auto` | `manual` | timeout - на карточке события: `GET /v1/events/:id/bookings` + Confirm/Decline (**обязательно**)
- inbox `/bookings` группа «К подтверждению»: `GET /v1/user/booking-requests`
(actionable pending, где user — owner календаря или `event.specialist_id`; past-pending
Back отдаёт как `expired` и **не** включает в inbox); те же
`PUT /v1/bookings/:id` `{ action: confirm | decline }` (на `expired``409`)
- UI `/bookings`: три группы (пустые скрывать): **К подтверждению** (`booking-requests`) +
**Записи студии** (`GET /v1/user/studio-bookings`, confirmed) + **Мои записи**
(upcoming/past participant). `specialist_invite` сюда **не** попадает.
Empty state: title + hint + CTA Discover / «Мои календари».
Время слота на list-карточках — soft (secondary to title).
- **Confirmed overlay на personal (Front-only, Front#35):** confirmed из
`GET /v1/user/bookings` на чужих commercial — догрузить event/calendar и нарисовать
на **своём default personal** grid; title `событие · студия · специалист`;
клик → `/c/:foreignCalId/e/:eventId`. Back API не менялся.
- **Specialist duty overlay на personal (Front-only, Front#43):** на своём personal —
слоты commercial, где `event.specialist_id` = текущий user. Gate: accepted invites
(`GET /v1/user/specialist-invites`) → unique `calendar_id` → active specialist
(`GET /v1/calendars/:id/specialists`) → list events viewport. Title `событие · студия`;
клик → `/c/:commercialId/e/:eventId`. Back expand virtuals без `specialist_id`
Front мержит masters + expand (`mergeExpandedWithMasters`) перед фильтром.
Legacy specialist без invite-записи — вне MVP. Не зеркалить чужой roster на personal owner.
- Статусы: `pending` | `confirmed` | `cancelled` | `expired`; confirmation: `auto` | `manual` | timeout.
Past+pending / `expired` в «Мои записи» и на `EventActionCard` — бейдж «истекла»,
Cancel скрыт. На past-событии Book скрыт. Defensive: если API ещё отдал past
`pending` — UI мапит в `expired`.
- Pending резервирует capacity (как на бэке) - Pending резервирует capacity (как на бэке)
### 5.7. Отзывы ### 5.7. Отзывы
@@ -160,8 +305,10 @@ UI: форма отзыва скрыта без confirmed booking (event — boo
`GET /v1/subscription`, `POST /v1/subscription` (`start_trial` | `activate` + `plan` + опционально `payment_info`). `GET /v1/subscription`, `POST /v1/subscription` (`start_trial` | `activate` + `plan` + опционально `payment_info`).
Планы и цены (как Back `plan_price/1`, minor units → ₽): monthly **999**, quarterly **2499**, biannual **4499**, annual **7999**. Планы и цены (как Back `plan_price/1`, minor units → ₽): monthly **999**, quarterly **2499**, biannual **4499**, annual **7999**.
UI: карточки сравнения, локализованный «Бесплатно» без подписки, trial, демо-оплата (шлюз-заглушка). UI: карточки сравнения, локализованный «Бесплатно» без подписки, trial, демо-оплата (шлюз-заглушка).
Создание commercial без подписки`402``/subscription`. После renew commercial-календари Create/upgrade commercial без **уже active** sub/trial`402``/subscription`
владельца снова с `booking_open=true` без смены type (BackSpec §2.1.2 restricted → full). (сначала явный `start_trial` или `activate`; auto-start trial при create **нет**,
Back#61 / BackSpec §2.1.2, §2.9). После renew commercial-календари владельца снова
с `booking_open=true` без смены type (restricted → full).
### 5.10. Тикеты ### 5.10. Тикеты
- ErrorBoundary / необработанные ошибки → `POST /v1/tickets` с `source=frontend` - ErrorBoundary / необработанные ошибки → `POST /v1/tickets` с `source=frontend`
+1 -1
View File
@@ -3,7 +3,7 @@
## 2. [EventHub Admin UI — Техническое задание](EventHubFrontAdminSpec.md) ## 2. [EventHub Admin UI — Техническое задание](EventHubFrontAdminSpec.md)
## 3. [EventHub Client UI — Техническое задание](EventHubFrontSpec.md) ## 3. [EventHub Client UI — Техническое задание](EventHubFrontSpec.md)
## 4. [Правила работы (workflow)](WORKFLOW.md) ## 4. [Правила работы (workflow)](WORKFLOW.md)
## 5. [Stage UI в Cursor Browser](STAGE-BROWSER.md) ## 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)
# **Репозитории разработки EventHub** # **Репозитории разработки EventHub**
+45 -23
View File
@@ -1,28 +1,58 @@
# Stage UI в Cursor Browser # Stage UI в Cursor Browser
Chromium / Cursor IDE Browser часто **не резолвят** `*.eventhub.local` (mDNS), даже если Windows/`nslookup` через OpenWRT уже видят stage. Stage уже доступен по **публичному DNS с валидным TLS** (Let's Encrypt). Агенты и ручной просмотр **по умолчанию** ходят **напрямую по HTTPS** — без локального `:8787` proxy.
Для агентов и ручного просмотра stage используем **локальный alias + HTTP-прокси**. Локальный alias-proxy — **только fallback**, если Cursor/Chromium не резолвит hostname stage (не из‑за сертификата).
## URL Публичные URL стенда также зафиксированы в `EventHubDevOps/docs/STANDS.md` (раздел stage).
## Канон (прямые HTTPS)
| URL | Сервис |
|-----|--------|
| `https://stage.calentiq.com/` | client-ui |
| `https://admin.stage.calentiq.com/` | admin-ui |
| `https://api.stage.calentiq.com` | eventhub API |
| `https://ws.stage.calentiq.com` | WebSocket |
| `https://admin-api.stage.calentiq.com` | admin API |
| `https://calentiq.com` | 301 → `stage.calentiq.com` |
Для агентского Cursor Browser обычно достаточно:
- Client: `https://stage.calentiq.com/login`
- Admin: `https://admin.stage.calentiq.com/`
Client/admin SPA ходят в API **same-origin** через nginx (`/v1/`, `/ws`).
Внутренний QA (`*.stage.eventhub.local`) по-прежнему есть; для браузера агента **предпочтительны** публичные `*.calentiq.com` выше.
## Auth / учётки
- Smoke: `~/.cursor/secrets/eventhub-stage.env` (`SMOKE_USER_*`).
- IFT e2e: только `~/.cursor/secrets/eventhub-ift.env` (не подмешивать stage).
- Не коммитить пароли/токены из smoke env.
## Fallback: локальный alias-proxy
Использовать **только** если прямое открытие `https://stage.calentiq.com/` (или admin) в Cursor Browser не работает из‑за **DNS / резолва hostname** (например Chromium и `*.local`). Не поднимать proxy «на всякий случай» и не из‑за TLS — публичный cert валиден.
| Открывать в браузере | Upstream | | Открывать в браузере | Upstream |
|----------------------|----------| |----------------------|----------|
| `http://ui.stage.eventhub.test:8787` | `https://ui.stage.eventhub.local``195.208.119.190` | | `http://ui.stage.eventhub.test:8787` | `https://ui.stage.eventhub.local`stage IP |
## Одноразовая настройка hosts ### Hosts (только для fallback)
В `C:\Windows\System32\drivers\etc\hosts` (или WSL `/etc/hosts` не помогает Cursor на Windows): В `C:\Windows\System32\drivers\etc\hosts` (WSL `/etc/hosts` не помогает Cursor на Windows):
```text ```text
127.0.0.1 ui.stage.eventhub.test 127.0.0.1 ui.stage.eventhub.test
``` ```
## Прокси ### Прокси
Скрипт в репозитории: [`scripts/stage-browser-alias-proxy.mjs`](scripts/stage-browser-alias-proxy.mjs) Скрипт: [`scripts/stage-browser-alias-proxy.mjs`](scripts/stage-browser-alias-proxy.mjs)
Копия для быстрого старта агента: `~/.cursor/eventhub/stage-browser-alias-proxy.mjs` (если есть — можно любой из двух). Копия: `~/.cursor/eventhub/stage-browser-alias-proxy.mjs` (если есть — можно любой).
**Важно:** процесс должен слушать на **Windows** `127.0.0.1:8787` (Node из Windows). Прокси только в WSL Cursor с Windows часто не достучится. **Важно:** процесс должен слушать на **Windows** `127.0.0.1:8787` (Node из Windows). Прокси только в WSL Cursor с Windows часто не достучится.
@@ -32,18 +62,15 @@ Start-Process -FilePath "C:\Program Files\nodejs\node.exe" `
-WindowStyle Hidden -WindowStyle Hidden
``` ```
Или из Spec (если скрипт уже скопирован в `~/.cursor/eventhub/`): Или из корня EventHubSpec:
```powershell ```powershell
# из корня EventHubSpec
Start-Process -FilePath "C:\Program Files\nodejs\node.exe" ` Start-Process -FilePath "C:\Program Files\nodejs\node.exe" `
-ArgumentList (Resolve-Path ".\scripts\stage-browser-alias-proxy.mjs") ` -ArgumentList (Resolve-Path ".\scripts\stage-browser-alias-proxy.mjs") `
-WindowStyle Hidden -WindowStyle Hidden
``` ```
Проверка: открыть `http://ui.stage.eventhub.test:8787/login`. Проверка fallback: `http://ui.stage.eventhub.test:8787/login`.
Переменные окружения (опционально):
| Env | Default | | Env | Default |
|-----|---------| |-----|---------|
@@ -53,16 +80,11 @@ Start-Process -FilePath "C:\Program Files\nodejs\node.exe" `
| `STAGE_UI_HOST` | `ui.stage.eventhub.local` | | `STAGE_UI_HOST` | `ui.stage.eventhub.local` |
| `ALIAS_HOST` | `ui.stage.eventhub.test` | | `ALIAS_HOST` | `ui.stage.eventhub.test` |
## Auth Прокси **не** подставляет пользователя: только Host/SNI rewrite на Traefik.
Same-origin через прокси: `/v1/login`, `/v1/register`, logout — как на настоящем stage.
- Smoke: `~/.cursor/secrets/eventhub-stage.env` (`SMOKE_USER_*`).
- IFT e2e: только `~/.cursor/secrets/eventhub-ift.env` (не подмешивать stage).
- Прокси **не** подставляет пользователя: только Host/SNI rewrite на Traefik.
## Что не делать ## Что не делать
- Не ждать, что Cursor Browser сам пойдёт на `https://ui.stage.eventhub.local`. - Не считать `:8787` / `ui.stage.eventhub.test` каноном — канон это `https://stage.calentiq.com` (и admin).
- Не поднимать прокси только в WSL, если агентский браузер на Windows. - Не поднимать proxy, пока не подтверждён сбой резолва прямого HTTPS.
- Не поднимать proxy только в WSL, если агентский браузер на Windows.
- Не коммитить пароли/токены из smoke env. - Не коммитить пароли/токены из smoke env.
+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.
+26
View File
@@ -72,6 +72,32 @@ Canvas: `eventhub-front-ux-review` → `eventhub-front-ux-rewalk` → **`eventhu
- [x] Booking: sheet swipe-dismiss, `/bookings` upcoming/past, post-book CTA ([EventHubFront#23](https://git.sabilin.com/EventHub/EventHubFront/issues/23)) - [x] Booking: sheet swipe-dismiss, `/bookings` upcoming/past, post-book CTA ([EventHubFront#23](https://git.sabilin.com/EventHub/EventHubFront/issues/23))
- [x] Stage verify: swipe Playwright (Pixel 5), prune on host, smoke-owned litter по префиксу - [x] Stage verify: swipe Playwright (Pixel 5), prune on host, smoke-owned litter по префиксу
## P2 stage journey (Front#35, Jul 2026) — shipped
- [x] SpecialistsPanel mobile density (имя/теги сверху, actions снизу full-width)
- [x] F6: EventActionCard past+pending → expired, без Cancel; Book скрыт на past
- [x] D9 модель B: guest roster → фильтр слотов по `specialist_id`; «Все слоты студии»
- [x] Confirmed foreign bookings overlay на default personal (Front-only)
Трекер: [EventHubFront#35](https://git.sabilin.com/EventHub/EventHubFront/issues/35).
## Mood + Lens workspace (Front#39, Jul 2026) — shipped
- [x] Mood chip убран из Ещё; 3 moods, без flip/CREATOR (placement → AppShell в Front#40)
- [x] Lens Обзор / Сегодня / Записи отдельно от mood и view modes
- [x] Strip над сеткой (Front-derive); mobile collapsed; desktop expanded
- [x] Persist: mood → `preferences.mood`; language → `user.language`; lens → localStorage
Трекер: [EventHubFront#39](https://git.sabilin.com/EventHub/EventHubFront/issues/39) (`c2cb754` / `d38a927`).
## Mood AppShell + week view (Front#40, Jul 2026) — shipped
- [x] Mood chip в AppShell рядом с BrandWordmark (**не** в toolbar Month/Week/Day)
- [x] 3 moods; `PATCH preferences.mood`; без дубля в Ещё
- [x] Week view: одна строка day-headers; scroll `.eh-cal-week-scroll` (mobile ~390)
Трекер: [EventHubFront#40](https://git.sabilin.com/EventHub/EventHubFront/issues/40) (mood `7c092d1`, week `db11c41`).
## Future (не блокер 10) ## Future (не блокер 10)
- [x] Swipe month/week + long-press create в календаре ([EventHubFront#24](https://git.sabilin.com/EventHub/EventHubFront/issues/24)) - [x] Swipe month/week + long-press create в календаре ([EventHubFront#24](https://git.sabilin.com/EventHub/EventHubFront/issues/24))
+1 -1
View File
@@ -1 +1 @@
0.1 0.2
+36 -9
View File
@@ -73,7 +73,7 @@ PowerShell в Cursor **ломает экранирование** (кавычки
| Windows `npm` / `erl` / `rebar3` | npm и rebar **только в WSL** | | Windows `npm` / `erl` / `rebar3` | npm и rebar **только в WSL** |
| Push Front без локальной проверки | WSL: `npm run lint && npm run build && npm run test:e2e:ift`, потом push | | Push Front без локальной проверки | WSL: `npm run lint && npm run build && npm run test:e2e:ift`, потом push |
Канон для агентов: `EventHubBack/.cursor/rules/shell-wsl-not-powershell.mdc`. Канон для агентов: `EventHubBack/.cursor/rules/shell-wsl-not-powershell.mdc`, **`agent-pitfalls.mdc`** (типовые сбои shell/git/docker).
| Tool | Version | Path (WSL) | | Tool | Version | Path (WSL) |
|------|---------|------------| |------|---------|------------|
@@ -94,6 +94,19 @@ rebar3 ct --suite=api_admins_SUITE
Agent: `~/.cursor/eventhub/compile-back.sh`, `run-api-tests.sh`; см. `~/.cursor/eventhub/OTP-REBAR.md`. Agent: `~/.cursor/eventhub/compile-back.sh`, `run-api-tests.sh`; см. `~/.cursor/eventhub/OTP-REBAR.md`.
### Экономия токенов (Agent)
| Принцип | Как |
|---------|-----|
| Один чат — одна задача | Новый issue → новый чат (§5); после «готово» не продолжать тред |
| Узкий scope в запросе | «Репо, issue, только X, не Y, done = …» |
| Меньше tool turns | `.sh` с первого раза; не PS retry; targeted eunit/lint |
| Subagents редко | Узкий промпт, один репо, без копии всей переписки |
| Режим | Ask/Plan для обсуждения; Agent для кода и shell |
| Повторяемое | Скрипты в `~/.cursor/eventhub/`, не заново в чате |
Канон: `EventHubBack/.cursor/rules/agent-pitfalls.mdc` §6.
### Git-коммиты из WSL (Agent и разработчик) ### Git-коммиты из WSL (Agent и разработчик)
Коммиты из **Agent Shell / PowerShell** ломаются: Cursor подставляет `--trailer "Co-authored-by: Cursor <cursoragent@cursor.com>"`, а PowerShell падает на `<`. Коммиты из **Agent Shell / PowerShell** ломаются: Cursor подставляет `--trailer "Co-authored-by: Cursor <cursoragent@cursor.com>"`, а PowerShell падает на `<`.
@@ -217,7 +230,7 @@ Issue: https://git.sabilin.com/EventHub/EventHubBack/issues/24
### Product version (`EventHubSpec/VERSION`) ### Product version (`EventHubSpec/VERSION`)
Единый источник правды: **`EventHubSpec/VERSION`** — формат **`MAJOR.MINOR`** (сейчас `0.1`). Единый источник правды: **`EventHubSpec/VERSION`** — формат **`MAJOR.MINOR`** (сейчас `0.2`).
- Бампается **вручную и редко** (смена продуктовой линейки). - Бампается **вручную и редко** (смена продуктовой линейки).
- В `EventHubBack` / `EventHubFrontAdmin` / `EventHubFront` файл `VERSION` — только **fallback** для локальных сборок без доступа к Spec. - В `EventHubBack` / `EventHubFrontAdmin` / `EventHubFront` файл `VERSION` — только **fallback** для локальных сборок без доступа к Spec.
@@ -240,7 +253,7 @@ Issue: https://git.sabilin.com/EventHub/EventHubBack/issues/24
{ {
"status": "ok", "status": "ok",
"service": "eventhub", "service": "eventhub",
"version": "0.1", "version": "0.2",
"build": 338, "build": 338,
"git_sha": "2b65a804d354", "git_sha": "2b65a804d354",
"built_at": "2026-07-17T18:00:00Z" "built_at": "2026-07-17T18:00:00Z"
@@ -253,8 +266,8 @@ Issue: https://git.sabilin.com/EventHub/EventHubBack/issues/24
Отображение: Отображение:
- Admin: `Admin UI 0.1.341 (eb37476)` / `API 0.1.338 (2b65a804d354)` - Admin: `Admin UI 0.2.{build} (sha)` / `API 0.2.{build} (sha)`
- Client: `UI 0.1.{build} (sha)` / `API 0.1.{build} (sha)` - Client: `UI 0.2.{build} (sha)` / `API 0.2.{build} (sha)`
Третье число в лейбле — **build**, не semver-patch релиза. Третье число в лейбле — **build**, не semver-patch релиза.
@@ -275,11 +288,12 @@ IFT и stage крутят **одну и ту же** сборку `sha-*`. Produc
### Stage UI в Cursor Browser ### Stage UI в Cursor Browser
`*.eventhub.local` часто не резолвится в Chromium/Cursor IDE Browser (mDNS). Для ручного и агентского просмотра stage: Stage: публичный DNS + валидный TLS. **Канон — прямой HTTPS** (не локальный `:8787`):
- URL: `http://ui.stage.eventhub.test:8787` - Client: `https://stage.calentiq.com/`
- Инструкция + proxy: **`STAGE-BROWSER.md`**, скрипт `scripts/stage-browser-alias-proxy.mjs` (Windows Node на `:8787`) - Admin: `https://admin.stage.calentiq.com/`
- Hosts: `127.0.0.1 ui.stage.eventhub.test` - Инструкция: **`STAGE-BROWSER.md`**
- Proxy `ui.stage.eventhub.test:8787`**только fallback** при сбое резолва hostname в Cursor/Chromium
- Учётки smoke: `~/.cursor/secrets/eventhub-stage.env` (не коммитить) - Учётки smoke: `~/.cursor/secrets/eventhub-stage.env` (не коммитить)
--- ---
@@ -361,6 +375,9 @@ error rate > 1% **или** mean request > 500 ms (hold ≥2 мин в фазе)
- После успешных тестов — обновить спеку, если менялся внешний контракт. - После успешных тестов — обновить спеку, если менялся внешний контракт.
- Одна задача — один логический объём работы. - Одна задача — один логический объём работы.
- Код фичи — в репозитории-владельце; `EventHubSpec` — для документации и процесса. - Код фичи — в репозитории-владельце; `EventHubSpec` — для документации и процесса.
- **Один агент — один репо** (не смешивать Front/Back/Spec/FrontAdmin в одной сессии): `.cursor/rules/one-repo-per-agent.mdc`.
- **Типовые ошибки агентов** (PS, git clean, docker scale, scope): `EventHubBack/.cursor/rules/agent-pitfalls.mdc`; в промпт субагента включать shell + pitfalls.
- **Экономия токенов:** один чат — одна задача; узкий scope; subagents редко; Ask vs Agent — см. `agent-pitfalls.mdc` §6 и §2.1 выше.
- Метки во всех репозиториях держать одинаковыми (`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).
@@ -376,6 +393,16 @@ error rate > 1% **или** mean request > 500 ms (hold ≥2 мин в фазе)
| Дата | Изменение | | Дата | Изменение |
|------|-----------| |------|-----------|
| 2026-07-29 | Spec sync Front#43: specialist duty overlay на personal (accepted invites + active check) |
| 2026-07-28 | Spec sync Front#40: Mood chip в AppShell у BrandWordmark; week single day-header + `.eh-cal-week-scroll` |
| 2026-07-28 | Spec sync Front#39: Mood chip в workspace (не Ещё); Lens Обзор/Сегодня/Записи + strip; mood→DB, lens→localStorage |
| 2026-07-28 | Spec sync Front#35: D9 B guest specialist schedule; confirmed overlay на personal; SpecialistsPanel density; F6 EventActionCard |
| 2026-07-27 | Spec sync Back#61: `specialist_id` на POST event; commercial без active sub → 402; trial только `start_trial` |
| 2026-07-27 | `agent-pitfalls.mdc` §6: экономия токенов (чат, scope, subagents, режимы) |
| 2026-07-27 | `agent-pitfalls.mdc`: типовые сбои PS/git/docker/scope; промпт субагентам |
| 2026-07-27 | Spec sync: booking `expired` (Back#60) + Front#31 UX (desktop chrome, empty bookings, agenda copy, Discover covers, nickname, invites badge) |
| 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-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 |
| 2026-07-15 | §7: анализ через EventHub `/metrics` + admin `nodes/metrics`; compose Prom опционален | | 2026-07-15 | §7: анализ через EventHub `/metrics` + admin `nodes/metrics`; compose Prom опционален |
+8
View File
@@ -0,0 +1,8 @@
# Design artifacts
| Папка | Назначение |
|-------|------------|
| [`front/`](front/) | Клиентский EventHubFront / бренд CalenTIQ |
| [`admin/`](admin/) | EventHubFrontAdmin (позже) |
См. также [`BRANDING.md`](../BRANDING.md).
+5
View File
@@ -0,0 +1,5 @@
# EventHubFrontAdmin — дизайн
Зарезервировано под утверждённые мокапы / борды админки.
Пока пусто: клиентский фронт — в [`../front/`](../front/).
+55
View File
@@ -0,0 +1,55 @@
# CalenTIQ / EventHubFront — дизайн (SoT)
Канонические артефакты клиентского фронта (EventHubFront). Публичный бренд: **CalenTIQ**.
Правила и решения: [`BRANDING.md`](../../BRANDING.md).
План visual parity (mood-first): [`UI-PARITY.md`](UI-PARITY.md).
Админские мокапы — отдельно в [`../admin/`](../admin/).
## Moods
| API key | UI-лейбл | Акцент |
|---------|----------|--------|
| `calm` | **FLOW** | sage `#ABC5B5` / primary `#2F7A68` |
| `energetic` | **MOMENTUM** | coral `#FF6A3D` на ink `#111820` |
| `business` | **CONTROL** | steel `#324A5F` |
Токены: [`tokens/colors.json`](tokens/colors.json), [`tokens/typography.json`](tokens/typography.json) (Syne / Manrope).
## IA (ограничения)
- Top nav (desktop) + bottom nav (mobile)
- **Без left sidebar**
- Без 4-го Creator mood
- Внутренние репо (`EventHub*`) не переименовываем
## Структура
### `boards/` — UI-борды
| Файл | Что это |
|------|---------|
| `calentiq-ui-board-final.png` | Утверждённый UI-борд направления CalenTIQ (Time Arc, moods) |
| `calentiq-icon-system-final.png` | Утверждённый борда иконок / app icons по moods |
| `eventhub-stage-ui-as-is.png` | As-is stage UI (референс текущего продукта) |
| `eventhub-discover-*.png` | As-is Discover по moods (flow / momentum / control) + as-is |
### `logo/` — Time Arc
- `mark.svg` (+ `mark-black.svg`, `mark-white.svg`) — master mark
- `lockup-horizontal.svg` (+ black/white) — wordmark lockup
- `mark-1024.png`, `lockup-horizontal.png` — растровые превью
### `mood-icons/`
- `appicon-{flow,momentum,control}.svg` — master mood icons
- `appicon-*-1024.png` — растры для сторов / превью
## Runtime-копии
В приложении те же векторы живут в `EventHubFront/public/brand/`.
Эта папка Spec — **дизайн SoT** для согласования; runtime Front может отставать или опережать до синка.
## Источник
Собрано из утверждённого local brand pack (`calentiq-brand-pack`) и Desktop as-is скринов.
ZIP-архивы и GPT-stub дубликаты в Spec не кладём.
+261
View File
@@ -0,0 +1,261 @@
# UI parity: EventHubFront ↔ CalenTIQ board
План визуального выравнивания клиентского фронта с утверждённым бордом.
SoT: [`boards/calentiq-ui-board-final.png`](boards/calentiq-ui-board-final.png).
Эпик: [EventHubSpec#10](https://git.sabilin.com/EventHub/EventHubSpec/issues/10).
**Статус:** workstreams 15 закрыты в коде Front (Refs Front#25#29). Stage smoke на `https://stage.calentiq.com`**done** (UI `0.2.29` / `79dadfb21f98`, API `0.2.158`).
---
## Known deviations from board (Front#29)
Зафиксировано осознанно / пока не делаем (после visual pass ×3 moods + stage):
| Зона | Борд | Сейчас |
|------|------|--------|
| 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 |
| 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` = **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 |
| 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 «Нет слотов» |
| More invites | пункт меню | скрыт при pending=0; badge счётчика при pending > 0 |
| D9 guest specialist schedule | — | **модель B (Front#35):** roster карточек → тап → фильтр слотов по `event.specialist_id`; «Все слоты студии»; book без specialist в body; **не** chip-filter |
| Confirmed на personal | — | Front-only overlay confirmed чужих commercial на default personal; title `событие · студия · спец`; клик → `/c/:foreignCalId/e/:eventId` |
| SpecialistsPanel density | — | mobile: имя/теги сверху, actions снизу full-width (Front#33/#35) |
| 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) |
IA без sidebar / без Creator mood — совпадает с non-goals (проверено на stage).
### Visual pass ×3 moods (local + stage)
**Checked (vite preview + Playwright shots + stage HTTPS):** Login §D, Calendar grid+agenda, Discover media, CONTROL slot/bookings (Confirm/Decline на event card), More mood cards — FLOW / MOMENTUM / CONTROL.
**Polish in this pass:** agenda copy/casing/counts + time-first rows; CONTROL steel rail emphasis; neutral Discover cover in mock (не flow-appicon во всех moods).
**Front#31 (2026-07-27):** desktop one-row chrome (logo|nav|logout); header nickname; bookings `expired` + soft list time + empty CTA; agenda personal vs commercial empty copy; Discover cover photo/fallback; More invites badge.
**Front#35 (2026-07-28, SHA `57cfcf3`):** D9 модель B (guest roster → schedule filter); confirmed foreign overlay на personal; SpecialistsPanel mobile density; F6 EventActionCard past+pending → expired / no Cancel, Book скрыт на past.
**Front#39 (2026-07-28, SHA `c2cb754` / `d38a927`):** Mood chip убран из Ещё; Lens Обзор/Сегодня/Записи + strip над сеткой во всех 3 moods; mobile sheets + strip collapsed; mood→DB, lens→localStorage; без flip / CREATOR. (Placement chip уточнён в Front#40.) **Controls IA (2026-07-29):** lens перенесён в шапку strip (labels: Обзор / На сегодня / Заявки); view рядом с периодом; owner schedule actions отдельным рядом.
**Front#40 (2026-07-28, SHA mood `7c092d1` / week `db11c41`):** Mood chip в AppShell рядом с BrandWordmark (**не** в calendar toolbar Month/Week/Day); 3 moods; `PATCH preferences.mood`; без дубля в Ещё. Week view: одна строка day-headers + `.eh-cal-week-scroll` для mobile ~390.
**Front#45 (2026-07-29):** Mobile mood chrome — chip icon-only (без truncate label); полный BrandWordmark CalenTIQ на mobile; sheet icon+label+tagline; `aria-label` на chip. Switcher glyphs по board §C (sprout/bolt/briefcase), не Time Arc appicon.
**Remaining intentional:** no pixel-perfect phone chrome; Discover без photo-hero grid (cover via `image_url` when API returns it; broken URL → wash+initials); §E specialist schedule card vs panel — не pixel-perfect board mocks (follow-up). Product flow D9 B — documented; board pixel mock — Future.
### 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** входящих; при
pending > 0 — **badge** со счётчиком. Deep-link `/invites?token=` работает. Исходящие
invite остаются в `SpecialistsPanel`.
4. **Не** класть строки `specialist_invite` в `/bookings`.
5. **Past-pending / `expired`:** Back#60 помечает past-pending → `expired` и исключает из
`booking-requests`; Confirm/Decline → `409`. UI: бейдж «истекла», Cancel скрыт
(Bookings + EventActionCard); defensive map past+pending → expired badge.
Front#35 F6: на past Book скрыт; past+pending на карточке → expired label.
API: `GET /v1/user/booking-requests` (Back) + существующий `PUT /v1/bookings/:id`
(`409` на expired / full).
### Guest specialist schedule + confirmed overlay (locked Front#35)
1. **D9 модель B (guest на commercial):** roster активных специалистов (карточки) →
тап → сетка только со слотами выбранного `event.specialist_id`; CTA «Все слоты
студии» сбрасывает фильтр. **Не** chip-filter на shared grid. Book body без
specialist. Owner `SpecialistsPanel` (invite/CRUD) не менять этим флоу.
2. **Confirmed на personal (Front-only):** `GET /v1/user/bookings` (confirmed) +
догрузка events/calendars → overlay на **default personal** grid; title
`event · studio · specialist`; клик → `/c/:foreignCalId/e/:eventId`. Back без
изменений.
3. **SpecialistsPanel density (mobile):** имя/теги сверху, actions снизу full-width.
### Mood + Lens (locked Front#39 / placement Front#40)
1. **Mood** = визуальная атмосфера (`html[data-mood]`, FLOW/MOMENTUM/CONTROL). Primary UX —
chip → sheet в **AppShell** рядом с BrandWordmark (Front#40); compact на auth.
**Нет** в `/more`; **не** в toolbar Month/Week/Day. Persist: `preferences.mood`
через `PATCH /v1/user/me`. Без flip; без CREATOR. Front#45: mobile chip icon-only;
полный 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.
5. **Week view (Front#40):** одна строка day-headers (без дубля `WeekDayColumn`);
горизонтальный скролл `.eh-cal-week-scroll` (mobile ~390).
### Follow-ups (вне этого fix-pass или частично)
| Тема | Статус |
|------|--------|
| Discover `image_url` в search serialization | Back отдаёт поле; upload API **нет** — только URL field / seed; Front#31: photo vs fallback media classes + onError fallback |
| 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; **product D9 B** — done Front#35 |
| Desktop one-row chrome | **done** Front#31; mood chip у logo — Front#40 (`7c092d1`) |
| 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`) |
## Mood concept = product surface
Mood — не переименование `calm|energetic|business` и не смена primary hex.
Каждый mood — **отдельная атмосфера продукта** при той же IA:
| Mood | Ощущение | Chrome / surfaces | Discover / booking feel |
|------|----------|-------------------|-------------------------|
| **FLOW** (`calm`) | спокойствие, баланс | soft sage panels, light wash, soft chips | мягкие карточки, спокойный CTA |
| **MOMENTUM** (`energetic`) | энергия, скорость | ink `#111820`, coral `#FF6A3D`, denser chips | тёмный discover, яркий CTA |
| **CONTROL** (`business`) | бизнес, доступность | steel `#324A5F`, cool neutrals | коммерческие карточки слот/специалист/CTA |
Референс на борде: секции **B Mood Overview**, **C Mood Switcher**, **D Core Screens**, **NEW Discover**, **E Booking / Commercial**.
Switcher — **branded cards** (иконка + короткий tagline), не plain list/tabs:
- FLOW — *Find balance*
- MOMENTUM — *Get more done*
- CONTROL — *Grow the business*
Опционально: mood-aware favicon из [`boards/calentiq-icon-system-final.png`](boards/calentiq-icon-system-final.png) / `mood-icons/`.
Токены сейчас узкие ([`tokens/colors.json`](tokens/colors.json)); для parity нужны surfaces, gradients, chip/event styles, chrome — не только primary.
---
## Workstreams (mood-first)
Порядок важен: сначала атмосфера и токены, затем экраны поверх них.
### 1. Mood system / atmosphere — **P0 / first** ✅
CSS-токены и chrome на mood beyond primary hex: surfaces, gradients, event chip styles; switcher как branded cards (icon + tagline); опционально mood favicon.
**Acceptance**
- [x] Per-mood token set: surface / wash / chip / border / CTA (не только primary)
- [x] Mood switcher = 3 branded cards с иконкой и tagline с борда
- [x] Смена mood меняет атмосферу chrome (не только accent button)
- [x] API keys остаются `calm|energetic|business`
- [x] Опционально: favicon следует активному mood
**Design:** board §B, §C, §F; `mood-icons/`; `tokens/`
**Done:** EventHubFront#25`index.css` mood tokens + `MoodSwitcher` cards + favicon via `applyMood`.
### 2. Calendar mood-chrome — **P0** ✅
Та же сетка/IA; soft panels (FLOW), denser chips (MOMENTUM), mood-tinted cells (CONTROL) — как на борде §B / §D Calendar.
**Acceptance**
- [x] Month/week выглядят различно по mood при той же навигации
- [x] Event chips / cells тонированы mood-токенами
- [x] Нет sidebar / смены layout ради «красоты»
**Design:** board §B Mood Overview, §D Calendar
**Done:** EventHubFront#26`--cal-*` tokens + `.eh-cal` / `.eh-chip` в `CalendarWidget`; IA без изменений.
**Follow-up:** month grid §B/D — FLOW sage cell wash + chip limit 2; MOMENTUM denser/hotter chips limit 5; CONTROL steel cells + sharper chips limit 3; split time/title chips.
### 3. Discover mood variants — **P1** ✅
Визуальная дифференциация FLOW vs MOMENTUM vs CONTROL: soft cards + CTA; соответствие борду (NEW Discover / §D Discover), не as-is скринам.
**Acceptance**
- [x] Discover в трёх moods визуально различим (фон, карточки, CTA)
- [x] Soft card + CTA pattern с борда (не плоский as-is list-only)
- [x] IA и фильтры без смены модели; только presentation
**Design:** board NEW Discover, §D Discover; as-is refs: `eventhub-discover-{flow,momentum,control}.png`
**Done:** EventHubFront#27`--disc-*` + `.eh-discover-card` / `.eh-discover-cta`; list → card grid.
**Follow-up:** media hierarchy — `.eh-discover-media` with per-mood size/radius/washes (FLOW air / MOMENTUM energy / CONTROL steel), optional cover via `image_url`, display title + soft-card rhythm.
### 4. CONTROL booking surfaces — **P1** ✅
Коммерческие карточки: слот, специалист, CTA — только Control-specific (§E).
**Acceptance**
- [x] Slot card + specialist card + primary CTA в CONTROL
- [x] Specialists остаются внутри Control (не 4-й mood)
- [x] FLOW/MOMENTUM не получают Control commercial chrome
**Design:** board §E Booking / Commercial
**Done:** EventHubFront#28`.eh-book-slot` / `.eh-book-specialist` / `.eh-book-request`; steel chrome только под `data-mood=business`.
**Follow-up:** §E polish — `--book-*` tokens; CONTROL steel left-rail + sharper radii; slot duration/available label; specialist/request avatars; mood-token CTA/Decline.
### 5. Polish + stage smoke — **P2** ✅
Мелкие расхождения с бордом, wordmark/lockup consistency, stage smoke трёх moods.
**Acceptance**
- [x] Mock smoke: calendar → discover → bookings в 3 moods (+ IA без sidebar)
- [x] Нет регрессий IA (top + bottom nav) — покрыто mock e2e
- [x] Известные отклонения от борда зафиксированы в UI-PARITY (Known deviations)
- [x] Visual pass ×3 moods (local preview / Playwright shots) — Login / Calendar / Discover / CONTROL booking
- [x] Stage smoke на живом стенде — UI `0.2.29` (`79dadfb21f98`) @ `https://stage.calentiq.com` (2026-07-27)
**Done:** EventHubFront#29`e2e/tests/mood-parity-smoke.spec.ts`, brand meta, CONTROL tagline polish.
**Follow-up:** visual pass polish — agenda Предстоящие + counts/time-first; CONTROL steel rail; mock Discover cover neutral. Stage: seed commercial + slots + pending booking для §E Confirm/Decline.
## Non-goals
- Left sidebar / смена IA
- 4-й Creator mood / AI-assistant home
- Rename API keys (`calm|energetic|business` остаются)
- Покупка доменов / DNS cutover
- Админский UI (`design/admin/`) — не трогать Front-бордами
- Полный pixel-perfect каждого пикселя борда (направление + mood atmosphere)
---
## Mapping → issues
| # | Workstream | Issue |
|---|------------|-------|
| — | Story (родитель) | [Spec#11](https://git.sabilin.com/EventHub/EventHubSpec/issues/11) |
| 1 | Mood system / atmosphere | [Front#25](https://git.sabilin.com/EventHub/EventHubFront/issues/25) |
| 2 | Calendar mood-chrome | [Front#26](https://git.sabilin.com/EventHub/EventHubFront/issues/26) |
| 3 | Discover mood variants | [Front#27](https://git.sabilin.com/EventHub/EventHubFront/issues/27) |
| 4 | CONTROL booking surfaces | [Front#28](https://git.sabilin.com/EventHub/EventHubFront/issues/28) |
| 5 | Polish + stage smoke | [Front#29](https://git.sabilin.com/EventHub/EventHubFront/issues/29) |
| — | P1/P2 UX polish + expired + desktop chrome | [Front#31](https://git.sabilin.com/EventHub/EventHubFront/issues/31) |
| — | Past-pending → `expired` | [Back#60](https://git.sabilin.com/EventHub/EventHubBack/issues/60) |
| — | P2: D9 B + confirmed personal + density/F6 | [Front#35](https://git.sabilin.com/EventHub/EventHubFront/issues/35) |
| — | Mood chip + Lens strip (workspace) | [Front#39](https://git.sabilin.com/EventHub/EventHubFront/issues/39) |
| — | Mood chip → AppShell + week scroll | [Front#40](https://git.sabilin.com/EventHub/EventHubFront/issues/40) |
| — | Mood chip icon-only mobile + mark-only logo | [Front#45](https://git.sabilin.com/EventHub/EventHubFront/issues/45) |
Эпик: [Spec#10](https://git.sabilin.com/EventHub/EventHubSpec/issues/10).
---
## Suggested start
Кодить с **Task 1 (Mood system / atmosphere)**: без токенов и switcher-as-cards остальные экраны будут «перекраской primary», а не mood product surface.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 420" fill="none">
<g transform="translate(20,8) scale(0.38)">
<g stroke="#14233A" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#14233A"/>
<circle cx="816.06" cy="207.94" r="18" fill="#14233A"/>
<circle cx="884.39" cy="297.00" r="18" fill="#14233A"/>
<circle cx="927.35" cy="400.71" r="18" fill="#14233A"/>
<circle cx="942.00" cy="512.00" r="18" fill="#14233A"/>
<circle cx="927.35" cy="623.29" r="18" fill="#14233A"/>
<circle cx="884.39" cy="727.00" r="18" fill="#14233A"/>
<circle cx="816.06" cy="816.06" r="18" fill="#14233A"/>
</g>
<text x="430" y="270" font-family="Syne, Montserrat, Arial Black, sans-serif" font-size="168" font-weight="800" fill="#14233A">Calen<tspan fill="#4C8E7A">TIQ</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 420" fill="none">
<g transform="translate(20,8) scale(0.38)">
<g stroke="#FFFFFF" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#FFFFFF"/>
<circle cx="816.06" cy="207.94" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="297.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="400.71" r="18" fill="#FFFFFF"/>
<circle cx="942.00" cy="512.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="623.29" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="727.00" r="18" fill="#FFFFFF"/>
<circle cx="816.06" cy="816.06" r="18" fill="#FFFFFF"/>
</g>
<text x="430" y="270" font-family="Syne, Montserrat, Arial Black, sans-serif" font-size="168" font-weight="800" fill="#FFFFFF">Calen<tspan fill="#ABC5B5">TIQ</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+18
View File
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 420" fill="none">
<g transform="translate(20,8) scale(0.38)">
<g stroke="#14233A" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#14233A"/>
<circle cx="816.06" cy="207.94" r="18" fill="#14233A"/>
<circle cx="884.39" cy="297.00" r="18" fill="#14233A"/>
<circle cx="927.35" cy="400.71" r="18" fill="#14233A"/>
<circle cx="942.00" cy="512.00" r="18" fill="#14233A"/>
<circle cx="927.35" cy="623.29" r="18" fill="#14233A"/>
<circle cx="884.39" cy="727.00" r="18" fill="#14233A"/>
<circle cx="816.06" cy="816.06" r="18" fill="#14233A"/>
</g>
<text x="430" y="270" font-family="Syne, Montserrat, Arial Black, sans-serif" font-size="168" font-weight="800" fill="#14233A">Calen<tspan fill="#4C8E7A">TIQ</tspan></text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<g stroke="#000000" stroke-width="48" stroke-linecap="round" stroke-linejoin="round">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#000000"/>
<circle cx="816.06" cy="207.94" r="18" fill="#000000"/>
<circle cx="884.39" cy="297.00" r="18" fill="#000000"/>
<circle cx="927.35" cy="400.71" r="18" fill="#000000"/>
<circle cx="942.00" cy="512.00" r="18" fill="#000000"/>
<circle cx="927.35" cy="623.29" r="18" fill="#000000"/>
<circle cx="884.39" cy="727.00" r="18" fill="#000000"/>
<circle cx="816.06" cy="816.06" r="18" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<g stroke="#FFFFFF" stroke-width="48" stroke-linecap="round" stroke-linejoin="round">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#FFFFFF"/>
<circle cx="816.06" cy="207.94" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="297.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="400.71" r="18" fill="#FFFFFF"/>
<circle cx="942.00" cy="512.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="623.29" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="727.00" r="18" fill="#FFFFFF"/>
<circle cx="816.06" cy="816.06" r="18" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<g stroke="#000000" stroke-width="48" stroke-linecap="round" stroke-linejoin="round">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#000000"/>
<circle cx="816.06" cy="207.94" r="18" fill="#000000"/>
<circle cx="884.39" cy="297.00" r="18" fill="#000000"/>
<circle cx="927.35" cy="400.71" r="18" fill="#000000"/>
<circle cx="942.00" cy="512.00" r="18" fill="#000000"/>
<circle cx="927.35" cy="623.29" r="18" fill="#000000"/>
<circle cx="884.39" cy="727.00" r="18" fill="#000000"/>
<circle cx="816.06" cy="816.06" r="18" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" fill="none">
<rect width="1024" height="1024" rx="228" fill="#324A5F"/>
<g stroke="#FFFFFF" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#FFFFFF"/>
<circle cx="816.06" cy="207.94" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="297.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="400.71" r="18" fill="#FFFFFF"/>
<circle cx="942.00" cy="512.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="623.29" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="727.00" r="18" fill="#FFFFFF"/>
<circle cx="816.06" cy="816.06" r="18" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" fill="none">
<rect width="1024" height="1024" rx="228" fill="#ABC5B5"/>
<g stroke="#FFFFFF" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#FFFFFF"/>
<circle cx="816.06" cy="207.94" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="297.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="400.71" r="18" fill="#FFFFFF"/>
<circle cx="942.00" cy="512.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="623.29" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="727.00" r="18" fill="#FFFFFF"/>
<circle cx="816.06" cy="816.06" r="18" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" fill="none">
<rect width="1024" height="1024" rx="228" fill="#111820"/>
<g stroke="#FFFFFF" stroke-width="48" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#FFFFFF"/>
<circle cx="816.06" cy="207.94" r="18" fill="#FF6A3D"/>
<circle cx="884.39" cy="297.00" r="18" fill="#FF6A3D"/>
<circle cx="927.35" cy="400.71" r="18" fill="#FF6A3D"/>
<circle cx="942.00" cy="512.00" r="18" fill="#FF6A3D"/>
<circle cx="927.35" cy="623.29" r="18" fill="#FF6A3D"/>
<circle cx="884.39" cy="727.00" r="18" fill="#FF6A3D"/>
<circle cx="816.06" cy="816.06" r="18" fill="#FF6A3D"/>
</svg>

After

Width:  |  Height:  |  Size: 865 B

+5
View File
@@ -0,0 +1,5 @@
{
"FLOW": { "appIcon": "#ABC5B5", "primary": "#2F7A68", "accent": "#ABC5B5", "foreground": "#0F2A24" },
"MOMENTUM": { "appIcon": "#111820", "primary": "#FF6A3D", "accent": "#FF6A3D", "foreground": "#F5F1EA" },
"CONTROL": { "appIcon": "#324A5F", "primary": "#324A5F", "accent": "#4A6B82", "foreground": "#14233A" }
}
+1
View File
@@ -0,0 +1 @@
{ "display": "Syne", "ui": "Manrope" }
+10 -4
View File
@@ -1,10 +1,16 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Cursor-browser alias for stage UI. * Optional DNS fallback for Cursor browser → internal stage UI.
* *
* Why: Chromium treats *.local as mDNS and often ignores unicast DNS / hosts. * Canonical access is direct HTTPS (valid public DNS + TLS):
* This proxy serves http://ui.stage.eventhub.test:8787 → https://stage * https://stage.calentiq.com/ (client)
* with Host/SNI ui.stage.eventhub.local (Traefik + existing cert). * https://admin.stage.calentiq.com/
*
* Use this proxy ONLY when Cursor/Chromium cannot resolve the stage hostname
* (e.g. *.local mDNS quirks) — not because of certificates.
*
* Serves http://ui.stage.eventhub.test:8787 → https://ui.stage.eventhub.local
* with Host/SNI rewrite for Traefik.
* *
* Usage: * Usage:
* node stage-browser-alias-proxy.mjs * node stage-browser-alias-proxy.mjs