d5a270f3af
- backend/logging_setup.py: setup_logging() (stdout INFO + файл /tmp/vector/vector.log,
ротация 5×2 МБ, VectorLOGLEVEL/VectorLOGFILE/VectorLOGDIR), get_logger('vector.*')
- события: vector.analyze (матрасчёт с длительностью), vector.auth (login_ok/failed/
lockout), vector.audit (каждая запись), vector.contour (ok/failed/rejected),
vector.operations (operation_complete), vector.geocode (успех/timeout/ошибка)
- существующие логгеры переведены в namespace vector.* (geo, geo.osm, rules, water, admin)
- README §13а: таблица наблюдаемых событий + правила (без ПДн/секретов)
- SETUP_GUIDE: §15 RPO/RTO + backup≠restore, §16 grep по vector.*, §17 диагностика
по цепочкам (БД, 500, fallback-зоны, вход), §17а чек-лист передачи проекта
- DEPLOY.md: вместо устаревшего дубля — ссылка на актуальные доки + шпаргалка стенда
- тесты: 239 passed
72 lines
3.1 KiB
Python
72 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
from backend.database import db
|
|
from backend.logging_setup import get_logger
|
|
from backend.routers.auth import require_roles
|
|
from backend.schemas import CaseListResponse, CaseResponse, CaseUpdate, DashboardResponse
|
|
|
|
log = get_logger('admin')
|
|
|
|
router = APIRouter(prefix='/api/v1/admin', tags=['admin'], dependencies=[Depends(require_roles(['admin']))])
|
|
|
|
|
|
@router.get('/cases', response_model=CaseListResponse)
|
|
def admin_cases(
|
|
page: int = Query(default=1, ge=1),
|
|
page_size: int = Query(default=20, ge=1, le=100),
|
|
status: str | None = None,
|
|
age_min: int | None = Query(default=None, ge=0),
|
|
age_max: int | None = Query(default=None, ge=0),
|
|
) -> dict:
|
|
cases = db.list_cases()
|
|
if status:
|
|
cases = [c for c in cases if c.status == status]
|
|
if age_min is not None:
|
|
cases = [c for c in cases if c.age is not None and c.age >= age_min]
|
|
if age_max is not None:
|
|
cases = [c for c in cases if c.age is not None and c.age <= age_max]
|
|
start = (page - 1) * page_size
|
|
end = start + page_size
|
|
return {'items': [c.to_detail() for c in cases[start:end]], 'total': len(cases), 'page': page, 'page_size': page_size}
|
|
|
|
|
|
@router.patch('/cases/{case_id}', response_model=CaseResponse)
|
|
def admin_update_case(case_id: str, payload: CaseUpdate) -> dict:
|
|
data = payload.model_dump(exclude_none=True)
|
|
# default_factory-списки ([]) при exclude_none всё равно попадают в data и
|
|
# ЗАТИРАЮТ реальные terrain/diagnosis_type — выкидываем пустые списки:
|
|
data = {k: v for k, v in data.items() if not (isinstance(v, list) and not v)}
|
|
note = data.pop('note', None)
|
|
if note is not None and 'notes' not in data:
|
|
data['notes'] = note
|
|
case = db.update_case(case_id, **data)
|
|
if not case:
|
|
raise HTTPException(status_code=404, detail='Case not found')
|
|
|
|
# B21-синхронизация: кейс закрыт вручную → его операция завершается автоматически
|
|
if data.get('status') == 'closed':
|
|
import uuid as _uuid
|
|
from backend.models import SearchOperation as _Op
|
|
from backend.database import SessionLocal as _SL
|
|
case_uuid = _uuid.UUID(str(case.id)) if isinstance(case.id, str) else case.id
|
|
with _SL() as session:
|
|
ops = session.query(_Op).filter(_Op.case_id == case_uuid,
|
|
_Op.status.in_(['planned', 'active', 'paused'])).all()
|
|
for op in ops:
|
|
op.status = 'completed'
|
|
op.closed_at = datetime.now(timezone.utc)
|
|
session.commit()
|
|
closed_ops = len(ops)
|
|
if closed_ops:
|
|
log.info('admin closed case %s → %d операция(ий) завершены автоматически', case_id, closed_ops)
|
|
return case.to_detail()
|
|
|
|
|
|
@router.get('/dashboard', response_model=DashboardResponse)
|
|
def admin_dashboard() -> dict:
|
|
return db.stats()
|