Feat : Graph data expand
Some checks failed
Build and Push Docker Images / docker (push) Failing after 8s

This commit is contained in:
jeanotx32
2026-05-19 01:16:59 -04:00
parent 3080826806
commit 43dd3c614d
5 changed files with 220 additions and 47 deletions

View File

@@ -109,7 +109,7 @@ export async function updateVps(vpsId, data) {
return handleResponse(res)
}
export async function fetchVpsStats(vpsId) {
const res = await fetch(`${BASE}/vps/${vpsId}/stats`, { headers: authHeaders() })
export async function fetchVpsStats(vpsId, duration = 600) {
const res = await fetch(`${BASE}/vps/${vpsId}/stats?duration=${duration}`, { headers: authHeaders() })
return handleResponse(res)
}

View File

@@ -30,6 +30,22 @@ function avg(arr) {
return (arr.reduce((a, b) => a + b, 0) / arr.length).toFixed(1)
}
/** Calcule un min/max adapté à la série pour que les variations soient visibles. */
function autoRange(data, absMin = 0, absMax = 100, minRange = 8) {
if (!data.length) return { min: absMin, max: absMax }
const lo = Math.min(...data)
const hi = Math.max(...data)
const margin = Math.max((hi - lo) * 0.2, 1)
let min = Math.max(absMin, lo - margin)
let max = Math.min(absMax, hi + margin)
if (max - min < minRange) {
const mid = (min + max) / 2
min = Math.max(absMin, mid - minRange / 2)
max = Math.min(absMax, min + minRange)
}
return { min: +min.toFixed(1), max: +max.toFixed(1) }
}
// ─── SVG Sparkline ────────────────────────────────────────────────────────────
function Sparkline({ data, min = 0, max = 100, color, fill, height = 60 }) {
@@ -116,13 +132,19 @@ function Sparkline({ data, min = 0, max = 100, color, fill, height = 60 }) {
)}
</svg>
{/* Labels min/max axe Y */}
<div className="absolute inset-y-0 right-0 flex flex-col justify-between text-right pointer-events-none" style={{ width: 28 }}>
<span className="text-gray-600" style={{ fontSize: 9, lineHeight: '1' }}>{max}%</span>
<span className="text-gray-600" style={{ fontSize: 9, lineHeight: '1' }}>{min}%</span>
</div>
{/* Tooltip bubble */}
{tooltip && (
<div
className="absolute top-0 left-1/2 -translate-x-1/2 bg-gray-800 border border-gray-700 rounded px-2 py-0.5 text-xs text-white pointer-events-none"
style={{ whiteSpace: 'nowrap' }}
>
{typeof tooltip.value === 'number' ? tooltip.value.toFixed(1) : tooltip.value}
{typeof tooltip.value === 'number' ? tooltip.value.toFixed(1) : tooltip.value}%
</div>
)}
</div>
@@ -215,25 +237,43 @@ function StatCard({ title, icon, current, average, unit, children }) {
)
}
// ─── Modal principal ──────────────────────────────────────────────────────────
// ─── Options de durée ─────────────────────────────────────────────────────────
const DURATION_OPTIONS = [
{ label: '10 min', value: 600 },
{ label: '1 h', value: 3_600 },
{ label: '6 h', value: 21_600 },
{ label: '24 h', value: 86_400 },
{ label: '7 j', value: 604_800 },
{ label: '30 j', value: 2_592_000 },
]
// ─── Modal principal ──────────────────────────────────────────────────────────────
export default function StatsModal({ vpsId, vpsName, onClose }) {
const [stats, setStats] = useState([])
const [loading, setLoading] = useState(true)
const [stats, setStats] = useState([])
const [loading, setLoading] = useState(true)
const [duration, setDuration] = useState(600)
// Refresh adaptatif : 5 s pour la vue courte, 30 s pour les vues historiques
const refreshMs = duration <= 600 ? 5_000 : 30_000
const load = useCallback(async () => {
try {
const data = await fetchVpsStats(vpsId)
const data = await fetchVpsStats(vpsId, duration)
setStats(data)
} catch { /* silencieux */ }
setLoading(false)
}, [vpsId])
}, [vpsId, duration])
// Réinit + recharge quand la durée change
useEffect(() => {
setLoading(true)
setStats([])
load()
const id = setInterval(load, 5000)
const id = setInterval(load, refreshMs)
return () => clearInterval(id)
}, [load])
}, [load, refreshMs])
const last = stats[stats.length - 1]
@@ -241,11 +281,15 @@ export default function StatsModal({ vpsId, vpsName, onClose }) {
const cpuData = stats.map(s => s.cpu)
const ramData = stats.map(s => s.ram_percent)
// Axes auto-scalés pour voir les variations même à faible charge
const cpuRange = autoRange(cpuData, 0, 100)
const ramRange = autoRange(ramData, 0, 100)
// Réseau : KB/s pour l'affichage du graphique
const sentKB = stats.map(s => s.net_sent_per_sec / 1024)
const recvKB = stats.map(s => s.net_recv_per_sec / 1024)
// Trafic cumulé (delta first → last, bytes depuis démarrage du collecteur)
// Trafic cumulé (delta first → last)
const sessionSent = stats.length > 1
? Math.max(0, stats.at(-1).net_bytes_sent - stats[0].net_bytes_sent)
: 0
@@ -253,32 +297,50 @@ export default function StatsModal({ vpsId, vpsName, onClose }) {
? Math.max(0, stats.at(-1).net_bytes_recv - stats[0].net_bytes_recv)
: 0
const points = stats.length
const windowMin = points > 0 ? `${(points * 5 / 60).toFixed(0)} min` : '—'
const durationLabel = DURATION_OPTIONS.find(o => o.value === duration)?.label ?? ''
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-gray-950 border border-gray-800 rounded-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto shadow-2xl">
{/* En-tête */}
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-800 sticky top-0 bg-gray-950 z-10">
<div className="flex items-center gap-3">
<BarChart2 size={18} className="text-indigo-400" />
<div>
<h2 className="font-semibold text-sm">{vpsName}</h2>
<p className="text-xs text-gray-500">
{points > 0
? `${points} points sur ~${windowMin} · collecte toutes les 5 s`
: 'En attente de données…'}
</p>
<div className="px-5 py-3 border-b border-gray-800 sticky top-0 bg-gray-950 z-10">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<BarChart2 size={18} className="text-indigo-400" />
<div>
<h2 className="font-semibold text-sm">{vpsName}</h2>
<p className="text-xs text-gray-500">
{stats.length > 0
? `${stats.length} points · fenêtre ${durationLabel} · rafraîchissement ${refreshMs / 1000} s`
: 'En attente de données…'}
</p>
</div>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-gray-800 text-gray-500 hover:text-white transition-colors"
>
<X size={16} />
</button>
</div>
{/* Sélecteur de durée */}
<div className="flex gap-1 flex-wrap">
{DURATION_OPTIONS.map(opt => (
<button
key={opt.value}
onClick={() => setDuration(opt.value)}
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-colors ${
duration === opt.value
? 'bg-indigo-600 text-white'
: 'bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-gray-200'
}`}
>
{opt.label}
</button>
))}
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-gray-800 text-gray-500 hover:text-white transition-colors"
>
<X size={16} />
</button>
</div>
{/* Contenu */}
@@ -303,7 +365,7 @@ export default function StatsModal({ vpsId, vpsName, onClose }) {
average={avg(cpuData)}
unit="%"
>
<Sparkline data={cpuData} min={0} max={100} color="#818cf8" fill="#818cf8" height={56} />
<Sparkline data={cpuData} min={cpuRange.min} max={cpuRange.max} color="#818cf8" fill="#818cf8" height={56} />
</StatCard>
<StatCard
@@ -313,7 +375,7 @@ export default function StatsModal({ vpsId, vpsName, onClose }) {
average={avg(ramData)}
unit="%"
>
<Sparkline data={ramData} min={0} max={100} color="#34d399" fill="#34d399" height={56} />
<Sparkline data={ramData} min={ramRange.min} max={ramRange.max} color="#34d399" fill="#34d399" height={56} />
{last && (
<p className="text-xs text-gray-600 -mt-1">
{fmtRam(last.ram_used)} / {fmtRam(last.ram_total)}