الملفات
Mohamed Moustafa c653222887 first commit
2026-07-27 23:27:12 +03:00

424 أسطر
15 KiB
JavaScript
خام الرابط الدائم اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غامضة

هذا الملف يحتوي على أحرف Unicode قد تُخلط مع أحرف أخرى. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

/* =========================================================================
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();
}
})();