Files
photoBooth/frontend/templates/admin/dashboard.html
T
admin 209f81aa3d
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
first commit
2026-07-16 00:55:29 +02:00

282 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard — JH Photomaton{% endblock %}
{% block nav_links %}
<a href="/admin" class="active">Dashboard</a>
<a href="/admin/gallery">Galerie admin</a>
<a href="/admin/print">Impression</a>
<a href="/admin/actions">Actions</a>
<a href="/gallery">Galerie publique</a>
<a href="/admin/logout" style="margin-left:auto;color:var(--text-dim)">Déconnexion</a>
{% endblock %}
{% block content %}
<div class="container">
<div class="flex items-center justify-between mb-2">
<h1 class="page-title" style="margin:0">Dashboard</h1>
<div class="flex gap-1 items-center">
<span class="text-sm text-muted">Mode impression :</span>
<select id="print-mode-select" class="form-control" style="width:auto">
<option value="direct" {% if print_mode=='direct' %}selected{% endif %}>Direct</option>
<option value="validation" {% if print_mode=='validation' %}selected{% endif %}>Validation</option>
<option value="gallery" {% if print_mode=='gallery' %}selected{% endif %}>Galerie</option>
</select>
</div>
</div>
<!-- Ressources système -->
<div class="section">
<div class="card-title">Ressources système</div>
<div class="grid-4">
<div class="card">
<div class="card-title">CPU</div>
<div class="card-value" id="cpu-val">{{ stats.cpu_percent|default('—') }}%</div>
<div class="progress mt-1"><div class="progress-bar" id="cpu-bar" style="width:{{ stats.cpu_percent|default(0) }}%"></div></div>
</div>
<div class="card">
<div class="card-title">RAM</div>
<div class="card-value" id="ram-val">{{ stats.ram_used_mb|default('—') }} Mo</div>
<div class="card-sub" id="ram-sub">sur {{ stats.ram_total_mb|default('?') }} Mo</div>
<div class="progress mt-1"><div class="progress-bar" id="ram-bar" style="width:{{ stats.ram_percent|default(0) }}%"></div></div>
</div>
<div class="card">
<div class="card-title">Température</div>
<div class="card-value" id="temp-val">{{ stats.cpu_temp|default('—') }}°C</div>
<div class="card-sub">CPU</div>
</div>
<div class="card">
<div class="card-title">Disque</div>
<div class="card-value" id="disk-val">{{ stats.disk_used_gb|default('—') }} Go</div>
<div class="card-sub" id="disk-sub">sur {{ stats.disk_total_gb|default('?') }} Go</div>
<div class="progress mt-1"><div class="progress-bar" id="disk-bar" style="width:{{ stats.disk_percent|default(0) }}%"></div></div>
</div>
</div>
</div>
<div class="grid-2 section">
<!-- Contrôle LED -->
<div class="card">
<div class="card-title">Anneau LED (35 LEDs · GPIO18)</div>
<div class="flex items-center gap-2 mt-1 mb-2">
<div class="led-ring {{ led_effect }}" id="led-ring">
<span id="led-label">{{ led_effect }}</span>
</div>
<div style="flex:1">
<div class="text-sm text-muted mb-1">Effet actuel</div>
<div class="flex gap-1" style="flex-wrap:wrap">
{% for effect in ['idle','countdown','capture','captured','finished','printing','error','disabled','off'] %}
<button class="btn btn-ghost btn-sm" onclick="setLedEffect('{{ effect }}')">{{ effect }}</button>
{% endfor %}
</div>
<div class="flex gap-1 mt-1 items-center">
<span class="text-sm text-muted">Couleur :</span>
<input type="color" id="led-color" value="#1e5080" style="border:none;background:none;cursor:pointer;width:32px;height:28px">
<button class="btn btn-ghost btn-sm" onclick="setLedColor()">Appliquer</button>
</div>
</div>
</div>
</div>
<!-- Statut bouton & relay -->
<div class="card">
<div class="card-title">Bouton physique (GPIO23) · Relay (GPIO12)</div>
<div class="flex gap-2 mt-1 mb-2">
<div>
<div class="text-sm text-muted mb-1">Relay bouton 12V</div>
<div class="flex gap-1">
<button class="btn btn-success btn-sm" onclick="setRelay('on')">✅ Activer</button>
<button class="btn btn-danger btn-sm" onclick="setRelay('off')">🔴 Désactiver</button>
</div>
<div class="text-xs text-muted mt-1">Statut : <span id="relay-state">{{ 'ON' if relay_state else 'OFF' }}</span></div>
</div>
<div>
<div class="text-sm text-muted mb-1">Simulation (test)</div>
<div class="flex gap-1" style="flex-wrap:wrap">
<button class="btn btn-ghost btn-sm" onclick="simulateClick(1)">1 clic</button>
<button class="btn btn-ghost btn-sm" onclick="simulateClick(2)">2 clics</button>
<button class="btn btn-ghost btn-sm" onclick="simulateClick(3)">3 clics</button>
<button class="btn btn-ghost btn-sm" onclick="simulateClick(0)">Long press</button>
</div>
</div>
</div>
<div class="text-xs text-muted" id="last-button-event">Dernier événement : —</div>
</div>
</div>
<!-- File d'impression en attente -->
<div class="section">
<div class="flex items-center justify-between mb-1">
<div class="card-title" style="margin:0">File d'impression (<span id="queue-count"></span>)</div>
<a href="/admin/print" class="btn btn-ghost btn-sm">Voir tout →</a>
</div>
<div class="card" id="print-queue-container">
<div class="text-muted text-sm">Chargement…</div>
</div>
</div>
<!-- Services -->
<div class="section">
<div class="card-title">Services</div>
<div class="card">
<table class="table">
<thead><tr><th>Service</th><th>Statut</th></tr></thead>
<tbody id="services-tbody">
{% for svc in services %}
<tr>
<td>{{ svc.label }}</td>
<td><span class="badge {{ 'badge-success' if svc.active else 'badge-error' }}">{{ 'Actif' if svc.active else 'Arrêté' }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ── Live stats ────────────────────────────────────────────────────────────────
function updateStats(s) {
if (!s) return;
if (s.cpu_percent !== undefined) {
document.getElementById('cpu-val').textContent = s.cpu_percent + '%';
const cpuBar = document.getElementById('cpu-bar');
cpuBar.style.width = s.cpu_percent + '%';
cpuBar.className = 'progress-bar' + (s.cpu_percent > 85 ? ' danger' : s.cpu_percent > 65 ? ' warn' : '');
}
if (s.ram_used_mb !== undefined) {
document.getElementById('ram-val').textContent = s.ram_used_mb + ' Mo';
document.getElementById('ram-sub').textContent = 'sur ' + s.ram_total_mb + ' Mo';
document.getElementById('ram-bar').style.width = s.ram_percent + '%';
}
if (s.cpu_temp !== undefined && s.cpu_temp !== null) {
document.getElementById('temp-val').textContent = s.cpu_temp + '°C';
}
if (s.disk_used_gb !== undefined) {
document.getElementById('disk-val').textContent = s.disk_used_gb + ' Go';
document.getElementById('disk-sub').textContent = 'sur ' + s.disk_total_gb + ' Go';
document.getElementById('disk-bar').style.width = s.disk_percent + '%';
}
}
// ── LED ───────────────────────────────────────────────────────────────────────
function updateLed(effect) {
const ring = document.getElementById('led-ring');
ring.className = 'led-ring ' + effect;
document.getElementById('led-label').textContent = effect;
}
async function setLedEffect(effect) {
try {
await api('POST', `/api/leds/effect?effect=${effect}`);
updateLed(effect);
} catch(e) { showToast('Erreur LED', 'error'); }
}
async function setLedColor() {
const hex = document.getElementById('led-color').value;
const r = parseInt(hex.slice(1,3),16);
const g = parseInt(hex.slice(3,5),16);
const b = parseInt(hex.slice(5,7),16);
try {
await api('POST', `/api/leds/color?r=${r}&g=${g}&b=${b}`);
showToast('Couleur appliquée', 'success');
} catch(e) { showToast('Erreur', 'error'); }
}
// ── Relay ─────────────────────────────────────────────────────────────────────
async function setRelay(state) {
try {
await api('POST', `/api/system/relay?state=${state}`);
document.getElementById('relay-state').textContent = state.toUpperCase();
showToast(`Relay ${state.toUpperCase()}`, 'success');
} catch(e) { showToast('Erreur relay', 'error'); }
}
// ── Simulation bouton ─────────────────────────────────────────────────────────
async function simulateClick(n) {
try {
await api('POST', `/api/system/button/simulate?clicks=${n}`);
showToast(n === 0 ? 'Long press simulé' : `${n} clic(s) simulé(s)`, 'info');
} catch(e) { showToast('Erreur simulation', 'error'); }
}
// ── Mode impression ────────────────────────────────────────────────────────────
document.getElementById('print-mode-select').addEventListener('change', async (e) => {
try {
await api('POST', `/api/print/mode?mode=${e.target.value}`);
showToast(`Mode: ${e.target.value}`, 'success');
} catch(err) { showToast('Erreur changement mode', 'error'); }
});
// ── File d'impression ─────────────────────────────────────────────────────────
async function loadQueue() {
try {
const data = await api('GET', '/api/print/queue?status=pending');
const queue = data.queue || [];
document.getElementById('queue-count').textContent = queue.length;
const container = document.getElementById('print-queue-container');
if (queue.length === 0) {
container.innerHTML = '<div class="text-muted text-sm">Aucune impression en attente</div>';
return;
}
container.innerHTML = `
<table class="table">
<thead><tr><th>Aperçu</th><th>Fichier</th><th>Copies</th><th>Demandé le</th><th>Actions</th></tr></thead>
<tbody>
${queue.slice(0,5).map(q => `
<tr>
<td><img src="${q.thumb_url}" class="print-thumb" onerror="this.style.display='none'"></td>
<td class="text-sm">${q.filename.split('/').pop()}</td>
<td>${q.copies}</td>
<td class="text-xs text-muted">${new Date(q.requested_at*1000).toLocaleTimeString('fr-FR')}</td>
<td>
<button class="btn btn-success btn-sm" onclick="executePrint('${q.id}',${q.copies})">🖨 Imprimer</button>
<button class="btn btn-danger btn-sm" onclick="cancelPrint('${q.id}')">✕</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
} catch(e) {}
}
async function executePrint(id, copies) {
try {
const r = await api('POST', `/api/print/execute/${id}?copies=${copies}`);
showToast(r.success ? '✅ Impression lancée' : '❌ ' + r.error, r.success ? 'success' : 'error');
loadQueue();
} catch(e) { showToast('Erreur impression', 'error'); }
}
async function cancelPrint(id) {
try {
await api('POST', `/api/print/cancel/${id}`);
showToast('Annulé', 'info');
loadQueue();
} catch(e) { showToast('Erreur annulation', 'error'); }
}
// ── WebSocket handler ─────────────────────────────────────────────────────────
function onWsMessage(msg) {
switch(msg.type) {
case 'system_stats': updateStats(msg.data); break;
case 'led_effect': updateLed(msg.effect); break;
case 'button_event':
document.getElementById('last-button-event').textContent =
`Dernier événement : ${msg.clicks === 0 ? 'Long press' : msg.clicks + ' clic(s)'}${msg.action || ''}`;
break;
case 'print_request': loadQueue(); break;
case 'print_result': loadQueue(); break;
}
}
// ── Init ──────────────────────────────────────────────────────────────────────
loadQueue();
setInterval(loadQueue, 15000);
</script>
{% endblock %}