Upload files to "q5-mithal-monitor"
هذا الالتزام موجود في:
150
q5-mithal-monitor/dashboard (1).html
Normal file
150
q5-mithal-monitor/dashboard (1).html
Normal file
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>مراقبة mithal.space</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0b0f14; --card:#121821; --line:#212a36;
|
||||
--ok:#22c55e; --warn:#f59e0b; --bad:#ef4444; --text:#e6edf3; --muted:#8b98a8; --accent:#3b82f6;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font-family:-apple-system,Segoe UI,Tahoma,Arial,sans-serif;padding:24px}
|
||||
h1{font-size:20px;margin:0 0 4px}
|
||||
.subtitle{color:var(--muted);font-size:13px;margin-bottom:20px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:20px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px}
|
||||
.label{color:var(--muted);font-size:13px;margin-bottom:8px}
|
||||
.value{font-size:26px;font-weight:700}
|
||||
.value.small{font-size:16px;font-weight:600}
|
||||
.ring-wrap{display:flex;align-items:center;gap:14px}
|
||||
svg{transform:rotate(-90deg)}
|
||||
.chart-card{margin-bottom:20px}
|
||||
canvas{width:100%;height:180px}
|
||||
table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden}
|
||||
th,td{padding:10px 14px;text-align:right;border-bottom:1px solid var(--line);font-size:13px}
|
||||
th{color:var(--muted);font-weight:600}
|
||||
tr:last-child td{border-bottom:none}
|
||||
.pill{padding:2px 8px;border-radius:6px;font-size:12px}
|
||||
.pill.ok{background:rgba(34,197,94,.15);color:var(--ok)}
|
||||
.pill.bad{background:rgba(239,68,68,.15);color:var(--bad)}
|
||||
.pill.warn{background:rgba(245,158,11,.15);color:var(--warn)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔍 مراقبة mithal.space</h1>
|
||||
<div class="subtitle">Uptime · Latency · SSL · DNS · Search — يُحدَّث كل 60 ثانية من <code>latest.json</code></div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">Uptime (آخر 24 ساعة)</div>
|
||||
<div class="ring-wrap">
|
||||
<svg width="64" height="64" viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="28" stroke="#212a36" stroke-width="8" fill="none"/>
|
||||
<circle id="uptimeRing" cx="32" cy="32" r="28" stroke="#22c55e" stroke-width="8" fill="none"
|
||||
stroke-dasharray="176" stroke-dashoffset="176" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div class="value" id="uptimeValue">--%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">آخر زمن استجابة (Latency)</div>
|
||||
<div class="value" id="latencyValue">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">شهادة SSL — الأيام المتبقية</div>
|
||||
<div class="value" id="sslValue">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">زمن تحليل DNS</div>
|
||||
<div class="value" id="dnsValue">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">زمن استجابة البحث</div>
|
||||
<div class="value" id="searchValue">--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card chart-card">
|
||||
<div class="label">زمن الاستجابة — آخر ساعة (ms)</div>
|
||||
<canvas id="latencyChart"></canvas>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>الوقت</th><th>الحالة</th><th>زمن الاستجابة</th><th>DNS</th><th>بحث</th></tr></thead>
|
||||
<tbody id="historyBody"></tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
const SNAPSHOT_URL = "latest.json";
|
||||
const POLL_MS = 30000;
|
||||
|
||||
function drawChart(points) {
|
||||
const canvas = document.getElementById("latencyChart");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = canvas.clientWidth, h = 180;
|
||||
canvas.width = w * dpr; canvas.height = h * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
if (!points.length) return;
|
||||
const max = Math.max(...points, 10) * 1.2;
|
||||
const stepX = w / Math.max(points.length - 1, 1);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = "#3b82f6";
|
||||
ctx.lineWidth = 2;
|
||||
points.forEach((p, i) => {
|
||||
const x = i * stepX;
|
||||
const y = h - (p / max) * h;
|
||||
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
||||
});
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const res = await fetch(SNAPSHOT_URL + "?_=" + Date.now(), { cache: "no-store" });
|
||||
const data = await res.json();
|
||||
const last = data.last_check;
|
||||
const history = data.history || [];
|
||||
|
||||
document.getElementById("uptimeValue").textContent = `${data.uptime_percent_24h}%`;
|
||||
const circumference = 176;
|
||||
const offset = circumference - (data.uptime_percent_24h / 100) * circumference;
|
||||
document.getElementById("uptimeRing").setAttribute("stroke-dashoffset", offset);
|
||||
|
||||
document.getElementById("latencyValue").textContent = last.latency_ms != null ? `${last.latency_ms} ms` : "N/A";
|
||||
document.getElementById("dnsValue").textContent = last.dns_ms != null ? `${last.dns_ms} ms` : "N/A";
|
||||
document.getElementById("searchValue").textContent = last.search?.latency_ms != null ? `${last.search.latency_ms} ms` : "N/A";
|
||||
|
||||
if (last.ssl?.valid) {
|
||||
document.getElementById("sslValue").textContent = `${last.ssl.days_remaining} يوم`;
|
||||
} else {
|
||||
document.getElementById("sslValue").innerHTML = `<span class="pill bad">غير صالحة</span>`;
|
||||
}
|
||||
|
||||
const lastHour = history.slice(-60);
|
||||
drawChart(lastHour.map(h => h.latency_ms || 0));
|
||||
|
||||
const rows = history.slice(-10).reverse().map(h => `
|
||||
<tr>
|
||||
<td>${new Date(h.timestamp).toLocaleTimeString("ar-EG")}</td>
|
||||
<td><span class="pill ${h.uptime.up ? "ok" : "bad"}">${h.uptime.up ? "UP" : "DOWN"} (${h.uptime.status_code ?? "—"})</span></td>
|
||||
<td>${h.latency_ms ?? "—"} ms</td>
|
||||
<td>${h.dns_ms ?? "—"} ms</td>
|
||||
<td>${h.search?.ok ? "✅" : "❌"} ${h.search?.latency_ms ?? ""}</td>
|
||||
</tr>`).join("");
|
||||
document.getElementById("historyBody").innerHTML = rows;
|
||||
} catch (e) {
|
||||
console.error("Failed to load snapshot:", e);
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, POLL_MS);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
153
q5-mithal-monitor/monitor.py
Normal file
153
q5-mithal-monitor/monitor.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
monitor.py — Monitoring script for mithal.space (Q5)
|
||||
|
||||
Collects every minute:
|
||||
- Latency: time-to-first-byte for the homepage
|
||||
- Uptime: HTTP status code check
|
||||
- SSL: certificate validity + days remaining until expiry
|
||||
- DNS: DNS resolution time
|
||||
- Search response: time to get a response from a search query
|
||||
|
||||
Stores results as newline-delimited JSON (JSONL) for easy append + parsing,
|
||||
and a rolling `latest.json` snapshot consumed by dashboard.html.
|
||||
|
||||
Usage:
|
||||
python3 monitor.py # runs forever, checks every 60s
|
||||
python3 monitor.py --once # single check, useful for cron/testing
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
TARGET_URL = "https://mithal.space"
|
||||
SEARCH_URL = "https://mithal.space/search?q=غيمة"
|
||||
CHECK_INTERVAL_SECONDS = 60
|
||||
DATA_DIR = Path(__file__).parent
|
||||
JSONL_LOG = DATA_DIR / "metrics.jsonl"
|
||||
LATEST_SNAPSHOT = DATA_DIR / "latest.json"
|
||||
MAX_HISTORY_IN_SNAPSHOT = 1440 # 24h at 1-minute resolution
|
||||
|
||||
|
||||
def check_dns(hostname: str) -> float:
|
||||
"""Returns DNS resolution time in ms."""
|
||||
start = time.perf_counter()
|
||||
socket.gethostbyname(hostname)
|
||||
return round((time.perf_counter() - start) * 1000, 2)
|
||||
|
||||
|
||||
def check_http(url: str, timeout: int = 10):
|
||||
"""Returns (status_code, latency_ms) or (None, None) on failure."""
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
resp = requests.get(url, timeout=timeout, headers={"User-Agent": "mithal-monitor/1.0"})
|
||||
latency_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return resp.status_code, latency_ms
|
||||
except requests.RequestException as exc:
|
||||
return None, None
|
||||
|
||||
|
||||
def check_ssl(hostname: str, port: int = 443, timeout: int = 10):
|
||||
"""Returns dict with ssl validity + days_remaining, or None on failure."""
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
with socket.create_connection((hostname, port), timeout=timeout) as sock:
|
||||
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
not_after = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
||||
days_remaining = (not_after - datetime.now(timezone.utc)).days
|
||||
return {
|
||||
"valid": True,
|
||||
"expires_at": not_after.isoformat(),
|
||||
"days_remaining": days_remaining,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {"valid": False, "error": str(exc)}
|
||||
|
||||
|
||||
def check_search(url: str, timeout: int = 15):
|
||||
"""Sends a search query and measures response time."""
|
||||
status, latency_ms = check_http(url, timeout=timeout)
|
||||
return {"status_code": status, "latency_ms": latency_ms, "ok": status == 200}
|
||||
|
||||
|
||||
def run_check() -> dict:
|
||||
hostname = urlparse(TARGET_URL).hostname
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
dns_ms = None
|
||||
try:
|
||||
dns_ms = check_dns(hostname)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
status_code, latency_ms = check_http(TARGET_URL)
|
||||
ssl_info = check_ssl(hostname)
|
||||
search_info = check_search(SEARCH_URL)
|
||||
|
||||
return {
|
||||
"timestamp": timestamp,
|
||||
"target": TARGET_URL,
|
||||
"uptime": {
|
||||
"status_code": status_code,
|
||||
"up": status_code == 200,
|
||||
},
|
||||
"latency_ms": latency_ms,
|
||||
"dns_ms": dns_ms,
|
||||
"ssl": ssl_info,
|
||||
"search": search_info,
|
||||
}
|
||||
|
||||
|
||||
def append_jsonl(record: dict):
|
||||
with open(JSONL_LOG, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def update_snapshot(record: dict):
|
||||
history = []
|
||||
if LATEST_SNAPSHOT.exists():
|
||||
try:
|
||||
history = json.loads(LATEST_SNAPSHOT.read_text(encoding="utf-8")).get("history", [])
|
||||
except json.JSONDecodeError:
|
||||
history = []
|
||||
history.append(record)
|
||||
history = history[-MAX_HISTORY_IN_SNAPSHOT:]
|
||||
|
||||
up_count = sum(1 for h in history if h["uptime"]["up"])
|
||||
uptime_pct = round((up_count / len(history)) * 100, 2) if history else 0.0
|
||||
|
||||
snapshot = {
|
||||
"last_check": record,
|
||||
"uptime_percent_24h": uptime_pct,
|
||||
"history": history,
|
||||
}
|
||||
LATEST_SNAPSHOT.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Monitor mithal.space")
|
||||
parser.add_argument("--once", action="store_true", help="Run a single check and exit")
|
||||
parser.add_argument("--interval", type=int, default=CHECK_INTERVAL_SECONDS)
|
||||
args = parser.parse_args()
|
||||
|
||||
while True:
|
||||
record = run_check()
|
||||
append_jsonl(record)
|
||||
update_snapshot(record)
|
||||
print(json.dumps(record, ensure_ascii=False))
|
||||
|
||||
if args.once:
|
||||
break
|
||||
time.sleep(args.interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
المرجع في مشكلة جديدة
حظر مستخدم