Polish device registry identity displays
This commit is contained in:
@ -16,14 +16,17 @@ Dokumen ini adalah snapshot kerja terakhir untuk melanjutkan project tanpa perlu
|
|||||||
- KPI warning sekarang menunjukkan breakdown stale vs degraded;
|
- KPI warning sekarang menunjukkan breakdown stale vs degraded;
|
||||||
- KPI card bisa dipakai sebagai quick filter;
|
- KPI card bisa dipakai sebagai quick filter;
|
||||||
- tabel Fleet Status menampilkan health bar, reason, signal, dan battery;
|
- tabel Fleet Status menampilkan health bar, reason, signal, dan battery;
|
||||||
- Device ID menjadi link langsung ke technical detail;
|
- kolom utama Fleet Status sekarang memakai Serial Number sebagai identitas utama, dengan device code hanya sebagai detail sekunder;
|
||||||
- Remote Actions menambahkan tombol `Power Off Device`;
|
- Remote Actions menambahkan tombol `Power Off Device`;
|
||||||
- layout mobile header/filter dibuat full-width agar tidak overflow.
|
- layout mobile header/filter dibuat full-width agar tidak overflow.
|
||||||
- Registry `/ui/device-registry-monitoring` sekarang mendukung koreksi device metadata:
|
- Registry `/ui/device-registry-monitoring` sekarang mendukung koreksi device metadata:
|
||||||
|
- tabel dan detail mengutamakan Serial Number, bukan internal device id;
|
||||||
- menu row punya `Edit Device`;
|
- menu row punya `Edit Device`;
|
||||||
|
- menu row baris bawah otomatis membuka ke atas agar tidak tertutup pagination;
|
||||||
- modal edit bisa koreksi `serial_number/dev-sn`, vendor, model, communication mode, status, dan firmware version;
|
- modal edit bisa koreksi `serial_number/dev-sn`, vendor, model, communication mode, status, dan firmware version;
|
||||||
- perubahan model ikut memperbarui `capability_profile_json` dari katalog model aktif;
|
- perubahan model ikut memperbarui `capability_profile_json` dari katalog model aktif;
|
||||||
- backend menolak duplicate `serial_number` supaya config pull/MQTT lookup tidak ambigu.
|
- backend menolak duplicate `serial_number` supaya config pull/MQTT lookup tidak ambigu.
|
||||||
|
- Device Technical Detail mengutamakan Serial Number di breadcrumb/title, menaruh device code sebagai detail, dan remote action menambahkan `Power Off Device`.
|
||||||
- Search UI yang sebelumnya dekoratif sudah mulai difungsikan:
|
- Search UI yang sebelumnya dekoratif sudah mulai difungsikan:
|
||||||
- Admin Dashboard global search route ke Device Registry, Merchant List, atau Transaction History dengan `?q=`;
|
- Admin Dashboard global search route ke Device Registry, Merchant List, atau Transaction History dengan `?q=`;
|
||||||
- Transaction History dan Merchant List membaca `?q=` sebagai initial search;
|
- Transaction History dan Merchant List membaca `?q=` sebagai initial search;
|
||||||
|
|||||||
@ -1062,9 +1062,21 @@ Changing SN affects config pull lookup and MQTT topic routing. Use the physical
|
|||||||
button.addEventListener("click", (event) => {
|
button.addEventListener("click", (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const rowId = event.currentTarget.getAttribute("data-id");
|
const rowId = event.currentTarget.getAttribute("data-id");
|
||||||
|
const activeMenu = Array.from(tableBody.querySelectorAll("[data-device-menu]"))
|
||||||
|
.find((menu) => menu.getAttribute("data-device-menu") === rowId);
|
||||||
|
const shouldOpen = Boolean(activeMenu?.classList.contains("hidden"));
|
||||||
tableBody.querySelectorAll("[data-device-menu]").forEach((menu) => {
|
tableBody.querySelectorAll("[data-device-menu]").forEach((menu) => {
|
||||||
menu.classList.toggle("hidden", menu.getAttribute("data-device-menu") !== rowId || !menu.classList.contains("hidden"));
|
menu.classList.add("hidden", "top-12");
|
||||||
|
menu.classList.remove("bottom-12");
|
||||||
});
|
});
|
||||||
|
if (!activeMenu || !shouldOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buttonRect = event.currentTarget.getBoundingClientRect();
|
||||||
|
const availableBelow = window.innerHeight - buttonRect.bottom;
|
||||||
|
activeMenu.classList.toggle("top-12", availableBelow >= 190);
|
||||||
|
activeMenu.classList.toggle("bottom-12", availableBelow < 190);
|
||||||
|
activeMenu.classList.remove("hidden");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -212,7 +212,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="text-body-md text-on-surface-variant flex items-center gap-1.5">
|
<p class="text-body-md text-on-surface-variant flex items-center gap-1.5">
|
||||||
<span class="material-symbols-outlined text-sm">tag</span>
|
<span class="material-symbols-outlined text-sm">tag</span>
|
||||||
<span id="device-serial-number">SN: -</span>
|
<span id="device-serial-number">Code: -</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-body-md text-on-surface-variant flex items-center gap-1.5">
|
<p class="text-body-md text-on-surface-variant flex items-center gap-1.5">
|
||||||
<span class="material-symbols-outlined text-sm">schedule</span>
|
<span class="material-symbols-outlined text-sm">schedule</span>
|
||||||
@ -405,6 +405,13 @@ Loading
|
|||||||
</div>
|
</div>
|
||||||
<span class="material-symbols-outlined text-slate-300">chevron_right</span>
|
<span class="material-symbols-outlined text-slate-300">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="poweroff-device" class="w-full flex items-center justify-between p-3 border border-danger/20 bg-danger/5 rounded-lg hover:bg-danger/10 transition-all group">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-danger">power_settings_new</span>
|
||||||
|
<span class="font-body-md font-bold text-danger">Power Off Device</span>
|
||||||
|
</div>
|
||||||
|
<span class="material-symbols-outlined text-danger/50">chevron_right</span>
|
||||||
|
</button>
|
||||||
<button id="update-device-firmware" class="w-full flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-all group">
|
<button id="update-device-firmware" class="w-full flex items-center justify-between p-3 border border-slate-200 rounded-lg hover:bg-slate-50 transition-all group">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-primary">system_update</span>
|
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-primary">system_update</span>
|
||||||
@ -596,6 +603,7 @@ Copy Command
|
|||||||
const calendarButton = document.getElementById("detail-calendar-button");
|
const calendarButton = document.getElementById("detail-calendar-button");
|
||||||
const copyPayloadButton = document.getElementById("copy-payload-stream");
|
const copyPayloadButton = document.getElementById("copy-payload-stream");
|
||||||
const rebootButton = document.getElementById("reboot-device");
|
const rebootButton = document.getElementById("reboot-device");
|
||||||
|
const poweroffButton = document.getElementById("poweroff-device");
|
||||||
const updateFirmwareButton = document.getElementById("update-device-firmware");
|
const updateFirmwareButton = document.getElementById("update-device-firmware");
|
||||||
const unbindButton = document.getElementById("unbind-device");
|
const unbindButton = document.getElementById("unbind-device");
|
||||||
const decommissionButton = document.getElementById("decommission-device");
|
const decommissionButton = document.getElementById("decommission-device");
|
||||||
@ -1327,11 +1335,12 @@ Copy Command
|
|||||||
showingAllEvents = false;
|
showingAllEvents = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelCode = device.device_code || device.code || device.serial_number || device.id || "Unknown Device";
|
const serialNumber = device.serial_number || device.device_code || device.code || device.id || "Unknown Device";
|
||||||
setText(els.breadcrumbCode, modelCode);
|
const deviceCode = device.device_code || device.code || device.id || "-";
|
||||||
setText(els.title, modelCode);
|
setText(els.breadcrumbCode, serialNumber);
|
||||||
|
setText(els.title, serialNumber);
|
||||||
setText(els.model, device.model || device.device_model || "Unknown model");
|
setText(els.model, device.model || device.device_model || "Unknown model");
|
||||||
setText(els.serialNumber, `SN: ${device.serial_number || "-"}`);
|
setText(els.serialNumber, `Code: ${deviceCode}`);
|
||||||
setText(els.location, device.location || device.last_known_city || "Unknown");
|
setText(els.location, device.location || device.last_known_city || "Unknown");
|
||||||
|
|
||||||
const latest = Array.isArray(heartbeats) && heartbeats.length ? heartbeats[0] : null;
|
const latest = Array.isArray(heartbeats) && heartbeats.length ? heartbeats[0] : null;
|
||||||
@ -1440,6 +1449,17 @@ Copy Command
|
|||||||
viewAllEventsBtn.textContent = showingAllEvents ? "Show Recent Events" : "View All Events";
|
viewAllEventsBtn.textContent = showingAllEvents ? "Show Recent Events" : "View All Events";
|
||||||
});
|
});
|
||||||
rebootButton?.addEventListener("click", () => sendDeviceCommand("device.reboot", { requested_from: "device_detail" }, rebootButton));
|
rebootButton?.addEventListener("click", () => sendDeviceCommand("device.reboot", { requested_from: "device_detail" }, rebootButton));
|
||||||
|
poweroffButton?.addEventListener("click", async () => {
|
||||||
|
const confirmed = await confirmAction({
|
||||||
|
title: "Power off device",
|
||||||
|
message: `This will send a power-off command to ${currentDevice?.serial_number || activeDeviceId || "this device"}. The device may stop reporting until manually powered on again.`,
|
||||||
|
buttonLabel: "Power Off"
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sendDeviceCommand("device.poweroff", { requested_from: "device_detail" }, poweroffButton);
|
||||||
|
});
|
||||||
updateFirmwareButton?.addEventListener("click", () => sendDeviceCommand("firmware.update", {
|
updateFirmwareButton?.addEventListener("click", () => sendDeviceCommand("firmware.update", {
|
||||||
requested_from: "device_detail",
|
requested_from: "device_detail",
|
||||||
target_version: currentDevice?.firmware_version || "latest"
|
target_version: currentDevice?.firmware_version || "latest"
|
||||||
|
|||||||
@ -144,7 +144,7 @@
|
|||||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||||
<thead class="bg-slate-50 text-left text-xs font-bold uppercase text-slate-500">
|
<thead class="bg-slate-50 text-left text-xs font-bold uppercase text-slate-500">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-5 py-3">Soundbox</th>
|
<th class="px-5 py-3">Serial Number</th>
|
||||||
<th class="px-5 py-3">Merchant</th>
|
<th class="px-5 py-3">Merchant</th>
|
||||||
<th class="px-5 py-3">Mode</th>
|
<th class="px-5 py-3">Mode</th>
|
||||||
<th class="px-5 py-3">Health</th>
|
<th class="px-5 py-3">Health</th>
|
||||||
@ -373,6 +373,7 @@
|
|||||||
const mode = modeMeta(device.communication_mode);
|
const mode = modeMeta(device.communication_mode);
|
||||||
const score = device.health_summary?.score;
|
const score = device.health_summary?.score;
|
||||||
const code = device.device_code || device.id || "-";
|
const code = device.device_code || device.id || "-";
|
||||||
|
const serial = device.serial_number || device.device_code || device.id || "-";
|
||||||
const heartbeat = device.latest_heartbeat || {};
|
const heartbeat = device.latest_heartbeat || {};
|
||||||
const signal = heartbeat.network_strength;
|
const signal = heartbeat.network_strength;
|
||||||
const battery = heartbeat.battery_level;
|
const battery = heartbeat.battery_level;
|
||||||
@ -381,8 +382,8 @@
|
|||||||
return `
|
return `
|
||||||
<tr class="hover:bg-slate-50">
|
<tr class="hover:bg-slate-50">
|
||||||
<td class="px-5 py-4">
|
<td class="px-5 py-4">
|
||||||
<a class="font-bold text-slate-950 hover:text-blue-700" href="${detailUrl}">${esc(code)}</a>
|
<a class="mono font-bold text-slate-950 hover:text-blue-700" href="${detailUrl}">${esc(serial)}</a>
|
||||||
<div class="mono mt-1 text-xs text-slate-500">${esc(device.serial_number || device.id || "-")}</div>
|
<div class="mono mt-1 text-xs text-slate-500">Code: ${esc(code)}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-5 py-4">
|
<td class="px-5 py-4">
|
||||||
<div class="font-semibold">${esc(merchantName(device))}</div>
|
<div class="font-semibold">${esc(merchantName(device))}</div>
|
||||||
@ -438,7 +439,7 @@
|
|||||||
state.devices.forEach((device) => {
|
state.devices.forEach((device) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = device.id;
|
option.value = device.id;
|
||||||
option.textContent = `${device.device_code || device.serial_number || device.id} · ${device.serial_number || device.model || "soundbox"}`;
|
option.textContent = `${device.serial_number || device.device_code || device.id} · ${device.model || device.device_code || "soundbox"}`;
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
if (current && state.devices.some((device) => device.id === current)) {
|
if (current && state.devices.some((device) => device.id === current)) {
|
||||||
@ -644,7 +645,7 @@
|
|||||||
|
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.classList.add("opacity-60");
|
button.classList.add("opacity-60");
|
||||||
status.textContent = `Sending ${actionLabel.toLowerCase()} to ${device?.device_code || device?.serial_number || deviceId}...`;
|
status.textContent = `Sending ${actionLabel.toLowerCase()} to ${device?.serial_number || device?.device_code || deviceId}...`;
|
||||||
status.className = "mt-2 min-h-5 text-xs font-semibold text-slate-500";
|
status.className = "mt-2 min-h-5 text-xs font-semibold text-slate-500";
|
||||||
try {
|
try {
|
||||||
const result = await api.createDeviceCommand(deviceId, {
|
const result = await api.createDeviceCommand(deviceId, {
|
||||||
|
|||||||
Reference in New Issue
Block a user