chore: matikan realtime stream di dashboard

Ganti SSE /api/dashboard/stream dengan fetch sekali ke /api/dashboard/realtime supaya halaman dashboard lebih ringan dan cepat dibuka.
This commit is contained in:
mwpn
2026-03-06 16:55:03 +07:00
parent 8d7cdd05b7
commit 461c5c7882

View File

@@ -43,8 +43,8 @@
<div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm overflow-hidden"> <div class="rounded-2xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700"> <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold">Realtime Attendance</h2> <h2 class="text-lg font-semibold">Riwayat Absensi Terbaru</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Live stream via Server-Sent Events. Newest at top.</p> <p class="text-sm text-gray-500 dark:text-gray-400 mt-0.5">Daftar absen terakhir (tanpa live stream), paling baru di atas.</p>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="w-full text-left"> <table class="w-full text-left">
@@ -57,11 +57,7 @@
<th class="px-6 py-3 font-medium">Status</th> <th class="px-6 py-3 font-medium">Status</th>
</tr> </tr>
</thead> </thead>
<tbody id="attendance-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"> <tbody id="attendance-tbody" class="divide-y divide-gray-200 dark:divide-gray-700"></tbody>
<tr>
<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">Connecting to stream…</td>
</tr>
</tbody>
</table> </table>
</div> </div>
</div> </div>
@@ -131,10 +127,7 @@
.catch(function() {}); .catch(function() {});
var tbody = document.getElementById('attendance-tbody'); var tbody = document.getElementById('attendance-tbody');
var streamUrl = baseUrl + '/api/dashboard/stream'; var realtimeApiUrl = baseUrl + '/api/dashboard/realtime';
var afterId = 0;
var placeholderRow = null;
var connected = false;
function badgeClass(status) { function badgeClass(status) {
var s = (status || '').toUpperCase(); var s = (status || '').toUpperCase();
@@ -152,10 +145,6 @@
} }
function addRow(data) { function addRow(data) {
if (placeholderRow && placeholderRow.parentNode) {
placeholderRow.remove();
placeholderRow = null;
}
var tr = document.createElement('tr'); var tr = document.createElement('tr');
tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30'; tr.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/30';
tr.setAttribute('data-id', data.id); tr.setAttribute('data-id', data.id);
@@ -166,8 +155,7 @@
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(data.class_name) + '</td>' + '<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(data.class_name) + '</td>' +
'<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(data.subject) + '</td>' + '<td class="px-6 py-3 text-gray-600 dark:text-gray-400">' + escapeHtml(data.subject) + '</td>' +
'<td class="px-6 py-3"><span class="inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium ' + statusClass + '">' + escapeHtml(data.status) + '</span></td>'; '<td class="px-6 py-3"><span class="inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium ' + statusClass + '">' + escapeHtml(data.status) + '</span></td>';
tbody.insertBefore(tr, tbody.firstChild); tbody.appendChild(tr);
if (data.id > afterId) afterId = data.id;
} }
function escapeHtml(str) { function escapeHtml(str) {
@@ -177,43 +165,29 @@
return div.innerHTML; return div.innerHTML;
} }
function setPlaceholder(msg) { function loadRealtimeOnce() {
if (placeholderRow && placeholderRow.parentNode) placeholderRow.remove(); fetch(realtimeApiUrl, { method: 'GET', credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
placeholderRow = document.createElement('tr'); .then(function(res) { return res.json().then(function(j) { return { ok: res.ok, data: j }; }); })
placeholderRow.innerHTML = '<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">' + escapeHtml(msg) + '</td>'; .then(function(r) {
tbody.appendChild(placeholderRow); if (!r.ok) return;
var list = (r.data && r.data.data) ? r.data.data : (Array.isArray(r.data) ? r.data : []);
tbody.innerHTML = '';
if (!list.length) {
var tr = document.createElement('tr');
tr.innerHTML = '<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">Belum ada data absensi hari ini.</td>';
tbody.appendChild(tr);
return;
}
list.forEach(function(row) { addRow(row); });
})
.catch(function() {
var tr = document.createElement('tr');
tr.innerHTML = '<td colspan="5" class="px-6 py-8 text-center text-red-500 dark:text-red-400">Gagal memuat data absensi.</td>';
tbody.appendChild(tr);
});
} }
function connect() { loadRealtimeOnce();
var url = streamUrl + (afterId ? '?after_id=' + afterId : '');
var es = new EventSource(url);
es.addEventListener('attendance', function(e) {
try {
var data = JSON.parse(e.data);
if (data && data.id) addRow(data);
} catch (err) {}
});
es.addEventListener('heartbeat', function() {
if (!connected) {
connected = true;
setPlaceholder('No attendance records yet. Waiting for new check-ins…');
}
});
es.addEventListener('timeout', function() {
es.close();
connected = false;
setPlaceholder('Stream ended. Reconnecting…');
setTimeout(connect, 3000);
});
es.onerror = function() {
es.close();
connected = false;
setPlaceholder('Connection lost. Reconnecting…');
setTimeout(connect, 3000);
};
}
connect();
if (typeof window.addEventListener === 'function') { if (typeof window.addEventListener === 'function') {
window.addEventListener('beforeunload', function() { window.addEventListener('beforeunload', function() {