Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1f6ba1c91 | |||
| 9e46a891b8 | |||
| 3540972799 | |||
| 0219ebace1 | |||
| 069b65450f | |||
| 2e81551f84 | |||
| 40cf2a618d | |||
| a8da8634e0 | |||
| 16eb79b340 | |||
| f22a98fc96 | |||
| f991bfe03e | |||
| 46534febd2 | |||
| 6a4734ba1c | |||
| 68a57a33c2 | |||
| 8f931db0f0 | |||
| 6d647dd037 |
@@ -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` + 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,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`
|
||||
@@ -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/`.
|
||||
+6
-24
@@ -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: |
|
||||
|
||||
@@ -16,6 +16,9 @@ test-results
|
||||
blob-report
|
||||
.playwright
|
||||
|
||||
# Temporary/scratch files
|
||||
.tmp-*
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 917 B |
+15
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 765 B |
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"common": {
|
||||
"appTitle": "EventHub",
|
||||
"appTitle": "CalenTIQ Admin",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"saving": "Saving…",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"common": {
|
||||
"appTitle": "EventHub",
|
||||
"appTitle": "CalenTIQ Admin",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение…",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user