631 lines
22 KiB
HTML
631 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ site_name }} Recounting</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);
|
|
--card-bg: rgba(255, 255, 255, 0.025);
|
|
--divider: rgba(255, 255, 255, 0.05);
|
|
--input-bg: rgba(0, 0, 0, 0.4);
|
|
--radius: 18px;
|
|
}
|
|
|
|
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;
|
|
animation: gridScroll 20s linear infinite;
|
|
pointer-events: none;
|
|
}
|
|
@keyframes gridScroll {
|
|
0% { background-position: 0 0; }
|
|
100% { background-position: 64px 64px; }
|
|
}
|
|
|
|
.orb {
|
|
position: fixed; border-radius: 50%; filter: blur(140px); pointer-events: none;
|
|
animation: orbFloat 18s ease-in-out infinite;
|
|
}
|
|
.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; animation-delay: -6s; }
|
|
@keyframes orbFloat {
|
|
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
33% { transform: translate(50px, -40px) scale(1.06); }
|
|
66% { transform: translate(-30px, 30px) scale(0.94); }
|
|
}
|
|
|
|
.container {
|
|
position: relative; z-index: 1;
|
|
max-width: 1600px; margin: 0 auto; padding: 24px 20px;
|
|
}
|
|
|
|
/* Header */
|
|
.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(--accent3), var(--accent2));
|
|
border-radius: 11px; font-size: 22px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
box-shadow: 0 0 28px rgba(255, 45, 120, 0.35);
|
|
animation: logoPulse 3s ease-in-out infinite;
|
|
}
|
|
@keyframes logoPulse {
|
|
0%, 100% { box-shadow: 0 0 22px rgba(255, 45, 120, 0.3); }
|
|
50% { box-shadow: 0 0 44px rgba(255, 45, 120, 0.6); }
|
|
}
|
|
.header h1 {
|
|
font-family: 'Orbitron', sans-serif; font-size: 20px; font-weight: 700; letter-spacing: 3px;
|
|
background: linear-gradient(90deg, var(--accent3), 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: 18px; }
|
|
.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(255, 45, 120, 0.1); color: var(--accent3);
|
|
border: 1px solid rgba(255, 45, 120, 0.2);
|
|
}
|
|
.live-chip .dot {
|
|
width: 7px; height: 7px; border-radius: 50%;
|
|
background: var(--accent3); box-shadow: 0 0 8px var(--accent3);
|
|
animation: blink 1.5s ease-in-out infinite;
|
|
}
|
|
.live-chip.live-chip-green {
|
|
background: rgba(0, 255, 136, 0.1); color: var(--accent4);
|
|
border: 1px solid rgba(0, 255, 136, 0.2);
|
|
}
|
|
.live-chip.live-chip-green .dot {
|
|
background: var(--accent4); box-shadow: 0 0 8px var(--accent4);
|
|
}
|
|
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
|
|
|
|
.section-title {
|
|
font-family: 'Orbitron', sans-serif; font-size: 13px; font-weight: 600; letter-spacing: 2px;
|
|
margin-bottom: 16px;
|
|
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Top row: two counter panels */
|
|
.counters-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.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;
|
|
position: relative; overflow: hidden;
|
|
}
|
|
.counter-card {
|
|
text-align: center;
|
|
padding: 20px 0;
|
|
}
|
|
.counter-card .counter-val {
|
|
font-family: 'Orbitron', sans-serif; font-size: 56px; font-weight: 700;
|
|
background: linear-gradient(180deg, #fff 0%, var(--accent) 100%);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
|
transition: all 0.15s ease;
|
|
}
|
|
.counter-card.recount .counter-val {
|
|
background: linear-gradient(180deg, #fff 0%, var(--accent3) 100%);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
|
}
|
|
.counter-card .counter-label { font-size: 10px; color: var(--text-secondary); letter-spacing: 2px; text-transform: uppercase; }
|
|
.counter-card .counter-sub {
|
|
font-size: 9px; color: var(--text-secondary); margin-top: 6px;
|
|
}
|
|
.counter-status {
|
|
font-size: 9px; letter-spacing: 1px; margin-top: 10px;
|
|
padding: 4px 12px; border-radius: 6px; display: inline-block;
|
|
}
|
|
.counter-status.connected {
|
|
color: var(--accent4); background: rgba(0, 255, 136, 0.06);
|
|
}
|
|
.counter-status.disconnected {
|
|
color: var(--accent3); background: rgba(255, 45, 120, 0.06);
|
|
}
|
|
|
|
/* Bottom row: video + file browser */
|
|
.bottom-row {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.video-panel {
|
|
background: var(--glass);
|
|
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
|
|
border: 1px solid var(--border-glow); border-radius: var(--radius);
|
|
padding: 12px;
|
|
position: relative; overflow: hidden;
|
|
min-height: 340px;
|
|
display: flex; flex-direction: column;
|
|
}
|
|
.video-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
margin-bottom: 10px; padding: 0 12px;
|
|
}
|
|
.video-container {
|
|
flex: 1; display: flex; align-items: center; justify-content: center;
|
|
background: #000; border-radius: 10px;
|
|
overflow: hidden; position: relative;
|
|
min-height: 280px;
|
|
}
|
|
.video-container img {
|
|
max-width: 100%; max-height: 100%; display: block;
|
|
}
|
|
.video-placeholder {
|
|
text-align: center; color: var(--text-secondary);
|
|
padding: 40px;
|
|
}
|
|
.video-placeholder .vp-icon {
|
|
font-size: 48px; opacity: 0.15; margin-bottom: 12px;
|
|
}
|
|
.video-placeholder .vp-text {
|
|
font-size: 11px; letter-spacing: 1px;
|
|
}
|
|
.video-placeholder .vp-hint {
|
|
font-size: 9px; margin-top: 8px; opacity: 0.5;
|
|
}
|
|
.video-status {
|
|
font-size: 9px; letter-spacing: 1px;
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 4px 10px; border-radius: 6px;
|
|
}
|
|
.video-status.connected {
|
|
color: var(--accent4); background: rgba(0, 255, 136, 0.06);
|
|
}
|
|
.video-status.stopped {
|
|
color: var(--text-secondary); background: var(--cell-bg);
|
|
}
|
|
|
|
/* File browser panel */
|
|
.file-panel {
|
|
background: var(--glass);
|
|
backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
|
|
border: 1px solid var(--border-glow); border-radius: var(--radius);
|
|
padding: 20px;
|
|
display: flex; flex-direction: column;
|
|
}
|
|
.file-panel .fp-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
}
|
|
.file-panel .fp-dir {
|
|
font-size: 9px; color: var(--accent); letter-spacing: 1px;
|
|
word-break: break-all; margin-bottom: 14px;
|
|
padding: 8px 12px; background: var(--cell-bg);
|
|
border-radius: 8px; border: 1px solid var(--cell-border);
|
|
}
|
|
.file-list {
|
|
flex: 1; overflow-y: auto; max-height: 400px;
|
|
}
|
|
.file-item {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 10px 12px; margin-bottom: 4px;
|
|
background: var(--cell-bg);
|
|
border: 1px solid var(--cell-border); border-radius: 8px;
|
|
cursor: pointer; transition: border-color 0.2s, background 0.2s;
|
|
}
|
|
.file-item:hover { border-color: var(--border-glow); background: var(--card-bg); }
|
|
.file-item.selected {
|
|
border-color: var(--accent2);
|
|
background: rgba(123, 47, 255, 0.08);
|
|
}
|
|
.file-item.streaming {
|
|
border-color: var(--accent3);
|
|
background: rgba(255, 45, 120, 0.08);
|
|
}
|
|
.file-item .fi-name {
|
|
font-size: 10px; color: var(--text-primary);
|
|
word-break: break-all; flex: 1;
|
|
}
|
|
.file-item .fi-size {
|
|
font-size: 8px; color: var(--text-secondary);
|
|
margin-left: 8px; white-space: nowrap;
|
|
}
|
|
.file-item .fi-icon {
|
|
font-size: 14px; margin-right: 8px;
|
|
color: var(--accent3);
|
|
}
|
|
.file-item.streaming .fi-icon { color: var(--accent4); animation: blink 1.5s ease-in-out infinite; }
|
|
|
|
/* Buttons */
|
|
.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-danger {
|
|
background: linear-gradient(135deg, var(--accent3), var(--accent2));
|
|
color: #fff;
|
|
}
|
|
.btn-danger:hover { opacity: 0.85; box-shadow: 0 0 20px rgba(255, 45, 120, 0.4); }
|
|
.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); }
|
|
.btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
|
|
.btn-row { display: flex; gap: 8px; margin-top: 12px; }
|
|
|
|
/* Footer */
|
|
.footer {
|
|
margin-top: 24px; text-align: center;
|
|
font-size: 9px; letter-spacing: 2px; color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
::-webkit-scrollbar { width: 5px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: var(--text-secondary); border-radius: 3px; }
|
|
|
|
@media (max-width: 900px) {
|
|
.counters-row, .bottom-row { grid-template-columns: 1fr; }
|
|
.container { padding: 12px 8px; }
|
|
.header { flex-direction: column; gap: 10px; padding: 14px 16px; }
|
|
.counter-card .counter-val { font-size: 40px; }
|
|
}
|
|
</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">✖</div>
|
|
<div>
|
|
<h1>{{ site_name }} Recounting</h1>
|
|
<div class="header-sub">Edge counter batch replay</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="live-chip live-chip-green">
|
|
<span class="dot"></span>
|
|
<span>LIVE</span>
|
|
</div>
|
|
<div class="live-chip">
|
|
<span class="dot"></span>
|
|
<span>RECOUNT</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Counter panels -->
|
|
<div class="counters-row">
|
|
<div class="panel">
|
|
<span class="section-title">◉ Live Counter</span>
|
|
<div class="counter-card">
|
|
<div class="counter-val" id="live-count">--</div>
|
|
<div class="counter-label">Objects Counted</div>
|
|
<div class="counter-sub">Batch #<span id="live-batch">--</span> · <span id="live-time">--</span></div>
|
|
<div class="counter-status disconnected" id="live-status">
|
|
waiting...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="panel">
|
|
<span class="section-title">◉ Recount Progress</span>
|
|
<div class="counter-card recount">
|
|
<div class="counter-val" id="recount-count">--</div>
|
|
<div class="counter-label">Objects Recounted</div>
|
|
<div class="counter-sub">Batch #<span id="recount-batch">--</span> · <span id="recount-time">--</span></div>
|
|
<div class="counter-status disconnected" id="recount-status">
|
|
waiting...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottom: video + file list -->
|
|
<div class="bottom-row">
|
|
<div class="video-panel">
|
|
<div class="video-header">
|
|
<span class="section-title" style="margin-bottom:0;">◉ Preview</span>
|
|
<div style="display:flex;align-items:center;gap:10px;">
|
|
<span class="video-status stopped" id="video-status">
|
|
<span style="width:6px;height:6px;border-radius:50%;background:var(--text-secondary);"></span>
|
|
stopped
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="video-container" id="video-container">
|
|
<div class="video-placeholder" id="video-placeholder">
|
|
<div class="vp-icon">▶</div>
|
|
<div class="vp-text">Select an MP4 file to stream</div>
|
|
<div class="vp-hint">Requires go2rtc on this host</div>
|
|
</div>
|
|
<img id="stream-img" alt="Stream" style="display:none;" />
|
|
</div>
|
|
</div>
|
|
<div class="file-panel">
|
|
<div class="fp-header">
|
|
<span class="section-title" style="margin-bottom:0;font-size:11px;">◉ Recordings</span>
|
|
<button class="btn btn-ghost" onclick="loadFiles()" style="font-size:8px;padding:4px 8px;">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
</div>
|
|
<div class="fp-dir" id="output-dir">{{ output_dir }}</div>
|
|
<div class="file-list" id="file-list">
|
|
<div style="font-size:10px;color:var(--text-secondary);text-align:center;padding:20px;">
|
|
Loading files...
|
|
</div>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" id="btn-start" onclick="startRecount()" disabled>
|
|
▶ START RECOUNT
|
|
</button>
|
|
<button class="btn btn-danger" id="btn-stop" onclick="stopRecount()" disabled>
|
|
■ STOP
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">{{ site_name }} Recounting Dashboard</div>
|
|
</div>
|
|
|
|
<script>
|
|
let selectedFile = null;
|
|
let streaming = false;
|
|
let streamUrl = '';
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadLiveProgress();
|
|
loadRecountProgress();
|
|
loadFiles();
|
|
|
|
setInterval(loadLiveProgress, 200);
|
|
setInterval(loadRecountProgress, 500);
|
|
setInterval(loadFiles, 30000);
|
|
});
|
|
|
|
// --- File list ---
|
|
|
|
async function loadFiles() {
|
|
try {
|
|
const res = await fetch('/api/mp4-files');
|
|
const files = await res.json();
|
|
const container = document.getElementById('file-list');
|
|
if (files.length === 0) {
|
|
container.innerHTML = '<div style="font-size:10px;color:var(--text-secondary);text-align:center;padding:20px;">No MP4 files found</div>';
|
|
return;
|
|
}
|
|
container.innerHTML = files.map(f => {
|
|
const sel = selectedFile === f.path;
|
|
const str = streaming && selectedFile === f.path;
|
|
let cls = 'file-item';
|
|
if (str) cls += ' streaming';
|
|
else if (sel) cls += ' selected';
|
|
return `
|
|
<div class="${cls}" onclick="selectFile('${f.path.replace(/'/g, "\\'")}', '${f.name.replace(/'/g, "\\'")}')">
|
|
<span class="fi-icon">${str ? '◉' : '▶'}</span>
|
|
<span class="fi-name">${f.name}</span>
|
|
<span class="fi-size">${formatSize(f.size)}</span>
|
|
</div>`;
|
|
}).join('');
|
|
} catch (err) {
|
|
console.error('Failed to load files:', err);
|
|
}
|
|
}
|
|
|
|
function selectFile(path, name) {
|
|
selectedFile = path;
|
|
document.getElementById('btn-start').disabled = false;
|
|
document.getElementById('stream-file-name').textContent = name;
|
|
loadFiles(); // refresh selection
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
|
|
return (bytes / 1073741824).toFixed(1) + ' GB';
|
|
}
|
|
|
|
// --- Streaming ---
|
|
|
|
async function startRecount() {
|
|
if (!selectedFile) return;
|
|
const btnStart = document.getElementById('btn-start');
|
|
const btnStop = document.getElementById('btn-stop');
|
|
btnStart.disabled = true;
|
|
btnStart.textContent = 'CONNECTING...';
|
|
|
|
try {
|
|
const res = await fetch('/api/start-recount', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: selectedFile })
|
|
});
|
|
const data = await res.json();
|
|
if (!data.success) {
|
|
alert('Failed: ' + (data.error || 'unknown'));
|
|
btnStart.disabled = false;
|
|
btnStart.textContent = '▶ START RECOUNT';
|
|
return;
|
|
}
|
|
streamUrl = data.stream_url;
|
|
streaming = true;
|
|
|
|
const img = document.getElementById('stream-img');
|
|
const placeholder = document.getElementById('video-placeholder');
|
|
img.src = streamUrl + '?' + Date.now();
|
|
img.style.display = 'block';
|
|
if (placeholder) placeholder.style.display = 'none';
|
|
|
|
btnStop.disabled = false;
|
|
btnStart.textContent = 'STREAMING';
|
|
btnStart.classList.add('btn-ghost');
|
|
|
|
document.getElementById('video-status').innerHTML =
|
|
'<span style="width:6px;height:6px;border-radius:50%;background:var(--accent4);box-shadow:0 0 6px var(--accent4);animation:blink 1.5s ease-in-out infinite;"></span> streaming ' + data.file;
|
|
document.getElementById('video-status').className = 'video-status connected';
|
|
|
|
loadFiles();
|
|
} catch (err) {
|
|
alert('Error: ' + err.message);
|
|
btnStart.disabled = false;
|
|
btnStart.textContent = '▶ START RECOUNT';
|
|
}
|
|
}
|
|
|
|
async function stopRecount() {
|
|
try {
|
|
await fetch('/api/stop-recount', { method: 'POST' });
|
|
} catch (err) {}
|
|
streaming = false;
|
|
streamUrl = '';
|
|
|
|
const img = document.getElementById('stream-img');
|
|
const placeholder = document.getElementById('video-placeholder');
|
|
img.src = '';
|
|
img.style.display = 'none';
|
|
if (placeholder) placeholder.style.display = '';
|
|
|
|
const btnStart = document.getElementById('btn-start');
|
|
const btnStop = document.getElementById('btn-stop');
|
|
btnStart.disabled = selectedFile ? false : true;
|
|
btnStart.textContent = '▶ START RECOUNT';
|
|
btnStart.classList.remove('btn-ghost');
|
|
btnStop.disabled = true;
|
|
|
|
document.getElementById('video-status').innerHTML =
|
|
'<span style="width:6px;height:6px;border-radius:50%;background:var(--text-secondary);"></span> stopped';
|
|
document.getElementById('video-status').className = 'video-status stopped';
|
|
|
|
loadFiles();
|
|
}
|
|
|
|
// --- Live counter ---
|
|
|
|
async function loadLiveProgress() {
|
|
const countEl = document.getElementById('live-count');
|
|
const batchEl = document.getElementById('live-batch');
|
|
const timeEl = document.getElementById('live-time');
|
|
const statusEl = document.getElementById('live-status');
|
|
try {
|
|
const res = await fetch('/api/live-progress');
|
|
const data = await res.json();
|
|
if (!data.success) {
|
|
countEl.textContent = '--';
|
|
batchEl.textContent = '--';
|
|
timeEl.textContent = data.error || 'unreachable';
|
|
statusEl.className = 'counter-status disconnected';
|
|
statusEl.textContent = 'no connection';
|
|
return;
|
|
}
|
|
countEl.textContent = data.count.toLocaleString();
|
|
batchEl.textContent = data.batch_number || '--';
|
|
if (data.last_detection_time) {
|
|
const d = new Date(data.last_detection_time);
|
|
timeEl.textContent = d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
} else {
|
|
timeEl.textContent = 'waiting...';
|
|
}
|
|
statusEl.className = 'counter-status connected';
|
|
statusEl.textContent = 'connected';
|
|
} catch (err) {
|
|
countEl.textContent = '--';
|
|
batchEl.textContent = '--';
|
|
timeEl.textContent = 'unreachable';
|
|
statusEl.className = 'counter-status disconnected';
|
|
statusEl.textContent = 'no connection';
|
|
}
|
|
}
|
|
|
|
// --- Recount counter ---
|
|
|
|
async function loadRecountProgress() {
|
|
const countEl = document.getElementById('recount-count');
|
|
const batchEl = document.getElementById('recount-batch');
|
|
const timeEl = document.getElementById('recount-time');
|
|
const statusEl = document.getElementById('recount-status');
|
|
try {
|
|
const res = await fetch('/api/recount-progress');
|
|
const data = await res.json();
|
|
if (!data.success) {
|
|
countEl.textContent = '--';
|
|
batchEl.textContent = '--';
|
|
timeEl.textContent = data.error || 'unreachable';
|
|
statusEl.className = 'counter-status disconnected';
|
|
statusEl.textContent = 'no connection';
|
|
return;
|
|
}
|
|
countEl.textContent = data.count.toLocaleString();
|
|
batchEl.textContent = data.batch_number || '--';
|
|
if (data.last_detection_time) {
|
|
const d = new Date(data.last_detection_time);
|
|
timeEl.textContent = d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
} else {
|
|
timeEl.textContent = 'waiting...';
|
|
}
|
|
statusEl.className = 'counter-status connected';
|
|
statusEl.textContent = 'connected';
|
|
} catch (err) {
|
|
countEl.textContent = '--';
|
|
batchEl.textContent = '--';
|
|
timeEl.textContent = 'unreachable';
|
|
statusEl.className = 'counter-status disconnected';
|
|
statusEl.textContent = 'no connection';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|