UNPKG

@sidequest/dashboard

Version:

@sidequest/dashboard is the web dashboard for Sidequest, a distributed background job queue system.

134 lines (116 loc) 3.99 kB
let currentRange = "12m"; const now = new Date(); const labels = []; for (let i = 11; i >= 0; i--) { const time = new Date(now.getTime() - i * 60000); labels.push(time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); } const ctx = document.getElementById("jobsTimeline").getContext("2d"); const jobsTimeline = new Chart(ctx, { type: "line", data: { labels: [], // will be set later datasets: [ { label: "Completed", data: [], borderColor: "rgb(34, 197, 94)", backgroundColor: "rgba(34, 197, 94, 0.1)", tension: 0.4, fill: true, }, { label: "Failed", data: [], borderColor: "rgb(239, 68, 68)", backgroundColor: "rgba(239, 68, 68, 0.1)", tension: 0.4, fill: true, }, ], }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { ticks: { color: "#ccc" }, grid: { color: "#333" }, }, y: { beginAtZero: true, ticks: { color: "#ccc" }, grid: { color: "#333" }, }, }, plugins: { legend: { labels: { color: "#ccc" }, }, tooltip: { mode: "index", intersect: false, }, }, }, }); async function refreshGraph() { const basePath = window.SIDEQUEST_BASE_PATH || ""; const res = await fetch(`${basePath}/dashboard/graph-data?range=${currentRange}`); const graph = await res.json(); const timestamps = graph.map((entry) => entry.timestamp); const newLabels = []; for (const timestamp of timestamps) { const bucketTime = new Date(timestamp); let label; if (currentRange === "12d") { label = bucketTime.toLocaleDateString([], { month: "short", day: "numeric" }); } else { label = bucketTime.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } newLabels.push(label); } const newCompleted = graph.map((entry) => entry.completed); const newFailed = graph.map((entry) => entry.failed); // Check if we have existing data and this is a time progression if (jobsTimeline.data.labels.length > 0 && newLabels.length === jobsTimeline.data.labels.length) { // Check if this is just a time shift by comparing labels const isTimeShift = jobsTimeline.data.labels[jobsTimeline.data.labels.length - 1] !== newLabels[newLabels.length - 1]; if (isTimeShift) { // Shift the timeline: remove first elements and add new ones at the end jobsTimeline.data.labels.shift(); jobsTimeline.data.labels.push(newLabels[newLabels.length - 1]); jobsTimeline.data.datasets[0].data.shift(); jobsTimeline.data.datasets[0].data.push(newCompleted[newCompleted.length - 1]); jobsTimeline.data.datasets[0].data[newCompleted.length - 2] = newCompleted[newCompleted.length - 2]; jobsTimeline.data.datasets[1].data.shift(); jobsTimeline.data.datasets[1].data.push(newFailed[newFailed.length - 1]); jobsTimeline.data.datasets[1].data[newFailed.length - 2] = newFailed[newFailed.length - 2]; jobsTimeline.update("default"); // Use default animation for smooth left shift return; } } // For initial load or when data structure changes, replace everything jobsTimeline.data.labels = newLabels; jobsTimeline.data.datasets[0].data = newCompleted; jobsTimeline.data.datasets[1].data = newFailed; jobsTimeline.update("default"); } // Helper function to compare arrays function arraysEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } refreshGraph(); const selectElement = document.getElementById("graph-range"); selectElement.addEventListener("change", (event) => { currentRange = event.target.value ?? "12m"; refreshGraph(); // Trigger HTMX to refresh stats htmx.trigger("#dashboard-stats", "refresh"); }); setInterval(refreshGraph, 1000);