Files
camwatch/app/templates/event_detail.html
T
percoandClaude Sonnet 4.6 ea58a860b8 Add vehicle whitelist and per-plate passage statistics
Whitelist: plates added to /whitelist are silently ignored on
detection — no clip, no frames, no DB entry. Manageable via
/whitelist page or from the event detail page per-event.

Stats: /stats shows all known plates ranked by passage count,
with first/last seen dates and one-click whitelist toggle.

Navigation links added to the index header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:33:53 +02:00

237 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CamWatch — {{ ev.time_str }}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwind.config = { darkMode: 'class' }</script>
<style>
body { background: #0f172a; color: #e2e8f0; }
.card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; }
.plate { font-family: monospace; letter-spacing: 0.15em; }
.btn-ghost { background: #1e293b; border: 1px solid #475569; color: #94a3b8; border-radius: 6px; padding: 6px 14px; font-size: 0.85rem; text-decoration: none; display: inline-block; }
.btn-ghost:hover { background: #334155; color: #e2e8f0; }
.btn-dl { background: #166534; border: 1px solid #16a34a; color: #4ade80; border-radius: 6px; padding: 6px 14px; font-size: 0.85rem; text-decoration: none; display: inline-block; }
.btn-dl:hover { background: #14532d; }
.frame-thumb { cursor: zoom-in; border: 2px solid #334155; border-radius: 6px; overflow: hidden; transition: border-color 0.12s; }
.frame-thumb:hover { border-color: #60a5fa; }
.frame-thumb.active { border-color: #3b82f6; }
label { color: #64748b; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
/* Lightbox */
#lightbox { display:none; position:fixed; inset:0; z-index:50; background:rgba(0,0,0,0.92); align-items:center; justify-content:center; }
#lightbox.open { display:flex; }
#lightbox img { max-width:95vw; max-height:92vh; object-fit:contain; border-radius:4px; }
#lb-nav { position:absolute; bottom:16px; left:50%; transform:translateX(-50%); display:flex; gap:12px; align-items:center; }
#lb-close { position:absolute; top:12px; right:16px; font-size:1.8rem; color:#94a3b8; cursor:pointer; line-height:1; }
#lb-close:hover { color:#fff; }
#lb-label { color:#94a3b8; font-size:0.85rem; }
</style>
</head>
<body class="min-h-screen">
<header class="sticky top-0 z-10 px-4 py-3 flex items-center gap-3" style="background:#0f172a;border-bottom:1px solid #1e293b;">
<a href="/" class="btn-ghost">← Retour</a>
<span class="text-xl">🚗</span>
<span class="font-bold flex-1">{{ ev.time_str }}</span>
{% if ev.plate %}
{% if is_whitelisted %}
<form action="/whitelist/remove/{{ ev.plate }}" method="post">
<input type="hidden" name="back" value="/event/{{ ev.id }}">
<button type="submit" class="btn-ghost text-green-400 hover:text-green-300 hover:border-green-600">🛡 Connu ✓</button>
</form>
{% else %}
<form action="/whitelist/add" method="post">
<input type="hidden" name="plate" value="{{ ev.plate }}">
<input type="hidden" name="back" value="/event/{{ ev.id }}">
<button type="submit" class="btn-ghost hover:text-green-400 hover:border-green-700">🛡 Whitelister</button>
</form>
{% endif %}
{% endif %}
<form action="/event/{{ ev.id }}/delete" method="post"
onsubmit="return confirm('Supprimer cet événement ?')">
<button type="submit" class="btn-ghost text-red-400 hover:text-red-300 hover:border-red-500">✕ Supprimer</button>
</form>
</header>
<main class="max-w-5xl mx-auto px-3 py-5 space-y-4">
<!-- Info + main image -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="card p-4 space-y-4">
<div>
<label>Plaque</label>
<div class="mt-1 mb-2">
{% if ev.plate %}
<span class="plate text-xl font-bold px-4 py-2 rounded" style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;">{{ ev.plate }}</span>
{% else %}
<span class="text-slate-500 italic text-sm">Non lue automatiquement</span>
{% endif %}
</div>
{% if plate_crop %}
<div class="mb-2">
<label>Détection brute (4×)</label>
<img src="/{{ plate_crop }}" class="mt-1 rounded border border-slate-600 w-full" style="image-rendering:pixelated;">
</div>
{% endif %}
{% if plate_ocr %}
<div class="mb-2">
<label>Image utilisée pour l'OCR</label>
<img src="/{{ plate_ocr }}" class="mt-1 rounded border border-blue-700 w-full" style="image-rendering:pixelated;">
</div>
{% endif %}
<form action="/event/{{ ev.id }}/plate" method="post" class="flex gap-2 mt-1">
<input type="text" name="plate" value="{{ ev.plate or '' }}"
placeholder="Saisir plaque…"
class="flex-1 text-sm font-mono uppercase"
style="background:#0f172a;border:1px solid #475569;color:#e2e8f0;border-radius:6px;padding:5px 8px;">
<button type="submit" class="text-xs px-3 py-1 rounded" style="background:#1e3a5f;color:#60a5fa;border:1px solid #2563eb;"></button>
</form>
</div>
<div>
<label>Couleur</label>
<div class="flex items-center gap-2 mt-1">
<div class="w-7 h-7 rounded-full border-2 border-slate-600" style="background:{{ ev.color_hex or '#808080' }};"></div>
<span class="font-medium">{{ ev.color_name or '—' }}</span>
<span class="text-slate-600 text-xs font-mono">{{ ev.color_hex or '' }}</span>
</div>
</div>
<div class="pt-2 border-t border-slate-700">
<label>Debug</label>
<div class="mt-1 space-y-0.5 text-xs font-mono text-slate-400">
<div>id: {{ ev.id[:8] }}…</div>
<div>frames: {{ frames|length }} (~{{ capture_total }} capturées)</div>
<div>clip: {% if ev.clip_path %}✓ {{ clip_size }}{% else %}—{% endif %}</div>
</div>
</div>
{% if has_clip %}
<a href="/{{ ev.clip_path }}" download class="btn-dl w-full text-center">⬇ Télécharger le clip</a>
{% endif %}
</div>
<div class="md:col-span-2 card overflow-hidden">
<div class="bg-black relative cursor-zoom-in" style="aspect-ratio:16/9;"
onclick="openLightbox('/{{ ev.snapshot_path }}', 'Meilleure frame ★')">
<img id="main-img"
src="{% if ev.snapshot_path %}/{{ ev.snapshot_path }}{% endif %}"
class="w-full h-full object-contain">
<span class="absolute bottom-2 right-2 text-xs text-slate-500">🔍 cliquer pour agrandir</span>
</div>
<div class="px-3 py-2 text-xs text-slate-500 text-center" id="img-label">Meilleure frame ★</div>
</div>
</div>
<!-- Video clip -->
{% if has_clip %}
<div class="card p-4">
<div class="flex items-center justify-between mb-3">
<label>Clip vidéo — {{ capture_duration }}s</label>
<a href="/{{ ev.clip_path }}" download class="btn-dl text-xs py-1 px-3">⬇ Télécharger</a>
</div>
<video controls preload="metadata" class="w-full rounded-lg" style="max-height:380px;background:#000;">
<source src="/{{ ev.clip_path }}" type="video/mp4">
<p class="text-slate-400 text-sm p-4">Lecture impossible dans le navigateur —
<a href="/{{ ev.clip_path }}" download class="text-blue-400 underline">télécharger le clip</a></p>
</video>
</div>
{% else %}
<div class="card p-4 text-slate-500 text-sm">
<label class="block mb-1">Clip vidéo</label>
{% if ev.clip_path %}Fichier introuvable{% else %}Événement antérieur à la fonctionnalité clip{% endif %}
</div>
{% endif %}
<!-- All frames -->
{% if frames %}
<div class="card p-4">
<label class="block mb-3">
Toutes les frames — {{ frames|length }} extraites à {{ capture_fps }}fps, triées meilleure plaque en premier
</label>
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-1.5">
{% for frame_path in frames %}
<div class="frame-thumb {% if loop.first %}active{% endif %}"
onclick="selectFrame(this, '/{{ frame_path }}', 'Frame #{{ loop.index }}{% if loop.first %} ★{% endif %}')"
ondblclick="openLightbox('/{{ frame_path }}', 'Frame #{{ loop.index }}')">
<img src="/{{ frame_path }}" class="w-full object-cover" style="aspect-ratio:16/9;" loading="lazy">
<div class="text-center text-xs text-slate-500 py-0.5 bg-slate-900 leading-none">
#{{ loop.index }}{% if loop.first %} ★{% endif %}
</div>
</div>
{% endfor %}
</div>
<p class="text-xs text-slate-600 mt-2">Clic simple → aperçu | Double-clic → plein écran</p>
</div>
{% else %}
<div class="card p-4 text-slate-500 text-sm">
<label class="block mb-1">Frames</label>
Événement antérieur à la fonctionnalité frames
</div>
{% endif %}
</main>
<!-- Lightbox -->
<div id="lightbox" onclick="closeLightboxIfOutside(event)">
<span id="lb-close" onclick="closeLightbox()">×</span>
<img id="lb-img" src="" alt="">
<div id="lb-nav">
<button onclick="lbPrev()" class="btn-ghost px-4"></button>
<span id="lb-label" class="text-slate-400 text-sm"></span>
<button onclick="lbNext()" class="btn-ghost px-4"></button>
</div>
</div>
<script>
let lbFrames = {{ frames | tojson }};
let lbIdx = 0;
function openLightbox(src, label) {
document.getElementById('lb-img').src = src;
document.getElementById('lb-label').textContent = label;
// Find index in frames array
const rel = src.startsWith('/') ? src.slice(1) : src;
lbIdx = lbFrames.indexOf(rel);
document.getElementById('lightbox').classList.add('open');
}
function closeLightbox() {
document.getElementById('lightbox').classList.remove('open');
}
function closeLightboxIfOutside(e) {
if (e.target.id === 'lightbox') closeLightbox();
}
function lbGo(idx) {
if (lbFrames.length === 0) return;
lbIdx = ((idx % lbFrames.length) + lbFrames.length) % lbFrames.length;
document.getElementById('lb-img').src = '/' + lbFrames[lbIdx];
document.getElementById('lb-label').textContent = 'Frame #' + (lbIdx + 1) + (lbIdx === 0 ? ' ★' : '');
}
function lbPrev() { lbGo(lbIdx - 1); }
function lbNext() { lbGo(lbIdx + 1); }
function selectFrame(el, src, label) {
document.getElementById('main-img').src = src;
document.getElementById('main-img').parentElement.onclick = () => openLightbox(src, label);
document.getElementById('img-label').textContent = label;
document.querySelectorAll('.frame-thumb').forEach(t => t.classList.remove('active'));
el.classList.add('active');
}
document.addEventListener('keydown', e => {
if (!document.getElementById('lightbox').classList.contains('open')) return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') lbPrev();
if (e.key === 'ArrowRight') lbNext();
});
</script>
</body>
</html>