From 00389df601e91669207cab4c1b40674bb479b2ed Mon Sep 17 00:00:00 2001 From: "sas.fajri" Date: Thu, 30 Apr 2026 15:08:54 +0700 Subject: [PATCH] Improve SSE row highlight and pluk audio --- .../templates/dashboard/index.html | 376 +++++++++++++++++- .../dashboard/partials/arrivals.html | 3 +- .../dashboard/partials/stations.html | 6 +- 3 files changed, 373 insertions(+), 12 deletions(-) diff --git a/cpone-dashboard/templates/dashboard/index.html b/cpone-dashboard/templates/dashboard/index.html index 6385f98..ee8444e 100644 --- a/cpone-dashboard/templates/dashboard/index.html +++ b/cpone-dashboard/templates/dashboard/index.html @@ -5,12 +5,154 @@ {{$proj := .Project}} @@ -71,8 +213,10 @@ -
+ + {{range $i := seq 3}}
{{end}} @@ -129,8 +273,10 @@
-
+ +

Station Status

@@ -140,8 +286,10 @@
Memuat data...
-
+ +

Arrival List

View all @@ -165,7 +313,6 @@
- 0; + } + + function triggerArrivalRows(target) { + const rows = target.querySelectorAll('.arrival-row'); + if (!rows || !rows.length) return; + let highlighted = 0; + rows.forEach(function (row, idx) { + const key = row.dataset.arrivalKey || ''; + const isNew = key && !arrivalSnapshot.has(key); + if (key) arrivalSnapshot.add(key); + if (!isNew) return; + + highlighted++; + row.classList.remove('arrival-row-updated'); + void row.offsetWidth; + setTimeout(function () { + row.classList.add('arrival-row-updated'); + }, Math.min(idx * 70, 420)); + setTimeout(function () { + row.classList.remove('arrival-row-updated'); + }, 2800 + Math.min(idx * 70, 420)); + }); + return highlighted > 0; + } + + function triggerHighlight(target) { + if (!target) return; + const isStations = target.id === 'sse-stations'; + let didHighlightRows = false; + if (!isStations) { + if (target.id === 'sse-arrivals') { + didHighlightRows = !!triggerArrivalRows(target); + } else { + target.classList.remove('sse-updated'); + void target.offsetWidth; + target.classList.add('sse-updated'); + didHighlightRows = true; + } + } else { + didHighlightRows = !!triggerStationRows(target); + } + if (didHighlightRows) { + playPluk(); + } + clearTimeout(target._sseHighlightTimer); + target._sseHighlightTimer = setTimeout(function () { + target.classList.remove('sse-updated'); + }, 2800); + } + + function setupMutationHighlight(targetId) { + const target = document.getElementById(targetId); + if (!target) return; + let armed = false; + let lastTriggerAt = 0; + + const observer = new MutationObserver(function (mutations) { + if (!armed) { + armed = true; + return; + } + const hasRealChange = mutations.some(function (m) { + return (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) || + (m.type === 'characterData' && m.oldValue !== m.target.data); + }); + if (!hasRealChange) return; + + const now = Date.now(); + if (now - lastTriggerAt < 800) return; + lastTriggerAt = now; + const beforeClass = target.className; + triggerHighlight(target); + // Fallback audio for edge cases where row diff doesn't mark highlight. + if ((targetId === 'sse-stations' || targetId === 'sse-arrivals') && beforeClass === target.className) { + playPluk(); + } + }); + + observer.observe(target, { + childList: true, + subtree: true, + characterData: true, + characterDataOldValue: true + }); + } + + // Browser policy: audio baru bisa aktif setelah interaksi user. + ['pointerdown', 'mousedown', 'keydown', 'touchstart', 'focus'].forEach(function (evtName) { + window.addEventListener(evtName, unlockAudio, { passive: true, once: true }); + }); document.body.addEventListener('htmx:afterSwap', function (evt) { const target = evt.detail && evt.detail.target ? evt.detail.target : null; @@ -242,11 +580,29 @@ function openPatientsModal() { return; } - target.classList.remove('sse-updated'); - void target.offsetWidth; - target.classList.add('sse-updated'); + triggerHighlight(target); }); + // Fallback trigger: beberapa setup SSE HTMX tidak selalu menembakkan afterSwap seperti yang diharapkan. + document.body.addEventListener('htmx:sseMessage', function (evt) { + const eventName = evt.detail && evt.detail.event ? evt.detail.event : ''; + const targetId = sseEventTargetMap[eventName]; + if (!targetId) return; + const target = document.getElementById(targetId); + if (!target) return; + // Skip first SSE frame per target, then animate on subsequent updates. + if (!target.dataset.sseHydrated) { + target.dataset.sseHydrated = '1'; + return; + } + triggerHighlight(target); + }); + + // Hard fallback: pastikan perubahan konten section tetap men-trigger efek meski event SSE/HTMX tidak konsisten. + setupMutationHighlight('sse-kpi'); + setupMutationHighlight('sse-stations'); + setupMutationHighlight('sse-arrivals'); + const palette = ['#3b50a0', '#6677d6', '#10b981', '#f59e0b']; const tatData = {{.TATChart}}; const trendData = {{.TrendChart}}; diff --git a/cpone-dashboard/templates/dashboard/partials/arrivals.html b/cpone-dashboard/templates/dashboard/partials/arrivals.html index 9667bd9..9d9fe0f 100644 --- a/cpone-dashboard/templates/dashboard/partials/arrivals.html +++ b/cpone-dashboard/templates/dashboard/partials/arrivals.html @@ -16,7 +16,8 @@ {{if .Rows}}
    {{range .Rows}} -
  • +
  • {{.Name | initials}}
    diff --git a/cpone-dashboard/templates/dashboard/partials/stations.html b/cpone-dashboard/templates/dashboard/partials/stations.html index 34e4ed9..de16015 100644 --- a/cpone-dashboard/templates/dashboard/partials/stations.html +++ b/cpone-dashboard/templates/dashboard/partials/stations.html @@ -21,7 +21,11 @@ {{range .Rows}} - + {{.Station | stationShort}}