diff --git a/vps-monitor/.pids b/vps-monitor/.pids index b291492..cb8ea29 100644 --- a/vps-monitor/.pids +++ b/vps-monitor/.pids @@ -1,2 +1,2 @@ -2317 -2396 +5812 +5870 diff --git a/vps-monitor/backend/__pycache__/main.cpython-313.pyc b/vps-monitor/backend/__pycache__/main.cpython-313.pyc index 0d87bb4..99e4c74 100644 Binary files a/vps-monitor/backend/__pycache__/main.cpython-313.pyc and b/vps-monitor/backend/__pycache__/main.cpython-313.pyc differ diff --git a/vps-monitor/backend/main.py b/vps-monitor/backend/main.py index 7674604..4fdba3c 100644 --- a/vps-monitor/backend/main.py +++ b/vps-monitor/backend/main.py @@ -5,6 +5,7 @@ Agrège les données de tous les agents et expose une API REST pour le frontend. """ import asyncio +import json import os import secrets import sqlite3 @@ -56,12 +57,22 @@ class VpsConfig(BaseModel): port: int = 8001 api_key: str description: str = "" + tags: list[str] = [] class ActionRequest(BaseModel): action: str # start | stop | restart +class VpsUpdateRequest(BaseModel): + name: str + host: str + port: int = 8001 + api_key: str = "" # vide = conserver la clé existante + description: str = "" + tags: list[str] = [] + + class ComposeUpdateRequest(BaseModel): project: str @@ -108,9 +119,15 @@ def init_db() -> None: host TEXT NOT NULL, port INTEGER NOT NULL DEFAULT 8001, api_key TEXT NOT NULL, - description TEXT NOT NULL DEFAULT '' + description TEXT NOT NULL DEFAULT '', + tags TEXT NOT NULL DEFAULT '[]' ) """) + # Migration : ajoute la colonne tags si elle n'existe pas encore + try: + conn.execute("ALTER TABLE vps ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'") + except Exception: + pass # colonne déjà présente init_db() @@ -133,14 +150,21 @@ def add_user(user: dict) -> None: def load_vps() -> list[dict]: with get_db() as conn: - return [dict(r) for r in conn.execute("SELECT * FROM vps").fetchall()] + rows = [dict(r) for r in conn.execute("SELECT * FROM vps").fetchall()] + for row in rows: + try: + row["tags"] = json.loads(row.get("tags") or "[]") + except Exception: + row["tags"] = [] + return rows def insert_vps(vps: dict) -> None: with get_db() as conn: conn.execute( - "INSERT INTO vps (id, name, host, port, api_key, description) VALUES (?, ?, ?, ?, ?, ?)", - (vps["id"], vps["name"], vps["host"], vps["port"], vps["api_key"], vps.get("description", "")), + "INSERT INTO vps (id, name, host, port, api_key, description, tags) VALUES (?, ?, ?, ?, ?, ?, ?)", + (vps["id"], vps["name"], vps["host"], vps["port"], vps["api_key"], + vps.get("description", ""), json.dumps(vps.get("tags", []))), ) @@ -150,6 +174,16 @@ def remove_vps(vps_id: str) -> bool: return cur.rowcount > 0 +def update_vps(vps_id: str, data: dict) -> bool: + with get_db() as conn: + cur = conn.execute( + "UPDATE vps SET name=?, host=?, port=?, api_key=?, description=?, tags=? WHERE id=?", + (data["name"], data["host"], data["port"], data["api_key"], + data["description"], json.dumps(data.get("tags", [])), vps_id), + ) + return cur.rowcount > 0 + + # ─── Auth helpers ───────────────────────────────────────────────────────────── def create_token(username: str, role: str) -> str: @@ -229,6 +263,7 @@ async def fetch_vps_status(vps: dict) -> dict: "online": True, "containers": containers_res, "system": system, + "tags": vps.get("tags", []), } except Exception as e: return { @@ -240,6 +275,7 @@ async def fetch_vps_status(vps: dict) -> dict: "error": str(e), "containers": [], "system": None, + "tags": vps.get("tags", []), } @@ -293,7 +329,8 @@ def me(current_user: Annotated[dict, Depends(get_current_user)]): def list_vps(_: Annotated[dict, Depends(get_current_user)]): """Liste les VPS configurés (sans les clés API).""" return [ - {"id": v["id"], "name": v["name"], "host": v["host"], "description": v.get("description", "")} + {"id": v["id"], "name": v["name"], "host": v["host"], + "description": v.get("description", ""), "tags": v.get("tags", [])} for v in load_vps() ] @@ -315,6 +352,24 @@ def delete_vps(vps_id: str, _: Annotated[dict, Depends(get_current_user)]): return {"status": "ok"} +@app.put("/api/vps/{vps_id}") +def edit_vps(vps_id: str, body: VpsUpdateRequest, _: Annotated[dict, Depends(get_current_user)]): + """Met à jour les paramètres d'un VPS (name, host, port, api_key, description).""" + existing = next((v for v in load_vps() if v["id"] == vps_id), None) + if not existing: + raise HTTPException(status_code=404, detail="VPS introuvable") + data = { + "name": body.name, + "host": body.host, + "port": body.port, + "api_key": body.api_key.strip() or existing["api_key"], + "description": body.description, + "tags": body.tags, + } + update_vps(vps_id, data) + return {"status": "ok"} + + @app.get("/api/status") async def all_status(_: Annotated[dict, Depends(get_current_user)]): """Retourne l'état de tous les VPS en parallèle.""" diff --git a/vps-monitor/frontend/src/App.jsx b/vps-monitor/frontend/src/App.jsx index 7cfd995..e3763be 100644 --- a/vps-monitor/frontend/src/App.jsx +++ b/vps-monitor/frontend/src/App.jsx @@ -1,12 +1,20 @@ import { useState, useEffect, useCallback } from 'react' -import { fetchAllStatus, containerAction, addVps, deleteVps, fetchLogs, authStatus, getToken, setToken, composeUpdate } from './api/client' -import Header from './components/Header' -import VpsCard from './components/VpsCard' -import LogsModal from './components/LogsModal' -import AddVpsModal from './components/AddVpsModal' -import LoginPage from './components/LoginPage' +import { fetchAllStatus, containerAction, addVps, deleteVps, fetchLogs, authStatus, getToken, setToken, composeUpdate, updateVps } from './api/client' +import Header from './components/Header' +import VpsCard from './components/VpsCard' +import LogsModal from './components/LogsModal' +import AddVpsModal from './components/AddVpsModal' +import EditVpsModal from './components/EditVpsModal' +import LoginPage from './components/LoginPage' -const REFRESH_INTERVAL = 30_000 +const INTERVAL_OPTIONS = [ + { label: '10 s', value: 10_000 }, + { label: '30 s', value: 30_000 }, + { label: '1 min', value: 60_000 }, + { label: '2 min', value: 120_000 }, + { label: '5 min', value: 300_000 }, + { label: 'Off', value: 0 }, +] export default function App() { const [token, setTokenState] = useState(() => getToken()) @@ -23,6 +31,16 @@ export default function App() { const [logsContent, setLogsContent] = useState('') const [logsLoading, setLogsLoading] = useState(false) const [showAddVps, setShowAddVps] = useState(false) + const [editVps, setEditVps] = useState(null) // objet vps à éditer + const [refreshInterval, setRefreshInterval] = useState(() => { + const stored = localStorage.getItem('refreshInterval') + return stored ? parseInt(stored, 10) : 30_000 + }) + + const handleIntervalChange = (val) => { + setRefreshInterval(val) + localStorage.setItem('refreshInterval', val) + } const [updateModal, setUpdateModal] = useState(null) // { vpsId, project } const [updateContent, setUpdateContent] = useState('') @@ -85,9 +103,10 @@ export default function App() { useEffect(() => { if (!token) return refresh() - const id = setInterval(() => refresh(), REFRESH_INTERVAL) + if (!refreshInterval) return + const id = setInterval(() => refresh(), refreshInterval) return () => clearInterval(id) - }, [refresh, token]) + }, [refresh, token, refreshInterval]) // Extrait le username du token stocké au rechargement de page useEffect(() => { @@ -145,6 +164,12 @@ export default function App() { await refresh(true) } + const handleEditVps = async (vpsId, data) => { + await updateVps(vpsId, data) + setEditVps(null) + await refresh(true) + } + // Attente vérification auth if (!authChecked) return null @@ -172,6 +197,9 @@ export default function App() { refreshing={refreshing} username={username} onLogout={handleLogout} + refreshInterval={refreshInterval} + onIntervalChange={handleIntervalChange} + intervalOptions={INTERVAL_OPTIONS} />
@@ -189,7 +217,7 @@ export default function App() { {[ { label: 'VPS en ligne', value: `${totalOnline}/${vpsList.length}`, color: 'text-emerald-400' }, { label: 'Conteneurs actifs', value: `${totalRunning}/${totalContainers}`, color: 'text-indigo-400' }, - { label: 'Actualisation auto', value: '30s', color: 'text-gray-400' }, + { label: 'Actualisation auto', value: INTERVAL_OPTIONS.find(o => o.value === refreshInterval)?.label ?? 'Off', color: 'text-gray-400' }, ].map(({ label, value, color }) => (

{value}

@@ -234,6 +262,7 @@ export default function App() { onLogs={openLogs} onDelete={handleDeleteVps} onUpdate={handleUpdate} + onEdit={setEditVps} /> ))}
@@ -267,6 +296,15 @@ export default function App() { onClose={() => setShowAddVps(false)} /> )} + + {/* Modal édition VPS */} + {editVps && ( + setEditVps(null)} + /> + )} ) } diff --git a/vps-monitor/frontend/src/api/client.js b/vps-monitor/frontend/src/api/client.js index ffcc613..a56ef78 100644 --- a/vps-monitor/frontend/src/api/client.js +++ b/vps-monitor/frontend/src/api/client.js @@ -99,3 +99,12 @@ export async function composeUpdate(vpsId, project) { }) return handleResponse(res) } + +export async function updateVps(vpsId, data) { + const res = await fetch(`${BASE}/vps/${vpsId}`, { + method: 'PUT', + headers: authHeaders(), + body: JSON.stringify(data), + }) + return handleResponse(res) +} diff --git a/vps-monitor/frontend/src/components/AddVpsModal.jsx b/vps-monitor/frontend/src/components/AddVpsModal.jsx index 59cecef..63eea07 100644 --- a/vps-monitor/frontend/src/components/AddVpsModal.jsx +++ b/vps-monitor/frontend/src/components/AddVpsModal.jsx @@ -1,7 +1,8 @@ import { useState, useEffect } from 'react' import { X } from 'lucide-react' +import TagInput from './TagInput' -const DEFAULTS = { id: '', name: '', host: '', port: '8001', api_key: '', description: '' } +const DEFAULTS = { id: '', name: '', host: '', port: '8001', api_key: '', description: '', tags: [] } const FIELDS = [ { key: 'name', label: 'Nom affiché', placeholder: 'Mon VPS 1', required: true, type: 'text' }, @@ -73,6 +74,12 @@ export default function AddVpsModal({ onSave, onClose }) {

)} +
+ + setForm(f => ({ ...f, tags }))} /> +

