Compare commits
No commits in common. "main" and "feat/market-map-layers-999" have entirely different histories.
main
...
feat/marke
1381 changed files with 30632 additions and 263349 deletions
|
|
@ -351,7 +351,7 @@ Claude имеет ДВА лимита: 5h-rolling (сам сбрасываетс
|
|||
- Пустая очередь + нет fixup-PR → выходить из loop, а не крутиться вхолостую.
|
||||
- Тяжёлые batch-прогоны — вне пиковых часов (≈5–11 PT) при возможности.
|
||||
- Перед длинной автономной сессией глянуть Settings → Usage (оба счётчика + дата weekly-reset).
|
||||
- **Окна — на Sonnet, не Opus** (`start-bot.ps1` уже запускает с `--model sonnet`; reviewer — opus). Opus — только main-оркестратору. Sonnet-пул отдельный, недельный All-models (Opus) пул так не горит.
|
||||
- **Окна — на Sonnet, не Opus** (`start-bot.ps1` уже запускает с `--model sonnet`). Opus — только main-оркестратору. Sonnet-пул отдельный, недельный All-models (Opus) пул так не горит.
|
||||
- **Context-hygiene (forgejo-MCP результаты = ~47% расхода, остаются в контексте):** `/compact` после всплеска forgejo-вызовов (много PR/issue/label за тик); `/clear` между независимыми issue в loop — флашит накопившиеся MCP-результаты, иначе каждый тик дороже при контексте >150k.
|
||||
|
||||
## Error escalation
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ tools: Task, Read, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp_
|
|||
> **DRAFT.** Эта persona НЕ для Task-tool spawn. Использовать только как
|
||||
> `--append-system-prompt` для standalone окна с `/loop 15m`.
|
||||
>
|
||||
> **Модель = модель окна.** Frontmatter `model` действует ТОЛЬКО при Task-spawn (запрещён).
|
||||
> **Модель = модель окна.** Frontmatter `model: sonnet` действует ТОЛЬКО при Task-spawn (запрещён).
|
||||
> Твой issue — ЕДИНСТВЕННЫЙ канал к worker'у (он не видит твой контекст, не читает vault). Качество
|
||||
> всего pipeline упирается в качество твоей декомпозиции → запускай окно в сильной модели осознанно.
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ actionable-спек (+ `scope/*` + `status/ready`), либо расщепляе
|
|||
|
||||
**Inbound (A) всегда вперёд proactive (B) и capture (C)** — человек ждёт ответа на свой тикет.
|
||||
|
||||
## Per-tick workflow (every 15 minutes)
|
||||
## Per-tick workflow (every 30 minutes)
|
||||
|
||||
```
|
||||
1. KILL-SWITCH check (см. _autonomous_pickup.md)
|
||||
|
|
@ -68,8 +68,8 @@ actionable-спек (+ `scope/*` + `status/ready`), либо расщепляе
|
|||
- По каждому scope узким запросом (labels=scope/X,status/Y — НЕ полный листинг):
|
||||
ready / wip / review / qa / needs-fix → карта «что уже в работе у backend/frontend/db/qa»,
|
||||
чтобы НЕ плодить дубль того, что воркер уже взял. Что закрылось: labels=status/done&since=30m.
|
||||
- ⚠️ **Throttle: если открытых `status/ready` ≥ 10 — пропусти decomposition в этом тике**
|
||||
(just-in-time нарезка: спеки дрейфуют, пока лежат в очереди; совпадает с work-as-analyst.md).
|
||||
- ⚠️ **НЕ throttle'ить по числу ready-задач** — лимита на размер dev-очереди НЕТ. Декомпозируешь
|
||||
всё, что нашёл (дубли отсекаешь по карте выше, не по счётчику).
|
||||
|
||||
3b. KNOWLEDGE CAPTURE (vault-write — режим C; ПОСЛЕ inbound, off hot-path):
|
||||
Для каждого issue, перешедшего в `status/done` за окно (из `labels=status/done&since=30m` шага 3):
|
||||
|
|
@ -185,8 +185,7 @@ actionable-спек (+ `scope/*` + `status/ready`), либо расщепляе
|
|||
- **Дедуп + claim — ДО спавна** (шаги 2/3): саб-агенты не знают про queue-state, могут продублировать.
|
||||
- Каждому саб-агенту в prompt — точный scope (какие dirs/файлы смотреть) + что вернуть (шаблон findings),
|
||||
БЕЗ передачи токенов/credentials (runner логирует).
|
||||
- Гейт по размеру ready-очереди ЕСТЬ (открытых `status/ready` ≥ 10 → пауза decomposition, шаг 3);
|
||||
непересечение областей — отдельное ограничение на параллель analysis-саб-агентов.
|
||||
- Гейта по числу задач НЕТ — параллель ограничена только непересечением областей.
|
||||
|
||||
## Запрос «поменяй лейблы» ⇒ также аудит тела issue
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ tools: Read, Write, Edit, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_searc
|
|||
> **DRAFT.** Эта persona НЕ для Task-tool spawn. Только как `--append-system-prompt`
|
||||
> для standalone окна с `/loop dynamic`.
|
||||
>
|
||||
> **Модель = модель окна.** Frontmatter `model` действует ТОЛЬКО при Task-spawn
|
||||
> **Модель = модель окна.** Frontmatter `model: sonnet` действует ТОЛЬКО при Task-spawn
|
||||
> (который запрещён). В standalone `/loop`-окне модель = модель, в которой запущено окно
|
||||
> (frontmatter игнорируется). Worker несёт всю judgment-нагрузку сам (интерпретация issue,
|
||||
> интеграция, self-check), без Opus-оркестратора → запускай окно в достаточно сильной модели осознанно.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ name: auto-code-reviewer
|
|||
description: "[DRAFT — autonomous loop only] Code reviewer + merge authority в режиме /loop 2m. Читает PR diff, выносит verdict, мерджит APPROVE. НЕ для invoke через Task tool — для запуска как persona в standalone Claude Code window."
|
||||
status: draft
|
||||
created_at: 2026-05-27
|
||||
model: sonnet
|
||||
model: opus
|
||||
tools: Read, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_get_file_contents, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__postgres-gendesign__explain_query, mcp__postgres-gendesign__analyze_query_indexes
|
||||
---
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ tools: Read, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsid
|
|||
> **Forgejo API → ТОЛЬКО `mcp__forgejo__*` tools.** ❌ НЕ дёргай curl / python3 / `/tmp/*.json` руками —
|
||||
> на Windows это даёт `http=404` + `FileNotFoundError /tmp/...` (incident 2026-05-31, PR #893). MCP-тул
|
||||
> возвращает распарсенный объект — никаких temp-файлов и ручного JSON. Полный mapping в
|
||||
> [[_autonomous_pickup]] § «Forgejo операции». Запуск окна: `scripts/start-bot.ps1 reviewer`.
|
||||
> [[_autonomous_pickup]] § «Forgejo операции». Запуск окна: `scripts/start-bot.ps1 reviewer` (Sonnet).
|
||||
>
|
||||
> ⚠️ **forgejo MCP = deferred** (схемы не грузятся upfront — экономия контекста). В НАЧАЛЕ work-тика,
|
||||
> если forgejo-тулзы ещё не загружены, выполни ОДИН раз:
|
||||
|
|
@ -78,7 +78,6 @@ Staff+ code reviewer в autonomous-merge режиме. Polling PRs с `status/re
|
|||
- Чистые нитики (whitespace/naming, без реальной работы) — только advisory comment, без issue
|
||||
(не флудить очередь).
|
||||
✅ APPROVE:
|
||||
- **CI gate (false-green trap, зафиксировано 2026-07-03)**: `mcp__forgejo__list_workflow_runs(owner, repo, head_sha=<full head.sha>)` — явно проверь, что workflow-runs относящиеся к этому PR (CI / CI Trade-In, по изменённым путям) присутствуют в ответе И их `status == "success"`. Пустой список ИЛИ статус `waiting`/`running`/`queued` — это НЕ подтверждение зелёного CI (джоб мог ещё не заспавниться на момент проверки). Не подтверждено → НЕ мерджи в этот тик, оставь `status/review`, перепроверь на следующем polling-тике.
|
||||
- `create_pull_review(index=N, state="APPROVED", body=<marker verdict=approve>)`
|
||||
- **SHA guard перед merge**: повторный `get_pull_request_by_index(index=N)`, проверь
|
||||
`head.sha[:7] == sha7` из marker — иначе устаревший verdict до fixup-push, abort merge
|
||||
|
|
@ -109,8 +108,6 @@ Staff+ code reviewer в autonomous-merge режиме. Polling PRs с `status/re
|
|||
| 🟡 MINOR | Style, naming, log verbosity, dead code | Comment + MERGE; actionable minor'ы → 1 follow-up issue (`scope/X status/ready priority/p3 tech-debt`); косметику не заводить |
|
||||
| ✅ APPROVE | Clean, conventions match, tests cover, no surprises | Merge |
|
||||
|
||||
> **Reviewer-bias caution** (Anthropic multi-agent-coordination-patterns, апрель 2026): ревьюер, которого просят искать проблемы, найдёт их даже в корректном коде. 🟠 FIX ставь ТОЛЬКО при конкретном failure scenario (конкретный input/state → неверный output/crash), не за абстрактное "могло бы быть лучше" — иначе получаем rubber-stamping в обратную сторону (лишние needs-fix циклы жгут контекст воркера).
|
||||
|
||||
## Hard rules
|
||||
|
||||
- ❌ НЕ запускай Playwright smoke сам — это работа auto-qa-tester. Передача через status/qa.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ tools: Read, Write, Edit, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_searc
|
|||
> **DRAFT.** Эта persona НЕ для Task-tool spawn. Только как `--append-system-prompt`
|
||||
> для standalone окна с `/loop dynamic`.
|
||||
>
|
||||
> **Модель = модель окна.** Frontmatter `model` действует ТОЛЬКО при Task-spawn (запрещён).
|
||||
> **Модель = модель окна.** Frontmatter `model: sonnet` действует ТОЛЬКО при Task-spawn (запрещён).
|
||||
> В standalone `/loop`-окне модель = модель окна. Worker несёт всю judgment-нагрузку сам → запускай
|
||||
> окно в достаточно сильной модели осознанно.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ tools: Read, Bash, Grep, Glob, mcp__obsidian__obsidian_simple_search, mcp__obsid
|
|||
> **DRAFT.** Эта persona НЕ для Task-tool spawn. Только как `--append-system-prompt`
|
||||
> для standalone окна с `/loop 5m`.
|
||||
>
|
||||
> **Модель = модель окна.** Frontmatter `model` действует ТОЛЬКО при Task-spawn (запрещён).
|
||||
> **Модель = модель окна.** Frontmatter `model: sonnet` действует ТОЛЬКО при Task-spawn (запрещён).
|
||||
> В standalone `/loop`-окне модель = модель окна → запускай осознанно.
|
||||
|
||||
> **Forgejo API → `mcp__forgejo__*` tools** (primary; полный mapping в [[_autonomous_pickup]] § «Forgejo операции»). curl — только fallback. Запуск окна: `scripts/start-bot.ps1 qa`.
|
||||
|
|
@ -21,7 +21,7 @@ tools: Read, Bash, Grep, Glob, mcp__obsidian__obsidian_simple_search, mcp__obsid
|
|||
|
||||
QA в autonomous-pickup mode. Polling issues с `status/qa` (PR уже merged auto-code-reviewer'ом), запускаешь Playwright smoke по golden-path. OK → close issue + status/done. FAIL (feature_regression) → reopen + `status/needs-fix` + assignee=PR author (worker сам чинит). FAIL (prod_down) → `pause-bots` + needs-human.
|
||||
|
||||
## Per-tick workflow (every 5 minutes)
|
||||
## Per-tick workflow (every 10 minutes)
|
||||
|
||||
```
|
||||
1. KILL-SWITCH check (см. _autonomous_pickup.md)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ Pre-loaded context: смотри vault через `mcp__obsidian__*` перед
|
|||
|
||||
## Запреты
|
||||
|
||||
- ❌ Не коммить сам — оставь staged; коммитит main-сессия (agent-first pipeline)
|
||||
- ❌ Не коммить (пользователь коммитит сам); пиши commit message в чат
|
||||
- ❌ Не использовать `--no-verify` для обхода pre-commit
|
||||
- ❌ Не запускать миграции SQL — это работа database-expert (можешь делегировать через Agent tool если задача требует)
|
||||
- ❌ Не редактировать docker-compose / Caddyfile / .github/workflows/ — это работа devops-engineer
|
||||
|
|
|
|||
|
|
@ -108,10 +108,10 @@ mcp__obsidian__obsidian_simple_search "<keyword>"
|
|||
- Time spent: ~3 min
|
||||
|
||||
### Critical issues (BLOCK push)
|
||||
- [ ] `file.py:42` — конкретный failure scenario (input/state → неверный output/crash) + fix suggestion. Без repro-сценария — не критикал, переквалифицируй в Minor или Positive observation.
|
||||
- [ ] `file.py:42` — описание проблемы + fix suggestion
|
||||
|
||||
### Minor issues (можно fix потом)
|
||||
- [ ] `file.py:84` — улучшение (не rubber-stamp: если это не влияет на поведение — Positive observations или пропусти)
|
||||
- [ ] `file.py:84` — улучшение
|
||||
|
||||
### Positive observations
|
||||
- ✅ Что сделано хорошо
|
||||
|
|
@ -146,7 +146,7 @@ mcp__obsidian__obsidian_simple_search "<keyword>"
|
|||
- Удаление prod данных без явного approval
|
||||
- `--no-verify` / `--force` / `--amend` в push
|
||||
- **Прямой push в main** — нарушение PR workflow (см. CLAUDE.md). Должен быть feature branch + PR.
|
||||
- **Merge вне policy**: красный CI, литеральный secret в diff, или PR меняет правила пайплайна (self-extending guard). Self-merge при зелёном CI разрешён с 2026-06-27 (git-pr.md § Auto-merge policy) — сам по себе НЕ блокер.
|
||||
- **Merge PR без user approval** — нарушение PR workflow.
|
||||
|
||||
### Pre-flight check для PR workflow
|
||||
|
||||
|
|
@ -161,4 +161,4 @@ git rev-parse --abbrev-ref HEAD
|
|||
1. `git stash` или backup commit'ов
|
||||
2. Создать ветку `git checkout -b feat/foo`
|
||||
3. Перенести коммиты
|
||||
4. `git push -u forgejo feat/foo` + `mcp__forgejo__create_pull_request` (gh CLI bypassed 2026-05-16)
|
||||
4. `git push -u origin feat/foo` + `gh pr create`
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Pre-loaded context: смотри vault через `mcp__obsidian__*` перед
|
|||
- PostgreSQL 16 + PostGIS 3.4
|
||||
- Партиционирование: `rosreestr_deals` (9 partitions, 2024Q1—2026Q1, ~7M rows)
|
||||
- GIST индексы на geom-полях
|
||||
- Фактический канал prod-миграций = `data/sql/NN_*.sql` + deploy (auto-apply на проде); Alembic (`backend/alembic/versions/`) — legacy/локально
|
||||
- Alembic для prod schema changes (`backend/alembic/versions/`)
|
||||
- Raw SQL artifacts в `data/sql/NN_xxx.sql` для больших миграций / views / bootstrap
|
||||
- psycopg v3 на стороне backend (НЕ psycopg2)
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ Pre-loaded context: смотри vault через `mcp__obsidian__*` перед
|
|||
5. **Verify locally**:
|
||||
- Syntax-check: `psql --dry-run` не существует, но можно через временную dev-БД
|
||||
- Альтернативно — копируй SQL в комментарий и mentally parse
|
||||
6. **Apply**: prod schema changes ТОЛЬКО через `data/sql/NN_*.sql` + deploy (auto-apply на проде); `mcp__postgres-gendesign__execute_sql` — только read-only verify (или явно одобренная user'ом ручная операция)
|
||||
6. **Apply** через `mcp__postgres-gendesign__execute_sql` (требуется SSH tunnel up + user approval для drops/alters на проде)
|
||||
7. **Verify post-apply**:
|
||||
- `information_schema.columns` для column changes
|
||||
- `pg_indexes` / `pg_views` для indexes/views (если разрешено читать pg_*)
|
||||
|
|
@ -92,5 +92,5 @@ Pre-loaded context: смотри vault через `mcp__obsidian__*` перед
|
|||
- ❌ Изменять Alembic version files задним числом — только новые revisions
|
||||
- ❌ Запускать миграцию на проде без backup confirmation (если она destructive)
|
||||
- ❌ Использовать `psycopg2` в коде (только v3)
|
||||
- ❌ Commit'ить сам — оставь staged; коммитит main-сессия (agent-first pipeline)
|
||||
- ❌ Commit'ить сам — пиши commit message в чат
|
||||
- ❌ Писать knowledge в `memory/memory-gendesign.jsonl` (deprecated) — только Obsidian vault
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ Short skeleton:
|
|||
|
||||
## Forgejo API conventions
|
||||
|
||||
- `$FORGEJO_URL` = `https://git.gendsgn.ru`, токен — `FORGEJO_ACCESS_TOKEN` / `FORGEJO_TOKEN_<ROLE>` из Windows User-scope env vars (выставляются ДО запуска claude; см. `_autonomous_pickup.md`)
|
||||
- `$FORGEJO_URL` = `https://git.gendsgn.ru`, `$FORGEJO_TOKEN` = в env (из `~/.claude/settings.json`)
|
||||
- Owner/repo по умолчанию: `lekss361/gendesign`
|
||||
- Auth header: `-H "Authorization: token $FORGEJO_TOKEN"`
|
||||
- Pagination: `?page=1&limit=50` (max 50 на странице)
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
## Auto-merge policy (PR review mode)
|
||||
|
||||
При review открытых Forgejo PR — если verdict **✅ APPROVE** (нет 🔴/🟠/🟡):
|
||||
**мержи сам, любой scope** (политика 2026-06-27, git-pr.md § Auto-merge policy).
|
||||
**мержи сам, любой scope** (user override 2026-05-16: «после аппрув в ревью можешь мерджить все что угодно»).
|
||||
|
||||
### Pre-merge checks (все обязательны)
|
||||
|
||||
|
|
@ -123,4 +123,3 @@ Forgejo API возвращает пустой body при успехе merge →
|
|||
- CI failing → comment "approved but CI red — wait for green"
|
||||
- Draft PR → comment "approved, ready when undrafted"
|
||||
- Head SHA changed после твоего scan'а → НЕ мержь stale verdict, re-review нужен
|
||||
- Diff меняет правила пайплайна: git-pr.md § Auto-merge policy, CLAUDE.md Critical rules, `_autonomous_pickup.md`, `auto-code-reviewer.md`, любой `work-as-*.md` → НЕ merge, label `needs-human` (self-extending guard)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: devops-engineer
|
||||
description: DevOps engineer for GenDesign — Docker, docker-compose, Caddyfile, Forgejo Actions / GitHub Actions workflows, SSH deploy, CouchDB stack, Obsidian LiveSync infra. Use proactively for any work in `docker-compose*.yml`, `Caddyfile`, `.forgejo/workflows/**`, `.github/workflows/**`, `scripts/setup-*.sh`, `ops/`, or SSH-deploy issues. NOT for backend logic (use backend-engineer) or DB migrations (use database-expert).
|
||||
description: DevOps engineer for GenDesign — Docker, docker-compose, Caddyfile, GitHub Actions workflows, SSH deploy, CouchDB stack, Obsidian LiveSync infra. Use proactively for any work in `docker-compose*.yml`, `Caddyfile`, `.github/workflows/`, `scripts/setup-*.sh`, `ops/`, or SSH-deploy issues. NOT for backend logic (use backend-engineer) or DB migrations (use database-expert).
|
||||
tools: Read, Write, Edit, Glob, Grep, Bash, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_complex_search, mcp__obsidian__obsidian_get_file_contents, mcp__obsidian__obsidian_batch_get_file_contents, mcp__obsidian__obsidian_list_files_in_dir, mcp__obsidian__obsidian_append_content, mcp__obsidian__obsidian_patch_content
|
||||
model: sonnet
|
||||
color: orange
|
||||
|
|
@ -24,7 +24,7 @@ Pre-loaded context: смотри vault через `mcp__obsidian__*` перед
|
|||
- Beget VPS, Москва, 2 vCPU / 4 GB / 40 GB NVMe
|
||||
- Docker + docker-compose (два стека: main + obsidian)
|
||||
- Caddy 2 (auto-TLS Let's Encrypt)
|
||||
- Forgejo Actions — фактический CI/deploy (`.forgejo/workflows/**`; build → GHCR → SSH → compose up); legacy GitHub Actions в `.github/workflows/**`
|
||||
- GitHub Actions (build → GHCR → SSH → compose up)
|
||||
- Docker images: backend (lean), worker (with-chromium), frontend, couchdb (вешний)
|
||||
- Shared external network `gendesign_shared` для связи main↔obsidian стеков
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ Pre-loaded context: смотри vault через `mcp__obsidian__*` перед
|
|||
|
||||
## Запреты
|
||||
|
||||
- ❌ Не коммить сам — оставь staged; коммитит main-сессия (agent-first pipeline)
|
||||
- ❌ Не коммить сам — пиши commit message в чат
|
||||
- ❌ Не push в main с `--force` (никогда)
|
||||
- ❌ Не редактировать `backend/`/`frontend/` исходники (только infra-конфиги)
|
||||
- ❌ Не выполнять prod SSH без явного approval пользователя (читать prod-логи — отдельно safety guard, нужен approval)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ Pre-loaded context: смотри vault через `mcp__obsidian__*` перед
|
|||
|
||||
## Запреты
|
||||
|
||||
- ❌ Не коммить сам — оставь staged; коммитит main-сессия (agent-first pipeline)
|
||||
- ❌ Не коммить сам — пиши commit message в чат
|
||||
- ❌ Не редактировать `backend/` — это работа backend-engineer
|
||||
- ❌ Не использовать `any`
|
||||
- ❌ Не использовать pages router (`src/pages/`) — только app router (`src/app/`)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ POST-комменты — `--data-binary @file` (не inline `-d`: Windows ср
|
|||
|
||||
→ определи changed paths:
|
||||
- `backend/app/api/v1/<X>.py` → curl endpoint этого роутера
|
||||
- `frontend/src/app/**` → playwright browser_navigate + browser_snapshot
|
||||
- `frontend/src/app/**` → chrome-devtools navigate + snapshot
|
||||
- `data/sql/NN_*.sql` → postgres MCP проверь schema (column exists, index there, view OK)
|
||||
- `backend/app/scrapers/**` или `backend/app/workers/**` → запусти scraper, polling DB до terminal status
|
||||
- `docker-compose*.yml` / `Caddyfile` → smoke production URLs + проверь containers running
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name: tech-analyst
|
||||
description: 'Tech analyst / planner для GenDesign. Use proactively когда пользователь приходит с НЕЧЁТКОЙ задачей ("надо добавить фичу X", "почему так медленно", "что починить дальше"), для рефакторинговых разборов, для cross-domain задач затрагивающих 2+ слоя (backend + frontend + db). Read-only — НЕ пишет код. Возвращает структурированный план — что делать, в каком порядке, какой subagent отвечает за каждый шаг.'
|
||||
tools: Read, Glob, Grep, WebSearch, WebFetch, mcp__obsidian__obsidian_simple_search, mcp__obsidian__obsidian_complex_search, mcp__obsidian__obsidian_get_file_contents, mcp__obsidian__obsidian_batch_get_file_contents, mcp__obsidian__obsidian_list_files_in_dir, mcp__obsidian__obsidian_get_recent_changes, mcp__postgres-gendesign__list_objects, mcp__postgres-gendesign__get_object_details, mcp__postgres-gendesign__list_schemas, mcp__postgres-gendesign__explain_query, mcp__postgres-gendesign__analyze_query_indexes, mcp__postgres-gendesign__analyze_db_health, mcp__postgres-gendesign__get_top_queries, mcp__postgres-tradein__list_objects, mcp__postgres-tradein__get_object_details, mcp__postgres-tradein__list_schemas, mcp__postgres-tradein__explain_query, Bash
|
||||
model: sonnet
|
||||
model: haiku
|
||||
color: yellow
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Write-Host "✓ PAT belongs to $($me.login) — analyst persona ready"
|
|||
|
||||
Следую правилам из `.claude/agents/auto-analyst.md` + `.claude/agents/_autonomous_pickup.md`.
|
||||
|
||||
**Что делаю каждый /loop tick (15m):**
|
||||
**Что делаю каждый /loop tick (30m):**
|
||||
|
||||
1. Kill-switch check (label `pause-bots` на repo)
|
||||
2. Read новые commits + vault inbox + closed-since-last-tick Forgejo issues
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ curl -sH "Authorization: token $FORGEJO_TOKEN" "$FORGEJO_URL/api/v1/user/tokens"
|
|||
|
||||
Следую правилам из `.claude/agents/auto-code-reviewer.md` + `_autonomous_pickup.md` + `.claude/agents/code-reviewer.md` + `.claude/rules/git-pr.md`.
|
||||
|
||||
**Per-tick workflow (2m):**
|
||||
**Per-tick workflow (5m):**
|
||||
|
||||
1. Kill-switch check
|
||||
2. GET pulls `status/review` без approve, oldest first, limit=1
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"postgres-gendesign": {
|
||||
"type": "stdio",
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "-e", "DATABASE_URI", "crystaldba/postgres-mcp", "--access-mode=restricted"],
|
||||
"args": ["run", "-i", "--rm", "-e", "DATABASE_URI", "crystaldba/postgres-mcp", "--access-mode=unrestricted"],
|
||||
"env": { "DATABASE_URI": "${GENDESIGN_DB_URI}" }
|
||||
},
|
||||
"playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest", "--cdp-endpoint=http://localhost:9222"], "alwaysLoad": true },
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"postgres-gendesign": {
|
||||
"type": "stdio",
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "-e", "DATABASE_URI", "crystaldba/postgres-mcp", "--access-mode=restricted"],
|
||||
"args": ["run", "-i", "--rm", "-e", "DATABASE_URI", "crystaldba/postgres-mcp", "--access-mode=unrestricted"],
|
||||
"env": { "DATABASE_URI": "${GENDESIGN_DB_URI}" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
paths:
|
||||
- backend/**/*.py
|
||||
- tradein-mvp/backend/**/*.py
|
||||
paths: backend/**/*.py
|
||||
---
|
||||
|
||||
# Backend conventions — Python 3.12 / FastAPI
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
# Delegation & task sizing
|
||||
|
||||
> Без `paths:` — загружается в каждой сессии. Канон лимитов делегирования (портировано из memory-фидбека 2026-06-27, routing/effort — из верифицированного ресёрча 2026-07-02).
|
||||
|
||||
## Routing (кому отдавать)
|
||||
|
||||
| Задача | Агент |
|
||||
|---|---|
|
||||
| Разведка: найти файлы / понять структуру | **Explore** (Haiku, read-only, дёшев) — НЕ general-purpose |
|
||||
| Спроектировать подход | **Plan** |
|
||||
| Код | доменный worker (backend/frontend/database/devops) + `isolation: "worktree"` |
|
||||
| Проверить результат | отдельный агент **в fresh context** — видит только diff и критерии, без bias автора |
|
||||
|
||||
- Продолжение работы существующего агента → `SendMessage` по agentId (контекст цел), НЕ новый спавн с пересказом.
|
||||
- Масштаб пачки: 1-3 независимых → параллельные Agent-вызовы одним сообщением; конвейер / 10+ агентов → Workflow (pipeline-default, `schema`-выход, adversarial verify находок). Caps на выборках — логируй отброшенное, молчаливое усечение читается как «покрыто всё».
|
||||
|
||||
## Бюджет одного сабагента (hard limits)
|
||||
|
||||
- **1 сабагент = 1 узкий deliverable.** Ориентиры: ~≤10 мин работы, ~≤20 tool-calls, ~≤150k токенов.
|
||||
Эмпирика 2026-06-27: агент-аудитор на 186k tok / 33 calls упал на StructuredOutput; 5 мелких параллельных прошли.
|
||||
- Поверхность больше бюджета → **дели на N узких сабагентов** (parallel при непересекающихся файлах, sequential при зависимостях). НЕ один большой.
|
||||
- Промпт сабагенту: конкретный deliverable + формат ответа + границы («что НЕ делать»). Расплывчатый scope = дубли и мусор.
|
||||
- Windows: очень длинный промпт субагенту может упасть на лимите командной строки (~8191 символ) — ещё один довод за компактность.
|
||||
|
||||
## Эскалация oversized-задачи (worker)
|
||||
|
||||
Issue/задача выглядит больше одного захода (эвристика: >5 файлов, ИЛИ >500 строк diff, ИЛИ >2ч) → **НЕ исполнять целиком**:
|
||||
- bot-pipeline: комментарий с планом сплита + label `status/needs-analysis`, снять claim
|
||||
- interactive: вернуть main-сессии план сплита вместо результата
|
||||
|
||||
## Единые пороги дробления (analyst / main)
|
||||
|
||||
- Estimate S(<2h) / M(2-8h) / **L(>8h) → обязан дробиться дальше** (до S/M)
|
||||
- Issue ≥1.5 дня → 3-4 sub-PR (Foundation → Schema → Workers → Integration), каждый ~200-500 строк — см. git-pr.md § Split big issues
|
||||
- 1 sub-issue ≈ 1-2 worker-захода, single-scope (не смешивать backend+frontend в одном issue)
|
||||
|
||||
## Параллелизм
|
||||
|
||||
Default = parallel на непересекающихся файлах (per-task worktree). Sequential — только overlap / hot-files / зависимый стек (git-pr.md § Parallel vs sequential PRs).
|
||||
|
||||
**Усилие ∝ сложности**: простой факт-запрос = 1 агент / 3-10 tool-calls; сложный разбор = N узких параллельных. Ширина пачки дешева, толщина одного агента — дорога и хрупка. Параллельные сессии/окна: практический потолок 3-5 (bottleneck — review, не Claude).
|
||||
|
||||
## Effort / model per agent
|
||||
|
||||
- Наследовать по умолчанию; модель НЕ переопределять без нужды (Explore и так на Haiku).
|
||||
- `effort: low/medium` — механика: точечные правки по списку, сбор данных, mass-grep, формат-конверсии.
|
||||
- `effort: high+` — только verify/judge-этапы, архитектурный синтез, решения.
|
||||
- Циклы/поллинг: prompt-cache живёт 5 мин — тик либо <~4.5 мин (кэш тёплый), либо сразу 20-30 мин; интервалы 5-15 мин = worst case (полный re-read контекста каждый тик без амортизации).
|
||||
|
|
@ -23,11 +23,10 @@ paths:
|
|||
|
||||
Reference incident: PR #346 (2026-05-18) deploy → user сам нашёл prod 500 на by-bbox, потом 400 на analyze, потом TypeError на poi-score, потом UI overlap. Каждое ловилось бы playwright smoke по `/site-finder/analysis/{cad}`.
|
||||
|
||||
## Path triggers (Forgejo Actions, `.forgejo/workflows/`)
|
||||
## GHA path triggers
|
||||
|
||||
- `backend/**`, `frontend/**`, `Caddyfile`, `caddy/**`, `docker-compose.prod.yml`, `data/sql/**`, `ops/glitchtip-auth-forwarder/**`, `.forgejo/workflows/deploy.yml` → `deploy.yml` (main Site Finder stack)
|
||||
- trade-in изменения → `deploy-tradein.yml` (отдельный stack; paths-filter base = last deployed SHA → накопленный diff, fail-safe build-all)
|
||||
- `docker-compose.obsidian.yml`, `scripts/setup-couchdb.sh`, `docs/obsidian-livesync.md` → `.forgejo/workflows/deploy-obsidian.yml`
|
||||
- `backend/**`, `frontend/**`, `Caddyfile`, `docker-compose.prod.yml`, `data/sql/*.sql` → `deploy.yml` (main stack)
|
||||
- `docker-compose.obsidian.yml`, `scripts/setup-couchdb.sh` → `deploy-obsidian.yml`
|
||||
- `docs/**` alone → НЕ триггерит деплой
|
||||
|
||||
## После изменения .env на VPS
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
paths:
|
||||
- frontend/**/*.{ts,tsx,jsx,js}
|
||||
- tradein-mvp/frontend/**/*.{ts,tsx,jsx,js}
|
||||
paths: frontend/**/*.{ts,tsx,jsx,js}
|
||||
---
|
||||
|
||||
# Frontend conventions — Next.js 15 / React 19 / TypeScript 5
|
||||
|
|
@ -32,7 +30,7 @@ Reference: vault `Bug_RenderMarkdown_JavascriptUrl_May14` (`renderMarkdown.ts:sa
|
|||
cd frontend && npm run codegen
|
||||
```
|
||||
|
||||
Обновляет `src/lib/api-types.ts` из live OpenAPI (`npm run codegen` = `openapi-typescript … -o src/lib/api-types.ts`). Без этого frontend build упадёт на missing types.
|
||||
Обновляет `src/types/openapi.ts` из live OpenAPI. Без этого frontend build упадёт на missing types.
|
||||
|
||||
## package.json + lockfile sync (CRITICAL — deploy aborts on mismatch)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ paths:
|
|||
```
|
||||
1. claude --bg --name "feat-<scope>" "task description"
|
||||
→ supervisor создаёт worktree от forgejo/main автоматически
|
||||
(settings worktree.baseRef=fresh, bgIsolation=worktree)
|
||||
(settings worktree.baseRef=default, bgIsolation=git)
|
||||
2. Session работает: код → commit → push → mcp__forgejo__create_pull_request
|
||||
3. PR URL появляется в agent view как row с зелёной/жёлтой/красной ● status dot
|
||||
4. User делает review через external Claude window (post-push)
|
||||
5. После merge — `Ctrl+X два раза` в agent view = удаление session + worktree
|
||||
5. После merge — `Ctrl+X dwa раза` в agent view = удаление session + worktree
|
||||
```
|
||||
|
||||
**Foreground workflow** (когда нужен интерактив, ad-hoc fixes):
|
||||
|
|
@ -45,8 +45,8 @@ paths:
|
|||
- **Imperative**: "fix crash" не "fixed crash"
|
||||
- **Body — почему**, не что (что видно в diff)
|
||||
- **NO `Co-Authored-By: Claude ...`** — никогда (~/.claude/CLAUDE.md rule)
|
||||
- Workers (subagents) оставляют staged — main session коммитит
|
||||
- **GenDesign (Mera/Ptica) = full self-service:** main/solo session коммитит → пушит → PR → **merge сам** (см. § Auto-merge policy). `balance_platform` — наоборот: только stage, commit message в чат, не коммитить/пушить/мержить
|
||||
- Workers оставляют staged — main session коммитит
|
||||
- **No auto-commit**: написать commit message в чат, user решает когда коммитить
|
||||
|
||||
## PR body template
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ Closes #N
|
|||
|
||||
1. `mcp__forgejo__get_pull_request` (или `curl -sH "$H" "$REPO/pulls/<N>"`) → читай `state`, `mergeable`, `head.sha`
|
||||
2. `state == merged` → stop polling
|
||||
3. Новый review/comment: `mcp__forgejo__list_pull_reviews` / `list_issue_comments`. Парсь marker `<!-- gendesign-review-bot: sha=<sha7> verdict=<approve|changes> -->`
|
||||
3. Новый review/comment: `mcp__forgejo__list_pr_reviews` / `list_issue_comments`. Парсь marker `<!-- gendesign-review-bot: sha=<sha7> verdict=<approve|changes> -->`
|
||||
- **SHA guard**: `marker.sha7 == head.sha[:7]` — иначе устаревший approval до fixup-push, игнорируй
|
||||
- `verdict=approve` + SHA match → `mcp__forgejo__merge_pull_request` (squash + delete branch)
|
||||
- `verdict=changes` → fixup commits + push в `forgejo feat/<scope>` + re-poll
|
||||
|
|
@ -85,26 +85,22 @@ Closes #N
|
|||
|
||||
## Auto-merge policy
|
||||
|
||||
**Self-merge разрешён (2026-06-27, Mera/Ptica).** Любая GenDesign-сессия — solo/foreground ИЛИ bot-pipeline — мержит свой PR сама (любой scope), когда checks зелёные. В bot-pipeline review остаётся (reviewer-окно ставит `verdict=approve` + SHA match), но merge-authority больше **не** эксклюзив reviewer'а — worker может смержить approved PR сам. Pre-merge gate: зелёный CI + (в pipeline) approve+SHA match. `balance_platform` — никогда не мержит (stage only).
|
||||
**Любой scope** — bot мержит при `verdict=approve` + SHA match. Blocked-list снят 2026-05-16 ([Auto-merge any scope] memory rule).
|
||||
|
||||
**Жёсткие исключения (даже при зелёном — НЕ merge, ping human):**
|
||||
- Diff содержит литеральный secret/token/password/credential (40-char hex, API keys, JWT, и т.д.) — security tripwire.
|
||||
- PR меняет правила самого пайплайна: блок `## Auto-merge policy` здесь, `Critical rules` в CLAUDE.md, `_autonomous_pickup.md` (claim/kill-switch/merge-FSM), `auto-code-reviewer.md` или любой `work-as-*.md` — **self-extending guard** (расширение/снятие собственных merge-прав всегда через human, предотвращает bot-loop).
|
||||
**Жёсткие исключения** (даже при APPROVE — НЕ merge, ping user):
|
||||
- Diff содержит литеральный secret/token/password/credential (40-char hex, API keys, JWT, и т.д.) — security tripwire
|
||||
- PR меняет блок `## Auto-merge policy` в этом файле, `Critical workflow rules` в CLAUDE.md, `_autonomous_pickup.md` (claim/kill-switch/merge-FSM contract), `auto-code-reviewer.md` или любой `work-as-*.md` (self-extending guard, decided 2026-05-24, расширен 2026-05-29 — изменение правил пайплайна всегда через human, предотвращает bot-loop где bot сам расширяет свои merge права)
|
||||
|
||||
## Parallel vs sequential PRs
|
||||
## Sequential PRs
|
||||
|
||||
**Default = параллельно**, если scope'ы НЕ пересекаются по файлам: каждая задача в своём worktree (`git worktree add` / isolation:"worktree"), своя ветка, свой PR.
|
||||
**Одна задача → один PR → merge → следующая.** Параллельно ТОЛЬКО если scope'ы строго orthogonal (разные файлы).
|
||||
|
||||
**Sequential обязателен когда:**
|
||||
- Пересечение файлов / hot-files: `backend/app/api/v1/parcels.py`, `frontend/src/types/site-finder.ts`, OverviewTab/LandTab/MarketTab
|
||||
- Зависимый стек sub-PR'ов (Foundation → Schema → Workers → Integration): PR N+1 только после merge PR N
|
||||
Опасные файлы для конфликтов: `backend/app/api/v1/parcels.py`, `frontend/src/types/site-finder.ts`, OverviewTab/LandTab/MarketTab.
|
||||
|
||||
## Split big issues
|
||||
|
||||
Issues ≥ 1.5 day → 3-4 sub-PRs: **Foundation → Schema → Workers → Integration**. Каждый ~200-500 lines. PR N+1 только после merge PR N.
|
||||
|
||||
Бюджет одного subagent-захода и правила эскалации oversized-задач: `.claude/rules/delegation.md`.
|
||||
|
||||
## Review workflow (no conflict)
|
||||
|
||||
- **Pre-push** (локально): spawn `code-reviewer` subagent на staged changes → lint pass (security, correctness, conventions). Блокирует push при 🔴 критикал.
|
||||
|
|
@ -116,18 +112,18 @@ Issues ≥ 1.5 day → 3-4 sub-PRs: **Foundation → Schema → Workers → Inte
|
|||
|
||||
- **Параллелизм**: 3-5 sessions одновременно (>5 = bottleneck на review, не на Claude per Anthropic метрика)
|
||||
- **Pin (`Ctrl+T`)** для long-running sessions (scraper monitors, deploy watchers) — supervisor не убьёт через 1h idle
|
||||
- **Cleanup**: `Ctrl+X два раза` после merge → session + worktree удалены атомарно
|
||||
- **NEVER** parallel sessions на one file — каждая ест в own worktree, last-merge wins (см. § Parallel vs sequential PRs)
|
||||
- **Cleanup**: `Ctrl+X dwa раза` после merge → session + worktree удалены атомарно
|
||||
- **NEVER** parallel sessions на one file — каждая ест в own worktree, last-merge wins (используй sequential PR rule выше)
|
||||
|
||||
**Manual `git worktree add` deprecated** — используй `claude --bg --name X`, supervisor сам isolation делает с `baseRef: fresh` (свежая ветка от main, не от твоей stale-сессии).
|
||||
**Manual `git worktree add` deprecated** — используй `claude --bg --name X`, supervisor сам isolation делает с `baseRef: default` (свежая ветка от main, не от твоей stale-сессии).
|
||||
|
||||
**Worktree cleanup** (cron weekly): `scripts/cleanup-merged-worktrees.sh` — удаляет worktrees для merged branches. Запускать вручную или weekly cron.
|
||||
**Worktree cleanup** (cron weekly): `scripts/cleanup-merged-worktrees.sh` — удаляет worktrees для merged branches. Запускать вручную или weekly cron.
|
||||
|
||||
## Запреты
|
||||
|
||||
- ❌ `git push forgejo main` / direct push в main
|
||||
- ❌ merge PR с литеральным secret в diff ИЛИ PR меняющий правила пайплайна (self-extending guard) — это через human. Иначе self-merge OK (зелёный CI; в pipeline дополнительно approve+SHA)
|
||||
- ❌ `mcp__forgejo__merge_pull_request` без approval (human "merge it" или bot verdict=approve + SHA match)
|
||||
- ❌ `gh pr *` — bypassed 2026-05-16, используй Forgejo MCP или curl + `$FORGEJO_TOKEN`
|
||||
- ❌ `--no-verify` / `--amend` / `--no-edit` / `--force` без явного approval
|
||||
- ❌ `@claude` в PR comments — plain text only (`feedback_no_claude_mentions`)
|
||||
- ❌ Параллельные PR на одни файлы / hot-files (см. § Parallel vs sequential PRs)
|
||||
- ❌ Параллельные PR на одни файлы (`feedback_sequential_prs`)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
---
|
||||
paths:
|
||||
- data/sql/**/*.sql
|
||||
- tradein-mvp/backend/data/sql/**/*.sql
|
||||
paths: data/sql/**/*.sql
|
||||
---
|
||||
|
||||
# SQL conventions — PostgreSQL 16 / PostGIS 3.4
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
paths:
|
||||
- tradein-mvp/**/*.py
|
||||
- tradein-mvp/**/*.sql
|
||||
- tradein-mvp/frontend/**/*.{ts,tsx}
|
||||
---
|
||||
|
||||
# trade-in (Mera) conventions — `tradein-mvp/`
|
||||
|
||||
Отдельный продукт + отдельный стек от Site Finder. Backend `tradein-mvp/backend/app/**`,
|
||||
SQL `tradein-mvp/backend/data/sql/NN_*.sql`, frontend `tradein-mvp/frontend/`. Backend Python
|
||||
подчиняется `.claude/rules/backend.md` (psycopg v3, CAST, ruff-100), SQL — `.claude/rules/sql.md`
|
||||
(NN naming, idempotency). Ниже — то, что СПЕЦИФИЧНО для trade-in.
|
||||
|
||||
## Две БД — не путай
|
||||
|
||||
- **`postgres-tradein`** (db=tradein) — скрейпленные листинги avito/cian/yandex, estimator,
|
||||
coverage, houses. Для ЛЮБОЙ tradein-задачи метрики/схему бери отсюда (`mcp__postgres-tradein__*`).
|
||||
- **`postgres-gendesign`** (db=gendesign) — Site Finder, НЕ trade-in.
|
||||
|
||||
## Тестировать HTTP только ВНУТРИ контейнера
|
||||
|
||||
SSH-туннель `localhost:8000` → `gendesign-backend` (Site Finder, db=gendesign, старый код),
|
||||
**НЕ** tradein-backend (порт не опубликован на хост). curl на туннель:8000 по trade-in endpoint =
|
||||
мусор / чужая БД (стоило ~2ч). Тест trade-in API только изнутри контейнера:
|
||||
|
||||
```bash
|
||||
ssh gendesign # затем:
|
||||
docker exec tradein-backend curl -s localhost:8000/<route> # админ-роуты: -H "X-Authenticated-User: admin"
|
||||
docker exec tradein-postgres psql -U <user> -d tradein -c "..."
|
||||
```
|
||||
|
||||
## Scheduler крутится в `tradein-scraper`, не `tradein-backend`
|
||||
|
||||
In-app scheduler (`scrape_schedules`, tick 60s, `python -m app.scheduler_main`,
|
||||
`SCHEDULER_ENABLE=true`) живёт в контейнере **`tradein-scraper`**; в `tradein-backend` намеренно
|
||||
`false`. Статус scheduled-задач смотри в scraper-контейнере (logs/printenv), не в backend.
|
||||
Ручной smoke: `UPDATE scrape_schedules SET next_run_at=now() WHERE source='X'` → подхват ≤60s.
|
||||
|
||||
## SQL авто-применяется на ПРОД (strict)
|
||||
|
||||
`tradein-mvp/backend/data/sql/NN_*.sql` применяется автоматически на деплое через `_schema_migrations`
|
||||
в `.forgejo/workflows/deploy-tradein.yml` (НЕ init-only, strict exit-1). Idempotency критична —
|
||||
деструктивный DDL хитит прод на деплое. NN-нумерация уже 3-значная и ИМЕЕТ коллизии (`108_*` ×2,
|
||||
`084_*` ×2) → перед новым файлом `ls tradein-mvp/backend/data/sql | grep '^NN'` на дубль basename,
|
||||
не доверяй `tail`.
|
||||
|
||||
## Rapid-merge trap
|
||||
|
||||
2 tradein-PR мержа за секунды → backend `test`-job cancelled → `build-backend` пропущен →
|
||||
«deploy success» на СТАРОМ образе (нет нового кода/deps). Сверяй `:latest` Created-timestamp vs
|
||||
время мержа + smoke в контейнере; не верь «деплой прошёл». Recovery: ручной `workflow_dispatch`
|
||||
для `deploy-tradein.yml`.
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__obsidian__obsidian_list_files_in_vault",
|
||||
"mcp__obsidian__obsidian_simple_search",
|
||||
"mcp__obsidian__obsidian_get_file_contents",
|
||||
"mcp__obsidian__obsidian_list_files_in_dir",
|
||||
"mcp__postgres-gendesign__get_object_details",
|
||||
"mcp__postgres-gendesign__explain_query",
|
||||
"mcp__postgres-gendesign__list_objects",
|
||||
"mcp__postgres-gendesign__list_schemas",
|
||||
"mcp__postgres-gendesign__analyze_query_indexes",
|
||||
"Bash(curl *)",
|
||||
"Bash(powershell *)",
|
||||
"Bash(gh pr comment:*)",
|
||||
"Bash(powershell.exe:*)",
|
||||
"Bash(powershell:*)",
|
||||
"Bash(pwsh:*)",
|
||||
"mcp__forgejo",
|
||||
"mcp__forgejo__create_issue",
|
||||
"mcp__forgejo__update_issue",
|
||||
"mcp__forgejo__add_issue_labels",
|
||||
"mcp__forgejo__remove_issue_labels",
|
||||
"mcp__forgejo__issue_state_change",
|
||||
"mcp__forgejo__create_issue_comment",
|
||||
"mcp__forgejo__create_pull_request",
|
||||
"mcp__forgejo__merge_pull_request",
|
||||
"mcp__forgejo__create_pull_review",
|
||||
"mcp__forgejo__get_pull_request_diff",
|
||||
"mcp__forgejo__list_repo_issues",
|
||||
"mcp__forgejo__list_repo_pull_requests",
|
||||
"mcp__forgejo__get_pull_request_by_index",
|
||||
"mcp__forgejo__list_pull_request_files",
|
||||
"mcp__forgejo__list_pull_reviews",
|
||||
"mcp__forgejo__list_repo_labels",
|
||||
"mcp__forgejo__get_issue_by_index",
|
||||
"mcp__forgejo__list_issue_comments",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Read",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Task",
|
||||
"TodoWrite",
|
||||
"EnterWorktree",
|
||||
"WebFetch",
|
||||
"WebSearch",
|
||||
"NotebookEdit",
|
||||
"mcp__obsidian",
|
||||
"mcp__postgres-gendesign",
|
||||
"mcp__playwright",
|
||||
"mcp__a11y",
|
||||
"mcp__lighthouse",
|
||||
"mcp__shadcn",
|
||||
"mcp__glitchtip",
|
||||
"mcp__context7",
|
||||
"mcp__fetch",
|
||||
"Bash(*)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf *)",
|
||||
"Bash(rm -rf /*)",
|
||||
"Bash(git push --force *)",
|
||||
"Bash(git push -f *)",
|
||||
"Bash(git reset --hard *)",
|
||||
"Bash(git commit --amend *)",
|
||||
"Bash(git rebase --interactive *)",
|
||||
"Bash(git commit --no-verify*)",
|
||||
"Bash(git push --no-verify*)",
|
||||
"Bash(git push --force-with-lease*)",
|
||||
"Bash(git push --force-if-includes*)",
|
||||
"Bash(docker compose down -v *)",
|
||||
"Bash(docker volume rm *)",
|
||||
"Read(./.env)",
|
||||
"Read(./.env.*)",
|
||||
"Read(./backend/.env)",
|
||||
"Read(./backend/.env.*)",
|
||||
"Read(**/.env)",
|
||||
"Read(**/.env.*)"
|
||||
],
|
||||
"defaultMode": "auto"
|
||||
},
|
||||
"enabledMcpjsonServers": [
|
||||
"obsidian",
|
||||
"context7",
|
||||
"fetch",
|
||||
"forgejo",
|
||||
"a11y",
|
||||
"lighthouse",
|
||||
"shadcn"
|
||||
],
|
||||
"worktree": {
|
||||
"baseRef": "fresh",
|
||||
"bgIsolation": "worktree"
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python ${CLAUDE_PROJECT_DIR}/scripts/claude-hooks/check-secret-read.py"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python ${CLAUDE_PROJECT_DIR}/scripts/claude-hooks/check-dangerous-commands.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python ${CLAUDE_PROJECT_DIR}/scripts/claude-hooks/check-no-print.py"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python ${CLAUDE_PROJECT_DIR}/scripts/claude-hooks/check-sql-pitfalls.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python ${CLAUDE_PROJECT_DIR}/scripts/claude-hooks/session-preflight.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "python ${CLAUDE_PROJECT_DIR}/scripts/claude-hooks/statusline.py"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# Preview authoring guide (design-sync, tradein-mvp-frontend)
|
||||
|
||||
You author `.design-sync/previews/<Name>.tsx` for assigned components so their preview cards
|
||||
render real, styled, plausible compositions. The bundle is already built; you only recompile
|
||||
your own previews.
|
||||
|
||||
## Import contract (IMPORTANT)
|
||||
- Import the component from the package name: `import { OfferCard } from 'tradein-mvp-frontend';`
|
||||
(the build maps this to the shipped bundle global — do NOT import from a relative src path).
|
||||
- Import realistic data from the shared fixtures: `import { FIXTURE_ESTIMATE } from './_fixtures';`
|
||||
- Type-only imports (`import type {...} from '@/types/trade-in'`) are erased — fine to use for casts.
|
||||
- A provider is ALREADY configured globally (the app's `Providers` = TanStack Query). You do NOT
|
||||
wrap previews in QueryClientProvider yourself (a second copy breaks context identity).
|
||||
|
||||
## Shared fixtures available from `./_fixtures` (realistic ЕКБ 2-к secondary)
|
||||
- `FIXTURE_ESTIMATE: AggregatedEstimate` — median 9.85M ₽, range 9.1–10.6M, confidence high,
|
||||
analogs[3], actual_deals[2], cian_valuation, avito_imv, dkp_corridor, price_trend[6]. Covers any
|
||||
`estimate: AggregatedEstimate` prop.
|
||||
- `FIXTURE_INPUT: TradeInEstimateInput` — the quartira params (area 55.3, 2 rooms, floor 5/11, monolith, good).
|
||||
- `FIXTURE_IMV: IMVBenchmarkResponse` — for IMVBenchmark.
|
||||
- `FIXTURE_HOUSES: HouseInfoForEstimate[]` — for HouseInfoCard.
|
||||
- `FIXTURE_PLACEMENT: PlacementHistoryItem[]` — placement history rows.
|
||||
- `FIXTURE_ANALYTICS: HouseAnalyticsResponse` — kpi + price_history[4] + recent_sold[2].
|
||||
- `FIXTURE_SELLTIME: SellTimeSensitivityResponse` — exposure-vs-premium buckets.
|
||||
- `FIXTURE_SALES: SalesVsListingsResponse` — street deals/listings pairs.
|
||||
Read `tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx` — it shows EXACT prop usage for
|
||||
many components (the canonical "hero" composition). Port it.
|
||||
|
||||
## How to find props
|
||||
Read each component's source `tradein-mvp/frontend/src/components/<group>/<Name>.tsx`. The `interface Props`
|
||||
(or inline destructure) is the contract. Many take `estimate`; some take other fixtures; some take
|
||||
callbacks (pass `() => {}`); some fetch via hooks by id (see "fetch-coupled" below).
|
||||
|
||||
## Recipe
|
||||
- 1 canonical story (the docs/page.tsx usage) named `Default`. Add 1–3 more ONLY if they visibly
|
||||
differ (sweep a real variant axis: a state, an enum, empty-vs-full). Budget 1–4 cells.
|
||||
- Realistic content only (the ЕКБ fixtures) — never foo/test.
|
||||
- Callbacks → `() => {}`. Booleans like `isLoading`/`isPending` → usually `false` for the full state.
|
||||
- Keep visually-identical variants OUT (e.g. a flag that only changes a link, not the view) — one cell.
|
||||
|
||||
## Fetch-coupled components (PlacementHistoryCard, HouseAnalyticsSection, PhotoUpload, ListingsCard
|
||||
maybe) take an `estimateId` and fetch internally. The global Provider's query cache is EMPTY, so they
|
||||
render their loading/empty state. If the component accepts data via props too, prefer that. If it can
|
||||
ONLY fetch and renders blank/loading, author the best you can and RECORD it in your learnings file as
|
||||
"renders loading/empty — fetch-coupled" — do NOT fake data you can't pass.
|
||||
|
||||
## Your loop (per component)
|
||||
1. Read source → write `.design-sync/previews/<Name>.tsx` (named exports, no marker line).
|
||||
2. Rebuild ONLY your components:
|
||||
`node .ds-sync/lib/preview-rebuild.mjs --config .design-sync/config.json --node-modules tradein-mvp/frontend/node_modules --out ./ds-bundle --components <YOUR_COMMA_LIST>`
|
||||
3. Capture ONLY your components:
|
||||
`node .ds-sync/package-capture.mjs --out ./ds-bundle --components <YOUR_COMMA_LIST>`
|
||||
4. READ each `ds-bundle/_screenshots/review/<group>__<Name>.png`. Grade each cell on the absolute
|
||||
rubric: Styled (DS tokens/font visible) · Complete (renders whole, no missing children) · Plausible.
|
||||
5. Write `.design-sync/.cache/review/<Name>.grade.json`:
|
||||
`{"cells":{"<CellLabel>":{"verdict":"good"|"needs-work","note":"..."}}}` (keys = exact cell labels
|
||||
from the capture log). `needs-work` → fix the .tsx, rebuild, recapture, regrade until `good`.
|
||||
|
||||
## HARD RULES (violating corrupts other agents' work)
|
||||
- Edit ONLY your assigned `previews/<Name>.tsx`, your `.cache/review/<Name>.grade.json`, and your
|
||||
`.design-sync/learnings/<BATCH>.md`. NOTHING else. Never touch config.json / NOTES.md / other previews.
|
||||
- NEVER run `package-build.mjs` or `package-validate.mjs`. NEVER run `package-capture.mjs` without
|
||||
`--components` (a full run prunes other agents' state). Only the two scoped commands above.
|
||||
- If the SAME root cause hits 2+ of your components, or ANY config-level issue (a needed provider chain,
|
||||
a missing token/css, a needed `cardMode`/`viewport` override, an import that won't resolve) → STOP on
|
||||
those, record in your learnings file `.design-sync/learnings/<BATCH>.md`, and report to the orchestrator.
|
||||
Config/NOTES changes are orchestrator-only.
|
||||
|
||||
## Wide/overlay components
|
||||
Cards with `className="card"` are full-width and fine. If a card visibly overflows its grid cell or an
|
||||
overlay (dialog/map modal) collapses, that needs a `cfg.overrides.<Name>.cardMode` change — you CANNOT
|
||||
do that (config is orchestrator-only). Record it in learnings and move on.
|
||||
|
||||
## Calibration learnings from the solo set (HeroSummary, OfferCard, PriceRangeBar)
|
||||
- The contract works: `import { X } from 'tradein-mvp-frontend'` + `./_fixtures` + global Provider.
|
||||
- Cards render fully styled with the trade-in tokens/Manrope font.
|
||||
- Components with analog photos show a grey placeholder (fixture photo_url=null, offline) — that is the
|
||||
component's REAL no-photo state, grade it good (do not try to add external image URLs).
|
||||
- Drop variants that don't change the view (brandSlug, isResubmitting rendered identical → single cell).
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
# design-sync NOTES — tradein-mvp-frontend
|
||||
|
||||
Target: `tradein-mvp/frontend` (NOT the repo-root `frontend/`, which is the Site-Finder app). Project: gendesign-tradein.
|
||||
|
||||
## Build gotchas
|
||||
- **No dist / no Storybook → synth-entry mode.** The package is a Next.js app, not a lib; `package.json` has no `main`/`module`/`exports`.
|
||||
- **`--entry` must point at a NON-existent dist path** (`tradein-mvp/frontend/dist/index.js`). Two jobs:
|
||||
1. PKG_DIR resolution walks up `dirname(--entry)` to the real named `package.json` → PKG_DIR=`tradein-mvp/frontend` (without `--entry`, PKG_DIR defaults to `node_modules/<pkg>` which doesn't self-install → ENOENT crash).
|
||||
2. The path being absent makes `resolveDistEntry(soft)` return null → `synthEntry=true` → `deriveComponentsFromSrc` discovers components from `src/`. A path that EXISTS (e.g. package.json) suppresses synth mode → `[ZERO_MATCH]` tokens-only.
|
||||
- **`cfg.*` path fields are PACKAGE-relative** (relative to PKG_DIR), not repo-relative. srcDir=`src`, cssEntry=`src/components/trade-in/trade-in.css`, tokensGlob=`src/app/globals.css`.
|
||||
- `--node-modules tradein-mvp/frontend/node_modules` (react + @types/react live there).
|
||||
|
||||
## Styling
|
||||
- Tokens in `src/app/globals.css` (`:root{--bg-app,--accent,…}`); component CSS in `src/components/trade-in/trade-in.css` (2277 lines). Manrope via remote Google Fonts `@import` (→ `[FONT_REMOTE]`, no action).
|
||||
|
||||
## Re-sync risks
|
||||
- Synth scan over-includes non-component PascalCase exports (51 found vs ~37 files) — prune with `componentSrcMap: {Name: null}` as identified.
|
||||
- Components are app-coupled (next/*, TanStack Query, data hooks); many need providers/mock props to render → expect floor cards / authored previews with composed props.
|
||||
|
||||
## Authoring (38 components, all authored-good)
|
||||
- **Import contract:** preview imports `{X} from 'tradein-mvp-frontend'` (mapped to window.TradeInUI bundle) + data from `./_fixtures` (re-exports the repo's offline `app/ui-preview/estimate/fixture.ts`). Type imports erased.
|
||||
- **Provider = seeded `PreviewProvider`** (`.design-sync/preview-provider.tsx`, wired via `cfg.extraEntries` + `cfg.provider`). Mirrors `app/ui-preview/estimate/page.tsx`'s seeded QueryClient: pre-fills `["auth","me"]` (fake admin user) + the estimate sub-queries (placement-history, house-analytics, sell-time-sensitivity, cian-price-changes=[], sales-vs-listings). This is what makes fetch-coupled cards (StreetDealsCard, HouseAnalyticsSection, PlacementHistoryCard) and auth-gated ones (RouteGuard, UserMenu) render offline.
|
||||
- **CRITICAL:** an extraEntries module must NOT import anything that reads `process.env` at module top-level (e.g. `@/lib/useMe` → `@/lib/api`) — extraEntries evaluate BEFORE the synth-entry `.pkg-shim.mjs`, so `process` is undefined and the whole IIFE aborts (38/38 vanish from the global). The provider hardcodes `ME_QUERY_KEY = ["auth","me"]` instead of importing useMe.
|
||||
- extraEntries path is PACKAGE-relative: `../../.design-sync/preview-provider.tsx`. Its `@/` aliases don't resolve from outside the tsconfig root → use relative `../tradein-mvp/frontend/src/...` for repo imports inside it.
|
||||
- **Chart cards gate on analog count ≥8** (DistributionCard, ExposureCard): the shared fixture has 3 analogs → "not enough data" fallback. Those two previews extend `analogs` to 10 realistic ЕКБ lots INLINE in their own preview file (spread FIXTURE_ESTIMATE, override analogs+n_analogs) — never edit `_fixtures.ts`.
|
||||
- **Admin/scraper panels** (DataQualitySection, ProviderProxySection, RunsTable, PacingSection, SystemHealthSection) fetch their own data with no repo fixture → they render their real styled LOADING/header state. Graded good as honest states; richer data would need admin fixtures that don't exist in the repo.
|
||||
- **MapCard / MapPicker**: Leaflet from unpkg loads in the capture env; OSM raster tiles sometimes don't paint within the screenshot window (external tile fetch) — markers/popup/controls still render. Both graded good. MapPicker is a `createPortal` full-viewport overlay — rendered contained in capture; if a future capture viewport change makes it escape, add `cfg.overrides.MapPicker.cardMode`/`viewport`.
|
||||
|
||||
## Re-sync risks
|
||||
- The `process` shim env defaults live in `.design-sync/overrides/source-kit.mjs` (synth entry). If the app reads NEW `process.env.NEXT_PUBLIC_*` vars, add them there or components throw.
|
||||
- `PreviewProvider` seed keys are pinned to the fixture's `estimate_id` and address/area/rooms — if `fixture.ts` changes those, update `preview-provider.tsx` to match (else fetch-coupled cards re-floor).
|
||||
- Synth scan re-includes Next route/layout/error files on every build → `componentSrcMap` nulls (13 entries) must persist. New app-router pages would need adding.
|
||||
- `.d.ts` props are weak (`[key:string]:unknown`) — synth mode has no built types. Real prop contracts live in each component's source `interface Props`. A real `tsup`/`tsc` lib build would fix this (recommend if the agent needs strong API contracts).
|
||||
- Grades clear on any `cfg.provider`/preview-affecting config change (expected) — re-grade from fresh sheets.
|
||||
|
||||
## Re-sync 2026-07-02 — v2 dashboard sync (main was 223 commits ahead; harness authored on June components)
|
||||
|
||||
**Two source-kit.mjs fork fixes were REQUIRED for the evolved v2 codebase (both committed in the override, declared in cfg.libOverrides):**
|
||||
1. **Exclude `next/font` importers from the synth-entry.** `src/app/v2/layout.tsx` calls `Manrope()`/`IBM_Plex_Mono()` from `next/font/google` at MODULE TOP-LEVEL. esbuild can't resolve the Next-only loader → stubs it `(void 0)` → `undefined()` aborts the whole browser IIFE → `window.TradeInUI` empty → ALL 58 components vanish (`[RENDER] root empty` everywhere, `[BUNDLE_EXPORT]`). Fix: `comps` filter drops any file whose content matches `/from\s+['"]next\/font/`. If a NEW file top-level-calls a Next build-time loader, same class of crash — extend the filter.
|
||||
2. **Re-export default-exported components.** `export * from <path>` does NOT re-export a module's `default`. The v2 views/nav/overlay/panel are authored `export default function <Name>` (AnalyticsView, HeroBar, HistoryView, ParamsPanel, SectionOverlay, SourcesView, TopNav) → absent from `window.TradeInUI` → `[BUNDLE_EXPORT] not a component`. Fix: entry now also emits `export { default as <Name> } from <path>` for each `export default function/class <Name>`. Named-export components (`export function X`) were always fine.
|
||||
|
||||
**componentSrcMap:** added `TradeInV2Layout / TradeInV2Page / SaleShareLayout / SaleSharePage` = null (Next route files, never DS components — same as RootLayout et al.).
|
||||
|
||||
**overrides (grid):** MapPicker `{cardMode:single, primaryStory:Default}` (portal/fixed), StreetDealsCard + Topbar `{cardMode:column}` (wider than a grid cell).
|
||||
|
||||
**Floor-card components (6, authorable on any future re-sync):** SaleShareControls, SaleShareList, SaleShareMap, SectionOverlay, LocationDrawer, BuildingListingsDrawer — overlays/drawers whose props don't seed rich data via PreviewProvider, so they show the honest typographic floor. All the OTHER v2 components (views/nav/hero/panel) render richly because the provider seeds their data.
|
||||
|
||||
**Known render warn:** ResultPanel — `variants identical` (Default vs Error cells render near-identically; not broken, the Error cell just doesn't diverge visually enough). Recorded here so re-syncs don't read it as new.
|
||||
|
||||
**Font note (re-sync risk):** the v2 HUD's real typefaces are **Manrope + IBM_Plex_Mono** (loaded by the excluded `v2/layout.tsx` via next/font → CSS vars `--font-manrope`/`--font-plex-mono`). `cfg.extraFonts` ships **Inter + JetBrains Mono** (June brand fonts). v2 previews therefore render Manrope-slot text in the shipped fallback. If brand-exact v2 rendering is wanted, add Manrope + IBM Plex Mono woff2 to `.design-sync/fonts/` + `cfg.extraFonts` and map the `--font-manrope`/`--font-plex-mono` vars.
|
||||
|
||||
**ResultPanel NAME COLLISION (fixed 2026-07-02):** two components named `ResultPanel` in src — `app/scrapers/_components/ScraperPage.tsx` (`export function ResultPanel`, scraper mut-panel) and `components/trade-in/v2/ResultPanel.tsx` (`export default function ResultPanel`, the v2 estimate result / honest hero). The default-re-export fix makes the v2 one win `window.TradeInUI.ResultPanel` (explicit `export {default as ResultPanel}` beats the star export). But the June authored preview `previews/ResultPanel.tsx` was the SCRAPER one (`mut` props) → the v2 component rendered fine (reads data from context) but its prompt.md documented the wrong (scraper) API. FIX: (1) `cfg.componentSrcMap.ResultPanel = "src/components/trade-in/v2/ResultPanel.tsx"` pins enrichment to v2; (2) re-authored `previews/ResultPanel.tsx` → `<ResultPanel onNavigate={()=>{}} />` (v2 API; `data` defaults to the component's built-in RESULT_FIXTURE = honest-hero). If a re-sync ever shows ResultPanel with scraper markup, the pin/preview regressed. Any NEW duplicate PascalCase component name will hit the same class of issue — the default-re-export makes the default-export win; pin + author the intended one.
|
||||
|
||||
**Upload gotcha (this machine):** after a follow-up single-component rebuild, `resync-verdict.json upload.deletePaths` came back with ALL other components (318 paths) — a stale-anchor diff artifact, NOT real deletions (ResultPanel + audit/uploads were absent from it). Do NOT feed that deletePaths to delete_files (would nuke the project). For a focused single-component re-upload: write just that component's `components/<group>/<Name>/*` + `_preview/<Name>.js` + `_ds_sync.json` (sentinel-fenced), deletes=[].
|
||||
- **CORRECTION (2026-07-03):** the 318 deletePaths were NOT a diff artifact — they were REAL local deletions caused by a source-kit fork bug (see below): the July-02 `componentSrcMap.ResultPanel` pin collapsed the synth build to 1 component, so the local ds-bundle genuinely lost the other 53. The "don't feed deletePaths blindly" advice stands (it saved the project), but the root cause is fixed now.
|
||||
|
||||
## Re-sync 2026-07-03 — 6 floor cards authored + v2 brand fonts (#2267)
|
||||
|
||||
**source-kit.mjs fork fix #3 (REQUIRED): componentSrcMap pin must AUGMENT synth discovery, not replace it.** In synth mode there is no shipped `.d.ts`, so `exportedNames()` is empty and `names` contains ONLY the non-null `componentSrcMap` pins. The old guard `if (!components.length && synthEntry) components = deriveComponentsFromSrc(...)` therefore never ran once a single pin existed (ResultPanel, added 2026-07-02) → the whole bundle collapsed to 1 component (`(stale preview: X — component no longer exported)` for everything else; verdict shows all others as `removed` + bogus deletePaths). Fix in the fork: when `synthEntry`, always union `deriveComponentsFromSrc(srcFiles)` (minus `null`-excluded) with the pinned names. Any future pin would have re-triggered this. NB: the fork edit re-keys EVERY component's sourceKey → full re-grade pass (done: 47/54 renderHashes byte-identical to the 2026-07-02 anchor → carry-forward grades; the rest eyeballed).
|
||||
|
||||
**6 floor cards → authored (all graded good):**
|
||||
- `SaleShareControls` — inline `SaleShareSummary` (histogram 7 корзин, coverage 92.1%); порог 8% приглушает нижнюю корзину; все фильтры.
|
||||
- `SaleShareList` — 6 инлайн-домов ЕКБ (heat-бейджи, «аварийный», over_100 «возможно, несколько корпусов», selected-row) + Empty cell. `cardMode: column`.
|
||||
- `SaleShareMap` — 8 heat-маркеров + открытый popup выбранного дома; OSM-тайлы офлайн не красятся (известно, как MapCard).
|
||||
- `SectionOverlay` (v2) — рендерится contained в relative-«артборде» (absolute-позиционирование против ближайшего positioned ancestor; wrapper height 680 + v2-градиент). 2 cells: HistoryView (04) / AnalyticsView (06) на их встроенных fixtures (data-props не переданы). `cardMode: column`.
|
||||
- `LocationDrawer` (v2) — open=true в таком же contained-артборде (height 640). `cardMode: column`.
|
||||
- `BuildingListingsDrawer` — `createPortal` + `.ss-drawer-overlay` = position:fixed → `cardMode: single` (прецедент MapPicker). Шапка богатая (props), тело fetch-coupled (`useBuildingListings`, data-prop нет) → офлайн честный error-state «Не удалось загрузить объявления дома». Сидировать можно было бы ключом `["sale-share","listings",<house_id>]` в PreviewProvider — сознательно НЕ сделано (кэш-ключ завязан на house_id фикстуры превью; хрупко).
|
||||
|
||||
**v2 brand fonts shipped (Manrope + IBM Plex Mono).** woff2 (latin+cyrillic; Manrope variable 200-800, Plex Mono static 300/400/500 — веса из `app/v2/layout.tsx`) скачаны с Google Fonts → `.design-sync/fonts/`, @font-face добавлены в `brand-fonts.css`. **ГОЧА: extraFonts-пайплайн (`css.mjs extractFonts`) извлекает ТОЛЬКО `@font-face`-блоки — `:root{}` из brand-fonts.css молча выбрасывается.** Маппинг `--font-manrope`/`--font-plex-mono` поэтому живёт в `preview-provider.tsx` (`<style>` в провайдере — капчер-путь) + задокументирован для design-консюмеров в `conventions.md` (сниппет `:root{...}`). После фикса v2-цифры реально в Plex Mono (проверено по sheets ResultPanel/SectionOverlay).
|
||||
|
||||
**Upload 2026-07-03: НЕ выполнен из worker-сессии** — DesignSync MCP-тулов в ней нет (ToolSearch пуст). Verdict готов и чист: ok=true, pendingGrade=0, deletePaths=0 (легитимно — remote-анкор полный, local снова 54 компонента), upload.any=true, components=54 (все re-key'нуты форк-фиксом), bundle+styling+aux=true. Main-сессия: залить ds-bundle по verdict'у (deletePaths пуст — ничего не удалять) и после успеха скопировать свежий `ds-bundle/_ds_sync.json` → `.design-sync/.cache/remote-sync.json` (новый анкор).
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
{
|
||||
"projectId": "e4b235af-089b-4532-8025-5199a2695a12",
|
||||
"shape": "package",
|
||||
"pkg": "tradein-mvp-frontend",
|
||||
"globalName": "TradeInUI",
|
||||
"srcDir": "src",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"cssEntry": "src/components/trade-in/trade-in.css",
|
||||
"tokensGlob": "src/app/globals.css",
|
||||
"buildCmd": "node .ds-sync/package-build.mjs --config .design-sync/config.json --node-modules tradein-mvp/frontend/node_modules --entry tradein-mvp/frontend/dist/index.js --out ./ds-bundle",
|
||||
"libOverrides": {
|
||||
"source-kit.mjs": "synth-entry process shim for Next.js app (process.env.NEXT_PUBLIC_* reads)"
|
||||
},
|
||||
"componentSrcMap": {
|
||||
"Error": null,
|
||||
"GlobalError": null,
|
||||
"Providers": null,
|
||||
"RootLayout": null,
|
||||
"AvitoScraperPage": null,
|
||||
"CachePage": null,
|
||||
"CianScraperPage": null,
|
||||
"HistoryPage": null,
|
||||
"PreviewEstimatePage": null,
|
||||
"ScraperPage": null,
|
||||
"ScrapersUnifiedPage": null,
|
||||
"TradeInPage": null,
|
||||
"YandexScraperPage": null,
|
||||
"TradeInV2Layout": null,
|
||||
"TradeInV2Page": null,
|
||||
"SaleShareLayout": null,
|
||||
"SaleSharePage": null,
|
||||
"ResultPanel": "src/components/trade-in/v2/ResultPanel.tsx"
|
||||
},
|
||||
"overrides": {
|
||||
"MapPicker": { "cardMode": "single", "primaryStory": "Default" },
|
||||
"StreetDealsCard": { "cardMode": "column" },
|
||||
"Topbar": { "cardMode": "column" },
|
||||
"SaleShareList": { "cardMode": "column" },
|
||||
"SectionOverlay": { "cardMode": "column" },
|
||||
"LocationDrawer": { "cardMode": "column" },
|
||||
"BuildingListingsDrawer": { "cardMode": "single", "primaryStory": "Default" }
|
||||
},
|
||||
"provider": {
|
||||
"component": "PreviewProvider"
|
||||
},
|
||||
"extraEntries": [
|
||||
"../../.design-sync/preview-provider.tsx"
|
||||
],
|
||||
"readmeHeader": ".design-sync/conventions.md",
|
||||
"extraFonts": [
|
||||
"../../.design-sync/fonts/brand-fonts.css"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# Trade-In UI — how to build with this design system
|
||||
|
||||
React components from the GenDesign **trade-in** product (real-estate trade-in valuation, RU/ЕКБ). Import everything from `tradein-mvp-frontend` (bound at `window.TradeInUI`). The cards are domain-specific and **prop-driven** — you compose them with data, you don't restyle their internals.
|
||||
|
||||
## Setup & wrapping (required for data components)
|
||||
|
||||
Most cards take their data as **props** and render standalone. But several read app state through **TanStack Query** hooks (`UserMenu`, `Topbar`, `RouteGuard` → `useMe`; `HouseAnalyticsSection`, `PlacementHistoryCard`, `StreetDealsCard` → estimate sub-queries). Those MUST be rendered inside a `QueryClientProvider` (the app ships one as `Providers`). Without it they throw "No QueryClient set"; with an empty client they render their loading/null state. Prop-only cards (`HeroSummary`, `OfferCard`, `PriceRangeBar`, `DealsCard`, `ListingsCard`, charts…) need no provider.
|
||||
|
||||
```tsx
|
||||
import { Providers, HeroSummary, OfferCard } from "tradein-mvp-frontend";
|
||||
|
||||
<Providers>
|
||||
<main className="page" style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<HeroSummary estimate={estimate} input={input} onResubmit={fn} />
|
||||
<OfferCard estimate={estimate} brandSlug={null} />
|
||||
</main>
|
||||
</Providers>
|
||||
```
|
||||
|
||||
Load `styles.css` once at the root — it pulls the component CSS + design tokens.
|
||||
|
||||
## Styling idiom — global stylesheet + CSS custom properties
|
||||
|
||||
This is **not Tailwind and not CSS-in-JS**. Styling is a **global stylesheet** of semantic class names plus **CSS custom-property design tokens**. Two rules:
|
||||
|
||||
1. **The components carry their own classes** (`.card`, `.card-head`, `.section-kicker`, `.count-strip`, `.pricebar`, `.source-chip`, `.control`, `.pill`, `.mono`, `.top-nav`, `.meta-grid`…). Don't reach inside them; compose via props. New class names you invent will not exist in the stylesheet.
|
||||
2. **For your own layout/wrapper glue, use the token vars + inline styles**, on the 4/8/12/16/24/32 spacing scale (tabular-nums for numbers). Color/shape tokens, all defined in the shipped CSS:
|
||||
|
||||
| Group | Tokens |
|
||||
|---|---|
|
||||
| Surface | `--bg-app` `--bg-card` `--bg-card-alt` `--bg-headline` |
|
||||
| Text | `--fg-primary` `--fg-secondary` `--fg-tertiary` `--fg-on-dark` |
|
||||
| Brand/CTA | `--accent` `--accent-hover` `--accent-soft` `--accent-2` |
|
||||
| Semantic | `--success` `--warn` `--danger` (`*-soft` variants) |
|
||||
| Border | `--border-soft` `--border-card` `--border-strong` |
|
||||
| Shape/type | `--radius` `--radius-sm` `--radius-lg` `--shadow-md` `--font-sans` (Manrope) `--font-mono` `--container` |
|
||||
|
||||
**v2 (МЕРА HUD) fonts:** the `v2/*` components read `var(--font-manrope)` / `var(--font-plex-mono)` (in the app these come from `next/font`). The woff2 for both families ships in `fonts/fonts.css`; define the vars once at your design root:
|
||||
|
||||
```css
|
||||
:root { --font-manrope: 'Manrope'; --font-plex-mono: 'IBM Plex Mono'; }
|
||||
```
|
||||
|
||||
```tsx
|
||||
<div style={{ background: "var(--bg-card)", border: "1px solid var(--border-card)",
|
||||
borderRadius: "var(--radius)", padding: 16, color: "var(--fg-primary)",
|
||||
fontFamily: "var(--font-sans)" }}>
|
||||
<b style={{ color: "var(--accent)" }}>9 850 000 ₽</b>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Where the truth is
|
||||
|
||||
- **Styles/tokens:** read the bound `styles.css` and its `@import` `_ds_bundle.css` — the authoritative class + token list.
|
||||
- **Per component:** `<Name>.d.ts` (props) and `<Name>.prompt.md` (usage). NB: synth-built `.d.ts` props are loose (`[key: string]: unknown`); the `.prompt.md` + preview card show real prop shapes and realistic data.
|
||||
- Money/area/dates are RU-formatted (`toLocaleString("ru-RU")`, `₽`, `м²`). Keep that idiom.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,112 +0,0 @@
|
|||
/* Brand fonts shipped with the trade-in DS bundle.
|
||||
* June set: Inter + JetBrains Mono (variable woff2).
|
||||
* v2 HUD set (2026-07-03): Manrope + IBM Plex Mono — the МЕРА v2 typefaces,
|
||||
* loaded in the app via next/font in app/v2/layout.tsx (CSS vars
|
||||
* --font-manrope / --font-plex-mono). next/font is excluded from the synth
|
||||
* bundle, so we ship the woff2 here and map the vars in :root below.
|
||||
* All fonts: latin + cyrillic subsets (ЕКБ addresses are Cyrillic). */
|
||||
|
||||
:root {
|
||||
--font-manrope: 'Manrope';
|
||||
--font-plex-mono: 'IBM Plex Mono';
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 200 800;
|
||||
font-display: swap;
|
||||
src: url('./Manrope-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Manrope';
|
||||
font-style: normal;
|
||||
font-weight: 200 800;
|
||||
font-display: swap;
|
||||
src: url('./Manrope-cyrillic.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('./IBMPlexMono-300-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('./IBMPlexMono-300-cyrillic.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./IBMPlexMono-400-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('./IBMPlexMono-400-cyrillic.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('./IBMPlexMono-500-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('./IBMPlexMono-500-cyrillic.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('./Inter-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('./Inter-cyrillic.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url('./JetBrainsMono-latin.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 100 800;
|
||||
font-display: swap;
|
||||
src: url('./JetBrainsMono-cyrillic.woff2') format('woff2');
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
// Non-storybook `package` adapter. Bundles dist/ when present (the authoritative
|
||||
// component list comes from shipped .d.ts; with no dist it synthesizes an
|
||||
// entry from src/ as a last resort) and opportunistically enriches each
|
||||
// component from src/ — JSDoc and dir-derived group. Every enrichment miss
|
||||
// degrades to the plain-dist behaviour.
|
||||
//
|
||||
// Discovery is heuristic-based; each heuristic has a `.design-sync/config.json`
|
||||
// override (ASSUMPTION comments below name them) so repos that don't match the
|
||||
// defaults write config, not code. `componentSrcMap` is the single override
|
||||
// knob for component inclusion: non-null value = add/pin src path, null =
|
||||
// exclude a .d.ts-exported internal.
|
||||
|
||||
import { existsSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { Project, Node, ts } from 'ts-morph';
|
||||
// forked from design-sync lib/source-kit.mjs — synth-entry process shim (Next.js app reads process.env.NEXT_PUBLIC_*)
|
||||
import { leadingJsdoc, readText, slash, walk } from '../../.ds-sync/lib/common.mjs';
|
||||
import { resolveDistEntry } from '../../.ds-sync/lib/bundle.mjs';
|
||||
import { exportedNames, isComponentName } from '../../.ds-sync/lib/dts.mjs';
|
||||
|
||||
const NON_IMPL_RX = /\.(stories|test|spec)\./;
|
||||
const SRC_IMPL_RX = /\.(tsx|jsx)$/;
|
||||
// Dir names that don't usefully group components — skip so the emitted path
|
||||
// is `components/<group>/<Name>` not `components/components/<Name>`.
|
||||
const GENERIC_DIR = new Set(['components', 'component', 'src', 'lib', 'ui', 'packages', 'react']);
|
||||
const slug = (s) => s.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'general';
|
||||
|
||||
// No .d.ts → scan src files for PascalCase value exports via ts-morph.
|
||||
function deriveComponentsFromSrc(srcFiles) {
|
||||
const project = new Project({
|
||||
skipAddingFilesFromTsConfig: true,
|
||||
compilerOptions: { jsx: ts.JsxEmit.Preserve, allowJs: true, skipLibCheck: true },
|
||||
});
|
||||
const seen = new Set();
|
||||
for (const p of srcFiles) {
|
||||
if (NON_IMPL_RX.test(p) || !SRC_IMPL_RX.test(p)) continue;
|
||||
const sf = project.addSourceFileAtPathIfExists(p);
|
||||
if (!sf) continue;
|
||||
for (const [name, decls] of sf.getExportedDeclarations()) {
|
||||
// `export default function Button()` is keyed as 'default' — recover
|
||||
// the declared name from the function/class node.
|
||||
const real = name === 'default'
|
||||
? decls.map((d) => d.getName?.()).find((n) => n && n !== 'default')
|
||||
: name;
|
||||
if (!real || !/^[A-Z][A-Za-z0-9]*$/.test(real)) continue;
|
||||
if (decls.some((d) => Node.isVariableDeclaration(d) || Node.isFunctionDeclaration(d) || Node.isClassDeclaration(d))) {
|
||||
seen.add(real);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...seen].sort().map((name) => ({ name, group: 'general' }));
|
||||
}
|
||||
|
||||
export async function resolvePackage(ctx) {
|
||||
const { PKG_DIR, pkgJson, ENTRY_OVERRIDE, PKG, OUT, cfg } = ctx;
|
||||
const srcMap = cfg.componentSrcMap ?? {};
|
||||
|
||||
// ── 1. src/ discovery (best-effort; feeds enrichment + synth-entry fallback).
|
||||
// ASSUMPTION: source root is first of src/ | lib/ | components/. Override: cfg.srcDir.
|
||||
const srcRoot = [cfg.srcDir, 'src', 'lib', 'components']
|
||||
.map((d) => d && resolve(PKG_DIR, d))
|
||||
.find((d) => d && existsSync(d));
|
||||
const srcFiles = srcRoot ? walk(srcRoot, (n) => /\.(tsx|jsx|mdx?)$/.test(n)) : [];
|
||||
|
||||
// ── 2. entry: dist if it exists, else synthesize from src/ (last resort).
|
||||
let entry = resolveDistEntry({ pkgDir: PKG_DIR, pkgJson, override: ENTRY_OVERRIDE, pkgName: PKG, soft: true });
|
||||
let synthEntry = false;
|
||||
if (!entry) {
|
||||
if (!srcRoot) {
|
||||
console.error(`[NO_DIST] ${PKG} has no built entry and no src/ to synthesize from — run its build.`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Next route files (app/**/layout.tsx, page.tsx) that call `next/font`
|
||||
// loaders (Manrope(), IBM_Plex_Mono()) at module top-level crash the browser
|
||||
// IIFE: esbuild can't resolve the Next-only loader → stubs it to `undefined`
|
||||
// → `undefined()` aborts the whole bundle → window.<global> stays empty and
|
||||
// every component vanishes. These files are never DS components anyway, so
|
||||
// drop any `next/font`-importing module from the synth-entry set.
|
||||
const comps = srcFiles.filter(
|
||||
(p) =>
|
||||
SRC_IMPL_RX.test(p) &&
|
||||
!NON_IMPL_RX.test(p) &&
|
||||
!/from\s+['"]next\/font/.test(readFileSync(p, 'utf8')),
|
||||
);
|
||||
// Next.js app code reads process.env.NEXT_PUBLIC_* at module top-level; in
|
||||
// the browser IIFE `process` is undefined → every component throws. Emit a
|
||||
// shim module and import it FIRST (ESM evaluates the first import's body
|
||||
// before the re-exported component modules), so process exists before any
|
||||
// component body runs.
|
||||
const shim = resolve(OUT, '.pkg-shim.mjs');
|
||||
writeFileSync(
|
||||
shim,
|
||||
'globalThis.process ??= { env: {} };\n' +
|
||||
'globalThis.process.env ??= {};\n' +
|
||||
'const __e = globalThis.process.env;\n' +
|
||||
"__e.NODE_ENV ??= 'development';\n" +
|
||||
"__e.NEXT_PUBLIC_ENABLE_PREVIEW ??= '1';\n" +
|
||||
"__e.NEXT_PUBLIC_BASE_PATH ??= '';\n" +
|
||||
"__e.NEXT_PUBLIC_API_BASE_URL ??= '';\n" +
|
||||
"__e.NEXT_PUBLIC_TRADEIN_CONTACT_EMAIL ??= 'trade-in@example.com';\n",
|
||||
);
|
||||
entry = join(OUT, '.pkg-entry.mjs');
|
||||
// `export *` does NOT re-export a module's default. Components authored as
|
||||
// `export default function <Name>` (the v2 views/nav/overlay/panel) would be
|
||||
// absent from window.<global> → [BUNDLE_EXPORT] "not a component". Re-export
|
||||
// each named default under its declared name so default-exported components
|
||||
// reach the global alongside the named ones.
|
||||
const defaultReexports = comps
|
||||
.map((p) => {
|
||||
const m = readFileSync(p, 'utf8').match(
|
||||
/export\s+default\s+(?:async\s+)?(?:function|class)\s+([A-Z][A-Za-z0-9]*)/,
|
||||
);
|
||||
return m ? `export { default as ${m[1]} } from ${JSON.stringify(p)};` : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
writeFileSync(
|
||||
entry,
|
||||
`import ${JSON.stringify(slash(shim))};\n` +
|
||||
comps.map((p) => `export * from ${JSON.stringify(p)};`).join('\n') +
|
||||
'\n' +
|
||||
defaultReexports.join('\n') +
|
||||
'\n',
|
||||
);
|
||||
synthEntry = true;
|
||||
console.error(
|
||||
`[NO_DIST] no built entry — synthesizing from ${comps.length} src files (run the package's build for best results)`,
|
||||
);
|
||||
}
|
||||
|
||||
// ── 3. component list: from shipped .d.ts (authoritative when dist exists).
|
||||
// ASSUMPTION: components = PascalCase value exports in the .d.ts tree.
|
||||
// Override: cfg.componentSrcMap (non-null adds/pins, null excludes).
|
||||
const exported = exportedNames(PKG_DIR, pkgJson);
|
||||
const names = new Set([...exported].filter(isComponentName));
|
||||
for (const [k, v] of Object.entries(srcMap)) {
|
||||
if (v === null) { names.delete(k); continue; }
|
||||
// Names reach `<script>` blocks in the emitted HTML — reject anything
|
||||
// that isn't a plain PascalCase identifier.
|
||||
if (!/^[A-Z][A-Za-z0-9]*$/.test(k)) {
|
||||
console.error(`[CONFIG] componentSrcMap: "${k}" is not a valid component name (PascalCase identifiers only)`);
|
||||
continue;
|
||||
}
|
||||
names.add(k);
|
||||
}
|
||||
let components = [...names].sort().map((name) => ({ name, group: 'general' }));
|
||||
if (synthEntry) {
|
||||
// Synth mode has no shipped .d.ts → `names` holds only componentSrcMap
|
||||
// pins. A non-null pin must AUGMENT src discovery (it exists to pin
|
||||
// enrichment to a specific file), not REPLACE it — with the old
|
||||
// `!components.length` guard a single pin (ResultPanel, 2026-07-02)
|
||||
// collapsed the whole synth bundle to just the pinned component.
|
||||
const derived = deriveComponentsFromSrc(srcFiles).filter((c) => srcMap[c.name] !== null);
|
||||
const have = new Set(components.map((c) => c.name));
|
||||
components = components
|
||||
.concat(derived.filter((c) => !have.has(c.name)))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
if (!components.length) {
|
||||
if (cfg.cssEntry || existsSync(join(PKG_DIR, 'styles.css'))) {
|
||||
console.error('[ZERO_MATCH] no component exports — treating as tokens-only DS');
|
||||
return { shape: 'package', entry, components: [], tokensOnly: true };
|
||||
}
|
||||
console.error(`[ZERO_MATCH] no PascalCase exports in ${PKG} and no styles — nothing to sync`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── 4. src/ enrichment per component. Every miss degrades to plain-dist.
|
||||
if (srcRoot) {
|
||||
for (const c of components) {
|
||||
// Pinned via config → skip fuzzy-find entirely.
|
||||
let hit = typeof srcMap[c.name] === 'string' ? slash(resolve(PKG_DIR, srcMap[c.name])) : null;
|
||||
if (!hit) {
|
||||
// ASSUMPTION: <Name>.tsx | <name>/<name>.tsx | <Name>/index.tsx |
|
||||
// <kebab-name>.tsx, case-insensitive; dir-match ranks above
|
||||
// bare-file match, then prefer one that actually exports `c.name`.
|
||||
// Override: cfg.componentSrcMap.
|
||||
const kebab = c.name.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
|
||||
const nameRx = new RegExp(
|
||||
`(?:^|/)(?:${c.name}/(?:index|${c.name})\\.(tsx|jsx)|(?:${c.name}|${kebab})\\.(tsx|jsx))$`,
|
||||
'i',
|
||||
);
|
||||
const hits = srcFiles
|
||||
.filter((p) => nameRx.test(p) && !NON_IMPL_RX.test(p))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(b.toLowerCase().includes(`/${c.name.toLowerCase()}/`) ? 1 : 0) -
|
||||
(a.toLowerCase().includes(`/${c.name.toLowerCase()}/`) ? 1 : 0),
|
||||
);
|
||||
const exportRx = new RegExp(`export\\s+(?:default\\s+)?(?:const|let|var|function|class)\\s+${c.name}\\b`);
|
||||
hit = hits.find((p) => exportRx.test(readText(p))) ?? hits[0];
|
||||
}
|
||||
if (!hit || !existsSync(hit)) continue;
|
||||
c.srcPath = hit;
|
||||
c.doc = leadingJsdoc(readText(hit), c.name) || undefined;
|
||||
// group = last src/ path segment that isn't the component's own dir or
|
||||
// a generic container name — else JSDoc @category — else 'general'.
|
||||
c.group = slug(
|
||||
slash(relative(srcRoot, dirname(hit)))
|
||||
.split('/')
|
||||
.filter((s) => s && s.toLowerCase() !== c.name.toLowerCase() && !GENERIC_DIR.has(s.toLowerCase()))
|
||||
.at(-1)
|
||||
|| (c.doc && /@category\s+(\S+)/.exec(c.doc)?.[1])
|
||||
|| 'general',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
` package: ${components.length} components` +
|
||||
(srcRoot ? ` (${components.filter((c) => c.srcPath).length} src-matched)` : ' (no src/ — dist-only)'),
|
||||
);
|
||||
return { shape: 'package', entry, components, synthEntry, exported };
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
"use client";
|
||||
|
||||
// Seeded provider for design-sync preview cards. Mirrors the repo's own offline
|
||||
// preview client (tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx):
|
||||
// a QueryClient pre-filled with the FIXTURE_* data + a fake authorized user, so
|
||||
// fetch-coupled cards (HouseAnalyticsSection, PlacementHistoryCard, StreetDealsCard)
|
||||
// and auth-gated ones (RouteGuard, UserMenu) render offline. Bundled into
|
||||
// window.<GLOBAL> via cfg.extraEntries → shares the SAME react-query instance as
|
||||
// the components (context identity), which a second copy in a preview would break.
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
FIXTURE_ANALYTICS,
|
||||
FIXTURE_ESTIMATE,
|
||||
FIXTURE_PLACEMENT,
|
||||
FIXTURE_SALES,
|
||||
FIXTURE_SELLTIME,
|
||||
} from "../tradein-mvp/frontend/src/app/ui-preview/estimate/fixture";
|
||||
|
||||
// Inlined to avoid importing @/lib/useMe → @/lib/api, which reads process.env
|
||||
// at module top-level; in an extraEntries module that evaluates before the
|
||||
// synth-entry process shim and aborts the whole bundle. Key must match
|
||||
// useMe.ts's ME_QUERY_KEY exactly.
|
||||
const ME_QUERY_KEY = ["auth", "me"] as const;
|
||||
const PREVIEW_ID = FIXTURE_ESTIMATE.estimate_id;
|
||||
|
||||
const PREVIEW_USER = {
|
||||
username: "preview",
|
||||
role: "admin",
|
||||
allowed_paths: ["/**"],
|
||||
deny_paths: [],
|
||||
brand: null,
|
||||
};
|
||||
|
||||
export function PreviewProvider({ children }: { children: React.ReactNode }) {
|
||||
const [client] = useState(() => {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, staleTime: Infinity, refetchOnWindowFocus: false },
|
||||
},
|
||||
});
|
||||
qc.setQueryData(ME_QUERY_KEY, PREVIEW_USER);
|
||||
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "placement-history"], FIXTURE_PLACEMENT);
|
||||
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "house-analytics"], FIXTURE_ANALYTICS);
|
||||
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "sell-time-sensitivity"], FIXTURE_SELLTIME);
|
||||
qc.setQueryData(["trade-in", "estimate", PREVIEW_ID, "cian-price-changes"], []);
|
||||
qc.setQueryData(
|
||||
[
|
||||
"trade-in",
|
||||
"sales-vs-listings",
|
||||
FIXTURE_ESTIMATE.target_address,
|
||||
FIXTURE_ESTIMATE.area_m2,
|
||||
FIXTURE_ESTIMATE.rooms,
|
||||
],
|
||||
FIXTURE_SALES,
|
||||
);
|
||||
return qc;
|
||||
});
|
||||
return (
|
||||
<QueryClientProvider client={client}>
|
||||
{/* v2 HUD font vars. The app defines --font-manrope/--font-plex-mono via
|
||||
next/font in app/v2/layout.tsx, which is excluded from the synth
|
||||
bundle (next/font loader crash) — so the vars never exist in capture
|
||||
and tokens.font.* fell back. The @font-face for both families ships
|
||||
in .design-sync/fonts/brand-fonts.css (cfg.extraFonts), but that
|
||||
pipeline extracts @font-face blocks ONLY (a :root{} there is
|
||||
dropped) — map the vars here instead. */}
|
||||
<style>{":root{--font-manrope:'Manrope';--font-plex-mono:'IBM Plex Mono'}"}</style>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { AddressInput } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Combobox адреса ЕКБ с автокомплитом. Dropdown открывается по focus/набору
|
||||
* (фетч suggest офлайн недоступен) — карточка показывает заполненный control. */
|
||||
export const Default = () => (
|
||||
<AddressInput
|
||||
value="Екатеринбург, ул. Репина, 75/2"
|
||||
onChange={() => {}}
|
||||
onPickCoords={() => {}}
|
||||
placeholder="ул. Малышева, 30 · Куйбышева, 48…"
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { BuildingListingsDrawer } from 'tradein-mvp-frontend';
|
||||
|
||||
// Правый drawer с активными объявлениями выбранного дома (/trade-in/sale-share):
|
||||
// createPortal в document.body, .ss-drawer-overlay = position:fixed по всему
|
||||
// вьюпорту (как MapPicker) → cardMode:single. Шапка — адрес + тепловой бейдж
|
||||
// доли + «N из M квартир» (из props). Тело FETCH-COUPLED: useBuildingListings
|
||||
// (GET /buildings/{id}/listings) — data-prop нет, глобальный query-cache пуст →
|
||||
// в офлайн-capture честный pending/error-state («Загрузка объявлений…» /
|
||||
// «Не удалось загрузить объявления дома»). На реальной странице — список
|
||||
// объявлений с ценой, ₽/м², этажом и ссылкой на оригинал.
|
||||
const building = {
|
||||
house_id: 3101,
|
||||
address: 'ул. Викулова, 46',
|
||||
lat: 56.8412,
|
||||
lon: 60.5556,
|
||||
sale_share_pct: 34.6,
|
||||
sale_share_pct_45d: 41.2,
|
||||
listings_45d: 33,
|
||||
over_100: false,
|
||||
active_secondary: 27,
|
||||
flat_count_effective: 78,
|
||||
gar_match_method: 'cadastre',
|
||||
median_price_rub: 4_950_000,
|
||||
median_price_per_m2: 158_500,
|
||||
avg_days_on_market: 74,
|
||||
year_built: 1972,
|
||||
house_type: 'panel',
|
||||
total_floors: 9,
|
||||
series_name: '1-468',
|
||||
is_emergency: false,
|
||||
};
|
||||
|
||||
/** Открытый drawer дома «ул. Викулова, 46» (34.6% в продаже, 27 из 78 квартир). */
|
||||
export const Default = () => (
|
||||
<BuildingListingsDrawer building={building} onClose={() => {}} />
|
||||
);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { CianValuationCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Компактный блок «Оценка Cian» — продажа 9.70 млн ₽ + аренда 42 000 ₽/мес,
|
||||
* спарклайн за 6 мес и бейдж ↑3.2%. Узкий компонент (section, не full-width card). */
|
||||
export const Default = () => <CianValuationCard data={FIXTURE_ESTIMATE.cian_valuation} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { DataQualitySection } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Coverage-секция: таблица заполнения полей по источникам + обогащение домов.
|
||||
* Фетчит /admin/scraper/data-quality через useQuery (без data-prop). В превью
|
||||
* без сети рендерит заголовок + hint + graceful-degradation. Fetch-coupled. */
|
||||
export const Default = () => <DataQualitySection />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { DealsCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Секция 3 «Сделки» — фактические ДКП-сделки Росреестра по аналогам (2 строки):
|
||||
* count-strip (кол-во / медиана / диапазон) + чипы источников + таблица. */
|
||||
export const Default = () => <DealsCard estimate={FIXTURE_ESTIMATE} />;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { DistributionCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
// Канонический FIXTURE_ESTIMATE содержит лишь 3 аналога — DistributionCard рисует
|
||||
// гистограмму только при ≥8 (иначе fallback-текст «мало аналогов»). Расширяем до 10
|
||||
// правдоподобных ЕКБ-аналогов (₽/м² вокруг median 178 000) — чтобы показать сам график.
|
||||
// Реалистичные ЕКБ-улицы/цены, не foo/test; правим только этот preview-файл.
|
||||
const ekbAnalogs = [
|
||||
{ address: 'ул. Репина, 73', area_m2: 54, rooms: 2, floor: 7, total_floors: 16, price_rub: 9_700_000, price_per_m2: 179_629, listing_date: '2026-05-12', days_on_market: 18, photo_url: null, source: 'avito', source_url: null, distance_m: 120, tier: null, lat: 56.8401, lon: 60.5702 },
|
||||
{ address: 'ул. Викулова, 33', area_m2: 57, rooms: 2, floor: 4, total_floors: 10, price_rub: 9_950_000, price_per_m2: 174_561, listing_date: '2026-05-20', days_on_market: 10, photo_url: null, source: 'cian', source_url: null, distance_m: 340, tier: null, lat: 56.8389, lon: 60.5681 },
|
||||
{ address: 'ул. Кирова, 28', area_m2: 53, rooms: 2, floor: 9, total_floors: 12, price_rub: 9_400_000, price_per_m2: 177_358, listing_date: '2026-05-04', days_on_market: 31, photo_url: null, source: 'avito', source_url: null, distance_m: 510, tier: null, lat: 56.8372, lon: 60.5749 },
|
||||
{ address: 'ул. Серафимы Дерябиной, 24', area_m2: 56, rooms: 2, floor: 6, total_floors: 14, price_rub: 9_600_000, price_per_m2: 171_429, listing_date: '2026-05-08', days_on_market: 42, photo_url: null, source: 'cian', source_url: null, distance_m: 680, tier: null, lat: 56.8318, lon: 60.5666 },
|
||||
{ address: 'ул. Ясная, 6', area_m2: 52, rooms: 2, floor: 11, total_floors: 16, price_rub: 9_600_000, price_per_m2: 184_615, listing_date: '2026-05-18', days_on_market: 14, photo_url: null, source: 'avito', source_url: null, distance_m: 430, tier: null, lat: 56.8295, lon: 60.5803 },
|
||||
{ address: 'ул. Белореченская, 17', area_m2: 58, rooms: 2, floor: 3, total_floors: 9, price_rub: 9_800_000, price_per_m2: 168_966, listing_date: '2026-04-28', days_on_market: 55, photo_url: null, source: 'yandex', source_url: null, distance_m: 900, tier: null, lat: 56.8267, lon: 60.5611 },
|
||||
{ address: 'ул. Гурзуфская, 16', area_m2: 55, rooms: 2, floor: 8, total_floors: 12, price_rub: 10_000_000, price_per_m2: 181_818, listing_date: '2026-05-22', days_on_market: 9, photo_url: null, source: 'cian', source_url: null, distance_m: 260, tier: null, lat: 56.8344, lon: 60.5727 },
|
||||
{ address: 'ул. Посадская, 40', area_m2: 51, rooms: 2, floor: 5, total_floors: 10, price_rub: 9_600_000, price_per_m2: 188_235, listing_date: '2026-05-15', days_on_market: 22, photo_url: null, source: 'avito', source_url: null, distance_m: 770, tier: null, lat: 56.8231, lon: 60.5832 },
|
||||
{ address: 'ул. Шаумяна, 86', area_m2: 60, rooms: 2, floor: 2, total_floors: 18, price_rub: 9_900_000, price_per_m2: 165_000, listing_date: '2026-04-25', days_on_market: 60, photo_url: null, source: 'yandex', source_url: null, distance_m: 1100, tier: null, lat: 56.8189, lon: 60.5598 },
|
||||
{ address: 'ул. Чкалова, 124', area_m2: 54, rooms: 2, floor: 12, total_floors: 19, price_rub: 10_400_000, price_per_m2: 192_593, listing_date: '2026-05-24', days_on_market: 7, photo_url: null, source: 'cian', source_url: null, distance_m: 350, tier: null, lat: 56.8276, lon: 60.5689 },
|
||||
];
|
||||
|
||||
const estimate = { ...FIXTURE_ESTIMATE, analogs: ekbAnalogs, n_analogs: ekbAnalogs.length };
|
||||
|
||||
/** Гистограмма распределения ₽/м² по 10 аналогам с маркером «ваша квартира»
|
||||
* на median 178 000 ₽/м² (оранжевый столбец/референс-линия). */
|
||||
export const Default = () => <DistributionCard estimate={estimate} />;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { EstimateForm } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Шаг 1/1 — параметры квартиры (адрес + площадь/комнаты/этаж/тип/ремонт +
|
||||
* свёрнутый CRM-блок). Полное «спокойное» состояние: не считает, без ошибок. */
|
||||
export const Default = () => (
|
||||
<EstimateForm
|
||||
onSubmit={() => {}}
|
||||
isPending={false}
|
||||
error={null}
|
||||
remaining={12}
|
||||
limit={15}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { ExposureCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
// Канонический FIXTURE_ESTIMATE содержит лишь 3 аналога — ExposureCard рисует
|
||||
// scatter (цена × срок экспозиции) только при ≥8 точках с days_on_market (иначе
|
||||
// fallback-текст «недостаточно данных»). Расширяем до 10 правдоподобных ЕКБ-аналогов
|
||||
// (у всех непустой days_on_market) — чтобы показать сам график. Правим только этот файл.
|
||||
const ekbAnalogs = [
|
||||
{ address: 'ул. Репина, 73', area_m2: 54, rooms: 2, floor: 7, total_floors: 16, price_rub: 9_700_000, price_per_m2: 179_629, listing_date: '2026-05-12', days_on_market: 18, photo_url: null, source: 'avito', source_url: null, distance_m: 120, tier: null, lat: 56.8401, lon: 60.5702 },
|
||||
{ address: 'ул. Викулова, 33', area_m2: 57, rooms: 2, floor: 4, total_floors: 10, price_rub: 9_950_000, price_per_m2: 174_561, listing_date: '2026-05-20', days_on_market: 10, photo_url: null, source: 'cian', source_url: null, distance_m: 340, tier: null, lat: 56.8389, lon: 60.5681 },
|
||||
{ address: 'ул. Кирова, 28', area_m2: 53, rooms: 2, floor: 9, total_floors: 12, price_rub: 9_400_000, price_per_m2: 177_358, listing_date: '2026-05-04', days_on_market: 31, photo_url: null, source: 'avito', source_url: null, distance_m: 510, tier: null, lat: 56.8372, lon: 60.5749 },
|
||||
{ address: 'ул. Серафимы Дерябиной, 24', area_m2: 56, rooms: 2, floor: 6, total_floors: 14, price_rub: 9_600_000, price_per_m2: 171_429, listing_date: '2026-05-08', days_on_market: 42, photo_url: null, source: 'cian', source_url: null, distance_m: 680, tier: null, lat: 56.8318, lon: 60.5666 },
|
||||
{ address: 'ул. Ясная, 6', area_m2: 52, rooms: 2, floor: 11, total_floors: 16, price_rub: 9_600_000, price_per_m2: 184_615, listing_date: '2026-05-18', days_on_market: 14, photo_url: null, source: 'avito', source_url: null, distance_m: 430, tier: null, lat: 56.8295, lon: 60.5803 },
|
||||
{ address: 'ул. Белореченская, 17', area_m2: 58, rooms: 2, floor: 3, total_floors: 9, price_rub: 9_800_000, price_per_m2: 168_966, listing_date: '2026-04-28', days_on_market: 55, photo_url: null, source: 'yandex', source_url: null, distance_m: 900, tier: null, lat: 56.8267, lon: 60.5611 },
|
||||
{ address: 'ул. Гурзуфская, 16', area_m2: 55, rooms: 2, floor: 8, total_floors: 12, price_rub: 10_000_000, price_per_m2: 181_818, listing_date: '2026-05-22', days_on_market: 9, photo_url: null, source: 'cian', source_url: null, distance_m: 260, tier: null, lat: 56.8344, lon: 60.5727 },
|
||||
{ address: 'ул. Посадская, 40', area_m2: 51, rooms: 2, floor: 5, total_floors: 10, price_rub: 9_600_000, price_per_m2: 188_235, listing_date: '2026-05-15', days_on_market: 22, photo_url: null, source: 'avito', source_url: null, distance_m: 770, tier: null, lat: 56.8231, lon: 60.5832 },
|
||||
{ address: 'ул. Шаумяна, 86', area_m2: 60, rooms: 2, floor: 2, total_floors: 18, price_rub: 9_900_000, price_per_m2: 165_000, listing_date: '2026-04-25', days_on_market: 60, photo_url: null, source: 'yandex', source_url: null, distance_m: 1100, tier: null, lat: 56.8189, lon: 60.5598 },
|
||||
{ address: 'ул. Чкалова, 124', area_m2: 54, rooms: 2, floor: 12, total_floors: 19, price_rub: 10_400_000, price_per_m2: 192_593, listing_date: '2026-05-24', days_on_market: 7, photo_url: null, source: 'cian', source_url: null, distance_m: 350, tier: null, lat: 56.8276, lon: 60.5689 },
|
||||
];
|
||||
|
||||
const estimate = { ...FIXTURE_ESTIMATE, analogs: ekbAnalogs, n_analogs: ekbAnalogs.length };
|
||||
|
||||
/** Scatter «цена × срок экспозиции» по 10 аналогам (дешевле → быстрее),
|
||||
* вертикальная референс-линия на median 9.85 млн ₽. */
|
||||
export const Default = () => <ExposureCard estimate={estimate} />;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { HeroSummary } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from './_fixtures';
|
||||
|
||||
/** Секция 1 «Сводка» — медиана + достоверность CV + параметры объекта.
|
||||
* Фото-аналог = плейсхолдер (фикстура офлайн, photo_url: null). */
|
||||
export const Default = () => (
|
||||
<HeroSummary
|
||||
estimate={FIXTURE_ESTIMATE}
|
||||
input={FIXTURE_INPUT}
|
||||
onResubmit={() => {}}
|
||||
isResubmitting={false}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { HeroTransparency } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Блок «доверие + действия» в шапке результата — CTA «заявка» (лид-инбокс задан
|
||||
* в shim env), «Скачать PDF», свежесть данных и collapsible «Как рассчитано»
|
||||
* (band достоверности, аналоги, точность адреса). Generic-бренд (brandSlug=null). */
|
||||
export const Default = () => (
|
||||
<HeroTransparency estimate={FIXTURE_ESTIMATE} brandSlug={null} brandName={null} />
|
||||
);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { HouseAnalyticsKpiRow } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ANALYTICS } from './_fixtures';
|
||||
|
||||
/** KPI-строка аналитики дома — 3 карточки: средняя экспозиция, средний торг,
|
||||
* доля снятых (sold_count из total_lots). */
|
||||
export const Default = () => <HouseAnalyticsKpiRow kpi={FIXTURE_ANALYTICS.kpi} />;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { HouseAnalyticsSection } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Секция-обёртка «Аналитика дома» — fetch-coupled: тянет house-analytics +
|
||||
* sell-time-sensitivity по estimateId через TanStack Query. Глобальный provider
|
||||
* preview-режима с пустым кэшем → isPending → секция возвращает null (пустой
|
||||
* рендер). Содержимое покрыто отдельными ячейками HouseAnalyticsKpiRow /
|
||||
* PriceHistoryChart / RecentSoldList / SellTimeSensitivity. */
|
||||
export const Default = () => <HouseAnalyticsSection estimateId={FIXTURE_ESTIMATE.estimate_id} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { HouseInfoCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_HOUSES } from './_fixtures';
|
||||
|
||||
/** Карточка «Дом» — ближайший дом из выборки: адрес, рейтинг, параметры дома
|
||||
* (год, этажность, тип, лифты, двор, застройщик). length>0 → не null. */
|
||||
export const Default = () => <HouseInfoCard houses={FIXTURE_HOUSES} isLoading={false} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { IMVBenchmark } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_IMV } from './_fixtures';
|
||||
|
||||
/** Avito IMV benchmark — рекомендованная цена + диапазон + аналоги в рынке,
|
||||
* сравнение с нашей медианой (diff_pct). available:true → не null. */
|
||||
export const Default = () => <IMVBenchmark benchmark={FIXTURE_IMV} isLoading={false} />;
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { ListingsCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Секция 2 «Рынок» — объявления-аналоги: count-strip, фильтры/источники,
|
||||
* ценовой бар P25–P75 + таблица. cian-price-changes fetch офлайн пуст → бейджи
|
||||
* скидок не показываются (graceful), всё остальное берётся из estimate-пропа. */
|
||||
export const Default = () => (
|
||||
<ListingsCard estimate={FIXTURE_ESTIMATE} estimateId={FIXTURE_ESTIMATE.estimate_id} />
|
||||
);
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { LocationDrawer } from 'tradein-mvp-frontend';
|
||||
|
||||
// «ПОЯСНЕНИЕ К РАСЧЁТУ» — правый drawer HUD «МЕРА Оценка» (/trade-in/v2),
|
||||
// открывается с «?» у «КОЭФ. ЛОКАЦИИ» в HeroBar. Честная методика: как
|
||||
// агрегируются источники (Циан/Я.Недвижимость/Авито/Домклик + Росреестр) и
|
||||
// явная плашка «коэффициент локации в разработке». Контент статичный —
|
||||
// данных не принимает, только open/onClose. Drawer absolute-позиционирован
|
||||
// (width 452 + scrim) — рендерим contained внутри relative-обёртки.
|
||||
|
||||
/** Открытое состояние: scrim + выдвинутая панель с методикой и info-плашкой
|
||||
* о локации. */
|
||||
export const Default = () => (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 640,
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(1100px 520px at 30% -10%, #f7fbff 0%, #eef4fa 55%, #e6eef7 100%)',
|
||||
}}
|
||||
>
|
||||
<LocationDrawer open={true} onClose={() => {}} />
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { MapCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Карта аналитики — target + 3 аналога с гео-точками (Leaflet + OSM-тайлы).
|
||||
* NB: компонент тянет Leaflet с unpkg CDN + тайлы с tile.openstreetmap.org. В
|
||||
* офлайн-capture карта не грузится (loadLeaflet → error → mapError, либо серый
|
||||
* холст --surface-2). Card-обёртка (шапка/легенда/футер) рендерится в любом случае. */
|
||||
export const Default = () => <MapCard estimate={FIXTURE_ESTIMATE} />;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { MapPicker } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Модалка выбора адреса на карте ЕКБ (Leaflet+OSM). Рендерится через
|
||||
* createPortal в document.body как position:fixed оверлей — НЕ заперт в ячейке
|
||||
* грида. Leaflet тянется с CDN (офлайн → «не удалось загрузить карту»). */
|
||||
export const Default = () => (
|
||||
<MapPicker onPick={() => {}} onClose={() => {}} />
|
||||
);
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { NoAccessScreen } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Полноэкранный «доступа нет» — pure-props, 4 реальных варианта enum.
|
||||
* variant сменяет title + subtitle (trial — со ссылкой в Telegram). */
|
||||
export const UserDenied = () => <NoAccessScreen variant="user" />;
|
||||
|
||||
export const PathDenied = () => (
|
||||
<NoAccessScreen variant="path" path="/trade-in/scrapers/avito" />
|
||||
);
|
||||
|
||||
export const SessionExpired = () => <NoAccessScreen variant="session" />;
|
||||
|
||||
export const TrialEnded = () => <NoAccessScreen variant="trial" />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { OfferCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Секция 4 «Оффер» — разбивка издержек самостоятельной продажи на дефолтных
|
||||
* ставках (brandSlug меняет только PDF-endpoint, не вид). */
|
||||
export const Default = () => <OfferCard estimate={FIXTURE_ESTIMATE} brandSlug={null} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { PacingSection } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Pacing-интервалы запросов по провайдерам. Фетчит /admin/scraper/pacing через
|
||||
* useQuery (data-prop нет). В превью без сети — заголовок + hint +
|
||||
* graceful-degradation. Fetch-coupled. */
|
||||
export const Default = () => <PacingSection />;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { PhotoUpload } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Загрузка фото квартиры (#394). Карточка с заголовком, счётчиком N/12 и
|
||||
* файл-инпутом. Реальный UUID → активный аплоадер «0 / 12» (фетч списка фото
|
||||
* офлайн молча игнорируется). */
|
||||
export const Default = () => (
|
||||
<PhotoUpload estimateId="3f2a9c10-7b4d-4e8a-9f12-6c5b1a2d3e4f" />
|
||||
);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { PlacementHistoryCard } from 'tradein-mvp-frontend';
|
||||
|
||||
/** История продаж в доме. Fetch-coupled: данные тянет useEstimatePlacementHistory
|
||||
* по estimateId; при пустом query-кэше isPending → компонент возвращает null
|
||||
* (нет loading/empty-вёрстки). Передать данные пропом нельзя. */
|
||||
export const Default = () => (
|
||||
<PlacementHistoryCard estimateId="preview-0000-0000-0000-000000000000" />
|
||||
);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { PriceHistoryChart } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ANALYTICS } from './_fixtures';
|
||||
|
||||
/** «История цен в этом доме» — recharts line-chart медианы ₽/м² по годам
|
||||
* (Avito-серия; фикстура без Яндекс-точек → одна линия). */
|
||||
export const Default = () => (
|
||||
<PriceHistoryChart points={FIXTURE_ANALYTICS.price_history} />
|
||||
);
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { PriceRangeBar } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
const wrap = (node: React.ReactNode) => (
|
||||
<div style={{ maxWidth: 460, padding: 12 }}>{node}</div>
|
||||
);
|
||||
|
||||
/** Типовой коридор asking-цены с медианой по центру. */
|
||||
export const Default = () =>
|
||||
wrap(
|
||||
<PriceRangeBar
|
||||
rangeLow={FIXTURE_ESTIMATE.range_low_rub}
|
||||
rangeHigh={FIXTURE_ESTIMATE.range_high_rub}
|
||||
median={FIXTURE_ESTIMATE.median_price_rub}
|
||||
/>,
|
||||
);
|
||||
|
||||
/** Широкий разброс — медиана смещена влево. */
|
||||
export const WideSpread = () =>
|
||||
wrap(<PriceRangeBar rangeLow={7_400_000} rangeHigh={12_900_000} median={9_100_000} />);
|
||||
|
||||
/** Узкий коридор — высокая достоверность. */
|
||||
export const Tight = () =>
|
||||
wrap(<PriceRangeBar rangeLow={9_600_000} rangeHigh={10_100_000} median={9_850_000} />);
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { PriceTrendCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** «Динамика ₽/м²» — линия recharts по 6 месяцам (168k → 178k), дельта в шапке. */
|
||||
export const Default = () => <PriceTrendCard estimate={FIXTURE_ESTIMATE} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { ProviderProxySection } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Прокси-блок внутри вкладки провайдера. Фетчит /admin/scraper/health через
|
||||
* useScraperHealth (по source), data-prop нет. В превью без сети — заголовок
|
||||
* «Прокси» + loading/error. Fetch-coupled. */
|
||||
export const Default = () => <ProviderProxySection source="avito" />;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { RecentSoldList } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ANALYTICS } from './_fixtures';
|
||||
|
||||
/** «Недавно снятые (12 мес)» — таблица лотов: комнаты/площадь/этаж, цена, торг,
|
||||
* дата снятия, экспозиция. */
|
||||
export const Default = () => (
|
||||
<RecentSoldList items={FIXTURE_ANALYTICS.recent_sold} />
|
||||
);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { ResultPanel } from 'tradein-mvp-frontend';
|
||||
|
||||
/**
|
||||
* ResultPanel (v2) — центральная панель результата оценки на `/trade-in/v2`:
|
||||
* одна цифра-герой (ожидаемая цена продажи / «оценка»), тиры median + ДКП,
|
||||
* доверительные диапазоны, источники и мини-гистограмма распределения.
|
||||
* Данные по умолчанию берутся из встроенной `RESULT_FIXTURE` компонента,
|
||||
* поэтому карточке достаточно передать `onNavigate` (навигация по секциям).
|
||||
*/
|
||||
export const Default = () => <ResultPanel onNavigate={() => {}} />;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { RouteGuard } from 'tradein-mvp-frontend';
|
||||
|
||||
/** RBAC-обёртка. useMe() читает /api/v1/me — в превью-окружении сети нет,
|
||||
* поэтому guard рендерит свой gated-state (NoAccessScreen "Доступа нет" при
|
||||
* ошибке /me, либо ничего во время загрузки). Fetch-coupled на useMe. */
|
||||
export const Default = () => (
|
||||
<RouteGuard>
|
||||
<div className="card" style={{ padding: 24 }}>
|
||||
<h2 style={{ margin: 0 }}>Защищённый раздел</h2>
|
||||
<p style={{ margin: '8px 0 0', color: 'var(--fg-secondary)' }}>
|
||||
Этот контент виден только при разрешённой роли.
|
||||
</p>
|
||||
</div>
|
||||
</RouteGuard>
|
||||
);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { RunsTable } from 'tradein-mvp-frontend';
|
||||
|
||||
/** История прогонов скраппера. Фетчит /admin/scrape/runs через useQuery
|
||||
* (data-prop нет), но заголовок + hint + фильтры (источник / статус-чипы)
|
||||
* рендерятся всегда — это видимый styled-каркас. Таблица fetch-coupled. */
|
||||
export const Default = () => <RunsTable source="avito" />;
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { SaleShareControls } from 'tradein-mvp-frontend';
|
||||
|
||||
// Панель «Порог и фильтры» страницы /trade-in/sale-share. Controlled-компонент:
|
||||
// состояние живёт в page.tsx и приходит через props. Сводка (гистограмма
|
||||
// распределения sale_share_pct) — реалистичные значения по покрытию ЕКБ
|
||||
// (view v_building_sale_share ≈ 6.7k домов вторички, знаменатель ГАР у ~92%).
|
||||
const summary = {
|
||||
total_secondary_buildings: 6667,
|
||||
buildings_with_denominator: 6143,
|
||||
coverage_pct: 92.1,
|
||||
max_pct: 42.0,
|
||||
p95_pct: 11.3,
|
||||
histogram: [
|
||||
{ bucket: '0-5', count: 4980 },
|
||||
{ bucket: '5-10', count: 642 },
|
||||
{ bucket: '10-20', count: 298 },
|
||||
{ bucket: '20-30', count: 121 },
|
||||
{ bucket: '30-50', count: 57 },
|
||||
{ bucket: '50-100', count: 18 },
|
||||
{ bucket: '100+', count: 6 },
|
||||
],
|
||||
};
|
||||
|
||||
const HOUSE_TYPES = ['panel', 'brick', 'monolith', 'monolith_brick', 'block', 'stalin'];
|
||||
|
||||
/** Порог 8% на слайдере: корзины ниже порога приглушены (opacity), тепловая
|
||||
* окраска столбцов green→red повторяет цвет маркеров карты. Фильтры: город,
|
||||
* цена, год постройки, тип дома, мин. объявлений, сортировка. */
|
||||
export const Default = () => (
|
||||
<SaleShareControls
|
||||
summary={summary}
|
||||
minPct={8}
|
||||
onMinPct={() => {}}
|
||||
city="Екатеринбург"
|
||||
onCity={() => {}}
|
||||
priceMin=""
|
||||
onPriceMin={() => {}}
|
||||
priceMax=""
|
||||
onPriceMax={() => {}}
|
||||
yearMin=""
|
||||
onYearMin={() => {}}
|
||||
yearMax=""
|
||||
onYearMax={() => {}}
|
||||
houseType=""
|
||||
onHouseType={() => {}}
|
||||
houseTypeOptions={HOUSE_TYPES}
|
||||
sort="share_desc"
|
||||
onSort={() => {}}
|
||||
window="now"
|
||||
onWindow={() => {}}
|
||||
minCount={3}
|
||||
onMinCount={() => {}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { SaleShareList } from 'tradein-mvp-frontend';
|
||||
|
||||
// Список домов /trade-in/sale-share, отсортирован по доле в продаже (server-side
|
||||
// share_desc). Реалистичные дома ЕКБ (view v_building_sale_share): адрес,
|
||||
// тепловой бейдж доли, «N из M квартир», медиана + ₽/м², экспозиция,
|
||||
// год/тип/этажность/серия. Включены оба edge-бейджа: «аварийный» и
|
||||
// «возможно, несколько корпусов» (over_100 — ГАР-коллизия адреса).
|
||||
const buildings = [
|
||||
{ house_id: 4212, address: 'ул. Космонавтов, 52', lat: 56.8890, lon: 60.6132, sale_share_pct: 128.0, sale_share_pct_45d: 131.5, listings_45d: 46, over_100: true, active_secondary: 32, flat_count_effective: 25, gar_match_method: 'address', median_price_rub: 3_650_000, median_price_per_m2: 121_700, avg_days_on_market: 96, year_built: 1961, house_type: 'brick', total_floors: 5, series_name: null, is_emergency: false },
|
||||
{ house_id: 3387, address: 'пер. Сапёров, 5', lat: 56.8271, lon: 60.6198, sale_share_pct: 48.9, sale_share_pct_45d: 52.4, listings_45d: 24, over_100: false, active_secondary: 22, flat_count_effective: 45, gar_match_method: 'cadastre', median_price_rub: 2_990_000, median_price_per_m2: 98_400, avg_days_on_market: 148, year_built: 1957, house_type: 'brick', total_floors: 3, series_name: null, is_emergency: true },
|
||||
{ house_id: 3101, address: 'ул. Викулова, 46', lat: 56.8412, lon: 60.5556, sale_share_pct: 34.6, sale_share_pct_45d: 41.2, listings_45d: 33, over_100: false, active_secondary: 27, flat_count_effective: 78, gar_match_method: 'cadastre', median_price_rub: 4_950_000, median_price_per_m2: 158_500, avg_days_on_market: 74, year_built: 1972, house_type: 'panel', total_floors: 9, series_name: '1-468', is_emergency: false },
|
||||
{ house_id: 2874, address: 'ул. Бебеля, 138', lat: 56.8664, lon: 60.5721, sale_share_pct: 22.4, sale_share_pct_45d: 25.0, listings_45d: 20, over_100: false, active_secondary: 17, flat_count_effective: 76, gar_match_method: 'cadastre', median_price_rub: 5_400_000, median_price_per_m2: 149_200, avg_days_on_market: 61, year_built: 1978, house_type: 'panel', total_floors: 9, series_name: '141', is_emergency: false },
|
||||
{ house_id: 5530, address: 'ул. Малышева, 84', lat: 56.8380, lon: 60.6203, sale_share_pct: 12.1, sale_share_pct_45d: 13.8, listings_45d: 16, over_100: false, active_secondary: 14, flat_count_effective: 116, gar_match_method: 'cadastre', median_price_rub: 7_850_000, median_price_per_m2: 172_300, avg_days_on_market: 47, year_built: 1954, house_type: 'stalin', total_floors: 6, series_name: null, is_emergency: false },
|
||||
{ house_id: 6119, address: 'ул. Щербакова, 20', lat: 56.7791, lon: 60.6120, sale_share_pct: 6.8, sale_share_pct_45d: 8.1, listings_45d: 22, over_100: false, active_secondary: 18, flat_count_effective: 264, gar_match_method: 'cadastre', median_price_rub: 8_900_000, median_price_per_m2: 164_800, avg_days_on_market: 38, year_built: 2016, house_type: 'monolith', total_floors: 25, series_name: null, is_emergency: false },
|
||||
];
|
||||
|
||||
/** Полная таблица (6 домов, выбран «ул. Викулова, 46» — строка is-selected),
|
||||
* сортировка по доле, кнопка «Объявления →» ведёт в drawer. */
|
||||
export const Default = () => (
|
||||
<SaleShareList
|
||||
buildings={buildings}
|
||||
sort="share_desc"
|
||||
onSort={() => {}}
|
||||
selectedHouseId={3101}
|
||||
hoveredHouseId={null}
|
||||
onSelect={() => {}}
|
||||
onHover={() => {}}
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
minPct={5}
|
||||
window="now"
|
||||
/>
|
||||
);
|
||||
|
||||
/** Пустой результат — фильтры отсекли все дома (честный empty-state). */
|
||||
export const Empty = () => (
|
||||
<SaleShareList
|
||||
buildings={[]}
|
||||
sort="share_desc"
|
||||
onSort={() => {}}
|
||||
selectedHouseId={null}
|
||||
hoveredHouseId={null}
|
||||
onSelect={() => {}}
|
||||
onHover={() => {}}
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
minPct={25}
|
||||
window="now"
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { SaleShareMap } from 'tradein-mvp-frontend';
|
||||
|
||||
// Тепловая карта домов /trade-in/sale-share (Leaflet + OSM с CDN, как
|
||||
// MapCard/MapPicker). Цвет и радиус circleMarker растут с sale_share_pct
|
||||
// (green → red, over_100 → тёмно-красный). 8 домов ЕКБ с координатами;
|
||||
// выбранный дом (selectedHouseId) получает открытый popup. OSM-тайлы —
|
||||
// внешний fetch: в офлайн-capture подложка может не прогрузиться,
|
||||
// маркеры/popup/контролы рендерятся всегда.
|
||||
const buildings = [
|
||||
{ house_id: 4212, address: 'ул. Космонавтов, 52', lat: 56.8890, lon: 60.6132, sale_share_pct: 128.0, sale_share_pct_45d: 131.5, listings_45d: 46, over_100: true, active_secondary: 32, flat_count_effective: 25, gar_match_method: 'address', median_price_rub: 3_650_000, median_price_per_m2: 121_700, avg_days_on_market: 96, year_built: 1961, house_type: 'brick', total_floors: 5, series_name: null, is_emergency: false },
|
||||
{ house_id: 3387, address: 'пер. Сапёров, 5', lat: 56.8271, lon: 60.6198, sale_share_pct: 48.9, sale_share_pct_45d: 52.4, listings_45d: 24, over_100: false, active_secondary: 22, flat_count_effective: 45, gar_match_method: 'cadastre', median_price_rub: 2_990_000, median_price_per_m2: 98_400, avg_days_on_market: 148, year_built: 1957, house_type: 'brick', total_floors: 3, series_name: null, is_emergency: true },
|
||||
{ house_id: 3101, address: 'ул. Викулова, 46', lat: 56.8412, lon: 60.5556, sale_share_pct: 34.6, sale_share_pct_45d: 41.2, listings_45d: 33, over_100: false, active_secondary: 27, flat_count_effective: 78, gar_match_method: 'cadastre', median_price_rub: 4_950_000, median_price_per_m2: 158_500, avg_days_on_market: 74, year_built: 1972, house_type: 'panel', total_floors: 9, series_name: '1-468', is_emergency: false },
|
||||
{ house_id: 2874, address: 'ул. Бебеля, 138', lat: 56.8664, lon: 60.5721, sale_share_pct: 22.4, sale_share_pct_45d: 25.0, listings_45d: 20, over_100: false, active_secondary: 17, flat_count_effective: 76, gar_match_method: 'cadastre', median_price_rub: 5_400_000, median_price_per_m2: 149_200, avg_days_on_market: 61, year_built: 1978, house_type: 'panel', total_floors: 9, series_name: '141', is_emergency: false },
|
||||
{ house_id: 5530, address: 'ул. Малышева, 84', lat: 56.8380, lon: 60.6203, sale_share_pct: 12.1, sale_share_pct_45d: 13.8, listings_45d: 16, over_100: false, active_secondary: 14, flat_count_effective: 116, gar_match_method: 'cadastre', median_price_rub: 7_850_000, median_price_per_m2: 172_300, avg_days_on_market: 47, year_built: 1954, house_type: 'stalin', total_floors: 6, series_name: null, is_emergency: false },
|
||||
{ house_id: 6119, address: 'ул. Щербакова, 20', lat: 56.7791, lon: 60.6120, sale_share_pct: 6.8, sale_share_pct_45d: 8.1, listings_45d: 22, over_100: false, active_secondary: 18, flat_count_effective: 264, gar_match_method: 'cadastre', median_price_rub: 8_900_000, median_price_per_m2: 164_800, avg_days_on_market: 38, year_built: 2016, house_type: 'monolith', total_floors: 25, series_name: null, is_emergency: false },
|
||||
{ house_id: 5871, address: 'ул. Крауля, 44', lat: 56.8443, lon: 60.5610, sale_share_pct: 17.9, sale_share_pct_45d: 19.6, listings_45d: 14, over_100: false, active_secondary: 12, flat_count_effective: 67, gar_match_method: 'cadastre', median_price_rub: 5_150_000, median_price_per_m2: 152_900, avg_days_on_market: 58, year_built: 1980, house_type: 'panel', total_floors: 9, series_name: '141', is_emergency: false },
|
||||
{ house_id: 6402, address: 'ул. 8 Марта, 190', lat: 56.8043, lon: 60.6094, sale_share_pct: 3.4, sale_share_pct_45d: 4.0, listings_45d: 11, over_100: false, active_secondary: 9, flat_count_effective: 262, gar_match_method: 'cadastre', median_price_rub: 9_300_000, median_price_per_m2: 176_400, avg_days_on_market: 33, year_built: 2019, house_type: 'monolith', total_floors: 26, series_name: null, is_emergency: false },
|
||||
];
|
||||
|
||||
/** Карта ЕКБ: 8 тепловых маркеров, выбран «ул. Викулова, 46» — открыт popup
|
||||
* (адрес, % в продаже, N из M квартир). */
|
||||
export const Default = () => (
|
||||
<SaleShareMap
|
||||
buildings={buildings}
|
||||
selectedHouseId={3101}
|
||||
hoveredHouseId={null}
|
||||
onSelect={() => {}}
|
||||
onHover={() => {}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { ScheduleControl } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Расписание + ручной запуск city-sweep. Фетчит /admin/scrape/schedules через
|
||||
* useSchedules (статус-блок fetch-coupled), но форма настроек (окно МСК,
|
||||
* pages/anchor, detail top-N, delay, радиус, enrich) рендерится всегда —
|
||||
* полный styled-каркас формы. paramConfig — avito-дефолты (с detail-параметрами). */
|
||||
const AVITO_PARAMS = {
|
||||
hasDetailParams: true,
|
||||
hasEnrichAddress: false,
|
||||
defaults: {
|
||||
pages_per_anchor: 3,
|
||||
radius_m: 1500,
|
||||
request_delay_sec: 4,
|
||||
detail_top_n: 12,
|
||||
enrich_houses: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
<ScheduleControl
|
||||
source="avito"
|
||||
scheduleSource="avito_city_sweep"
|
||||
paramConfig={AVITO_PARAMS}
|
||||
/>
|
||||
);
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { SectionOverlay } from 'tradein-mvp-frontend';
|
||||
|
||||
// Glass/blur оверлей секций HUD «МЕРА Оценка» (/trade-in/v2): скобки-уголки,
|
||||
// шапка «номер секции + заголовок + ← К ОЦЕНКЕ», скроллируемое тело со
|
||||
// сменной view. Позиционируется absolute относительно артборда — в preview
|
||||
// рендерим contained внутри relative-обёртки с фоном v2-страницы.
|
||||
// Данные не передаём: каждая view падает на свой встроенный fixture-набор
|
||||
// (HISTORY_FIXTURE / ANALYTICS_FIXTURE …) — как storybook/unwired usage.
|
||||
|
||||
const Artboard = ({ children }: { children?: unknown }) => (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 680,
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(1100px 520px at 30% -10%, #f7fbff 0%, #eef4fa 55%, #e6eef7 100%)',
|
||||
}}
|
||||
>
|
||||
{children as React.ReactNode}
|
||||
</div>
|
||||
);
|
||||
|
||||
/** Секция 04 «ПРОДАЖИ В ДОМЕ» (active=1 → HistoryView на fixture-данных):
|
||||
* таблица ДКП-продаж дома с ценами и датами. */
|
||||
export const HistorySection = () => (
|
||||
<Artboard>
|
||||
<SectionOverlay active={1} onClose={() => {}} onNavigate={() => {}} />
|
||||
</Artboard>
|
||||
);
|
||||
|
||||
/** Секция 06 «АНАЛИТИКА ДОМА» (active=3 → AnalyticsView): KPI дома,
|
||||
* динамика цены, недавние продажи. */
|
||||
export const AnalyticsSection = () => (
|
||||
<Artboard>
|
||||
<SectionOverlay active={3} onClose={() => {}} onNavigate={() => {}} />
|
||||
</Artboard>
|
||||
);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { SellTimeSensitivity } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_SELLTIME } from './_fixtures';
|
||||
|
||||
/** «Срок продажи в зависимости от цены» — 4 бакета премии (−5% / медиана / +5% /
|
||||
* +10%) с медианой экспозиции и p25–p75. Принимает data-пропом напрямую. */
|
||||
export const Default = () => <SellTimeSensitivity data={FIXTURE_SELLTIME} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { SourcesProgress } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** Шаг B «Агрегация» — 7 строк-источников. Циан/Авито/Росреестр = done (с лотами),
|
||||
* ДомКлик/Restate/Я.Недв/N1 = idle. isPending=false → финальное состояние, бар 100%. */
|
||||
export const Default = () => <SourcesProgress estimate={FIXTURE_ESTIMATE} isPending={false} />;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { StreetDealsCard } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE } from './_fixtures';
|
||||
|
||||
/** «По вашей улице» — ДКП-сделки Росреестра + историч. ASK.
|
||||
* FETCH-COUPLED: данные ТОЛЬКО из useSalesVsListings (нет data-prop). Глобальный
|
||||
* query-cache в preview пуст → isLoading/isError → `return null`. В офлайн-capture
|
||||
* карточка рендерится пусто; на реальной странице — таблица сделок с привязкой к ASK. */
|
||||
export const Default = () => <StreetDealsCard estimate={FIXTURE_ESTIMATE} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { SystemHealthSection } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Статус системы скраппера: fetch-режим, browser-сервис, прокси провайдеров.
|
||||
* Фетчит /admin/scraper/health через useScraperHealth (data-prop нет). В превью
|
||||
* без сети — заголовок + hint + loading/error. Fetch-coupled. */
|
||||
export const Default = () => <SystemHealthSection />;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { TestPresets } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Быстрое заполнение формы тестовыми квартирами ЕКБ — сетка из 6 preset-чипов
|
||||
* (адрес + подсказка по покрытию). Self-contained (`<style>` внутри). */
|
||||
export const Default = () => <TestPresets onPick={() => {}} />;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { Topbar } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Шапка приложения «Мера» — бренд-марка + навигация. useMe/useBrand офлайн
|
||||
* не резолвятся → fallback показывает полный набор nav-айтемов; UserMenu сам
|
||||
* рендерит null. Активная вкладка — «Оценка». */
|
||||
export const Default = () => <Topbar active="estimate" />;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { UserMenu } from 'tradein-mvp-frontend';
|
||||
|
||||
/** Аватар-кнопка личного кабинета в Topbar. Читает useMe() — без данных /me
|
||||
* (isLoading || error || !data) компонент намеренно возвращает null.
|
||||
* Fetch-coupled: в превью без сети рендерит пусто. */
|
||||
export const Default = () => (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: 12 }}>
|
||||
<UserMenu />
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { WhatIfPanel } from 'tradein-mvp-frontend';
|
||||
import { FIXTURE_ESTIMATE, FIXTURE_INPUT } from './_fixtures';
|
||||
|
||||
/** Интерактив «Что-если» — segmented ремонт + этаж (stepper/slider) + площадь +
|
||||
* балкон, авто-пересчёт. Без взаимодействия показывает baseline-оценку
|
||||
* (mutation idle). */
|
||||
export const Default = () => (
|
||||
<WhatIfPanel baseEstimate={FIXTURE_ESTIMATE} baseInput={FIXTURE_INPUT} />
|
||||
);
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
// Shared preview fixtures — re-export the repo's own offline UI-preview fixture
|
||||
// (realistic ЕКБ 2-к secondary). Type imports inside are erased by esbuild.
|
||||
export * from '../../tradein-mvp/frontend/src/app/ui-preview/estimate/fixture';
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
name: CI Trade-In
|
||||
|
||||
# Forgejo Actions pre-merge gate for the SUBPROJECT tradein-mvp/.
|
||||
# WHY THIS FILE EXISTS (#2208): основной .forgejo/workflows/ci.yml гейтит
|
||||
# ТОЛЬКО backend/** + frontend/** главного стека — tradein-PR проходили на
|
||||
# пусто-зелёных чеках (paths-filter no-op), а pytest tradein жил лишь в
|
||||
# post-merge deploy-tradein.yml. Итог: сломанный tradein-код мержился в main
|
||||
# и обнаруживался только на деплое. Этот workflow добавляет РЕАЛЬНЫЙ pre-merge
|
||||
# gate: tradein-backend pytest + tradein-frontend type-check/lint ДО мержа.
|
||||
on:
|
||||
# ТОЛЬКО pull_request — НЕТ push-триггера на feature-ветки (CI-шторм #1709,
|
||||
# см. подробное обоснование в ci.yml). Кратко: раньше push+pull_request на один
|
||||
# SHA давали разный github.ref → разные concurrency-группы → 2× прогон на
|
||||
# дефицитных раннерах. В bot-пайплайне каждый коммит идёт через PR, так что
|
||||
# pull_request гейтит его полностью; push-прогон был чистым дублем.
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
# github.ref стабилен на весь PR (refs/pull/<N>/merge) → новый push в ветку PR
|
||||
# отменяет предыдущий незавершённый прогон ЭТОГО PR вместо накопления.
|
||||
group: ci-tradein-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Paths-filter: гейт бежит ТОЛЬКО когда поменялся tradein-код.
|
||||
# PR не трогающий tradein-mvp/ → оба job'а no-op'ятся → дёшево.
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- 'tradein-mvp/backend/**'
|
||||
- 'tradein-mvp/packages/**'
|
||||
# workspace root: lock-only bump (uv lock --upgrade) или правка
|
||||
# [tool.uv.workspace] меняют реальные зависимости → гейт обязан бежать.
|
||||
- 'tradein-mvp/uv.lock'
|
||||
- 'tradein-mvp/pyproject.toml'
|
||||
- '.forgejo/workflows/ci-tradein.yml'
|
||||
frontend:
|
||||
- 'tradein-mvp/frontend/**'
|
||||
- '.forgejo/workflows/ci-tradein.yml'
|
||||
|
||||
backend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.backend == 'true'
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./tradein-mvp/backend
|
||||
env:
|
||||
# psycopg v3 требует parseable URL на импорте; реального коннекта нет —
|
||||
# DB-тесты мокаются (mirror deploy-tradein.yml test-job).
|
||||
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
# Официальный standalone-инсталлер. НЕ astral-sh/setup-uv — он ломается
|
||||
# на Forgejo-runner с PEP 668 externally-managed-environment (#666 CI).
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache uv packages
|
||||
# Кросс-прогонный кэш скачанных/собранных wheel'ов (~/.cache/uv).
|
||||
# continue-on-error чтобы сбой cache-бэкенда раннера НИКОГДА не ронял gate.
|
||||
# Ключ по workspace-локу tradein-mvp/uv.lock (tracked с воркспейса #2137).
|
||||
uses: actions/cache@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ~/.cache/uv
|
||||
key: uv-tradein-${{ runner.os }}-${{ hashFiles('tradein-mvp/uv.lock') }}
|
||||
restore-keys: |
|
||||
uv-tradein-${{ runner.os }}-
|
||||
|
||||
- name: Sync deps (incl. dev group — pytest)
|
||||
# Workspace-лок tradein-mvp/uv.lock TRACKED (с воркспейса #2137; gitignored
|
||||
# только старый backend/uv.lock) → --frozen детерминирован и зеркалит
|
||||
# Dockerfile (uv sync --frozen --no-dev там). uv находит workspace root
|
||||
# вверх от cwd.
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Run pytest (tradein-mvp/backend)
|
||||
# DESELECT (актуализировано 2026-07-02, #2208): test_search_cache_hit падает
|
||||
# ТОЛЬКО в whole-suite ordering (401 vs 200; в изоляции проходит) — global-state
|
||||
# leak из другого test-модуля, pre-existing. Второй исторический deselect
|
||||
# (test_cian_valuation::test_cache_hit_returns_cached) убран — проходит в
|
||||
# полном прогоне (проверено локально: 2947 passed / 1 failed). Список обязан
|
||||
# совпадать с test-job в deploy-tradein.yml.
|
||||
run: |
|
||||
uv run pytest -q \
|
||||
--deselect "tests/test_search_api.py::test_search_cache_hit"
|
||||
|
||||
frontend-checks:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./tradein-mvp/frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
# Node 20 — major из tradein-mvp/frontend/Dockerfile (node:20-alpine).
|
||||
# npm-кэш setup-node НЕ настраиваем: в tradein-mvp/frontend нет
|
||||
# package-lock.json (Dockerfile ставит через npm install), а cache=npm
|
||||
# без lockfile падает. Кэш wheels/node тут не критичен для type-check/lint.
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install deps (npm install, no lockfile)
|
||||
# ТОЧНЫЕ флаги из tradein-mvp/frontend/Dockerfile (deps stage):
|
||||
# --legacy-peer-deps — Tailwind/React 19 peer-dep mismatches;
|
||||
# --no-audit --no-fund — тише и быстрее в CI. `install` (не `ci`):
|
||||
# в tradein-mvp/frontend НЕТ package-lock.json (есть pnpm-lock.yaml, но
|
||||
# Dockerfile ставит именно npm install) → `npm ci` упал бы.
|
||||
run: npm install --legacy-peer-deps --no-audit --no-fund
|
||||
|
||||
- name: Type-check (tsc --noEmit)
|
||||
# Blocking: любая TS-ошибка → job RED.
|
||||
run: npm run type-check
|
||||
|
||||
- name: Lint (next lint)
|
||||
# Blocking: любая ESLint-ошибка → job RED.
|
||||
run: npm run lint
|
||||
|
|
@ -15,22 +15,23 @@ name: CI
|
|||
# FUTURE: добавить `postgis/postgis:16-3.4` service + гонять mv_layout — см.
|
||||
# .github/workflows/ci.yml как образец service-блока.
|
||||
on:
|
||||
# ТОЛЬКО pull_request — НЕТ push-триггера на feature-ветки (CI-шторм #1709).
|
||||
# WHY: раньше был и push: [feat/**,fix/**,...]. Каждый коммит в ветку с открытым
|
||||
# PR триггерил ДВА прогона на ОДИН SHA: push-событие (github.ref=refs/heads/<branch>)
|
||||
# и pull_request-событие (github.ref=refs/pull/<N>/merge). Разный github.ref →
|
||||
# разные concurrency-группы (см. ниже) → прогоны НЕ отменяют друг друга → 2× job
|
||||
# при и так дефицитных раннерах. В bot-пайплайне каждый коммит идёт через PR, так
|
||||
# что pull_request гейтит его полностью; push-прогон был чистым дублем.
|
||||
# Trade-off: push в feature-ветку БЕЗ открытого PR не получит CI до открытия PR
|
||||
# (бот открывает PR сразу после первого push) — приемлемо.
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches:
|
||||
# Mirror the bot-PR / feature-branch flow в .claude/rules/git-pr.md:
|
||||
# PR получает gate, прямые пуши в feature-ветки — тоже.
|
||||
- "feat/**"
|
||||
- "fix/**"
|
||||
- "refactor/**"
|
||||
- "chore/**"
|
||||
- "docs/**"
|
||||
- "perf/**"
|
||||
- "test/**"
|
||||
- "ci/**"
|
||||
- "hotfix/**"
|
||||
|
||||
concurrency:
|
||||
# Теперь, когда остался только pull_request, github.ref стабилен на весь PR
|
||||
# (refs/pull/<N>/merge) → новый push в ветку PR отменяет предыдущий незавершённый
|
||||
# прогон ЭТОГО PR (cancel-in-progress) вместо накопления параллельных.
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
|
@ -42,7 +43,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
|
|
@ -53,9 +53,6 @@ jobs:
|
|||
- 'backend/**'
|
||||
- 'data/sql/**'
|
||||
- '.forgejo/workflows/ci.yml'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
- '.forgejo/workflows/ci.yml'
|
||||
|
||||
backend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -88,19 +85,6 @@ jobs:
|
|||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache uv packages
|
||||
# Кросс-прогонный кэш скачанных/собранных wheel'ов (~/.cache/uv по умолч.).
|
||||
# `uv sync --frozen` без него каждый прогон тянет весь geo-стек заново —
|
||||
# доминирующая часть времени job (#1709). Ключ по uv.lock; continue-on-error
|
||||
# чтобы сбой cache-бэкенда раннера НИКОГДА не ронял gate.
|
||||
uses: actions/cache@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ~/.cache/uv
|
||||
key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }}
|
||||
restore-keys: |
|
||||
uv-${{ runner.os }}-
|
||||
|
||||
- name: Install system deps for geo + WeasyPrint
|
||||
# libpq/gdal/proj/geos — geo-стек (geopandas/shapely/pyproj).
|
||||
# libcairo2/libpango* — нативные либы WeasyPrint: с ними PDF-тесты
|
||||
|
|
@ -122,7 +106,7 @@ jobs:
|
|||
# format-pass = future enhancement. `ruff check .` сейчас green.
|
||||
run: uv run ruff check .
|
||||
|
||||
- name: Test (pytest + coverage gate)
|
||||
- name: Test (pytest)
|
||||
# --ignore=tests/smoke: prod-smoke бьёт по live https://gendsgn.ru
|
||||
# (помечены @pytest.mark.prod_smoke; pyproject addopts уже их deselect'ит,
|
||||
# но --ignore — belt-and-suspenders на случай сбора фикстур).
|
||||
|
|
@ -131,172 +115,4 @@ jobs:
|
|||
# tests/sql/ mv_layout self-skip'ается через Postgres-connectivity probe
|
||||
# (5432 недоступен в этом mock-lane) → SKIP. Это intended.
|
||||
# PDF-тесты ИДУТ (libpango выше). Target: 0 failed, skips OK.
|
||||
#
|
||||
# Coverage-gate (#68): --cov=app меряет покрытие пакета app/.
|
||||
# --cov-fail-under=65 → job RED если покрытие упало ниже baseline
|
||||
# (измерено 2026-06: mock-lane сьют ~71%, см. [tool.coverage] в pyproject;
|
||||
# 65 = floor с запасом, не flaky). На CI PDF-тесты РЕАЛЬНО идут (libpango),
|
||||
# поэтому реальное CI-покрытие ≥ локально-измеренного floor.
|
||||
# coverage.xml — артефакт для будущего Codecov/Coveralls upload (#68 badge).
|
||||
# term-missing → видно непокрытые строки прямо в job-логе.
|
||||
run: |
|
||||
uv run pytest -q --ignore=tests/smoke \
|
||||
--cov=app \
|
||||
--cov-report=term-missing:skip-covered \
|
||||
--cov-report=xml:coverage.xml \
|
||||
--cov-fail-under=65
|
||||
|
||||
- name: Coverage summary → job output
|
||||
# Дешёвый human-readable итог. Бежит даже если gate упал (if: always) —
|
||||
# чтобы было видно НАСКОЛЬКО просело покрытие, а не только "fail".
|
||||
# GITHUB_STEP_SUMMARY поддержан не во всех версиях Forgejo act_runner →
|
||||
# если переменная пустая/файла нет, печатаем в обычный лог (fallback).
|
||||
if: always()
|
||||
run: |
|
||||
[ -f coverage.xml ] || { echo "coverage.xml отсутствует — пропускаю summary"; exit 0; }
|
||||
report="$(uv run coverage report --skip-covered --sort=cover | tail -40)"
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
{ echo '```'; echo "$report"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "$report"
|
||||
fi
|
||||
|
||||
frontend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.frontend == 'true'
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
# Node 20 — совпадает с major из frontend/Dockerfile (node:20-alpine).
|
||||
# cache=npm + cache-dependency-path на lockfile → переиспользуем ~/.npm
|
||||
# между прогонами (mirror Dockerfile's `--mount=type=cache,target=/root/.npm`).
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install deps (npm ci, frozen lockfile)
|
||||
# ТОЧНЫЕ флаги из frontend/Dockerfile (deps stage):
|
||||
# --legacy-peer-deps — Tailwind 4 alpha + React 19 peer-dep mismatches;
|
||||
# --no-audit --no-fund — тише и быстрее в CI. `ci` (не `install`) =
|
||||
# детерминированно из package-lock.json, fail при дрейфе lock vs package.json.
|
||||
run: npm ci --legacy-peer-deps --no-audit --no-fund
|
||||
|
||||
- name: Test (vitest)
|
||||
# `npm run test` = `vitest run` (single-shot, не watch). Только тесты —
|
||||
# `next build` НАМЕРЕННО не здесь (тяжёлый, verified в deploy.yml build).
|
||||
run: npm run test
|
||||
|
||||
# OpenAPI → TS codegen drift-gate (#69). Защищает фронт от молчаливого
|
||||
# рассинхрона типов: если backend изменил OpenAPI-схему, а
|
||||
# frontend/src/lib/api-types.ts не перегенерён (`npm run codegen`) — job RED.
|
||||
#
|
||||
# Бежит когда поменялся backend ИЛИ frontend (backend-change может застейлить
|
||||
# типы даже без правок во frontend/). Отдельный job (не внутри frontend-tests),
|
||||
# чтобы vitest и codegen-gate скейлились независимо.
|
||||
#
|
||||
# WHY no running server: `npm run codegen` бьёт по http://localhost:8000/openapi.json,
|
||||
# но схема = app.openapi() — её можно сдампить из python БЕЗ uvicorn/DB
|
||||
# (app.main импортируется под TESTING=1 со stub-DSN, как в backend-tests).
|
||||
# Это убирает flaky port-wait. openapi-typescript v7 принимает локальный файл.
|
||||
#
|
||||
# WHY prettier: committed api-types.ts форматируется prettier'ом через
|
||||
# pre-commit hook (mirrors-prettier, no config → defaults, 2-space). Raw
|
||||
# openapi-typescript отдаёт 4-space → diff-шум. Прогоняем prettier (defaults)
|
||||
# на regen, чтобы сравнивать ТОЛЬКО контент, не форматирование.
|
||||
openapi-codegen-check:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: |
|
||||
needs.changes.outputs.backend == 'true' ||
|
||||
needs.changes.outputs.frontend == 'true'
|
||||
env:
|
||||
# Те же stub-переменные, что backend-tests: psycopg требует parseable URL
|
||||
# на импорте; реального коннекта нет (схему дампим, не обслуживаем запросы).
|
||||
TESTING: "1"
|
||||
DATABASE_URL: postgresql+psycopg://test:test@localhost:5432/test
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache uv packages
|
||||
# См. backend-tests: кросс-прогонный кэш ~/.cache/uv, тот же ключ по uv.lock.
|
||||
uses: actions/cache@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ~/.cache/uv
|
||||
key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }}
|
||||
restore-keys: |
|
||||
uv-${{ runner.os }}-
|
||||
|
||||
- name: Install system deps for geo + WeasyPrint
|
||||
# app.main транзитивно тянет geo/PDF-модули. На macOS-dev импорт схемы
|
||||
# проходит и без этих либ, но на ubuntu ставим как backend-tests
|
||||
# (belt-and-suspenders, чтобы import app.main точно не падал).
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libpq-dev libgdal-dev libproj-dev libgeos-dev \
|
||||
libcairo2 libpango-1.0-0 libpangoft2-1.0-0
|
||||
|
||||
- name: Install backend deps (uv sync --frozen --no-dev)
|
||||
working-directory: backend
|
||||
# --no-dev: этот job только дампит app.openapi() (нужен runtime app.main).
|
||||
# pytest/ruff/coverage не используются → не ставим dev-группу (быстрее).
|
||||
# Dockerfile тоже собирает с --no-dev → импорт app.main гарантированно ок.
|
||||
run: uv sync --frozen --no-dev
|
||||
|
||||
- name: Install frontend deps (npm ci)
|
||||
working-directory: frontend
|
||||
run: npm ci --legacy-peer-deps --no-audit --no-fund
|
||||
|
||||
- name: Dump OpenAPI schema from app (no server)
|
||||
working-directory: backend
|
||||
run: uv run python -c "import json; from app.main import app; print(json.dumps(app.openapi()))" > /tmp/openapi.json
|
||||
|
||||
- name: Regenerate api-types.ts + format (project-local pinned prettier)
|
||||
working-directory: frontend
|
||||
# 1) openapi-typescript из дампнутого файла (эквивалент `npm run codegen`,
|
||||
# который читает ту же схему по URL). 2) ./node_modules/.bin/prettier —
|
||||
# PROJECT-LOCAL, pinned (prettier 3.9.0 в devDependencies). НЕ `npx
|
||||
# prettier` (тот резолвится в плавающий latest и расходится с pre-commit,
|
||||
# ломая этот gate). Pre-commit hook гоняет тот же локальный prettier 3.9.0
|
||||
# → байт-в-байт идентичный формат. См. .pre-commit-config.yaml.
|
||||
run: |
|
||||
npx openapi-typescript /tmp/openapi.json -o src/lib/api-types.ts
|
||||
./node_modules/.bin/prettier --write src/lib/api-types.ts
|
||||
|
||||
- name: Assert api-types.ts is up-to-date
|
||||
working-directory: frontend
|
||||
# git diff --exit-code: 0 если файл не изменился (типы актуальны),
|
||||
# 1 если regen дал другой результат (типы устарели → fail с подсказкой).
|
||||
run: |
|
||||
if ! git diff --exit-code -- src/lib/api-types.ts; then
|
||||
echo "::error::frontend/src/lib/api-types.ts устарел относительно backend OpenAPI." \
|
||||
"Запусти: cd frontend && npm run codegen (с backend на :8000), затем закоммить." \
|
||||
"Pre-commit prettier отформатирует автоматически."
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ api-types.ts актуален относительно backend OpenAPI-схемы."
|
||||
run: uv run pytest -q --ignore=tests/smoke
|
||||
|
|
|
|||
|
|
@ -24,89 +24,18 @@ jobs:
|
|||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
backend: ${{ steps.set-all.outputs.backend || steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.set-all.outputs.frontend || steps.filter.outputs.frontend }}
|
||||
browser: ${{ steps.set-all.outputs.browser || steps.filter.outputs.browser }}
|
||||
infra: ${{ steps.set-all.outputs.infra || steps.filter.outputs.infra }}
|
||||
scraper: ${{ steps.set-all.outputs.scraper || steps.filter.outputs.scraper }}
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
browser: ${{ steps.filter.outputs.browser }}
|
||||
infra: ${{ steps.filter.outputs.infra }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Resolve base SHA: read last-successfully-deployed SHA from the VPS host file.
|
||||
# The file is written by the deploy job on every successful deploy.
|
||||
# Fail-safe: if we cannot read the file, or the SHA is not an ancestor of HEAD,
|
||||
# we leave DEPLOYED_SHA empty — the next step will then build everything.
|
||||
- name: Resolve deployed base SHA
|
||||
id: resolve-base
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: |
|
||||
# Write SSH key to a temp file
|
||||
SSH_KEY_FILE=$(mktemp)
|
||||
echo "$DEPLOY_SSH_KEY" > "$SSH_KEY_FILE"
|
||||
chmod 600 "$SSH_KEY_FILE"
|
||||
|
||||
# Try to read the marker file from the VPS. Suppress errors — if host is
|
||||
# unreachable or file missing, RAW_SHA will be empty.
|
||||
RAW_SHA=$(ssh -i "$SSH_KEY_FILE" \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o ConnectTimeout=10 \
|
||||
-p "${DEPLOY_PORT:-22}" \
|
||||
"${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||
"cat /opt/gendesign/.tradein-deployed-sha 2>/dev/null || true" \
|
||||
2>/dev/null || true)
|
||||
RAW_SHA=$(echo "$RAW_SHA" | tr -d '[:space:]')
|
||||
|
||||
rm -f "$SSH_KEY_FILE"
|
||||
|
||||
# Validate: non-empty, looks like a git SHA, and is an ancestor of HEAD.
|
||||
DEPLOYED_SHA=""
|
||||
if [ -n "$RAW_SHA" ] && echo "$RAW_SHA" | grep -qE '^[0-9a-f]{40}$'; then
|
||||
if git merge-base --is-ancestor "$RAW_SHA" HEAD 2>/dev/null; then
|
||||
DEPLOYED_SHA="$RAW_SHA"
|
||||
echo "Resolved deployed base: $DEPLOYED_SHA"
|
||||
else
|
||||
echo "WARNING: stored SHA $RAW_SHA is not an ancestor of HEAD — falling back to build-all"
|
||||
fi
|
||||
else
|
||||
echo "No valid deployed SHA found — falling back to build-all"
|
||||
fi
|
||||
|
||||
echo "deployed_sha=$DEPLOYED_SHA" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# FAIL-SAFE: if no valid base SHA, emit all=true and skip paths-filter.
|
||||
# This covers: first run, post-force-push, VPS unreachable, corrupt marker.
|
||||
# A spurious full build is always safer than a missed build.
|
||||
- name: Build-all fallback (no base SHA)
|
||||
id: set-all
|
||||
if: steps.resolve-base.outputs.deployed_sha == ''
|
||||
run: |
|
||||
echo "No base SHA — enabling build-all"
|
||||
echo "backend=true" >> "$GITHUB_OUTPUT"
|
||||
echo "frontend=true" >> "$GITHUB_OUTPUT"
|
||||
echo "browser=true" >> "$GITHUB_OUTPUT"
|
||||
echo "infra=true" >> "$GITHUB_OUTPUT"
|
||||
echo "scraper=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Cumulative diff: compare deployed SHA → HEAD so that a fast chain of merges
|
||||
# (e.g. backend #1829 then frontend #1830) doesn't lose earlier changes.
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
if: steps.resolve-base.outputs.deployed_sha != ''
|
||||
with:
|
||||
base: ${{ steps.resolve-base.outputs.deployed_sha }}
|
||||
filters: |
|
||||
backend:
|
||||
- 'tradein-mvp/backend/**'
|
||||
# scraper-kit вкомпилирован в backend-образ (build context tradein-mvp/,
|
||||
# scheduler_main импортирует пакет) — kit-only изменение обязано
|
||||
# пересобрать образ, иначе деплой рестартует контейнеры на старом.
|
||||
- 'tradein-mvp/packages/scraper-kit/**'
|
||||
frontend:
|
||||
- 'tradein-mvp/frontend/**'
|
||||
browser:
|
||||
|
|
@ -115,25 +44,11 @@ jobs:
|
|||
- 'tradein-mvp/docker-compose.prod.yml'
|
||||
- 'tradein-mvp/deploy/**'
|
||||
- '.forgejo/workflows/deploy-tradein.yml'
|
||||
scraper:
|
||||
- 'tradein-mvp/backend/app/services/scrapers/**'
|
||||
- 'tradein-mvp/backend/app/services/scrape_pipeline.py'
|
||||
- 'tradein-mvp/backend/app/services/scheduler.py'
|
||||
- 'tradein-mvp/backend/app/scheduler_main.py'
|
||||
- 'tradein-mvp/backend/app/tasks/**'
|
||||
# #2188: scheduler исполняет matching/dedup при каждом scrape-тике —
|
||||
# без этих путей scraper-контейнер оставался на старом коде
|
||||
# (2026-07-02: fias-dedup доехал до tradein-backend, но не до
|
||||
# tradein-scraper). После USE_KIT_SCHEDULER=true kit-код и есть
|
||||
# scheduler — его правки тоже обязаны пересоздавать контейнер.
|
||||
- 'tradein-mvp/backend/app/services/matching/**'
|
||||
- 'tradein-mvp/backend/app/services/house_dedup_merge.py'
|
||||
- 'tradein-mvp/packages/scraper-kit/**'
|
||||
|
||||
# Quality gate: pytest MUST pass before any image is built/deployed (#666).
|
||||
# Runs the tradein-mvp/backend suite; a red test blocks build + deploy.
|
||||
# Tests use mocks + a stub DATABASE_URL — no real Postgres/Redis needed.
|
||||
# 1 pre-existing order-dependent test is deselected (see DESELECT note below).
|
||||
# 2 pre-existing order-dependent tests are deselected (see DESELECT note below).
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
|
|
@ -158,21 +73,20 @@ jobs:
|
|||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Sync deps (incl. dev group — pytest)
|
||||
# Workspace-лок tradein-mvp/uv.lock TRACKED (с воркспейса #2137; gitignored
|
||||
# только старый backend/uv.lock) → --frozen детерминирован и зеркалит
|
||||
# Dockerfile (uv sync --frozen --no-dev). Актуализировано в #2208.
|
||||
run: uv sync --frozen
|
||||
# NB: tradein-mvp/backend/uv.lock is gitignored (see tradein-mvp/.gitignore),
|
||||
# so --frozen can't work in CI (no lockfile in a fresh checkout). Resolve
|
||||
# from pyproject.toml like the Dockerfile does (uv sync --no-dev there).
|
||||
run: uv sync
|
||||
|
||||
- name: Run pytest (tradein-mvp/backend)
|
||||
# DESELECT (актуализировано 2026-07-02, #2208): test_search_cache_hit падает
|
||||
# ТОЛЬКО в whole-suite ordering (401 vs 200; в изоляции проходит) — global-state
|
||||
# leak из другого test-модуля, pre-existing. Второй исторический deselect
|
||||
# (test_cian_valuation::test_cache_hit_returns_cached) убран — проходит в полном
|
||||
# прогоне (проверено 2026-07-02: 2947 passed / 1 failed). Список обязан
|
||||
# совпадать с backend-tests в ci-tradein.yml (pre-merge гейт).
|
||||
# DESELECT (2026-05): two pre-existing tests fail only in whole-suite
|
||||
# ordering (pass in isolation) due to global state leak from other test
|
||||
# modules — not introduced here. Excluded so the gate is reliably green;
|
||||
# tracked separately. Everything else (1087 tests) must pass.
|
||||
run: |
|
||||
uv run pytest -q \
|
||||
--deselect "tests/test_search_api.py::test_search_cache_hit"
|
||||
--deselect "tests/test_search_api.py::test_search_cache_hit" \
|
||||
--deselect "tests/test_cian_valuation.py::test_cache_hit_returns_cached"
|
||||
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -196,10 +110,7 @@ jobs:
|
|||
- name: Build & push tradein-backend
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
# Context = tradein-mvp/ (uv workspace root): образу нужен packages/scraper-kit
|
||||
# для editable install (#2137). Dockerfile — в backend/.
|
||||
context: ./tradein-mvp
|
||||
file: ./tradein-mvp/backend/Dockerfile
|
||||
context: ./tradein-mvp/backend
|
||||
push: true
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_BACKEND }}:buildcache,mode=max
|
||||
|
|
@ -232,13 +143,10 @@ jobs:
|
|||
context: ./tradein-mvp/frontend
|
||||
push: true
|
||||
# basePath=/trade-in baked-in во время build (Next.js)
|
||||
# NB (#2205): НЕ передаём NEXT_PUBLIC_ENABLE_PREVIEW — preview-роут
|
||||
# (/ui-preview/estimate, статичная demo-фикстура) собирается ТОЛЬКО в
|
||||
# dev/CI (a11y/lighthouse). В прод-образе флаг не задан → страница
|
||||
# уходит в notFound (404), не индексируется и не краулится.
|
||||
build-args: |
|
||||
NEXT_PUBLIC_BASE_PATH=/trade-in
|
||||
NEXT_PUBLIC_API_BASE_URL=/trade-in
|
||||
NEXT_PUBLIC_ENABLE_PREVIEW=1
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_FRONTEND }}:buildcache,mode=max
|
||||
tags: |
|
||||
|
|
@ -295,20 +203,12 @@ jobs:
|
|||
env:
|
||||
IMAGE_TAG: latest
|
||||
GHCR_PAT: ${{ secrets.GHCR_PAT }}
|
||||
# Phase 0: generic infra edits (compose / workflow / deploy/**) must NOT
|
||||
# recreate the scraper and SIGKILL a running multi-hour job. Only genuine
|
||||
# scraper-code paths (the `scraper` paths-filter already covers
|
||||
# app/services/scrapers/**, scrape_pipeline.py, scheduler.py,
|
||||
# scheduler_main.py, app/tasks/**) — or a manual workflow_dispatch —
|
||||
# should trigger a scraper recreate. (infra term intentionally dropped.)
|
||||
SCRAPER_CHANGED: ${{ needs.changes.outputs.scraper == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
port: ${{ secrets.DEPLOY_PORT }}
|
||||
envs: IMAGE_TAG,GHCR_PAT,SCRAPER_CHANGED,GITHUB_SHA
|
||||
envs: IMAGE_TAG,GHCR_PAT
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd /opt/gendesign
|
||||
|
|
@ -338,45 +238,15 @@ jobs:
|
|||
|
||||
export IMAGE_TAG="$IMAGE_TAG"
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml pull
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d
|
||||
|
||||
# ── Порядок деплоя (issue #2216): МИГРАЦИИ ДО НОВОГО app-кода ──────────
|
||||
# Раньше backend/frontend/scraper поднимались ПЕРЕД миграциями: при сбое
|
||||
# миграции новый код уже крутился на СТАРОЙ схеме (рассинхрон код↔схема).
|
||||
# Теперь строго: (1) только postgres → (2) ждём готовности БД →
|
||||
# (3) ВЕСЬ блок миграций → (4) app-контейнеры → (5) Caddy + health.
|
||||
# ИНВАРИАНТ ПРИ СБОЕ МИГРАЦИИ: строгий gate делает exit 1 ДО подъёма
|
||||
# нового кода → старые контейнеры продолжают работать на СТАРОМ коде +
|
||||
# СТАРОЙ схеме (консистентная пара). Это и есть цель: никогда «новый
|
||||
# код на старой схеме». Откат = просто ничего не поднимали.
|
||||
|
||||
# (1) Только БД — чтобы прогнать миграции до нового app-кода.
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps postgres
|
||||
|
||||
# (2) Ждём готовности postgres (pg_isready в цикле, НЕ тупой sleep).
|
||||
echo "→ Ожидание готовности postgres..."
|
||||
pg_ready=""
|
||||
for i in $(seq 1 30); do
|
||||
if docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
pg_isready -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein >/dev/null 2>&1; then
|
||||
pg_ready="yes"; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$pg_ready" ]; then
|
||||
echo "ERROR: postgres не стал ready за отведённое время — прерываю деплой."
|
||||
echo " Новый app-код НЕ поднят; старые контейнеры не тронуты."
|
||||
exit 1
|
||||
fi
|
||||
echo "→ postgres ready."
|
||||
|
||||
# (3) Применяем SQL миграции (если есть backend/data/sql/*.sql) — ДО app.
|
||||
# Применяем SQL миграции (если есть backend/data/sql/*.sql)
|
||||
# Postgres init load *.sql из /docker-entrypoint-initdb.d ТОЛЬКО при первом
|
||||
# старте volume. Здесь — для повторных миграций после первого запуска.
|
||||
# Tracking через _schema_migrations (порт паттерна из deploy.yml):
|
||||
# каждый .sql применяется РОВНО один раз, failed migration → exit 1
|
||||
# (никаких swallowed errors). cwd = /opt/gendesign/tradein-mvp.
|
||||
# NB: цикл берёт только *.sql — data/sql/_manifest_applied.txt (инвариант
|
||||
# #2216) glob'ом не подхватывается.
|
||||
sleep 5
|
||||
|
||||
# Pre-existence detection ДО CREATE TABLE: если таблицы ещё нет, это
|
||||
# первый deploy после внедрения tracking на уже-наполненной prod-БД
|
||||
|
|
@ -431,132 +301,16 @@ jobs:
|
|||
done
|
||||
echo "All migrations applied."
|
||||
|
||||
# Bootstrap gendesign_reader password from env (post-migration, #976).
|
||||
# SQL migration 101_gendesign_reader_role.sql creates role passwordless;
|
||||
# password lives only in /opt/gendesign/tradein-mvp/.env.runtime.
|
||||
# .env.runtime already sourced above (set -a; source .env.runtime).
|
||||
#
|
||||
# ⚠️ psql variable substitution (:'pw') НЕ работает внутри -c
|
||||
# (переменная доходит до сервера as literal → syntax error at ':').
|
||||
# Решение: передаём SQL через stdin (< file), psql интерполирует
|
||||
# :'pw' ВНЕ dollar-quoted блока. Детали: ops/db-bootstrap/set_gendesign_reader_password.sql.
|
||||
if [ -n "${TRADEIN_READER_PASSWORD:-}" ]; then
|
||||
echo "→ Applying gendesign_reader password from env"
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on \
|
||||
-v "pw=${TRADEIN_READER_PASSWORD}" \
|
||||
< ops/db-bootstrap/set_gendesign_reader_password.sql
|
||||
else
|
||||
echo "WARNING: TRADEIN_READER_PASSWORD not set in .env.runtime — gendesign_reader без пароля, ETL #976 не сможет подключиться"
|
||||
fi
|
||||
|
||||
# (4) Теперь — новый app-код: схема уже актуальна.
|
||||
#
|
||||
# #1951: раньше scraper пересоздавался ВТОРОЙ отдельной командой `up -d`,
|
||||
# уже ПОСЛЕ browser/backend/frontend. Если это происходило посреди
|
||||
# in-flight sweep'а (avito/cian/rosreestr full-load), процесс убивался на
|
||||
# лету, а осиротевшая scrape_runs-строка сидела status='running' с
|
||||
# замёрзшим heartbeat до периодического 6h zombie-reaper'а — false "hang"
|
||||
# investigation вместо честного deploy-артефакта (см. cian_full_load #404).
|
||||
# Три меры ниже — все devops-only (shell в deploy-скрипте), БЕЗ правок
|
||||
# Python в scraper-стартап-пути (scheduler_main.py / scraper-kit /
|
||||
# app/services/scrapers — намеренно не тронуты, см. PR-описание):
|
||||
#
|
||||
# 1) Атомарный recreate — browser/backend/frontend[+scraper] поднимаются
|
||||
# ОДНИМ `docker compose up -d` инвокейшном (список сервисов собирается
|
||||
# заранее в $SERVICES), а не двумя последовательными командами.
|
||||
# 2) Graceful drain — если scraper будет пересоздан, ждём (до 5 мин, poll
|
||||
# каждые 10s) пока scrape_runs.status='running' не станет 0, ПРЕЖДЕ чем
|
||||
# инициировать recreate. Таймаут не блокирует деплой навсегда —
|
||||
# SIGTERM-drain (#1182 Phase 2/3a) + stop_grace_period 120s остаются
|
||||
# финальной страховкой для того, что не успело дойти до checkpoint'а.
|
||||
# 3) Startup-reap — сразу после recreate помечаем 'cancelled' любые
|
||||
# 'running'-строки, чей heartbeat не обновлялся с МОМЕНТА (по часам
|
||||
# самой БД — SELECT NOW(), без risk clock-skew раннера), взятого
|
||||
# непосредственно перед stop. Такие строки заведомо осиротели ЭТИМ
|
||||
# recreate'ом (старый контейнер физически не может писать heartbeat
|
||||
# после своей остановки) — не ждём 6h периодического reap_zombies().
|
||||
# 'cancelled' (не 'zombie') — честно отличает «убит деплоем» от
|
||||
# «непонятно завис» (последнее по-прежнему ловит только 6h-reaper).
|
||||
# Порог — по метке времени конкретного recreate, а не по фиксированному
|
||||
# интервалу: не зависит от heartbeat-каденса разных источников и не
|
||||
# рискует ложно отменить НЕ относящийся к этому recreate run (напр.
|
||||
# admin-triggered scrape внутри backend, если backend в этом деплое
|
||||
# не пересоздавался — его heartbeat продолжит расти после checkpoint'а).
|
||||
SERVICES="browser backend frontend"
|
||||
SCRAPER_STOP_TS=""
|
||||
if [ "${SCRAPER_CHANGED:-true}" = "true" ]; then
|
||||
echo "→ scraper paths changed — waiting for in-flight scrape_runs to drain (up to 5 min)"
|
||||
drained=""
|
||||
for i in $(seq 1 30); do
|
||||
# NB: не сливать "psql не ответил" с "0 running" — иначе неудачный
|
||||
# прогон психgl молча читается как «слито», и graceful drain
|
||||
# становится no-op именно в момент проблем с БД во время деплоя.
|
||||
running_count=""
|
||||
psql_out="$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \
|
||||
"SELECT COUNT(*) FROM scrape_runs WHERE status='running';" 2>/dev/null)" \
|
||||
&& running_count="$(printf '%s' "$psql_out" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
if [ -n "$running_count" ] && [ "$running_count" = "0" ]; then
|
||||
drained="yes"; break
|
||||
fi
|
||||
if [ -z "$running_count" ]; then
|
||||
echo " ...не удалось прочитать running_count (psql failed) — считаем как «ещё активен», жду 10s (попытка $i/30)"
|
||||
else
|
||||
echo " ...${running_count} активных run(ов) ещё бегут, жду 10s (попытка $i/30)"
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
if [ -n "$drained" ]; then
|
||||
echo "→ Активных run'ов нет — recreate scraper безопасен."
|
||||
else
|
||||
echo "WARNING: активные scrape_runs остались после 5 мин ожидания — recreate продолжится."
|
||||
echo " SIGTERM-drain (#1182) + stop_grace_period 120s постараются сохранить checkpoint;"
|
||||
echo " startup-reap ниже подчистит то, что не успеет."
|
||||
fi
|
||||
|
||||
# Checkpoint по часам БД (не раннера) прямо перед recreate.
|
||||
# NB: tr -d '[:space:]' сломан для timestamptz-литерала — убирает и
|
||||
# внутренний пробел между датой и временем ("2026-07-04 06:43" →
|
||||
# "2026-07-0406:43"), CAST(...AS timestamptz) на такое падает молча
|
||||
# (см. WARNING-фолбэк ниже). sed убирает только leading/trailing.
|
||||
SCRAPER_STOP_TS="$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc "SELECT NOW();" 2>/dev/null | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" || SCRAPER_STOP_TS=""
|
||||
echo "→ scraper checkpoint ts (DB clock): ${SCRAPER_STOP_TS:-unknown}"
|
||||
|
||||
SERVICES="$SERVICES scraper"
|
||||
else
|
||||
echo "→ scraper unchanged — tradein-scraper left running (подхватит новый image при следующем своём рестарте)"
|
||||
fi
|
||||
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps $SERVICES
|
||||
|
||||
if [ "${SCRAPER_CHANGED:-true}" = "true" ] && [ -n "${SCRAPER_STOP_TS:-}" ]; then
|
||||
echo "→ Startup-reap (#1951): помечаем orphaned running-строки, замороженные recreate'ом"
|
||||
# NB: psql `-c` НЕ поддерживает `:'var'`-подстановку (переменная доходит до
|
||||
# сервера как литерал → syntax error, см. комментарий выше про TRADEIN_READER_PASSWORD)
|
||||
# — поэтому подставляем bash-значением напрямую. SCRAPER_STOP_TS сгенерирован
|
||||
# самим Postgres (SELECT NOW()), не внешний ввод → безопасно.
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on -c "
|
||||
UPDATE scrape_runs
|
||||
SET status = 'cancelled',
|
||||
finished_at = NOW(),
|
||||
error = 'deploy #1951: tradein-scraper recreated mid-run (startup-reap, checkpoint ${SCRAPER_STOP_TS})'
|
||||
WHERE status = 'running'
|
||||
AND heartbeat_at < CAST('${SCRAPER_STOP_TS}' AS timestamptz);
|
||||
" || echo "WARNING: startup-reap query failed — orphaned runs (if any) fall back to the 6h zombie reaper"
|
||||
fi
|
||||
|
||||
# (5) `docker restart tradein-backend` БОЛЬШЕ НЕ НУЖЕН (issue #2216).
|
||||
# История (PR #493 / deploy 1156): backend раньше поднимался ПЕРЕД
|
||||
# миграциями, его lifespan-hook (ensure_fdw_user_mapping) падал с
|
||||
# "server gendesign_remote does not exist" — FOREIGN SERVER создаёт
|
||||
# 060_postgres_fdw_extension.sql, ещё не прогнанная на тот момент.
|
||||
# Требовался рестарт для повторной попытки хука. Теперь backend
|
||||
# стартует на шаге (4), т.е. ПОСЛЕ применения миграций (шаг 3) →
|
||||
# lifespan-hook гарантированно видит применённые миграции (FOREIGN
|
||||
# SERVER gendesign_remote существует) уже с первого старта. Рестарт удалён.
|
||||
# Retry backend lifespan hook AFTER migrations applied.
|
||||
# tradein-backend startup runs ensure_fdw_user_mapping which needs
|
||||
# FOREIGN SERVER gendesign_remote (created by 060_postgres_fdw_extension.sql).
|
||||
# Without restart, the first compose-up's startup hook failed with
|
||||
# "server gendesign_remote does not exist" because migrations hadn't run yet.
|
||||
# See PR #493 deploy/1156 for the incident details.
|
||||
echo "→ Restarting tradein-backend so lifespan hook retries USER MAPPING setup"
|
||||
docker restart tradein-backend
|
||||
# Give backend time to come up before Caddy reload + health check below
|
||||
sleep 5
|
||||
|
||||
# Caddy reload — основной Caddyfile содержит inline tradein routes
|
||||
# (см. Caddyfile в корне репы). Reload, чтобы Caddy перечитал DNS
|
||||
|
|
@ -565,28 +319,12 @@ jobs:
|
|||
docker compose -p gendesign -f docker-compose.prod.yml exec -T caddy \
|
||||
caddy reload --config /etc/caddy/Caddyfile || true
|
||||
|
||||
# Health check — деплой ВАЛИТСЯ, если backend не поднялся (#2214).
|
||||
# Раньше цикл после 30 неуспешных попыток молча продолжал скрипт и
|
||||
# доходил до записи success-маркера → мёртвый backend помечался
|
||||
# «задеплоено». Теперь: флаг healthy выставляется ТОЛЬКО при HTTP 200
|
||||
# на /health; после цикла — hard exit 1, если флаг пуст. exit 1
|
||||
# происходит ДО записи .tradein-deployed-sha (маркер пишется последним,
|
||||
# ниже) → следующий прогон changes-job возьмёт корректную базу.
|
||||
# NB set -e: curl стоит в условии `if` (exempt из errexit) — неуспешная
|
||||
# попытка НЕ фатальна, а лишь провоцирует следующую итерацию цикла.
|
||||
healthy=""
|
||||
# Health check
|
||||
for i in $(seq 1 30); do
|
||||
if docker compose -p gendesign-tradein -f /opt/gendesign/tradein-mvp/docker-compose.prod.yml \
|
||||
exec -T backend curl -fsS http://localhost:8000/health >/dev/null 2>&1; then
|
||||
healthy="yes"; break
|
||||
fi
|
||||
docker compose -p gendesign-tradein -f /opt/gendesign/tradein-mvp/docker-compose.prod.yml \
|
||||
exec -T backend curl -fsS http://localhost:8000/health && break
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "$healthy" ]; then
|
||||
echo "ERROR: backend не ответил на /health за 30s — деплой FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo "→ backend healthy на /health."
|
||||
|
||||
# Cleanup старых образов
|
||||
for repo in ghcr.io/lekss361/gendesign-tradein-backend \
|
||||
|
|
@ -596,9 +334,3 @@ jobs:
|
|||
| xargs -r docker rmi 2>/dev/null || true
|
||||
done
|
||||
docker image prune -af || true
|
||||
|
||||
# Mark this SHA as successfully deployed.
|
||||
# Written LAST — only after all of the above completed without error.
|
||||
# The changes job reads this file on the next run to compute cumulative diff.
|
||||
echo "$GITHUB_SHA" > /opt/gendesign/.tradein-deployed-sha
|
||||
echo "→ Deployed SHA marker updated: $GITHUB_SHA"
|
||||
|
|
|
|||
|
|
@ -164,22 +164,12 @@ jobs:
|
|||
GHCR_PAT: ${{ secrets.GHCR_PAT }}
|
||||
GLITCHTIP_BACKEND_DSN: ${{ secrets.GLITCHTIP_BACKEND_DSN }}
|
||||
OBJECTIVE_API_KEY: ${{ secrets.OBJECTIVE_API_KEY }}
|
||||
# LLM chat (#960/#957): оба UNSET по умолчанию → feature dormant
|
||||
# (settings.llm_enabled=False, settings.openai_api_key=None).
|
||||
# OPENAI_API_KEY — sensitive → secret. LLM_ENABLED — non-sensitive
|
||||
# boolean toggle → actions variable (vars.*).
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLM_ENABLED: ${{ vars.LLM_ENABLED }}
|
||||
# OWN_DEVELOPER_IDS (#1169 §25.3): свои developer_id (companyGroup) для
|
||||
# own-portfolio каннибализации. Non-sensitive (публичные id) → actions
|
||||
# variable. UNSET → каннибализация отдаёт proxy (фича дормант).
|
||||
OWN_DEVELOPER_IDS: ${{ vars.OWN_DEVELOPER_IDS }}
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
port: ${{ secrets.DEPLOY_PORT }}
|
||||
envs: IMAGE_TAG,SENTRY_RELEASE_VAL,GHCR_PAT,GLITCHTIP_BACKEND_DSN,OBJECTIVE_API_KEY,OPENAI_API_KEY,LLM_ENABLED,OWN_DEVELOPER_IDS
|
||||
envs: IMAGE_TAG,SENTRY_RELEASE_VAL,GHCR_PAT,GLITCHTIP_BACKEND_DSN,OBJECTIVE_API_KEY
|
||||
script: |
|
||||
set -euo pipefail
|
||||
cd /opt/gendesign
|
||||
|
|
@ -192,21 +182,6 @@ jobs:
|
|||
git fetch origin main
|
||||
git reset --hard origin/main
|
||||
|
||||
# Re-assert +x on ops scripts (#71). These are committed 100755, so a
|
||||
# reset normally preserves the bit — but this is belt-and-suspenders so
|
||||
# a script that ever lands as 644 can't silently break its cron caller
|
||||
# (cron `30 3 * * * bash /opt/gendesign/ops/backup.sh` is +x-independent,
|
||||
# but other callers may invoke the raw path).
|
||||
chmod +x ops/*.sh 2>/dev/null || true
|
||||
|
||||
# Full PDF report bind-source (#2259 PR-D). docker создаёт отсутствующий
|
||||
# bind-source как root:root — worker пишет PDF под uid 1000 → PermissionError
|
||||
# → вечный «building». Создаём каталог заранее + chown под контейнерный uid.
|
||||
# chown под non-root deploy-юзером требует sudo → fallback (|| true — если и
|
||||
# sudo нет, каталог уже наш и chown не нужен).
|
||||
mkdir -p reports
|
||||
chown 1000:1000 reports 2>/dev/null || sudo chown 1000:1000 reports 2>/dev/null || true
|
||||
|
||||
# Sentry release tracking
|
||||
mkdir -p backend
|
||||
touch backend/.env.runtime
|
||||
|
|
@ -233,35 +208,6 @@ jobs:
|
|||
else
|
||||
printf 'OBJECTIVE_API_KEY=%s\n' "$OBJECTIVE_API_KEY" >> backend/.env.runtime
|
||||
fi
|
||||
# LLM chat (#960/#957) — OpenAI key + enable toggle. В ОТЛИЧИЕ от
|
||||
# OBJECTIVE_API_KEY пишем ТОЛЬКО при non-empty value: если Forgejo
|
||||
# secret/var UNSET — ничего не пишем, app держит safe defaults
|
||||
# (settings.llm_enabled=False, settings.openai_api_key=None) → feature
|
||||
# dormant, без мусорной пустой строки `OPENAI_API_KEY=` в .env.runtime.
|
||||
if [ -n "${OPENAI_API_KEY:-}" ]; then
|
||||
if grep -q '^OPENAI_API_KEY=' backend/.env.runtime; then
|
||||
sed -i "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$OPENAI_API_KEY|" backend/.env.runtime
|
||||
else
|
||||
printf 'OPENAI_API_KEY=%s\n' "$OPENAI_API_KEY" >> backend/.env.runtime
|
||||
fi
|
||||
fi
|
||||
if [ -n "${LLM_ENABLED:-}" ]; then
|
||||
if grep -q '^LLM_ENABLED=' backend/.env.runtime; then
|
||||
sed -i "s|^LLM_ENABLED=.*|LLM_ENABLED=$LLM_ENABLED|" backend/.env.runtime
|
||||
else
|
||||
printf 'LLM_ENABLED=%s\n' "$LLM_ENABLED" >> backend/.env.runtime
|
||||
fi
|
||||
fi
|
||||
# OWN_DEVELOPER_IDS (#1169 §25.3 own-portfolio каннибализация). Тоже
|
||||
# conditional-on-non-empty: UNSET var → не трогаем (если строка уже есть
|
||||
# в .env.runtime — она переживает git reset, .gitignored — остаётся).
|
||||
if [ -n "${OWN_DEVELOPER_IDS:-}" ]; then
|
||||
if grep -q '^OWN_DEVELOPER_IDS=' backend/.env.runtime; then
|
||||
sed -i "s|^OWN_DEVELOPER_IDS=.*|OWN_DEVELOPER_IDS=$OWN_DEVELOPER_IDS|" backend/.env.runtime
|
||||
else
|
||||
printf 'OWN_DEVELOPER_IDS=%s\n' "$OWN_DEVELOPER_IDS" >> backend/.env.runtime
|
||||
fi
|
||||
fi
|
||||
chmod 600 backend/.env.runtime
|
||||
|
||||
# External network для Caddy + obsidian-stack share
|
||||
|
|
|
|||
|
|
@ -9,14 +9,6 @@ name: Deploy Obsidian
|
|||
#
|
||||
# Не пересобирает никаких Docker-образов (CouchDB официальный с DockerHub).
|
||||
# Не трогает main-стек (backend / frontend / postgres / worker / beat / caddy).
|
||||
#
|
||||
# ИСТОРИЯ (2026-07-05): жил в .github/workflows/ с момента миграции с GitHub
|
||||
# (16.05.2026), помечен в README как «остался на GitHub» — но живого зеркала
|
||||
# на github.com с настроенными секретами не оказалось: 0 запусков за всю
|
||||
# историю Forgejo Actions (12000+ прогонов остальных workflow), контейнер
|
||||
# не пересоздавался с 17.05 до ручного SSH-фикса 04.07. Перенесён сюда —
|
||||
# единственная директория, которую реально исполняет этот инстанс.
|
||||
# См. issue #2416.
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -25,7 +17,7 @@ on:
|
|||
- "docker-compose.obsidian.yml"
|
||||
- "scripts/setup-couchdb.sh"
|
||||
- "docs/obsidian-livesync.md"
|
||||
- ".forgejo/workflows/deploy-obsidian.yml"
|
||||
- ".github/workflows/deploy-obsidian.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
|
|
@ -1,4 +1,4 @@
|
|||
# Memory graph (hardlink to C:\mcp\.aim\memory-gendesign.jsonl)
|
||||
# Memory graph (hardlink to C:\mcp\.aim\memory-gendesign.jsonl)
|
||||
*.jsonl
|
||||
!memory/memory-gendesign.jsonl
|
||||
|
||||
|
|
@ -44,6 +44,7 @@ tradein-mvp/scripts/.*cookies*.json
|
|||
# Agents and rules ARE tracked: they are team-shared config.
|
||||
# Explicit per-path list — `.claude/` + `!.claude/agents/` whitelist pattern
|
||||
# does not work because git cannot re-include files under an ignored parent.
|
||||
.claude/settings.json
|
||||
.claude/settings.local.json
|
||||
.claude/cache/
|
||||
.claude/sessions/
|
||||
|
|
@ -80,21 +81,3 @@ sf_anton_snapshot.db-*
|
|||
*.csv
|
||||
*.csv.zip
|
||||
!backend/db/init/*.sql
|
||||
|
||||
# OSRM routing graph build output (#39) — ~1.5GB .osrm* + intermediate .pbf.
|
||||
# Строится offline на VPS через scripts/build_osrm.sh, монтируется в osrm-service.
|
||||
# НЕ коммитим артефакты; .gitkeep держит каталог.
|
||||
data/osrm/*
|
||||
!data/osrm/.gitkeep
|
||||
|
||||
# Full PDF report bind-source (#2259 PR-D) — worker пишет PDF в ./reports:/app/reports.
|
||||
# Артефакты рантайма, не коммитим (untracked и так выживает при deploy git reset --hard).
|
||||
/reports/
|
||||
|
||||
# Log cruft at repo root
|
||||
debug.log
|
||||
.ds-sync/
|
||||
ds-bundle/
|
||||
.design-sync/.cache/
|
||||
.design-sync/learnings/
|
||||
.design-sync/node_modules
|
||||
|
|
|
|||
|
|
@ -17,11 +17,6 @@ repos:
|
|||
- id: check-toml
|
||||
- id: check-added-large-files
|
||||
args: ["--maxkb=512"]
|
||||
# #1966: the frozen backtest regression-gate fixture is gzipped prod
|
||||
# inputs (~3 MB). It is re-extracted rarely (only when ground-truth
|
||||
# refreshes), so it does not bloat history per estimator change — the
|
||||
# per-change artifact is the 3 KB backtest_baseline.json.
|
||||
exclude: ^tradein-mvp/backend/tests/fixtures/backtest_full_fixture\.json\.gz$
|
||||
- id: check-merge-conflict
|
||||
- id: detect-private-key
|
||||
|
||||
|
|
@ -35,17 +30,10 @@ repos:
|
|||
- id: ruff-format
|
||||
files: ^(backend|tradein-mvp/backend)/
|
||||
|
||||
# Frontend — prettier on TS/TSX/JSON.
|
||||
# additional_dependencies pins the EXACT prettier engine (3.9.0) so this hook
|
||||
# and CI (`openapi-codegen-check` → ./node_modules/.bin/prettier, also 3.9.0
|
||||
# via frontend/package.json) format byte-for-byte identically. Без явного pin
|
||||
# mirrors-prettier@v4.0.0-alpha.8 тянет CLI-обёртку, а CI `npx prettier`
|
||||
# плавает в latest → расхождение формата → codegen-gate RED. Меняешь версию
|
||||
# здесь — синхронно меняй prettier в frontend/package.json + lockfile.
|
||||
# Frontend — prettier on TS/TSX/JSON
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
additional_dependencies: ["prettier@3.9.0"]
|
||||
files: ^frontend/.*\.(ts|tsx|js|jsx|json|css|md)$
|
||||
exclude: ^frontend/(node_modules|\.next|package-lock\.json)/
|
||||
|
|
|
|||
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -21,7 +21,7 @@ Live: `https://gendsgn.ru/` — Свердловская обл. (ЕКБ, ПЗЗ
|
|||
|
||||
2. **Branch + PR mandatory.** Никаких direct push в main. `feat/...` / `fix/...` / `refactor/...` / `docs/...` / `chore/...` → ветка от `forgejo/main` → `mcp__forgejo__create_pull_request` → PR URL пользователю. После PR create — сразу `Skill loop` polling. `gh` CLI bypassed (2026-05-16). **Полные правила: `.claude/rules/git-pr.md`**.
|
||||
|
||||
3. **Agent-first workflow.** Main session orchestrates ONLY — не пишет код inline. Любая задача >1 файл / >50 строк → subagent. Pre-push: spawn `code-reviewer` subagent на staged changes. Post-push review — внешнее окно Claude (НЕ дублировать). Лимиты размера subagent-задачи: `.claude/rules/delegation.md`.
|
||||
3. **Agent-first workflow.** Main session orchestrates ONLY — не пишет код inline. Любая задача >1 файл / >50 строк → subagent. Pre-push: spawn `code-reviewer` subagent на staged changes. Post-push review — внешнее окно Claude (НЕ дублировать).
|
||||
|
||||
4. **psycopg v3 only.** `import psycopg2` → ModuleNotFoundError. `CAST(:x AS type)` в SQL — никогда `:x::type`. **Полные правила: `.claude/rules/backend.md` + `sql.md`**.
|
||||
|
||||
|
|
@ -41,16 +41,12 @@ Live: `https://gendsgn.ru/` — Свердловская обл. (ЕКБ, ПЗЗ
|
|||
| `database-expert` | `data/sql/**.sql`, Alembic, EXPLAIN ANALYZE |
|
||||
| `devops-engineer` | `docker-compose*.yml`, `Caddyfile`, `.github/workflows/**`, `.forgejo/workflows/**` |
|
||||
| `code-reviewer` | Pre-push lint (security, correctness, conventions) |
|
||||
| `deep-code-reviewer` | Тщательный review критичных PR (миграции / auth / scrapers) + merge authority при ✅ APPROVE (не эксклюзивно: self-merge разрешён любой сессии с 2026-06-27) |
|
||||
| `qa-tester` | Post-deploy smoke (playwright / curl / SQL) сразу после merge+deploy — rule #7 |
|
||||
|
||||
`auto-*` в `.claude/agents/` — standalone bot-персоны (`/work-as-*`), НЕ для Task-spawn; общий контракт — `_autonomous_pickup.md`.
|
||||
|
||||
**Routing:** тривиально (typo, 1-line) → main session. Single-domain clear → worker. Cross-domain / нечётко → `tech-analyst` first. Worker → `code-reviewer` → main commits → push → PR.
|
||||
|
||||
## Where to look
|
||||
|
||||
- **Path-scoped rules:** `.claude/rules/` (backend, frontend, sql, git-pr, deploy, tradein, ui-tokens, ui-conventions, ui-microcopy) — auto-loaded при работе с соответствующими файлами через `paths:` frontmatter; `delegation.md` — намеренно БЕЗ `paths:` (грузится в каждой сессии, не добавлять frontmatter)
|
||||
- **Path-scoped rules:** `.claude/rules/` (backend, frontend, sql, git-pr, deploy, ui-tokens, ui-conventions, ui-microcopy) — auto-loaded при работе с соответствующими файлами через `paths:` frontmatter
|
||||
- **Personal preferences:** `~/.claude/CLAUDE.md` (cross-project: no auto-commit, no Co-Authored-By, no @claude)
|
||||
- **Workflow memory:** `~/.claude/projects/<repo>/memory/MEMORY.md` index
|
||||
- **Vault MOCs:** `meta/`, `domains/<area>/<area>-MOC.md`, `decisions/`, `fixes/`, `limitations/`, `code/patterns/`
|
||||
|
|
@ -58,7 +54,7 @@ Live: `https://gendsgn.ru/` — Свердловская обл. (ЕКБ, ПЗЗ
|
|||
|
||||
## Don't (cross-domain — specifics → `.claude/rules/`)
|
||||
|
||||
- ❌ Direct push в main; merge вне policy (красный CI / tripwires — git-pr.md § Auto-merge policy)
|
||||
- ❌ Direct push в main / merge без approval
|
||||
- ❌ `--no-verify` / `--amend` / `git push --force`
|
||||
- ❌ Knowledge-файлы вне vault (`docs/research/`, `notes/`, `wiki/`)
|
||||
- ❌ Hardcode credentials в коде / коммитах
|
||||
|
|
|
|||
52
Caddyfile
52
Caddyfile
|
|
@ -16,13 +16,15 @@
|
|||
# (basic_auth runs before handle), making /health and /preview/* exclusions
|
||||
# ineffective. With route { }, handlers are matched top-to-bottom as written.
|
||||
|
||||
# #2213: global `log_credentials` УБРАН. Он заставлял Caddy писать raw
|
||||
# Authorization header (Base64 `user:password`, тривиально обратимый) в ОБА лог-файла
|
||||
# (gendsgn.ru.log + auth_audit.log). Даже при root-only доступе и коротком retention
|
||||
# это plaintext-пароли пилотов на диске — недопустимый риск против скромной выгоды.
|
||||
# Trade-off: glitchtip-auth-forwarder больше не извлекает `attempted_username` из 401
|
||||
# (тег деградирует в "(none)" — forwarder это уже обрабатывает gracefully, не падает).
|
||||
# Событие basic_auth 401 (remote_ip / uri / method) по-прежнему уходит в GlitchTip.
|
||||
{
|
||||
servers {
|
||||
# ВКЛЮЧАЕТ raw Authorization/Cookie headers в access log
|
||||
# (по default Caddy 2.x редактирует их как "REDACTED").
|
||||
# Plain password будет в gendsgn.ru.log + auth_audit.log — оба root-only на VPS.
|
||||
# См. forwarder.py _extract_attempted_username для использования.
|
||||
log_credentials
|
||||
}
|
||||
}
|
||||
|
||||
gendsgn.ru {
|
||||
encode zstd gzip
|
||||
|
|
@ -86,23 +88,9 @@ gendsgn.ru {
|
|||
uri strip_prefix /trade-in
|
||||
reverse_proxy tradein-backend:8000 {
|
||||
header_up X-Authenticated-User {http.auth.user.id}
|
||||
# #2213 defense-in-depth: общий секрет Caddy↔tradein-backend. header_up
|
||||
# с value ПЕРЕЗАПИСЫВАЕТ (стирает) любой клиентский X-Internal-Auth-Secret —
|
||||
# тот же механизм, что защищает X-Authenticated-User выше. Пусто пока
|
||||
# TRADEIN_INTERNAL_AUTH_SECRET не задан в .env (fail-open, backend не проверяет).
|
||||
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
||||
}
|
||||
}
|
||||
|
||||
# gendsgn.ru/sale-share — короткий адрес standalone-продукта «Поиск домов».
|
||||
# Next basePath=/trade-in → редиректим на канонический /trade-in/sale-share
|
||||
# (тот же tradein-frontend контейнер; query-string сохраняется). True vanity-URL
|
||||
# в адресной строке требует отдельного Next-app с basePath=/sale-share.
|
||||
@saleshare path /sale-share /sale-share/
|
||||
handle @saleshare {
|
||||
redir /trade-in/sale-share permanent
|
||||
}
|
||||
|
||||
# Matcher `path /trade-in /trade-in/*` ловит И /trade-in (без слеша),
|
||||
# И /trade-in/ + /trade-in/anything. Без обоих случаев `handle /trade-in/*`
|
||||
# пропускал /trade-in без слеша → попадал в общий frontend → пустой ответ.
|
||||
|
|
@ -111,9 +99,6 @@ gendsgn.ru {
|
|||
# Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
|
||||
reverse_proxy tradein-frontend:3000 {
|
||||
header_up X-Authenticated-User {http.auth.user.id}
|
||||
# #2213: симметрично с /trade-in/api/* — перезаписываем секрет из env
|
||||
# (стирает клиентский), на случай SSR-forwardʼa фронтом в backend.
|
||||
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,25 +149,6 @@ errors.gendsgn.ru {
|
|||
}
|
||||
}
|
||||
|
||||
# Uptime Kuma — self-hosted uptime monitoring + public status page (#75 B6-1).
|
||||
# DNS: A-record status.gendsgn.ru → IP VPS (добавить перед деплоем стека).
|
||||
# Контейнер из docker-compose.uptime.yml (project gendesign-uptime) на shared
|
||||
# gendesign_shared network. Если стек не запущен — Caddy отдаёт 502 ТОЛЬКО на
|
||||
# этом домене, main-сайт не страдает (как obsidian.gendsgn.ru).
|
||||
#
|
||||
# ВНИМАНИЕ: status-page НАМЕРЕННО публичен (trust-building для пилотов, issue #75).
|
||||
# Admin-панель Kuma (/dashboard, /manage-*) защищена собственным логином Kuma —
|
||||
# НЕ кладём её за caddy/users.caddy.snippet, иначе double-auth сломает setup.
|
||||
status.gendsgn.ru {
|
||||
encode zstd gzip
|
||||
|
||||
reverse_proxy uptime-kuma:3001
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/status.gendsgn.ru.log
|
||||
}
|
||||
}
|
||||
|
||||
# Forgejo — self-hosted git (migration 2026-05-16).
|
||||
# DNS: A-record git.gendsgn.ru → IP VPS.
|
||||
# Forgejo container из forgejo-migration/docker-compose.yml на shared
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue