diff --git a/.cursor/rules/stage-browser.mdc b/.cursor/rules/stage-browser.mdc new file mode 100644 index 0000000..af3807a --- /dev/null +++ b/.cursor/rules/stage-browser.mdc @@ -0,0 +1,15 @@ +--- +description: Как открыть stage UI в Cursor Browser (alias + proxy) +alwaysApply: true +--- + +# Stage browser (Cursor) + +`*.eventhub.local` часто не резолвится в Chromium/Cursor Browser. + +Канон: **`http://ui.stage.eventhub.test:8787`** через локальный proxy. + +Полная инструкция: `EventHubSpec/STAGE-BROWSER.md` +Скрипт: `EventHubSpec/scripts/stage-browser-alias-proxy.mjs` (запуск **Windows Node** на `:8787`). + +Hosts: `127.0.0.1 ui.stage.eventhub.test` diff --git a/README.md b/README.md index b9a9155..d05cc71 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ ## 2. [EventHub Admin UI — Техническое задание](EventHubFrontAdminSpec.md) ## 3. [EventHub Client UI — Техническое задание](EventHubFrontSpec.md) ## 4. [Правила работы (workflow)](WORKFLOW.md) +## 5. [Stage UI в Cursor Browser](STAGE-BROWSER.md) +## 6. [UX backlog (Client UI)](UX-BACKLOG.md) # **Репозитории разработки EventHub** ## 1. [EventHub Backend](https://git.sabilin.com/EventHub/EventHubBack) diff --git a/STAGE-BROWSER.md b/STAGE-BROWSER.md new file mode 100644 index 0000000..05e105b --- /dev/null +++ b/STAGE-BROWSER.md @@ -0,0 +1,68 @@ +# Stage UI в Cursor Browser + +Chromium / Cursor IDE Browser часто **не резолвят** `*.eventhub.local` (mDNS), даже если Windows/`nslookup` через OpenWRT уже видят stage. + +Для агентов и ручного просмотра stage используем **локальный alias + HTTP-прокси**. + +## URL + +| Открывать в браузере | Upstream | +|----------------------|----------| +| `http://ui.stage.eventhub.test:8787` | `https://ui.stage.eventhub.local` → `195.208.119.190` | + +## Одноразовая настройка hosts + +В `C:\Windows\System32\drivers\etc\hosts` (или WSL `/etc/hosts` не помогает Cursor на Windows): + +```text +127.0.0.1 ui.stage.eventhub.test +``` + +## Прокси + +Скрипт в репозитории: [`scripts/stage-browser-alias-proxy.mjs`](scripts/stage-browser-alias-proxy.mjs) + +Копия для быстрого старта агента: `~/.cursor/eventhub/stage-browser-alias-proxy.mjs` (если есть — можно любой из двух). + +**Важно:** процесс должен слушать на **Windows** `127.0.0.1:8787` (Node из Windows). Прокси только в WSL Cursor с Windows часто не достучится. + +```powershell +Start-Process -FilePath "C:\Program Files\nodejs\node.exe" ` + -ArgumentList "$env:USERPROFILE\.cursor\eventhub\stage-browser-alias-proxy.mjs" ` + -WindowStyle Hidden +``` + +Или из Spec (если скрипт уже скопирован в `~/.cursor/eventhub/`): + +```powershell +# из корня EventHubSpec +Start-Process -FilePath "C:\Program Files\nodejs\node.exe" ` + -ArgumentList (Resolve-Path ".\scripts\stage-browser-alias-proxy.mjs") ` + -WindowStyle Hidden +``` + +Проверка: открыть `http://ui.stage.eventhub.test:8787/login`. + +Переменные окружения (опционально): + +| Env | Default | +|-----|---------| +| `ALIAS_LISTEN_HOST` | `127.0.0.1` | +| `ALIAS_LISTEN_PORT` | `8787` | +| `STAGE_IP` | `195.208.119.190` | +| `STAGE_UI_HOST` | `ui.stage.eventhub.local` | +| `ALIAS_HOST` | `ui.stage.eventhub.test` | + +## Auth + +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`. +- Не поднимать прокси только в WSL, если агентский браузер на Windows. +- Не коммитить пароли/токены из smoke env. diff --git a/UX-BACKLOG.md b/UX-BACKLOG.md new file mode 100644 index 0000000..e866474 --- /dev/null +++ b/UX-BACKLOG.md @@ -0,0 +1,23 @@ +# EventHub Front — UX backlog (после stage walk) + +Оценка baseline ~6.8/10. После P0 ориентир ~7.8–8.2; P0+P1 ~8.3–8.6. + +См. также canvas `eventhub-front-ux-review` в Cursor. +Трекер: [EventHubFront#17](https://git.sabilin.com/EventHub/EventHubFront/issues/17). + +## P0 + +- [x] Форматировать даты локалью (ru-RU / текущий i18n) везде: Записи, события, подписка +- [x] Убрать / спрятать opaque id из consumer UI (календари, поиск, специалисты) +- [x] Починить дубль title события (`displayTitle` — схлопывание `TitleTitle`) + +## P1 + +- [x] Локализовать статусы + semantic pills (`StatusBadge`) +- [x] В заявках на событии показывать display name / email (`user_nickname` / `user_email` с API) +- [x] **Specialists invite: одно поле адресата** — lookup + email без отдельного «Или email» + +## P2 + +- [ ] Управление календарями: поиск, фильтр personal/commercial, pin/недавние +- [ ] Контраст метаданных профиля в dark / «Энергичный» mood — частично (`text-foreground` на значениях) diff --git a/WORKFLOW.md b/WORKFLOW.md index 5941a6c..fccc818 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -62,6 +62,19 @@ IFT smoke Admin UI (живой API после деплоя): `npm run test:e2e:i Окружение: **WSL + asdf**. PowerShell/Windows — только IDE; сборка и тесты — в WSL. +### Shell для Agent (обязательно) + +PowerShell в Cursor **ломает экранирование** (кавычки, `$`, JSON, heredoc, вложенный `bash -lc "..."`). + +| Нельзя | Нужно | +|--------|--------| +| Сложные one-liner’ы в PowerShell | Файл `.sh` + `wsl -e bash /mnt/c/.../script.sh` | +| `wsl -e bash -lc "…\"…\"…"` | `wsl -e bash -lc 'cd … && cmd'` (снаружи `'…'`) | +| Windows `npm` / `erl` / `rebar3` | npm и rebar **только в WSL** | +| Push Front без локальной проверки | WSL: `npm run lint && npm run build && npm run test:e2e:ift`, потом push | + +Канон для агентов: `EventHubBack/.cursor/rules/shell-wsl-not-powershell.mdc`. + | Tool | Version | Path (WSL) | |------|---------|------------| | Erlang/OTP | **28.2** | `/home/alexc/.asdf/installs/erlang/28.2/bin` | @@ -260,6 +273,15 @@ IFT и stage крутят **одну и ту же** сборку `sha-*`. Produc - **IFT smoke:** после успешного Deploy IFT — workflow `e2e-ift.yml` → `npm run test:e2e:ift` (без моков). Учётки с хоста IFT: `/opt/eventhub-ift/.env` (`SMOKE_ADMIN_*` → `ADMIN_SUPER_*`). - Селекторы: `data-testid` + a11y; контракт в `EventHubFrontAdmin/e2e/TESTIDS.md`. +### Stage UI в Cursor Browser + +`*.eventhub.local` часто не резолвится в Chromium/Cursor IDE Browser (mDNS). Для ручного и агентского просмотра stage: + +- URL: `http://ui.stage.eventhub.test:8787` +- Инструкция + proxy: **`STAGE-BROWSER.md`**, скрипт `scripts/stage-browser-alias-proxy.mjs` (Windows Node на `:8787`) +- Hosts: `127.0.0.1 ui.stage.eventhub.test` +- Учётки smoke: `~/.cursor/secrets/eventhub-stage.env` (не коммитить) + --- ## 7. Нагрузочное тестирование IFT (Tsung) @@ -354,6 +376,8 @@ error rate > 1% **или** mean request > 500 ms (hold ≥2 мин в фазе) | Дата | Изменение | |------|-----------| +| 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-15 | §7: анализ через EventHub `/metrics` + admin `nodes/metrics`; compose Prom опционален | | 2026-07-15 | §7: cleanup дампов на IFT + мониторинг в анализе; KEEP_OBSERVABILITY | | 2026-07-15 | §7: сбор и анализ логов — обязательный gate после stress | diff --git a/scripts/stage-browser-alias-proxy.mjs b/scripts/stage-browser-alias-proxy.mjs new file mode 100644 index 0000000..02f470e --- /dev/null +++ b/scripts/stage-browser-alias-proxy.mjs @@ -0,0 +1,101 @@ +#!/usr/bin/env node +/** + * Cursor-browser alias for stage UI. + * + * Why: Chromium treats *.local as mDNS and often ignores unicast DNS / hosts. + * This proxy serves http://ui.stage.eventhub.test:8787 → https://stage + * with Host/SNI ui.stage.eventhub.local (Traefik + existing cert). + * + * Usage: + * node stage-browser-alias-proxy.mjs + * open http://ui.stage.eventhub.test:8787/login + * + * Requires hosts: 127.0.0.1 ui.stage.eventhub.test + * + * Docs: EventHubSpec/STAGE-BROWSER.md + */ +import http from 'node:http'; +import https from 'node:https'; + +const LISTEN_HOST = process.env.ALIAS_LISTEN_HOST || '127.0.0.1'; +const LISTEN_PORT = Number(process.env.ALIAS_LISTEN_PORT || 8787); +const UPSTREAM_IP = process.env.STAGE_IP || '195.208.119.190'; +const UPSTREAM_HOST = process.env.STAGE_UI_HOST || 'ui.stage.eventhub.local'; +const ALIAS_HOST = process.env.ALIAS_HOST || 'ui.stage.eventhub.test'; + +const agent = new https.Agent({ + rejectUnauthorized: false, + servername: UPSTREAM_HOST, + keepAlive: true, +}); + +function rewriteLocation(value) { + if (!value) return value; + return value + .replaceAll(`https://${UPSTREAM_HOST}`, `http://${ALIAS_HOST}:${LISTEN_PORT}`) + .replaceAll(`http://${UPSTREAM_HOST}`, `http://${ALIAS_HOST}:${LISTEN_PORT}`); +} + +function scrubHeaders(headers) { + const out = { ...headers, host: UPSTREAM_HOST }; + delete out['content-length']; + // Avoid compression surprises while piping + delete out['accept-encoding']; + return out; +} + +function proxyHttp(req, res) { + const opts = { + hostname: UPSTREAM_IP, + port: 443, + path: req.url, + method: req.method, + headers: scrubHeaders(req.headers), + agent, + }; + const up = https.request(opts, (upRes) => { + const headers = { ...upRes.headers }; + if (headers.location) headers.location = rewriteLocation(headers.location); + res.writeHead(upRes.statusCode || 502, headers); + upRes.pipe(res); + }); + up.on('error', (err) => { + if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain' }); + res.end(`alias-proxy upstream error: ${err.message}`); + }); + req.pipe(up); +} + +function proxyUpgrade(req, socket, head) { + const opts = { + hostname: UPSTREAM_IP, + port: 443, + path: req.url, + method: 'GET', + headers: scrubHeaders(req.headers), + agent, + }; + const up = https.request(opts); + up.on('upgrade', (upRes, upSocket, upHead) => { + const lines = [`HTTP/1.1 ${upRes.statusCode} Switching Protocols`]; + for (const [k, v] of Object.entries(upRes.headers)) { + if (Array.isArray(v)) v.forEach((x) => lines.push(`${k}: ${x}`)); + else lines.push(`${k}: ${v}`); + } + lines.push('', ''); + socket.write(lines.join('\r\n')); + if (upHead?.length) socket.write(upHead); + upSocket.pipe(socket); + socket.pipe(upSocket); + }); + up.on('error', () => socket.destroy()); + up.end(head); +} + +const server = http.createServer(proxyHttp); +server.on('upgrade', proxyUpgrade); +server.listen(LISTEN_PORT, LISTEN_HOST, () => { + console.log( + `stage browser alias: http://${ALIAS_HOST}:${LISTEN_PORT} → https://${UPSTREAM_HOST} (${UPSTREAM_IP})` + ); +});