Complete QF100 ops commands and detail UI

This commit is contained in:
Wira Basalamah
2026-06-07 02:55:57 +07:00
parent 1550484d1d
commit e3d7e60ff3
8 changed files with 608 additions and 63 deletions

View File

@ -455,13 +455,14 @@ Rows
<img alt="Soundbox V2 Product" class="w-20 h-20 rounded-xl bg-slate-100" data-alt="A clean professional studio product shot of a minimalist electronic soundbox speaker device with a small LCD screen and premium matte plastic finish. The lighting is soft and corporate with subtle blue reflections on the surface consistent with a high-end fintech hardware brand. The background is a clean neutral white studio setting." src="https://lh3.googleusercontent.com/aida-public/AB6AXuC-CSPTCnxQuDTN1XM0atRPM9hIcVzf3zpbuxEUGTIlC-c1BivDqPa9osmBscvoiUcJeMBwUaXbZ6Ut5FuG2a91sVtZjzWRTgLck34kJJJy3N2E9O3uVtZw6InOpX9Gkph2OJxu_Z-PkR_t3F56EVZY3u8o2iZO3iH8hj9_ajrku7g1r_l54uobcRoN3dRH3k_at6GTuGbMtSSD4ew24sX8nePUsVvILKJauQLcMKD14J6mtAGm0x5PfViQQKdJzf_pYMqKswr3Yz4"/>
<div>
<h4 class="font-bold text-headline-md mb-1" id="device-detail-title">-</h4>
<p class="font-mono text-body-md text-slate-500" id="device-detail-serial">SN: -</p>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold bg-slate-100 text-slate-600" id="device-detail-model">Device</span>
</div>
</div>
<div class="space-y-6" id="device-detail-content"></div>
</div>
<div class="p-6 border-t border-slate-200 grid grid-cols-2 gap-4">
<button class="w-full py-2.5 border border-slate-200 rounded-xl font-bold hover:bg-slate-50 transition-colors">Reboot Device</button>
<button id="drawer-reboot-device" class="w-full py-2.5 border border-slate-200 rounded-xl font-bold hover:bg-slate-50 transition-colors">Reboot Device</button>
<button class="w-full py-2.5 bg-danger/10 text-danger rounded-xl font-bold hover:bg-danger/20 transition-colors">Unbind Merchant</button>
</div>
</div>
@ -495,8 +496,10 @@ Rows
const detailDrawer = document.getElementById("device-detail-drawer");
const detailCloseButton = document.getElementById("device-detail-close");
const detailTitle = document.getElementById("device-detail-title");
const detailSerial = document.getElementById("device-detail-serial");
const detailModel = document.getElementById("device-detail-model");
const detailContent = document.getElementById("device-detail-content");
const drawerRebootButton = document.getElementById("drawer-reboot-device");
const registerModal = document.getElementById("device-register-modal");
const registerForm = document.getElementById("device-register-form");
const topbarRegisterOpenButton = document.getElementById("topbar-register-device-open");
@ -612,6 +615,9 @@ Rows
if (value === "applied") {
return { label: "Applied", className: "bg-success/10 text-success border-success/20", icon: "check_circle" };
}
if (value === "pulled_not_pushed") {
return { label: "Pulled by Device", className: "bg-success/10 text-success border-success/20", icon: "check_circle" };
}
if (value === "pending_ack") {
return { label: "Pending ACK", className: "bg-warning/10 text-warning border-warning/20", icon: "pending" };
}
@ -834,6 +840,7 @@ Rows
let currentPage = 1;
let pageSize = Number(pageSizeSelect?.value || 10);
let currentSearchQuery = "";
let activeDrawerDevice = null;
let merchants = [];
let outlets = [];
let terminals = [];
@ -855,6 +862,7 @@ Rows
tableBody.innerHTML = items
.map((device) => {
const id = device.device_code || device.id || "";
const serialNumber = escapeHtml(device.serial_number || "-");
const model = device.model || "Unknown";
const binding = device.binding_summary || {};
const merchantName = merchantMap.get(binding.merchant_id) || "Unassigned";
@ -866,7 +874,10 @@ Rows
return `
<tr class="hover:bg-slate-50 transition-colors group">
<td class="px-6 py-row-height">
<span class="font-mono text-primary font-bold">${id || "-"}</span>
<div class="space-y-1">
<span class="block font-mono text-primary font-bold">${id || "-"}</span>
<span class="block font-mono text-[12px] text-slate-500">SN: ${serialNumber}</span>
</div>
</td>
<td class="px-6 py-row-height">${model}</td>
<td class="px-6 py-row-height">
@ -1140,6 +1151,7 @@ Rows
if (!detailDrawer || !detailOverlay || !detailTitle || !detailModel || !detailContent) {
return;
}
activeDrawerDevice = device;
const binding = device.binding_summary || {};
const connection = connectionMeta(device.communication_mode);
@ -1152,7 +1164,11 @@ Rows
: "No active warning";
const id = device.device_code || device.id || "-";
const serialNumber = escapeHtml(device.serial_number || "-");
detailTitle.textContent = id;
if (detailSerial) {
detailSerial.textContent = `SN: ${serialNumber}`;
}
detailModel.textContent = device.model || "Unknown";
detailModel.className = `inline-flex items-center px-2 py-0.5 rounded-full text-xs font-bold ${status.className}`;
@ -1160,6 +1176,10 @@ Rows
<section>
<h5 class="text-label-md font-label-md text-slate-500 uppercase mb-3">Device Detail</h5>
<div class="bg-slate-50 p-4 rounded-xl border border-slate-100">
<div class="flex justify-between gap-4 mb-2">
<span class="text-on-surface-variant">Serial Number</span>
<span class="font-mono font-bold text-right">${serialNumber}</span>
</div>
<div class="flex justify-between mb-2">
<span class="text-on-surface-variant">Model</span>
<span class="font-bold">${device.model || "Unknown"}</span>
@ -1233,6 +1253,33 @@ Rows
loadDrawerConfig(device.id);
};
const sendDrawerRebootCommand = async () => {
if (!activeDrawerDevice || !drawerRebootButton) {
return;
}
const originalText = drawerRebootButton.textContent || "Reboot Device";
drawerRebootButton.disabled = true;
drawerRebootButton.textContent = "Sending...";
try {
const result = await api.createDeviceCommand(activeDrawerDevice.id, {
command: "device.reboot",
payload: { requested_from: "device_registry_drawer" }
});
drawerRebootButton.textContent = result.status === "delivered" ? "Reboot Sent" : "Reboot Queued";
window.setTimeout(() => {
drawerRebootButton.textContent = originalText;
drawerRebootButton.disabled = false;
}, 1800);
} catch (error) {
drawerRebootButton.textContent = "Reboot Failed";
window.setTimeout(() => {
drawerRebootButton.textContent = originalText;
drawerRebootButton.disabled = false;
}, 2200);
}
};
const loadDrawerConfig = async (deviceId) => {
const box = document.getElementById("device-config-status-box");
if (!box || !deviceId) {
@ -1244,6 +1291,7 @@ Rows
const meta = configStatusMeta(configStatus.drift_status);
const latestPush = configStatus.latest_push;
const latestAck = configStatus.latest_ack;
const latestPull = configStatus.latest_config_pull;
const canRetry = configStatus.retry_recommended;
box.innerHTML = `
<div class="flex items-center justify-between gap-3 mb-3">
@ -1253,7 +1301,11 @@ Rows
</span>
<span class="font-mono text-slate-500">v${configStatus.desired_config_version || "-"}</span>
</div>
<div class="grid grid-cols-2 gap-3 text-sm mb-4">
<div class="grid grid-cols-3 gap-3 text-sm mb-4">
<div>
<p class="text-slate-500">Latest Pull</p>
<p class="font-bold">${latestPull ? formatLastSeen(latestPull.received_at || latestPull.timestamp) : "-"}</p>
</div>
<div>
<p class="text-slate-500">Latest Push</p>
<p class="font-bold">${latestPush ? formatLastSeen(latestPush.created_at) : "-"}</p>
@ -1472,6 +1524,7 @@ Rows
if (!detailDrawer || !detailOverlay) {
return;
}
activeDrawerDevice = null;
detailDrawer.classList.add("translate-x-full");
detailOverlay.classList.remove("opacity-100");
detailOverlay.classList.add("opacity-0", "pointer-events-none");
@ -1578,6 +1631,7 @@ Rows
});
detailOverlay?.addEventListener("click", closeDrawer);
detailCloseButton?.addEventListener("click", closeDrawer);
drawerRebootButton?.addEventListener("click", sendDrawerRebootCommand);
topbarRegisterOpenButton?.addEventListener("click", openRegisterModal);
refreshButton?.addEventListener("click", refresh);
registerCloseButton?.addEventListener("click", closeRegisterModal);