feat: add manual & auto batch counting modes, dual-port operator and monitoring dashboards, and historical batch data corrections
This commit is contained in:
commit
0235a0f597
53 files changed
+14127
No files matched your search
@@ -0,0 +1,681 @@
|
||||
{% 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 -->
|
||||
<div style="display: inline-flex; background: var(--bg-base); border: 1px solid var(--border-color); border-radius: 20px; padding: 2px;">
|
||||
<button id="btnModeManual" class="btn btn-sm" style="padding: 4px 10px; font-size: 11px; border-radius: 18px; border: none; background: var(--accent-blue); color: #fff; font-weight: 600;" onclick="switchBatchMode('manual')">
|
||||
<i class="fa-solid fa-hand"></i> Manual
|
||||
</button>
|
||||
<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>
|
||||
</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="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">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>
|
||||
|
||||
<!-- 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 batchNumEl = document.getElementById('liveBatchNumber');
|
||||
const lastDetEl = document.getElementById('liveLastDetection');
|
||||
const statusEl = document.getElementById('liveStatus');
|
||||
const fpsEl = document.getElementById('liveFps');
|
||||
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.success && data.batch_number) {
|
||||
// Batch sedang berjalan
|
||||
const currentCount = parseInt(data.count) || 0;
|
||||
countEl.textContent = currentCount.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';
|
||||
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 = 'manual';
|
||||
|
||||
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);
|
||||
}
|
||||
} 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 modeText = document.getElementById('liveBatchModeText');
|
||||
const manualSection = document.getElementById('manualControlSection');
|
||||
const autoHint = document.getElementById('autoModeHint');
|
||||
|
||||
if (mode === 'auto') {
|
||||
btnAuto.style.background = 'var(--accent-blue)';
|
||||
btnAuto.style.color = '#fff';
|
||||
btnAuto.style.fontWeight = '600';
|
||||
btnManual.style.background = 'transparent';
|
||||
btnManual.style.color = 'var(--text-secondary)';
|
||||
btnManual.style.fontWeight = 'normal';
|
||||
if (modeText) {
|
||||
modeText.textContent = 'Otomatis (AI Truk)';
|
||||
modeText.style.color = 'var(--accent-orange)';
|
||||
}
|
||||
if (manualSection) manualSection.style.display = 'none';
|
||||
if (autoHint) autoHint.style.display = 'block';
|
||||
} else {
|
||||
btnManual.style.background = 'var(--accent-blue)';
|
||||
btnManual.style.color = '#fff';
|
||||
btnManual.style.fontWeight = '600';
|
||||
btnAuto.style.background = 'transparent';
|
||||
btnAuto.style.color = 'var(--text-secondary)';
|
||||
btnAuto.style.fontWeight = 'normal';
|
||||
if (modeText) {
|
||||
modeText.textContent = 'Manual (Tombol)';
|
||||
modeText.style.color = 'var(--accent-blue)';
|
||||
}
|
||||
if (manualSection) manualSection.style.display = 'flex';
|
||||
if (autoHint) autoHint.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Start interval pollings
|
||||
setInterval(pollCurrentBatch, 2000);
|
||||
setInterval(pollFinalizedBatch, 3000);
|
||||
setInterval(checkBatchMode, 5000);
|
||||
|
||||
// Initial first load
|
||||
checkBatchMode();
|
||||
pollCurrentBatch();
|
||||
pollFinalizedBatch();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user