From b2b660e03509a788e9709874aee58be949327c06 Mon Sep 17 00:00:00 2001 From: jeanotx32 Date: Tue, 2 Jun 2026 19:59:57 -0400 Subject: [PATCH] Feat: update agent version to 1.2.0 and add systemd services listing in VpsCard component --- vps-monitor/.pids | 4 +- vps-monitor/agent/agent.py | 42 ++++++++++++++++- .../backend/__pycache__/main.cpython-313.pyc | Bin 65572 -> 65805 bytes vps-monitor/backend/main.py | 10 +++- .../frontend/src/components/VpsCard.jsx | 43 +++++++++++++++++- 5 files changed, 94 insertions(+), 5 deletions(-) diff --git a/vps-monitor/.pids b/vps-monitor/.pids index c191dd7..f2b7530 100644 --- a/vps-monitor/.pids +++ b/vps-monitor/.pids @@ -1,2 +1,2 @@ -80001 -80063 +83491 +83555 diff --git a/vps-monitor/agent/agent.py b/vps-monitor/agent/agent.py index 0a3cbc9..a07a53a 100644 --- a/vps-monitor/agent/agent.py +++ b/vps-monitor/agent/agent.py @@ -19,7 +19,7 @@ from fastapi.security import APIKeyHeader # ─── Config ─────────────────────────────────────────────────────────────────── -AGENT_VERSION = "1.1.0" +AGENT_VERSION = "1.2.0" REPO_BASE = os.getenv("AGENT_REPO_BASE", "https://git.jeanbonapp.com/jeanbon/ScriptVPS/raw/branch/main") INSTALL_DIR = os.getenv("AGENT_INSTALL_DIR", "/opt/vps-monitor-agent") @@ -219,6 +219,46 @@ def compose_update(project: str, _: None = Depends(require_api_key)): return {"output": output, "project": project, "working_dir": working_dir} +@app.get("/services") +def list_services(_: None = Depends(require_api_key)): + """Retourne la liste des services systemd (hors Docker) avec leur état.""" + try: + result = subprocess.run( + ["systemctl", "list-units", "--type=service", "--no-legend", "--no-pager", "--all"], + capture_output=True, text=True, timeout=10, + ) + except FileNotFoundError: + raise HTTPException(status_code=501, detail="systemctl introuvable — système non-systemd") + except subprocess.TimeoutExpired: + raise HTTPException(status_code=504, detail="systemctl a expiré") + + _DOCKER_SERVICES = {"docker.service", "containerd.service", "docker.socket"} + + services = [] + for line in result.stdout.strip().splitlines(): + # Supprime les puces (● ○) et les espaces de début + line = line.lstrip("●○").strip() + if not line: + continue + parts = line.split(None, 4) + if len(parts) < 4: + continue + name = parts[0] + if not name.endswith(".service"): + continue + if name.lower() in _DOCKER_SERVICES or name.lower().startswith("docker"): + continue + services.append({ + "name": name, + "load": parts[1], + "active": parts[2], + "sub": parts[3], + "description": parts[4].strip() if len(parts) > 4 else "", + }) + + return sorted(services, key=lambda s: s["name"]) + + # ─── Entrée ─────────────────────────────────────────────────────────────────── if __name__ == "__main__": diff --git a/vps-monitor/backend/__pycache__/main.cpython-313.pyc b/vps-monitor/backend/__pycache__/main.cpython-313.pyc index af6dd0842e6ca23f07dc4410021f81331afe9a6a..d7fe43533c38dbbf529bc9ae46b5d6901564f0de 100644 GIT binary patch delta 3767 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 delta 3635 zcmaJ@2~Dx@5%DNFo_vKyaKPcSfxeC5kQP zM5=JrYF$A~=&9JqGd3<93rM0Sda$-yC)OPBXp7o{(X`g2rvLwji8hkVIrH7`{rBJQ zz5iWm)`~af0CZ0D3kT_JYRp1R>A-NNuJHIJ}>6+9LG!( zA}Vt^vmil7Dgl6>ozLYHJ(RP=a9l2L7O~$h}(|mNV;8AvFGGab|uo+hf%ck&P}M#pg<_e5yX)>~9Y6s@P?3de8={&N*`s zwCv6Fu%MylV6UQCpG$H@mYg118Ir_#rMYsRk0%yHeu}5hgzO9^-#E^Po81n7M+}!I zdo?wj1tLU4c6Kp}n_W@NWeQPTF#$dMpn3sz8T1x=^u`vG?9zq>*6%Rno2() zzHCvMk=2&HNObIK+33`{5R|?G%S6ox^8t)9or*jjyP6Q{5Jn&@05JNn$x950n#H9~ zmxHc!i#ChXMdyKu+jqH@I!hh)d}?*7UfsmXvWkM)o+U#^Vlmu-zjG%*FZVXr@1G`V zuSF#`1vf3d6qRu$JY$3Z1DV}j(#j6nq5?y8f!cmfH$;1HA{UW)FP432OVLDgx{&*v zKO4LB1y-~)yfWM`Q2Qn4H$?kYzu+%vzjii5hU-X9Bv~IY9m<7d8^kSHBG9(*fb0ma zEs@i~7WCQSaeHH=Lju3RvVNiJ(0o9HOUA9r^NZ_#-TmUx; zz^8SLAz;^G(q(4~9qDR!20@;Q-KRNiPPlMdn2L40w{_h>+9R*TD;@3J_TP$w&0 zIbK=`a*J|cu02wbNZ7-l~{4Bok>b(EZAWcmB-FKS$+i zR=BZBu0^_&-Q74$u0zULKurjw7GE<#`ZWle6idxdc+$v@Z%UPZ1A3IB10Dv6<2M0$B z*^R9)jVwX22EQL8EQU|FSm1Ejj2haEeTNVl+5Byjq$5DDC|kC5k$^owjYOR&BM;rq z?IWe5Ap4iHcl%ME2yDSlM_e14wgb#I3iOxg9(NpF4*-Hj>^;SLcgD*dK)>Oce%Auo zW22Ps8ZFAFQKO#K@0&XOW27G;oB>#+o1XJ>&itI{8L!OwaWw5@-|VXi+7BH@-mNLJ z+N>^X5p7_*>kGUSBsD~boI~?%5Zo=e3T#xtT;5{o4T(}0^ceP+G>Eb~%av?tJH3!8punRFjCij^aW zHVQMp*THp6ri%EJ2bhDDIOcSHNH(Wic|DDzT-V)f0^fm$>X7sMYe>jwob1dXoX5eF# zG+=6;4Z=<|N3ge!z5YTKcBtFoAIR<@s5SOaWPJ$z2>Z}>E3(Ig`5iY&1n$fxW$N*V zeD)#`!)Vo!&q6%``k*xbiX5*gotZ{r8GqIU zC6#j4r1umyRR@JT7iMWQbDkXywtLTpOM0-4Vy$OyPQ)x81uU3rjxM#ksAJi3OM%V$ z^o&48uv?Gd*mUTKr|%f5uOy#Ev{~F2KuhR*|jV6m)rOug~+j>m_Ln5@LWD9 zDgZDB(9Otyi$*wTdKf=j;6{l}7_7v6JaaMv!>I#IWDl=qNTcA}tW0zl@{#J5^d@@A zLwFUO!V;^xlzn(@j5HQ>tCiks$H=pz>bO39e4Pkp(HI~I9L%QwKR)oKUOi7TR>Q=w z(9efSDPXlpnfQ6yAoE<;9c$P#sB3nvdtS)%7;qedn(9HEi-mB1#c3yka61?}DGTZ! zuWadAIEeRvn{ktx2lb9F)onKc*s6deOQ8#1O;bEgCQSpWQNs@Rg|Mod22UDK%IDiO z^sN2nP;DkCzlW);f>hF#uv>;fDr3&=Si{LdQ(@J&=aMw`m)o-;=B~-+5Pfc70y$i%+^ZsBbV=hq+3ZY|^Z@KY4Z>YZ9*h4&nYZXw{c zM)9(tc&Jl6hAE!46i-Ep2OPyijpFG<@o1rVL{QxD6!#LvO+vApDV86_g{QbgbOx(` mFie{d6ufiJbmh#0ZDQ~gqTMI&DA7IlzQ*r%ghvt7fntM|K diff --git a/vps-monitor/backend/main.py b/vps-monitor/backend/main.py index e6118dc..4d198f3 100644 --- a/vps-monitor/backend/main.py +++ b/vps-monitor/backend/main.py @@ -47,7 +47,7 @@ from webauthn.helpers.structs import ( # ─── Config ─────────────────────────────────────────────────────────────────── DB_FILE = Path(os.getenv("DB_FILE", "data/monitor.db")) -EXPECTED_AGENT_VERSION = os.getenv("EXPECTED_AGENT_VERSION", "1.1.0") +EXPECTED_AGENT_VERSION = os.getenv("EXPECTED_AGENT_VERSION", "1.2.0") # ─── Ring buffer de stats (en mémoire) ─────────────────────────────────────── _STATS_MAX_POINTS = 120 # 10 min à 5 s d'intervalle @@ -498,6 +498,12 @@ async def fetch_vps_status(vps: dict) -> dict: try: containers_res = await agent_get(vps, "/containers") + # Services systemd (non-Docker) — optionnel, agent >= 1.2.0 + try: + services_res = await agent_get(vps, "/services") + except Exception: + services_res = [] + # Source unique : ring buffer → carte et modal affichent exactement la même valeur history = _stats_history.get(vps["id"]) if history: @@ -531,6 +537,7 @@ async def fetch_vps_status(vps: dict) -> dict: "description": vps.get("description", ""), "online": True, "containers": containers_res, + "services": services_res, "system": system, "tags": vps.get("tags", []), "agent_version": agent_version, @@ -546,6 +553,7 @@ async def fetch_vps_status(vps: dict) -> dict: "online": False, "error": str(e), "containers": [], + "services": [], "system": None, "tags": vps.get("tags", []), "agent_version": None, diff --git a/vps-monitor/frontend/src/components/VpsCard.jsx b/vps-monitor/frontend/src/components/VpsCard.jsx index 6b149b0..2d4c3e1 100644 --- a/vps-monitor/frontend/src/components/VpsCard.jsx +++ b/vps-monitor/frontend/src/components/VpsCard.jsx @@ -1,4 +1,4 @@ -import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp, RefreshCw, Cpu, MemoryStick, ArrowUp, ArrowDown, Pencil, BarChart2, CloudDownload, Copy, Check } from 'lucide-react' +import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp, RefreshCw, Cpu, MemoryStick, ArrowUp, ArrowDown, Pencil, BarChart2, CloudDownload, Copy, Check, Activity } from 'lucide-react' import { useState } from 'react' import ContainerRow from './ContainerRow' import { tagColor } from './TagInput' @@ -19,6 +19,7 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onE const [updatingProject, setUpdatingProject] = useState(null) const [updatingAgent, setUpdatingAgent] = useState(false) const [exported, setExported] = useState(false) + const [servicesExpanded, setServicesExpanded] = useState(false) const handleExport = async () => { await onExport(vps.id) @@ -208,6 +209,46 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onE

{vps.description}

)} + {/* Services systemd */} + {!collapsed && vps.online && vps.services?.length > 0 && ( +
+ + {servicesExpanded && ( +
+ {vps.services.map(svc => { + const isActive = svc.active === 'active' + const isFailed = svc.active === 'failed' + const dotColor = isActive ? 'bg-emerald-400' : isFailed ? 'bg-red-400' : 'bg-gray-500' + const nameColor = isActive ? 'text-gray-200' : isFailed ? 'text-red-400' : 'text-gray-500' + return ( +
+ + {svc.name} + {svc.sub} +
+ ) + })} +
+ )} +
+ )} + {/* Conteneurs */} {!collapsed && vps.online && (