16 Commits

Author SHA1 Message Date
aleksey a1f6ba1c91 feat(auth): call POST /v1/admin/logout before clear storage. Fixes EventHub/EventHubFrontAdmin#43
CI / test (push) Successful in 52s
CI / push-image (push) Successful in 1m38s
CI / deploy-ift (push) Successful in 8s
CI / ci-done (push) Successful in 0s
CI / e2e-ift (push) Successful in 21s
CI / deploy-stage (push) Successful in 1m30s
CI / e2e-stage (push) Successful in 25s
2026-08-14 20:23:35 +03:00
aleksey 9e46a891b8 chore(rules): ignore .tmp scratch; IFT e2e needs SMOKE_USER. [skip ci] 2026-08-14 12:52:19 +03:00
aleksey 3540972799 docs(rules): require run-wsl-sh for CRLF .sh scripts
CI / test (push) Successful in 56s
CI / push-image (push) Successful in 1m49s
CI / deploy-ift (push) Successful in 6s
CI / ci-done (push) Successful in 0s
CI / e2e-ift (push) Successful in 20s
CI / deploy-stage (push) Successful in 1m44s
CI / e2e-stage (push) Successful in 21s
Cursor Write on Windows often saves CRLF; bare wsl -e bash script.sh
then fails with No such file. Canon is run-wsl-sh.sh (strip CR).
2026-07-29 12:55:21 +03:00
aleksey 0219ebace1 fix(ci-reports): chmod a+r on report.tar.gz for nginx read
mktemp leaves 600; nginx worker got 403 Forbidden on download.

[skip ci]
2026-07-28 19:42:21 +03:00
aleksey 069b65450f feat(ci-reports): publish report.tar.gz archive for offline download
Pack HTML report after publish; link archive in run index and CI log.
Add fetch-ci-report.sh helper for agent curl/extract.

[skip ci]
2026-07-28 13:52:26 +03:00
aleksey 2e81551f84 docs(rules): git commit via -F file when message has parentheses
Document fix(ci): and special chars breaking wsl -lc one-liners.
Use .tmp-commit-msg.txt + GIT_EDITOR=true git commit -F in .sh scripts.

[skip ci]
2026-07-28 10:21:54 +03:00
aleksey 40cf2a618d fix(ci): auto-detect stand for ci-reports URL on IFT runner
Remove hardcoded dev CI_REPORTS_BASE_URL/RUNNER_STAND from workflow.
detect_stand() picks ift/stage/dev from host paths and swarm services.

