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_configpackage:RuntimeConfigclient +RuntimeConfigCache(TTL 30s) +HmacSigner+verify_chain()для tamper detection.razmakh_api.adminrouter: 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 в один заход.
Что НЕ сделано (отложено)
- Per-org override API endpoints (L4 fully wired): table создана + queried в
_get_org_override, но нет PUT/api/admin/overrideendpoint. Phase 1.5 когда появятся partner orgs. - WebSocket для real-time UI updates: TanStack Query refetchInterval 30s достаточно для skeleton. SSE/WS — Phase 1.5 если будет UX feedback.
- Dry-run mode для high-risk изменений: пока только confirm() prompt. Phase 1.5 — preview impact endpoint.
- Verifier-agent F-09 Stage 6 chain replay cron:
verify_chain()функция готова в audit.py, но cron job для periodic verification — Phase 1.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 на VPSssh 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 deployssh nikolay@razmakh-vps 'cd /opt/razmakh/repo && sudo docker compose build razmakh-web && sudo docker compose up -d razmakh-web'
# 4. Verifycurl -H "Authorization: Bearer <jwt>" https://razmakh.ru/api/admin/overview | jq