TIFNGK_E41222722/templates/riwayat_deteksi.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 %}