Update ops handoff and dashboard last seen

This commit is contained in:
Wira Basalamah
2026-06-07 03:18:01 +07:00
parent 1e0f36f134
commit 836eb7db85
2 changed files with 72 additions and 2 deletions

View File

@ -1,9 +1,69 @@
# Codex Handoff - QRIS Soundbox Platform
Tanggal update: 2026-06-06, Asia/Jakarta.
Tanggal update: 2026-06-07, Asia/Jakarta.
Dokumen ini adalah snapshot kerja terakhir untuk melanjutkan project tanpa perlu membaca ulang seluruh chat.
## Update Terbaru - 2026-06-07
- Production saat ini fokus ke portal Soundbox Ops di `sms.bizone.id`, dengan MQTT broker `broker.bizone.id`.
- Commit terakhir yang sudah dipush sebelum update handoff ini:
- `1e0f36f Split MQTT trace and commands pages`
- `ef23b09 Parse QF100 heartbeat time as WIB`
- `e3d7e60 Complete QF100 ops commands and detail UI`
- Perubahan UI/ops terbaru:
- menu `MQTT Trace` sekarang halaman sendiri: `/ui/mqtt-trace`;
- menu `Config & Commands` sekarang halaman sendiri: `/ui/config-commands`;
- sidebar tidak lagi lompat ke anchor dashboard untuk dua menu tersebut;
- dashboard tetap ada summary ringkas, tetapi menu operasional utama pindah ke screen masing-masing.
- Halaman `/ui/mqtt-trace`:
- menampilkan trail uplink/downlink ala audit trail;
- filter direction `uplink/downlink`;
- filter message type;
- search topic/device/type/payload;
- limit 100/250/500;
- auto refresh 10 detik;
- klik event untuk lihat full payload JSON.
- Endpoint `/admin/mqtt/status` sekarang mendukung:
- `limit` sampai 500;
- `direction`;
- `message_type`;
- `device_id`.
- Halaman `/ui/config-commands`:
- menampilkan status database, pending/failed notification, export worker;
- menampilkan MQTT publisher/subscriber runtime;
- dropdown device dengan SN, model, connection, last seen;
- tombol `Reboot Device`;
- tombol `Open Device Detail`.
- Fix dashboard terbaru:
- kolom `Last Seen` di `/ui/soundbox-ops` tidak lagi menampilkan `[object Object]`;
- formatter sekarang membaca `latest_heartbeat.received_at`, `latest_heartbeat.timestamp`, lalu fallback `last_seen_at`.
- QF100 firmware categories yang sudah disiapkan:
- category `1`: payment sound, payload `data.pay-amount`;
- category `3`: heartbeat dari device;
- category `4`: dynamic QR display, payload `data.qr-url`, `data.amount`, `data.expire-seconds`;
- category `5`: reboot command, payload `data.command = "reboot"`.
- Dynamic QR:
- modal preview di Device Technical Detail sudah generate QR dari `data["qr-url"]`;
- tombol `Send Test QR` mengirim command `dynamic_qr.display`;
- backend menerima payload nested `header/data` dan publish category `4` ke `soundbox/{serial_number}/down`.
- Reboot:
- command `device.reboot` publish category `5` ke `soundbox/{serial_number}/down`;
- bisa dikirim dari Device Technical Detail, Device Registry drawer, dan `/ui/config-commands`.
- Heartbeat:
- QF100 mengirim `data.time` sebagai WIB/UTC+7;
- backend sekarang parse waktu itu sebagai WIB lalu simpan UTC;
- contoh `20260607023400` disimpan menjadi `2026-06-06T19:34:00.000Z`.
- Device Registry dan Device Detail:
- SN ditampilkan di tabel Device Registry;
- SN ditampilkan di drawer Device Detail;
- SN ditampilkan di header Device Technical Detail.
- Verifikasi lokal terakhir:
- `npm run typecheck`: pass;
- `node scripts/ui-qa-check.mjs`: pass;
- `node --check scripts/smoke-qf100-adapter.mjs`: pass pada patch QF100 sebelumnya;
- `git diff --check`: pass.
## Update Terbaru - 2026-06-06
- Fokus produk sekarang: `sms.bizone.id` menjadi portal utama Soundbox Ops / Monitoring, bukan katalog UI atau dashboard admin campuran.

View File

@ -233,6 +233,9 @@
function lastSeen(value) {
if (!value) return "No heartbeat";
if (typeof value === "object") {
return lastSeen(value.received_at || value.timestamp || value.created_at || value.updated_at);
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
@ -242,6 +245,13 @@
return `${Math.floor(minutes / 1440)}d ago`;
}
function deviceLastSeen(device) {
return device?.latest_heartbeat?.received_at ||
device?.latest_heartbeat?.timestamp ||
device?.last_seen_at ||
null;
}
function statusMeta(value) {
const status = normalize(value);
if (status === "online") return { label: "Online", badge: "bg-emerald-50 text-emerald-700 border-emerald-200", dot: "bg-emerald-500" };
@ -342,7 +352,7 @@
</div>
<div class="mt-1 text-xs text-slate-500">Health ${healthLabel(device)}</div>
</td>
<td class="px-5 py-4 text-right text-slate-600">${lastSeen(device.latest_heartbeat)}</td>
<td class="px-5 py-4 text-right text-slate-600">${lastSeen(deviceLastSeen(device))}</td>
<td class="px-5 py-4 text-right">
<a class="inline-flex items-center gap-1 rounded-lg border border-slate-200 px-3 py-1.5 text-xs font-bold text-slate-700 hover:bg-slate-50" href="${detailUrl}">
Detail <span class="material-symbols-outlined text-[16px]">open_in_new</span>