Admin: onglet Infos systeme (reseau, RAM, disque, temp, camera)
- Endpoint /api/systeme/info : IPs par interface, hostname, uptime, disque, RAM, temperature CPU, version Python, statut camera - Onglet Infos dans le backoffice avec affichage visuel (barres, couleurs) - Bouton Actualiser pour rafraichir les donnees en temps reel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
103
backend/main.py
103
backend/main.py
@@ -152,6 +152,109 @@ async def api_preview():
|
|||||||
return {"image": f"data:image/jpeg;base64,{b64}"}
|
return {"image": f"data:image/jpeg;base64,{b64}"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/systeme/info")
|
||||||
|
async def api_systeme_info():
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Interfaces reseau avec leurs IPs
|
||||||
|
interfaces = []
|
||||||
|
try:
|
||||||
|
import netifaces
|
||||||
|
for iface in netifaces.interfaces():
|
||||||
|
addrs = netifaces.ifaddresses(iface)
|
||||||
|
ipv4 = addrs.get(netifaces.AF_INET, [])
|
||||||
|
ipv6 = addrs.get(netifaces.AF_INET6, [])
|
||||||
|
ips = [a['addr'] for a in ipv4 if a.get('addr') != '127.0.0.1']
|
||||||
|
ips += [a['addr'].split('%')[0] for a in ipv6 if not a.get('addr', '').startswith('::1')]
|
||||||
|
if ips:
|
||||||
|
interfaces.append({"nom": iface, "ips": ips})
|
||||||
|
except ImportError:
|
||||||
|
# Fallback sans netifaces
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(["ip", "-o", "addr", "show"], text=True)
|
||||||
|
for ligne in out.splitlines():
|
||||||
|
parts = ligne.split()
|
||||||
|
if len(parts) >= 4:
|
||||||
|
iface = parts[1]
|
||||||
|
ip = parts[3].split('/')[0]
|
||||||
|
if ip not in ('127.0.0.1', '::1') and not ip.startswith('fe80'):
|
||||||
|
existing = next((x for x in interfaces if x['nom'] == iface), None)
|
||||||
|
if existing:
|
||||||
|
existing['ips'].append(ip)
|
||||||
|
else:
|
||||||
|
interfaces.append({"nom": iface, "ips": [ip]})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Hostname
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
|
||||||
|
# Uptime
|
||||||
|
uptime_str = ""
|
||||||
|
try:
|
||||||
|
with open('/proc/uptime') as f:
|
||||||
|
secs = float(f.read().split()[0])
|
||||||
|
h, rem = divmod(int(secs), 3600)
|
||||||
|
m = rem // 60
|
||||||
|
uptime_str = f"{h}h {m}min"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Espace disque (partition racine)
|
||||||
|
disque = {}
|
||||||
|
try:
|
||||||
|
total, used, free = shutil.disk_usage("/")
|
||||||
|
disque = {
|
||||||
|
"total": round(total / 1e9, 1),
|
||||||
|
"utilise": round(used / 1e9, 1),
|
||||||
|
"libre": round(free / 1e9, 1),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Memoire RAM
|
||||||
|
ram = {}
|
||||||
|
try:
|
||||||
|
with open('/proc/meminfo') as f:
|
||||||
|
lignes = {l.split(':')[0]: l.split(':')[1].strip() for l in f.readlines()}
|
||||||
|
total_kb = int(lignes.get('MemTotal', '0 kB').split()[0])
|
||||||
|
libre_kb = int(lignes.get('MemAvailable', '0 kB').split()[0])
|
||||||
|
utilise_kb = total_kb - libre_kb
|
||||||
|
ram = {
|
||||||
|
"total": round(total_kb / 1e6, 1),
|
||||||
|
"utilise": round(utilise_kb / 1e6, 1),
|
||||||
|
"libre": round(libre_kb / 1e6, 1),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Temperature CPU (RPi / Linux)
|
||||||
|
temp = None
|
||||||
|
try:
|
||||||
|
with open('/sys/class/thermal/thermal_zone0/temp') as f:
|
||||||
|
temp = round(int(f.read().strip()) / 1000, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Version Python et app
|
||||||
|
import sys
|
||||||
|
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"hostname": hostname,
|
||||||
|
"interfaces": interfaces,
|
||||||
|
"uptime": uptime_str,
|
||||||
|
"disque": disque,
|
||||||
|
"ram": ram,
|
||||||
|
"temperature_cpu": temp,
|
||||||
|
"python": python_version,
|
||||||
|
"camera_mode": camera.mode,
|
||||||
|
"camera_connectee": camera.connectee,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/camera/statut")
|
@app.get("/api/camera/statut")
|
||||||
async def api_camera_statut():
|
async def api_camera_statut():
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -780,6 +780,82 @@ html, body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Infos systeme */
|
||||||
|
.infos-systeme-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-systeme-contenu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-section {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-section h4 {
|
||||||
|
margin: 0 0 0.8rem 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-ligne {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.35rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.07);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-ligne:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-ligne span {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-ip {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-barre-fond {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-barre {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infos-chargement, .infos-erreur {
|
||||||
|
opacity: 0.5;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-petit {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Toggles */
|
/* Toggles */
|
||||||
.toggle-groupe {
|
.toggle-groupe {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -331,6 +331,7 @@
|
|||||||
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
||||||
<button class="onglet" data-onglet="evenement">Evenement</button>
|
<button class="onglet" data-onglet="evenement">Evenement</button>
|
||||||
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
||||||
|
<button class="onglet" data-onglet="infos" onclick="chargerInfosSysteme()">Infos</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panneau Materiel (Camera + Imprimante) -->
|
<!-- Panneau Materiel (Camera + Imprimante) -->
|
||||||
@@ -567,6 +568,17 @@
|
|||||||
<button class="btn-danger" onclick="confirmerRedemarrage()">Redemarrer le systeme</button>
|
<button class="btn-danger" onclick="confirmerRedemarrage()">Redemarrer le systeme</button>
|
||||||
<button class="btn-danger" onclick="confirmerExtinction()">Eteindre le systeme</button>
|
<button class="btn-danger" onclick="confirmerExtinction()">Eteindre le systeme</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panneau Infos systeme -->
|
||||||
|
<div class="admin-panneau" id="panneau-infos">
|
||||||
|
<div class="infos-systeme-header">
|
||||||
|
<h3>Informations systeme</h3>
|
||||||
|
<button class="btn-action btn-petit" onclick="chargerInfosSysteme()">↻ Actualiser</button>
|
||||||
|
</div>
|
||||||
|
<div id="infos-systeme-contenu" class="infos-systeme-contenu">
|
||||||
|
<p class="infos-chargement">Chargement...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -685,6 +685,81 @@ async function sauvegarderBooth() {
|
|||||||
chargerBoothInfo();
|
chargerBoothInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === INFOS SYSTEME ===
|
||||||
|
|
||||||
|
async function chargerInfosSysteme() {
|
||||||
|
const contenu = document.getElementById('infos-systeme-contenu');
|
||||||
|
contenu.innerHTML = '<p class="infos-chargement">Chargement...</p>';
|
||||||
|
try {
|
||||||
|
const info = await apiGet('/api/systeme/info');
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Reseau
|
||||||
|
html += '<div class="infos-section">';
|
||||||
|
html += '<h4>🌐 Reseau</h4>';
|
||||||
|
html += `<div class="infos-ligne"><span>Hostname</span><strong>${info.hostname}</strong></div>`;
|
||||||
|
if (info.interfaces && info.interfaces.length > 0) {
|
||||||
|
info.interfaces.forEach(iface => {
|
||||||
|
iface.ips.forEach(ip => {
|
||||||
|
html += `<div class="infos-ligne"><span>${iface.nom}</span><strong class="infos-ip">${ip}</strong></div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html += '<div class="infos-ligne"><span>Aucune interface</span><strong>—</strong></div>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Systeme
|
||||||
|
html += '<div class="infos-section">';
|
||||||
|
html += '<h4>💻 Systeme</h4>';
|
||||||
|
if (info.uptime) html += `<div class="infos-ligne"><span>Uptime</span><strong>${info.uptime}</strong></div>`;
|
||||||
|
if (info.temperature_cpu !== null && info.temperature_cpu !== undefined) {
|
||||||
|
const couleur = info.temperature_cpu > 70 ? '#e53935' : info.temperature_cpu > 55 ? '#fb8c00' : '#43a047';
|
||||||
|
html += `<div class="infos-ligne"><span>Temperature CPU</span><strong style="color:${couleur}">${info.temperature_cpu} °C</strong></div>`;
|
||||||
|
}
|
||||||
|
if (info.python) html += `<div class="infos-ligne"><span>Python</span><strong>${info.python}</strong></div>`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Stockage
|
||||||
|
if (info.disque && info.disque.total) {
|
||||||
|
const pct = Math.round((info.disque.utilise / info.disque.total) * 100);
|
||||||
|
const couleurDisk = pct > 85 ? '#e53935' : pct > 65 ? '#fb8c00' : '#43a047';
|
||||||
|
html += '<div class="infos-section">';
|
||||||
|
html += '<h4>💾 Stockage</h4>';
|
||||||
|
html += `<div class="infos-ligne"><span>Total</span><strong>${info.disque.total} Go</strong></div>`;
|
||||||
|
html += `<div class="infos-ligne"><span>Utilise</span><strong style="color:${couleurDisk}">${info.disque.utilise} Go (${pct}%)</strong></div>`;
|
||||||
|
html += `<div class="infos-ligne"><span>Libre</span><strong>${info.disque.libre} Go</strong></div>`;
|
||||||
|
html += '<div class="infos-barre-fond"><div class="infos-barre" style="width:' + pct + '%;background:' + couleurDisk + '"></div></div>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAM
|
||||||
|
if (info.ram && info.ram.total) {
|
||||||
|
const pctRam = Math.round((info.ram.utilise / info.ram.total) * 100);
|
||||||
|
const couleurRam = pctRam > 85 ? '#e53935' : pctRam > 65 ? '#fb8c00' : '#43a047';
|
||||||
|
html += '<div class="infos-section">';
|
||||||
|
html += '<h4>🫀 Memoire RAM</h4>';
|
||||||
|
html += `<div class="infos-ligne"><span>Total</span><strong>${info.ram.total} Go</strong></div>`;
|
||||||
|
html += `<div class="infos-ligne"><span>Utilise</span><strong style="color:${couleurRam}">${info.ram.utilise} Go (${pctRam}%)</strong></div>`;
|
||||||
|
html += `<div class="infos-ligne"><span>Libre</span><strong>${info.ram.libre} Go</strong></div>`;
|
||||||
|
html += '<div class="infos-barre-fond"><div class="infos-barre" style="width:' + pctRam + '%;background:' + couleurRam + '"></div></div>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera
|
||||||
|
html += '<div class="infos-section">';
|
||||||
|
html += '<h4>📷 Camera</h4>';
|
||||||
|
const camOk = info.camera_connectee;
|
||||||
|
html += `<div class="infos-ligne"><span>Mode</span><strong>${info.camera_mode}</strong></div>`;
|
||||||
|
html += `<div class="infos-ligne"><span>Statut</span><strong style="color:${camOk ? '#43a047' : '#e53935'}">${camOk ? 'Connectee' : 'Deconnectee'}</strong></div>`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
contenu.innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
contenu.innerHTML = '<p class="infos-erreur">Impossible de charger les informations systeme.</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function regenererBoothPassword() {
|
async function regenererBoothPassword() {
|
||||||
const booth = config.booth || {};
|
const booth = config.booth || {};
|
||||||
const url = getValue('admin-booth-url').replace(/\/$/, '');
|
const url = getValue('admin-booth-url').replace(/\/$/, '');
|
||||||
|
|||||||
Reference in New Issue
Block a user