This commit is contained in:
proitlab committed 2026-07-10 00:45:53 +07:00
1 parent 91786b07c1
commit 660b9abef4
4 files changed
+528 -182

No files matched your search

+29 -179
View File
@@ -149,6 +149,17 @@
}
.theme-toggle:hover { border-color: var(--accent); background: var(--cell-bg); }
.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); }
/* Section title */
.section-title {
font-family: 'Orbitron', sans-serif; font-size: 13px; font-weight: 600; letter-spacing: 2px;
@@ -406,55 +417,6 @@
}
.table-toolbar .toolbar-group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Snapshot gallery */
.snapshot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 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.in { background: rgba(0,255,136,0.12); color: var(--accent4); }
.snap-kind.out { 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); }
.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;
}
.table-toolbar input[type="date"] {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
background: var(--input-bg); color: var(--text-primary);
@@ -645,6 +607,7 @@
<span class="dot"></span>
<span>{{ site_name }}</span>
</div>
<a class="nav-link" href="/snapshots" title="Snapshots"><i class="fas fa-images"></i> SNAPSHOTS</a>
<button class="theme-toggle" id="themeToggle" onclick="toggleTheme()" title="Toggle theme">☀️</button>
</div>
</div>
@@ -655,7 +618,7 @@
<span class="s-chip live-chip-tag"><span class="ldot"></span>LIVE</span>
<div class="s-icon primary"><i class="fas fa-calendar-day"></i></div>
<div class="s-val" id="today-count">--</div>
<div class="s-label">Total Count Today</div>
<div class="s-label">Diff Today (In &minus; Out)</div>
<div class="sub-cards">
<div class="sub-card in">
<div class="sc-head"><i class="fas fa-arrow-right-to-bracket"></i> In</div>
@@ -671,7 +634,7 @@
<span class="s-chip info">Yesterday</span>
<div class="s-icon warm"><i class="fas fa-history"></i></div>
<div class="s-val" id="yesterday-count">--</div>
<div class="s-label">Total Count Yesterday</div>
<div class="s-label">Diff Yesterday (In &minus; Out)</div>
<div class="sub-cards">
<div class="sub-card in">
<div class="sc-head"><i class="fas fa-arrow-right-to-bracket"></i> In</div>
@@ -749,8 +712,8 @@
<div class="qs-left">
<div class="qs-icon primary"><i class="fas fa-layer-group"></i></div>
<div>
<div class="qs-label">Grand Total</div>
<div class="qs-sub">All time</div>
<div class="qs-label">Grand Diff</div>
<div class="qs-sub">All time (In &minus; Out)</div>
</div>
</div>
<span class="qs-val" id="grand-total">--</span>
@@ -805,7 +768,7 @@
<thead>
<tr>
<th style="padding-left:4px;">Date</th>
<th>Total Count</th>
<th>Diff (In &minus; Out)</th>
<th>In</th>
<th>Out</th>
<th style="padding-right:4px;">Status</th>
@@ -818,47 +781,9 @@
</div>
</div>
<!-- Snapshots -->
<div class="table-panel">
<div class="table-toolbar">
<div>
<span class="section-title" style="margin-bottom:0;">&#9673; Crossing Snapshots</span>
<div style="font-size:10px;color:var(--text-secondary);letter-spacing:1px;">Detected & counted frames — correlate by track ID</div>
</div>
<div class="toolbar-group">
<div class="chart-filters" id="snap-filters">
<button class="active" data-kind="all" onclick="loadSnapshots('all')">ALL</button>
<button data-kind="in" onclick="loadSnapshots('in')">IN</button>
<button data-kind="out" onclick="loadSnapshots('out')">OUT</button>
{% if show_detect %}<button data-kind="detect" onclick="loadSnapshots('detect')">DETECT</button>{% endif %}
</div>
<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="snapshot-empty" style="display:none;text-align:center;padding:32px;color:var(--text-secondary);">
<div style="font-size:32px;opacity:0.2;margin-bottom:8px;">&#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() {
@@ -893,7 +818,6 @@ document.addEventListener('DOMContentLoaded', () => {
loadChartData(7);
loadRecentDays();
resetFilter();
loadSnapshots('all');
setInterval(() => {
loadSummary();
@@ -903,10 +827,6 @@ document.addEventListener('DOMContentLoaded', () => {
setInterval(() => {
loadCurrentCounter();
}, 2000);
setInterval(() => {
if (snapCurrentKind && !snapDateFilter) loadSnapshots(snapCurrentKind, true);
}, 15000);
});
async function loadCurrentCounter() {
@@ -915,9 +835,11 @@ async function loadCurrentCounter() {
if (!res.ok) throw new Error('No active counter');
const data = await res.json();
document.getElementById('today-count').textContent = (data.count || 0).toLocaleString();
document.getElementById('today-in').textContent = (data.count_in || 0).toLocaleString();
document.getElementById('today-out').textContent = (data.count_out || 0).toLocaleString();
const tin = data.count_in || 0;
const tout = data.count_out || 0;
document.getElementById('today-count').textContent = (tin - tout).toLocaleString();
document.getElementById('today-in').textContent = tin.toLocaleString();
document.getElementById('today-out').textContent = tout.toLocaleString();
} catch (err) {
document.getElementById('today-count').textContent = '0';
document.getElementById('today-in').textContent = '0';
@@ -932,7 +854,7 @@ async function loadSummary() {
document.getElementById('current-date').textContent = data.today.date;
document.getElementById('yesterday-count').textContent = data.yesterday.total_count.toLocaleString();
document.getElementById('yesterday-count').textContent = (data.yesterday.total_in - data.yesterday.total_out).toLocaleString();
document.getElementById('yesterday-in').textContent = data.yesterday.total_in.toLocaleString();
document.getElementById('yesterday-out').textContent = data.yesterday.total_out.toLocaleString();
@@ -942,7 +864,7 @@ async function loadSummary() {
document.getElementById('best-count').textContent = data.best_day.count.toLocaleString();
document.getElementById('best-date').textContent = data.best_day.date || '--';
document.getElementById('grand-total').textContent = data.all_time.grand_total.toLocaleString();
document.getElementById('grand-total').textContent = (data.all_time.grand_in - data.all_time.grand_out).toLocaleString();
document.getElementById('grand-in').textContent = data.all_time.grand_in.toLocaleString();
document.getElementById('grand-out').textContent = data.all_time.grand_out.toLocaleString();
@@ -965,7 +887,7 @@ async function loadChartData(days) {
const date = new Date(d.date);
return date.toLocaleDateString('en-US', { weekday: 'short', day: 'numeric', month: 'short' });
});
const counts = data.map(d => d.total_count);
const counts = data.map(d => d.total_in - d.total_out);
const ins = data.map(d => d.total_in);
const outs = data.map(d => d.total_out);
@@ -1001,7 +923,7 @@ async function loadChartData(days) {
yAxisID: 'y'
},
{
label: 'Total',
label: 'Diff',
data: counts,
type: 'line',
borderColor: 'rgba(0, 240, 255, 1)',
@@ -1072,7 +994,7 @@ async function loadRecentDays() {
</div>
</div>
<div style="text-align:right;">
<div class="ri-count">${d.total_count.toLocaleString()}</div>
<div class="ri-count">${(d.total_in - d.total_out).toLocaleString()}</div>
</div>
</div>
`).join('');
@@ -1125,7 +1047,7 @@ function renderDailyTable(dates) {
</div>
</div>
</td>
<td><span style="font-family:'Orbitron',sans-serif;font-size:15px;font-weight:700;">${d.total_count.toLocaleString()}</span></td>
<td><span style="font-family:'Orbitron',sans-serif;font-size:15px;font-weight:700;">${(d.total_in - d.total_out).toLocaleString()}</span></td>
<td><span class="badge badge-green">${d.total_in.toLocaleString()}</span></td>
<td><span class="badge badge-gray">${d.total_out.toLocaleString()}</span></td>
<td style="padding-right:4px;">
@@ -1156,81 +1078,9 @@ function formatTime(isoString) {
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { hideVideoError(); closeSnapModal(); }
if (e.key === 'Escape') { hideVideoError(); }
});
// --- Crossing Snapshots ---
let snapCurrentKind = 'all';
let snapDateFilter = '';
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=120`;
if (snapDateFilter) url += `&date=${snapDateFilter}`;
const res = await fetch(url);
const data = await res.json();
const grid = document.getElementById('snapshot-grid');
const empty = document.getElementById('snapshot-empty');
const items = (data && data.snapshots) || [];
if (items.length === 0) {
grid.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
grid.innerHTML = items.map(s => {
const url = `/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('${url}','${kindLabel}',${s.track_id},'${timeStr}',${s.frame})">
<img class="snap-thumb" src="${url}" loading="lazy" 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>`;
}).join('');
} catch (err) {
if (!silent) console.error('Failed to load snapshots:', err);
}
}
function applySnapFilter() {
snapDateFilter = document.getElementById('snap-date-filter').value || '';
loadSnapshots(snapCurrentKind);
}
function resetSnapFilter() {
snapDateFilter = '';
document.getElementById('snap-date-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 = '';
}
// --- Live Video Feed ---
function toggleVideo() {