/* ========================================================================= Mithal.space Monitor — Dashboard JavaScript ========================================================================= */ (function () { "use strict"; // ----------------------------------------------------------------------- // Configuration // ----------------------------------------------------------------------- const METRICS_URL = "metrics.json"; const REFRESH_INTERVAL = 30; // seconds const CHART_MAX_POINTS = 60; // last hour (60 × 60s) const LAST_HOUR_CHECKS = 60; // ----------------------------------------------------------------------- // State // ----------------------------------------------------------------------- let countdownValue = REFRESH_INTERVAL; let countdownTimer = null; let metricsData = []; let latencyChart = null; let searchChart = null; let dnsChart = null; // ----------------------------------------------------------------------- // DOM References // ----------------------------------------------------------------------- const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); const els = { loading: $("#loadingIndicator"), currentStatus: $("#currentStatus"), statusIndicator: $("#statusIndicator"), currentLatency: $("#currentLatency"), currentDns: $("#currentDns"), currentSsl: $("#currentSsl"), currentSearch: $("#currentSearch"), uptimePercent: $("#uptimePercent"), countdown: $("#countdown"), refreshBtn: $("#refreshBtn"), themeToggle: $("#themeToggle"), exportCsv: $("#exportCsv"), metricsBody: $("#metricsBody"), }; // ----------------------------------------------------------------------- // Theme // ----------------------------------------------------------------------- function loadTheme() { const saved = localStorage.getItem("mithal-theme"); if (saved) { document.documentElement.setAttribute("data-theme", saved); } } function toggleTheme() { const current = document.documentElement.getAttribute("data-theme"); const next = current === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", next); localStorage.setItem("mithal-theme", next); updateChartColors(); } // ----------------------------------------------------------------------- // Chart Colour Helpers // ----------------------------------------------------------------------- function getThemeColors() { const style = getComputedStyle(document.documentElement); return { line: style.getPropertyValue("--chart-line").trim(), fill: style.getPropertyValue("--chart-fill").trim(), text: style.getPropertyValue("--text-secondary").trim(), grid: style.getPropertyValue("--border-color").trim(), }; } function updateChartColors() { const c = getThemeColors(); [latencyChart, searchChart, dnsChart].forEach((chart) => { if (!chart) return; chart.options.scales.x.ticks.color = c.text; chart.options.scales.y.ticks.color = c.text; chart.options.scales.x.grid.color = c.grid; chart.options.scales.y.grid.color = c.grid; if (chart.data.datasets[0]) { chart.data.datasets[0].borderColor = c.line; chart.data.datasets[0].backgroundColor = c.fill; } chart.update("none"); }); } // ----------------------------------------------------------------------- // Fetch Metrics // ----------------------------------------------------------------------- async function fetchMetrics() { try { els.loading.classList.add("active"); const resp = await fetch(METRICS_URL + "?t=" + Date.now()); if (!resp.ok) throw new Error("HTTP " + resp.status); metricsData = await resp.json(); } catch (err) { console.warn("Failed to load metrics:", err); metricsData = []; } finally { els.loading.classList.remove("active"); } } // ----------------------------------------------------------------------- // Update Cards // ----------------------------------------------------------------------- function updateCards() { if (metricsData.length === 0) { els.currentStatus.textContent = "--"; els.currentLatency.textContent = "--"; els.currentDns.textContent = "--"; els.currentSsl.textContent = "--"; els.currentSearch.textContent = "--"; els.uptimePercent.textContent = "--"; return; } const latest = metricsData[metricsData.length - 1]; // Status if (latest.uptime) { els.currentStatus.textContent = "UP"; els.currentStatus.className = "card-value status-up"; els.statusIndicator.style.background = "var(--green)"; els.currentStatus.classList.add("card-status-pulse"); } else { els.currentStatus.textContent = "DOWN"; els.currentStatus.className = "card-value status-down"; els.statusIndicator.style.background = "var(--red)"; els.currentStatus.classList.remove("card-status-pulse"); } // Values els.currentLatency.textContent = latest.latency_ms != null ? Math.round(latest.latency_ms) : "--"; els.currentDns.textContent = latest.dns_ms != null ? Math.round(latest.dns_ms) : "--"; els.currentSearch.textContent = latest.search_ms != null ? Math.round(latest.search_ms) : "--"; // SSL if (latest.ssl && latest.ssl.days_remaining != null) { const days = latest.ssl.days_remaining; els.currentSsl.textContent = days; if (days <= 14) { els.currentSsl.className = "card-value status-down"; } else if (days <= 30) { els.currentSsl.className = "card-value status-warn"; } else { els.currentSsl.className = "card-value"; } } else { els.currentSsl.textContent = "--"; els.currentSsl.className = "card-value"; } // Uptime % const upChecks = metricsData.filter((m) => m.uptime).length; const uptime = ((upChecks / metricsData.length) * 100).toFixed(2); els.uptimePercent.textContent = uptime; if (parseFloat(uptime) >= 99.5) { els.uptimePercent.className = "card-value status-up"; } else if (parseFloat(uptime) >= 95) { els.uptimePercent.className = "card-value status-warn"; } else { els.uptimePercent.className = "card-value status-down"; } } // ----------------------------------------------------------------------- // Update Charts // ----------------------------------------------------------------------- function updateCharts() { const c = getThemeColors(); // Last hour of data (approx 60 checks at 60s interval) const lastHour = metricsData.slice(-LAST_HOUR_CHECKS); const labels = lastHour.map((m) => { const d = new Date(m.timestamp); return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }); const latencyData = lastHour.map((m) => (m.latency_ms != null ? Math.round(m.latency_ms) : null)); const searchData = lastHour.map((m) => (m.search_ms != null ? Math.round(m.search_ms) : null)); const dnsData = lastHour.map((m) => (m.dns_ms != null ? Math.round(m.dns_ms) : null)); // --- Latency Chart --- if (latencyChart) latencyChart.destroy(); latencyChart = new Chart($("#latencyChart"), { type: "line", data: { labels: labels, datasets: [ { label: "Latency (ms)", data: latencyData, borderColor: c.line, backgroundColor: c.fill, fill: true, tension: 0.35, pointRadius: 2, borderWidth: 2, }, ], }, options: chartOptions(c, "ms"), }); // --- Search Chart --- if (searchChart) searchChart.destroy(); searchChart = new Chart($("#searchChart"), { type: "line", data: { labels: labels, datasets: [ { label: "Search (ms)", data: searchData, borderColor: "#22c55e", backgroundColor: "rgba(34,197,94,0.1)", fill: true, tension: 0.35, pointRadius: 2, borderWidth: 2, }, ], }, options: chartOptions(c, "ms"), }); // --- DNS Chart --- if (dnsChart) dnsChart.destroy(); dnsChart = new Chart($("#dnsChart"), { type: "bar", data: { labels: labels, datasets: [ { label: "DNS (ms)", data: dnsData, backgroundColor: "rgba(234,179,8,0.6)", borderColor: "#eab308", borderWidth: 1, borderRadius: 3, }, ], }, options: chartOptions(c, "ms"), }); } function chartOptions(c, unit) { return { responsive: true, maintainAspectRatio: true, interaction: { intersect: false, mode: "index" }, plugins: { legend: { display: false }, tooltip: { backgroundColor: "rgba(0,0,0,0.8)", titleColor: "#fff", bodyColor: "#fff", padding: 10, cornerRadius: 8, callbacks: { label: function (ctx) { return ctx.parsed.y != null ? ctx.parsed.y + " " + unit : "N/A"; }, }, }, }, scales: { x: { ticks: { color: c.text, maxTicksLimit: 8, font: { size: 11 } }, grid: { color: c.grid }, }, y: { ticks: { color: c.text, font: { size: 11 } }, grid: { color: c.grid }, beginAtZero: true, }, }, }; } // ----------------------------------------------------------------------- // Update Table // ----------------------------------------------------------------------- function updateTable() { const last10 = metricsData.slice(-10).reverse(); if (last10.length === 0) { els.metricsBody.innerHTML = '