Feat: update agent version to 1.2.0 and add systemd services listing in VpsCard component
All checks were successful
Build and Push Docker Images / docker (push) Successful in 25s

This commit is contained in:
jeanotx32
2026-06-02 19:59:57 -04:00
parent 57132f92ee
commit b2b660e035
5 changed files with 94 additions and 5 deletions

View File

@@ -1,2 +1,2 @@
80001
80063
83491
83555

View File

@@ -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__":

View File

@@ -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,

View File

@@ -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
<p className="px-4 py-2 text-xs text-gray-500 border-b border-gray-800/60">{vps.description}</p>
)}
{/* Services systemd */}
{!collapsed && vps.online && vps.services?.length > 0 && (
<div className="border-b border-gray-800/60">
<button
onClick={() => setServicesExpanded(e => !e)}
className="w-full flex items-center justify-between px-4 py-2 text-xs text-gray-400 hover:bg-gray-800/40 transition-colors"
>
<span className="flex items-center gap-1.5">
<Activity size={11} className="text-indigo-400" />
Services systemd
<span className="ml-1 px-1.5 py-0.5 rounded-full bg-gray-800 text-gray-500 text-[10px]">
{vps.services.filter(s => s.active === 'active').length}/{vps.services.length}
</span>
</span>
{servicesExpanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</button>
{servicesExpanded && (
<div className="divide-y divide-gray-800/40 max-h-64 overflow-y-auto">
{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 (
<div key={svc.name} className="flex items-center gap-2.5 px-4 py-1.5 hover:bg-gray-800/30">
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${dotColor}`} />
<span className={`text-xs font-mono truncate flex-1 ${nameColor}`}>{svc.name}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded flex-shrink-0 ${
isActive ? 'bg-emerald-500/10 text-emerald-400' :
isFailed ? 'bg-red-500/10 text-red-400' :
'bg-gray-700/40 text-gray-500'
}`}>{svc.sub}</span>
</div>
)
})}
</div>
)}
</div>
)}
{/* Conteneurs */}
{!collapsed && vps.online && (
<div className="divide-y divide-gray-800/50 flex-1">