From f2e5a24b3719811c999507ea9750aa6be3acfae1 Mon Sep 17 00:00:00 2001 From: jeanotx32 Date: Tue, 2 Jun 2026 18:55:11 -0400 Subject: [PATCH] Feat : Lot of stuff --- vps-monitor/.pids | 4 +- vps-monitor/agent/agent.py | 45 ++ .../backend/__pycache__/main.cpython-313.pyc | Bin 42692 -> 47740 bytes vps-monitor/backend/main.py | 140 ++++- vps-monitor/docker-compose.yml | 5 +- vps-monitor/frontend/src/App.jsx | 18 +- vps-monitor/frontend/src/api/client.js | 27 + .../frontend/src/components/AdminPage.jsx | 523 +++++++++++++----- .../frontend/src/components/VpsCard.jsx | 44 +- 9 files changed, 655 insertions(+), 151 deletions(-) diff --git a/vps-monitor/.pids b/vps-monitor/.pids index 4e28a64..c191dd7 100644 --- a/vps-monitor/.pids +++ b/vps-monitor/.pids @@ -1,2 +1,2 @@ -8423 -8473 +80001 +80063 diff --git a/vps-monitor/agent/agent.py b/vps-monitor/agent/agent.py index 1662abd..0a3cbc9 100644 --- a/vps-monitor/agent/agent.py +++ b/vps-monitor/agent/agent.py @@ -6,6 +6,7 @@ Expose une API REST utilisée par le backend central pour interroger les contene import os import subprocess +import threading import time from datetime import datetime, timezone @@ -18,6 +19,11 @@ from fastapi.security import APIKeyHeader # ─── Config ─────────────────────────────────────────────────────────────────── +AGENT_VERSION = "1.1.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") + API_KEY = os.getenv("AGENT_API_KEY", "changeme-please") AGENT_PORT = int(os.getenv("AGENT_PORT", "8001")) @@ -55,6 +61,45 @@ def health(): return {"status": "ok", "timestamp": datetime.now(timezone.utc).isoformat()} +@app.get("/version") +def get_version(): + """Retourne la version de l'agent — sans authentification.""" + return {"version": AGENT_VERSION} + + +@app.post("/self-update") +def self_update(_: None = Depends(require_api_key)): + """Télécharge la dernière version de l'agent depuis le dépôt et redémarre le service.""" + + def _do_update(): + time.sleep(0.5) # laisse la réponse HTTP partir + try: + for filename in ("agent.py", "requirements.txt"): + src = f"{REPO_BASE}/vps-monitor/agent/{filename}" + dst = f"{INSTALL_DIR}/{filename}" + subprocess.run( + ["curl", "-fsSL", src, "-o", dst], + timeout=60, + check=True, + ) + subprocess.run( + [f"{INSTALL_DIR}/venv/bin/pip", "install", "-r", + f"{INSTALL_DIR}/requirements.txt", "-q"], + timeout=120, + check=True, + ) + subprocess.run( + ["systemctl", "restart", "vps-monitor-agent"], + timeout=30, + check=True, + ) + except Exception: + pass + + threading.Thread(target=_do_update, daemon=True).start() + return {"status": "update_started"} + + @app.get("/containers") def list_containers(_: None = Depends(require_api_key)): """Retourne tous les conteneurs (actifs et arrêtés).""" diff --git a/vps-monitor/backend/__pycache__/main.cpython-313.pyc b/vps-monitor/backend/__pycache__/main.cpython-313.pyc index 327310135d2651ffb61d1b666b65a4429ffaf553..81e3f2e40fe6c412f50ece2cbc374d4702c1ba4c 100644 GIT binary patch delta 13535 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 dict: except Exception: system = None + # Version de l'agent (endpoint sans auth) + try: + version_res = await agent_get(vps, "/version") + agent_version = version_res.get("version", "unknown") + except Exception: + agent_version = "unknown" + return { - "id": vps["id"], - "name": vps["name"], - "host": vps["host"], - "description": vps.get("description", ""), - "online": True, - "containers": containers_res, - "system": system, - "tags": vps.get("tags", []), + "id": vps["id"], + "name": vps["name"], + "host": vps["host"], + "description": vps.get("description", ""), + "online": True, + "containers": containers_res, + "system": system, + "tags": vps.get("tags", []), + "agent_version": agent_version, + "expected_agent_version": EXPECTED_AGENT_VERSION, + "agent_up_to_date": agent_version == EXPECTED_AGENT_VERSION, } except Exception as e: return { - "id": vps["id"], - "name": vps["name"], - "host": vps["host"], - "description": vps.get("description", ""), - "online": False, - "error": str(e), - "containers": [], - "system": None, - "tags": vps.get("tags", []), + "id": vps["id"], + "name": vps["name"], + "host": vps["host"], + "description": vps.get("description", ""), + "online": False, + "error": str(e), + "containers": [], + "system": None, + "tags": vps.get("tags", []), + "agent_version": None, + "expected_agent_version": EXPECTED_AGENT_VERSION, + "agent_up_to_date": False, } @@ -591,6 +612,74 @@ def admin_list_users(_: Annotated[dict, Depends(require_admin)]): ] +@app.get("/api/admin/db/info") +def admin_db_info(_: Annotated[dict, Depends(require_admin)]): + """Retourne des informations sur le contenu de la base de données.""" + with get_db() as conn: + def _table_info(table: str) -> dict: + row = conn.execute( + f"SELECT COUNT(*) AS cnt, MIN(ts) AS oldest, MAX(ts) AS newest FROM {table}" + ).fetchone() + return { + "count": row["cnt"], + "oldest_ts": row["oldest"], + "newest_ts": row["newest"], + } + return { + "vps_stats": _table_info("vps_stats"), + "login_logs": _table_info("login_logs"), + } + + +_ALLOWED_TABLES = frozenset({"vps_stats", "login_logs"}) +_ALLOWED_PERIODS = frozenset({"last_24h", "last_7d", "last_30d", "all", "custom"}) + + +@app.delete("/api/admin/db/purge") +def admin_db_purge(body: PurgeRequest, _: Annotated[dict, Depends(require_admin)]): + """Supprime des entrées de la base de données selon la table et la période choisies.""" + if body.table not in _ALLOWED_TABLES and body.table != "all": + raise HTTPException(status_code=400, detail="Table inconnue") + if body.period not in _ALLOWED_PERIODS: + raise HTTPException(status_code=400, detail="Période inconnue") + + tables = list(_ALLOWED_TABLES) if body.table == "all" else [body.table] + + now = int(time.time()) + period_cutoff = { + "last_24h": now - 24 * 3600, + "last_7d": now - 7 * 24 * 3600, + "last_30d": now - 30 * 24 * 3600, + } + + deleted: dict[str, int] = {} + with get_db() as conn: + for tbl in tables: + if body.period == "all": + cur = conn.execute(f"DELETE FROM {tbl}") # nosec – tbl validated above + elif body.period == "custom": + if body.from_ts is None or body.to_ts is None: + raise HTTPException( + status_code=400, + detail="Période personnalisée : from_ts et to_ts sont requis", + ) + if body.from_ts > body.to_ts: + raise HTTPException(status_code=400, detail="from_ts doit être ≤ to_ts") + cur = conn.execute( + f"DELETE FROM {tbl} WHERE ts >= ? AND ts <= ?", # nosec + (body.from_ts, body.to_ts), + ) + else: + cutoff = period_cutoff[body.period] + cur = conn.execute( + f"DELETE FROM {tbl} WHERE ts >= ?", # nosec + (cutoff,), + ) + deleted[tbl] = cur.rowcount + + return {"deleted": deleted, "status": "ok"} + + # ─── Routes VPS ─────────────────────────────────────────────────────────────── @app.get("/api/vps") @@ -756,3 +845,18 @@ async def compose_update( return await r.json() except Exception as e: raise HTTPException(status_code=502, detail=str(e)) + + +@app.post("/api/vps/{vps_id}/agent/update") +async def agent_self_update( + vps_id: str, + _: Annotated[dict, Depends(get_current_user)] = None, +): + """Déclenche la mise à jour de l'agent sur le VPS.""" + vps = next((v for v in load_vps() if v["id"] == vps_id), None) + if not vps: + raise HTTPException(status_code=404, detail="VPS introuvable") + try: + return await agent_post(vps, "/self-update") + except Exception as e: + raise HTTPException(status_code=502, detail=str(e)) diff --git a/vps-monitor/docker-compose.yml b/vps-monitor/docker-compose.yml index f65a791..cf05434 100644 --- a/vps-monitor/docker-compose.yml +++ b/vps-monitor/docker-compose.yml @@ -1,6 +1,6 @@ services: backend: - build: ./backend + image: git.jeanbonapp.com/jeanbon/scriptvps/backend:latest ports: - "8000:8000" volumes: @@ -9,9 +9,10 @@ services: restart: unless-stopped frontend: - build: ./frontend + image: git.jeanbonapp.com/jeanbon/scriptvps/frontend:latest ports: - "3000:80" depends_on: - backend restart: unless-stopped + diff --git a/vps-monitor/frontend/src/App.jsx b/vps-monitor/frontend/src/App.jsx index 9b49d8a..ff9c040 100644 --- a/vps-monitor/frontend/src/App.jsx +++ b/vps-monitor/frontend/src/App.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { fetchAllStatus, containerAction, addVps, deleteVps, fetchLogs, authStatus, getToken, setToken, composeUpdate, updateVps } from './api/client' +import { fetchAllStatus, containerAction, addVps, deleteVps, fetchLogs, authStatus, getToken, setToken, composeUpdate, updateVps, updateAgent } from './api/client' import Header from './components/Header' import VpsCard from './components/VpsCard' import LogsModal from './components/LogsModal' @@ -165,6 +165,21 @@ export default function App() { } } + const handleUpdateAgent = async (vpsId) => { + setUpdateModal({ vpsId, project: 'agent' }) + setUpdateLoading(true) + setUpdateContent('Lancement de la mise à jour de l\'agent…\n') + try { + await updateAgent(vpsId) + setUpdateContent('Mise à jour lancée. L\'agent va redémarrer dans quelques secondes.\nActualisez dans un moment pour vérifier la nouvelle version.') + } catch (e) { + setUpdateContent(`Erreur lors de la mise à jour de l'agent :\n${e.message}`) + } finally { + setUpdateLoading(false) + setTimeout(() => refresh(), 8000) + } + } + const handleAddVps = async (formData) => { await addVps(formData) setShowAddVps(false) @@ -289,6 +304,7 @@ export default function App() { onUpdate={handleUpdate} onEdit={setEditVps} onStats={(vpsId, vpsName) => setStatsModal({ vpsId, vpsName })} + onUpdateAgent={handleUpdateAgent} /> ))} diff --git a/vps-monitor/frontend/src/api/client.js b/vps-monitor/frontend/src/api/client.js index 0dacf45..310114b 100644 --- a/vps-monitor/frontend/src/api/client.js +++ b/vps-monitor/frontend/src/api/client.js @@ -100,6 +100,14 @@ export async function composeUpdate(vpsId, project) { return handleResponse(res) } +export async function updateAgent(vpsId) { + const res = await fetch(`${BASE}/vps/${vpsId}/agent/update`, { + method: 'POST', + headers: authHeaders(), + }) + return handleResponse(res) +} + export async function updateVps(vpsId, data) { const res = await fetch(`${BASE}/vps/${vpsId}`, { method: 'PUT', @@ -152,3 +160,22 @@ export async function getAdminUsers() { const res = await fetch(`${BASE}/admin/users`, { headers: authHeaders() }) return handleResponse(res) } + +export async function getDbInfo() { + const res = await fetch(`${BASE}/admin/db/info`, { headers: authHeaders() }) + return handleResponse(res) +} + +export async function purgeDb({ table, period, fromTs, toTs }) { + const body = { table, period } + if (period === 'custom') { + body.from_ts = fromTs + body.to_ts = toTs + } + const res = await fetch(`${BASE}/admin/db/purge`, { + method: 'DELETE', + headers: authHeaders(), + body: JSON.stringify(body), + }) + return handleResponse(res) +} diff --git a/vps-monitor/frontend/src/components/AdminPage.jsx b/vps-monitor/frontend/src/components/AdminPage.jsx index c2c0cc0..6a32765 100644 --- a/vps-monitor/frontend/src/components/AdminPage.jsx +++ b/vps-monitor/frontend/src/components/AdminPage.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react' -import { ShieldCheck, ArrowLeft, RefreshCw, ToggleLeft, ToggleRight, Check, X } from 'lucide-react' -import { getAdminSettings, setAdminSetting, getLoginLogs } from '../api/client' +import { ShieldCheck, ArrowLeft, RefreshCw, ToggleLeft, ToggleRight, Check, X, Database, Trash2, AlertTriangle } from 'lucide-react' +import { getAdminSettings, setAdminSetting, getLoginLogs, getDbInfo, purgeDb } from '../api/client' const PAGE_SIZE = 50 @@ -27,6 +27,8 @@ function ToggleRow({ label, description, enabled, onChange, loading }) { } export default function AdminPage({ onBack }) { + const [activeTab, setActiveTab] = useState('settings') // 'settings' | 'logs' | 'database' + // ─── Settings ──────────────────────────────────────────────────────────── const [settings, setSettings] = useState(null) const [settingsLoading, setSettingsLoading] = useState(true) @@ -96,6 +98,82 @@ export default function AdminPage({ onBack }) { const totalPages = Math.ceil(logsTotal / PAGE_SIZE) + // ─── Database management ───────────────────────────────────────────────── + const [dbInfo, setDbInfo] = useState(null) + const [dbInfoLoading, setDbInfoLoading] = useState(false) + const [dbInfoError, setDbInfoError] = useState(null) + const [purgeLoading, setPurgeLoading] = useState(false) + const [purgeResult, setPurgeResult] = useState(null) + const [purgeError, setPurgeError] = useState(null) + const [confirmState, setConfirmState] = useState(null) // { table, period, fromTs?, toTs? } + // Custom range + const [customFrom, setCustomFrom] = useState('') + const [customTo, setCustomTo] = useState('') + + const loadDbInfo = useCallback(async () => { + setDbInfoLoading(true) + setDbInfoError(null) + try { + const data = await getDbInfo() + setDbInfo(data) + } catch (err) { + setDbInfoError(err.message) + } finally { + setDbInfoLoading(false) + } + }, []) + + useEffect(() => { + if (activeTab === 'database') loadDbInfo() + }, [activeTab, loadDbInfo]) + + const requestPurge = (table, period, extraOpts = {}) => { + setPurgeResult(null) + setPurgeError(null) + setConfirmState({ table, period, ...extraOpts }) + } + + const confirmPurge = async () => { + if (!confirmState) return + setPurgeLoading(true) + setPurgeResult(null) + setPurgeError(null) + try { + const result = await purgeDb(confirmState) + const total = Object.values(result.deleted).reduce((a, b) => a + b, 0) + setPurgeResult(`${total} entrée${total !== 1 ? 's' : ''} supprimée${total !== 1 ? 's' : ''}.`) + loadDbInfo() + } catch (err) { + setPurgeError(err.message) + } finally { + setPurgeLoading(false) + setConfirmState(null) + } + } + + const periodLabel = { + last_24h: '24 dernières heures', + last_7d: '7 derniers jours', + last_30d: '30 derniers jours', + all: 'toutes les entrées', + custom: 'la période personnalisée', + } + + const tableLabel = { + vps_stats: 'Statistiques VPS', + login_logs: 'Logs de connexion', + all: 'toutes les tables', + } + + function fmtTs(ts) { + if (!ts) return '—' + return new Date(ts * 1000).toLocaleString('fr-FR') + } + + function fmtCount(n) { + return new Intl.NumberFormat('fr-FR').format(n) + } + return (
@@ -107,152 +185,345 @@ export default function AdminPage({ onBack }) { Retour au tableau de bord -
+

Administration

- {/* ── Section Paramètres ── */} -
-

Paramètres

-

Configuration globale de l'application.

+ {/* ── Tabs ── */} +
+ {[ + { key: 'settings', label: 'Paramètres' }, + { key: 'logs', label: 'Connexions' }, + { key: 'database', label: 'Base de données' }, + ].map(tab => ( + + ))} +
- {settingsError && ( -
- {settingsError} -
- )} + {/* ── Tab: Paramètres ── */} + {activeTab === 'settings' && ( +
+

Paramètres

+

Configuration globale de l'application.

- {settingsLoading - ?

Chargement…

- : ( -
- + {settingsError && ( +
+ {settingsError}
- ) - } -
+ )} - {/* ── Section Logs de connexion ── */} -
-
-
-

Tentatives de connexion

-

{logsTotal} entrée{logsTotal !== 1 ? 's' : ''} au total

+ {settingsLoading + ?

Chargement…

+ : ( +
+ +
+ ) + } +
+ )} + + {/* ── Tab: Connexions ── */} + {activeTab === 'logs' && ( +
+
+
+

Tentatives de connexion

+

{logsTotal} entrée{logsTotal !== 1 ? 's' : ''} au total

+
+
+ setFilterUser(e.target.value)} + className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:border-indigo-500 transition-colors w-44" + /> + + +
-
- {/* Filtre utilisateur */} - setFilterUser(e.target.value)} - className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:border-indigo-500 transition-colors w-44" - /> - {/* Filtre succès */} - + + {logsError && ( +
+ {logsError} +
+ )} + + {logsLoading && logs.length === 0 + ?

Chargement…

+ : filteredLogs.length === 0 + ?

Aucune entrée.

+ : ( +
+ + + + + + + + + + + + {filteredLogs.map(log => ( + + + + + + + + ))} + +
Date / HeureUtilisateurAdresse IPRésultatDétail
+ {new Date(log.ts).toLocaleString('fr-FR')} + {log.username}{log.ip} + {log.success + ? ( + + Succès + + ) : ( + + Échec + + ) + } + {log.reason || '—'}
+
+ ) + } + + {totalPages > 1 && ( +
+ + + Page {logsPage + 1} / {totalPages} + + +
+ )} +
+ )} + + {/* ── Tab: Base de données ── */} + {activeTab === 'database' && ( +
+ {/* En-tête */} +
+
+

Gestion de la base de données

+

Supprimez les données historiques par table et par période.

+
-
- {logsError && ( -
- {logsError} -
- )} + {dbInfoError && ( +
+ {dbInfoError} +
+ )} + {purgeResult && ( +
+ {purgeResult} +
+ )} + {purgeError && ( +
+ {purgeError} +
+ )} - {logsLoading && logs.length === 0 - ?

Chargement…

- : filteredLogs.length === 0 - ?

Aucune entrée.

- : ( -
- - - - - - - - - - - - {filteredLogs.map(log => ( - - - - - - - - ))} - -
Date / HeureUtilisateurAdresse IPRésultatDétail
- {new Date(log.ts).toLocaleString('fr-FR')} - {log.username}{log.ip} - {log.success - ? ( - - Succès - - ) : ( - - Échec - - ) - } - {log.reason || '—'}
-
+ {/* Cards par table */} + {[ + { key: 'vps_stats', label: 'Statistiques VPS', icon: }, + { key: 'login_logs', label: 'Logs de connexion', icon: }, + ].map(({ key, label, icon }) => { + const info = dbInfo?.[key] + return ( +
+
+ {icon} +

{label}

+ {info && ( + + {fmtCount(info.count)} entrée{info.count !== 1 ? 's' : ''} + {info.oldest_ts && ` · du ${fmtTs(info.oldest_ts)} au ${fmtTs(info.newest_ts)}`} + + )} +
+ +
+ {[ + { period: 'last_24h', label: '24 dernières heures' }, + { period: 'last_7d', label: '7 derniers jours' }, + { period: 'last_30d', label: '30 derniers jours' }, + { period: 'all', label: 'Tout effacer', danger: true }, + ].map(({ period, label: btnLabel, danger }) => ( + + ))} +
+
) - } + })} - {/* Pagination */} - {totalPages > 1 && ( -
- - - Page {logsPage + 1} / {totalPages} - - + {/* Période personnalisée */} +
+

Période personnalisée

+

Supprimez les données comprises entre deux dates précises.

+ +
+
+ + setCustomFrom(e.target.value)} + className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:border-indigo-500 transition-colors" + /> +
+
+ + setCustomTo(e.target.value)} + className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:border-indigo-500 transition-colors" + /> +
+
+ + +
+ +
+
+
+ )} + + {/* ── Modale de confirmation de purge ── */} + {confirmState && ( +
+
+
+
+ +
+

Confirmer la suppression

+
+

+ Vous êtes sur le point de supprimer{' '} + {periodLabel[confirmState.period]}{' '} + dans{' '} + {tableLabel[confirmState.table]}. + Cette action est irréversible. +

+
+ + +
- )} -
+
+ )} +
) diff --git a/vps-monitor/frontend/src/components/VpsCard.jsx b/vps-monitor/frontend/src/components/VpsCard.jsx index 60d49c8..b181a97 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 } from 'lucide-react' +import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp, RefreshCw, Cpu, MemoryStick, ArrowUp, ArrowDown, Pencil, BarChart2, CloudDownload } from 'lucide-react' import { useState } from 'react' import ContainerRow from './ContainerRow' import { tagColor } from './TagInput' @@ -14,9 +14,10 @@ function formatRam(bytes) { return `${(bytes / 1024 ** 3).toFixed(1)} GB` } -export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onEdit, onStats }) { +export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onEdit, onStats, onUpdateAgent }) { const [collapsed, setCollapsed] = useState(false) const [updatingProject, setUpdatingProject] = useState(null) + const [updatingAgent, setUpdatingAgent] = useState(false) const running = vps.containers.filter(c => c.status === 'running').length const total = vps.containers.length @@ -28,6 +29,11 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onE try { await onUpdate(vps.id, project) } finally { setUpdatingProject(null) } } + const handleUpdateAgent = async () => { + setUpdatingAgent(true) + try { await onUpdateAgent(vps.id) } finally { setUpdatingAgent(false) } + } + return (
{/* Header */} @@ -171,6 +177,40 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onE
)} + {/* Version de l'agent + bouton mise à jour */} + {!collapsed && vps.online && ( +
+ Agent : + {vps.agent_version ? ( + + {vps.agent_up_to_date ? '✓' : '⚠'} v{vps.agent_version} + + ) : ( + + inconnu + + )} + {(!vps.agent_up_to_date || vps.agent_version === 'unknown') && ( + + )} +
+ )} + {/* Footer stats */} {!collapsed && vps.online && total > 0 && (