Add RAM usage in GB and swap metrics to agent, server, UI and CSV export
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
Package: info-user-agent
|
Package: info-user-agent
|
||||||
Version: 1.0.1
|
Version: 1.0.3
|
||||||
Section: admin
|
Section: admin
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: all
|
Architecture: all
|
||||||
|
|||||||
@@ -147,15 +147,31 @@ def get_load_avg():
|
|||||||
return None
|
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 = []
|
procs = []
|
||||||
for p in psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]):
|
for p in processes:
|
||||||
try:
|
try:
|
||||||
procs.append({
|
procs.append({
|
||||||
"pid": p.info["pid"],
|
"pid": p.pid,
|
||||||
"name": p.info["name"],
|
"name": p.name(),
|
||||||
"cpu_percent": round(p.info["cpu_percent"] or 0, 1),
|
"cpu_percent": round(p.cpu_percent(interval=None), 1),
|
||||||
"ram_percent": round((p.info["memory_percent"] or 0), 1),
|
"ram_percent": round(p.memory_percent(), 1),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@@ -166,6 +182,21 @@ def get_top_processes(limit=10):
|
|||||||
def collect():
|
def collect():
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
sessions = get_active_sessions()
|
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 = {
|
payload = {
|
||||||
"hostname": socket.gethostname(),
|
"hostname": socket.gethostname(),
|
||||||
"username": get_display_username(sessions),
|
"username": get_display_username(sessions),
|
||||||
@@ -175,12 +206,18 @@ def collect():
|
|||||||
"public_ip": get_public_ip() if cfg.get("send_public_ip") else None,
|
"public_ip": get_public_ip() if cfg.get("send_public_ip") else None,
|
||||||
"uptime_sec": get_uptime(),
|
"uptime_sec": get_uptime(),
|
||||||
"cpu_percent": psutil.cpu_percent(interval=1),
|
"cpu_percent": psutil.cpu_percent(interval=1),
|
||||||
"ram_percent": psutil.virtual_memory().percent,
|
"ram_percent": vm.percent,
|
||||||
"ram_total_gb": round(psutil.virtual_memory().total / (2**30), 1),
|
"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(),
|
"disk_info": get_disk_info(),
|
||||||
"cpu_temp": get_cpu_temp(),
|
"cpu_temp": get_cpu_temp(),
|
||||||
"load_avg": get_load_avg(),
|
"load_avg": get_load_avg(),
|
||||||
"processes": get_top_processes(10),
|
"processes": get_top_processes(10, baseline_procs),
|
||||||
}
|
}
|
||||||
return payload, cfg
|
return payload, cfg
|
||||||
|
|
||||||
|
|||||||
+46
-9
@@ -147,15 +147,31 @@ def get_load_avg():
|
|||||||
return None
|
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 = []
|
procs = []
|
||||||
for p in psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]):
|
for p in processes:
|
||||||
try:
|
try:
|
||||||
procs.append({
|
procs.append({
|
||||||
"pid": p.info["pid"],
|
"pid": p.pid,
|
||||||
"name": p.info["name"],
|
"name": p.name(),
|
||||||
"cpu_percent": round(p.info["cpu_percent"] or 0, 1),
|
"cpu_percent": round(p.cpu_percent(interval=None), 1),
|
||||||
"ram_percent": round((p.info["memory_percent"] or 0), 1),
|
"ram_percent": round(p.memory_percent(), 1),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@@ -166,6 +182,21 @@ def get_top_processes(limit=10):
|
|||||||
def collect():
|
def collect():
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
sessions = get_active_sessions()
|
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 = {
|
payload = {
|
||||||
"hostname": socket.gethostname(),
|
"hostname": socket.gethostname(),
|
||||||
"username": get_display_username(sessions),
|
"username": get_display_username(sessions),
|
||||||
@@ -175,12 +206,18 @@ def collect():
|
|||||||
"public_ip": get_public_ip() if cfg.get("send_public_ip") else None,
|
"public_ip": get_public_ip() if cfg.get("send_public_ip") else None,
|
||||||
"uptime_sec": get_uptime(),
|
"uptime_sec": get_uptime(),
|
||||||
"cpu_percent": psutil.cpu_percent(interval=1),
|
"cpu_percent": psutil.cpu_percent(interval=1),
|
||||||
"ram_percent": psutil.virtual_memory().percent,
|
"ram_percent": vm.percent,
|
||||||
"ram_total_gb": round(psutil.virtual_memory().total / (2**30), 1),
|
"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(),
|
"disk_info": get_disk_info(),
|
||||||
"cpu_temp": get_cpu_temp(),
|
"cpu_temp": get_cpu_temp(),
|
||||||
"load_avg": get_load_avg(),
|
"load_avg": get_load_avg(),
|
||||||
"processes": get_top_processes(10),
|
"processes": get_top_processes(10, baseline_procs),
|
||||||
}
|
}
|
||||||
return payload, cfg
|
return payload, cfg
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+30
-3
@@ -39,10 +39,37 @@ def run_migrations():
|
|||||||
conn.execute(text("ALTER TABLE audit_log ADD COLUMN username TEXT"))
|
conn.execute(text("ALTER TABLE audit_log ADD COLUMN username TEXT"))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Heartbeat: add sessions column if missing
|
# Heartbeat: add missing columns
|
||||||
if "heartbeats" in insp.get_table_names():
|
if "heartbeats" in insp.get_table_names():
|
||||||
hb_cols = {c["name"] for c in insp.get_columns("heartbeats")}
|
hb_cols = {c["name"] for c in insp.get_columns("heartbeats")}
|
||||||
if "sessions" not in hb_cols:
|
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:
|
with engine.connect() as conn:
|
||||||
conn.execute(text("ALTER TABLE heartbeats ADD COLUMN sessions TEXT"))
|
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()
|
conn.commit()
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ class Computer(Base):
|
|||||||
current_cpu_percent = Column(Float, nullable=True)
|
current_cpu_percent = Column(Float, nullable=True)
|
||||||
current_ram_percent = Column(Float, nullable=True)
|
current_ram_percent = Column(Float, nullable=True)
|
||||||
current_ram_total_gb = 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):
|
class Heartbeat(Base):
|
||||||
@@ -43,6 +49,12 @@ class Heartbeat(Base):
|
|||||||
cpu_percent = Column(Float, nullable=True)
|
cpu_percent = Column(Float, nullable=True)
|
||||||
ram_percent = Column(Float, nullable=True)
|
ram_percent = Column(Float, nullable=True)
|
||||||
ram_total_gb = 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)
|
disk_info = Column(JSON, nullable=True)
|
||||||
cpu_temp = Column(Float, nullable=True)
|
cpu_temp = Column(Float, nullable=True)
|
||||||
load_avg = Column(String, nullable=True)
|
load_avg = Column(String, nullable=True)
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ def heartbeat(
|
|||||||
computer.current_cpu_percent = payload.cpu_percent
|
computer.current_cpu_percent = payload.cpu_percent
|
||||||
computer.current_ram_percent = payload.ram_percent
|
computer.current_ram_percent = payload.ram_percent
|
||||||
computer.current_ram_total_gb = payload.ram_total_gb
|
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
|
sessions = payload.sessions
|
||||||
if sessions:
|
if sessions:
|
||||||
@@ -83,6 +89,12 @@ def heartbeat(
|
|||||||
cpu_percent=payload.cpu_percent,
|
cpu_percent=payload.cpu_percent,
|
||||||
ram_percent=payload.ram_percent,
|
ram_percent=payload.ram_percent,
|
||||||
ram_total_gb=payload.ram_total_gb,
|
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 [],
|
disk_info=payload.disk_info or [],
|
||||||
cpu_temp=payload.cpu_temp,
|
cpu_temp=payload.cpu_temp,
|
||||||
load_avg=payload.load_avg,
|
load_avg=payload.load_avg,
|
||||||
@@ -202,7 +214,9 @@ def export_csv(
|
|||||||
writer = csv.writer(output)
|
writer = csv.writer(output)
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
"ID", "Hostname", "User", "IP", "Status", "OS", "CPU %", "RAM %",
|
"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:
|
for c in computers:
|
||||||
writer.writerow([
|
writer.writerow([
|
||||||
@@ -214,7 +228,13 @@ def export_csv(
|
|||||||
c.os_info or "",
|
c.os_info or "",
|
||||||
c.current_cpu_percent or "",
|
c.current_cpu_percent or "",
|
||||||
c.current_ram_percent or "",
|
c.current_ram_percent or "",
|
||||||
|
c.current_ram_used_gb or "",
|
||||||
c.current_ram_total_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.current_uptime_sec or "",
|
||||||
c.last_seen.isoformat() if c.last_seen else "",
|
c.last_seen.isoformat() if c.last_seen else "",
|
||||||
c.first_seen.isoformat() if c.first_seen else "",
|
c.first_seen.isoformat() if c.first_seen else "",
|
||||||
|
|||||||
@@ -127,12 +127,22 @@ def computer_page(computer_id: int, request: Request, db: Session = Depends(get_
|
|||||||
"local_ip": h.local_ip,
|
"local_ip": h.local_ip,
|
||||||
"cpu_percent": h.cpu_percent,
|
"cpu_percent": h.cpu_percent,
|
||||||
"ram_percent": h.ram_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,
|
"cpu_temp": h.cpu_temp,
|
||||||
"processes": h.processes or [],
|
"processes": h.processes or [],
|
||||||
"sessions": h.sessions 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
|
latest_sessions = h.sessions
|
||||||
|
break
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"computer.html",
|
"computer.html",
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ class HeartbeatPayload(BaseModel):
|
|||||||
cpu_percent: float
|
cpu_percent: float
|
||||||
ram_percent: float
|
ram_percent: float
|
||||||
ram_total_gb: Optional[float] = None
|
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
|
disk_info: Optional[List[dict]] = None
|
||||||
cpu_temp: Optional[float] = None
|
cpu_temp: Optional[float] = None
|
||||||
load_avg: Optional[str] = None
|
load_avg: Optional[str] = None
|
||||||
@@ -77,6 +83,13 @@ class HeartbeatOut(BaseModel):
|
|||||||
local_ip: Optional[str]
|
local_ip: Optional[str]
|
||||||
cpu_percent: Optional[float]
|
cpu_percent: Optional[float]
|
||||||
ram_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]
|
cpu_temp: Optional[float]
|
||||||
sessions: Optional[List[SessionInfo]] = None
|
sessions: Optional[List[SessionInfo]] = None
|
||||||
|
|
||||||
@@ -94,6 +107,13 @@ class ComputerOut(BaseModel):
|
|||||||
current_ip: Optional[str]
|
current_ip: Optional[str]
|
||||||
current_cpu_percent: Optional[float]
|
current_cpu_percent: Optional[float]
|
||||||
current_ram_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)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card text-center shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-muted small">RAM</div>
|
||||||
|
<div class="fw-bold">
|
||||||
|
{% if computer.current_ram_used_gb is not none and computer.current_ram_total_gb is not none %}
|
||||||
|
{{ computer.current_ram_used_gb | round(1) }} / {{ computer.current_ram_total_gb | round(1) }} GB
|
||||||
|
<span class="text-muted small">({{ computer.current_ram_percent | round(1) if computer.current_ram_percent is not none else '-' }}%)</span>
|
||||||
|
{% else %}
|
||||||
|
{{ computer.current_ram_percent | round(1) if computer.current_ram_percent is not none else '-' }}%
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if computer.current_ram_available_gb is not none %}
|
||||||
|
<div class="text-muted small">доступно {{ computer.current_ram_available_gb | round(1) }} GB</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card text-center shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="text-muted small">Swap</div>
|
||||||
|
<div class="fw-bold">
|
||||||
|
{% if computer.current_swap_used_gb is not none and computer.current_swap_total_gb is not none %}
|
||||||
|
{{ computer.current_swap_used_gb | round(1) }} / {{ computer.current_swap_total_gb | round(1) }} GB
|
||||||
|
<span class="text-muted small">({{ computer.current_swap_percent | round(1) if computer.current_swap_percent is not none else '-' }}%)</span>
|
||||||
|
{% else %}
|
||||||
|
{{ computer.current_swap_percent | round(1) if computer.current_swap_percent is not none else '-' }}%
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if computer.current_swap_free_gb is not none %}
|
||||||
|
<div class="text-muted small">свободно {{ computer.current_swap_free_gb | round(1) }} GB</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Активные сессии</h5>
|
<h5 class="card-title">Активные сессии</h5>
|
||||||
@@ -75,7 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4 mb-4">
|
<div class="row g-4 mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">CPU, %</h5>
|
<h5 class="card-title">CPU, %</h5>
|
||||||
@@ -83,7 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">RAM, %</h5>
|
<h5 class="card-title">RAM, %</h5>
|
||||||
@@ -91,6 +130,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Swap, %</h5>
|
||||||
|
<canvas id="swapChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
@@ -120,13 +167,21 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">История heartbeats</h5>
|
<h5 class="card-title">История heartbeats</h5>
|
||||||
<table class="table table-sm">
|
<table class="table table-sm">
|
||||||
<thead><tr><th>Время</th><th>CPU</th><th>RAM</th><th>IP</th></tr></thead>
|
<thead><tr><th>Время</th><th>CPU</th><th>RAM</th><th>RAM GB</th><th>Swap</th><th>IP</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for h in heartbeats[-20:] | reverse %}
|
{% for h in heartbeats[-20:] | reverse %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ h.timestamp[:16] | replace('T', ' ') if h.timestamp else '-' }}</td>
|
<td>{{ h.timestamp[:16] | replace('T', ' ') if h.timestamp else '-' }}</td>
|
||||||
<td>{{ h.cpu_percent | round(1) }}%</td>
|
<td>{{ h.cpu_percent | round(1) }}%</td>
|
||||||
<td>{{ h.ram_percent | round(1) }}%</td>
|
<td>{{ h.ram_percent | round(1) }}%</td>
|
||||||
|
<td>
|
||||||
|
{% 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 %}
|
||||||
|
</td>
|
||||||
|
<td>{{ h.swap_percent | round(1) if h.swap_percent is not none else '-' }}%</td>
|
||||||
<td>{{ h.local_ip or '-' }}</td>
|
<td>{{ h.local_ip or '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -135,22 +190,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
const heartbeats = {{ heartbeats | tojson }};
|
const heartbeats = {{ heartbeats | tojson }};
|
||||||
const labels = heartbeats.map(h => h.timestamp.replace('T', ' ').slice(0, 16));
|
const labels = heartbeats.map(h => h.timestamp.replace('T', ' ').slice(0, 16));
|
||||||
const cpuData = heartbeats.map(h => h.cpu_percent);
|
const cpuData = heartbeats.map(h => h.cpu_percent ?? null);
|
||||||
const ramData = heartbeats.map(h => h.ram_percent);
|
const ramData = heartbeats.map(h => h.ram_percent ?? null);
|
||||||
|
const swapData = heartbeats.map(h => h.swap_percent ?? null);
|
||||||
|
|
||||||
new Chart(document.getElementById('cpuChart'), {
|
new Chart(document.getElementById('cpuChart'), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: { labels, datasets: [{ label: 'CPU %', data: cpuData, borderColor: 'rgb(255, 99, 132)', tension: 0.2 }] },
|
data: { labels, datasets: [{ label: 'CPU %', data: cpuData, borderColor: 'rgb(255, 99, 132)', tension: 0.2 }] },
|
||||||
options: { scales: { y: { beginAtZero: true, max: 100 } } }
|
options: { scales: { y: { beginAtZero: true, max: 100 } }, spanGaps: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
new Chart(document.getElementById('ramChart'), {
|
new Chart(document.getElementById('ramChart'), {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: { labels, datasets: [{ label: 'RAM %', data: ramData, borderColor: 'rgb(54, 162, 235)', tension: 0.2 }] },
|
data: { labels, datasets: [{ label: 'RAM %', data: ramData, borderColor: 'rgb(54, 162, 235)', tension: 0.2 }] },
|
||||||
options: { scales: { y: { beginAtZero: true, max: 100 } } }
|
options: { scales: { y: { beginAtZero: true, max: 100 } }, spanGaps: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
new Chart(document.getElementById('swapChart'), {
|
||||||
|
type: 'line',
|
||||||
|
data: { labels, datasets: [{ label: 'Swap %', data: swapData, borderColor: 'rgb(255, 159, 64)', tension: 0.2 }] },
|
||||||
|
options: { scales: { y: { beginAtZero: true, max: 100 } }, spanGaps: true }
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
<th>Статус</th>
|
<th>Статус</th>
|
||||||
<th>CPU</th>
|
<th>CPU</th>
|
||||||
<th>RAM</th>
|
<th>RAM</th>
|
||||||
|
<th>Swap</th>
|
||||||
<th>OS</th>
|
<th>OS</th>
|
||||||
<th>Последний чек</th>
|
<th>Последний чек</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -84,7 +85,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ c.current_cpu_percent | round(1) if c.current_cpu_percent is not none else '-' }}%</td>
|
<td>{{ c.current_cpu_percent | round(1) if c.current_cpu_percent is not none else '-' }}%</td>
|
||||||
<td>{{ c.current_ram_percent | round(1) if c.current_ram_percent is not none else '-' }}%</td>
|
<td>
|
||||||
|
{% 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 %}
|
||||||
|
</td>
|
||||||
|
<td>{{ c.current_swap_percent | round(1) if c.current_swap_percent is not none else '-' }}%</td>
|
||||||
<td>{{ c.os_info or '-' }}</td>
|
<td>{{ c.os_info or '-' }}</td>
|
||||||
<td class="last-seen">{{ c.last_seen | localtime }}</td>
|
<td class="last-seen">{{ c.last_seen | localtime }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -187,7 +195,8 @@ async function loadDashboard() {
|
|||||||
<td>${escapeHtml(c.current_ip || '-')}</td>
|
<td>${escapeHtml(c.current_ip || '-')}</td>
|
||||||
<td>${c.status === 'online' ? '<span class="badge bg-success">Online</span>' : '<span class="badge bg-secondary">Offline</span>'}</td>
|
<td>${c.status === 'online' ? '<span class="badge bg-success">Online</span>' : '<span class="badge bg-secondary">Offline</span>'}</td>
|
||||||
<td>${c.current_cpu_percent != null ? c.current_cpu_percent.toFixed(1) : '-'}%</td>
|
<td>${c.current_cpu_percent != null ? c.current_cpu_percent.toFixed(1) : '-'}%</td>
|
||||||
<td>${c.current_ram_percent != null ? c.current_ram_percent.toFixed(1) : '-'}%</td>
|
<td>${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) + '%' : '-')}</td>
|
||||||
|
<td>${c.current_swap_percent != null ? c.current_swap_percent.toFixed(1) : '-'}%</td>
|
||||||
<td>${escapeHtml(c.os_info || '-')}</td>
|
<td>${escapeHtml(c.os_info || '-')}</td>
|
||||||
<td class="last-seen">${formatServerTime(c.last_seen)}</td>
|
<td class="last-seen">${formatServerTime(c.last_seen)}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
Reference in New Issue
Block a user