Files

493 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZenAI KPC Snapshots</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&family=Orbitron:wght@400;500;700;900&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-deep: #06080e;
--bg-surface: rgba(12, 16, 28, 0.9);
--border-glow: rgba(0, 240, 255, 0.12);
--accent: #00f0ff;
--accent2: #7b2fff;
--accent3: #ff2d78;
--accent4: #00ff88;
--text-primary: #dce2f0;
--text-secondary: #6b7394;
--glass: rgba(12, 16, 32, 0.7);
--cell-bg: rgba(255, 255, 255, 0.02);
--cell-border: rgba(255, 255, 255, 0.05);
--divider: rgba(255, 255, 255, 0.05);
--input-bg: rgba(0, 0, 0, 0.4);
--radius: 18px;
}
[data-theme="light"] {
--bg-deep: #eef1f5;
--bg-surface: rgba(255, 255, 255, 0.9);
--border-glow: rgba(0, 160, 200, 0.15);
--accent: #0090c0;
--accent2: #6a20e0;
--accent3: #d02060;
--accent4: #00a060;
--text-primary: #1a1d30;
--text-secondary: #5a6088;
--glass: rgba(255, 255, 255, 0.8);
--cell-bg: rgba(0, 0, 0, 0.03);
--cell-border: rgba(0, 0, 0, 0.06);
--divider: rgba(0, 0, 0, 0.06);
--input-bg: rgba(0, 0, 0, 0.05);
}
body {
font-family: 'JetBrains Mono', monospace;
background: var(--bg-deep);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
.bg-grid {
position: fixed; inset: 0; z-index: 0;
background-image:
linear-gradient(rgba(0, 240, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 240, 255, 0.025) 1px, transparent 1px);
background-size: 64px 64px;
pointer-events: none;
}
.orb {
position: fixed; border-radius: 50%; filter: blur(140px); pointer-events: none;
}
.orb-1 { width: 600px; height: 600px; background: rgba(0, 240, 255, 0.04); top: -250px; left: -150px; }
.orb-2 { width: 500px; height: 500px; background: rgba(123, 47, 255, 0.04); bottom: -200px; right: -100px; }
.container {
position: relative; z-index: 1;
max-width: 1600px; margin: 0 auto; padding: 24px 20px;
}
.header {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 28px; margin-bottom: 24px;
background: var(--glass);
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border: 1px solid var(--border-glow); border-radius: var(--radius);
}
.header-left { display: flex; align-items: center; gap: 14px; }
.logo-icon {
width: 44px; height: 44px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
border-radius: 11px; font-size: 22px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 0 28px rgba(0, 240, 255, 0.35);
}
.header h1 {
font-family: 'Orbitron', sans-serif; font-size: 20px; font-weight: 700; letter-spacing: 3px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.header-sub { font-size: 10px; color: var(--text-secondary); letter-spacing: 2px; text-transform: uppercase; }
.header-right { display: flex; align-items: center; gap: 14px; }
.live-chip {
display: flex; align-items: center; gap: 8px;
font-size: 9px; letter-spacing: 1.5px; padding: 5px 12px; border-radius: 12px;
font-weight: 600; text-transform: uppercase;
background: rgba(0, 255, 136, 0.1); color: var(--accent4);
border: 1px solid rgba(0, 255, 136, 0.2);
}
.live-chip .dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--accent4); box-shadow: 0 0 8px var(--accent4);
animation: blink 1.5s ease-in-out infinite;
}
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
.nav-link {
display: inline-flex; align-items: center; gap: 6px;
background: none; border: 1px solid var(--cell-border);
border-radius: 8px; padding: 7px 12px; cursor: pointer;
font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 1px;
color: var(--text-secondary); text-decoration: none;
transition: border-color 0.3s, color 0.3s, background 0.3s;
}
.nav-link:hover { border-color: var(--accent); color: var(--text-primary); background: var(--cell-bg); }
.nav-link.active { border-color: var(--accent); color: var(--accent); }
.theme-toggle {
background: none; border: 1px solid var(--cell-border);
border-radius: 8px; padding: 6px 10px; cursor: pointer;
font-size: 16px; line-height: 1;
transition: border-color 0.3s, background 0.3s;
}
.theme-toggle:hover { border-color: var(--accent); background: var(--cell-bg); }
.section-title {
font-family: 'Orbitron', sans-serif; font-size: 13px; font-weight: 600; letter-spacing: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
display: inline-block;
}
.panel {
background: var(--glass);
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
border: 1px solid var(--border-glow); border-radius: var(--radius);
padding: 24px 26px; margin-bottom: 24px;
}
.toolbar {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 20px; flex-wrap: wrap; gap: 12px;
}
.toolbar-group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.filters { display: flex; gap: 6px; }
.filters button {
font-family: 'JetBrains Mono', monospace; font-size: 10px;
letter-spacing: 1px; padding: 6px 14px; border-radius: 7px;
border: 1px solid var(--cell-border);
background: var(--cell-bg); color: var(--text-secondary);
cursor: pointer; transition: all 0.3s;
}
.filters button:hover { border-color: var(--accent); color: var(--text-primary); }
.filters button.active {
background: linear-gradient(135deg, var(--accent), var(--accent2));
color: #000; border-color: transparent; font-weight: 600;
}
input[type="date"] {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
background: var(--input-bg); color: var(--text-primary);
border: 1px solid var(--cell-border); border-radius: 7px;
padding: 7px 12px; outline: none; transition: border-color 0.3s;
}
input[type="date"]:focus { border-color: var(--accent); }
.btn {
font-family: 'Orbitron', sans-serif; font-size: 10px; letter-spacing: 2px;
padding: 8px 16px; border-radius: 7px; cursor: pointer; font-weight: 600;
border: none; transition: opacity 0.3s, box-shadow 0.3s;
}
.btn-primary { background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #000; }
.btn-primary:hover { opacity: 0.85; box-shadow: 0 0 20px rgba(0, 240, 255, 0.3); }
.btn-ghost {
background: var(--cell-bg); color: var(--text-secondary);
border: 1px solid var(--cell-border);
}
.btn-ghost:hover { border-color: var(--accent); color: var(--text-primary); }
.snapshot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 14px;
}
.snap-card {
background: var(--cell-bg);
border: 1px solid var(--cell-border); border-radius: 12px;
overflow: hidden; cursor: pointer;
transition: border-color 0.2s, transform 0.15s;
}
.snap-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.snap-thumb {
width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; background: #000;
}
.snap-info { padding: 8px 10px; }
.snap-info .si-top {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;
}
.snap-kind {
font-size: 8px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase;
padding: 2px 8px; border-radius: 8px;
}
.snap-kind.left { background: rgba(0,255,136,0.12); color: var(--accent4); }
.snap-kind.right { background: rgba(255,45,120,0.12); color: var(--accent3); }
.snap-kind.zone_1 { background: rgba(0,255,136,0.12); color: var(--accent4); }
.snap-kind.zone_2 { background: rgba(255,45,120,0.12); color: var(--accent3); }
.snap-kind.detect { background: rgba(0,240,255,0.10); color: var(--accent); }
.snap-tid {
font-family: 'Orbitron', sans-serif; font-size: 11px; font-weight: 700; color: var(--text-primary);
}
.snap-time { font-size: 9px; color: var(--text-secondary); }
.empty {
text-align: center; padding: 48px; color: var(--text-secondary);
}
.empty .e-icon { font-size: 42px; opacity: 0.2; margin-bottom: 12px; }
.count-note { font-size: 10px; color: var(--text-secondary); letter-spacing: 1px; }
.modal-overlay {
position: fixed; inset: 0; z-index: 100;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center; padding: 20px;
}
.modal-overlay.hidden { display: none; }
.close-btn {
background: none; border: 1px solid var(--cell-border); color: var(--text-secondary);
width: 32px; height: 32px; border-radius: 8px; cursor: pointer;
font-size: 16px; display: flex; align-items: center; justify-content: center;
transition: border-color 0.3s, color 0.3s;
}
.close-btn:hover { border-color: var(--accent3); color: var(--accent3); }
.snap-lightbox {
position: relative;
background: var(--bg-surface);
border: 1px solid var(--border-glow); border-radius: var(--radius);
padding: 14px; max-width: 92vw; max-height: 90vh;
display: flex; flex-direction: column; align-items: center;
}
.snap-lightbox img { max-width: 88vw; max-height: 78vh; border-radius: 10px; display: block; }
.snap-modal-meta {
margin-top: 10px; font-size: 11px; color: var(--text-secondary); letter-spacing: 1px; text-align: center;
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--text-secondary); border-radius: 3px; }
.footer {
margin-top: 24px; text-align: center;
font-size: 9px; letter-spacing: 2px; color: var(--text-secondary); text-transform: uppercase;
}
@media (max-width: 768px) {
.container { padding: 16px 12px; }
.header { flex-direction: column; gap: 10px; padding: 14px 20px; }
.header-right { flex-wrap: wrap; justify-content: center; }
.toolbar { flex-direction: column; align-items: flex-start; }
}
</style>
</head>
<body>
<div class="bg-grid"></div>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-left">
<div class="logo-icon">&#9670;</div>
<div>
<h1>ZenAI KPC Snapshots</h1>
<div class="header-sub">Detected &amp; counted frames — correlate by track ID</div>
</div>
</div>
<div class="header-right">
<div class="live-chip">
<span class="dot"></span>
<span>{{ site_name }}</span>
</div>
<a class="nav-link" href="/"><i class="fas fa-gauge-high"></i> DASHBOARD</a>
<a class="nav-link active" href="/snapshots"><i class="fas fa-images"></i> SNAPSHOTS</a>
<button class="theme-toggle" id="themeToggle" onclick="toggleTheme()" title="Toggle theme">☀️</button>
</div>
</div>
<!-- Snapshots -->
<div class="panel">
<div class="toolbar">
<div>
<span class="section-title">&#9673; Zone Entry Snapshots</span>
<div class="count-note" id="snap-count-note">--</div>
</div>
<div class="toolbar-group">
<div class="filters" id="snap-filters">
<button class="active" data-kind="all" onclick="loadSnapshots('all')">ALL</button>
{% for z in zones %}
<button data-kind="{{ z.id }}" onclick="loadSnapshots('{{ z.id }}')">{{ z.name | upper }}</button>
{% endfor %}
{% if show_detect %}<button data-kind="detect" onclick="loadSnapshots('detect')">DETECT</button>{% endif %}
</div>
<input type="number" id="snap-tid-filter" placeholder="Track ID" min="0"
style="width:110px;font-family:'JetBrains Mono',monospace;font-size:11px;background:var(--input-bg);color:var(--text-primary);border:1px solid var(--cell-border);border-radius:7px;padding:7px 12px;outline:none;">
<input type="date" id="snap-date-filter">
<button class="btn btn-primary" onclick="applySnapFilter()">FILTER</button>
<button class="btn btn-ghost" onclick="resetSnapFilter()">RESET</button>
</div>
</div>
<div id="snapshot-grid" class="snapshot-grid">
<!-- Populated by JS -->
</div>
<div id="snap-sentinel" style="height:1px;"></div>
<div id="snapshot-empty" class="empty" style="display:none;">
<div class="e-icon">&#9635;</div>
<div style="font-size:10px;letter-spacing:1px;">NO SNAPSHOTS AVAILABLE</div>
<div style="font-size:9px;margin-top:6px;opacity:0.7;">Enable SAVE_CROSS_SNAPSHOT{% if show_detect %} / SAVE_DETECT_SNAPSHOT{% endif %} in counter .env</div>
</div>
</div>
<div class="footer">Edge Nano Counter by ZenAi</div>
</div>
<!-- Snapshot lightbox -->
<div class="modal-overlay hidden" id="snap-modal" onclick="closeSnapModal(event)">
<div class="snap-lightbox" id="snap-lightbox">
<button class="close-btn" onclick="closeSnapModal()" style="position:absolute;top:12px;right:12px;z-index:2;">&times;</button>
<img id="snap-modal-img" alt="Snapshot" />
<div class="snap-modal-meta" id="snap-modal-meta">--</div>
</div>
</div>
<script>
// --- Theme ---
(function() {
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
updateToggleIcon();
})();
function toggleTheme() {
const el = document.documentElement;
const current = el.getAttribute('data-theme');
const next = current === 'light' ? '' : 'light';
if (next) el.setAttribute('data-theme', next);
else el.removeAttribute('data-theme');
localStorage.setItem('theme', next || 'dark');
updateToggleIcon();
}
function updateToggleIcon() {
const btn = document.getElementById('themeToggle');
if (!btn) return;
btn.textContent = document.documentElement.getAttribute('data-theme') === 'light' ? '🌙' : '☀️';
}
// --- Snapshots ---
let snapCurrentKind = 'all';
let snapDateFilter = '';
let snapTidFilter = '';
let snapItems = []; // all fetched metadata
let snapRendered = 0; // how many cards are in the DOM
const SNAP_BATCH = 60; // cards added per chunk
let snapObserver = null;
document.addEventListener('DOMContentLoaded', () => {
const sentinel = document.getElementById('snap-sentinel');
snapObserver = new IntersectionObserver((entries) => {
if (entries.some(e => e.isIntersecting)) renderNextBatch();
}, { rootMargin: '600px' });
if (sentinel) snapObserver.observe(sentinel);
// Filter immediately when date changes or track ID is typed.
document.getElementById('snap-date-filter').addEventListener('change', applySnapFilter);
let tidDebounce = null;
document.getElementById('snap-tid-filter').addEventListener('input', () => {
clearTimeout(tidDebounce);
tidDebounce = setTimeout(applySnapFilter, 350);
});
loadSnapshots('all');
setInterval(() => {
if (snapCurrentKind && !snapDateFilter && !snapTidFilter) loadSnapshots(snapCurrentKind, true);
}, 15000);
});
function snapCardHtml(s) {
const imgUrl = `/api/snapshot-image/${s.file}`;
const t = new Date(s.timestamp);
const timeStr = t.toLocaleString('en-US', { hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' });
const kindLabel = s.kind.toUpperCase();
return `
<div class="snap-card" onclick="openSnapModal('${imgUrl}','${kindLabel}',${s.track_id},'${timeStr}',${s.frame})">
<img class="snap-thumb" src="${imgUrl}" loading="lazy" decoding="async" alt="snapshot" />
<div class="snap-info">
<div class="si-top">
<span class="snap-kind ${s.kind}">${kindLabel}</span>
<span class="snap-tid">ID ${s.track_id}</span>
</div>
<div class="snap-time">${timeStr}</div>
</div>
</div>`;
}
function renderNextBatch() {
if (snapRendered >= snapItems.length) return;
const grid = document.getElementById('snapshot-grid');
const next = snapItems.slice(snapRendered, snapRendered + SNAP_BATCH);
grid.insertAdjacentHTML('beforeend', next.map(snapCardHtml).join(''));
snapRendered += next.length;
updateSnapNote();
}
function updateSnapNote() {
const note = document.getElementById('snap-count-note');
note.textContent = `Showing ${snapRendered} of ${snapItems.length}`;
}
async function loadSnapshots(kind, silent) {
if (kind !== undefined) snapCurrentKind = kind;
document.querySelectorAll('#snap-filters button').forEach(b => {
b.classList.toggle('active', b.dataset.kind === snapCurrentKind);
});
try {
let url = `/api/snapshots?kind=${snapCurrentKind}&limit=5000`;
if (snapDateFilter) url += `&date=${snapDateFilter}`;
if (snapTidFilter) url += `&track_id=${snapTidFilter}`;
const res = await fetch(url);
const data = await res.json();
const grid = document.getElementById('snapshot-grid');
const empty = document.getElementById('snapshot-empty');
snapItems = (data && data.snapshots) || [];
snapRendered = 0;
grid.innerHTML = '';
if (snapItems.length === 0) {
empty.style.display = 'block';
document.getElementById('snap-count-note').textContent = '';
return;
}
empty.style.display = 'none';
renderNextBatch();
} catch (err) {
if (!silent) console.error('Failed to load snapshots:', err);
}
}
function applySnapFilter() {
snapDateFilter = document.getElementById('snap-date-filter').value || '';
snapTidFilter = document.getElementById('snap-tid-filter').value || '';
loadSnapshots(snapCurrentKind);
}
function resetSnapFilter() {
snapDateFilter = '';
snapTidFilter = '';
document.getElementById('snap-date-filter').value = '';
document.getElementById('snap-tid-filter').value = '';
snapCurrentKind = 'all';
loadSnapshots('all');
}
function openSnapModal(url, kind, tid, timeStr, frame) {
document.getElementById('snap-modal-img').src = url;
document.getElementById('snap-modal-meta').textContent =
`${kind} \u00b7 Track ID ${tid} \u00b7 Frame ${frame} \u00b7 ${timeStr}`;
document.getElementById('snap-modal').classList.remove('hidden');
}
function closeSnapModal(event) {
if (event && event.target && event.target.id !== 'snap-modal' && event.type === 'click') return;
document.getElementById('snap-modal').classList.add('hidden');
document.getElementById('snap-modal-img').src = '';
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSnapModal();
});
</script>
</body>
</html>