هذا الالتزام موجود في:
Mohamed Moustafa
2026-07-27 23:27:12 +03:00
التزام c653222887
43 ملفات معدلة مع 3837 إضافات و0 حذوفات

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