first commit
هذا الالتزام موجود في:
423
q5-mithal-monitor/script.js
Normal file
423
q5-mithal-monitor/script.js
Normal file
@@ -0,0 +1,423 @@
|
||||
/* =========================================================================
|
||||
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();
|
||||
}
|
||||
})();
|
||||
المرجع في مشكلة جديدة
حظر مستخدم