364 lines
14 KiB
HTML
364 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Riwayat Deteksi{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h2 class="mb-4"><i class="fas fa-history me-2"></i>Riwayat Deteksi</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info" id="historyStatus">
|
|
<i class="fas fa-circle-notch fa-spin me-2"></i>
|
|
Halaman riwayat dimuat terlebih dahulu, lalu data diambil dari localStorage browser ini.
|
|
</div>
|
|
|
|
<!-- Statistik Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card bg-primary text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title">Total Deteksi</h6>
|
|
<h2 class="mb-0" id="statTotal">{{ stats.total }}</h2>
|
|
</div>
|
|
<i class="fas fa-camera fa-3x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-success text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title">Sehat</h6>
|
|
<h2 class="mb-0" id="statSehat">{{ stats.negatif }}</h2>
|
|
</div>
|
|
<i class="fas fa-check-circle fa-3x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-warning text-dark">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title">Positif PMK</h6>
|
|
<h2 class="mb-0" id="statSakit">{{ stats.positif }}</h2>
|
|
</div>
|
|
<i class="fas fa-exclamation-triangle fa-3x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-info text-white">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title">Akurasi Rata-rata</h6>
|
|
<h2 class="mb-0" id="statAkurasi">0.0%</h2>
|
|
</div>
|
|
<i class="fas fa-chart-line fa-3x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Detections Table -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header bg-success text-white">
|
|
<h5 class="mb-0"><i class="fas fa-history me-2"></i>Riwayat Deteksi Terbaru</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
|
|
<div class="table-responsive" id="tableWrapper">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Waktu</th>
|
|
<th>Gambar</th>
|
|
<th>Hasil</th>
|
|
<th>Keyakinan</th>
|
|
<th>Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="riwayatTableBody">
|
|
<tr id="loadingHistoryRow">
|
|
<td colspan="5" class="text-center py-4 text-muted">
|
|
<i class="fas fa-circle-notch fa-spin me-2"></i>Memuat data riwayat...
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="alert alert-warning d-none mt-3" id="emptyHistoryNotice">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>Tidak ada riwayat.</strong> Data pada local storage browser ini masih kosong atau belum cocok dengan data database.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
// ==== REAL-TIME CLOCK UPDATE ====
|
|
function formatTimeAgo(isoDateString) {
|
|
try {
|
|
const date = new Date(isoDateString);
|
|
|
|
// Format manual: DD/MM/YYYY HH:MM:SS
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const year = date.getFullYear();
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
|
|
const formatted = `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
|
|
return formatted;
|
|
} catch (e) {
|
|
return isoDateString;
|
|
}
|
|
}
|
|
|
|
function updateRealtimeTimestamps() {
|
|
const elements = document.querySelectorAll('.timestamp-realtime');
|
|
elements.forEach(el => {
|
|
const timestamp = el.dataset.timestamp;
|
|
const formatted = formatTimeAgo(timestamp);
|
|
el.textContent = formatted;
|
|
el.title = timestamp;
|
|
});
|
|
}
|
|
|
|
// Update setiap detik
|
|
setInterval(updateRealtimeTimestamps, 1000);
|
|
// Update immediately on page load
|
|
document.addEventListener('DOMContentLoaded', updateRealtimeTimestamps);
|
|
updateRealtimeTimestamps();
|
|
|
|
// Chart untuk distribusi hasil
|
|
const ctx1 = document.getElementById('resultChart')?.getContext('2d');
|
|
const resultDataValues = JSON.parse(document.getElementById('resultChart')?.dataset?.values || '[]');
|
|
if (ctx1 && resultDataValues.length) {
|
|
new Chart(ctx1, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: ['Sehat', 'Positif PMK'],
|
|
datasets: [{
|
|
data: resultDataValues,
|
|
backgroundColor: ['#28a745', '#ffc107'],
|
|
borderColor: ['#218838', '#e0a800'],
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
|
|
});
|
|
}
|
|
|
|
// Chart untuk tren (contoh data)
|
|
const ctx2 = document.getElementById('trendChart')?.getContext('2d');
|
|
if (ctx2) {
|
|
new Chart(ctx2, {
|
|
type: 'line',
|
|
data: {
|
|
labels: ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'],
|
|
datasets: [{
|
|
label: 'Deteksi',
|
|
data: [12, 19, 15, 17, 24, 23, 20],
|
|
borderColor: '#28a745',
|
|
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
});
|
|
}
|
|
|
|
function viewDetails(filename) {
|
|
alert('View details for: ' + filename);
|
|
}
|
|
|
|
function exportData(format) {
|
|
if (format === 'csv') {
|
|
window.location.href = "{{ url_for('export_csv') }}";
|
|
} else if (format === 'excel') {
|
|
window.location.href = "{{ url_for('export_excel') }}";
|
|
}
|
|
}
|
|
|
|
function loadLocalStorageIds() {
|
|
try {
|
|
const value = JSON.parse(localStorage.getItem('pmk_predictions_ids') || '[]');
|
|
return Array.isArray(value) ? [...new Set(value.map(String))] : [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function formatTimestamp(timestamp) {
|
|
if (!timestamp) return '-';
|
|
try {
|
|
const date = new Date(timestamp);
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const year = date.getFullYear();
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
|
|
} catch (e) {
|
|
return timestamp;
|
|
}
|
|
}
|
|
|
|
function createHistoryRow(item) {
|
|
const tr = document.createElement('tr');
|
|
tr.id = `pred-${item.id}`;
|
|
tr.dataset.predictionId = item.id;
|
|
tr.dataset.prediction = item.prediction || '';
|
|
tr.dataset.source = item.source || 'image_processing';
|
|
|
|
const fileName = item.filename || item.original_filename || item.image_path || '-';
|
|
const imageCell = item.source === 'manual_expert_system'
|
|
? '<span class="badge bg-secondary">Sistem Pakar</span>'
|
|
: item.image_url
|
|
? `<a href="${item.image_url}" target="_blank"><i class="fas fa-image me-1"></i>${fileName.length > 20 ? fileName.slice(0, 20) + '...' : fileName}</a>`
|
|
: fileName;
|
|
|
|
const badgeClass = item.prediction === 'sakit' ? 'bg-warning text-dark' : 'bg-success';
|
|
const badgeText = item.source === 'manual_expert_system'
|
|
? (item.display_label || item.diagnosis_label || 'Hasil Forward Chaining')
|
|
: (item.prediction === 'sakit' ? 'Positif PMK' : 'Sehat');
|
|
const progressClass = item.prediction === 'sakit' ? 'bg-warning' : 'bg-success';
|
|
const showConfidence = item.show_confidence !== false && item.confidence !== null && item.confidence !== undefined;
|
|
const confidence = showConfidence ? Number(item.confidence || 0).toFixed(1) : '-';
|
|
const safeConfidence = showConfidence ? Math.max(0, Math.min(100, Number(item.confidence || 0))) : 0;
|
|
|
|
tr.innerHTML = `
|
|
<td><span class="timestamp-realtime" data-timestamp="${item.timestamp || ''}">${formatTimestamp(item.timestamp)}</span></td>
|
|
<td>${imageCell}</td>
|
|
<td><span class="badge ${badgeClass}">${badgeText}</span></td>
|
|
<td>
|
|
${showConfidence ? `<div class="progress" style="height: 20px;">
|
|
<div class="progress-bar ${progressClass}" style="width: ${safeConfidence}%">${confidence}%</div>
|
|
</div>` : '<span class="text-muted">-</span>'}
|
|
</td>
|
|
<td>
|
|
<a href="${item.detail_url}" class="btn btn-sm btn-info">
|
|
<i class="fas fa-eye"></i> Detail
|
|
</a>
|
|
</td>
|
|
`;
|
|
|
|
return tr;
|
|
}
|
|
|
|
function updateStats(stats) {
|
|
const statTotal = document.getElementById('statTotal');
|
|
const statSehat = document.getElementById('statSehat');
|
|
const statSakit = document.getElementById('statSakit');
|
|
const statAkurasi = document.getElementById('statAkurasi');
|
|
|
|
if (statTotal) statTotal.textContent = stats.total ?? 0;
|
|
if (statSehat) statSehat.textContent = stats.negatif ?? 0;
|
|
if (statSakit) statSakit.textContent = stats.positif ?? 0;
|
|
if (statAkurasi) {
|
|
statAkurasi.textContent = `${Number(stats.akurasi_rata_rata ?? 0).toFixed(1)}%`;
|
|
}
|
|
}
|
|
|
|
async function loadRiwayatDeteksi() {
|
|
const ids = loadLocalStorageIds();
|
|
const tableBody = document.getElementById('riwayatTableBody');
|
|
const tableWrapper = document.getElementById('tableWrapper');
|
|
const emptyNotice = document.getElementById('emptyHistoryNotice');
|
|
const localStorageInfo = document.getElementById('localStorageInfo');
|
|
const historyStatus = document.getElementById('historyStatus');
|
|
|
|
if (!ids.length) {
|
|
if (tableWrapper) tableWrapper.classList.add('d-none');
|
|
if (emptyNotice) emptyNotice.classList.remove('d-none');
|
|
if (historyStatus) {
|
|
historyStatus.className = 'alert alert-warning';
|
|
historyStatus.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>Tidak ada ID riwayat pada localStorage browser ini.';
|
|
}
|
|
updateStats({ total: 0, positif: 0, negatif: 0, akurasi_rata_rata: 0 });
|
|
if (tableBody) tableBody.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch("{{ url_for('get_data_riwayat_deteksi') }}", {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ ids })
|
|
});
|
|
|
|
const payload = await response.json();
|
|
if (!response.ok || !payload.success) {
|
|
throw new Error(payload.message || 'Gagal memuat data riwayat');
|
|
}
|
|
|
|
const recent = Array.isArray(payload.recent) ? payload.recent : [];
|
|
const stats = payload.stats || { total: 0, positif: 0, negatif: 0, akurasi_rata_rata: 0 };
|
|
|
|
if (tableBody) {
|
|
tableBody.innerHTML = '';
|
|
recent.forEach(item => tableBody.appendChild(createHistoryRow(item)));
|
|
}
|
|
|
|
updateStats(stats);
|
|
if (localStorageInfo) localStorageInfo.classList.remove('d-none');
|
|
|
|
if (historyStatus) {
|
|
historyStatus.className = 'alert alert-success';
|
|
historyStatus.innerHTML = '<i class="fas fa-check-circle me-2"></i>Data riwayat berhasil dimuat dari localStorage dan database.';
|
|
}
|
|
|
|
if (recent.length === 0) {
|
|
if (tableWrapper) tableWrapper.classList.add('d-none');
|
|
if (emptyNotice) emptyNotice.classList.remove('d-none');
|
|
} else {
|
|
if (tableWrapper) tableWrapper.classList.remove('d-none');
|
|
if (emptyNotice) emptyNotice.classList.add('d-none');
|
|
if (window.location.hash) {
|
|
setTimeout(() => {
|
|
const target = document.querySelector(window.location.hash);
|
|
if (target) {
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}, 150);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (tableBody) {
|
|
tableBody.innerHTML = '<tr><td colspan="5" class="text-center py-4 text-danger">Gagal memuat data riwayat.</td></tr>';
|
|
}
|
|
if (historyStatus) {
|
|
historyStatus.className = 'alert alert-danger';
|
|
historyStatus.innerHTML = `<i class="fas fa-exclamation-circle me-2"></i>${error.message || 'Gagal memuat data riwayat'}`;
|
|
}
|
|
if (emptyNotice) emptyNotice.classList.remove('d-none');
|
|
if (tableWrapper) tableWrapper.classList.remove('d-none');
|
|
updateStats({ total: 0, positif: 0, negatif: 0, akurasi_rata_rata: 0 });
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadRiwayatDeteksi);
|
|
</script>
|
|
{% endblock %}
|