🔴 Critical (3):
1. cleanup-stale-claims.sh: export CUTOFF + STALE_HOURS
Без `export` Python heredoc child process получал "0" вместо реального
cutoff → cron всегда видел все issues как fresh → ничего не освобождал.
Plus added abort при CUTOFF=0 в Python heredoc для defense-in-depth.
2. cleanup-stale-claims.sh: datetime.UTC → datetime.timezone.utc
datetime.UTC требует Python 3.11+. Forgejo runner ubuntu-latest имеет 3.10.
datetime.timezone.utc works в 3.8+.
3. cleanup-stale-claims.sh: invert label order — DELETE wip FIRST, потом
POST ready. Если step 2 (add ready) fails — issue в neutral state без
status/*, менее опасно чем оба status/ready+status/wip одновременно.
🟠 + 🟡 + 🟢 (5):
4. Added pagination loop (defensive cap 10 pages × 50 = 500 issues), но
реализована через temp file + Python parsing (не bash JSON concat).
5. Removed dead code: iso_to_epoch() shell helper не использовался.
6. _autonomous_pickup.md: fix broken `Out-Null | if (-not $?)` pattern.
GetEnvironmentVariable НЕ throws при missing — возвращает $null. $? у
Get* всегда $true. Заменено на прямую проверку результата.
7. stale-claims.yml: добавлен concurrency group чтобы overlapping runs
не stomp'ились (хотя 5-min timeout делает overlap unlikely, defensive).
8. Documented updated_at != heartbeat assumption в header script'а.
Не сделано (low/optional):
- Setup script tokens в memory (UX trade-off — не critical)
- Token suffix в console output (минор info leak в shell history)
Refs: PR #610 review by review-bot (sha=297eb29, verdict=changes)
#11: slash-commands и _autonomous_pickup.md теперь читают persistent User-scope
env vars напрямую (FORGEJO_TOKEN_<ROLE>) вместо file-based из ~/.claude/secrets/.
Один раз: scripts/setup-bot-env.ps1 (запускается user'ом локально, кладёт
токены из vault в Windows User registry). После этого окна grab credentials
через [System.Environment]::GetEnvironmentVariable() — никаких файлов.
Все 5 work-as-*.md обновлены (analyst/backend/frontend/reviewer/qa).
#12: stale-claim cleanup — cron каждые 30 минут.
- scripts/cleanup-stale-claims.sh — bash, parses Forgejo API + Python для JSON
- .forgejo/workflows/stale-claims.yml — cron schedule `*/30 * * * *`
Освобождает issues застрявшие в status/wip >4h:
- assignees=[]
- +status/ready -status/wip
- comment "stale claim released, worker likely crashed"
Использует secret FORGEJO_BOT_QA_TOKEN (минимум прав = write:issue).
Setup: secret нужно завести в Forgejo Actions UI после merge —
git.gendsgn.ru/lekss361/gendesign/-/settings/actions/secrets.
Refs: PRs #608, #609. Closes TODO с PR #608 (stale-claim cron).
* fix(frontend): apiFetchWithStatus body stream double-read crash
Per user report 2026-05-14: 'Failed to execute text on Response: body stream
already read' при analyze с non-JSON error response.
apiFetchWithStatus делал .json() (consumes stream), потом .text() в catch
(FAILS — stream already consumed). Fetch API: body — ReadableStream,
consumable один раз.
Fix: read text() once → JSON.parse on string. На fail (HTML / non-JSON)
оборачиваем в {detail: rawText}. Никаких double-reads.
Site-finder cad search больше не крашится на error pages.
* feat(infra): auto-apply data/sql/*.sql migrations in deploy pipeline (#150)
Закрывает root cause production 500 на /api/v1/admin/site-finder/weight-profiles
(а также 2 unapplied migrations: #89/#91).
## Why это нужно
После audit обнаружили что:
- backend/alembic/versions/ пустой (Alembic configured но не используется)
- deploy.yml не имеет migration step
- 47+ raw SQL files накопилось без auto-apply
- 3 файла unapplied: 87 (engineering), 89 (drop brin), 90 (weight profiles)
User report: GET weight-profiles → 500 потому что user_weight_profiles
table не существует на prod.
## Changes
### deploy.yml +39 lines
- Path trigger расширен на data/sql/*.sql
- New step 'Apply DB migrations' между compose pull и compose up -d:
- Idempotent CREATE TABLE _schema_migrations
- Loop по data/sql/*.sql sorted → skip applied → psql with ON_ERROR_STOP=on
- Record applied filename в tracking table
- Exit 1 на failure → containers НЕ обновляются (no partial state)
### NN collision fix
- 87_engineering_networks_191fz.sql → 91_engineering_networks_191fz.sql
(collision с 87_on_demand_indexes.sql, мой renumber 85→87 был неудачным)
### CLAUDE.md +32 lines
- Subsection 'Auto-apply' под Migration pattern
- Bootstrap procedure docs
- Idempotency requirement
- Failure recovery
### scripts/bootstrap_schema_migrations.sh +100 lines
- One-time seed _schema_migrations с 48 already-applied files
- 3 файла intentionally NOT seeded: 89/90/91 (auto-apply на first deploy)
- Usage docs + verification query
## Vault
domains/infra/Runbook_Auto_Apply_Migrations.md NEW
infra-MOC.md updated
## Required manual steps ДО merge
1. SSH gendesign (tunnel)
2. POSTGRES creds export
3. bash scripts/bootstrap_schema_migrations.sh — seed 48 already-applied files
4. Verify: SELECT COUNT(*) FROM _schema_migrations → 48
5. После merge → first deploy auto-apply #89/#90/#91 → 500 closes
Closes#150
---------
Co-authored-by: lekss361 <claudestars@proton.me>
Worker крашился на старте с `ModuleNotFoundError: No module named 'psycopg2'`
при импорте app/workers/tasks/objective_etl, потому что app/services/objective_etl
писал на psycopg2 API, а в pyproject.toml есть только psycopg[binary]>=3.2.0.
Изменения:
- psycopg2 → psycopg (v3)
- psycopg2.extras.execute_values → _bulk_upsert helper, эмулирующий тот же
behavior через cur.executemany (в psycopg v3 это pipeline-mode, overhead
на сетевые round-trips минимален)
- psycopg.connect совместим с тем же connection string
Тесты: AST parse ✓, ruff ✓, import ✓
Obsidian Self-hosted LiveSync (CouchDB) выведен в отдельный docker-compose
stack и GHA workflow. Любой стек теперь редеплоится независимо.
* docker-compose.obsidian.yml: только couchdb в shared external network
* docker-compose.prod.yml: убрана couchdb-секция; caddy подключён к
shared network для маршрута obsidian.gendsgn.ru → couchdb:5984
* .github/workflows/deploy-obsidian.yml: новый workflow с path-filters
(триггерится только при изменениях obsidian-related файлов)
* .github/workflows/deploy.yml: + path-filters (триггерится только при
main-related файлах) + создание shared network в bootstrap
* docs/obsidian-livesync.md: обновлены инструкции под split-архитектуру
После push:
1. ssh gendesign 'docker network create gendesign_shared' (один раз)
2. Прописать COUCHDB_USER/PASSWORD в backend/.env.runtime
3. Добавить DNS A-record obsidian.gendsgn.ru → IP VPS
4. git push → GHA задеплоит обе части независимо
Преимущества:
- Изменения main не разрывают LiveSync клиентов
- Изменения obsidian не рестартуют main
- Падение obsidian-стека не влияет на основное приложение