Files
photoBooth/frontend/templates/admin/dashboard.html
T
admin d5f111d60b
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled
feat: gestion événements — historique, export ZIP, filtre galerie
2026-07-17 14:43:05 +02:00

334 lines
15 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</a>
<a href="/admin/actions">Actions</a>
<a href="/admin/print">Impression</a>
<a href="/admin/settings">Réglages</a>
<a href="/admin/events">Événements</a>
<a href="/admin/logout">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>
<!-- Événement en cours -->
<div class="section">
<div class="card-title">🎫 Événement en cours</div>
<div class="card">
<div class="flex gap-2 items-center mb-2" style="flex-wrap:wrap">
<input type="text" id="event-name-input" class="form-control"
placeholder="Nom de l'événement…" style="flex:1;min-width:180px">
<button class="btn btn-primary btn-sm" onclick="saveEvent(false)">💾 Enregistrer</button>
<button class="btn btn-ghost btn-sm" onclick="saveEvent(true)"
title="Terminer cet événement et en démarrer un nouveau (les stats repartent de zéro)">
↻ Nouvel événement
</button>
</div>
<div class="flex gap-3 text-sm" id="event-stats-row">
<span>📸 <strong id="ev-photos"></strong> photos</span>
<span>⬇️ <strong id="ev-downloads"></strong> téléchargements</span>
<span>🖨 <strong id="ev-prints"></strong> impressions</span>
</div>
<div class="text-xs text-muted mt-1" id="event-slug-display"></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;
}
}
// ── Événement ─────────────────────────────────────────────────────────────────
async function loadEvent() {
try {
const e = await api('GET', '/api/event');
document.getElementById('event-name-input').value = e.name || '';
const s = e.stats || {};
document.getElementById('ev-photos').textContent = s.photos_taken ?? '0';
document.getElementById('ev-downloads').textContent = s.downloads ?? '0';
document.getElementById('ev-prints').textContent = s.prints_done ?? '0';
document.getElementById('event-slug-display').textContent =
`Fichiers : ${e.slug}_By_LSDW_YYYY-MM-DD.jpg · Démarré le ${
e.started_at ? new Date(e.started_at * 1000).toLocaleDateString('fr-FR') : '—'
}`;
} catch(err) { console.warn('Événement:', err); }
}
async function saveEvent(isNew) {
const name = document.getElementById('event-name-input').value.trim();
if (!name) { showToast('Entrez un nom d\'événement', 'error'); return; }
if (isNew && !confirm(`Terminer l'événement actuel et démarrer "${name}" ?\n\nLes statistiques de l'ancien événement seront archivées.`)) return;
try {
const r = await api('PUT', '/api/event', {name, new_event: isNew});
showToast(isNew ? `↻ Nouvel événement : ${r.name}` : `✅ Événement : ${r.name}`, 'success');
loadEvent();
} catch(err) { showToast('Erreur sauvegarde événement', 'error'); }
}
// ── Init ──────────────────────────────────────────────────────────────────────
loadQueue();
loadEvent();
setInterval(loadQueue, 15000);
setInterval(loadEvent, 60000);
</script>
{% endblock %}