File size: 3,382 Bytes
ac7ea7c
 
 
 
 
 
 
 
 
 
 
 
 
 
214c544
ac7ea7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214c544
ac7ea7c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
"""DDA admin diagnostics and maintenance (Phase 7)."""
from __future__ import annotations

import logging
import shutil
from datetime import datetime, timedelta, timezone

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session

from ..database import DATA_DIR, get_db
from ..models import DetectionRun, User
from .dda_auth import current_dda_user, get_user_role, require_min_role
from .job_runner import is_job_runner_busy, reconcile_stale_jobs
from .tree.image_service import list_all_images
from .models import DetectionJob, RegionReview

logger = logging.getLogger(__name__)
router = APIRouter()


def _require_dda():
    from .config import IS_DDA_MODE
    if not IS_DDA_MODE:
        raise HTTPException(status_code=404, detail="DDA mode is not enabled")


@router.get("/admin/status")
def admin_status(
    request: Request,
    db: Session = Depends(get_db),
    user: User = Depends(current_dda_user),
):
    """System status for UAT / ops (analyst+)."""
    _require_dda()
    require_min_role(user, db, "analyst")

    runs = db.query(DetectionRun).count()
    jobs_queued = db.query(DetectionJob).filter(DetectionJob.status == "queued").count()
    jobs_running = db.query(DetectionJob).filter(DetectionJob.status == "running").count()
    fp_count = db.query(RegionReview).filter(RegionReview.status == "false_positive").count()

    disk = shutil.disk_usage(DATA_DIR)
    return {
        "role": get_user_role(db, user),
        "libraryImages": len(list_all_images(db)),
        "detectionRuns": runs,
        "jobsQueued": jobs_queued,
        "jobsRunning": jobs_running,
        "runnerBusy": is_job_runner_busy(),
        "falsePositiveArchive": fp_count,
        "dataDir": str(DATA_DIR),
        "diskFreeGb": round(disk.free / (1024 ** 3), 2),
        "diskTotalGb": round(disk.total / (1024 ** 3), 2),
    }


@router.post("/admin/reconcile-jobs")
def admin_reconcile_jobs(
    request: Request,
    db: Session = Depends(get_db),
    user: User = Depends(current_dda_user),
):
    _require_dda()
    require_min_role(user, db, "admin")
    fixed = reconcile_stale_jobs(db)
    return {"ok": True, "staleRunningFixed": fixed}


@router.delete("/admin/purge-runs")
def admin_purge_old_runs(
    request: Request,
    days: int = Query(90, ge=7, le=3650),
    db: Session = Depends(get_db),
    user: User = Depends(current_dda_user),
):
    """Delete detection runs older than N days (admin only)."""
    _require_dda()
    require_min_role(user, db, "admin")
    cutoff = datetime.now(timezone.utc) - timedelta(days=days)
    old = db.query(DetectionRun).filter(DetectionRun.created_at < cutoff).all()
    count = 0
    overlays_root = DATA_DIR
    for run in old:
        db.query(RegionReview).filter(RegionReview.run_id == run.id).delete()
        for job in db.query(DetectionJob).filter(DetectionJob.run_id == run.id).all():
            job.run_id = None
        for path_attr in ("overlay_path", "before_full_path", "before_thumb_path", "after_thumb_path", "after_full_path"):
            path_val = getattr(run, path_attr, None)
            if path_val:
                f = overlays_root / path_val
                if f.exists():
                    f.unlink(missing_ok=True)
        db.delete(run)
        count += 1
    db.commit()
    return {"ok": True, "purgedRuns": count, "olderThanDays": days}