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
All checks were successful
Build and Push Docker Images / docker (push) Successful in 25s
This commit is contained in:
@@ -1,2 +1,2 @@
|
|||||||
80001
|
83491
|
||||||
80063
|
83555
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from fastapi.security import APIKeyHeader
|
|||||||
|
|
||||||
# ─── Config ───────────────────────────────────────────────────────────────────
|
# ─── 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")
|
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")
|
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}
|
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 ───────────────────────────────────────────────────────────────────
|
# ─── Entrée ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Binary file not shown.
@@ -47,7 +47,7 @@ from webauthn.helpers.structs import (
|
|||||||
# ─── Config ───────────────────────────────────────────────────────────────────
|
# ─── Config ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
DB_FILE = Path(os.getenv("DB_FILE", "data/monitor.db"))
|
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) ───────────────────────────────────────
|
# ─── Ring buffer de stats (en mémoire) ───────────────────────────────────────
|
||||||
_STATS_MAX_POINTS = 120 # 10 min à 5 s d'intervalle
|
_STATS_MAX_POINTS = 120 # 10 min à 5 s d'intervalle
|
||||||
@@ -498,6 +498,12 @@ async def fetch_vps_status(vps: dict) -> dict:
|
|||||||
try:
|
try:
|
||||||
containers_res = await agent_get(vps, "/containers")
|
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
|
# Source unique : ring buffer → carte et modal affichent exactement la même valeur
|
||||||
history = _stats_history.get(vps["id"])
|
history = _stats_history.get(vps["id"])
|
||||||
if history:
|
if history:
|
||||||
@@ -531,6 +537,7 @@ async def fetch_vps_status(vps: dict) -> dict:
|
|||||||
"description": vps.get("description", ""),
|
"description": vps.get("description", ""),
|
||||||
"online": True,
|
"online": True,
|
||||||
"containers": containers_res,
|
"containers": containers_res,
|
||||||
|
"services": services_res,
|
||||||
"system": system,
|
"system": system,
|
||||||
"tags": vps.get("tags", []),
|
"tags": vps.get("tags", []),
|
||||||
"agent_version": agent_version,
|
"agent_version": agent_version,
|
||||||
@@ -546,6 +553,7 @@ async def fetch_vps_status(vps: dict) -> dict:
|
|||||||
"online": False,
|
"online": False,
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
"containers": [],
|
"containers": [],
|
||||||
|
"services": [],
|
||||||
"system": None,
|
"system": None,
|
||||||
"tags": vps.get("tags", []),
|
"tags": vps.get("tags", []),
|
||||||
"agent_version": None,
|
"agent_version": None,
|
||||||
|
|||||||
@@ -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 { useState } from 'react'
|
||||||
import ContainerRow from './ContainerRow'
|
import ContainerRow from './ContainerRow'
|
||||||
import { tagColor } from './TagInput'
|
import { tagColor } from './TagInput'
|
||||||
@@ -19,6 +19,7 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onE
|
|||||||
const [updatingProject, setUpdatingProject] = useState(null)
|
const [updatingProject, setUpdatingProject] = useState(null)
|
||||||
const [updatingAgent, setUpdatingAgent] = useState(false)
|
const [updatingAgent, setUpdatingAgent] = useState(false)
|
||||||
const [exported, setExported] = useState(false)
|
const [exported, setExported] = useState(false)
|
||||||
|
const [servicesExpanded, setServicesExpanded] = useState(false)
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
await onExport(vps.id)
|
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>
|
<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 */}
|
{/* Conteneurs */}
|
||||||
{!collapsed && vps.online && (
|
{!collapsed && vps.online && (
|
||||||
<div className="divide-y divide-gray-800/50 flex-1">
|
<div className="divide-y divide-gray-800/50 flex-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user