diff --git a/agent-deb/opt/info-user-agent/agent.py b/agent-deb/opt/info-user-agent/agent.py index 84af438..247d0cd 100755 --- a/agent-deb/opt/info-user-agent/agent.py +++ b/agent-deb/opt/info-user-agent/agent.py @@ -7,6 +7,7 @@ import getpass import json import requests import logging +import subprocess from pathlib import Path logging.basicConfig( @@ -70,6 +71,62 @@ def get_disk_info(): return disks +def get_active_sessions(): + """Return active user sessions similar to the `who` command. + + Each session is a dict with keys: username, tty, login_time, origin. + Falls back to the current process user if `who` is unavailable. + """ + try: + result = subprocess.run( + ["who", "-u"], + capture_output=True, + text=True, + timeout=5, + check=False, + ) + if result.returncode != 0: + return [] + sessions = [] + for line in result.stdout.strip().splitlines(): + line = line.strip() + if not line: + continue + parts = line.split() + if len(parts) < 5: + continue + username = parts[0] + tty = parts[1] + # login time is usually "YYYY-MM-DD HH:MM" (parts[2] and parts[3]) + login_time = " ".join(parts[2:4]) if len(parts) >= 4 else "" + # The last field may be a comment in parentheses, e.g. (192.168.255.50) + origin = "" + if parts[-1].startswith("(") and parts[-1].endswith(")"): + origin = parts[-1][1:-1] + sessions.append({ + "username": username, + "tty": tty, + "login_time": login_time, + "origin": origin, + }) + return sessions + except Exception: + return [] + + +def get_display_username(sessions): + """Return a short username representation for the dashboard. + + Single user -> username + Multiple -> comma-separated usernames + No sessions -> fallback to process user + """ + usernames = [s["username"] for s in sessions if s.get("username")] + if not usernames: + return getpass.getuser() + return ", ".join(dict.fromkeys(usernames)) + + def get_cpu_temp(): try: temps = psutil.sensors_temperatures() @@ -108,9 +165,11 @@ def get_top_processes(limit=10): def collect(): cfg = load_config() + sessions = get_active_sessions() payload = { "hostname": socket.gethostname(), - "username": getpass.getuser(), + "username": get_display_username(sessions), + "sessions": sessions, "os_info": f"{platform.system()} {platform.release()}", "local_ip": get_local_ip(), "public_ip": get_public_ip() if cfg.get("send_public_ip") else None, diff --git a/agent/agent.py b/agent/agent.py index 84af438..247d0cd 100644 --- a/agent/agent.py +++ b/agent/agent.py @@ -7,6 +7,7 @@ import getpass import json import requests import logging +import subprocess from pathlib import Path logging.basicConfig( @@ -70,6 +71,62 @@ def get_disk_info(): return disks +def get_active_sessions(): + """Return active user sessions similar to the `who` command. + + Each session is a dict with keys: username, tty, login_time, origin. + Falls back to the current process user if `who` is unavailable. + """ + try: + result = subprocess.run( + ["who", "-u"], + capture_output=True, + text=True, + timeout=5, + check=False, + ) + if result.returncode != 0: + return [] + sessions = [] + for line in result.stdout.strip().splitlines(): + line = line.strip() + if not line: + continue + parts = line.split() + if len(parts) < 5: + continue + username = parts[0] + tty = parts[1] + # login time is usually "YYYY-MM-DD HH:MM" (parts[2] and parts[3]) + login_time = " ".join(parts[2:4]) if len(parts) >= 4 else "" + # The last field may be a comment in parentheses, e.g. (192.168.255.50) + origin = "" + if parts[-1].startswith("(") and parts[-1].endswith(")"): + origin = parts[-1][1:-1] + sessions.append({ + "username": username, + "tty": tty, + "login_time": login_time, + "origin": origin, + }) + return sessions + except Exception: + return [] + + +def get_display_username(sessions): + """Return a short username representation for the dashboard. + + Single user -> username + Multiple -> comma-separated usernames + No sessions -> fallback to process user + """ + usernames = [s["username"] for s in sessions if s.get("username")] + if not usernames: + return getpass.getuser() + return ", ".join(dict.fromkeys(usernames)) + + def get_cpu_temp(): try: temps = psutil.sensors_temperatures() @@ -108,9 +165,11 @@ def get_top_processes(limit=10): def collect(): cfg = load_config() + sessions = get_active_sessions() payload = { "hostname": socket.gethostname(), - "username": getpass.getuser(), + "username": get_display_username(sessions), + "sessions": sessions, "os_info": f"{platform.system()} {platform.release()}", "local_ip": get_local_ip(), "public_ip": get_public_ip() if cfg.get("send_public_ip") else None, diff --git a/server/database.py b/server/database.py index 8961bed..b0ecd78 100644 --- a/server/database.py +++ b/server/database.py @@ -38,3 +38,11 @@ def run_migrations(): with engine.connect() as conn: conn.execute(text("ALTER TABLE audit_log ADD COLUMN username TEXT")) conn.commit() + + # Heartbeat: add sessions column if missing + if "heartbeats" in insp.get_table_names(): + hb_cols = {c["name"] for c in insp.get_columns("heartbeats")} + if "sessions" not in hb_cols: + with engine.connect() as conn: + conn.execute(text("ALTER TABLE heartbeats ADD COLUMN sessions TEXT")) + conn.commit() diff --git a/server/models.py b/server/models.py index 8fa2305..52a4a0c 100644 --- a/server/models.py +++ b/server/models.py @@ -47,6 +47,7 @@ class Heartbeat(Base): cpu_temp = Column(Float, nullable=True) load_avg = Column(String, nullable=True) processes = Column(JSON, nullable=True) + sessions = Column(JSON, nullable=True) class AuditLog(Base): diff --git a/server/routes/api_routes.py b/server/routes/api_routes.py index 98a655f..79b6a87 100644 --- a/server/routes/api_routes.py +++ b/server/routes/api_routes.py @@ -69,6 +69,10 @@ def heartbeat( computer.current_ram_percent = payload.ram_percent computer.current_ram_total_gb = payload.ram_total_gb + sessions = payload.sessions + if sessions: + sessions = [s.model_dump() for s in sessions] + heartbeat = Heartbeat( computer_id=computer.id, timestamp=now, @@ -83,6 +87,7 @@ def heartbeat( cpu_temp=payload.cpu_temp, load_avg=payload.load_avg, processes=payload.processes or [], + sessions=sessions, ) db.add(heartbeat) db.commit() diff --git a/server/routes/web_routes.py b/server/routes/web_routes.py index ed2ca6c..a81c1f8 100644 --- a/server/routes/web_routes.py +++ b/server/routes/web_routes.py @@ -115,6 +115,7 @@ def computer_page(computer_id: int, request: Request, db: Session = Depends(get_ ) hb_data = [] + latest_sessions = [] for h in heartbeats: hb_data.append({ "timestamp": h.timestamp.isoformat() if h.timestamp else None, @@ -124,7 +125,10 @@ def computer_page(computer_id: int, request: Request, db: Session = Depends(get_ "ram_percent": h.ram_percent, "cpu_temp": h.cpu_temp, "processes": h.processes or [], + "sessions": h.sessions or [], }) + if not latest_sessions and h.sessions: + latest_sessions = h.sessions return templates.TemplateResponse( "computer.html", @@ -133,6 +137,7 @@ def computer_page(computer_id: int, request: Request, db: Session = Depends(get_ "user": current_user, "computer": computer, "heartbeats": hb_data, + "latest_sessions": latest_sessions, "unread_count": get_unread_count(db), }, ) diff --git a/server/schemas.py b/server/schemas.py index 74f4603..43355ac 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -46,6 +46,13 @@ class LoginPayload(BaseModel): password: str +class SessionInfo(BaseModel): + username: Optional[str] = None + tty: Optional[str] = None + login_time: Optional[str] = None + origin: Optional[str] = None + + class HeartbeatPayload(BaseModel): hostname: str username: str @@ -60,6 +67,7 @@ class HeartbeatPayload(BaseModel): cpu_temp: Optional[float] = None load_avg: Optional[str] = None processes: Optional[List[dict]] = None + sessions: Optional[List[SessionInfo]] = None class HeartbeatOut(BaseModel): @@ -70,6 +78,7 @@ class HeartbeatOut(BaseModel): cpu_percent: Optional[float] ram_percent: Optional[float] cpu_temp: Optional[float] + sessions: Optional[List[SessionInfo]] = None model_config = ConfigDict(from_attributes=True) diff --git a/server/templates/computer.html b/server/templates/computer.html index e8d1e9f..0508185 100644 --- a/server/templates/computer.html +++ b/server/templates/computer.html @@ -23,7 +23,11 @@
Пользователь
+ {% if computer.current_user and ',' in computer.current_user %} + за ПК работают {{ computer.current_user.split(',') | length }} пользователя + {% else %}
{{ computer.current_user or '-' }}
+ {% endif %}
@@ -45,6 +49,31 @@ +
+
+
Активные сессии
+ {% if latest_sessions %} + + + + + + {% for s in latest_sessions %} + + + + + + + {% endfor %} + +
ПользовательTTYВремя входаОткуда
{{ s.username or '-' }}{{ s.tty or '-' }}{{ s.login_time or '-' }}{{ s.origin or '-' }}
+ {% else %} +
Нет данных о сессиях.
+ {% endif %} +
+
+
diff --git a/server/templates/dashboard.html b/server/templates/dashboard.html index 6a0979f..9ac485a 100644 --- a/server/templates/dashboard.html +++ b/server/templates/dashboard.html @@ -64,11 +64,17 @@ - + {% for c in computers %} {{ c.hostname }} - {{ c.current_user or '-' }} + + {% if c.current_user and ',' in c.current_user %} + за ПК работают {{ c.current_user.split(',') | length }} пользователя + {% else %} + {{ c.current_user or '-' }} + {% endif %} + {{ c.current_ip or '-' }} {% if c.status == 'online' %} @@ -101,6 +107,15 @@