Перейти к содержимому

F-12 Admin Control Panel skeleton — что выучили

Что сделано

  • Alembic migration 0004: core.runtime_config (key/value/scope/category/risk + audit_editable), core.runtime_config_history (append-only HMAC chain), core.organization_override (per-org P22 L4), finance.llm_call + finance.infra_cost (BN tracking).
  • razmakh_core.runtime_config package: RuntimeConfig client + RuntimeConfigCache (TTL 30s) + HmacSigner + verify_chain() для tamper detection.
  • razmakh_api.admin router: 7 endpoints (overview / list / get / put / audit / rollback / finance.summary), JWT role gates (owner+admin для writes, +operator для reads, viewer denied).
  • Frontend apps/web/src/app/(app)/admin/: layout с RBAC redirect, 9 страниц (Overview / Modules / Features / LLM / Logging / Notifications / ETL / Maintenance / Finance), 3 shared компонента (ConfigTable / AuditLog / CategoryPage).
  • 35 новых тестов, 253 total passing. Migration deployed на VPS (alembic upgrade 0003 → 0004).

Что выучили

1. FOR UPDATE требует UPDATE privilege на table — не подходит для append-only audit

Что было: SELECT … FOR UPDATE LIMIT 1 на runtime_config_history для serialization конкурентных HMAC chain writers.

Где сломалось: history table имеет REVOKE UPDATE, DELETE … FROM razmakh_app (append-only по N security T2). FOR UPDATE падает permission denied for table runtime_config_history.

Что сделали: pg_advisory_xact_lock(0x52415A4D, 1) — namespace “RAZM” + obj_id для history chain. Lock scope = transaction (auto-released на commit/rollback). Сериализует writers без UPDATE permission.

Урок: для append-only tables — используй advisory locks, не row locks. Для F-06 PersistentRateLimiter тот же паттерн advisory lock уже был — следовало вспомнить раньше.

2. CurrentClaimsDep = Depends(get_current_claims) не работает без type annotation на параметре

Что было: пытался async def handler(claims=CurrentClaimsDep): ... (без : JwtClaims).

Где сломалось: FastAPI видел request: Any в get_current_claims(request: Any) (тоже без конкретного типа), пытался инжектить request как query parameter → 422 даже для public-failing случаев (401 не достигался).

Что сделали: request: Request в get_current_claims + claims: JwtClaims = CurrentClaimsDep на routes. Сразу всё заработало.

Урок: FastAPI DI требует явные типы. Any annotated параметры FastAPI пытается резолвить как query/body. Существующий код F-08 имел этот баг — починил попутно (не breaking — endpoints не было).

3. asyncpg не bind один и тот же :param дважды в одном запросе

Что было: WHERE called_at >= :start AND called_at < (:start::date + interval '1 month').

Где сломалось: asyncpg generates $1 для :start и не видит второй :start → syntax error.

Что сделали: разные имена :start + :start_end, передаём одно и то же значение dvazhdy: {"start": month_first, "start_end": month_first}.

Урок: asyncpg ≠ psycopg2. У psycopg2 одно :name mapped to value один раз и переиспользуется. asyncpg делает 1:1 mapping → нужны уникальные имена. Альтернатива: pre-compute end_date в Python.

4. Docker secrets как env vars — но alembic нужен razmakh_admin/postgres pass

Что было: app container запущен с DATABASE_URL для razmakh_app (NO superuser). Alembic требует CREATE TABLE → нужен postgres или razmakh_admin.

Где сломалось: alembic upgrade head падал permission denied for schema.

Что сделали: ran via docker exec -e DATABASE_URL=postgresql+asyncpg://postgres:<pass>@… с superuser pass (тот же postgres_password docker secret). Migrate ok. После — manual ALTER TABLE … OWNER TO razmakh_admin (чтобы соответствовать остальной 0001-0003 convention).

Урок: alembic migrations должны идти под admin user, runtime under app user — это не security конфликт, это разделение responsibility. В будущем — Dockerfile/entrypoint script с pre-startup alembic upgrade head через переключение URL.

5. JSONB envelope {"value": <actual>} — workaround для scalar storage

Why: PG JSONB не хранит naked scalars красиво (true, 42, "str" work но null сложно). Wrap в {"value": ...} envelope упрощает unwrap логику в Python + видно в SQL.

Урок: для key-value config с heterogeneous types — JSONB + explicit envelope. Простее и предсказуемее чем columnar type-aware design.

6. План сильно завышен (1 день = ~1-2h actual)

Plan: 2 дня (16h). Actual: ~3.5h continuous work. Ratio: 4.6x faster.

Подтверждает memory estimates-llm-driven-dev.md — F-XX plan дни / 6-8 = реальные часы. Полный backend + frontend skeleton + 35 tests + deploy + lesson в один заход.

Что НЕ сделано (отложено)

  1. Per-org override API endpoints (L4 fully wired): table создана + queried в _get_org_override, но нет PUT /api/admin/override endpoint. Phase 1.5 когда появятся partner orgs.
  2. WebSocket для real-time UI updates: TanStack Query refetchInterval 30s достаточно для skeleton. SSE/WS — Phase 1.5 если будет UX feedback.
  3. Dry-run mode для high-risk изменений: пока только confirm() prompt. Phase 1.5 — preview impact endpoint.
  4. Verifier-agent F-09 Stage 6 chain replay cron: verify_chain() функция готова в audit.py, но cron job для periodic verification — Phase 1.5.
  5. Infisical secret integration для HMAC_SIGNING_KEY: пока env var RAZMAKH_ADMIN_HMAC_KEY. Production deployment должен забрать через Infisical entrypoint (P17 паттерн как JWT_SECRET).

Связь с другими частями

  • P22 Runtime configurability: основа закрыта. Любой module может теперь runtime_config_keys в manifest → seed в 0004 (или Alembic data migration в Phase 1.5).
  • N security T2 Audit chain: HMAC implementation готова, verify_chain() доступна для F-09 Stage 6 verifier (cron job)
  • BE Flexibility & Admin panel: 5 уровней config L0-L4 — L0-L3 покрыты (через category), L4 table готова но API endpoint отложен.
  • BN Financial model: схемы созданы (finance.llm_call + finance.infra_cost), summary endpoint работает. LLM call instrumentation (writes в finance.llm_call) — Phase 1.5 когда появится первый LLM agent.
  • Y RBAC: использует existing 4 roles (owner / admin / operator / viewer). 6-template система — Phase 1.5 с core.organization_member_template.

Деплой commands (для replay)

Окно терминала
# 1. Apply migration на VPS
ssh nikolay@razmakh-vps 'sudo docker cp 0004_*.py razmakh-api:/app/apps/api/alembic/versions/'
ssh nikolay@razmakh-vps "sudo docker exec -e DATABASE_URL='...postgres@postgres:5432...' razmakh-api alembic upgrade head"
ssh nikolay@razmakh-vps 'sudo docker exec razmakh-postgres psql -U postgres -d razmakh -c "ALTER TABLE core.runtime_config OWNER TO razmakh_admin; ..."'
# 2. Backend deploy (after git push)
ssh nikolay@razmakh-vps 'cd /opt/razmakh/repo && sudo git pull && sudo docker compose build razmakh-api && sudo docker compose up -d razmakh-api'
# 3. Frontend deploy
ssh nikolay@razmakh-vps 'cd /opt/razmakh/repo && sudo docker compose build razmakh-web && sudo docker compose up -d razmakh-web'
# 4. Verify
curl -H "Authorization: Bearer <jwt>" https://razmakh.ru/api/admin/overview | jq