Update ops handoff and dashboard last seen
This commit is contained in:
@ -1,9 +1,69 @@
|
|||||||
# Codex Handoff - QRIS Soundbox Platform
|
# 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.
|
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
|
## Update Terbaru - 2026-06-06
|
||||||
|
|
||||||
- Fokus produk sekarang: `sms.bizone.id` menjadi portal utama Soundbox Ops / Monitoring, bukan katalog UI atau dashboard admin campuran.
|
- 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) {
|
function lastSeen(value) {
|
||||||
if (!value) return "No heartbeat";
|
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);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (Number.isNaN(date.getTime())) return value;
|
||||||
const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
|
const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
|
||||||
@ -242,6 +245,13 @@
|
|||||||
return `${Math.floor(minutes / 1440)}d ago`;
|
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) {
|
function statusMeta(value) {
|
||||||
const status = normalize(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" };
|
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>
|
||||||
<div class="mt-1 text-xs text-slate-500">Health ${healthLabel(device)}</div>
|
<div class="mt-1 text-xs text-slate-500">Health ${healthLabel(device)}</div>
|
||||||
</td>
|
</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">
|
<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}">
|
<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>
|
Detail <span class="material-symbols-outlined text-[16px]">open_in_new</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user