diff --git a/agent-deb/DEBIAN/control b/agent-deb/DEBIAN/control index 2837c79..ad3fefd 100644 --- a/agent-deb/DEBIAN/control +++ b/agent-deb/DEBIAN/control @@ -1,5 +1,5 @@ Package: info-user-agent -Version: 1.0.1 +Version: 1.0.3 Section: admin Priority: optional Architecture: all diff --git a/agent-deb/opt/info-user-agent/agent.py b/agent-deb/opt/info-user-agent/agent.py index 247d0cd..b12e060 100755 --- a/agent-deb/opt/info-user-agent/agent.py +++ b/agent-deb/opt/info-user-agent/agent.py @@ -147,15 +147,31 @@ def get_load_avg(): return None -def get_top_processes(limit=10): +def get_top_processes(limit=10, processes=None): + """Return top processes by CPU usage. + + If ``processes`` is provided, it should be a list of psutil.Process objects + whose CPU baseline has already been established (cpu_percent called once). + The actual CPU usage is measured with the next call to cpu_percent(). + """ + if processes is None: + processes = [] + for p in psutil.process_iter(["pid", "name"]): + try: + p.cpu_percent(interval=None) + processes.append(p) + except Exception: + continue + time.sleep(1) + procs = [] - for p in psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]): + for p in processes: try: procs.append({ - "pid": p.info["pid"], - "name": p.info["name"], - "cpu_percent": round(p.info["cpu_percent"] or 0, 1), - "ram_percent": round((p.info["memory_percent"] or 0), 1), + "pid": p.pid, + "name": p.name(), + "cpu_percent": round(p.cpu_percent(interval=None), 1), + "ram_percent": round(p.memory_percent(), 1), }) except Exception: continue @@ -166,6 +182,21 @@ def get_top_processes(limit=10): def collect(): cfg = load_config() sessions = get_active_sessions() + + # Establish per-process CPU baseline first. The 1-second interval below for + # system CPU also gives us meaningful per-process CPU percentages without + # an extra delay. + baseline_procs = [] + for p in psutil.process_iter(["pid", "name"]): + try: + p.cpu_percent(interval=None) + baseline_procs.append(p) + except Exception: + continue + + vm = psutil.virtual_memory() + sm = psutil.swap_memory() + payload = { "hostname": socket.gethostname(), "username": get_display_username(sessions), @@ -175,12 +206,18 @@ def collect(): "public_ip": get_public_ip() if cfg.get("send_public_ip") else None, "uptime_sec": get_uptime(), "cpu_percent": psutil.cpu_percent(interval=1), - "ram_percent": psutil.virtual_memory().percent, - "ram_total_gb": round(psutil.virtual_memory().total / (2**30), 1), + "ram_percent": vm.percent, + "ram_total_gb": round(vm.total / (2**30), 1), + "ram_used_gb": round(vm.used / (2**30), 1), + "ram_available_gb": round(vm.available / (2**30), 1), + "swap_total_gb": round(sm.total / (2**30), 1), + "swap_used_gb": round(sm.used / (2**30), 1), + "swap_free_gb": round(sm.free / (2**30), 1), + "swap_percent": sm.percent, "disk_info": get_disk_info(), "cpu_temp": get_cpu_temp(), "load_avg": get_load_avg(), - "processes": get_top_processes(10), + "processes": get_top_processes(10, baseline_procs), } return payload, cfg diff --git a/agent/agent.py b/agent/agent.py index 247d0cd..b12e060 100644 --- a/agent/agent.py +++ b/agent/agent.py @@ -147,15 +147,31 @@ def get_load_avg(): return None -def get_top_processes(limit=10): +def get_top_processes(limit=10, processes=None): + """Return top processes by CPU usage. + + If ``processes`` is provided, it should be a list of psutil.Process objects + whose CPU baseline has already been established (cpu_percent called once). + The actual CPU usage is measured with the next call to cpu_percent(). + """ + if processes is None: + processes = [] + for p in psutil.process_iter(["pid", "name"]): + try: + p.cpu_percent(interval=None) + processes.append(p) + except Exception: + continue + time.sleep(1) + procs = [] - for p in psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]): + for p in processes: try: procs.append({ - "pid": p.info["pid"], - "name": p.info["name"], - "cpu_percent": round(p.info["cpu_percent"] or 0, 1), - "ram_percent": round((p.info["memory_percent"] or 0), 1), + "pid": p.pid, + "name": p.name(), + "cpu_percent": round(p.cpu_percent(interval=None), 1), + "ram_percent": round(p.memory_percent(), 1), }) except Exception: continue @@ -166,6 +182,21 @@ def get_top_processes(limit=10): def collect(): cfg = load_config() sessions = get_active_sessions() + + # Establish per-process CPU baseline first. The 1-second interval below for + # system CPU also gives us meaningful per-process CPU percentages without + # an extra delay. + baseline_procs = [] + for p in psutil.process_iter(["pid", "name"]): + try: + p.cpu_percent(interval=None) + baseline_procs.append(p) + except Exception: + continue + + vm = psutil.virtual_memory() + sm = psutil.swap_memory() + payload = { "hostname": socket.gethostname(), "username": get_display_username(sessions), @@ -175,12 +206,18 @@ def collect(): "public_ip": get_public_ip() if cfg.get("send_public_ip") else None, "uptime_sec": get_uptime(), "cpu_percent": psutil.cpu_percent(interval=1), - "ram_percent": psutil.virtual_memory().percent, - "ram_total_gb": round(psutil.virtual_memory().total / (2**30), 1), + "ram_percent": vm.percent, + "ram_total_gb": round(vm.total / (2**30), 1), + "ram_used_gb": round(vm.used / (2**30), 1), + "ram_available_gb": round(vm.available / (2**30), 1), + "swap_total_gb": round(sm.total / (2**30), 1), + "swap_used_gb": round(sm.used / (2**30), 1), + "swap_free_gb": round(sm.free / (2**30), 1), + "swap_percent": sm.percent, "disk_info": get_disk_info(), "cpu_temp": get_cpu_temp(), "load_avg": get_load_avg(), - "processes": get_top_processes(10), + "processes": get_top_processes(10, baseline_procs), } return payload, cfg diff --git a/info-user-agent_1.0.1_all.deb b/info-user-agent_1.0.1_all.deb deleted file mode 100644 index 5915307..0000000 Binary files a/info-user-agent_1.0.1_all.deb and /dev/null differ diff --git a/info-user-agent_1.0.3_all.deb b/info-user-agent_1.0.3_all.deb new file mode 100644 index 0000000..9d21a9a Binary files /dev/null and b/info-user-agent_1.0.3_all.deb differ diff --git a/server/database.py b/server/database.py index b0ecd78..a6f2d42 100644 --- a/server/database.py +++ b/server/database.py @@ -39,10 +39,37 @@ def run_migrations(): conn.execute(text("ALTER TABLE audit_log ADD COLUMN username TEXT")) conn.commit() - # Heartbeat: add sessions column if missing + # Heartbeat: add missing columns 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() + new_hb_cols = [ + ("sessions", "TEXT"), + ("ram_used_gb", "REAL"), + ("ram_available_gb", "REAL"), + ("swap_total_gb", "REAL"), + ("swap_used_gb", "REAL"), + ("swap_free_gb", "REAL"), + ("swap_percent", "REAL"), + ] + with engine.connect() as conn: + for col, col_type in new_hb_cols: + if col not in hb_cols: + conn.execute(text(f"ALTER TABLE heartbeats ADD COLUMN {col} {col_type}")) + conn.commit() + + # Computer: add missing columns + if "computers" in insp.get_table_names(): + pc_cols = {c["name"] for c in insp.get_columns("computers")} + new_pc_cols = [ + ("current_ram_used_gb", "REAL"), + ("current_ram_available_gb", "REAL"), + ("current_swap_total_gb", "REAL"), + ("current_swap_used_gb", "REAL"), + ("current_swap_free_gb", "REAL"), + ("current_swap_percent", "REAL"), + ] + with engine.connect() as conn: + for col, col_type in new_pc_cols: + if col not in pc_cols: + conn.execute(text(f"ALTER TABLE computers ADD COLUMN {col} {col_type}")) + conn.commit() diff --git a/server/models.py b/server/models.py index 52a4a0c..c749c32 100644 --- a/server/models.py +++ b/server/models.py @@ -28,6 +28,12 @@ class Computer(Base): current_cpu_percent = Column(Float, nullable=True) current_ram_percent = Column(Float, nullable=True) current_ram_total_gb = Column(Float, nullable=True) + current_ram_used_gb = Column(Float, nullable=True) + current_ram_available_gb = Column(Float, nullable=True) + current_swap_total_gb = Column(Float, nullable=True) + current_swap_used_gb = Column(Float, nullable=True) + current_swap_free_gb = Column(Float, nullable=True) + current_swap_percent = Column(Float, nullable=True) class Heartbeat(Base): @@ -43,6 +49,12 @@ class Heartbeat(Base): cpu_percent = Column(Float, nullable=True) ram_percent = Column(Float, nullable=True) ram_total_gb = Column(Float, nullable=True) + ram_used_gb = Column(Float, nullable=True) + ram_available_gb = Column(Float, nullable=True) + swap_total_gb = Column(Float, nullable=True) + swap_used_gb = Column(Float, nullable=True) + swap_free_gb = Column(Float, nullable=True) + swap_percent = Column(Float, nullable=True) disk_info = Column(JSON, nullable=True) cpu_temp = Column(Float, nullable=True) load_avg = Column(String, nullable=True) diff --git a/server/routes/api_routes.py b/server/routes/api_routes.py index 79b6a87..5471c25 100644 --- a/server/routes/api_routes.py +++ b/server/routes/api_routes.py @@ -68,6 +68,12 @@ def heartbeat( computer.current_cpu_percent = payload.cpu_percent computer.current_ram_percent = payload.ram_percent computer.current_ram_total_gb = payload.ram_total_gb + computer.current_ram_used_gb = payload.ram_used_gb + computer.current_ram_available_gb = payload.ram_available_gb + computer.current_swap_total_gb = payload.swap_total_gb + computer.current_swap_used_gb = payload.swap_used_gb + computer.current_swap_free_gb = payload.swap_free_gb + computer.current_swap_percent = payload.swap_percent sessions = payload.sessions if sessions: @@ -83,6 +89,12 @@ def heartbeat( cpu_percent=payload.cpu_percent, ram_percent=payload.ram_percent, ram_total_gb=payload.ram_total_gb, + ram_used_gb=payload.ram_used_gb, + ram_available_gb=payload.ram_available_gb, + swap_total_gb=payload.swap_total_gb, + swap_used_gb=payload.swap_used_gb, + swap_free_gb=payload.swap_free_gb, + swap_percent=payload.swap_percent, disk_info=payload.disk_info or [], cpu_temp=payload.cpu_temp, load_avg=payload.load_avg, @@ -202,7 +214,9 @@ def export_csv( writer = csv.writer(output) writer.writerow([ "ID", "Hostname", "User", "IP", "Status", "OS", "CPU %", "RAM %", - "RAM Total GB", "Uptime sec", "Last seen", "First seen", + "RAM Used GB", "RAM Total GB", "RAM Available GB", "Swap %", + "Swap Used GB", "Swap Total GB", "Swap Free GB", "Uptime sec", + "Last seen", "First seen", ]) for c in computers: writer.writerow([ @@ -214,7 +228,13 @@ def export_csv( c.os_info or "", c.current_cpu_percent or "", c.current_ram_percent or "", + c.current_ram_used_gb or "", c.current_ram_total_gb or "", + c.current_ram_available_gb or "", + c.current_swap_percent or "", + c.current_swap_used_gb or "", + c.current_swap_total_gb or "", + c.current_swap_free_gb or "", c.current_uptime_sec or "", c.last_seen.isoformat() if c.last_seen else "", c.first_seen.isoformat() if c.first_seen else "", diff --git a/server/routes/web_routes.py b/server/routes/web_routes.py index 73eac56..e65cfaf 100644 --- a/server/routes/web_routes.py +++ b/server/routes/web_routes.py @@ -127,12 +127,22 @@ def computer_page(computer_id: int, request: Request, db: Session = Depends(get_ "local_ip": h.local_ip, "cpu_percent": h.cpu_percent, "ram_percent": h.ram_percent, + "ram_total_gb": h.ram_total_gb, + "ram_used_gb": h.ram_used_gb, + "ram_available_gb": h.ram_available_gb, + "swap_total_gb": h.swap_total_gb, + "swap_used_gb": h.swap_used_gb, + "swap_free_gb": h.swap_free_gb, + "swap_percent": h.swap_percent, "cpu_temp": h.cpu_temp, "processes": h.processes or [], "sessions": h.sessions or [], }) - if not latest_sessions and h.sessions: + # Take the most recent heartbeat that actually has session data + for h in reversed(heartbeats): + if h.sessions: latest_sessions = h.sessions + break return templates.TemplateResponse( "computer.html", diff --git a/server/schemas.py b/server/schemas.py index 0b53fe2..b3e2fa9 100644 --- a/server/schemas.py +++ b/server/schemas.py @@ -63,6 +63,12 @@ class HeartbeatPayload(BaseModel): cpu_percent: float ram_percent: float ram_total_gb: Optional[float] = None + ram_used_gb: Optional[float] = None + ram_available_gb: Optional[float] = None + swap_total_gb: Optional[float] = None + swap_used_gb: Optional[float] = None + swap_free_gb: Optional[float] = None + swap_percent: Optional[float] = None disk_info: Optional[List[dict]] = None cpu_temp: Optional[float] = None load_avg: Optional[str] = None @@ -77,6 +83,13 @@ class HeartbeatOut(BaseModel): local_ip: Optional[str] cpu_percent: Optional[float] ram_percent: Optional[float] + ram_total_gb: Optional[float] + ram_used_gb: Optional[float] + ram_available_gb: Optional[float] + swap_total_gb: Optional[float] + swap_used_gb: Optional[float] + swap_free_gb: Optional[float] + swap_percent: Optional[float] cpu_temp: Optional[float] sessions: Optional[List[SessionInfo]] = None @@ -94,6 +107,13 @@ class ComputerOut(BaseModel): current_ip: Optional[str] current_cpu_percent: Optional[float] current_ram_percent: Optional[float] + current_ram_total_gb: Optional[float] + current_ram_used_gb: Optional[float] + current_ram_available_gb: Optional[float] + current_swap_total_gb: Optional[float] + current_swap_used_gb: Optional[float] + current_swap_free_gb: Optional[float] + current_swap_percent: Optional[float] model_config = ConfigDict(from_attributes=True) diff --git a/server/templates/computer.html b/server/templates/computer.html index 0508185..9b254b9 100644 --- a/server/templates/computer.html +++ b/server/templates/computer.html @@ -49,6 +49,45 @@ +
| Время | CPU | RAM | IP | ||
|---|---|---|---|---|---|
| Время | CPU | RAM | RAM GB | Swap | IP |
| {{ h.timestamp[:16] | replace('T', ' ') if h.timestamp else '-' }} | {{ h.cpu_percent | round(1) }}% | {{ h.ram_percent | round(1) }}% | ++ {% if h.ram_used_gb is not none and h.ram_total_gb is not none %} + {{ h.ram_used_gb | round(1) }} / {{ h.ram_total_gb | round(1) }} + {% else %} + - + {% endif %} + | +{{ h.swap_percent | round(1) if h.swap_percent is not none else '-' }}% | {{ h.local_ip or '-' }} | Статус | CPU | RAM | +Swap | OS | Последний чек | @@ -84,7 +85,14 @@ {% endif %} | {{ c.current_cpu_percent | round(1) if c.current_cpu_percent is not none else '-' }}% | -{{ c.current_ram_percent | round(1) if c.current_ram_percent is not none else '-' }}% | ++ {% if c.current_ram_used_gb is not none and c.current_ram_total_gb is not none %} + {{ c.current_ram_used_gb | round(1) }} / {{ c.current_ram_total_gb | round(1) }} GB + {% else %} + {{ c.current_ram_percent | round(1) if c.current_ram_percent is not none else '-' }}% + {% endif %} + | +{{ c.current_swap_percent | round(1) if c.current_swap_percent is not none else '-' }}% | {{ c.os_info or '-' }} | {{ c.last_seen | localtime }} | @@ -187,7 +195,8 @@ async function loadDashboard() { | ${escapeHtml(c.current_ip || '-')} | ${c.status === 'online' ? 'Online' : 'Offline'} | ${c.current_cpu_percent != null ? c.current_cpu_percent.toFixed(1) : '-'}% | -${c.current_ram_percent != null ? c.current_ram_percent.toFixed(1) : '-'}% | +${c.current_ram_used_gb != null && c.current_ram_total_gb != null ? `${c.current_ram_used_gb.toFixed(1)} / ${c.current_ram_total_gb.toFixed(1)} GB` : (c.current_ram_percent != null ? c.current_ram_percent.toFixed(1) + '%' : '-')} | +${c.current_swap_percent != null ? c.current_swap_percent.toFixed(1) : '-'}% | ${escapeHtml(c.os_info || '-')} | ${formatServerTime(c.last_seen)} |