Prepare Soundbox Ops deployment

This commit is contained in:
Wira Basalamah
2026-06-06 20:58:04 +07:00
parent 60b1537c4c
commit 00580a98fc
16 changed files with 3238 additions and 270 deletions

View File

@ -15,34 +15,38 @@
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 999px; }
</style>
</head>
<body class="min-h-screen bg-slate-50 text-slate-950">
<aside class="fixed inset-y-0 left-0 z-40 hidden w-64 border-r border-slate-200 bg-white px-4 py-6 lg:flex lg:flex-col">
<body id="top" class="min-h-screen bg-slate-50 text-slate-950">
<aside class="fixed inset-y-0 left-0 z-50 hidden w-64 border-r border-slate-200 bg-white px-4 py-6 lg:flex lg:flex-col">
<div class="px-2">
<h1 class="text-xl font-extrabold text-blue-700">Soundbox Ops</h1>
<p class="mt-1 text-xs font-semibold uppercase text-slate-500">Monitoring Console</p>
<h1 class="text-[22px] font-extrabold leading-tight text-blue-700">Soundbox Ops</h1>
<p class="mt-1 text-[12px] font-bold uppercase leading-none text-slate-500">Monitoring Console</p>
</div>
<nav class="mt-8 flex flex-1 flex-col gap-1">
<a class="flex items-center gap-3 rounded-lg bg-blue-50 px-3 py-2 font-bold text-blue-700" href="/ui/soundbox-ops">
<span class="material-symbols-outlined">monitor_heart</span>
Soundbox Monitoring
<a class="flex h-11 items-center gap-3 rounded-lg bg-blue-50 px-3 text-[15px] font-semibold leading-none text-blue-700 transition-colors" href="/ui/soundbox-ops">
<span class="material-symbols-outlined shrink-0 text-[22px]">monitor_heart</span>
<span class="truncate">Monitoring</span>
</a>
<a class="flex items-center gap-3 rounded-lg px-3 py-2 text-slate-600 hover:bg-slate-100" href="/ui/device-registry-monitoring">
<span class="material-symbols-outlined">speaker_group</span>
Device Registry
<a class="flex h-11 items-center gap-3 rounded-lg px-3 text-[15px] font-semibold leading-none text-slate-600 transition-colors hover:bg-slate-100 hover:text-blue-700" href="/ui/device-registry-monitoring">
<span class="material-symbols-outlined shrink-0 text-[22px]">speaker_group</span>
<span class="truncate">Registry</span>
</a>
<a class="flex items-center gap-3 rounded-lg px-3 py-2 text-slate-600 hover:bg-slate-100" href="/ui/transaction-history-monitoring">
<span class="material-symbols-outlined">receipt_long</span>
Transactions
<a class="flex h-11 items-center gap-3 rounded-lg px-3 text-[15px] font-semibold leading-none text-slate-600 transition-colors hover:bg-slate-100 hover:text-blue-700" href="#mqtt-trace">
<span class="material-symbols-outlined shrink-0 text-[22px]">lan</span>
<span class="truncate">MQTT Trace</span>
</a>
<a class="flex items-center gap-3 rounded-lg px-3 py-2 text-slate-600 hover:bg-slate-100" href="/ui/admin-dashboard-overview">
<span class="material-symbols-outlined">dashboard</span>
Admin Overview
<a class="flex h-11 items-center gap-3 rounded-lg px-3 text-[15px] font-semibold leading-none text-slate-600 transition-colors hover:bg-slate-100 hover:text-blue-700" href="#config-commands">
<span class="material-symbols-outlined shrink-0 text-[22px]">settings_remote</span>
<span class="truncate">Config & Commands</span>
</a>
<a class="flex h-11 items-center gap-3 rounded-lg px-3 text-[15px] font-semibold leading-none text-slate-600 transition-colors hover:bg-slate-100 hover:text-blue-700" href="/ui/soundbox-catalog">
<span class="material-symbols-outlined shrink-0 text-[22px]">category</span>
<span class="truncate">Catalog</span>
</a>
</nav>
<div class="border-t border-slate-200 pt-4">
<button id="logout-button" class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-slate-600 hover:bg-slate-100">
<span class="material-symbols-outlined">logout</span>
Logout
<button id="logout-button" class="flex h-11 w-full items-center gap-3 rounded-lg px-3 text-left text-[15px] font-semibold leading-none text-slate-600 transition-colors hover:bg-slate-100 hover:text-blue-700">
<span class="material-symbols-outlined shrink-0 text-[22px]">logout</span>
<span class="truncate">Logout</span>
</button>
</div>
</aside>
@ -124,7 +128,7 @@
</div>
<div class="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(360px,0.8fr)]">
<section class="rounded-lg border border-slate-200 bg-white">
<section id="fleet-status" class="rounded-lg border border-slate-200 bg-white">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-5 py-4">
<div>
<h3 class="text-lg font-extrabold">Fleet Status</h3>
@ -155,10 +159,10 @@
</section>
<aside class="space-y-6">
<section class="rounded-lg border border-slate-200 bg-white">
<section id="config-commands" class="rounded-lg border border-slate-200 bg-white transition-shadow">
<div class="border-b border-slate-200 px-5 py-4">
<h3 class="text-lg font-extrabold">Operations Health</h3>
<p id="ops-generated" class="mt-1 text-sm text-slate-500">Waiting for summary</p>
<h3 class="text-lg font-extrabold">Config & Commands</h3>
<p id="ops-generated" class="mt-1 text-sm text-slate-500">Broker, config worker, and notification state</p>
</div>
<div class="space-y-3 p-5">
<div class="flex items-center justify-between rounded-lg bg-slate-50 px-4 py-3">
@ -180,7 +184,7 @@
</div>
</section>
<section class="rounded-lg border border-slate-200 bg-white">
<section id="mqtt-trace" class="rounded-lg border border-slate-200 bg-white transition-shadow">
<div class="border-b border-slate-200 px-5 py-4">
<h3 class="text-lg font-extrabold">Recent MQTT Trace</h3>
<p class="mt-1 text-sm text-slate-500">latest uplink and downlink records</p>
@ -199,6 +203,9 @@
(function () {
const api = window.AdminUIAPI;
const state = { devices: [], merchants: new Map(), mqtt: null, observability: null };
const isPreviewMode =
new URLSearchParams(window.location.search).get("preview") === "1" ||
((window.location.hostname === "127.0.0.1" || window.location.hostname === "localhost") && window.location.port === "4173");
const $ = (id) => document.getElementById(id);
const normalize = (value) => String(value || "").toLowerCase().trim();
@ -369,9 +376,124 @@
renderMqtt();
}
function focusSection(id) {
const target = document.getElementById(id);
if (!target) {
return;
}
target.scrollIntoView({ behavior: "smooth", block: "start" });
target.classList.add("ring-2", "ring-blue-500", "ring-offset-2");
window.setTimeout(() => {
target.classList.remove("ring-2", "ring-blue-500", "ring-offset-2");
}, 1400);
}
function loadPreviewData() {
const now = Date.now();
state.merchants = new Map([
["merchant_mbiz", "MBiz Jakarta"],
["merchant_demo", "Demo Mart"],
["merchant_qf100", "QF100 Pilot Store"]
]);
state.devices = [
{
id: "dev_qf100_static_01",
device_code: "QF100-STATIC-01",
serial_number: "SN-QF100-0001",
vendor: "QF100",
model: "QF100 Static",
communication_mode: "mqtt",
derived_status: "online",
latest_heartbeat: new Date(now - 90 * 1000).toISOString(),
health_summary: { score: 98 },
binding_summary: { merchant_id: "merchant_qf100" }
},
{
id: "dev_qf100_dynamic_01",
device_code: "QF100-DYN-01",
serial_number: "SN-QF100-0002",
vendor: "QF100",
model: "QF100 Dynamic",
communication_mode: "mqtt",
derived_status: "stale",
latest_heartbeat: new Date(now - 42 * 60 * 1000).toISOString(),
health_summary: { score: 64 },
binding_summary: { merchant_id: "merchant_mbiz" }
},
{
id: "dev_counter_03",
device_code: "SND-COUNTER-03",
serial_number: "SN-DEMO-0003",
vendor: "Generic",
model: "Soundbox V2",
communication_mode: "api",
derived_status: "degraded",
latest_heartbeat: new Date(now - 12 * 60 * 1000).toISOString(),
health_summary: { score: 72 },
binding_summary: { merchant_id: "merchant_demo" }
},
{
id: "dev_stock_04",
device_code: "SND-STOCK-04",
serial_number: "SN-DEMO-0004",
vendor: "Generic",
model: "Unassigned Stock",
communication_mode: "static",
derived_status: "offline",
latest_heartbeat: null,
health_summary: { score: 0 },
binding_summary: null
}
];
state.mqtt = {
publisher: {
mode: "broker",
connected: true,
broker_url: "mqtts://broker.bizone.id:8883"
},
subscriber: {
connected: true
},
last_messages: [
{
direction: "downlink",
topic: "devices/dev_qf100_static_01/downlink/qf100",
message_type: "payment_success",
publish_status: "sent",
created_at: new Date(now - 75 * 1000).toISOString()
},
{
direction: "uplink",
topic: "devices/dev_qf100_dynamic_01/uplink/dynamic-qr/request",
message_type: "dynamic_qr_request",
publish_status: "recorded",
created_at: new Date(now - 7 * 60 * 1000).toISOString()
},
{
direction: "downlink",
topic: "devices/dev_counter_03/downlink/config/push",
message_type: "config_push",
publish_status: "sent",
created_at: new Date(now - 18 * 60 * 1000).toISOString()
}
]
};
state.observability = {
generated_at: new Date().toISOString(),
database: { status: "ok" },
notifications: { pending_count: 1, failed_count: 0 },
export_jobs: { worker: { enabled: true } }
};
renderAll();
}
async function refresh() {
const error = $("error-banner");
error.classList.add("hidden");
if (isPreviewMode && !api.getToken()) {
loadPreviewData();
return;
}
try {
api.requireToken();
const [devices, merchants, mqtt, observability] = await Promise.all([
@ -402,8 +524,22 @@
api.clearToken();
window.location.href = "/ui/admin-login";
});
document.querySelectorAll("a[href^='#']").forEach((link) => {
link.addEventListener("click", (event) => {
const id = link.getAttribute("href").slice(1);
if (!id) {
return;
}
event.preventDefault();
history.replaceState(null, "", `#${id}`);
focusSection(id);
});
});
refresh();
if (window.location.hash) {
window.setTimeout(() => focusSection(window.location.hash.slice(1)), 250);
}
window.setInterval(refresh, 30000);
})();
</script>