Update ops handoff and dashboard last seen
This commit is contained in:
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user