[skip ci]
2026-07-28 10:16:58 +03:00
aleksey a8da8634e0 fix(ci): publish reports to ci-reports.dev on runner host
CI / test (push) Successful in 49s
CI / push-image (push) Successful in 1m41s
CI / deploy-ift (push) Successful in 5s
CI / ci-done (push) Successful in 0s
CI / e2e-ift (push) Successful in 19s
CI / deploy-stage (push) Successful in 1m37s
CI / e2e-stage (push) Successful in 21s
2026-07-28 00:39:46 +03:00
aleksey 16eb79b340 fix(ci): chown ci-reports dir for act runner publish
CI / test (push) Successful in 48s
CI / push-image (push) Has been cancelled
CI / deploy-ift (push) Has been cancelled
CI / e2e-ift (push) Has been cancelled
CI / deploy-stage (push) Has been cancelled
CI / e2e-stage (push) Has been cancelled
CI / ci-done (push) Has been cancelled
2026-07-28 00:29:13 +03:00
aleksey f22a98fc96 docs: forbid bare git push; use git-push.sh helpers.
[skip ci]
2026-07-27 23:11:02 +03:00
aleksey f991bfe03e chore: push without CI
[skip ci]
2026-07-27 23:01:19 +03:00
aleksey 46534febd2 chore: trigger CI for ci-reports 2026-07-27 22:55:16 +03:00
aleksey 6a4734ba1c Prune admin-ui-dist artifacts; drop Playwright report zips.
Refs EventHub/EventHubFrontAdmin#42
2026-07-27 22:55:10 +03:00
aleksey 68a57a33c2 docs: agent pitfalls + token economy rules for Cursor [skip ci] 2026-07-27 21:53:10 +03:00
aleksey 8f931db0f0 feat: CalenTIQ Admin branding (wordmark, favicon, titles). Refs EventHub/EventHubSpec#10
CI / test (push) Successful in 47s
CI / push-image (push) Successful in 1m46s
CI / deploy-ift (push) Successful in 7s
CI / ci-done (push) Successful in 0s
CI / e2e-ift (push) Successful in 20s
CI / deploy-stage (push) Successful in 1m29s
CI / e2e-stage (push) Successful in 23s
2026-07-26 22:25:25 +03:00
aleksey 6d647dd037 chore: bump fallback VERSION to 0.2. Refs EventHub/EventHubSpec#10
CI / test (push) Failing after 23s
CI / push-image (push) Has been skipped
CI / deploy-ift (push) Has been skipped
CI / ci-done (push) Failing after 0s
CI / e2e-ift (push) Has been skipped
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been cancelled
2026-07-26 22:06:29 +03:00
24 changed files with 755 additions and 63 deletions
+120
View File
@@ -0,0 +1,120 @@
---
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` |
| IFT e2e: **N skipped**, exit 0 | Нет `SMOKE_USER_*` (голый `npm run test:e2e:ift`) | `run-front-ift.sh` / source `eventhub-ift.env` **до** npm. Не путать со «стенд недоступен». CI уже крутит e2e-ift → локально не дублировать |
| `unexpected EOF`, `ParserError`, пустой curl | PS ломает `"`, `$`, heredoc, `&&` | Файл `.sh` → `run-wsl-sh.sh` |
| `bash: syntax error near '('` | `grep -E ^(...)` в PS; **`fix(ci):` / `(…)` в `wsl -lc` one-liner** | Только внутри `.sh`; commit → `-F` файл (см. ниже) |
| `{{.Names}}: command not found` | `docker --format "{{…}}"` в PS | Формат в `.sh` или простой `docker ps` |
| `&&` is not valid | PowerShell не bash | Отдельные команды или `.sh` |
**Запрещено в PS:** голый `wsl -e bash /mnt/c/.../.tmp-*.sh` (без `run-wsl-sh.sh`), `wsl -e bash -lc "…\"…"`, `curl -d "{\"x\":…}"`, heredoc commit, `npm`/`rebar3`.
Канон запуска скриптов: см. `shell-wsl-not-powershell.mdc` § CRLF.
## 2. Git
| Симптом | Как правильно |
|---------|---------------|
| `#` в `-m` обрезает сообщение | `-F` файл; в тексте `Refs EventHub/EventHubBack#N` |
| `syntax error near '('` на commit | Сообщение `fix(ci): …` inline в `wsl -lc '… git-commit.sh … fix(ci): …'` | **Только** `.tmp-commit-msg.txt` + `git commit -F`; или `.sh` без inline `-m` |
| `Co-authored-by: Cursor <…>` / `<` ParserError | `git commit -F .tmp-msg.txt` из PS **или** `git-commit.sh` **из `.sh`**, не из PS one-liner |
| `editor` / not a terminal | Не `git commit` без `-F` в WSL; из Windows: `git commit -F file` |
| Сотни `M` после reset на `/mnt/c` | `git config core.filemode false` (локально); не «чинить» код |
| Удалились `.cursor/`, `.idea/` | **`git clean -fdx` только с backup** env + certs; IDE-директории **не трогать** |
| Push «висит» / ждёт login | **Не** `git push origin` — только `~/.cursor/eventhub/git-push.sh` / `git-push-main.sh` |
| Push заблокирован Auto-review | Push в `master` — только по явной просьбе; `request_smart_mode_approval` |
**`git clean` / reset к origin:** сначала сохранить `docker/.env*`, `.env.development`, `docker/traefik/certs/*`.
**Push:** запрещены голые `git push` / `git push origin …`. Канон — `bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh <repo-dir>` (токен из secrets).
**Commit (сообщение со скобками, `#`, `$`, пробелами):** не передавать subject/body аргументом через PowerShell или `wsl -e bash -lc '…'`. Пиши `.tmp-commit-msg.txt` в репо, затем в `.sh`:
```bash
cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack
git add path/to/file
GIT_EDITOR=true git commit -F .tmp-commit-msg.txt
```
Запуск из PS: `wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/.../EventHubBack/.tmp-commit.sh` — **без** голого `wsl -e bash …/.tmp-commit.sh`, **без** `-lc` и **без** `-m "fix(ci): …"`.
## 3. Docker dev (Swarm локально)
| Ошибка | Правильно |
|--------|-----------|
| «Балансировка» → 3× eventhub | **dev:** `traefik=1` + **`eventhub=1`** (см. `EventHubDevOps/docs/STANDS.md`) |
| `host-mode port already in use` | У eventhub опубликованы 8080/8445 — **нельзя** scale >1; LB = traefik |
| Собрали admin-ui/front без просьбы | «Только бэк» → eventhub (+ traefik если нужен HTTPS), **без** Front/Admin образов |
| Эмулятор не остановился | `docker service scale eventhub_bot-emulator-users=0` |
| `No such image: eventhub:latest` | Сначала `docker build -t eventhub:latest …` или `docker/build-images.sh` |
| Admin API dev не отвечает на :443 | Traefik без ports → **`http://127.0.0.1:8445`**; stage → SSH + `--resolve …:443:127.0.0.1` |
## 4. Scope (не додумывать)
- Делать **ровно** запрос; не раздувать (полный `build-images.sh`, 3 ноды, admin-ui, spec commit).
- Перед scale/deploy — прочитать compose и STANDS.md.
- Секреты: `gitea.ps1` / `.sh` + env; не inline `source secrets` в PS one-liner.
## 5. Субагенты (Task)
В промпт субагента добавлять:
```text
Shell: только WSL + .sh (см. shell-wsl-not-powershell.mdc, agent-pitfalls.mdc).
Один репо. Итог на русском. Не push без явного OK.
```
## 6. Экономия токенов / лимитов моделей
### Контекст чата
- **Один чат — одна задача**; после «готово / CI зелёный» — новый чат на следующий шаг (см. `WORKFLOW.md` §5).
- Workspace: для Back-задачи достаточно **`EventHubBack`** (+ Spec при необходимости); не держать все репо без нужды.
- **`@` точечно** — файл/папка, не `@Codebase` без причины.
- Не просить «проанализируй все transcripts» — они огромные.
### Дешёвые vs дорогие действия
| Дорого | Дешевле |
|--------|---------|
| Task explore «very thorough» | `grep` + 12 файла |
| Полный `build-images.sh` | Только нужный образ |
| Весь лог CI (80k строк) | API job + 20 строк вокруг ошибки |
| Несколько subagents «на всякий» | 1 subagent, узкий промпт |
| Browser deep-walk stage | Smoke-скрипт / один URL |
### Меньше итераций
- Сразу **`.sh`**, не 5 попыток PS one-liner (каждая = новый turn).
- Git перед commit: **один** `status` + `diff`, не три раза подряд.
- Тесты: `rebar3 eunit --module=…` если трогали один модуль; не полный suite без нужды.
- Docker: не rebuild, если `docker images` уже есть нужный tag.
### Модели и режимы
- **Ask / Plan** — вопросы и варианты без tool calls.
- **Agent** — только когда нужны команды и правки.
- Bugbot / Security Review — не на каждый мелкий diff.
### Scope в запросе (шаблон)
```text
Репо: EventHubBack
Issue: #N
Scope: только …
Не: front, docker, push, spec
Done: eunit / lint / …
```
### Subagents
- Редко; промпт = issue + файлы + done, **без** всей переписки.
- `one-repo-per-agent` — меньше контекста и ошибок.
### Повторяемое — в скрипты
Токены verify, smoke creds, stage deploy — **`~/.cursor/eventhub/*.sh`**, не заново через SSH в каждом чате.
+27
View File
@@ -0,0 +1,27 @@
---
description: Ответы субагентов и итоги работы — на русском
alwaysApply: true
---
# EventHub: язык ответов агентов
## Правило
Все **вызванные / фоновые агенты** (Task subagents) пишут пользователю и parent-агенту **на русском**:
- итоги, вердикты, чеклисты, UX-оценки;
- комментарии в Gitea (как и раньше — только русский);
- краткие status-апдейты в конце задачи.
## Допустимо на английском
- Идентификаторы кода, API paths, commit subject если в репо уже принят EN-стиль сообщений (можно и RU — главное единообразие в рамках задачи);
- цитаты из логов CI / stack traces без перевода;
- имена файлов, SHA, URL.
## Родитель-координатор
Тоже отвечает пользователю по-русски.
## Промпт субагенту (обязательно)
- «Return summary in Russian»
- «Shell: WSL + `.sh` only — `shell-wsl-not-powershell.mdc`, `agent-pitfalls.mdc`»
- «One repo per agent — `one-repo-per-agent.mdc`»
- «Экономия токенов: §6 agent-pitfalls — узкий scope, без лишних subagents/explore»
- Не push / не `git clean -fdx` без явного OK пользователя
+18
View File
@@ -0,0 +1,18 @@
---
description: EventHubFrontAdmin — npm/lint/e2e только через WSL (Windows npm сломан)
alwaysApply: true
---
# EventHubFrontAdmin: Node / npm (WSL)
**Запрещено:** `npm` / `npx` / `node` из Windows PowerShell/CMD.
**Обязательно:** команды только в **WSL**:
```bash
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFrontAdmin && npm run lint'
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFrontAdmin && npm run build'
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFrontAdmin && npm test'
```
Push: обычно `master` через `bash ~/.cursor/eventhub/git-push.sh …/EventHubFrontAdmin`
+29
View File
@@ -0,0 +1,29 @@
---
description: Один агент — один репозиторий; не смешивать Front и Back
alwaysApply: true
---
# EventHub: отдельные агенты по репозиториям
## Правило
При работе с **несколькими репозиториями** EventHub запускай **отдельного агента на каждый репо**. Не смешивай Front и Back (и Spec/FrontAdmin) в одном агенте.
| Репо | Зона |
|------|------|
| `EventHubFront` | клиентский SPA |
| `EventHubBack` | Erlang API |
| `EventHubSpec` | спеки, UI-PARITY, контракты |
| `EventHubFrontAdmin` | admin SPA |
## Запрещено
- Один агент правит и Front, и Back в одной сессии
- Коммиты/`Refs`/`Fixes` не в тот репо (Front-код → issue Back, и наоборот)
- Закрывать/комментировать issue чужого репо «заодно»
## Как делать
1. Спека/issue → агент **Spec** (или короткий foreground), если только доки.
2. API → отдельный агент **Back** (`Back#N`, eunit, push Back).
3. UI → отдельный агент **Front** (`Front#N`, lint/IFT, push Front).
4. Родитель-координатор только маршрутизирует; не пишет код сразу в два репо.
Кросс-репо фича (например Bookings inbox): **сначала Back**, потом **Front** (или параллельно двумя агентами), Spec — третьим или после стабилизации API.
+130
View File
@@ -0,0 +1,130 @@
---
description: EventHub — команды только через WSL/bash; PowerShell для сложного запрещён
alwaysApply: true
---
# EventHub: как запускать команды (обязательно)
## Проблема
PowerShell в Cursor **ломает** кавычки, `$`, JSON, heredoc и вложенный `bash -lc "..."`. Из‑за этого агенты снова и снова получают `unexpected EOF`, `Invalid JSON`, пустой curl.
## Правило
1. **Нельзя** гонять сложные one-liner’ы в PowerShell (npm, curl+JSON, python -c, git commit heredoc, вложенный bash с кавычками).
2. **Можно** в PowerShell только простые вещи: `cd`, `git status`, `ls`, одиночный `wsl …` без вложенных кавычек в аргументе.
3. Рабочий способ — **WSL bash** или **файл `.sh` + launcher** (см. ниже).
## CRLF / «No such file or directory» (обязательно)
Cursor `Write` на Windows часто сохраняет `.sh` с **CRLF**. Тогда:
```text
wsl -e bash /mnt/c/.../EventHubFront/.tmp-run.sh
# bash: .../.tmp-run.sh: No such file or directory
```
Файл **есть** (`ls` его видит) — ломается shebang (`#!/usr/bin/env bash\r`). Это **не** «файл не записался».
### Канон запуска любого `.sh` с `/mnt/c/...`
**Всегда** через хелпер (снимает `\r`, потом `bash`):
```text
wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront/.tmp-run.sh
```
**Запрещено** после `Write` голый:
```text
wsl -e bash /mnt/c/.../repo/.tmp-something.sh
```
Альтернатива без хелпера (тот же смысл):
```text
wsl -e bash -lc 'sed -i "s/\r$//" /mnt/c/.../script.sh && bash /mnt/c/.../script.sh'
```
Предпочтительно класть долгоживущие скрипты в `/mnt/c/Users/alexc/.cursor/eventhub/` и тоже гонять через `run-wsl-sh.sh`.
## Канон (копируй)
### Вариант A — скрипт (предпочтительно)
1. Запиши команды в файл, например `EventHubBack/.tmp-run.sh` или `~/.cursor/eventhub/….sh` (`#!/usr/bin/env bash`).
2. Запусти **только** так (из PowerShell, без вложенных кавычек):
```text
wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack/.tmp-run.sh
```
### Вариант B — один простой WSL вызов
Только если внутри **одинарные** кавычки bash и нет `"`/`$` конфликтов с PS (и **нет** нового `.sh` с диска C):
```text
wsl -e bash -lc 'cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront && npm run lint'
```
В PowerShell для `-lc` используй **одинарные** кавычки снаружи (`'...'`), внутри — обычный bash.
### Запрещено
```text
wsl -e bash /mnt/c/.../.tmp-run.sh # без run-wsl-sh / sed — CRLF → No such file
wsl -e bash -lc ".... python -c \"import...\" ...." # ломается
curl ... -d "{\"email\":\"$x\"}" # в PS ломается
git commit -m "$(cat <<'EOF' ...)" # в PS не так
wsl -e bash -lc 'git-commit.sh … fix(ci): subject' # ( ) ломают bash
wsl -e bash -lc 'git commit -m "fix(ci): …"' # то же
npm / npx / node из Windows # сломан
```
### Git commit — канон (subject с `( )`, `#`, `$`)
1. Текст коммита — в файл репо, напр. `.tmp-commit-msg.txt` (без heredoc из PS).
2. Скрипт `.tmp-commit.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
cd /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack
git add path1 path2
GIT_EDITOR=true git commit -F .tmp-commit-msg.txt
```
3. Из PowerShell:
```text
wsl -e bash /mnt/c/Users/alexc/.cursor/eventhub/run-wsl-sh.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack/.tmp-commit.sh
```
**Не** передавать сообщение аргументом в `git-commit.sh` / `-m` через `wsl -lc` — скобки в `fix(ci):` парсятся как subshell.
## Стек → команда
| Задача | Как |
|-----|-----|
| Front lint/build/e2e | `.sh` + `run-wsl-sh.sh` или `-lc` с PATH-fix + `npm …` |
| Front перед push main | сначала `npm run test:e2e:ift` в WSL, потом `git-push-main.sh` |
| Back rebar/eunit | `source scripts/wsl-dev-env.sh` в WSL |
| Git commit/push | **commit:** `.tmp-commit-msg.txt` + `git commit -F` в `.sh` через `run-wsl-sh.sh`; **push:** `bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh …` |
| JSON/API/python | скрипт `.sh`/`.py` + `run-wsl-sh.sh` |
## Git push (обязательно)
**Запрещено агентам:** `git push`, `git push origin`, `git push origin HEAD` / `master` напрямую — без токена зависают на HTTPS prompt.
**Канон:**
```text
bash /mnt/c/Users/alexc/.cursor/eventhub/git-push.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubBack
bash /mnt/c/Users/alexc/.cursor/eventhub/git-push-main.sh /mnt/c/Users/alexc/IdeaProjects/eventHub/EventHubFront
```
Путь WSL: `/mnt/c/Users/alexc/.cursor/eventhub/` (не `~/.cursor/…` — home в WSL может отличаться).
Credential store в WSL уже настроен (`credential.helper=store` + `git.sabilin.com`); хелпер всё равно предпочтителен (явный URL с токеном из secrets).
## Перед push Front
1. WSL: `npm run lint && npm run build` (ловит синтаксис/TS)
2. WSL: `npm run test:e2e:ift`
3. Только потом push через `git-push-main.sh`. **Не пушить**, если lint/build/IFT красные.
## Частые сбои (подробнее)
См. **`agent-pitfalls.mdc`**: CRLF/`run-wsl-sh.sh`, git `-F`, `&&` в PS, docker dev scale, `git clean` vs `.cursor/`.
+6 -24
View File
@@ -86,14 +86,6 @@ jobs:
path: dist/
retention-days: 1
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Mark test complete
id: mark_test
if: success()
@@ -122,6 +114,12 @@ jobs:
name: admin-ui-dist
path: dist
- name: Prune admin-ui-dist artifacts
env:
GITEA_TOKEN: ${{ secrets.REGISTRY_PASSWORD }}
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: bash scripts/prune-gitea-artifacts.sh EventHub/EventHubFrontAdmin admin-ui-dist 0
- name: Wait for Docker daemon
run: bash scripts/ensure-docker-daemon.sh 240
@@ -273,14 +271,6 @@ jobs:
E2E_IFT_BASE_URL: ${{ secrets.E2E_IFT_BASE_URL }}
run: npm run test:e2e:ift
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v3
with:
name: playwright-ift-report
path: playwright-report/
retention-days: 7
deploy-stage:
needs: e2e-ift
if: |
@@ -395,14 +385,6 @@ jobs:
E2E_STAGE_BASE_URL: ${{ secrets.E2E_STAGE_BASE_URL }}
run: npm run test:e2e:stage
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v3
with:
name: playwright-stage-report
path: playwright-report/
retention-days: 7
ci-done:
needs: [test, push-image]
if: |
+3
View File
@@ -16,6 +16,9 @@ test-results
blob-report
.playwright
# Temporary/scratch files
.tmp-*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
+1 -1
View File
@@ -1 +1 @@
0.1
0.2
+2 -1
View File
@@ -3,8 +3,9 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EventHub Admin</title>
<title>CalenTIQ Admin</title>
</head>
<body>
<div id="root"></div>
+1 -1
View File
@@ -26,7 +26,7 @@ export default defineConfig({
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: 1,
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
reporter: 'list',
timeout: 90_000,
expect: { timeout: 10_000 },
use: {
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="none">
<g stroke="#FFFFFF" stroke-width="48" stroke-linecap="round" stroke-linejoin="round">
<path d="M512 132 A380 380 0 1 0 512 892"/>
<path d="M512 512 L377.77 434.50"/>
<path d="M512 512 L698.20 404.50"/>
</g>
<circle cx="512" cy="512" r="24" fill="#FFFFFF"/>
<circle cx="816.06" cy="207.94" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="297.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="400.71" r="18" fill="#FFFFFF"/>
<circle cx="942.00" cy="512.00" r="18" fill="#FFFFFF"/>
<circle cx="927.35" cy="623.29" r="18" fill="#FFFFFF"/>
<circle cx="884.39" cy="727.00" r="18" fill="#FFFFFF"/>
<circle cx="816.06" cy="816.06" r="18" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

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

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

+15 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 765 B

+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Удаляет каталоги отчётов старше CI_REPORTS_KEEP_DAYS (default 14).
# Usage: prune-ci-reports.sh [/var/eventhub/ci-reports]
set -euo pipefail
ROOT="${1:-${CI_REPORTS_DIR:-/var/eventhub/ci-reports}}"
KEEP_DAYS="${CI_REPORTS_KEEP_DAYS:-14}"
if [[ ! -d "${ROOT}" ]]; then
echo "ci-reports: ${ROOT} отсутствует — пропуск"
exit 0
fi
echo "ci-reports prune: root=${ROOT} keep_days=${KEEP_DAYS}"
find "${ROOT}" -mindepth 2 -maxdepth 2 -type d -mtime "+${KEEP_DAYS}" -print -exec rm -rf {} + 2>/dev/null || true
echo "ci-reports prune: done"
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Delete Gitea Actions artifacts by exact name (keep newest N).
# Usage: prune-gitea-artifacts.sh <owner/repo> <artifact-name> [keep_last]
# Env: GITEA_BASE_URL, GITEA_TOKEN (or REGISTRY_PASSWORD)
set -euo pipefail
REPO="${1:?owner/repo e.g. EventHub/EventHubFront}"
NAME_FILTER="${2:?artifact name}"
KEEP_LAST="${3:-0}"
BASE="${GITEA_BASE_URL:-https://git.sabilin.com}"
TOKEN="${GITEA_TOKEN:-${REGISTRY_PASSWORD:-}}"
if [[ -z "${TOKEN}" ]]; then
echo "prune-gitea-artifacts: no token — skip" >&2
exit 0
fi
python3 - "${BASE}" "${TOKEN}" "${REPO}" "${NAME_FILTER}" "${KEEP_LAST}" <<'PY'
import json, os, sys, urllib.request, urllib.error
base, token, repo, name_filter, keep_last = sys.argv[1:6]
keep_last = int(keep_last)
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
def get(url):
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=60) as resp:
return json.load(resp)
def delete(url):
req = urllib.request.Request(url, headers=headers, method="DELETE")
try:
with urllib.request.urlopen(req, timeout=60) as resp:
return resp.status
except urllib.error.HTTPError as e:
return e.code
arts = []
page = 1
while page <= 40:
url = f"{base.rstrip('/')}/api/v1/repos/{repo}/actions/artifacts?limit=50&page={page}"
try:
data = get(url)
except Exception as exc:
print(f"prune-gitea-artifacts: list failed: {exc}", file=sys.stderr)
sys.exit(0)
batch = data if isinstance(data, list) else (data.get("artifacts") or data.get("data") or [])
if not batch:
break
for a in batch:
n = str(a.get("name") or "")
if n == name_filter or n.startswith(name_filter):
arts.append(a)
if len(batch) < 50:
break
page += 1
# Prefer newest first by id desc
arts.sort(key=lambda a: int(a.get("id") or 0), reverse=True)
to_delete = arts[keep_last:]
print(f"prune-gitea-artifacts: {repo} name={name_filter} matched={len(arts)} delete={len(to_delete)} keep={keep_last}")
for a in to_delete:
aid = a.get("id")
code = delete(f"{base.rstrip('/')}/api/v1/repos/{repo}/actions/artifacts/{aid}")
print(f" delete id={aid} name={a.get('name')} http={code}")
PY
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# Publish HTML CI report tree to local runner host and print HTTPS URL.
# Usage: publish-ci-report.sh <repo> <run_id> <kind> <src_dir>
# repo: EventHubBack | EventHubFront | …
# run_id: usually GITHUB_RUN_NUMBER or sha
# kind: eunit | ct | allure | e2e-ift | e2e-stage | …
# src_dir: directory with HTML to copy
set -euo pipefail
REPO="${1:?repo}"
RUN_ID="${2:?run_id}"
KIND="${3:?kind}"
SRC="${4:?src_dir}"
REPORTS_DIR="${CI_REPORTS_DIR:-/var/eventhub/ci-reports}"
STAND="${RUNNER_STAND:-}"
detect_stand() {
if [[ -n "${RUNNER_STAND:-}" ]]; then
echo "${RUNNER_STAND}"
return 0
fi
if [[ -f /opt/eventhub-ift/.env ]] || [[ -d /opt/eventhub-ift ]]; then
echo "ift"
return 0
fi
if [[ -f /opt/eventhub-stage/.env ]] || [[ -d /opt/eventhub-stage ]]; then
echo "stage"
return 0
fi
if command -v docker >/dev/null 2>&1 \
&& docker service ls --format '{{.Name}}' 2>/dev/null | grep -qx 'eventhub-ift-core_ci-reports'; then
echo "ift"
return 0
fi
if command -v docker >/dev/null 2>&1 \
&& docker service ls --format '{{.Name}}' 2>/dev/null | grep -qx 'eventhub_ci-reports'; then
echo "dev"
return 0
fi
echo "dev"
}
if [[ -n "${CI_REPORTS_BASE_URL:-}" ]]; then
BASE_URL="${CI_REPORTS_BASE_URL%/}"
STAND="${RUNNER_STAND:-custom}"
else
STAND="$(detect_stand)"
BASE_URL="https://ci-reports.${STAND}.eventhub.local"
fi
if [[ ! -d "${SRC}" ]]; then
echo "publish-ci-report: src missing: ${SRC}" >&2
exit 1
fi
# act runner: workflow may create REPORTS_DIR via sudo (root-owned); ensure CI user can write.
ensure_ci_reports_dir() {
if [[ -d "${REPORTS_DIR}" ]] && [[ -w "${REPORTS_DIR}" ]]; then
return 0
fi
if mkdir -p "${REPORTS_DIR}" 2>/dev/null && [[ -w "${REPORTS_DIR}" ]]; then
return 0
fi
if command -v sudo >/dev/null 2>&1; then
sudo mkdir -p "${REPORTS_DIR}"
sudo chown -R "$(id -u):$(id -g)" "${REPORTS_DIR}"
return 0
fi
mkdir -p "${REPORTS_DIR}"
}
ensure_ci_reports_dir
ensure_ci_reports_nginx() {
command -v docker >/dev/null 2>&1 || return 0
local svc replicas
for svc in eventhub-ift-core_ci-reports eventhub_ci-reports; do
docker service ls --format '{{.Name}}' 2>/dev/null | grep -qx "${svc}" || continue
replicas=$(docker service ls --format '{{.Name}} {{.Replicas}}' 2>/dev/null | awk -v s="${svc}" '$1==s{print $2; exit}')
if [[ "${replicas}" == 0/* ]] || [[ -z "${replicas}" ]]; then
echo "Starting ${svc}=1 (ci-reports nginx)..."
docker service scale "${svc}=1" 2>/dev/null || true
fi
return 0
done
}
ensure_ci_reports_nginx
DEST="${REPORTS_DIR}/${REPO}/${RUN_ID}/${KIND}"
mkdir -p "${REPORTS_DIR}/${REPO}/${RUN_ID}"
rm -rf "${DEST}"
mkdir -p "${DEST}"
cp -a "${SRC}/." "${DEST}/"
create_report_archive() {
local dest="$1"
local archive="${dest}/report.tar.gz"
local tmp
tmp="$(mktemp)"
tar -czf "${tmp}" -C "${dest}" .
mv -f "${tmp}" "${archive}"
chmod a+r "${archive}"
}
create_report_archive "${DEST}"
# Convenience: CT puts index under ct_run.*; expose DEST/index.html
if [[ ! -f "${DEST}/index.html" ]]; then
CT_INDEX="$(find "${DEST}" -type f -name index.html 2>/dev/null | head -n 1 || true)"
if [[ -n "${CT_INDEX}" ]]; then
REL="${CT_INDEX#"${DEST}/"}"
cat > "${DEST}/index.html" <<EOF
<!DOCTYPE html>
<html lang="ru"><head>
<meta charset="utf-8"/>
<meta http-equiv="refresh" content="0; url=${REL}"/>
<title>CT report</title>
</head><body>
<p><a href="${REL}">Open Common Test report</a></p>
</body></html>
EOF
fi
fi
# Refresh run index
INDEX="${REPORTS_DIR}/${REPO}/${RUN_ID}/index.html"
{
echo "<!DOCTYPE html>"
echo "<html lang=\"ru\"><head><meta charset=\"utf-8\"/><title>${REPO} #${RUN_ID}</title>"
echo "<style>body{font-family:system-ui,sans-serif;margin:2rem}a{display:block;margin:.4rem 0}</style>"
echo "</head><body>"
echo "<h1>${REPO} — run ${RUN_ID}</h1>"
echo "<ul>"
for d in "${REPORTS_DIR}/${REPO}/${RUN_ID}"/*/; do
[[ -d "${d}" ]] || continue
name="$(basename "${d}")"
if [[ -f "${d}index.html" ]]; then
if [[ -f "${d}report.tar.gz" ]]; then
echo "<li><a href=\"./${name}/index.html\">${name}</a> — <a href=\"./${name}/report.tar.gz\">report.tar.gz</a></li>"
else
echo "<li><a href=\"./${name}/index.html\">${name}</a></li>"
fi
elif [[ -f "${d}report.tar.gz" ]]; then
echo "<li><a href=\"./${name}/\">${name}/</a> — <a href=\"./${name}/report.tar.gz\">report.tar.gz</a></li>"
else
echo "<li><a href=\"./${name}/\">${name}/</a></li>"
fi
done
echo "</ul></body></html>"
} > "${INDEX}"
URL="${BASE_URL}/${REPO}/${RUN_ID}/${KIND}/"
ARCHIVE_URL="${BASE_URL}/${REPO}/${RUN_ID}/${KIND}/report.tar.gz"
INDEX_URL="${BASE_URL}/${REPO}/${RUN_ID}/"
echo "Report stand: ${STAND}"
echo "Report: ${URL}"
echo "Report archive: ${ARCHIVE_URL}"
echo "Report index: ${INDEX_URL}"
echo "Report path: ${DEST}"
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
{
echo "### Test report"
echo "- [${KIND}](${URL})"
echo "- [${KIND} archive](${ARCHIVE_URL})"
echo "- [index](${INDEX_URL})"
} >> "${GITHUB_STEP_SUMMARY}"
fi
+3
View File
@@ -16,6 +16,9 @@ export const authApi = {
const { data } = await apiClient.get('/v1/admin/me');
return data;
},
logout: async (refreshToken: string): Promise<void> => {
await apiClient.post('/v1/admin/logout', { refresh_token: refreshToken });
},
updateMe: async (payload: Partial<Admin>): Promise<Admin> => {
const { data } = await apiClient.put('/v1/admin/me', payload);
return data;
+75
View File
@@ -0,0 +1,75 @@
import React from 'react';
import { cn } from '@/lib/utils';
type Size = 'sm' | 'md' | 'lg';
type Props = {
size?: Size;
className?: string;
/** Show Time Arc mark next to wordmark (default true). */
withMark?: boolean;
/** Append muted "Admin" after the brand (sidebar / login). */
withAdmin?: boolean;
};
const sizeMap: Record<Size, { mark: string; text: string; admin: string }> = {
sm: { mark: 'h-5 w-5', text: 'text-sm', admin: 'text-[11px]' },
md: { mark: 'h-7 w-7', text: 'text-2xl', admin: 'text-sm' },
lg: { mark: 'h-10 w-10', text: 'text-4xl', admin: 'text-base' },
};
/**
* Public brand lockup: Time Arc + CalenTIQ (Admin).
* Uses currentColor so the mark follows theme foreground.
*/
const BrandWordmark: React.FC<Props> = ({
size = 'md',
className,
withMark = true,
withAdmin = false,
}) => {
const s = sizeMap[size];
return (
<div
className={cn('inline-flex items-center gap-2 text-foreground', className)}
data-testid="brand-wordmark"
aria-label={withAdmin ? 'CalenTIQ Admin' : 'CalenTIQ'}
>
{withMark && (
<svg
className={cn('shrink-0', s.mark)}
viewBox="0 0 1024 1024"
fill="none"
aria-hidden
>
<g
stroke="currentColor"
strokeWidth="48"
strokeLinecap="round"
strokeLinejoin="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="currentColor" />
<circle cx="816.06" cy="207.94" r="18" fill="currentColor" />
<circle cx="884.39" cy="297.00" r="18" fill="currentColor" />
<circle cx="927.35" cy="400.71" r="18" fill="currentColor" />
<circle cx="942.00" cy="512.00" r="18" fill="currentColor" />
<circle cx="927.35" cy="623.29" r="18" fill="currentColor" />
<circle cx="884.39" cy="727.00" r="18" fill="currentColor" />
<circle cx="816.06" cy="816.06" r="18" fill="currentColor" />
</svg>
)}
<p className={cn('leading-none font-extrabold tracking-tight', s.text)}>
Calen<span className="text-primary">TIQ</span>
{withAdmin && (
<span className={cn('ml-1.5 font-semibold opacity-70', s.admin)}>Admin</span>
)}
</p>
</div>
);
};
export default BrandWordmark;
+7 -4
View File
@@ -38,6 +38,7 @@ import {
} from '@/config/adminAccess';
import { notify } from '@/lib/notify';
import { cn } from '@/lib/utils';
import BrandWordmark from '@/components/BrandWordmark';
import { testid } from '@/lib/testid';
import { getTableDensity, toggleTableDensity, type TableDensity } from '@/lib/density';
import { Button } from '@/components/ui/button';
@@ -251,11 +252,13 @@ const ControlCenterLayout: React.FC = () => {
collapsed ? 'w-16' : 'w-60'
)}
>
<div className="flex h-14 items-center justify-between px-3 border-b border-white/10">
<div className="flex h-14 items-center justify-between gap-1 px-3 border-b border-white/10">
{!collapsed && (
<span className="font-semibold text-sm truncate">
{import.meta.env.VITE_APP_TITLE || t('common.appTitle')}
</span>
<BrandWordmark
size="sm"
withAdmin
className="min-w-0 truncate text-sidebar-foreground [&_span.text-primary]:text-sidebar-foreground"
/>
)}
<Button
variant="ghost"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"common": {
"appTitle": "EventHub",
"appTitle": "CalenTIQ Admin",
"cancel": "Cancel",
"save": "Save",
"saving": "Saving…",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"common": {
"appTitle": "EventHub",
"appTitle": "CalenTIQ Admin",
"cancel": "Отмена",
"save": "Сохранить",
"saving": "Сохранение…",
+2 -1
View File
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import BrandWordmark from '@/components/BrandWordmark';
const LoginPage: React.FC = () => {
const navigate = useNavigate();
@@ -61,7 +62,7 @@ const LoginPage: React.FC = () => {
/>
<Card className="relative z-10 w-full max-w-md border-border/80 shadow-lg shadow-slate-900/5 backdrop-blur-sm">
<CardHeader className="space-y-3">
<p className="text-3xl font-semibold tracking-tight text-foreground">EventHub</p>
<BrandWordmark size="md" withAdmin />
<div>
<CardTitle className="text-xl">{t('login.title')}</CardTitle>
<CardDescription className="mt-1">{t('login.subtitle')}</CardDescription>
+8
View File
@@ -37,6 +37,14 @@ export const useAuthStore = create<AuthState>((set) => ({
},
logout: async () => {
const refresh = localStorage.getItem(REFRESH_TOKEN_KEY);
if (refresh) {
try {
await authApi.logout(refresh);
} catch {
// сеть/401 — всё равно чистим локально
}
}
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
useMetricsStore.getState().reset();