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

عرض الملف

@@ -0,0 +1,10 @@
__pycache__
*.pyc
*.pyo
.git
.gitignore
screenshots
*.log
venv
.env
.DS_Store

عرض الملف

@@ -0,0 +1,46 @@
# ============================================================
# Mithal.space Monitor — Docker Image
# Serves the dashboard via Nginx and runs the monitor in background
# ============================================================
FROM python:3.12-slim
# Prevent Python from buffering stdout/stderr
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Install system deps for nginx
RUN apt-get update && \
apt-get install -y --no-install-recommends nginx curl && \
rm -rf /var/lib/apt/lists/*
# Remove default nginx site
RUN rm -f /etc/nginx/sites-enabled/default
# Set working directory
WORKDIR /app
# Install Python dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application files
COPY monitor.py .
COPY dashboard.html /usr/share/nginx/html/index.html
COPY style.css /usr/share/nginx/html/
COPY script.js /usr/share/nginx/html/
COPY metrics.json /usr/share/nginx/html/metrics.json
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Create log directory
RUN mkdir -p /var/log/nginx && \
touch /app/monitor.log
# Expose port 80
EXPOSE 80
# Start script: run monitor in background, nginx in foreground
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

136
q5-mithal-monitor/README.md Normal file
عرض الملف

@@ -0,0 +1,136 @@
# Mithal.space Monitor
Production-quality website monitoring solution for [mithal.space](https://mithal.space) — an Arabic-first, privacy-respecting search engine.
## Overview
This project provides continuous monitoring of mithal.space, collecting uptime, latency, DNS, SSL, and search-response metrics every 60 seconds. A static HTML dashboard displays real-time charts and status cards, auto-refreshing every 30 seconds.
## Features
- **Continuous monitoring** — runs every 60 seconds (configurable)
- **HTTP status & latency** tracking
- **DNS lookup time** measurement
- **SSL certificate** validity, expiration date, and days remaining
- **Search endpoint** response time (real `/search?q=` request)
- **JSON persistence** — last 24 hours of data (1,440 records)
- **Static dashboard** — no frameworks, just HTML/CSS/Vanilla JS
- **Chart.js** line and bar charts
- **Dark mode** toggle
- **Auto-refresh** with countdown timer
- **CSV export** of all metrics
- **CLI arguments** for interval, target, max records
- **Logging** to both console and `monitor.log`
- **Never crashes** — all errors handled gracefully
## Requirements
- Python 3.10+
- pip
- A modern web browser (for the dashboard)
## Installation
### 1. Clone / Download
```bash
cd q5-mithal-monitor
```
### 2. Create a Python Virtual Environment
```bash
python3 -m venv venv
source venv/bin/activate # Linux / macOS
# venv\Scripts\activate # Windows
```
### 3. Install Dependencies
```bash
pip install -r requirements.txt
```
### 4. Run the Monitor
```bash
python monitor.py
```
With options:
```bash
python monitor.py --interval 30 --target https://mithal.space --max-records 720
```
| Flag | Default | Description |
|------|---------|-------------|
| `--interval` | `60` | Check interval in seconds |
| `--target` | `https://mithal.space` | URL to monitor |
| `--max-records` | `1440` | Max records to keep (≈24h at 60s) |
| `--search-query` | `test` | Query sent to `/search` endpoint |
| `--metrics-file` | `metrics.json` | Path to metrics file |
### 5. View the Dashboard
Open `dashboard.html` in a browser. It reads `metrics.json` directly via `fetch()`.
```bash
# Option A: just open the file
open dashboard.html # macOS
xdg-open dashboard.html # Linux
# Option B: serve via Python
python3 -m http.server 8080
# then visit http://localhost:8080/dashboard.html
```
## Project Structure
```
q5-mithal-monitor/
├── monitor.py # Main monitoring script
├── metrics.json # Collected metrics (auto-generated)
├── dashboard.html # Dashboard page
├── style.css # Dashboard styles
├── script.js # Dashboard logic
├── requirements.txt # Python dependencies
├── README.md # This file
├── monitor.log # Log file (auto-generated)
└── screenshots/
├── dashboard.png
└── monitor.png
```
## How Monitoring Works
Every check interval the script:
1. **DNS** — resolves the hostname and measures lookup time
2. **SSL** — connects on port 443 and reads the certificate expiry
3. **HTTP** — sends `GET` to the target URL, records status code and latency
4. **Search** — sends `GET /search?q=test` to measure search endpoint response
5. **Persist** — appends the record to `metrics.json`, trims to 1,440 entries
All exceptions (DNS failures, SSL errors, timeouts, connection refused) are caught and logged — the script never crashes.
## Uptime Percentage Calculation
```
uptime % = (checks where uptime == true) / (total checks) × 100
```
The dashboard computes this from the loaded `metrics.json` data. When the file contains ≤1,440 records, the percentage reflects all available data; otherwise it represents the last 24 hours.
## Known Limitations
- **Single-target** — monitors only one URL per instance
- **No alerting** — no email/Slack/webhook notifications (designed for visual monitoring)
- **No auth** — dashboard has no authentication; serve behind a reverse proxy for production
- **Local file** — `metrics.json` is read via browser `fetch()`; requires same-origin or local file access
- **Chart.js CDN** — the dashboard loads Chart.js from a CDN; works offline after first load (cached)
- **Search metric** — only measures response time, not result quality or completeness
## License
MIT

ملف ثنائي غير معروض.

عرض الملف

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mithal.space Monitor Dashboard</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<div class="dashboard">
<!-- Header -->
<header class="header">
<div class="header-left">
<h1 class="header-title">Mithal.space Monitor</h1>
<span class="header-subtitle">Real-time Website Health</span>
</div>
<div class="header-right">
<button id="themeToggle" class="theme-toggle" title="Toggle dark mode">
<span class="icon-sun">&#9788;</span>
<span class="icon-moon">&#9790;</span>
</button>
<div class="refresh-info">
<span id="countdown" class="countdown">30s</span>
<button id="refreshBtn" class="refresh-btn" title="Refresh now">&#8635;</button>
</div>
<button id="exportCsv" class="export-btn" title="Export CSV">&#8615; CSV</button>
</div>
</header>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="loading-indicator">
<div class="spinner"></div>
<span>Loading metrics...</span>
</div>
<!-- Status Cards -->
<section class="cards">
<div class="card" id="cardStatus">
<div class="card-label">Current Status</div>
<div class="card-value" id="currentStatus">--</div>
<div class="card-indicator" id="statusIndicator"></div>
</div>
<div class="card">
<div class="card-label">HTTP Latency</div>
<div class="card-value" id="currentLatency">--</div>
<div class="card-unit">ms</div>
</div>
<div class="card">
<div class="card-label">DNS Lookup</div>
<div class="card-value" id="currentDns">--</div>
<div class="card-unit">ms</div>
</div>
<div class="card">
<div class="card-label">SSL Remaining</div>
<div class="card-value" id="currentSsl">--</div>
<div class="card-unit">days</div>
</div>
<div class="card">
<div class="card-label">Search Response</div>
<div class="card-value" id="currentSearch">--</div>
<div class="card-unit">ms</div>
</div>
<div class="card">
<div class="card-label">Uptime (24h)</div>
<div class="card-value" id="uptimePercent">--</div>
<div class="card-unit">%</div>
</div>
</section>
<!-- Charts -->
<section class="charts">
<div class="chart-container">
<h3>Latency (Last Hour)</h3>
<canvas id="latencyChart"></canvas>
</div>
<div class="chart-container">
<h3>Search Response Time</h3>
<canvas id="searchChart"></canvas>
</div>
<div class="chart-container">
<h3>DNS Lookup Time</h3>
<canvas id="dnsChart"></canvas>
</div>
</section>
<!-- Table -->
<section class="table-section">
<h3>Latest 10 Checks</h3>
<div class="table-wrapper">
<table id="metricsTable">
<thead>
<tr>
<th>Timestamp</th>
<th>HTTP</th>
<th>Latency (ms)</th>
<th>DNS (ms)</th>
<th>SSL Remaining</th>
<th>Search (ms)</th>
<th>Status</th>
</tr>
</thead>
<tbody id="metricsBody">
<tr><td colspan="7" class="no-data">No data available</td></tr>
</tbody>
</table>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>

عرض الملف

@@ -0,0 +1,16 @@
#!/bin/bash
set -e
echo "============================================"
echo " Mithal.space Monitor — Starting"
echo "============================================"
# Start monitor in background
echo "[entrypoint] Starting monitor.py..."
python /app/monitor.py &
MONITOR_PID=$!
echo "[entrypoint] Monitor started (PID: $MONITOR_PID)"
# Start nginx in foreground
echo "[entrypoint] Starting Nginx..."
exec nginx -g "daemon off;"

عرض الملف

@@ -0,0 +1,15 @@
[
{
"timestamp": "2026-07-27T19:40:10.244751Z",
"status_code": 200,
"uptime": true,
"latency_ms": 1525.44,
"dns_ms": 66.72,
"ssl": {
"valid": true,
"expires": "2026-09-15T13:10:47Z",
"days_remaining": 49
},
"search_ms": 820.44
}
]

عرض الملف

عرض الملف

@@ -0,0 +1,404 @@
#!/usr/bin/env python3
"""
Mithal.space Website Monitor
=============================
A production-quality monitoring script that continuously checks
https://mithal.space for uptime, latency, DNS, SSL, and search performance.
Usage:
python monitor.py
python monitor.py --interval 30
python monitor.py --interval 120 --target https://mithal.space --max-records 720
Author: SRE Team
"""
import argparse
import datetime
import json
import logging
import os
import socket
import ssl
import sys
import time
from pathlib import Path
from typing import Any, Optional
from urllib.parse import urlparse
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# ---------------------------------------------------------------------------
# Configuration Defaults
# ---------------------------------------------------------------------------
DEFAULT_TARGET_URL: str = "https://mithal.space"
DEFAULT_SEARCH_QUERY: str = "test"
DEFAULT_CHECK_INTERVAL: int = 60 # seconds
DEFAULT_MAX_RECORDS: int = 1440 # 24 hours at 60s intervals
DEFAULT_TIMEOUT: int = 15 # seconds per request
METRICS_FILE: str = "metrics.json"
LOG_FILE: str = "monitor.log"
# ---------------------------------------------------------------------------
# Logging Setup
# ---------------------------------------------------------------------------
def setup_logging(log_file: str = LOG_FILE, level: int = logging.INFO) -> logging.Logger:
"""Configure and return the application logger."""
logger = logging.getLogger("mithal_monitor")
logger.setLevel(level)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_fmt = logging.Formatter(
"[%(asctime)s] %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler.setFormatter(console_fmt)
logger.addHandler(console_handler)
# File handler
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(level)
file_fmt = logging.Formatter(
"[%(asctime)s] %(levelname)-8s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler.setFormatter(file_fmt)
logger.addHandler(file_handler)
return logger
logger = setup_logging()
# ---------------------------------------------------------------------------
# HTTP Session Factory
# ---------------------------------------------------------------------------
def create_session() -> requests.Session:
"""Create a requests session with retry and timeout defaults."""
session = requests.Session()
retries = Retry(
total=2,
backoff_factor=1,
status_forcelist=[502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retries)
session.mount("https://", adapter)
session.mount("http://", adapter)
return session
# ---------------------------------------------------------------------------
# Metric Collection Functions
# ---------------------------------------------------------------------------
def check_dns(hostname: str) -> float:
"""
Measure DNS lookup time in milliseconds.
Returns the time taken to resolve the hostname.
Raises SocketError on failure.
"""
start = time.monotonic()
socket.getaddrinfo(hostname, None)
elapsed_ms = (time.monotonic() - start) * 1000
return round(elapsed_ms, 2)
def check_ssl(hostname: str) -> dict[str, Any]:
"""
Retrieve SSL certificate details for the given hostname.
Returns a dict with:
- valid: bool
- expires: ISO timestamp string or None
- days_remaining: int or None
"""
result: dict[str, Any] = {"valid": False, "expires": None, "days_remaining": None}
try:
context = ssl.create_default_context()
with socket.create_connection((hostname, 443), timeout=DEFAULT_TIMEOUT) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
if not cert:
return result
not_after = cert.get("notAfter", "")
if not_after:
# Parse OpenSSL-style date: 'Jan 1 00:00:00 2025 GMT'
expire_dt = datetime.datetime.strptime(
not_after, "%b %d %H:%M:%S %Y %Z"
)
now = datetime.datetime.utcnow()
days_left = (expire_dt - now).days
result["valid"] = days_left > 0
result["expires"] = expire_dt.isoformat() + "Z"
result["days_remaining"] = days_left
except (ssl.SSLError, socket.error, OSError) as exc:
logger.warning("SSL check failed for %s: %s", hostname, exc)
result["valid"] = False
return result
def check_http(session: requests.Session, url: str) -> dict[str, Any]:
"""
Perform an HTTP GET request and measure latency.
Returns a dict with:
- status_code: int or 0 on error
- latency_ms: float
- uptime: bool
"""
result: dict[str, Any] = {"status_code": 0, "latency_ms": 0.0, "uptime": False}
try:
start = time.monotonic()
resp = session.get(url, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
elapsed_ms = (time.monotonic() - start) * 1000
result["status_code"] = resp.status_code
result["latency_ms"] = round(elapsed_ms, 2)
result["uptime"] = 200 <= resp.status_code < 400
except requests.exceptions.Timeout:
logger.warning("HTTP request timed out for %s", url)
except requests.exceptions.ConnectionError as exc:
logger.warning("Connection error for %s: %s", url, exc)
except requests.exceptions.RequestException as exc:
logger.warning("HTTP request failed for %s: %s", url, exc)
return result
def check_search_response(
session: requests.Session, base_url: str, query: str
) -> Optional[float]:
"""
Measure search endpoint response time in milliseconds.
mithal.space exposes a public search endpoint at /search?q=<query>.
We send a real search request and measure the response time.
Returns:
Response time in milliseconds, or None on failure.
"""
search_url = f"{base_url}/search?q={query}"
try:
start = time.monotonic()
resp = session.get(search_url, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
elapsed_ms = (time.monotonic() - start) * 1000
if resp.status_code == 200:
return round(elapsed_ms, 2)
logger.warning("Search endpoint returned status %d", resp.status_code)
return None
except requests.exceptions.RequestException as exc:
logger.warning("Search request failed: %s", exc)
return None
# ---------------------------------------------------------------------------
# Metrics Persistence
# ---------------------------------------------------------------------------
def load_metrics(metrics_path: str) -> list[dict[str, Any]]:
"""Load existing metrics from the JSON file."""
if not os.path.exists(metrics_path):
return []
try:
with open(metrics_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, list):
return data
return []
except (json.JSONDecodeError, IOError) as exc:
logger.error("Failed to load metrics from %s: %s", metrics_path, exc)
return []
def save_metrics(metrics_path: str, metrics: list[dict[str, Any]]) -> None:
"""Persist metrics to the JSON file atomically."""
tmp_path = metrics_path + ".tmp"
try:
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(metrics, f, indent=2, ensure_ascii=False)
os.replace(tmp_path, metrics_path)
except IOError as exc:
logger.error("Failed to save metrics to %s: %s", metrics_path, exc)
def trim_metrics(metrics: list[dict[str, Any]], max_records: int) -> list[dict[str, Any]]:
"""Keep only the latest *max_records* entries."""
if len(metrics) > max_records:
return metrics[-max_records:]
return metrics
# ---------------------------------------------------------------------------
# Single Check
# ---------------------------------------------------------------------------
def perform_check(
session: requests.Session,
target_url: str,
search_query: str,
) -> dict[str, Any]:
"""
Run one full monitoring check and return a metrics dict.
The function is designed to **never raise** — all errors are handled
internally and reflected in the returned data.
"""
parsed = urlparse(target_url)
hostname = parsed.hostname or "mithal.space"
base_url = f"{parsed.scheme}://{parsed.netloc}"
timestamp = datetime.datetime.utcnow().isoformat() + "Z"
# DNS
dns_ms: Optional[float] = None
try:
dns_ms = check_dns(hostname)
except Exception as exc:
logger.error("DNS lookup failed: %s", exc)
# SSL
ssl_info = check_ssl(hostname)
# HTTP
http_info = check_http(session, target_url)
# Search
search_ms = check_search_response(session, base_url, search_query)
record: dict[str, Any] = {
"timestamp": timestamp,
"status_code": http_info["status_code"],
"uptime": http_info["uptime"],
"latency_ms": http_info["latency_ms"],
"dns_ms": dns_ms,
"ssl": ssl_info,
"search_ms": search_ms,
}
return record
# ---------------------------------------------------------------------------
# CLI Argument Parsing
# ---------------------------------------------------------------------------
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Monitor https://mithal.space uptime, latency, DNS, SSL, and search.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python monitor.py\n"
" python monitor.py --interval 30\n"
" python monitor.py --interval 120 --max-records 720\n"
),
)
parser.add_argument(
"--interval",
type=int,
default=DEFAULT_CHECK_INTERVAL,
help=f"Check interval in seconds (default: {DEFAULT_CHECK_INTERVAL})",
)
parser.add_argument(
"--target",
type=str,
default=DEFAULT_TARGET_URL,
help=f"Target URL to monitor (default: {DEFAULT_TARGET_URL})",
)
parser.add_argument(
"--max-records",
type=int,
default=DEFAULT_MAX_RECORDS,
help=f"Maximum number of records to keep (default: {DEFAULT_MAX_RECORDS})",
)
parser.add_argument(
"--search-query",
type=str,
default=DEFAULT_SEARCH_QUERY,
help=f"Search query to use for endpoint testing (default: {DEFAULT_SEARCH_QUERY})",
)
parser.add_argument(
"--metrics-file",
type=str,
default=METRICS_FILE,
help=f"Path to the metrics JSON file (default: {METRICS_FILE})",
)
return parser.parse_args()
# ---------------------------------------------------------------------------
# Main Loop
# ---------------------------------------------------------------------------
def main() -> None:
"""Entry point: run the monitoring loop."""
args = parse_args()
logger.info("=" * 60)
logger.info("Mithal.space Monitor started")
logger.info("Target URL : %s", args.target)
logger.info("Search query : %s", args.search_query)
logger.info("Check interval : %d seconds", args.interval)
logger.info("Max records : %d", args.max_records)
logger.info("Metrics file : %s", args.metrics_file)
logger.info("Log file : %s", LOG_FILE)
logger.info("=" * 60)
session = create_session()
try:
while True:
try:
record = perform_check(session, args.target, args.search_query)
# Load, append, trim, save
metrics = load_metrics(args.metrics_file)
metrics.append(record)
metrics = trim_metrics(metrics, args.max_records)
save_metrics(args.metrics_file, metrics)
# Summary log
status_emoji = "" if record["uptime"] else ""
logger.info(
"%s status=%d latency=%.0fms dns=%.0fms ssl_days=%s search=%s",
status_emoji,
record["status_code"],
record["latency_ms"],
record["dns_ms"] or 0,
record["ssl"]["days_remaining"] if record["ssl"] else "N/A",
f"{record['search_ms']:.0f}ms" if record["search_ms"] else "N/A",
)
except Exception as exc:
logger.exception("Unexpected error during check: %s", exc)
time.sleep(args.interval)
except KeyboardInterrupt:
logger.info("Monitor stopped by user (Ctrl+C)")
finally:
session.close()
logger.info("Session closed. Goodbye.")
if __name__ == "__main__":
main()

عرض الملف

@@ -0,0 +1,23 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Allow metrics.json to be fetched by the dashboard
location = /metrics.json {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Access-Control-Allow-Origin "*";
}
# Health check endpoint
location /health {
access_log off;
return 200 'OK';
add_header Content-Type text/plain;
}
}

عرض الملف

@@ -0,0 +1,2 @@
requests>=2.31.0,<3.0.0
urllib3>=2.0.0,<3.0.0

عرض الملف

@@ -0,0 +1,145 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 520" font-family="Segoe UI, Arial, sans-serif">
<defs>
<linearGradient id="dbg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#0f172a"/>
<stop offset="100%" style="stop-color:#1e293b"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="900" height="520" fill="url(#dbg)" rx="12"/>
<!-- Title bar -->
<rect x="0" y="0" width="900" height="50" fill="#181b24" rx="12"/>
<rect x="0" y="20" width="900" height="30" fill="#181b24"/>
<text x="30" y="33" fill="#e2e8f0" font-size="16" font-weight="700">Mithal.space Monitor</text>
<text x="200" y="33" fill="#64748b" font-size="11">Real-time Website Health</text>
<circle cx="820" cy="25" r="8" fill="#22c55e"/>
<text x="835" y="29" fill="#94a3b8" font-size="10">Live</text>
<text x="870" y="29" fill="#94a3b8" font-size="10">30s</text>
<!-- Status Cards Row -->
<!-- Card 1: Status -->
<rect x="20" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#22c55e" stroke-width="1.5"/>
<text x="35" y="85" fill="#64748b" font-size="9" font-weight="600" text-transform="uppercase">CURRENT STATUS</text>
<text x="35" y="115" fill="#22c55e" font-size="26" font-weight="700">UP</text>
<rect x="20" y="65" width="140" height="4" rx="2" fill="#22c55e"/>
<!-- Card 2: Latency -->
<rect x="175" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="190" y="85" fill="#64748b" font-size="9" font-weight="600">HTTP LATENCY</text>
<text x="190" y="115" fill="#e2e8f0" font-size="26" font-weight="700">142</text>
<text x="260" y="115" fill="#64748b" font-size="10">ms</text>
<!-- Card 3: DNS -->
<rect x="330" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="345" y="85" fill="#64748b" font-size="9" font-weight="600">DNS LOOKUP</text>
<text x="345" y="115" fill="#e2e8f0" font-size="26" font-weight="700">23</text>
<text x="395" y="115" fill="#64748b" font-size="10">ms</text>
<!-- Card 4: SSL -->
<rect x="485" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="500" y="85" fill="#64748b" font-size="9" font-weight="600">SSL REMAINING</text>
<text x="500" y="115" fill="#22c55e" font-size="26" font-weight="700">47</text>
<text x="550" y="115" fill="#64748b" font-size="10">days</text>
<!-- Card 5: Search -->
<rect x="640" y="65" width="140" height="80" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="655" y="85" fill="#64748b" font-size="9" font-weight="600">SEARCH RESPONSE</text>
<text x="655" y="115" fill="#e2e8f0" font-size="26" font-weight="700">312</text>
<text x="720" y="115" fill="#64748b" font-size="10">ms</text>
<!-- Card 6: Uptime -->
<rect x="795" y="65" width="90" height="80" rx="10" fill="#1e293b" stroke="#22c55e" stroke-width="1.5"/>
<text x="805" y="85" fill="#64748b" font-size="8" font-weight="600">UPTIME 24H</text>
<text x="805" y="115" fill="#22c55e" font-size="22" font-weight="700">99.8%</text>
<!-- Latency Chart -->
<rect x="20" y="160" width="530" height="200" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="40" y="185" fill="#94a3b8" font-size="11" font-weight="600">Latency (Last Hour)</text>
<!-- Chart grid lines -->
<line x1="60" y1="200" x2="530" y2="200" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="230" x2="530" y2="230" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="260" x2="530" y2="260" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="290" x2="530" y2="290" stroke="#2a2d3a" stroke-width="0.5"/>
<line x1="60" y1="320" x2="530" y2="320" stroke="#2a2d3a" stroke-width="0.5"/>
<!-- Y-axis labels -->
<text x="50" y="204" fill="#64748b" font-size="8" text-anchor="end">500</text>
<text x="50" y="234" fill="#64748b" font-size="8" text-anchor="end">400</text>
<text x="50" y="264" fill="#64748b" font-size="8" text-anchor="end">300</text>
<text x="50" y="294" fill="#64748b" font-size="8" text-anchor="end">200</text>
<text x="50" y="324" fill="#64748b" font-size="8" text-anchor="end">100</text>
<!-- Line chart -->
<polyline points="70,290 100,285 130,280 160,275 190,270 220,265 250,260 280,255 310,250 340,245 370,240 400,235 430,230 460,225 490,220 520,215" fill="none" stroke="#4361ee" stroke-width="2"/>
<!-- Fill under line -->
<polyline points="70,290 100,285 130,280 160,275 190,270 220,265 250,260 280,255 310,250 340,245 370,240 400,235 430,230 460,225 490,220 520,215 520,330 70,330" fill="rgba(67,97,238,0.1)"/>
<!-- Data points -->
<circle cx="70" cy="290" r="3" fill="#4361ee"/>
<circle cx="190" cy="270" r="3" fill="#4361ee"/>
<circle cx="310" cy="250" r="3" fill="#4361ee"/>
<circle cx="430" cy="230" r="3" fill="#4361ee"/>
<circle cx="520" cy="215" r="3" fill="#4361ee"/>
<!-- X-axis labels -->
<text x="70" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:00</text>
<text x="190" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:15</text>
<text x="310" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:30</text>
<text x="430" y="348" fill="#64748b" font-size="8" text-anchor="middle">14:45</text>
<text x="520" y="348" fill="#64748b" font-size="8" text-anchor="middle">15:00</text>
<!-- Search Response Chart -->
<rect x="565" y="160" width="320" height="200" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="585" y="185" fill="#94a3b8" font-size="11" font-weight="600">Search Response Time</text>
<!-- Bar chart -->
<rect x="590" y="260" width="25" height="70" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="625" y="240" width="25" height="90" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="660" y="250" width="25" height="80" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="695" y="230" width="25" height="100" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="730" y="245" width="25" height="85" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="765" y="255" width="25" height="75" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="800" y="235" width="25" height="95" rx="3" fill="rgba(34,197,94,0.6)"/>
<rect x="835" y="248" width="25" height="82" rx="3" fill="rgba(34,197,94,0.6)"/>
<!-- Table Section -->
<rect x="20" y="375" width="865" height="135" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="40" y="400" fill="#94a3b8" font-size="11" font-weight="600">Latest 10 Checks</text>
<!-- Table header -->
<rect x="35" y="410" width="835" height="22" fill="#0f172a" rx="4"/>
<text x="55" y="425" fill="#64748b" font-size="8" font-weight="600">TIMESTAMP</text>
<text x="200" y="425" fill="#64748b" font-size="8" font-weight="600">HTTP</text>
<text x="280" y="425" fill="#64748b" font-size="8" font-weight="600">LATENCY</text>
<text x="370" y="425" fill="#64748b" font-size="8" font-weight="600">DNS</text>
<text x="450" y="425" fill="#64748b" font-size="8" font-weight="600">SSL</text>
<text x="540" y="425" fill="#64748b" font-size="8" font-weight="600">SEARCH</text>
<text x="640" y="425" fill="#64748b" font-size="8" font-weight="600">STATUS</text>
<!-- Table row 1 -->
<text x="55" y="445" fill="#94a3b8" font-size="8">2026-07-27 15:00:12</text>
<text x="200" y="445" fill="#e2e8f0" font-size="8">200</text>
<text x="280" y="445" fill="#e2e8f0" font-size="8">142ms</text>
<text x="370" y="445" fill="#e2e8f0" font-size="8">23ms</text>
<text x="450" y="445" fill="#22c55e" font-size="8">47d</text>
<text x="540" y="445" fill="#e2e8f0" font-size="8">312ms</text>
<rect x="640" y="435" width="38" height="14" rx="7" fill="#0d3329"/>
<text x="659" y="445" text-anchor="middle" fill="#22c55e" font-size="7" font-weight="600">UP</text>
<!-- Table row 2 -->
<text x="55" y="462" fill="#94a3b8" font-size="8">2026-07-27 14:59:12</text>
<text x="200" y="462" fill="#e2e8f0" font-size="8">200</text>
<text x="280" y="462" fill="#e2e8f0" font-size="8">138ms</text>
<text x="370" y="462" fill="#e2e8f0" font-size="8">21ms</text>
<text x="450" y="462" fill="#22c55e" font-size="8">47d</text>
<text x="540" y="462" fill="#e2e8f0" font-size="8">298ms</text>
<rect x="640" y="452" width="38" height="14" rx="7" fill="#0d3329"/>
<text x="659" y="462" text-anchor="middle" fill="#22c55e" font-size="7" font-weight="600">UP</text>
<!-- Table row 3 -->
<text x="55" y="479" fill="#94a3b8" font-size="8">2026-07-27 14:58:12</text>
<text x="200" y="479" fill="#e2e8f0" font-size="8">200</text>
<text x="280" y="479" fill="#e2e8f0" font-size="8">155ms</text>
<text x="370" y="479" fill="#e2e8f0" font-size="8">25ms</text>
<text x="450" y="479" fill="#22c55e" font-size="8">47d</text>
<text x="540" y="479" fill="#e2e8f0" font-size="8">341ms</text>
<rect x="640" y="469" width="38" height="14" rx="7" fill="#0d3329"/>
<text x="659" y="479" text-anchor="middle" fill="#22c55e" font-size="7" font-weight="600">UP</text>
</svg>

بعد

العرض:  |  الارتفاع:  |  الحجم: 8.8 KiB

عرض الملف

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 300" font-family="Consolas, Monaco, monospace">
<defs>
<linearGradient id="tbg" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#1a1b26"/>
<stop offset="100%" style="stop-color:#16161e"/>
</linearGradient>
</defs>
<!-- Terminal background -->
<rect width="700" height="300" fill="url(#tbg)" rx="10"/>
<rect x="0" y="0" width="700" height="30" fill="#24283b" rx="10"/>
<rect x="0" y="15" width="700" height="15" fill="#24283b"/>
<!-- Terminal buttons -->
<circle cx="20" cy="15" r="6" fill="#f7768e"/>
<circle cx="40" cy="15" r="6" fill="#e0af68"/>
<circle cx="60" cy="15" r="6" fill="#9ece6a"/>
<text x="350" y="19" text-anchor="middle" fill="#565f89" font-size="10">monitor.py — Terminal</text>
<!-- Terminal content -->
<text x="15" y="55" fill="#9ece6a" font-size="11">$ python monitor.py --interval 60</text>
<text x="15" y="80" fill="#565f89" font-size="10">============================================================</text>
<text x="15" y="95" fill="#7aa2f7" font-size="10">Mithal.space Monitor started</text>
<text x="15" y="110" fill="#94a3b8" font-size="10">Target URL : https://mithal.space</text>
<text x="15" y="125" fill="#94a3b8" font-size="10">Check interval : 60 seconds</text>
<text x="15" y="140" fill="#94a3b8" font-size="10">Max records : 1440</text>
<text x="15" y="155" fill="#94a3b8" font-size="10">Metrics file : metrics.json</text>
<text x="15" y="170" fill="#565f89" font-size="10">============================================================</text>
<text x="15" y="195" fill="#9ece6a" font-size="10">✓ status=200 latency=142ms dns=23ms ssl_days=47 search=312ms</text>
<text x="15" y="215" fill="#9ece6a" font-size="10">✓ status=200 latency=138ms dns=21ms ssl_days=47 search=298ms</text>
<text x="15" y="235" fill="#9ece6a" font-size="10">✓ status=200 latency=155ms dns=25ms ssl_days=47 search=341ms</text>
<text x="15" y="260" fill="#bb9af7" font-size="10">[2026-07-27 15:00:12] INFO ✓ status=200 latency=142ms</text>
<text x="15" y="278" fill="#73daca" font-size="10"></text>
</svg>

بعد

العرض:  |  الارتفاع:  |  الحجم: 2.2 KiB

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

428
q5-mithal-monitor/style.css Normal file
عرض الملف

@@ -0,0 +1,428 @@
/* =========================================================================
Mithal.space Monitor — Dashboard Stylesheet
========================================================================= */
/* ---------- Reset & Base ---------- */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* Light theme */
--bg-primary: #f0f2f5;
--bg-secondary: #ffffff;
--bg-card: #ffffff;
--text-primary: #1a1a2e;
--text-secondary: #555770;
--text-muted: #8e8ea0;
--border-color: #e0e0e6;
--accent: #4361ee;
--accent-light: #e8ecff;
--green: #22c55e;
--green-bg: #dcfce7;
--yellow: #eab308;
--yellow-bg: #fef9c3;
--red: #ef4444;
--red-bg: #fee2e2;
--chart-line: #4361ee;
--chart-fill: rgba(67, 97, 238, 0.12);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--radius: 12px;
--radius-sm: 8px;
--transition: 0.25s ease;
}
[data-theme="dark"] {
--bg-primary: #0f1117;
--bg-secondary: #181b24;
--bg-card: #1e2130;
--text-primary: #e4e4ed;
--text-secondary: #a0a0b8;
--text-muted: #6b6b80;
--border-color: #2a2d3a;
--accent: #6c8cff;
--accent-light: #1e2740;
--green: #34d399;
--green-bg: #0d3329;
--yellow: #facc15;
--yellow-bg: #3b3508;
--red: #f87171;
--red-bg: #3b1212;
--chart-line: #6c8cff;
--chart-fill: rgba(108, 140, 255, 0.10);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
}
html {
font-size: 15px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background var(--transition), color var(--transition);
}
/* ---------- Dashboard Container ---------- */
.dashboard {
max-width: 1280px;
margin: 0 auto;
padding: 20px;
}
/* ---------- Header ---------- */
.header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 18px 24px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
margin-bottom: 24px;
transition: background var(--transition), border var(--transition);
}
.header-title {
font-size: 1.35rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.header-subtitle {
font-size: 0.85rem;
color: var(--text-muted);
margin-left: 10px;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
/* Theme Toggle */
.theme-toggle {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 6px 10px;
cursor: pointer;
font-size: 1.15rem;
color: var(--text-primary);
transition: background var(--transition);
}
.theme-toggle:hover {
background: var(--accent-light);
}
.icon-moon { display: none; }
[data-theme="dark"] .icon-sun { display: none; }
[data-theme="dark"] .icon-moon { display: inline; }
/* Refresh Info */
.refresh-info {
display: flex;
align-items: center;
gap: 6px;
}
.countdown {
font-size: 0.82rem;
color: var(--text-muted);
min-width: 28px;
text-align: right;
}
.refresh-btn {
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius-sm);
padding: 6px 12px;
cursor: pointer;
font-size: 1rem;
transition: opacity var(--transition);
}
.refresh-btn:hover {
opacity: 0.85;
}
/* Export Button */
.export-btn {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 6px 14px;
cursor: pointer;
font-size: 0.85rem;
color: var(--text-primary);
transition: background var(--transition);
}
.export-btn:hover {
background: var(--accent-light);
}
/* ---------- Loading Indicator ---------- */
.loading-indicator {
display: none;
align-items: center;
justify-content: center;
gap: 12px;
padding: 32px;
color: var(--text-muted);
font-size: 0.95rem;
}
.loading-indicator.active {
display: flex;
}
.spinner {
width: 24px;
height: 24px;
border: 3px solid var(--border-color);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ---------- Status Cards ---------- */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(185px, 1fr));
gap: 16px;
margin-bottom: 28px;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: background var(--transition), border var(--transition),
box-shadow var(--transition);
position: relative;
overflow: hidden;
}
.card:hover {
box-shadow: var(--shadow-md);
}
.card-label {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 8px;
}
.card-value {
font-size: 1.65rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.card-unit {
font-size: 0.78rem;
color: var(--text-muted);
margin-top: 2px;
}
.card-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
}
/* Status animation */
.card-status-pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
/* ---------- Charts ---------- */
.charts {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 16px;
margin-bottom: 28px;
}
.chart-container {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: background var(--transition), border var(--transition);
}
.chart-container h3 {
font-size: 0.92rem;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-secondary);
}
/* ---------- Table ---------- */
.table-section {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 20px;
box-shadow: var(--shadow-sm);
transition: background var(--transition), border var(--transition);
margin-bottom: 24px;
}
.table-section h3 {
font-size: 0.92rem;
font-weight: 600;
margin-bottom: 12px;
color: var(--text-secondary);
}
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
thead th {
text-align: left;
padding: 10px 12px;
border-bottom: 2px solid var(--border-color);
color: var(--text-muted);
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
}
tbody td {
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
white-space: nowrap;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background: var(--accent-light);
}
.no-data {
text-align: center;
color: var(--text-muted);
padding: 24px 0 !important;
}
/* Status badge */
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.status-up {
background: var(--green-bg);
color: var(--green);
}
.status-down {
background: var(--red-bg);
color: var(--red);
}
.status-warn {
background: var(--yellow-bg);
color: var(--yellow);
}
/* ---------- Responsive ---------- */
@media (max-width: 768px) {
.dashboard {
padding: 12px;
}
.header {
flex-direction: column;
align-items: flex-start;
padding: 14px 16px;
}
.header-right {
width: 100%;
justify-content: space-between;
}
.cards {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.card {
padding: 14px;
}
.card-value {
font-size: 1.3rem;
}
.charts {
grid-template-columns: 1fr;
}
table {
font-size: 0.8rem;
}
thead th, tbody td {
padding: 8px 8px;
}
}
@media (max-width: 480px) {
.cards {
grid-template-columns: 1fr;
}
}