Entrée ou virgule pour valider

+
+
+
+ +
+ {FIELDS.map(({ key, label, placeholder, required, type }) => ( +
+ + +
+ ))} + + {error && ( +

+ {error} +

+ )} + +
+ + setForm(f => ({ ...f, tags }))} /> +

Entrée ou virgule pour valider

+
+ +
+ + +
+
+ + + ) +} diff --git a/vps-monitor/frontend/src/components/Header.jsx b/vps-monitor/frontend/src/components/Header.jsx index 1d561fa..ce6114e 100644 --- a/vps-monitor/frontend/src/components/Header.jsx +++ b/vps-monitor/frontend/src/components/Header.jsx @@ -1,6 +1,6 @@ -import { Monitor, LogOut } from 'lucide-react' +import { Monitor, LogOut, Timer } from 'lucide-react' -export default function Header({ lastUpdate, onRefresh, onAddVps, refreshing, username, onLogout }) { +export default function Header({ lastUpdate, onRefresh, onAddVps, refreshing, username, onLogout, refreshInterval, onIntervalChange, intervalOptions }) { return (
@@ -17,6 +17,21 @@ export default function Header({ lastUpdate, onRefresh, onAddVps, refreshing, us
+ {/* Sélecteur d'intervalle */} +
+ + +
+ + + ))} + setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={tags.length === 0 ? 'Ajouter un tag…' : ''} + className="flex-1 min-w-[120px] bg-transparent text-sm outline-none placeholder-gray-600" + /> +
+ ) +} diff --git a/vps-monitor/frontend/src/components/VpsCard.jsx b/vps-monitor/frontend/src/components/VpsCard.jsx index f6723f3..72f0590 100644 --- a/vps-monitor/frontend/src/components/VpsCard.jsx +++ b/vps-monitor/frontend/src/components/VpsCard.jsx @@ -1,6 +1,7 @@ -import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp, RefreshCw, Cpu, MemoryStick, ArrowUp, ArrowDown } from 'lucide-react' +import { Server, Wifi, WifiOff, Trash2, ChevronDown, ChevronUp, RefreshCw, Cpu, MemoryStick, ArrowUp, ArrowDown, Pencil } from 'lucide-react' import { useState } from 'react' import ContainerRow from './ContainerRow' +import { tagColor } from './TagInput' function formatBytes(bps) { if (bps < 1024) return `${bps.toFixed(0)} B/s` @@ -13,7 +14,7 @@ function formatRam(bytes) { return `${(bytes / 1024 ** 3).toFixed(1)} GB` } -export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate }) { +export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate, onEdit }) { const [collapsed, setCollapsed] = useState(false) const [updatingProject, setUpdatingProject] = useState(null) @@ -61,6 +62,14 @@ export default function VpsCard({ vps, onAction, onLogs, onDelete, onUpdate }) { {collapsed ? : } + +