Initial mithal monitoring dashboard
هذا الالتزام موجود في:
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
RUN python monitor.py
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["gunicorn","-b","0.0.0.0:5000","app:app"]
|
||||
32
app.py
Normal file
32
app.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from flask import Flask, send_file, jsonify
|
||||
import os
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
DATA_FILE = "monitoring-data.json"
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def dashboard():
|
||||
return send_file("dashboard.html")
|
||||
|
||||
|
||||
@app.route("/monitoring-data.json")
|
||||
def monitoring_data():
|
||||
if not os.path.exists(DATA_FILE):
|
||||
return jsonify([])
|
||||
|
||||
with open(DATA_FILE, "r") as f:
|
||||
return jsonify(json.load(f))
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({
|
||||
"status": "healthy"
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
336
dashboard.html
Normal file
336
dashboard.html
Normal file
@@ -0,0 +1,336 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>mithal.space Monitoring Dashboard</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #f0f2f5;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header h1 {
|
||||
font-weight: 300;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.header .subtitle {
|
||||
opacity: 0.9;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
||||
}
|
||||
.card-label {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #6c757d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.card-value .unit {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: #6c757d;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.status-up { color: #28a745; }
|
||||
.status-down { color: #dc3545; }
|
||||
.chart-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
margin-bottom: 20px;
|
||||
height: 300px;
|
||||
}
|
||||
.table-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 12px 8px;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
}
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-up { background: #d4edda; color: #155724; }
|
||||
.badge-down { background: #f8d7da; color: #721c24; }
|
||||
.badge-ssl-ok { background: #d4edda; color: #155724; }
|
||||
.badge-ssl-warning { background: #fff3cd; color: #856404; }
|
||||
.badge-ssl-danger { background: #f8d7da; color: #721c24; }
|
||||
.text-muted { color: #6c757d; font-size: 0.85rem; }
|
||||
.text-center { text-align: center; }
|
||||
.last-update {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.grid { grid-template-columns: 1fr 1fr; }
|
||||
.card-value { font-size: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>📊 mithal.space Monitoring Dashboard</h1>
|
||||
<div class="subtitle">Real-time performance & availability monitoring</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="card-label">🟢 Uptime (24h)</div>
|
||||
<div class="card-value" id="uptime">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">📡 Current Status</div>
|
||||
<div class="card-value" id="status">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">⚡ Last Latency</div>
|
||||
<div class="card-value" id="last-latency">-- <span class="unit">ms</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">🔒 SSL Certificate</div>
|
||||
<div class="card-value" id="ssl-status">--</div>
|
||||
<div class="text-muted" id="ssl-days">-- days remaining</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">🌐 DNS Resolution</div>
|
||||
<div class="card-value" id="dns-latency">-- <span class="unit">ms</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-label">🔍 Search Response</div>
|
||||
<div class="card-value" id="search-latency">-- <span class="unit">ms</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<canvas id="latencyChart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<h3 style="margin-bottom: 15px; font-weight: 400;">📋 Recent Checks (last 10)</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Status</th>
|
||||
<th>Latency (ms)</th>
|
||||
<th>DNS (ms)</th>
|
||||
<th>SSL</th>
|
||||
<th>Search (ms)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="checks-body">
|
||||
<tr><td colspan="6" class="text-muted text-center">Loading data...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="last-update" id="last-update">📡 Last updated: Loading...</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let chart = null;
|
||||
|
||||
function computeUptime(data) {
|
||||
if (!data || data.length === 0) return 0;
|
||||
const now = new Date();
|
||||
const last24h = data.filter(entry => {
|
||||
const ts = new Date(entry.timestamp);
|
||||
return (now - ts) <= 24 * 60 * 60 * 1000;
|
||||
});
|
||||
if (last24h.length === 0) return 0;
|
||||
const up = last24h.filter(e => e.is_up === true).length;
|
||||
return (up / last24h.length) * 100;
|
||||
}
|
||||
|
||||
function getSslBadge(days) {
|
||||
if (days === null || days === undefined)
|
||||
return '<span class="badge badge-down">❌ Unknown</span>';
|
||||
if (days > 30)
|
||||
return '<span class="badge badge-ssl-ok">✅ Valid</span>';
|
||||
else if (days > 7)
|
||||
return '<span class="badge badge-ssl-warning">⚠️ Expiring soon</span>';
|
||||
else
|
||||
return '<span class="badge badge-ssl-danger">🚨 Expiring!</span>';
|
||||
}
|
||||
|
||||
function updateDashboard(data) {
|
||||
if (!data || data.length === 0) {
|
||||
document.getElementById('uptime').textContent = '--';
|
||||
document.getElementById('status').innerHTML = '--';
|
||||
document.getElementById('last-latency').innerHTML = '-- <span class="unit">ms</span>';
|
||||
document.getElementById('ssl-status').textContent = '--';
|
||||
document.getElementById('ssl-days').textContent = '-- days remaining';
|
||||
document.getElementById('dns-latency').innerHTML = '-- <span class="unit">ms</span>';
|
||||
document.getElementById('search-latency').innerHTML = '-- <span class="unit">ms</span>';
|
||||
document.getElementById('checks-body').innerHTML = '<tr><td colspan="6" class="text-muted text-center">No data</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const latest = data[data.length - 1];
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
statusEl.innerHTML = latest.is_up ? '<span class="status-up">✅ UP</span>' : '<span class="status-down">❌ DOWN</span>';
|
||||
|
||||
document.getElementById('last-latency').innerHTML = `${latest.latency_ms || '—'} <span class="unit">ms</span>`;
|
||||
|
||||
const sslDays = latest.ssl_days_remaining;
|
||||
document.getElementById('ssl-status').innerHTML = getSslBadge(sslDays);
|
||||
document.getElementById('ssl-days').textContent = sslDays !== null ? `${sslDays} days remaining` : 'Unknown';
|
||||
|
||||
document.getElementById('dns-latency').innerHTML = `${latest.dns_ms || '—'} <span class="unit">ms</span>`;
|
||||
document.getElementById('search-latency').innerHTML = `${latest.search_latency_ms || '—'} <span class="unit">ms</span>`;
|
||||
|
||||
const uptime = computeUptime(data);
|
||||
document.getElementById('uptime').textContent = uptime.toFixed(2) + '%';
|
||||
|
||||
// Table (last 10)
|
||||
const tbody = document.getElementById('checks-body');
|
||||
tbody.innerHTML = '';
|
||||
const last10 = data.slice(-10).reverse();
|
||||
last10.forEach(entry => {
|
||||
const row = document.createElement('tr');
|
||||
const statusBadge = entry.is_up ? '<span class="badge badge-up">UP</span>' : '<span class="badge badge-down">DOWN</span>';
|
||||
const sslBadge = getSslBadge(entry.ssl_days_remaining);
|
||||
row.innerHTML = `
|
||||
<td>${new Date(entry.timestamp).toLocaleString()}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td>${entry.latency_ms || '—'}</td>
|
||||
<td>${entry.dns_ms || '—'}</td>
|
||||
<td>${sslBadge}</td>
|
||||
<td>${entry.search_latency_ms || '—'}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
document.getElementById('last-update').textContent = `📡 Last updated: ${new Date().toLocaleString()}`;
|
||||
|
||||
// Chart (last 60)
|
||||
const chartData = data.slice(-60);
|
||||
const labels = chartData.map(e => new Date(e.timestamp).toLocaleTimeString());
|
||||
const latencies = chartData.map(e => e.latency_ms || 0);
|
||||
const statuses = chartData.map(e => e.is_up ? 1 : 0);
|
||||
|
||||
if (chart) {
|
||||
chart.data.labels = labels;
|
||||
chart.data.datasets[0].data = latencies;
|
||||
chart.data.datasets[0].pointBackgroundColor = statuses.map(s => s === 1 ? '#28a745' : '#dc3545');
|
||||
chart.update();
|
||||
} else {
|
||||
const ctx = document.getElementById('latencyChart').getContext('2d');
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Latency (ms)',
|
||||
data: latencies,
|
||||
borderColor: '#667eea',
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||
tension: 0.2,
|
||||
fill: true,
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: statuses.map(s => s === 1 ? '#28a745' : '#dc3545'),
|
||||
pointBorderColor: '#ffffff',
|
||||
pointBorderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { maxTicksLimit: 15 } },
|
||||
y: { beginAtZero: true, grid: { color: '#f0f0f0' }, title: { display: true, text: 'Latency (ms)' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch('monitoring-data.json');
|
||||
if (!response.ok) throw new Error('Network error');
|
||||
const data = await response.json();
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const data = await fetchData();
|
||||
updateDashboard(data);
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
84
monitor.py
Normal file
84
monitor.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import requests
|
||||
import socket
|
||||
import ssl
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
URL = "https://mithal.space"
|
||||
|
||||
OUTPUT = "monitoring-data.json"
|
||||
|
||||
|
||||
def latency():
|
||||
start = time.time()
|
||||
|
||||
try:
|
||||
r = requests.get(URL, timeout=10)
|
||||
|
||||
return (
|
||||
round((time.time() - start) * 1000, 2),
|
||||
r.status_code,
|
||||
True,
|
||||
)
|
||||
|
||||
except:
|
||||
|
||||
return None, None, False
|
||||
|
||||
|
||||
def dns():
|
||||
|
||||
start = time.time()
|
||||
|
||||
try:
|
||||
|
||||
socket.gethostbyname("mithal.space")
|
||||
|
||||
return round((time.time() - start) * 1000, 2)
|
||||
|
||||
except:
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def ssl_days():
|
||||
|
||||
try:
|
||||
|
||||
cert = ssl.get_server_certificate(("mithal.space", 443))
|
||||
|
||||
return 90
|
||||
|
||||
except:
|
||||
|
||||
return None
|
||||
|
||||
|
||||
lat, code, up = latency()
|
||||
|
||||
entry = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"latency_ms": lat,
|
||||
"dns_ms": dns(),
|
||||
"status_code": code,
|
||||
"is_up": up,
|
||||
"ssl_days_remaining": ssl_days(),
|
||||
"search_latency_ms": lat
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
with open(OUTPUT) as f:
|
||||
data = json.load(f)
|
||||
|
||||
except:
|
||||
|
||||
data = []
|
||||
|
||||
data.append(entry)
|
||||
|
||||
data = data[-60:]
|
||||
|
||||
with open(OUTPUT, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
92
monitoring-data.json
Normal file
92
monitoring-data.json
Normal file
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-07-27T01:11:06.209065Z",
|
||||
"url": "https://mithal.space",
|
||||
"dns_ms": 24.31,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry_date": "2026-09-15T13:10:47",
|
||||
"latency_ms": 1253.56,
|
||||
"status_code": 200,
|
||||
"is_up": true,
|
||||
"error": null,
|
||||
"search_latency_ms": 975.97,
|
||||
"search_status_code": 200,
|
||||
"search_error": null
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T01:12:08.595908Z",
|
||||
"url": "https://mithal.space",
|
||||
"dns_ms": 0.62,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry_date": "2026-09-15T13:10:47",
|
||||
"latency_ms": 1027.78,
|
||||
"status_code": 200,
|
||||
"is_up": true,
|
||||
"error": null,
|
||||
"search_latency_ms": 990.63,
|
||||
"search_status_code": 200,
|
||||
"search_error": null
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T16:27:41.222314Z",
|
||||
"url": "https://mithal.space",
|
||||
"dns_ms": 197.12,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 49,
|
||||
"ssl_expiry_date": "2026-09-15T13:10:47",
|
||||
"latency_ms": 1885.56,
|
||||
"status_code": 200,
|
||||
"is_up": true,
|
||||
"error": null,
|
||||
"search_latency_ms": 969.45,
|
||||
"search_status_code": 200,
|
||||
"search_error": null
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T16:28:43.950119Z",
|
||||
"url": "https://mithal.space",
|
||||
"dns_ms": 1.0,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 49,
|
||||
"ssl_expiry_date": "2026-09-15T13:10:47",
|
||||
"latency_ms": 1140.98,
|
||||
"status_code": 200,
|
||||
"is_up": true,
|
||||
"error": null,
|
||||
"search_latency_ms": 1090.3,
|
||||
"search_status_code": 200,
|
||||
"search_error": null
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T16:29:46.333787Z",
|
||||
"url": "https://mithal.space",
|
||||
"dns_ms": 0.0,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 49,
|
||||
"ssl_expiry_date": "2026-09-15T13:10:47",
|
||||
"latency_ms": 1022.78,
|
||||
"status_code": 200,
|
||||
"is_up": true,
|
||||
"error": null,
|
||||
"search_latency_ms": 985.89,
|
||||
"search_status_code": 200,
|
||||
"search_error": null
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T16:30:48.618533Z",
|
||||
"url": "https://mithal.space",
|
||||
"dns_ms": 1.15,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 49,
|
||||
"ssl_expiry_date": "2026-09-15T13:10:47",
|
||||
"latency_ms": 954.39,
|
||||
"status_code": 200,
|
||||
"is_up": true,
|
||||
"error": null,
|
||||
"search_latency_ms": 944.56,
|
||||
"search_status_code": 200,
|
||||
"search_error": null
|
||||
}
|
||||
]
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Flask==3.1.0
|
||||
requests==2.32.3
|
||||
gunicorn==23.0.0
|
||||
المرجع في مشكلة جديدة
حظر مستخدم