feat: show RDP session duration in Overview and Events (0.5.16)

Add session_duration_sec to EventSummary and a Duration column (HH:MM:SS / Nd HH:MM:SS).
This commit is contained in:
PTah
2026-07-14 10:30:13 +10:00
parent 52125a5a98
commit 985d51c4ba
13 changed files with 97 additions and 7 deletions
+1
View File
@@ -78,6 +78,7 @@ class EventSummary(BaseModel):
title: str
summary: str
actor_user: str | None = None
session_duration_sec: int | None = None
rdg_flap: bool = False
rdg_flap_pair_event_id: int | None = None
rdg_flap_qwinsta_event_id: int | None = None
+4
View File
@@ -6,6 +6,7 @@ from app.services.event_actor_user import extract_event_actor_user
from app.services.host_sessions import event_session_terminated
from app.services.rdg_display import build_rdg_display
from app.services.rdg_session_flap import resolve_rdg_flap_summary
from app.services.session_duration import extract_session_duration_sec
def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
@@ -44,6 +45,9 @@ def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
title=title,
summary=summary,
actor_user=extract_event_actor_user(event.type, event.details),
session_duration_sec=extract_session_duration_sec(
event.details if isinstance(event.details, dict) else None
),
rdg_flap=rdg_flap,
rdg_flap_pair_event_id=rdg_flap_pair_event_id,
rdg_flap_qwinsta_event_id=rdg_flap_qwinsta_event_id,
+37
View File
@@ -0,0 +1,37 @@
"""Format and extract RDP/RDG session_duration_sec for UI."""
from __future__ import annotations
from typing import Any
def extract_session_duration_sec(details: dict[str, Any] | None) -> int | None:
if not isinstance(details, dict):
return None
raw = details.get("session_duration_sec")
if raw is None or raw == "":
return None
try:
value = int(raw)
except (TypeError, ValueError):
return None
if value < 0:
return None
return value
def format_session_duration(seconds: int) -> str:
"""
Human-readable session length for lists:
- under 24h → HH:MM:SS (05:05:24)
- 24h+ → Nд HH:MM:SS (1д 00:01:25)
"""
if seconds < 0:
return ""
days, rem = divmod(int(seconds), 86_400)
hours, rem = divmod(rem, 3600)
minutes, secs = divmod(rem, 60)
clock = f"{hours:02d}:{minutes:02d}:{secs:02d}"
if days:
return f"{days}д {clock}"
return clock
+5 -3
View File
@@ -564,9 +564,11 @@ def _format_rdg_html(event: Event) -> str:
err = _detail(details, "gateway_error_code", "error_code", default="")
if err != "-":
msg += _line("⚠️", "Код ошибки", html_escape(err))
dur = _detail(details, "session_duration_sec", default="")
if dur not in ("-", "0", ""):
msg += _line("⏱️", "Длительность", f"{html_escape(dur)} с")
from app.services.session_duration import extract_session_duration_sec, format_session_duration
dur_sec = extract_session_duration_sec(details if isinstance(details, dict) else None)
if dur_sec is not None and dur_sec > 0:
msg += _line("⏱️", "Длительность", html_escape(format_session_duration(dur_sec)))
msg += _line("🕐", "Время", format_time(event.occurred_at))
win_id = _detail(details, "event_id_windows", default="")
if win_id != "-":
+1 -1
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center"
APP_VERSION = "0.5.15"
APP_VERSION = "0.5.16"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
+20
View File
@@ -0,0 +1,20 @@
"""Tests for session_duration_sec extract/format."""
from app.services.session_duration import extract_session_duration_sec, format_session_duration
def test_extract_session_duration_sec():
assert extract_session_duration_sec({"session_duration_sec": 18324}) == 18324
assert extract_session_duration_sec({"session_duration_sec": "42"}) == 42
assert extract_session_duration_sec({"session_duration_sec": ""}) is None
assert extract_session_duration_sec({}) is None
assert extract_session_duration_sec(None) is None
assert extract_session_duration_sec({"session_duration_sec": -1}) is None
def test_format_session_duration():
assert format_session_duration(0) == "00:00:00"
assert format_session_duration(18324) == "05:05:24"
assert format_session_duration(2428) == "00:40:28"
assert format_session_duration(86400 + 85) == "1д 00:01:25"
assert format_session_duration(2 * 86400 + 3661) == "2д 01:01:01"