282 lines
13 KiB
HTML
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 %}
|