diff --git a/backend/app/api/v1/dashboards.py b/backend/app/api/v1/dashboards.py index 636a7f7..e5ae341 100644 --- a/backend/app/api/v1/dashboards.py +++ b/backend/app/api/v1/dashboards.py @@ -10,7 +10,7 @@ from app.config import get_settings from app.database import get_db from app.models import Event, Host, Problem from app.schemas.list_models import EventSummary -from app.services.host_health import DAILY_REPORT_SSH, HEARTBEAT_TYPE, count_stale_hosts +from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts router = APIRouter(prefix="/dashboards", tags=["dashboards"]) @@ -89,7 +89,7 @@ def dashboard_summary( daily_reports_24h = db.scalar( select(func.count()) .select_from(Event) - .where(Event.received_at >= since, Event.type == DAILY_REPORT_SSH) + .where(Event.received_at >= since, Event.type.in_(DAILY_REPORT_TYPES)) ) or 0 problems_open = ( db.scalar(select(func.count()).select_from(Problem).where(Problem.status == "open")) or 0 diff --git a/backend/app/api/v1/hosts.py b/backend/app/api/v1/hosts.py index 1b4efe6..e7a9439 100644 --- a/backend/app/api/v1/hosts.py +++ b/backend/app/api/v1/hosts.py @@ -8,9 +8,9 @@ from app.database import get_db from app.models import Event, Host from app.schemas.list_models import HostListResponse, HostSummary from app.services.host_health import ( - DAILY_REPORT_SSH, HEARTBEAT_TYPE, agent_status, + max_daily_report_time_by_host, max_event_time_by_host, ) @@ -47,7 +47,7 @@ def list_hosts( settings = get_settings() hb_map = max_event_time_by_host(db, HEARTBEAT_TYPE) - report_map = max_event_time_by_host(db, DAILY_REPORT_SSH) + report_map = max_daily_report_time_by_host(db) items: list[HostSummary] = [] for host in rows: diff --git a/backend/app/api/v1/stream.py b/backend/app/api/v1/stream.py index f618f7e..b2896f6 100644 --- a/backend/app/api/v1/stream.py +++ b/backend/app/api/v1/stream.py @@ -11,7 +11,7 @@ from app.auth.jwt_auth import verify_access_token from app.database import SessionLocal from app.models import Event, Problem from app.config import get_settings -from app.services.host_health import DAILY_REPORT_SSH, HEARTBEAT_TYPE, count_stale_hosts +from app.services.host_health import DAILY_REPORT_TYPES, HEARTBEAT_TYPE, count_stale_hosts from app.version import APP_VERSION router = APIRouter(prefix="/stream", tags=["stream"]) @@ -43,7 +43,7 @@ def _dashboard_snapshot(db: Session) -> dict: "daily_reports_24h": db.scalar( select(func.count()) .select_from(Event) - .where(Event.received_at >= since, Event.type == DAILY_REPORT_SSH) + .where(Event.received_at >= since, Event.type.in_(DAILY_REPORT_TYPES)) ) or 0, "problems_open": problems_open, diff --git a/backend/app/services/host_health.py b/backend/app/services/host_health.py index 1126211..d9b0567 100644 --- a/backend/app/services/host_health.py +++ b/backend/app/services/host_health.py @@ -9,6 +9,8 @@ from app.models import Event, Host HEARTBEAT_TYPE = "agent.heartbeat" DAILY_REPORT_SSH = "report.daily.ssh" +DAILY_REPORT_RDP = "report.daily.rdp" +DAILY_REPORT_TYPES = (DAILY_REPORT_SSH, DAILY_REPORT_RDP) def max_event_time_by_host(db: Session, event_type: str) -> dict[int, datetime]: @@ -20,6 +22,15 @@ def max_event_time_by_host(db: Session, event_type: str) -> dict[int, datetime]: return {int(host_id): ts for host_id, ts in rows if ts is not None} +def max_daily_report_time_by_host(db: Session) -> dict[int, datetime]: + rows = db.execute( + select(Event.host_id, func.max(Event.received_at)) + .where(Event.type.in_(DAILY_REPORT_TYPES)) + .group_by(Event.host_id) + ).all() + return {int(host_id): ts for host_id, ts in rows if ts is not None} + + def agent_status( last_heartbeat_at: datetime | None, *, diff --git a/backend/tests/test_dashboard.py b/backend/tests/test_dashboard.py index a0b4958..9ef1737 100644 --- a/backend/tests/test_dashboard.py +++ b/backend/tests/test_dashboard.py @@ -92,6 +92,20 @@ def test_dashboard_summary_top_and_problems_24h(client, db_session, jwt_headers) assert types["agent.heartbeat"] == 2 +def test_dashboard_daily_reports_24h_includes_ssh_and_rdp(client, db_session, jwt_headers): + h_ssh = _host(db_session, "ssh-pilot") + h_rdp = _host(db_session, "rdp-pilot") + h_rdp.product = "rdp-login-monitor" + now = datetime.now(timezone.utc) + _event(db_session, h_ssh, "report.daily.ssh", received_at=now) + _event(db_session, h_rdp, "report.daily.rdp", received_at=now) + db_session.commit() + + r = client.get("/api/v1/dashboards/summary", headers=jwt_headers) + assert r.status_code == 200 + assert r.json()["daily_reports_24h"] == 2 + + def test_dashboard_recent_events_ordered_by_received_at(client, db_session, jwt_headers): h = _host(db_session, "live-host") now = datetime.now(timezone.utc) diff --git a/deploy/sac-deploy.sh b/deploy/sac-deploy.sh index 3f5d593..8f46a56 100644 --- a/deploy/sac-deploy.sh +++ b/deploy/sac-deploy.sh @@ -122,4 +122,22 @@ else die "${SERVICE_NAME} запущен, но /health не ответил — journalctl -u ${SERVICE_NAME} -n 80 --no-pager" fi +for aux in sac-retention sac-daily-report; do + for suffix in service timer; do + src="${APP_ROOT}/deploy/systemd/${aux}.${suffix}" + dst="/etc/systemd/system/${aux}.${suffix}" + if [ -f "${src}" ]; then + if [ ! -f "${dst}" ] || ! cmp -s "${src}" "${dst}"; then + log "Обновление ${dst}" + cp "${src}" "${dst}" + systemctl daemon-reload + fi + fi + done + if [ -f "/etc/systemd/system/${aux}.timer" ]; then + systemctl enable "${aux}.timer" 2>/dev/null || true + systemctl start "${aux}.timer" 2>/dev/null || true + fi +done + log "Готово." diff --git a/docs/analytics-backlog.md b/docs/analytics-backlog.md index a9b3360..f7ae7d5 100644 --- a/docs/analytics-backlog.md +++ b/docs/analytics-backlog.md @@ -9,7 +9,7 @@ | analytics-a1 | `GET /api/v1/analytics/series` (`bucket=hour\|day\|week\|month`, `from`/`to`) | JSON series + тесты | | analytics-a2 | `group_by=severity\|type\|product`, `exclude_types=agent.heartbeat` | heartbeat не в общем счётчике | | analytics-a3 | Presets 24h / 7d / 30d / 90d | документация API | -| analytics-a4 | Dashboard: `daily_reports_24h` учитывает `report.daily.rdp` | test_dashboard | +| analytics-a4 | Dashboard: `daily_reports_24h` учитывает `report.daily.rdp` | ✅ test_dashboard | | analytics-a5 | Индексы под агрегации | EXPLAIN без full scan | ## Фаза B — UI