424 أسطر
15 KiB
JavaScript
424 أسطر
15 KiB
JavaScript
/* =========================================================================
|
||
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 =
|
||
'<tr><td colspan="7" class="no-data">No data available</td></tr>';
|
||
return;
|
||
}
|
||
|
||
els.metricsBody.innerHTML = last10
|
||
.map((m) => {
|
||
const ts = new Date(m.timestamp).toLocaleString();
|
||
const sslDays = m.ssl && m.ssl.days_remaining != null ? m.ssl.days_remaining : "--";
|
||
const searchMs = m.search_ms != null ? Math.round(m.search_ms) : "--";
|
||
|
||
let statusClass = "status-down";
|
||
let statusText = "DOWN";
|
||
if (m.uptime) {
|
||
if (m.status_code >= 300) {
|
||
statusClass = "status-warn";
|
||
statusText = "WARN";
|
||
} else {
|
||
statusClass = "status-up";
|
||
statusText = "UP";
|
||
}
|
||
}
|
||
|
||
return `<tr>
|
||
<td>${ts}</td>
|
||
<td>${m.status_code || "--"}</td>
|
||
<td>${m.latency_ms != null ? Math.round(m.latency_ms) : "--"}</td>
|
||
<td>${m.dns_ms != null ? Math.round(m.dns_ms) : "--"}</td>
|
||
<td>${sslDays}</td>
|
||
<td>${searchMs}</td>
|
||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Countdown & Auto-Refresh
|
||
// -----------------------------------------------------------------------
|
||
function resetCountdown() {
|
||
countdownValue = REFRESH_INTERVAL;
|
||
els.countdown.textContent = countdownValue + "s";
|
||
}
|
||
|
||
async function refreshAll() {
|
||
await fetchMetrics();
|
||
updateCards();
|
||
updateCharts();
|
||
updateTable();
|
||
resetCountdown();
|
||
}
|
||
|
||
function startCountdown() {
|
||
if (countdownTimer) clearInterval(countdownTimer);
|
||
countdownTimer = setInterval(() => {
|
||
countdownValue--;
|
||
if (countdownValue <= 0) {
|
||
refreshAll();
|
||
} else {
|
||
els.countdown.textContent = countdownValue + "s";
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// CSV Export
|
||
// -----------------------------------------------------------------------
|
||
function exportCsv() {
|
||
if (metricsData.length === 0) return;
|
||
|
||
const headers = [
|
||
"Timestamp",
|
||
"Status Code",
|
||
"Uptime",
|
||
"Latency (ms)",
|
||
"DNS (ms)",
|
||
"SSL Valid",
|
||
"SSL Expires",
|
||
"SSL Days Remaining",
|
||
"Search (ms)",
|
||
];
|
||
|
||
const rows = metricsData.map((m) => [
|
||
m.timestamp,
|
||
m.status_code,
|
||
m.uptime,
|
||
m.latency_ms,
|
||
m.dns_ms,
|
||
m.ssl ? m.ssl.valid : "",
|
||
m.ssl ? m.ssl.expires : "",
|
||
m.ssl ? m.ssl.days_remaining : "",
|
||
m.search_ms,
|
||
]);
|
||
|
||
const csvContent = [headers, ...rows].map((r) => r.join(",")).join("\n");
|
||
const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" });
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = "mithal_monitor_" + new Date().toISOString().slice(0, 10) + ".csv";
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Init
|
||
// -----------------------------------------------------------------------
|
||
function init() {
|
||
loadTheme();
|
||
refreshAll();
|
||
startCountdown();
|
||
|
||
els.refreshBtn.addEventListener("click", () => refreshAll());
|
||
els.themeToggle.addEventListener("click", toggleTheme);
|
||
els.exportCsv.addEventListener("click", exportCsv);
|
||
}
|
||
|
||
// Start when DOM is ready
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|