diff --git a/q5-mithal-monitor/Dockerfile b/q5-mithal-monitor/Dockerfile
new file mode 100644
index 0000000..f729810
--- /dev/null
+++ b/q5-mithal-monitor/Dockerfile
@@ -0,0 +1,5 @@
+FROM python:3.12-slim
+WORKDIR /app
+COPY monitor.py dashboard.html ./
+EXPOSE 5000
+CMD ["python", "monitor.py"]
\ No newline at end of file
diff --git a/q5-mithal-monitor/__pycache__/monitor.cpython-313.pyc b/q5-mithal-monitor/__pycache__/monitor.cpython-313.pyc
new file mode 100644
index 0000000..90aad1b
Binary files /dev/null and b/q5-mithal-monitor/__pycache__/monitor.cpython-313.pyc differ
diff --git a/q5-mithal-monitor/dashboard.html b/q5-mithal-monitor/dashboard.html
new file mode 100644
index 0000000..aa449d5
--- /dev/null
+++ b/q5-mithal-monitor/dashboard.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+ mithal.space monitor
+
+
+
+
+ mithal.space — Monitoring
+ loading...
+
+
+
+
+
SSL (days left)
+
-
+
+
+
+
+
+
+
Last 10 checks
+
+
+
+ | time |
+ latency |
+ dns |
+ search |
+ status |
+ result |
+
+
+
+
+
+
+
+
+
diff --git a/q5-mithal-monitor/metrics.csv b/q5-mithal-monitor/metrics.csv
new file mode 100644
index 0000000..7e8bb1f
--- /dev/null
+++ b/q5-mithal-monitor/metrics.csv
@@ -0,0 +1,2 @@
+timestamp,latency_ms,uptime,status_code,ssl_valid,ssl_expires,ssl_days_remaining,dns_ms,search_ms,search_status_code,overall
+2026-07-29T01:55:40.917123+00:00,705.91,True,200,True,2026-09-15T13:10:47+00:00,48,0.54,1684.08,200,True
diff --git a/q5-mithal-monitor/metrics.json b/q5-mithal-monitor/metrics.json
new file mode 100644
index 0000000..fa7ade2
--- /dev/null
+++ b/q5-mithal-monitor/metrics.json
@@ -0,0 +1,41 @@
+[
+ {
+ "timestamp": "2026-07-29T01:55:40.917123+00:00",
+ "latency_ms": 705.91,
+ "uptime": true,
+ "status_code": 200,
+ "ssl_valid": true,
+ "ssl_expires": "2026-09-15T13:10:47+00:00",
+ "ssl_days_remaining": 48,
+ "dns_ms": 0.54,
+ "search_ms": 1684.08,
+ "search_status_code": 200,
+ "overall": true
+ },
+ {
+ "timestamp": "2026-07-29T02:06:20.105132Z",
+ "latency_ms": 515.49,
+ "uptime": true,
+ "status_code": 200,
+ "dns_ms": 10.14,
+ "ssl_valid": false,
+ "ssl_expires": "Sep 15 13:10:47 2026 GMT",
+ "ssl_days_remaining": null,
+ "search_ms": 476.59,
+ "search_status_code": 200,
+ "overall": false
+ },
+ {
+ "timestamp": "2026-07-29T02:06:35.031662Z",
+ "latency_ms": 502.26,
+ "uptime": true,
+ "status_code": 200,
+ "dns_ms": 0.51,
+ "ssl_valid": false,
+ "ssl_expires": "Sep 15 13:10:47 2026 GMT",
+ "ssl_days_remaining": null,
+ "search_ms": 496.97,
+ "search_status_code": 200,
+ "overall": false
+ }
+]
\ No newline at end of file
diff --git a/q5-mithal-monitor/monitor.py b/q5-mithal-monitor/monitor.py
new file mode 100644
index 0000000..e4ecafa
--- /dev/null
+++ b/q5-mithal-monitor/monitor.py
@@ -0,0 +1,122 @@
+import json
+import os
+import socket
+import ssl
+import threading
+import time
+from datetime import datetime
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from urllib.error import HTTPError
+from urllib.request import urlopen
+
+URL = "https://mithal.space"
+SEARCH_URL = "https://mithal.space/search?q=test"
+HOST = "mithal.space"
+METRICS_FILE = "metrics.json"
+
+
+def check_http(url):
+ start = time.time()
+ status = 0
+ ok = False
+ try:
+ res = urlopen(url, timeout=15)
+ status = res.status
+ ok = status == 200
+ res.read(1024)
+ except HTTPError as e:
+ status = e.code
+ ok = status == 200
+ except Exception:
+ ok = False
+ ms = round((time.time() - start) * 1000, 2)
+ return ms, status, ok
+
+
+def check_dns():
+ start = time.time()
+ ok = False
+ try:
+ socket.gethostbyname(HOST)
+ ok = True
+ except Exception:
+ ok = False
+ ms = round((time.time() - start) * 1000, 2)
+ return ms, ok
+
+
+def check_ssl():
+ valid = False
+ expires = None
+ days_left = None
+ try:
+ ctx = ssl.create_default_context()
+ with socket.create_connection((HOST, 443), timeout=10) as s:
+ with ctx.wrap_socket(s, server_hostname=HOST) as ss:
+ cert = ss.getpeercert()
+ expires = cert["notAfter"]
+ exp_date = datetime.strptime(expires, "%b %d %H:%M:%S %Y %GMT")
+ days_left = (exp_date - datetime.utcnow()).days
+ valid = True
+ except Exception:
+ pass
+ return valid, expires, days_left
+
+
+def run_check():
+ latency, status_code, uptime = check_http(URL)
+ dns_ms, dns_ok = check_dns()
+ ssl_ok, ssl_expires, ssl_days = check_ssl()
+ search_ms, search_code, search_ok = check_http(SEARCH_URL)
+
+ row = {
+ "timestamp": datetime.utcnow().isoformat() + "Z",
+ "latency_ms": latency,
+ "uptime": uptime,
+ "status_code": status_code,
+ "dns_ms": dns_ms,
+ "ssl_valid": ssl_ok,
+ "ssl_expires": ssl_expires,
+ "ssl_days_remaining": ssl_days,
+ "search_ms": search_ms,
+ "search_status_code": search_code,
+ "overall": uptime and dns_ok and ssl_ok and search_ok,
+ }
+ return row
+
+
+def save_row(row):
+ data = []
+ if os.path.exists(METRICS_FILE):
+ with open(METRICS_FILE, "r", encoding="utf-8") as f:
+ data = json.load(f)
+
+ data.append(row)
+
+ # keep last 24 hours (1440 checks if every 1 min)
+ if len(data) > 1440:
+ data = data[-1440:]
+
+ with open(METRICS_FILE, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=2)
+
+
+def monitor_loop():
+ while True:
+ row = run_check()
+ save_row(row)
+ print(row)
+ time.sleep(60)
+
+
+def start_server():
+ os.chdir(os.path.dirname(os.path.abspath(__file__)))
+ server = HTTPServer(("0.0.0.0", 5000), SimpleHTTPRequestHandler)
+ print("dashboard: http://0.0.0.0:5000/dashboard.html")
+ server.serve_forever()
+
+
+if __name__ == "__main__":
+ save_row(run_check())
+ threading.Thread(target=monitor_loop, daemon=True).start()
+ start_server()