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.
Риски и разрывы
wsgi.pyиapp/main.pyописывают разные реальности. Production compose запускает Flask, а FastAPI-файлы и часть тестов ожидают migration-complete состояние.- FastAPI SSR path ранее не имел полноценного CSRF: в
app/routers/deps.pyтокен был заглушен пустой строкой, mutation dependencies не требовали CSRF. В этом проходе закрыт/admin/*FastAPI router; остальные FastAPI SSR/API mutation paths требуют отдельного маршрута внедрения. - Admin audit сейчас в основном ticket-centric. Админские изменения ролей, блокировок, doctrine import/export, backup restore/create и scripts пишутся не единым admin audit event shape, а точечно в logger или вообще без отдельного audit event.
- System dashboard показывает базовые checks, но не раскрывает операционные статусы worker/cron/integrations в виде отдельной модели. Это важно для crisis-продукта.
- В
app/api.pyиapp/routers/api.pyplaybook step mutation вызываетcommit_session()доlog_ticket_action(...); audit event создается после commit и может не сохраниться. - Локальная среда на текущей машине неполная: системный Python 3.9 без pytest, системный
npmи Docker отсутствуют. Проверки выполняются через игнорируемый.venvна Python >=3.11 и bundled Node runtime.
Сделано в этом проходе
fix(api): сохранять audit для playbook mutationslog_ticket_action(...)перенесен доcommit_session()в Flask и FastAPI API paths;- добавлен regression test на сохранение
TicketAudit(action="playbook_step"). feat(admin): добавить structured audit helper- добавлен
utils/admin_audit.pyбез миграции БД; - admin events пишутся в app logger и существующий external sink;
- secret-like поля в metadata редактируются перед записью;
- helper подключен к role/block changes, doctrine import/export, backup create/restore и safe script start.
feat(admin): расширить system dashboard model- добавлен
utils/system_status.pyкак общий контракт для Flask/FastAPI; /admin/status/dataсохраняет старые поляchecksиmetrics;- дополнительно возвращаются
workers,cron,integrations,storage,last_errors; - модель не раскрывает значения секретов, только configured/not configured.
test(admin): стабилизировать secret bootstrapSECRET_KEYвыставляется до imports, которые могут загрузитьconfig.BaseConfig;- это убирает ложное падение тестов до входа в реальные fixtures.
feat(admin): добавить FastAPI csrf contract- FastAPI templates теперь получают session CSRF token вместо пустой строки;
- unsafe
/admin/*requests в FastAPI path требуют form/header CSRF token для session-authenticated browser requests; - добавлены unit-тесты на accept/reject CSRF token;
- чистые unit-тесты помечаются
no_reference_data, чтобы не поднимать GeoAlchemy/SpatiaLite fixtures без необходимости. feat(admin): показать operations health на status page- страница
/admin/statusтеперь отображает workers/cron, integrations и storage; - worker-блок дополнительно показывает ARQ heartbeat key, когда worker реально запущен;
- динамические списки собираются через DOM API без
innerHTML; - существующие checks/metrics и polling сохранены.
fix(models): использовать sqlite geometry fallback без spatialite- SQLite/local test mode больше не пытается создать GeoAlchemy geometry column через SpatiaLite, если
ENABLE_SQLITE_SPATIALITEне включен явно; - PostGIS/non-SQLite path сохраняет GeoAlchemy geometry;
- это снимает локальный blocker для admin/geozone тестов без изменения production Postgres-контракта.
fix(admin): защитить storage status от fab overlap- добавлен узкий CSS-scope только для storage-card на
/admin/status; - бейджи storage health больше не попадают под плавающую Janus/FAB кнопку на desktop;
- AV DS tokens и глобальная дизайн-система не менялись.
fix(admin): стабилизировать regression suite и local smoke- закрыты baseline-падения routes/approvals/search/undo/help/PDF/export/backup/ARQ без изменения публичных URL;
- PDF generation получает безопасный fallback, когда native WeasyPrint-библиотеки недоступны локально;
- frontend smoke modules восстановлены, а system-status polling устойчив к login redirects и partial JSON;
- обновлен 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 и
TicketAudithash chain; - escalation rules, assignment rules, playbooks;
- ICS/NIMS/SitRep/CAP форматы;
- ingestion sources и crisis-specific cron jobs;
- WeasyPrint/PDF/report generation;
- QazCompute/QazGeo/QazLake integrations.
План маленьких коммитов
docs(admin): описать план модернизации- этот документ, карта production/FastAPI split, риски и safe plan.
fix(api): сохранять audit для playbook mutations- перенести
log_ticket_action(...)до commit или добавить второй commit в Flask и FastAPI API paths; - добавить regression test для сохранения
playbook_stepaudit event. feat(admin): добавить admin audit helper без миграции БД- структурированный logger/external sink helper для admin events;
- использовать для role/block/backup/script/doctrine mutation paths;
- без новой таблицы, чтобы не менять схему в первом проходе.
feat(admin): расширить system dashboard model- helper, который возвращает
checks,metrics,workers,cron,integrations,storage; - подключить к Flask
/admin/status/data; - FastAPI path обновить тем же shape, если без риска.
test(admin): усилить smoke админки- unauth redirect, protected route access, CSRF reject, role restriction, audit write, system status smoke.
docs(admin): обновить smoke/deploy notes- что проверено локально, что требует 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-checkexists; - 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и productiondist/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 healthhealthy; DB/Redis такжеhealthy./login: productiondist/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, старые английские строки отсутствуют.
Следующие безопасные шаги
- Синхронизировать локальный Node/npm bootstrap: на host сейчас нет
npm, поэтому стандартныеnpm run ...команды требуют отдельной установки Node toolchain. - Синхронизировать
package-lock.jsonс текущимpackage.json, чтобыnpm ciснова был воспроизводимым в чистой среде. - Расширить FastAPI CSRF contract за пределы
/admin/*: сейчас закрыт admin router, но остальные FastAPI SSR/API mutation paths требуют отдельной инвентаризации JS/form клиентов. - Если команде нужны именно 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,redisrunning. - 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.