770 lines
28 KiB
HTML
770 lines
28 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Live CCTV Monitoring - Karung.AI{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<style>
|
|
.monitoring-grid {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 24px;
|
|
align-items: start;
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.monitoring-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Video Feed Panel */
|
|
.video-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.video-container {
|
|
position: relative;
|
|
width: 100%;
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
background-color: #000000;
|
|
border: 1px solid var(--border-color);
|
|
aspect-ratio: 16/9;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.video-feed {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
display: block;
|
|
}
|
|
|
|
.video-placeholder {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #FFFFFF;
|
|
background-color: #111111;
|
|
gap: 12px;
|
|
}
|
|
|
|
.video-placeholder i {
|
|
font-size: 40px;
|
|
color: var(--accent-blue);
|
|
}
|
|
|
|
.video-placeholder p {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #8E8E93;
|
|
}
|
|
|
|
.video-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.video-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* Active Batch Details (Right Column) */
|
|
.control-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
}
|
|
|
|
.active-batch-card {
|
|
background-color: var(--bg-card);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--radius-lg);
|
|
padding: 24px;
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.card-label {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.count-display {
|
|
font-size: 64px;
|
|
font-weight: 700;
|
|
letter-spacing: -2px;
|
|
line-height: 1;
|
|
color: var(--accent-blue);
|
|
margin: 12px 0;
|
|
}
|
|
|
|
.meta-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
margin-top: 20px;
|
|
border-top: 1px solid var(--border-color);
|
|
padding-top: 16px;
|
|
}
|
|
|
|
.meta-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.meta-key {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.meta-value {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Live Event Logs */
|
|
.log-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 380px;
|
|
}
|
|
|
|
.log-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.log-title {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.log-container {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
.log-item {
|
|
display: flex;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
border-radius: var(--radius-md);
|
|
background-color: var(--bg-base);
|
|
border: 1px solid var(--border-color);
|
|
font-size: 13px;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.log-item:hover {
|
|
background-color: rgba(0,0,0,0.02);
|
|
}
|
|
|
|
[data-theme="dark"] .log-item:hover {
|
|
background-color: rgba(255,255,255,0.02);
|
|
}
|
|
|
|
.log-time {
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
font-family: monospace;
|
|
min-width: 60px;
|
|
}
|
|
|
|
.log-content {
|
|
flex: 1;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.log-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 9px;
|
|
color: #FFFFFF;
|
|
flex-shrink: 0;
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.log-icon.masuk {
|
|
background-color: var(--accent-green);
|
|
}
|
|
|
|
.log-icon.keluar {
|
|
background-color: var(--accent-red);
|
|
}
|
|
|
|
.log-icon.info {
|
|
background-color: var(--accent-blue);
|
|
}
|
|
|
|
/* Apple Style Switch */
|
|
.switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 44px;
|
|
height: 24px;
|
|
}
|
|
|
|
.switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
inset: 0;
|
|
background-color: var(--border-color);
|
|
transition: .3s;
|
|
border-radius: 24px;
|
|
}
|
|
|
|
.slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 18px;
|
|
width: 18px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
transition: .3s;
|
|
border-radius: 50%;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
input:checked + .slider {
|
|
background-color: var(--accent-green);
|
|
}
|
|
|
|
input:checked + .slider:before {
|
|
transform: translateX(20px);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="monitoring-grid">
|
|
<!-- Left Column: Video Feed -->
|
|
<div class="video-panel">
|
|
<div class="video-header">
|
|
<h2 class="video-title">
|
|
<i class="fa-solid fa-camera"></i> CCTV Feed - CC1
|
|
</h2>
|
|
<div style="display: flex; align-items: center; gap: 16px;">
|
|
<!-- Live CCTV Toggle Switch -->
|
|
<div style="display: flex; align-items: center; gap: 8px;">
|
|
<span style="font-size: 13px; font-weight: 500; color: var(--text-secondary);">Live View:</span>
|
|
<label class="switch">
|
|
<input type="checkbox" id="liveFeedToggle" onchange="toggleLiveFeed()">
|
|
<span class="slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="btn btn-secondary btn-sm" onclick="reloadStream()" title="Reload Stream Feed">
|
|
<i class="fa-solid fa-refresh"></i> Refresh Feed
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="video-container">
|
|
<!-- Fetch direct live frame stream from local dashboard Flask -->
|
|
<img id="liveVideoFeed" class="video-feed" src="" alt="CCTV Stream Feed" style="display: none;" onerror="handleStreamError()">
|
|
<div id="videoPlaceholder" class="video-placeholder" style="display: flex;">
|
|
<i class="fa-solid fa-play" style="font-size: 40px; cursor: pointer; color: var(--accent-blue);" onclick="startLiveFeedManual()"></i>
|
|
<p>Live Feed dinonaktifkan (Hemat CPU & Bandwidth)</p>
|
|
<div class="btn btn-secondary" onclick="startLiveFeedManual()">Aktifkan Live View CCTV</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Control & Active Batch Stats -->
|
|
<div class="control-panel">
|
|
<!-- Card 1: Current Batch Stats -->
|
|
<div class="active-batch-card">
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
|
<h3 class="card-label" style="margin-bottom: 0;">Active Batch</h3>
|
|
<!-- Mode Switcher: auto | do_manual | manual -->
|
|
<div style="display: inline-flex; background: var(--bg-base); border: 1px solid var(--border-color); border-radius: 20px; padding: 2px;">
|
|
<button id="btnModeAuto" class="btn btn-sm" style="padding: 4px 10px; font-size: 11px; border-radius: 18px; border: none; background: transparent; color: var(--text-secondary);" onclick="switchBatchMode('auto')">
|
|
<i class="fa-solid fa-robot"></i> Otomatis
|
|
</button>
|
|
<button id="btnModeDoManual" class="btn btn-sm" style="padding: 4px 10px; font-size: 11px; border-radius: 18px; border: none; background: transparent; color: var(--text-secondary);" onclick="switchBatchMode('do_manual')">
|
|
<i class="fa-solid fa-file-lines"></i> DO Manual
|
|
</button>
|
|
<button id="btnModeManual" class="btn btn-sm" style="padding: 4px 10px; font-size: 11px; border-radius: 18px; border: none; background: transparent; color: var(--text-secondary);" onclick="switchBatchMode('manual')">
|
|
<i class="fa-solid fa-hand"></i> Manual
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="count-display" id="liveCount">0</div>
|
|
<div class="card-label" style="font-size: 11px; margin-top: -4px;">Sacks Counted</div>
|
|
|
|
<div class="count-display" id="liveBoxCount">0</div>
|
|
<div class="card-label" style="font-size: 11px; margin-top: -4px;">Boxes Counted</div>
|
|
|
|
<div class="meta-list">
|
|
<div class="meta-item">
|
|
<span class="meta-key">Mode Batch</span>
|
|
<span class="meta-value" id="liveBatchModeText" style="font-weight: 600; color: var(--accent-blue);">Manual (Tombol)</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-key">Model Mode</span>
|
|
<span class="meta-value" id="liveModelMode" style="font-weight: 600;">--</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-key">Batch Number</span>
|
|
<span class="meta-value" id="liveBatchNumber">--</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-key">Last Detection</span>
|
|
<span class="meta-value" id="liveLastDetection">Waiting...</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-key">Status</span>
|
|
<span class="meta-value" id="liveStatus" style="color: var(--text-secondary);">Standby (Idle)</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-key">Pipeline Speed</span>
|
|
<span class="meta-value"><span id="liveFps">--</span> FPS</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-key">OCR Engine</span>
|
|
<span class="meta-value">
|
|
<select id="monOcrEngine" style="padding:4px 8px;border-radius:8px;border:1px solid var(--border-color);background:var(--bg-base);color:var(--text-primary);font-size:12px;" onchange="setOcrEngine(this.value)">
|
|
<option value="tesseract">tesseract</option>
|
|
<option value="paddle">paddle</option>
|
|
<option value="none">none</option>
|
|
</select>
|
|
</span>
|
|
</div>
|
|
<div class="meta-item">
|
|
<span class="meta-key">Require plat</span>
|
|
<span class="meta-value">
|
|
<label style="font-weight:400;">
|
|
<input type="checkbox" id="monRequirePlate" onchange="setRequirePlate(this.checked)"> wajib
|
|
</label>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manual Batch Control Buttons for Office Dashboard (Hidden when in Auto Mode) -->
|
|
<div id="manualControlSection" style="margin-top: 20px; display: flex; gap: 10px;">
|
|
<button id="monBtnStart" class="btn btn-primary" style="flex: 1; background-color: var(--accent-green);" onclick="confirmStartBatch()">
|
|
<i class="fa-solid fa-play"></i> Mulai Batch
|
|
</button>
|
|
<button id="monBtnStop" class="btn btn-primary" style="flex: 1; background-color: var(--accent-red); display: none;" onclick="confirmStopBatch()">
|
|
<i class="fa-solid fa-square"></i> Akhiri Batch
|
|
</button>
|
|
</div>
|
|
<div id="autoModeHint" style="margin-top: 14px; font-size: 12px; color: var(--text-secondary); text-align: center; display: none;">
|
|
<i class="fa-solid fa-info-circle"></i> Batch dimulai & diakhiri otomatis oleh AI deteksi truk
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card 2: Live Activity Logs -->
|
|
<div class="card log-card">
|
|
<div class="log-header">
|
|
<h3 class="log-title">Live Activity Log</h3>
|
|
<span class="live-badge" style="font-size: 9px;"><span class="dot"></span> Real-time</span>
|
|
</div>
|
|
<div class="log-container" id="logContainer">
|
|
<div class="log-item" id="emptyLogItem">
|
|
<div class="log-time">--:--:--</div>
|
|
<div class="log-content">Sistem aktif. Menunggu batch dimulai...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Konfirmasi Start -->
|
|
<div id="modalStart" style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 999; padding: 20px;">
|
|
<div style="background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 24px; max-width: 400px; width: 100%; text-align: center;">
|
|
<h3 style="font-size: 18px; font-weight: 700; margin-bottom: 12px;"><i class="fa-solid fa-play" style="color: var(--accent-green);"></i> Mulai Batch Baru?</h3>
|
|
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 24px;">Counter akan diaktifkan dan batch baru akan dibuat.</p>
|
|
<div style="display: flex; gap: 12px;">
|
|
<button class="btn btn-secondary" style="flex: 1;" onclick="document.getElementById('modalStart').style.display='none'">Batal</button>
|
|
<button class="btn btn-primary" style="flex: 1; background-color: var(--accent-green);" onclick="executeStartBatch()">Ya, Mulai</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Konfirmasi Stop -->
|
|
<div id="modalStop" style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 999; padding: 20px;">
|
|
<div style="background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 24px; max-width: 400px; width: 100%; text-align: center;">
|
|
<h3 style="font-size: 18px; font-weight: 700; margin-bottom: 12px;"><i class="fa-solid fa-square" style="color: var(--accent-red);"></i> Selesai Batch?</h3>
|
|
<p style="font-size: 14px; color: var(--text-secondary); margin-bottom: 24px;">Pemuatan batch akan difinalisasi dan disimpan ke database.</p>
|
|
<div style="display: flex; gap: 12px;">
|
|
<button class="btn btn-secondary" style="flex: 1;" onclick="document.getElementById('modalStop').style.display='none'">Batal</button>
|
|
<button class="btn btn-primary" style="flex: 1; background-color: var(--accent-red);" onclick="executeStopBatch()">Ya, Selesai</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
let streamRetryCount = 0;
|
|
|
|
function startLiveFeedManual() {
|
|
const toggle = document.getElementById('liveFeedToggle');
|
|
toggle.checked = true;
|
|
toggleLiveFeed();
|
|
}
|
|
|
|
function handleStreamError() {
|
|
const feed = document.getElementById('liveVideoFeed');
|
|
const placeholder = document.getElementById('videoPlaceholder');
|
|
feed.style.display = 'none';
|
|
placeholder.style.display = 'flex';
|
|
|
|
if (streamRetryCount < 10) {
|
|
streamRetryCount++;
|
|
setTimeout(reloadStream, 3000);
|
|
}
|
|
}
|
|
|
|
function reloadStream() {
|
|
const toggle = document.getElementById('liveFeedToggle');
|
|
if (!toggle.checked) return; // Jangan reload jika dimatikan
|
|
|
|
const feed = document.getElementById('liveVideoFeed');
|
|
const placeholder = document.getElementById('videoPlaceholder');
|
|
|
|
feed.style.display = 'block';
|
|
placeholder.style.display = 'none';
|
|
feed.src = '/api/live-video?t=' + new Date().getTime();
|
|
}
|
|
|
|
function toggleLiveFeed() {
|
|
const toggle = document.getElementById('liveFeedToggle');
|
|
const feed = document.getElementById('liveVideoFeed');
|
|
const placeholder = document.getElementById('videoPlaceholder');
|
|
|
|
if (toggle.checked) {
|
|
// Aktifkan live feed
|
|
feed.style.display = 'block';
|
|
placeholder.style.display = 'none';
|
|
feed.src = '/api/live-video';
|
|
} else {
|
|
// Matikan live feed (putus koneksi stream)
|
|
feed.style.display = 'none';
|
|
placeholder.style.display = 'flex';
|
|
placeholder.querySelector('p').textContent = 'Live Feed dinonaktifkan (Hemat CPU & Bandwidth)';
|
|
feed.src = ''; // Putus koneksi stream dengan backend!
|
|
}
|
|
}
|
|
|
|
function confirmStartBatch() {
|
|
document.getElementById('modalStart').style.display = 'flex';
|
|
}
|
|
|
|
function confirmStopBatch() {
|
|
document.getElementById('modalStop').style.display = 'flex';
|
|
}
|
|
|
|
async function executeStartBatch() {
|
|
document.getElementById('modalStart').style.display = 'none';
|
|
try {
|
|
const res = await fetch('/api/batch/start', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
await pollCurrentBatch();
|
|
} else {
|
|
alert('Gagal memulai batch: ' + (data.error || 'Terjadi kesalahan'));
|
|
}
|
|
} catch (e) {
|
|
alert('Gagal terhubung ke server');
|
|
}
|
|
}
|
|
|
|
async function executeStopBatch() {
|
|
document.getElementById('modalStop').style.display = 'none';
|
|
try {
|
|
const res = await fetch('/api/batch/stop', { method: 'POST' });
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
await pollCurrentBatch();
|
|
} else {
|
|
alert('Gagal mengakhiri batch: ' + (data.error || 'Terjadi kesalahan'));
|
|
}
|
|
} catch (e) {
|
|
alert('Gagal terhubung ke server');
|
|
}
|
|
}
|
|
|
|
// Polling current batch stats every 2 seconds
|
|
async function pollCurrentBatch() {
|
|
try {
|
|
const res = await fetch('/api/current-batch');
|
|
const data = await res.json();
|
|
|
|
const countEl = document.getElementById('liveCount');
|
|
const boxCountEl = document.getElementById('liveBoxCount');
|
|
const batchNumEl = document.getElementById('liveBatchNumber');
|
|
const lastDetEl = document.getElementById('liveLastDetection');
|
|
const statusEl = document.getElementById('liveStatus');
|
|
const fpsEl = document.getElementById('liveFps');
|
|
const modelModeEl = document.getElementById('liveModelMode');
|
|
const btnStart = document.getElementById('monBtnStart');
|
|
const btnStop = document.getElementById('monBtnStop');
|
|
|
|
if (data.fps !== undefined && data.fps !== null) {
|
|
fpsEl.textContent = data.fps;
|
|
} else {
|
|
fpsEl.textContent = '--';
|
|
}
|
|
if (data.model_mode && modelModeEl) modelModeEl.textContent = data.model_mode;
|
|
|
|
if (data.success && data.batch_number) {
|
|
// Batch sedang berjalan
|
|
const currentCount = parseInt(data.count) || 0;
|
|
countEl.textContent = currentCount.toLocaleString();
|
|
if (boxCountEl) boxCountEl.textContent = (parseInt(data.box_count) || 0).toLocaleString();
|
|
batchNumEl.textContent = '#' + (data.batch_number || '--');
|
|
statusEl.textContent = 'Counting Sacks (Active)';
|
|
statusEl.style.color = 'var(--accent-blue)';
|
|
|
|
if (data.last_detection_time) {
|
|
const lastDet = new Date(data.last_detection_time);
|
|
lastDetEl.textContent = lastDet.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
} else {
|
|
lastDetEl.textContent = 'waiting...';
|
|
}
|
|
|
|
if (btnStart && btnStop) {
|
|
btnStart.style.display = 'none';
|
|
btnStop.style.display = 'flex';
|
|
}
|
|
|
|
// Tambahkan event ke log secara dinamis jika jumlah hitungan bertambah
|
|
addLogItem(new Date().toLocaleTimeString('en-US', { hour12: false }), `Karung ke-${currentCount} terhitung di area counting.`, 'masuk');
|
|
} else {
|
|
// Tidak ada batch aktif (standby)
|
|
countEl.textContent = '0';
|
|
if (boxCountEl) boxCountEl.textContent = (parseInt(data.box_count) || 0).toLocaleString();
|
|
batchNumEl.textContent = '--';
|
|
lastDetEl.textContent = '--';
|
|
statusEl.textContent = 'Standby (Menunggu Mulai)';
|
|
statusEl.style.color = 'var(--text-secondary)';
|
|
|
|
if (btnStart && btnStop) {
|
|
btnStart.style.display = 'flex';
|
|
btnStop.style.display = 'none';
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to poll current batch:', err);
|
|
}
|
|
}
|
|
|
|
// Polling previous batch details periodically to see if a batch just finalized
|
|
let lastFinalizedBatchNum = null;
|
|
async function pollFinalizedBatch() {
|
|
try {
|
|
const res = await fetch('/api/previous-batch');
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
if (lastFinalizedBatchNum !== null && lastFinalizedBatchNum !== data.batch_number) {
|
|
// Ada batch baru yang baru saja diselesaikan!
|
|
addLogItem(
|
|
new Date(data.end_time).toLocaleTimeString('en-US', { hour12: false }),
|
|
`Batch #${data.batch_number} selesai. Total: ${data.count} karung, Durasi: ${data.duration_minutes} m.`,
|
|
'info'
|
|
);
|
|
}
|
|
lastFinalizedBatchNum = data.batch_number;
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to poll previous batch:', err);
|
|
}
|
|
}
|
|
|
|
const addedLogs = new Set();
|
|
function addLogItem(time, message, type = 'info') {
|
|
const key = `${time}_${message}`;
|
|
if (addedLogs.has(key)) return;
|
|
addedLogs.add(key);
|
|
|
|
const container = document.getElementById('logContainer');
|
|
const emptyItem = document.getElementById('emptyLogItem');
|
|
if (emptyItem) emptyItem.remove();
|
|
|
|
const item = document.createElement('div');
|
|
item.className = 'log-item';
|
|
|
|
let iconClass = 'fa-info';
|
|
if (type === 'masuk') iconClass = 'fa-plus';
|
|
if (type === 'keluar') iconClass = 'fa-minus';
|
|
|
|
item.innerHTML = `
|
|
<div class="log-icon ${type}"><i class="fa-solid ${iconClass}"></i></div>
|
|
<div class="log-time">${time}</div>
|
|
<div class="log-content">${message}</div>
|
|
`;
|
|
|
|
container.insertBefore(item, container.firstChild);
|
|
|
|
// Batasi log maksimal 40 baris agar tidak membebani memori browser
|
|
while (container.children.length > 40) {
|
|
container.removeChild(container.lastChild);
|
|
}
|
|
}
|
|
|
|
let currentBatchMode = 'auto';
|
|
|
|
async function switchBatchMode(mode) {
|
|
try {
|
|
const res = await fetch('/api/batch/mode', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ mode: mode })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
updateModeUI(data.mode);
|
|
} else if (res.status === 409) {
|
|
alert(data.error || 'Batch sedang berjalan — hentikan dulu.');
|
|
} else if (res.status === 403) {
|
|
alert(data.error || 'Hanya dari port kantor.');
|
|
} else {
|
|
alert(data.error || 'Gagal ganti mode');
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to switch batch mode:', e);
|
|
}
|
|
}
|
|
|
|
async function checkBatchMode() {
|
|
try {
|
|
const res = await fetch('/api/batch/mode');
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
updateModeUI(data.mode);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to check batch mode:', e);
|
|
}
|
|
}
|
|
|
|
function updateModeUI(mode) {
|
|
currentBatchMode = mode;
|
|
const btnManual = document.getElementById('btnModeManual');
|
|
const btnAuto = document.getElementById('btnModeAuto');
|
|
const btnDo = document.getElementById('btnModeDoManual');
|
|
const modeText = document.getElementById('liveBatchModeText');
|
|
const manualSection = document.getElementById('manualControlSection');
|
|
const autoHint = document.getElementById('autoModeHint');
|
|
|
|
const setActive = (el, on) => {
|
|
if (!el) return;
|
|
if (on) {
|
|
el.style.background = 'var(--accent-blue)';
|
|
el.style.color = '#fff';
|
|
el.style.fontWeight = '600';
|
|
} else {
|
|
el.style.background = 'transparent';
|
|
el.style.color = 'var(--text-secondary)';
|
|
el.style.fontWeight = 'normal';
|
|
}
|
|
};
|
|
setActive(btnAuto, mode === 'auto');
|
|
setActive(btnDo, mode === 'do_manual');
|
|
setActive(btnManual, mode === 'manual');
|
|
|
|
if (modeText) {
|
|
if (mode === 'auto') {
|
|
modeText.textContent = 'Otomatis (AI Truk)';
|
|
modeText.style.color = 'var(--accent-orange)';
|
|
} else if (mode === 'do_manual') {
|
|
modeText.textContent = 'DO Manual (Scan Surat)';
|
|
modeText.style.color = 'var(--accent-blue)';
|
|
} else {
|
|
modeText.textContent = 'Manual (Tombol)';
|
|
modeText.style.color = 'var(--accent-blue)';
|
|
}
|
|
}
|
|
if (mode === 'auto') {
|
|
if (manualSection) manualSection.style.display = 'none';
|
|
if (autoHint) autoHint.style.display = 'block';
|
|
} else {
|
|
if (manualSection) manualSection.style.display = 'flex';
|
|
if (autoHint) autoHint.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function loadDoSettings() {
|
|
try {
|
|
const res = await fetch('/api/do/settings');
|
|
const data = await res.json();
|
|
if (!data.success) return;
|
|
const sel = document.getElementById('monOcrEngine');
|
|
if (sel && data.ocr_engine) sel.value = data.ocr_engine;
|
|
const chk = document.getElementById('monRequirePlate');
|
|
if (chk) chk.checked = !!data.require_plate;
|
|
} catch (e) { console.error(e); }
|
|
}
|
|
|
|
async function setOcrEngine(eng) {
|
|
try {
|
|
const res = await fetch('/api/do/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ ocr_engine: eng })
|
|
});
|
|
const data = await res.json();
|
|
if (!data.success) alert(data.error || 'Gagal set OCR');
|
|
loadDoSettings();
|
|
} catch (e) { alert('Gagal terhubung'); loadDoSettings(); }
|
|
}
|
|
|
|
async function setRequirePlate(val) {
|
|
try {
|
|
const res = await fetch('/api/do/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ require_plate: val })
|
|
});
|
|
const data = await res.json();
|
|
if (!data.success) alert(data.error || 'Gagal set require_plate');
|
|
loadDoSettings();
|
|
} catch (e) { alert('Gagal terhubung'); loadDoSettings(); }
|
|
}
|
|
|
|
// Start interval pollings
|
|
setInterval(pollCurrentBatch, 2000);
|
|
setInterval(pollFinalizedBatch, 3000);
|
|
setInterval(checkBatchMode, 5000);
|
|
setInterval(loadDoSettings, 8000);
|
|
|
|
// Initial first load
|
|
checkBatchMode();
|
|
loadDoSettings();
|
|
pollCurrentBatch();
|
|
pollFinalizedBatch();
|
|
</script>
|
|
{% endblock %}
|