Compare commits
45 Commits
mvp-0.1
...
d451753be2
| Author | SHA1 | Date | |
|---|---|---|---|
| d451753be2 | |||
| d7c5ae3ac4 | |||
| 1bed282cfb | |||
| 5fa00a44bf | |||
| 80e10caf3d | |||
| e87236b981 | |||
| 8e8e361e77 | |||
| 73ff212020 | |||
| e611f6b92c | |||
| bc1d770f16 | |||
| 951d070d63 | |||
| 94170ebd13 | |||
| 2ec5d1b6d6 | |||
| 4d8f4db284 | |||
| a93997b0bb | |||
| 9dfc4f0097 | |||
| 2fa4635a9f | |||
| 9d79babac5 | |||
| 89073c9e76 | |||
| 8451bb352d | |||
| 86e53b5f0b | |||
| a7132b9b64 | |||
| a81262b3e0 | |||
| 8dae666119 | |||
| 96f320cabc | |||
| 644ded803c | |||
| 546f888487 | |||
| 7c21fa2aef | |||
| cc469d5698 | |||
| 7a6b1a84cc | |||
| 01904e9873 | |||
| a60a939566 | |||
| f62071b86b | |||
| 3bc3bd4236 | |||
| d3a011b746 | |||
| 567090ebcc | |||
| 862af12208 | |||
| 8825a24dde | |||
| 85201075c2 | |||
| f2b200a0f5 | |||
| ab1a1e6dba | |||
| 5e3c5cb691 | |||
| 1f6bc3decc | |||
| 6f88205675 | |||
| 22360a66f7 |
@@ -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` + 1–2 файла |
|
||||||
|
| Полный `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 в каждом чате.
|
||||||
@@ -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 пользователя
|
||||||
@@ -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`.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
description: Один агент — один репозиторий; не смешивать Front и Back
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# EventHub: отдельные агенты по репозиториям
|
||||||
|
|
||||||
|
## Правило
|
||||||
|
При работе с **несколькими репозиториями** EventHub запускай **отдельного агента на каждый репо**. Не смешивай Front и Back (и Spec/FrontAdmin) в одном агенте.
|
||||||
|
|
||||||
|
| Репо | Зона |
|
||||||
|
|------|------|
|
||||||
|
| `EventHubFront` | клиентский SPA |
|
||||||
|
| `EventHubBack` | Erlang API |
|
||||||
|
| `EventHubSpec` | спеки, UI-PARITY, контракты |
|
||||||
|
| `EventHubFrontAdmin` | admin SPA |
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
- Один агент правит и Front, и Back в одной сессии
|
||||||
|
- Коммиты/`Refs`/`Fixes` не в тот репо (Front-код → issue Back, и наоборот)
|
||||||
|
- Закрывать/комментировать issue чужого репо «заодно»
|
||||||
|
|
||||||
|
## Как делать
|
||||||
|
1. Спека/issue → агент **Spec** (или короткий foreground), если только доки.
|
||||||
|
2. API → отдельный агент **Back** (`Back#N`, eunit, push Back).
|
||||||
|
3. UI → отдельный агент **Front** (`Front#N`, lint/IFT, push Front).
|
||||||
|
4. Родитель-координатор только маршрутизирует; не пишет код сразу в два репо.
|
||||||
|
|
||||||
|
Кросс-репо фича (например Bookings inbox): **сначала Back**, потом **Front** (или параллельно двумя агентами), Spec — третьим или после стабилизации API.
|
||||||
@@ -0,0 +1,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/`.
|
||||||
@@ -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`
|
|
||||||
|
|||||||
@@ -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 (Let’s 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 опционален |
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Design artifacts
|
||||||
|
|
||||||
|
| Папка | Назначение |
|
||||||
|
|-------|------------|
|
||||||
|
| [`front/`](front/) | Клиентский EventHubFront / бренд CalenTIQ |
|
||||||
|
| [`admin/`](admin/) | EventHubFrontAdmin (позже) |
|
||||||
|
|
||||||
|
См. также [`BRANDING.md`](../BRANDING.md).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# EventHubFrontAdmin — дизайн
|
||||||
|
|
||||||
|
Зарезервировано под утверждённые мокапы / борды админки.
|
||||||
|
|
||||||
|
Пока пусто: клиентский фронт — в [`../front/`](../front/).
|
||||||
@@ -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 не кладём.
|
||||||
@@ -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 1–5 закрыты в коде 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.
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
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 |
|
After Width: | Height: | Size: 28 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 |
|
After Width: | Height: | Size: 25 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
|
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 |
|
After Width: | Height: | Size: 37 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="#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 |
|
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 |
@@ -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" }
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{ "display": "Syne", "ui": "Manrope" }
|
||||||
@@ -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
|
||||||
|
|||||||