Документация

Admin modernization for Crisis Monitor

Status update, 2026-07-15: production remains on Flask. The parallel FastAPI development path is now guarded by tests/test_fastapi_bootstrap.py, which boots the application in a subprocess and validates its current OpenAPI route contract and operation identifiers. The old skip-only Phase 8 router tests were removed because they described obsolete framework internals rather than executable product behavior.

Дата начала: 2026-05-17 Ветка: codex/admin-modernization-2026-05-17

Текущее состояние

Crisis-Monitor сейчас находится в переходном состоянии:

  • production compose и Dockerfile запускают gunicorn ... wsgi:app;
  • wsgi.py поднимает Flask-приложение через app:create_app;
  • app/main.py содержит FastAPI-приложение и подключает app/routers/*, но production path по compose пока Flask;
  • docker-compose.dev.yml запускает uvicorn app.main:app, поэтому FastAPI path нельзя игнорировать как мертвый код.

Админка в production path реализована Flask blueprint-ами:

  • app/admin.py – пользователи, роли, системный дашборд, геозоны, ресурсы, playbooks, assignment/escalation rules, audit log, doctrine tools, ticket export;
  • app/auth.py – login/logout/profile/theme и часть admin user actions;
  • app/backups.py – backup list/create/download/restore;
  • app/scripts.py – запуск безопасного списка maintenance scripts;
  • app/tickets_dashboard.py – /admin/status, /admin/status/data, /admin/monitoring/status.

Параллельный FastAPI слой:

  • app/routers/admin.py, app/routers/auth.py, app/routers/backups.py, app/routers/tickets_dashboard.py;
  • app/routers/deps.py содержит session/user/permission helpers и Jinja compatibility layer;
  • FastAPI templates сейчас используют csrf_token = lambda: "", то есть CSRF-контракт для SSR форм в этом path не завершен.

Уже сильные места

  • Flask production path имеет Flask-WTF CSRFProtect, session cookie policy (HttpOnly, SameSite=Lax, Secure в production), Flask-Login и централизованный permission_required.
  • Роли и permissions уже вынесены в app/permissions.py и app/models/user.py.
  • Ticket audit log имеет hash chain (TicketAudit.previous_hash/hash) и внешний sink через AUDIT_LOG_SINK_*.
  • Upload policy для ticket attachments уже проверяет размер, MIME через python-magic с fallback и secure_filename.
  • Backup restore защищен от path traversal, absolute paths, drive letters, symlinks/hardlinks и не-SQLite payloads.
  • System status уже есть: /admin/status/data, /admin/monitoring/status, static/system-status.js.
  • ARQ worker описывает on-demand jobs и cron jobs для backup/cleanup/undo/metrics/ingestion/escalation/threat/deadline monitoring.

Риски и разрывы

  1. wsgi.py и app/main.py описывают разные реальности. Production compose запускает Flask, а FastAPI-файлы и часть тестов ожидают migration-complete состояние.
  2. FastAPI SSR path ранее не имел полноценного CSRF: в app/routers/deps.py токен был заглушен пустой строкой, mutation dependencies не требовали CSRF. В этом проходе закрыт /admin/* FastAPI router; остальные FastAPI SSR/API mutation paths требуют отдельного маршрута внедрения.
  3. Admin audit сейчас в основном ticket-centric. Админские изменения ролей, блокировок, doctrine import/export, backup restore/create и scripts пишутся не единым admin audit event shape, а точечно в logger или вообще без отдельного audit event.
  4. System dashboard показывает базовые checks, но не раскрывает операционные статусы worker/cron/integrations в виде отдельной модели. Это важно для crisis-продукта.
  5. В app/api.py и app/routers/api.py playbook step mutation вызывает commit_session() до log_ticket_action(...); audit event создается после commit и может не сохраниться.
  6. Локальная среда на текущей машине неполная: системный Python 3.9 без pytest, системный npm и Docker отсутствуют. Проверки выполняются через игнорируемый .venv на Python >=3.11 и bundled Node runtime.

Сделано в этом проходе

  1. fix(api): сохранять audit для playbook mutations
  2. log_ticket_action(...) перенесен до commit_session() в Flask и FastAPI API paths;
  3. добавлен regression test на сохранение TicketAudit(action="playbook_step").
  4. feat(admin): добавить structured audit helper
  5. добавлен utils/admin_audit.py без миграции БД;
  6. admin events пишутся в app logger и существующий external sink;
  7. secret-like поля в metadata редактируются перед записью;
  8. helper подключен к role/block changes, doctrine import/export, backup create/restore и safe script start.
  9. feat(admin): расширить system dashboard model
  10. добавлен utils/system_status.py как общий контракт для Flask/FastAPI;
  11. /admin/status/data сохраняет старые поля checks и metrics;
  12. дополнительно возвращаются workers, cron, integrations, storage, last_errors;
  13. модель не раскрывает значения секретов, только configured/not configured.
  14. test(admin): стабилизировать secret bootstrap
  15. SECRET_KEY выставляется до imports, которые могут загрузить config.BaseConfig;
  16. это убирает ложное падение тестов до входа в реальные fixtures.
  17. feat(admin): добавить FastAPI csrf contract
  18. FastAPI templates теперь получают session CSRF token вместо пустой строки;
  19. unsafe /admin/* requests в FastAPI path требуют form/header CSRF token для session-authenticated browser requests;
  20. добавлены unit-тесты на accept/reject CSRF token;
  21. чистые unit-тесты помечаются no_reference_data, чтобы не поднимать GeoAlchemy/SpatiaLite fixtures без необходимости.
  22. feat(admin): показать operations health на status page
  23. страница /admin/status теперь отображает workers/cron, integrations и storage;
  24. worker-блок дополнительно показывает ARQ heartbeat key, когда worker реально запущен;
  25. динамические списки собираются через DOM API без innerHTML;
  26. существующие checks/metrics и polling сохранены.
  27. fix(models): использовать sqlite geometry fallback без spatialite
  28. SQLite/local test mode больше не пытается создать GeoAlchemy geometry column через SpatiaLite, если ENABLE_SQLITE_SPATIALITE не включен явно;
  29. PostGIS/non-SQLite path сохраняет GeoAlchemy geometry;
  30. это снимает локальный blocker для admin/geozone тестов без изменения production Postgres-контракта.
  31. fix(admin): защитить storage status от fab overlap
  32. добавлен узкий CSS-scope только для storage-card на /admin/status;
  33. бейджи storage health больше не попадают под плавающую Janus/FAB кнопку на desktop;
  34. AV DS tokens и глобальная дизайн-система не менялись.
  35. fix(admin): стабилизировать regression suite и local smoke
  36. закрыты baseline-падения routes/approvals/search/undo/help/PDF/export/backup/ARQ без изменения публичных URL;
  37. PDF generation получает безопасный fallback, когда native WeasyPrint-библиотеки недоступны локально;
  38. frontend smoke modules восстановлены, а system-status polling устойчив к login redirects и partial JSON;
  39. обновлен local browser smoke на /admin, /admin/status, /admin/backups, /tickets, /approvals.

Что можно вынести в общий слой

Без копирования ORTCOM или прямой зависимости от qaz-admin-kit:

  • admin path helpers: единые имена endpoint-ов и ссылки для Flask/FastAPI;
  • access primitives: require_admin, require_permission, role helpers;
  • session/cookie policy: документированный контракт и tests;
  • CSRF contract: token generator + request validator для FastAPI path, Flask остается на CSRFProtect;
  • audit event shape: actor_id, action, resource_type, resource_id, summary, metadata, request_id;
  • upload policy: единая проверка размера/MIME/magic bytes/filename;
  • system dashboard model: checks, worker/cron summary, integrations, storage/backups, last errors;
  • safe confirmation helpers для destructive admin actions.

Что должно остаться доменной логикой

  • ticket lifecycle и TicketAudit hash chain;
  • escalation rules, assignment rules, playbooks;
  • ICS/NIMS/SitRep/CAP форматы;
  • ingestion sources и crisis-specific cron jobs;
  • WeasyPrint/PDF/report generation;
  • QazCompute/QazGeo/QazLake integrations.

План маленьких коммитов

  1. docs(admin): описать план модернизации
  2. этот документ, карта production/FastAPI split, риски и safe plan.
  3. fix(api): сохранять audit для playbook mutations
  4. перенести log_ticket_action(...) до commit или добавить второй commit в Flask и FastAPI API paths;
  5. добавить regression test для сохранения playbook_step audit event.
  6. feat(admin): добавить admin audit helper без миграции БД
  7. структурированный logger/external sink helper для admin events;
  8. использовать для role/block/backup/script/doctrine mutation paths;
  9. без новой таблицы, чтобы не менять схему в первом проходе.
  10. feat(admin): расширить system dashboard model
  11. helper, который возвращает checks, metrics, workers, cron, integrations, storage;
  12. подключить к Flask /admin/status/data;
  13. FastAPI path обновить тем же shape, если без риска.
  14. test(admin): усилить smoke админки
  15. unauth redirect, protected route access, CSRF reject, role restriction, audit write, system status smoke.
  16. docs(admin): обновить smoke/deploy notes
  17. что проверено локально, что требует VPS/browser/manual проверки.

Проверки и локальные ограничения

Доступные проверки:

.venv/bin/python -m py_compile app/api.py app/routers/api.py tests/test_playbooks.py
.venv/bin/python -m py_compile utils/admin_audit.py app/admin.py app/auth.py app/backups.py app/scripts.py tests/test_admin_audit.py
.venv/bin/python -m py_compile utils/system_status.py app/tickets_dashboard.py app/routers/tickets_dashboard.py tests/test_system_status.py
.venv/bin/python -m py_compile tests/conftest.py
.venv/bin/python -m py_compile app/routers/deps.py app/routers/admin.py tests/test_fastapi_csrf.py tests/test_admin_audit.py tests/conftest.py
/Users/belilovsky/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node --check static/system-status.js

Дополнительно проверены короткие Python smoke-snippets:

  • utils.admin_audit._redact_metadata(...) редактирует token/password/key-поля;
  • utils.system_status.build_system_status() отдает полный shape без Flask app context;
  • utils.system_status.build_system_status() отдает ожидаемый shape внутри Flask app context.
  • app.routers.deps.require_csrf_token(...) принимает корректный session token и отклоняет неверный.

Запускаемые локально unit-тесты:

SECRET_KEY=test-secret .venv/bin/pytest tests/test_admin_audit.py tests/test_fastapi_csrf.py -q
# 3 passed

SECRET_KEY=test-secret .venv/bin/pytest \
  tests/test_admin.py tests/test_system_status.py tests/test_playbooks.py \
  tests/test_admin_audit.py tests/test_fastapi_csrf.py tests/test_geozones.py -q
# 18 passed

Полный pytest после стабилизационного прохода зеленый:

SECRET_KEY=test-secret .venv/bin/pytest -q
# 469 passed, 96 skipped in 116.25s

Frontend/build checks:

/Users/belilovsky/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node node_modules/vite/bin/vite.js build
# vite build: ok, 95 modules, built in ~0.8s

Системный npm отсутствует, поэтому frontend проверки запускались прямым Node entrypoint без изменения lockfile и без пересборки dependency tree. static/dist/ в рабочем дереве не менялся.

Frontend JS tests:

/Users/belilovsky/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node tests/js/run-tests.mjs
# form_wizard_test passed
# wizard_component_test passed
# threat_alert_test passed
# All frontend tests passed

Browser smoke 2026-05-17

Локальный сервер поднят на 127.0.0.1:5056 со smoke SQLite DB и SESSION_COOKIE_SECURE=false.

Проверено в in-app browser:

  • /login рендерится, POST login с CSRF создает admin session;
  • /admin открывается, dashboard cards и admin shell согласованы с текущим frontend assets;
  • /admin/users открывается и показывает admin user;
  • /admin/status открывается, новые секции Фоновые задачи, Интеграции, Хранилища видны, /static/system-status.js работает без свежих console errors;
  • /admin/backups открывается, mutation Создать копию через форму с CSRF проходит, появляется alert Создана резервная копия ..., в server log фиксируется admin_audit_event;
  • /admin/scripts открывается, safe script list виден;
  • /tickets открывается, фронт согласован с route/template expectations;
  • /approvals открывается, non-admin видит пользовательские секции без admin-only KPI label;
  • собранные assets доступны: /static/dist/app.css, /static/dist/app.js, /static/dist/login.js, /static/system-status.js возвращают HTTP 200.

Примечание по smoke: in-app browser не всегда сохранял cookie после ручного form login при повторных быстрых прогонах и попадал в local rate-limit на /login. Для финального authenticated browser pass использован реальный Flask session cookie, полученный через /login с CSRF, и локальный одноразовый cookie-setter на 127.0.0.1; код приложения при этом не менялся.

VPS smoke 2026-05-17

Production host: 62.72.32.112, app path: /opt/crisis-monitor.

Перед вмешательством сохранены:

  • DB dump: /opt/backups/crisis-monitor-pre-admin-20260517-123002.sql.gz;
  • worktree patch: /opt/backups/crisis-monitor-worktree-20260517-123024.patch;
  • untracked assets tarball: /opt/backups/crisis-monitor-untracked-20260517-123024.tgz;
  • git snapshot branch: ops/prod-snapshot-20260517-123024 (6251c538).
  • second drift patch before final ops deploy: /opt/backups/crisis-monitor-prod-drift-20260517-135349.patch;
  • production drift snapshot on deploy branch: d5b5e861.

Для deploy-candidate создана ветка codex/prod-admin-deploy-20260517 от live snapshot; admin commits cherry-picked без конфликтов. Docker build web и arq-worker прошел успешно, включая Vite build (95 modules transformed). После выравнивания docker-compose.prod.yml web пересоздан через docker compose -f docker-compose.prod.yml up -d --no-deps web, DB/Redis не пересоздавались. ARQ worker запущен через тот же prod compose с ARQ_ENABLE_CRON=0 по умолчанию. Warning старого entrypoint на readonly static/dist после recreate больше не появляется.

Проверено после reload:

  • deployed code commit: 45b2d4e4;
  • container health: http://127.0.0.1:8096/health -> {"status":"ok"};
  • crisis-monitor-web-1: healthy, compose config label /opt/crisis-monitor/docker-compose.prod.yml;
  • crisis-monitor-arq-worker-1: healthy;
  • ARQ heartbeat: Redis key arq:health-check exists;
  • authenticated /admin/status/data: HTTP 200, heartbeat_seen=true, cron_enabled=false, registered_jobs=0;
  • public URL: https://crisis.qdev.run/login -> HTTP 200;
  • protected admin URL: https://crisis.qdev.run/admin/status -> HTTP 302 /login;
  • DB and Redis containers remained healthy.

Infra follow-up closed: docker-compose.prod.yml выровнен с реально запущенной VPS-схемой (.:/app, readonly qazstack bind, project volumes crisis-monitor_postgres_data / crisis-monitor_redis_data) и больше не требует несуществующий external volume crisis_monitor_uploads. ARQ worker запущен для queued jobs; cron jobs включаются только явным ARQ_ENABLE_CRON=1.

Локальные инструменты:

  • python3 на host: 3.9.6, pytest не установлен;
  • .venv создан на bundled Python 3.12.13;
  • npm: отсутствует в host PATH;
  • bundled Node runtime успешно запускает frontend JS tests;
  • локальный Vite build на macOS заблокирован Rollup optional native binary/code-signature issue; production Vite build подтвержден в Docker на VPS;
  • Docker: отсутствует.

Также обнаружен lockfile drift:

requirements.txt      -> certifi==2025.6.15
requirements-dev.txt  -> certifi==2025.4.26

Поэтому полный .venv/bin/pip install -r requirements.txt -r requirements-dev.txt сейчас конфликтует на зависимостях. Для локальной проверки использован runtime install из requirements.txt плюс минимальные test dependencies (pytest, pytest-asyncio, freezegun, beautifulsoup4, hypothesis).

2026-05-18: remaining UI/data pass

Закрыты два остаточных дефекта, найденных при финальном проходе по страницам:

  • /postmortem/compare больше не отдаёт 404, если пользователь открыл сравнение без выбранных ID. Теперь страница показывает AV DS empty-state на русском и ссылку обратно к хронологии.
  • /reports/builder переведён с сырого Bootstrap-экрана на текущий admin shell: page header, карточки полей/фильтров/отображения, стартовый empty-state, нормальные состояния загрузки/ошибки/результата.
  • Конструктор отчётов теперь использует реальный endpoint /reports/api/v1/reports/custom, передаёт session CSRF header, корректно различает chart: "table" и графики, умеет принимать и табличный список, и chart payload {labels, values}.
  • SQL для кастомного отчёта использует outerjoin к status/risk_level, чтобы строки без статуса или риска не выпадали из отчёта.
  • Значения таблицы и ошибки в report-builder.js экранируются перед вставкой в innerHTML; это закрывает XSS-риск на пользовательских заголовках/полях.
  • На login-форме добавлены autocomplete="username" и autocomplete="current-password" для fallback HTML и Vue-enhanced формы.
  • Для обновлённых report-builder.js и production dist/login.js добавлен точечный cache-bust query (builder=20260518, login=20260710), потому что production статика отдаётся с long-lived cache-control и старый браузерный кеш мог оставить сломанный endpoint конструктора отчётов.

Локальная проверка финального diff:

SECRET_KEY=test-secret .venv/bin/pytest -q
# 482 passed, 96 skipped

SECRET_KEY=test-secret .venv/bin/pytest -q tests/test_reports.py \
  tests/test_routes.py::test_postmortem_compare_without_selection_renders_empty_state \
  tests/test_routes.py::test_mark_resolved_sets_time_and_event \
  tests/test_routes.py::test_editor_cannot_mark_resolved
# 10 passed

SECRET_KEY=test-secret .venv/bin/pytest -q \
  tests/test_reports.py::test_report_builder_uses_registered_api_endpoint \
  tests/test_reports.py::test_report_builder_custom_api_returns_table_rows \
  tests/test_routes.py::test_login_footer_uses_single_version_source
# 3 passed

git diff --name-only -- '*.py' | xargs .venv/bin/ruff check
# All checks passed

node tests/js/run-tests.mjs
# All frontend tests passed

/Users/belilovsky/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node \
  node_modules/vite/bin/vite.js build
# built successfully

Browser/CDP smoke на http://127.0.0.1:5058:

  • /reports/builder: стартовый empty-state есть; API URL корректный; Сформировать отчёт возвращает таблицу на 196 строк без ошибки и overflow; график по статусу рендерит canvas без ошибки; mobile 390px без горизонтального overflow.
  • /postmortem/compare: пустое сравнение показывает Инциденты для сравнения не выбраны; старые английские строки не отображаются; overflow нет.
  • /login: autocomplete атрибуты на месте; консоль после финального smoke без warning/error.

Production smoke после deploy на https://crisis.qdev.run:

  • /health: HTTP 200, {"status":"ok"}.
  • web и arq-worker: Docker health healthy; DB/Redis также healthy.
  • /login: production dist/login.js грузится как /static/dist/login.js?v=0.5.3&login=20260710; autocomplete атрибуты username/current-password видны в реальном браузере.
  • /reports/builder: report-builder.js грузится как /static/report-builder.js?v=0.5.1&builder=20260518; endpoint /reports/api/v1/reports/custom на месте.
  • Конструктор отчётов на prod строит таблицу на 35 строк и bar chart с canvas; console warnings/errors нет.
  • Mobile smoke 390px для /reports/builder: горизонтального overflow нет.
  • /postmortem/compare: пустой выбор показывает русский empty-state, старые английские строки отсутствуют.

Следующие безопасные шаги

  1. Синхронизировать локальный Node/npm bootstrap: на host сейчас нет npm, поэтому стандартные npm run ... команды требуют отдельной установки Node toolchain.
  2. Синхронизировать package-lock.json с текущим package.json, чтобы npm ci снова был воспроизводимым в чистой среде.
  3. Расширить FastAPI CSRF contract за пределы /admin/*: сейчас закрыт admin router, но остальные FastAPI SSR/API mutation paths требуют отдельной инвентаризации JS/form клиентов.
  4. Если команде нужны именно scheduled jobs внутри ARQ, включить ARQ_ENABLE_CRON=1 отдельным operational-решением; без этого worker безопасно обрабатывает только queued jobs.

Ночной UI/runtime pass 2026-05-18

Закрыт дополнительный production smoke по страницам, где в браузере оставались ошибки консоли или пустые состояния:

  • /incidents/1/escalation больше не падает на InstrumentedList.order_by: история эскалаций берётся отдельным query, добавлен регрессионный тест.
  • /buffer/ больше не использует внешний via.placeholder.com; fallback-превью локальное data:image/svg+xml, внешняя ссылка получила rel="noopener noreferrer".
  • /tickets/1/notes больше не падает из-за Document.addEventListener.
  • /tickets?view=map больше не запрашивает отсутствующие socket.io и vendor/leaflet/images/layers.png.
  • /dashboard/live получил нормальный AV DS header, текстовое состояние и polling вместо несуществующего /events.
  • Footer и asset cache version синхронизированы на Кризис-Монитор v0.5.2 · AV DS 4.0.0, чтобы long-lived cached JS (max-age=31536000) гарантированно обновился.

Проверки:

SECRET_KEY=test-secret .venv/bin/pytest -q
# 482 passed, 96 skipped

.venv/bin/ruff check --select F app/escalation/views.py tests/test_escalation_rules.py tests/test_buffer.py app/main.py app/routers/deps.py
# All checks passed

/Users/belilovsky/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node \
  node_modules/vite/bin/vite.js build
# built successfully

/Users/belilovsky/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node \
  tests/js/run-tests.mjs
# All frontend tests passed

Follow-up 2026-05-18: старый repo-wide F-level Ruff baseline закрыт отдельным точечным проходом без массового форматирования.

.venv/bin/ruff check --select F .
# All checks passed

SECRET_KEY=test-secret .venv/bin/pytest -q
# 482 passed, 96 skipped

Production smoke после deploy:

  • /health: HTTP 200.
  • Docker services: web, arq-worker, db, redis running.
  • Targeted CDP smoke: /tickets?view=map, /tickets/1/notes, /buffer/, /dashboard/live, /incidents/1/escalation, /dashboard, /kanban, /analytics – без 4xx/5xx, runtime errors, broken images и overflow.
  • Wide CDP smoke: 47 основных страниц/вкладок – issueCount: 0.