const elements = { health: document.querySelector("#health"), payloadCount: document.querySelector("#payloadCount"), apiCount: document.querySelector("#apiCount"), serverCount: document.querySelector("#serverCount"), activity: document.querySelector("#activity"), refresh: document.querySelector("#refresh"), autoRefresh: document.querySelector("#autoRefresh"), requestSearch: document.querySelector("#requestSearch"), methodFilter: document.querySelector("#methodFilter"), statusFilter: document.querySelector("#statusFilter"), requestSort: document.querySelector("#requestSort"), requestLimit: document.querySelector("#requestLimit"), requestSummary: document.querySelector("#requestSummary"), payloadModal: document.querySelector("#payloadModal"), payloadModalTitle: document.querySelector("#payloadModalTitle"), payloadMeta: document.querySelector("#payloadMeta"), payloadDetails: document.querySelector("#payloadDetails"), closePayloadModal: document.querySelector("#closePayloadModal"), columnToggles: [...document.querySelectorAll("[data-column-toggle]")], }; const columnPreferenceKey = "arduino-playground-visible-columns"; const autoRefreshPreferenceKey = "arduino-playground-auto-refresh"; let requests = []; function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (character) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """, })[character]); } function readColumnPreferences() { try { return JSON.parse(localStorage.getItem(columnPreferenceKey)) ?? {}; } catch { return {}; } } function applyColumnPreferences() { const preferences = readColumnPreferences(); for (const toggle of elements.columnToggles) { const column = toggle.dataset.columnToggle; if (column in preferences) toggle.checked = preferences[column]; document.querySelectorAll(`[data-column="${column}"]`).forEach((cell) => { cell.hidden = !toggle.checked; }); } } function saveColumnPreferences() { const preferences = Object.fromEntries( elements.columnToggles.map((toggle) => [toggle.dataset.columnToggle, toggle.checked]), ); localStorage.setItem(columnPreferenceKey, JSON.stringify(preferences)); applyColumnPreferences(); } function updateMethodFilter() { const selected = elements.methodFilter.value; const methods = [...new Set(requests.map((event) => event.detail.method).filter(Boolean))].sort(); elements.methodFilter.innerHTML = '' + methods.map((method) => ``).join(""); elements.methodFilter.value = methods.includes(selected) ? selected : "all"; } function filteredRequests() { const query = elements.requestSearch.value.trim().toLocaleLowerCase(); const method = elements.methodFilter.value; const statusClass = elements.statusFilter.value; const sort = elements.requestSort.value; return requests.filter((event) => { const detail = event.detail; if (method !== "all" && detail.method !== method) return false; if (statusClass !== "all" && Math.floor((detail.status ?? 0) / 100) !== Number(statusClass)) return false; return !query || JSON.stringify(event).toLocaleLowerCase().includes(query); }).sort((left, right) => { if (sort === "oldest") return new Date(left.timestamp) - new Date(right.timestamp); if (sort === "status-asc") return (left.detail.status ?? 0) - (right.detail.status ?? 0); if (sort === "status-desc") return (right.detail.status ?? 0) - (left.detail.status ?? 0); return new Date(right.timestamp) - new Date(left.timestamp); }); } function openPayloadModal(event) { const detail = event.detail; if (!detail.payload) return; elements.payloadModalTitle.textContent = `${detail.method} ${detail.path}`; elements.payloadMeta.innerHTML = `
Time
${escapeHtml(new Date(event.timestamp).toLocaleString("en-GB"))}
Device
${escapeHtml(detail.deviceId ?? "—")}
Status
${escapeHtml(detail.status ?? "—")}
`; elements.payloadDetails.textContent = JSON.stringify(detail.payload, null, 2); elements.payloadModal.showModal(); } function renderRequests() { const matching = filteredRequests(); const limit = Number(elements.requestLimit.value); const visible = matching.slice(0, limit); elements.activity.innerHTML = visible.map((event) => { const detail = event.detail; const hasPayload = Boolean(detail.payload); return ` ${new Date(event.timestamp).toLocaleString("en-GB")} ${escapeHtml(detail.deviceId ?? "—")} ${escapeHtml(detail.clientName ?? "—")} ${escapeHtml(detail.path ?? "—")} ${escapeHtml(detail.method ?? "—")} ${escapeHtml(detail.status ?? "—")} ${hasPayload ? '' : "—"} `; }).join("") || 'No matching requests.'; elements.requestSummary.textContent = `Showing ${visible.length} of ${matching.length} matching requests (${requests.length} total).`; applyColumnPreferences(); } async function refresh() { elements.refresh.disabled = true; try { const dashboardRequest = { headers: { "X-Monitor-Request": "dashboard" } }; const [healthResponse, payloadResponse, activityResponse] = await Promise.all([ fetch("/api/health", dashboardRequest), fetch("/api/payloads/count", dashboardRequest), fetch("/api/activity", dashboardRequest), ]); if (![healthResponse, payloadResponse, activityResponse].every((response) => response.ok)) { throw new Error("Serveren returnerede en fejl"); } const [health, payloadSummary, activity] = await Promise.all([ healthResponse.json(), payloadResponse.json(), activityResponse.json(), ]); elements.health.textContent = `API online · ${new Date(health.timestamp).toLocaleTimeString("en-GB")}`; elements.health.className = "status online"; elements.payloadCount.textContent = payloadSummary.count; elements.apiCount.textContent = activity.filter((event) => event.source === "api").length; elements.serverCount.textContent = activity.filter((event) => event.source === "server").length; requests = activity.filter((event) => event.source === "api"); updateMethodFilter(); renderRequests(); } catch { elements.health.textContent = "Server offline"; elements.health.className = "status"; } finally { elements.refresh.disabled = false; } } elements.refresh.addEventListener("click", refresh); elements.columnToggles.forEach((toggle) => toggle.addEventListener("change", saveColumnPreferences)); [elements.requestSearch, elements.methodFilter, elements.statusFilter, elements.requestSort, elements.requestLimit] .forEach((control) => control.addEventListener("input", renderRequests)); elements.activity.addEventListener("click", (event) => { const row = event.target.closest("[data-request-id]"); const request = requests.find((item) => item.id === row?.dataset.requestId); if (request) openPayloadModal(request); }); elements.activity.addEventListener("keydown", (event) => { if (!["Enter", " "].includes(event.key)) return; const row = event.target.closest("[data-request-id]"); const request = requests.find((item) => item.id === row?.dataset.requestId); if (request?.detail.payload) { event.preventDefault(); openPayloadModal(request); } }); elements.closePayloadModal.addEventListener("click", () => elements.payloadModal.close()); elements.payloadModal.addEventListener("click", (event) => { if (event.target === elements.payloadModal) elements.payloadModal.close(); }); elements.autoRefresh.addEventListener("change", () => { localStorage.setItem(autoRefreshPreferenceKey, String(elements.autoRefresh.checked)); }); elements.autoRefresh.checked = localStorage.getItem(autoRefreshPreferenceKey) !== "false"; applyColumnPreferences(); refresh(); setInterval(() => { if (elements.autoRefresh.checked && !document.hidden) refresh(); }, 3000);