From 340847c88e2cc14abb244eaf49f6693c969437b2 Mon Sep 17 00:00:00 2001 From: Mistral Date: Mon, 20 Jul 2026 19:35:34 +0200 Subject: [PATCH 1/6] Fix(status-monitor): v1.1.1 - Corrections JavaScript et ajout CT202 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Corriger les guillemets dans onclick (SyntaxError JavaScript) - Corriger indentation setGlobalLoad - Ajouter service CT202 (Gitea) SSH + HTTP - Incrémenter version à 1.1.1 - Corriger deploy.sh (CRLF + méthode SCP depuis WSL) --- status-monitor/app.py | 853 ++++++++++++++++++++------------------- status-monitor/deploy.sh | 80 ++-- 2 files changed, 468 insertions(+), 465 deletions(-) diff --git a/status-monitor/app.py b/status-monitor/app.py index 6c26a92..85de485 100755 --- a/status-monitor/app.py +++ b/status-monitor/app.py @@ -1,425 +1,428 @@ -#!/usr/bin/env python3 -"""Homelab Status Monitor v1.1.0""" - -import socket -import ssl -import urllib.request -import json -import threading -from datetime import datetime -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer - -VERSION = "1.1.0" - -# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https) -SERVICES = { - # Containers LXC - "ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22}, - "ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"}, - - "ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22}, - "ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"}, - - "ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22}, - "ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"}, - "ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True}, - - "ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22}, - "ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"}, - - "ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22}, - "ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"}, - - "ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22}, - "ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"}, - "ct200-traefik-https": {"name": "Traefik HTTPS", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "insecure": True}, - - "ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22}, - "ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"}, - - # VM - "vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22}, - "vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/"}, - - # Proxmox hosts - "prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22}, - "prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22}, - - # Raspberry Pi - "pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22}, - - # External HTTPS endpoints - "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "insecure": True}, - "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "insecure": True}, - "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "insecure": True}, - "status-peis-fr": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "insecure": True}, -} - -status_history = {} -status_cache = {} -lock = threading.Lock() - - -def check_ssh(host, port, timeout=3): - """Check if SSH port is open.""" - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - result = sock.connect_ex((host, port)) - sock.close() - if result == 0: - return {"status": "online", "error": None} - else: - return {"status": "offline", "error": f"Connection refused (code: {result})"} - except socket.timeout: - return {"status": "timeout", "error": "Connection timed out"} - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_http(host, port, url="/", timeout=5): - """Check HTTP endpoint.""" - try: - full_url = f"http://{host}:{port}{url}" - req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) - with urllib.request.urlopen(req, timeout=timeout) as r: - return {"status": "online", "status_code": r.status, "error": None} - except urllib.error.HTTPError as e: - return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_https(host, port, url="/", timeout=5, insecure=False): - """Check HTTPS endpoint.""" - try: - full_url = f"https://{host}:{port}{url}" - req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) - if insecure: - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - else: - ctx = ssl.create_default_context() - with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r: - return {"status": "online", "status_code": r.status, "error": None} - except ssl.SSLError as e: - return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"} - except urllib.error.HTTPError as e: - return {"status": "online", "status_code": e.code, "error": None} - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_service(service_id, config): - """Check a single service based on its type.""" - st = config["type"] - if st == "ssh": - return check_ssh(config["host"], config["port"]) - elif st == "http": - return check_http(config["host"], config["port"], config.get("url", "/")) - elif st == "https": - return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False)) - return {"status": "error", "error": f"Unknown type: {st}"} - - -def update_history(sid, old, new): - """Update status change history.""" - global status_history - if old != new: - status_history[sid] = { - "old_status": old, - "new_status": new, - "changed_at": datetime.now().isoformat() - } - - -def check_single(sid): - """Check a single service and return its status.""" - if sid not in SERVICES: - return None - result = check_service(sid, SERVICES[sid]) - return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()} - - -def check_all(): - """Check all services and update cache.""" - global status_cache, status_history - new_cache = {} - for sid, cfg in SERVICES.items(): - result = check_service(sid, cfg) - old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") - new = result["status"] - if old != new: - update_history(sid, old, new) - new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()} - with lock: - status_cache = new_cache - return new_cache - - -# HTML Template (using a function to avoid f-string escaping issues) -def generate_html(): - """Generate the HTML page.""" - return ''' - - - - - Homelab Status Monitor v''' + VERSION + ''' - - - -
-

Homelab Status Monitor

-
v''' + VERSION + '''
-
- - - - -
-
-
0
Total
-
0
Online
-
0
Offline
-
0
Errors
-
-
-
- - -''' - - -class Handler(BaseHTTPRequestHandler): - """HTTP request handler.""" - - def log_message(self, *args): - """Suppress default logging.""" - pass - - def do_GET(self): - """Handle GET requests.""" - try: - if self.path in ['/', '/index.html']: - self.send_response(200) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.end_headers() - self.wfile.write(generate_html().encode('utf-8')) - - elif self.path == '/api/status': - check_all() # Always refresh cache - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.send_header('Cache-Control', 'no-cache') - self.end_headers() - self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8')) - - elif self.path == '/api/refresh': - result = check_all() - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8')) - - elif self.path.startswith('/api/check/'): - sid = self.path.split('/')[-1] - if sid in SERVICES: - old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") - rd = check_single(sid) - new_st = rd["result"]["status"] - if old != new_st: - update_history(sid, old, new_st) - with lock: - status_cache[sid] = rd - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8')) - else: - self.send_response(404) - self.end_headers() - - else: - self.send_response(404) - self.end_headers() - - except Exception as e: - self.send_response(500) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8')) - - -def main(): - """Main entry point.""" - print(f"Starting Status Monitor v{VERSION}") - check_all() - server = ThreadingHTTPServer(('', 80), Handler) - print(f'Status Monitor v{VERSION} running on port 80') - server.serve_forever() - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +"""Homelab Status Monitor v1.1.1""" + +import socket +import ssl +import urllib.request +import json +import threading +from datetime import datetime +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +VERSION = "1.1.1" + +# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https) +SERVICES = { + # Containers LXC + "ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22}, + "ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"}, + + "ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22}, + "ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"}, + + "ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22}, + "ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"}, + "ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True}, + + "ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22}, + "ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"}, + + "ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22}, + "ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"}, + + "ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22}, + "ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"}, + "ct200-traefik-https": {"name": "Traefik HTTPS", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "insecure": True}, + + "ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22}, + "ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"}, + + "ct202-gitea": {"name": "Gitea", "type": "ssh", "host": "192.168.0.202", "port": 22}, + "ct202-gitea-http": {"name": "Gitea Web", "type": "http", "host": "192.168.0.202", "port": 3000, "url": "/"}, + + # VM + "vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22}, + "vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/"}, + + # Proxmox hosts + "prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22}, + "prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22}, + + # Raspberry Pi + "pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22}, + + # External HTTPS endpoints + "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "insecure": True}, + "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "insecure": True}, + "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "insecure": True}, + "status-peis-fr": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "insecure": True}, +} + +status_history = {} +status_cache = {} +lock = threading.Lock() + + +def check_ssh(host, port, timeout=3): + """Check if SSH port is open.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((host, port)) + sock.close() + if result == 0: + return {"status": "online", "error": None} + else: + return {"status": "offline", "error": f"Connection refused (code: {result})"} + except socket.timeout: + return {"status": "timeout", "error": "Connection timed out"} + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_http(host, port, url="/", timeout=5): + """Check HTTP endpoint.""" + try: + full_url = f"http://{host}:{port}{url}" + req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return {"status": "online", "status_code": r.status, "error": None} + except urllib.error.HTTPError as e: + return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_https(host, port, url="/", timeout=5, insecure=False): + """Check HTTPS endpoint.""" + try: + full_url = f"https://{host}:{port}{url}" + req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) + if insecure: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + else: + ctx = ssl.create_default_context() + with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r: + return {"status": "online", "status_code": r.status, "error": None} + except ssl.SSLError as e: + return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"} + except urllib.error.HTTPError as e: + return {"status": "online", "status_code": e.code, "error": None} + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_service(service_id, config): + """Check a single service based on its type.""" + st = config["type"] + if st == "ssh": + return check_ssh(config["host"], config["port"]) + elif st == "http": + return check_http(config["host"], config["port"], config.get("url", "/")) + elif st == "https": + return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False)) + return {"status": "error", "error": f"Unknown type: {st}"} + + +def update_history(sid, old, new): + """Update status change history.""" + global status_history + if old != new: + status_history[sid] = { + "old_status": old, + "new_status": new, + "changed_at": datetime.now().isoformat() + } + + +def check_single(sid): + """Check a single service and return its status.""" + if sid not in SERVICES: + return None + result = check_service(sid, SERVICES[sid]) + return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()} + + +def check_all(): + """Check all services and update cache.""" + global status_cache, status_history + new_cache = {} + for sid, cfg in SERVICES.items(): + result = check_service(sid, cfg) + old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") + new = result["status"] + if old != new: + update_history(sid, old, new) + new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()} + with lock: + status_cache = new_cache + return new_cache + + +# HTML Template (using a function to avoid f-string escaping issues) +def generate_html(): + """Generate the HTML page.""" + return ''' + + + + + Homelab Status Monitor v''' + VERSION + ''' + + + +
+

Homelab Status Monitor

+
v''' + VERSION + '''
+
+ + + + +
+
+
0
Total
+
0
Online
+
0
Offline
+
0
Errors
+
+
+
+ + +''' + + +class Handler(BaseHTTPRequestHandler): + """HTTP request handler.""" + + def log_message(self, *args): + """Suppress default logging.""" + pass + + def do_GET(self): + """Handle GET requests.""" + try: + if self.path in ['/', '/index.html']: + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(generate_html().encode('utf-8')) + + elif self.path == '/api/status': + check_all() # Always refresh cache + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Cache-Control', 'no-cache') + self.end_headers() + self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8')) + + elif self.path == '/api/refresh': + result = check_all() + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8')) + + elif self.path.startswith('/api/check/'): + sid = self.path.split('/')[-1] + if sid in SERVICES: + old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") + rd = check_single(sid) + new_st = rd["result"]["status"] + if old != new_st: + update_history(sid, old, new_st) + with lock: + status_cache[sid] = rd + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + + else: + self.send_response(404) + self.end_headers() + + except Exception as e: + self.send_response(500) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8')) + + +def main(): + """Main entry point.""" + print(f"Starting Status Monitor v{VERSION}") + check_all() + server = ThreadingHTTPServer(('', 80), Handler) + print(f'Status Monitor v{VERSION} running on port 80') + server.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/status-monitor/deploy.sh b/status-monitor/deploy.sh index ccf8d02..21016cd 100755 --- a/status-monitor/deploy.sh +++ b/status-monitor/deploy.sh @@ -1,40 +1,40 @@ -#!/bin/bash -# Script de déploiement DevOps pour status-monitor -# Utilisation: wsl bash ~/homelab-config/status-monitor/deploy.sh - -set -e - -echo "[DEPLOY] Début du déploiement depuis Git..." - -# Se connecte à CT201 via rebond CT200 et déploye -ssh -J root@192.168.0.100 root@192.168.0.201 << 'ENDSSH' -cd /opt/status-monitor - -echo "[DEPLOY] Mise à jour depuis Git..." -if [ ! -d "homelab-config" ]; then - git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git - echo "[DEPLOY] Dépôt cloné" -else - cd homelab-config - git pull - echo "[DEPLOY] Dépôt mis à jour" - cd .. -fi - -echo "[DEPLOY] Copie des fichiers..." -cp -f homelab-config/status-monitor/app.py . - -# Copier le service systemd si présent -if [ -f "homelab-config/status-monitor/status-monitor.service" ]; then - cp -f homelab-config/status-monitor/status-monitor.service /etc/systemd/system/ - systemctl daemon-reload - echo "[DEPLOY] Service systemd mis à jour" -fi - -echo "[DEPLOY] Redémarrage du service..." -systemctl restart status-monitor - -echo "[DEPLOY] ✅ Déploiement terminé !" -ENDSSH - -echo "[DEPLOY] Déploiement terminé avec succès !" +#!/bin/bash +# Script de déploiement DevOps pour status-monitor +# Utilisation: wsl bash ~/homelab-config/status-monitor/deploy.sh + +set -e + +echo "[DEPLOY] Début du déploiement depuis Git..." + +# Se connecte à CT201 via rebond CT200 et déploye +ssh -J root@192.168.0.100 root@192.168.0.201 << 'ENDSSH' +cd /opt/status-monitor + +echo "[DEPLOY] Mise à jour depuis Git..." +if [ ! -d "homelab-config" ]; then + git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git + echo "[DEPLOY] Dépôt cloné" +else + cd homelab-config + git pull + echo "[DEPLOY] Dépôt mis à jour" + cd .. +fi + +echo "[DEPLOY] Copie des fichiers..." +cp -f homelab-config/status-monitor/app.py . + +# Copier le service systemd si présent +if [ -f "homelab-config/status-monitor/status-monitor.service" ]; then + cp -f homelab-config/status-monitor/status-monitor.service /etc/systemd/system/ + systemctl daemon-reload + echo "[DEPLOY] Service systemd mis à jour" +fi + +echo "[DEPLOY] Redémarrage du service..." +systemctl restart status-monitor + +echo "[DEPLOY] ✅ Déploiement terminé !" +ENDSSH + +echo "[DEPLOY] Déploiement terminé avec succès !" From 3ce9d01b9c7599c076d407fc5c4e3b725c720d8c Mon Sep 17 00:00:00 2001 From: Mistral Date: Mon, 20 Jul 2026 20:01:30 +0200 Subject: [PATCH 2/6] docs(deploy): Flux IDE -> WSL -> CT200 -> CT201 - Corriger deploy.sh : synchronisation IDE -> WSL avant deploy - Remplacer here-doc par commande ssh directe pour eviter erreurs CRLF/encoding - Mettre a jour INSTRUCTIONS.md avec le flux de deploiement status-monitor - Mettre a jour AGENT_devops.md avec le workflow complet IDE -> WSL -> CT200 -> CT201 --- AGENT_devops.md | 540 ++++++++++++++++++++------------------- INSTRUCTIONS.md | 113 +++++--- status-monitor/deploy.sh | 45 ++-- 3 files changed, 367 insertions(+), 331 deletions(-) diff --git a/AGENT_devops.md b/AGENT_devops.md index f138491..b65f8a0 100755 --- a/AGENT_devops.md +++ b/AGENT_devops.md @@ -1,263 +1,277 @@ -# AGENTS.md - Instructions pour Mistral Vibe et autres agents - -## 🎯 Workflow DevOps Homelab - -### ✅ RÈGLES FONDAMENTALES - -1. **TOUJOURS travailler depuis le dépôt Git** - - Dépôt principal : https://git.peis.fr/VisualPCI/homelab-config - - Clone local : `~/homelab-config` (WSL) - - **JAMAIS** modifier les fichiers locaux sur `C:\Users\theo\.vibe\Proxmox\inventair\` - -2. **Avant TOUTE action** : - ```bash - cd ~/homelab-config - GIT_SSL_NO_VERIFY=true git pull origin main - ``` - -3. **Après TOUTE modification** : - ```bash - git add . - git commit -m "description claire de la modification" - GIT_SSL_NO_VERIFY=true git push origin main - ``` - -4. **Authentification Git** : - - URL : `https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git` - - Utilisateur : VisualPCI - - Mot de passe : 3N14E7GWVSgi3K - ---- - -## 📁 Structure du dépôt - -``` -homelab-config/ -├── AGENTS.md ← Ce fichier -├── INSTRUCTIONS.md ← Instructions générales -├── homelab-inventaire.md ← Inventaire principal -├── status-monitor/ ← Service de monitoring -│ ├── app.py ← Code principal (corrigé) -│ ├── app_v1.py ← Backup ancienne version -│ ├── app_v2.py ← Backup ancienne version -│ ├── deploy.sh ← Script de déploiement -│ └── dynamic_traefik.yml ← Configuration Traefik -├── traefik-config/ ← Configurations Traefik -└── procedure-*.md ← Documentations -``` - ---- - -## 🚀 Déploiement Status Monitor (CT201) - -### Serveur cible -- **IP** : 192.168.0.201 -- **Service** : `status-monitor` (systemd) -- **Dossier** : `/opt/status-monitor/` -- **URL** : https://status.peis.fr - -### Workflow de déploiement - -#### 1. Modifier le code -```bash -cd ~/homelab-config -# Modifier les fichiers dans status-monitor/ -git add status-monitor/ -git commit -m "Fix: description de la correction" -git push origin main -``` - -#### 2. Déployer sur CT201 -**Option A - Déploiement automatique :** -```bash -wsl bash ~/homelab-config/status-monitor/deploy.sh -``` - -**Option B - Déploiement manuel :** -```bash -# Copier les fichiers -scp ~/homelab-config/status-monitor/app.py root@192.168.0.201:/opt/status-monitor/ -scp ~/homelab-config/status-monitor/deploy.sh root@192.168.0.201:/opt/status-monitor/ - -# Redémarrer le service -ssh root@192.168.0.201 "chmod +x /opt/status-monitor/deploy.sh" -ssh root@192.168.0.201 "systemctl restart status-monitor" -``` - -#### 3. Vérifier le déploiement -```bash -# Vérifier l'API -curl -k -s https://status.peis.fr/api/status | python3 -m json.tool - -# Ou via navigateur -https://status.peis.fr -``` - ---- - -## 🐞 Corrections courantes - -### Problème : Cache non rafraîchi -**Symptôme** : La page status affiche des données anciennes - -**Cause** : `status_cache` n'était pas rafraîchi après le premier chargement - -**Solution appliquée** : -```python -# AVANT (bug) : -if not status_cache: - check_all() - -# APRÈS (corrigé) : -check_all() # Toujours rafraîchir à chaque appel -``` - -**Fichier** : `status-monitor/app.py` (ligne 372) - ---- - -## 📋 Commandes utiles - -### Git -```bash -# État -cd ~/homelab-config && git status - -# Pull -cd ~/homelab-config && git pull - -# Commit -cd ~/homelab-config && git add . && git commit -m "message" && git push - -# Voir l'historique -cd ~/homelab-config && git log --oneline --graph -``` - -### SSH CT201 (via rebond CT200) -```bash -# Se connecter (CT200 est le bastion) -ssh -J root@192.168.0.100 root@192.168.0.201 - -# Voir les logs du service -ssh -J root@192.168.0.100 root@192.168.0.201 "journalctl -u status-monitor -f" - -# État du service -ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl status status-monitor" - -# Redémarrer -ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl restart status-monitor" -``` - -### SSH CT200 (direct) -```bash -# Se connecter directement -ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 - -# Copier des fichiers vers CT200 -scp -i ~/.ssh/id_ed25519 fichier root@192.168.0.100:/destination/ - -# Copier des fichiers de CT200 vers local -scp -i ~/.ssh/id_ed25519 root@192.168.0.100:/source/fichier ./ -``` - -### Gitea -- **URL** : https://git.peis.fr -- **Compte** : VisualPCI -- **Dépôt** : https://git.peis.fr/VisualPCI/homelab-config - -### Workflow complet utilisé pour la correction du 20/07/2026 - -**Problème** : SyntaxError: Unexpected identifier 'card' (at index.html:101:35) -**Cause** : Dans `status-monitor/app.py`, les strings JavaScript utilisaient `class=\"` qui générait du code JS invalide. - -**Workflow appliqué** : -```bash -# 1. Cloner depuis WSL (Ubuntu) -cd ~/homelab-config -git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git - -# 2. Récupérer le fichier buggé depuis CT201 (via CT200) -ssh -J root@192.168.0.100 root@192.168.0.201 cat /opt/status-monitor/app.py > status-monitor/app_from_ct201.py - -# 3. Corriger le bug avec Python (remplacer \" par ') -python3 -c " -with open('status-monitor/app.py', 'r') as f: - content = f.read() -content = content.replace('\\\"', \"'\") -with open('status-monitor/app.py', 'w') as f: - f.write(content) -" - -# 4. Vérifier la syntaxe Python -python3 -m py_compile status-monitor/app.py - -# 5. Commiter avec GIT_SSL_NO_VERIFY -GIT_SSL_NO_VERIFY=true git add status-monitor/app.py -GIT_SSL_NO_VERIFY=true git commit -m "Fix(status-monitor): Correction des guillemets dans le JavaScript - Resout SyntaxError loading" - -# 6. Pull avec rebase (conflit détecté) -GIT_SSL_NO_VERIFY=true git pull --rebase origin main - -# 7. Pousser vers git.peis.fr -GIT_SSL_NO_VERIFY=true git push origin main - -# 8. Deployer sur CT201 (via CT200 comme bastion) -# Méthode 1 : Copier via CT200 -cat status-monitor/app.py | ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'cat > /tmp/app_new.py' -ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'scp /tmp/app_new.py root@192.168.0.201:/opt/status-monitor/app.py && chmod +x /opt/status-monitor/app.py' -ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'ssh root@192.168.0.201 systemctl restart status-monitor' - -# Méthode 2 : Direct avec -J (bastion) -scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 status-monitor/app.py root@192.168.0.201:/opt/status-monitor/app.py -ssh -J root@192.168.0.100 root@192.168.0.201 'chmod +x /opt/status-monitor/app.py && systemctl restart status-monitor' -``` - -**Résultat** : -- Commit `685060c` poussé vers git.peis.fr -- Fichier corrigé déployé sur CT201 -- Service redémarré avec succès - -**Leçons apprises** : -- Toujours utiliser `-J` pour le rebond SSH vers CT201 -- `GIT_SSL_NO_VERIFY=true` est nécessaire pour contourner les problèmes de certificat -- Utiliser `python3` pour les scripts de correction complexes -- Tester la syntaxe Python avant de deployer - ---- - -## 🚨 Erreurs à éviter - -- ❌ Modifier directement les fichiers sur CT201 sans passer par Git -- ❌ Oublier de faire `git pull` avant de modifier un fichier -- ❌ Oublier de commiter et pusher après une modification -- ❌ Travailler depuis plusieurs emplacements différents -- ❌ Modifier les fichiers dans `C:\Users\theo\.vibe\Proxmox\inventair\` - ---- - -## ✅ Checklist avant déploiement - -- [ ] `git pull` exécuté -- [ ] Modifications testées localement -- [ ] `git add .` exécuté -- [ ] `git commit -m "..."` exécuté -- [ ] `git push origin main` exécuté -- [ ] Déploiement vérifié sur https://status.peis.fr - ---- - -## 📊 Historique des modifications - -| Date | Commit | Description | -|------|--------|-------------| -| 2026-07-20 | `685060c` | Fix(status-monitor): Correction des guillemets dans le JavaScript - Resout SyntaxError loading | -| 2026-07-19 | `6b77f49` | Fix: Force cache refresh on /api/status endpoint | -| 2026-07-19 | `3ed1d96` | Add: deploy.sh script for DevOps workflow | -| 2026-07-19 | `da6b794` | Ajout: INSTRUCTIONS.md pour workflow Git | - ---- - -*Dernière mise à jour : 2026-07-20* - -*Dernière mise à jour : 2026-07-19* -*Pour : Mistral Vibe et agents homelab* +# AGENTS.md - Instructions pour Mistral Vibe et autres agents + +## 🎯 Workflow DevOps Homelab + +### ✅ RÈGLES FONDAMENTALES + +1. **TOUJOURS travailler depuis le dépôt Git** + - Dépôt principal : https://git.peis.fr/VisualPCI/homelab-config + - Clone local : `~/homelab-config` (WSL) + - **JAMAIS** modifier les fichiers locaux sur `C:\Users\theo\.vibe\Proxmox\inventair\` + +2. **Avant TOUTE action** : + ```bash + cd ~/homelab-config + GIT_SSL_NO_VERIFY=true git pull origin main + ``` + +3. **Après TOUTE modification** : + ```bash + git add . + git commit -m "description claire de la modification" + GIT_SSL_NO_VERIFY=true git push origin main + ``` + +4. **Authentification Git** : + - URL : `https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git` + - Utilisateur : VisualPCI + - Mot de passe : 3N14E7GWVSgi3K + +--- + +## 📁 Structure du dépôt + +``` +homelab-config/ +├── AGENTS.md ← Ce fichier +├── INSTRUCTIONS.md ← Instructions générales +├── homelab-inventaire.md ← Inventaire principal +├── status-monitor/ ← Service de monitoring +│ ├── app.py ← Code principal (corrigé) +│ ├── app_v1.py ← Backup ancienne version +│ ├── app_v2.py ← Backup ancienne version +│ ├── deploy.sh ← Script de déploiement +│ └── dynamic_traefik.yml ← Configuration Traefik +├── traefik-config/ ← Configurations Traefik +└── procedure-*.md ← Documentations +``` + +--- + +## 🚀 Déploiement Status Monitor (CT201) + +### Serveur cible +- **IP** : 192.168.0.201 +- **Service** : `status-monitor` (systemd) +- **Dossier** : `/opt/status-monitor/` +- **URL** : https://status.peis.fr + +### Workflow de déploiement + +**⚠️ Flux obligatoire : IDE (Windows) -> WSL -> CT200/CT100 (bastion 192.168.0.100) -> CT201 (192.168.0.201)** + +``` +IDE (C:\Users\theo\homelab_ia\homelab-config) + └── copy ───> WSL (~/homelab-config) + └── scp -J root@192.168.0.100 ───> CT201 (192.168.0.201) +``` + +#### 1. Modifier le code dans l'IDE +Modifier `status-monitor/app.py` dans l'IDE Windows. + +#### 2. Synchroniser IDE -> WSL +```bash +cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py +``` + +#### 3. Commiter et pusher +```bash +cd ~/homelab-config +GIT_SSL_NO_VERIFY=true git add status-monitor/ +GIT_SSL_NO_VERIFY=true git commit -m "Fix: description de la correction" +GIT_SSL_NO_VERIFY=true git push origin main +``` + +#### 4. Déployer sur CT201 +**Option A - Déploiement automatique (recommandé) :** +```bash +wsl bash ~/homelab-config/status-monitor/deploy.sh +``` +Ce script synchronise automatiquement l'IDE -> WSL puis déploie via CT200/CT100. + +**Option B - Déploiement manuel :** +```bash +# WSL -> CT201 (rebond via CT200/CT100 bastion) +scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 ~/homelab-config/status-monitor/app.py root@192.168.0.201:/tmp/app.py + +# Installation et redémarrage sur CT201 +ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 'mv /tmp/app.py /opt/status-monitor/app.py && systemctl restart status-monitor' +``` + +#### 5. Vérifier le déploiement +```bash +# Vérifier l'API +curl -k -s https://status.peis.fr/api/status | python3 -m json.tool + +# Ou via navigateur +https://status.peis.fr +``` + +--- + +## 🐞 Corrections courantes + +### Problème : Cache non rafraîchi +**Symptôme** : La page status affiche des données anciennes + +**Cause** : `status_cache` n'était pas rafraîchi après le premier chargement + +**Solution appliquée** : +```python +# AVANT (bug) : +if not status_cache: + check_all() + +# APRÈS (corrigé) : +check_all() # Toujours rafraîchir à chaque appel +``` + +**Fichier** : `status-monitor/app.py` (ligne 372) + +--- + +## 📋 Commandes utiles + +### Git +```bash +# État +cd ~/homelab-config && git status + +# Pull +cd ~/homelab-config && git pull + +# Commit +cd ~/homelab-config && git add . && git commit -m "message" && git push + +# Voir l'historique +cd ~/homelab-config && git log --oneline --graph +``` + +### SSH CT201 (via rebond CT200) +```bash +# Se connecter (CT200 est le bastion) +ssh -J root@192.168.0.100 root@192.168.0.201 + +# Voir les logs du service +ssh -J root@192.168.0.100 root@192.168.0.201 "journalctl -u status-monitor -f" + +# État du service +ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl status status-monitor" + +# Redémarrer +ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl restart status-monitor" +``` + +### SSH CT200 (direct) +```bash +# Se connecter directement +ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 + +# Copier des fichiers vers CT200 +scp -i ~/.ssh/id_ed25519 fichier root@192.168.0.100:/destination/ + +# Copier des fichiers de CT200 vers local +scp -i ~/.ssh/id_ed25519 root@192.168.0.100:/source/fichier ./ +``` + +### Gitea +- **URL** : https://git.peis.fr +- **Compte** : VisualPCI +- **Dépôt** : https://git.peis.fr/VisualPCI/homelab-config + +### Workflow complet utilisé pour la correction du 20/07/2026 + +**Problème** : SyntaxError: Unexpected identifier 'card' (at index.html:101:35) +**Cause** : Dans `status-monitor/app.py`, les strings JavaScript utilisaient `class=\"` qui générait du code JS invalide. + +**Workflow appliqué** : +```bash +# 1. Cloner depuis WSL (Ubuntu) +cd ~/homelab-config +git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git + +# 2. Récupérer le fichier buggé depuis CT201 (via CT200) +ssh -J root@192.168.0.100 root@192.168.0.201 cat /opt/status-monitor/app.py > status-monitor/app_from_ct201.py + +# 3. Corriger le bug avec Python (remplacer \" par ') +python3 -c " +with open('status-monitor/app.py', 'r') as f: + content = f.read() +content = content.replace('\\\"', \"'\") +with open('status-monitor/app.py', 'w') as f: + f.write(content) +" + +# 4. Vérifier la syntaxe Python +python3 -m py_compile status-monitor/app.py + +# 5. Commiter avec GIT_SSL_NO_VERIFY +GIT_SSL_NO_VERIFY=true git add status-monitor/app.py +GIT_SSL_NO_VERIFY=true git commit -m "Fix(status-monitor): Correction des guillemets dans le JavaScript - Resout SyntaxError loading" + +# 6. Pull avec rebase (conflit détecté) +GIT_SSL_NO_VERIFY=true git pull --rebase origin main + +# 7. Pousser vers git.peis.fr +GIT_SSL_NO_VERIFY=true git push origin main + +# 8. Deployer sur CT201 (via CT200 comme bastion) +# Méthode 1 : Copier via CT200 +cat status-monitor/app.py | ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'cat > /tmp/app_new.py' +ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'scp /tmp/app_new.py root@192.168.0.201:/opt/status-monitor/app.py && chmod +x /opt/status-monitor/app.py' +ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'ssh root@192.168.0.201 systemctl restart status-monitor' + +# Méthode 2 : Direct avec -J (bastion) +scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 status-monitor/app.py root@192.168.0.201:/opt/status-monitor/app.py +ssh -J root@192.168.0.100 root@192.168.0.201 'chmod +x /opt/status-monitor/app.py && systemctl restart status-monitor' +``` + +**Résultat** : +- Commit `685060c` poussé vers git.peis.fr +- Fichier corrigé déployé sur CT201 +- Service redémarré avec succès + +**Leçons apprises** : +- Toujours utiliser `-J` pour le rebond SSH vers CT201 +- `GIT_SSL_NO_VERIFY=true` est nécessaire pour contourner les problèmes de certificat +- Utiliser `python3` pour les scripts de correction complexes +- Tester la syntaxe Python avant de deployer + +--- + +## 🚨 Erreurs à éviter + +- ❌ Modifier directement les fichiers sur CT201 sans passer par Git +- ❌ Oublier de faire `git pull` avant de modifier un fichier +- ❌ Oublier de commiter et pusher après une modification +- ❌ Travailler depuis plusieurs emplacements différents +- ❌ Modifier les fichiers dans `C:\Users\theo\.vibe\Proxmox\inventair\` + +--- + +## ✅ Checklist avant déploiement + +- [ ] `git pull` exécuté +- [ ] Modifications testées localement +- [ ] `git add .` exécuté +- [ ] `git commit -m "..."` exécuté +- [ ] `git push origin main` exécuté +- [ ] Déploiement vérifié sur https://status.peis.fr + +--- + +## 📊 Historique des modifications + +| Date | Commit | Description | +|------|--------|-------------| +| 2026-07-20 | `685060c` | Fix(status-monitor): Correction des guillemets dans le JavaScript - Resout SyntaxError loading | +| 2026-07-19 | `6b77f49` | Fix: Force cache refresh on /api/status endpoint | +| 2026-07-19 | `3ed1d96` | Add: deploy.sh script for DevOps workflow | +| 2026-07-19 | `da6b794` | Ajout: INSTRUCTIONS.md pour workflow Git | + +--- + +*Dernière mise à jour : 2026-07-20* + +*Dernière mise à jour : 2026-07-19* +*Pour : Mistral Vibe et agents homelab* diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index b3bf47b..4c2dce5 100755 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -1,39 +1,74 @@ -# INSTRUCTIONS POUR VIBE - -## RÈGLES ABSOLUES - -1. TOUJOURS vérifier Git en premier - cd ~/homelab-config - git pull - -2. TOUJOURS travailler depuis ~/homelab-config (WSL) Tu as WSL d'installé avec ubuntu. utilise le pour cloner git... - -3. JAMAIS toucher aux fichiers locaux sur C:/Users/theo/.vibe/Proxmox/inventair/ - -4. TOUJOURS commiter et pusher après modification - git add . - git commit -m "description claire" - GIT_SSL_NO_VERIFY=true git push origin main - -## Sources de vérité -- Dépôt Git : https://git.peis.fr/VisualPCI/homelab-config -- Clone local : ~/homelab-config (WSL) -- Backup local : A déplacer par Theo - -## ERREURS À ÉVITER -- Modifier directement C:/Users/theo/.vibe/Proxmox/inventair/* -- Oublier de faire git pull avant modification -- Oublier de pusher après modification -- Travailler depuis plusieurs emplacements - -## WORKFLOW STANDARD -1. Recevoir la demande -2. cd ~/homelab-config && git pull -3. Modifier les fichiers dans ~/homelab-config -4. Tester/valider -5. git add . && git commit -m "..." && git push -6. Confirmer à l'utilisateur - ---- -Créé par : Theo -Pour : Mistral Vibe +# INSTRUCTIONS POUR AGENTS + +## 📋 Sommaire des agents + +- **[AGENT_devops.md](AGENT_devops.md)** : Opérations quotidiennes (Status Monitor, commandes SSH, corrections courantes) +- **[AGENT_MAJ_traefik_GIT_inventaire.md](AGENT_MAJ_traefik_GIT_inventaire.md)** : Synchronisation Traefik/GIT (procédure détaillée) +- **[AGENT-creation-lxc-proxmox.md](AGENT-creation-lxc-proxmox.md)** : Création de containers LXC sur Proxmox +- **[AGENT-TRAEFIK-https-homelab-infomaniak.md](AGENT-TRAEFIK-https-homelab-infomaniak.md)** : Configuration HTTPS/Traefik avec Let's Encrypt +- **[AGENT_Transfert_de_certificat_viaCT200.md](AGENT_Transfert_de_certificat_viaCT200.md)** : Transfert de certificats via CT200 comme bastion + +--- + +## RÈGLES ABSOLUES + +1. TOUJOURS vérifier Git en premier + cd ~/homelab-config + git pull + +2. TOUJOURS travailler depuis ~/homelab-config (WSL) - Tu as WSL d'installé avec ubuntu. utilise le pour cloner git... + +3. JAMAIS toucher aux fichiers locaux sur C:/Users/theo/.vibe/Proxmox/inventair/ + +4. TOUJOURS commiter et pusher après modification + git add . + git commit -m "description claire" + GIT_SSL_NO_VERIFY=true git push origin main + +## FLUX DE DÉPLOIEMENT STATUS-MONITOR + +L'ordre est critique pour éviter les déploiements de vieilles versions : + +``` +IDE (Windows) ──copy──> WSL (~/homelab-config) ──scp──> CT200/CT100 (192.168.0.100 bastion) ──scp──> CT201 (192.168.0.201 status-monitor) +``` + +### Commande automatique +```bash +wsl bash ~/homelab-config/status-monitor/deploy.sh +``` + +### Commande manuelle (si deploy.sh ne fonctionne pas) +```bash +# 1. IDE -> WSL +cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py + +# 2. WSL -> CT201 (rebond via CT200/CT100) +scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 ~/homelab-config/status-monitor/app.py root@192.168.0.201:/tmp/app.py + +# 3. Installation sur CT201 +ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 'mv /tmp/app.py /opt/status-monitor/app.py && systemctl restart status-monitor' +``` + +## Sources de vérité +- Dépôt Git : https://git.peis.fr/VisualPCI/homelab-config +- Clone local : ~/homelab-config (WSL) +- Backup local : A déplacer par Theo + +## ERREURS À ÉVITER +- Modifier directement C:/Users/theo/.vibe/Proxmox/inventair/* +- Oublier de faire git pull avant modification +- Oublier de pusher après modification +- Travailler depuis plusieurs emplacements + +## WORKFLOW STANDARD +1. Recevoir la demande +2. cd ~/homelab-config && git pull +3. Modifier les fichiers dans ~/homelab-config +4. Tester/valider +5. git add . && git commit -m "..." && git push +6. Confirmer à l'utilisateur + +--- +Créé par : Theo +Pour : Mistral Vibe et autres agents diff --git a/status-monitor/deploy.sh b/status-monitor/deploy.sh index 21016cd..f13e236 100755 --- a/status-monitor/deploy.sh +++ b/status-monitor/deploy.sh @@ -1,40 +1,27 @@ #!/bin/bash # Script de déploiement DevOps pour status-monitor +# Flux: IDE (Windows) -> WSL -> CT200/CT100 (bastion 192.168.0.100) -> CT201 (192.168.0.201) # Utilisation: wsl bash ~/homelab-config/status-monitor/deploy.sh set -e -echo "[DEPLOY] Début du déploiement depuis Git..." +WSL_REPO="$HOME/homelab-config" +IDE_REPO="/mnt/c/Users/theo/homelab_ia/homelab-config" -# Se connecte à CT201 via rebond CT200 et déploye -ssh -J root@192.168.0.100 root@192.168.0.201 << 'ENDSSH' -cd /opt/status-monitor - -echo "[DEPLOY] Mise à jour depuis Git..." -if [ ! -d "homelab-config" ]; then - git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git - echo "[DEPLOY] Dépôt cloné" -else - cd homelab-config - git pull - echo "[DEPLOY] Dépôt mis à jour" - cd .. +# Etape 1: Synchroniser IDE (Windows) -> WSL +if [ -d "$IDE_REPO" ] && [ -d "$WSL_REPO" ]; then + echo "[DEPLOY] Synchronisation IDE -> WSL..." + cp "$IDE_REPO/status-monitor/app.py" "$WSL_REPO/status-monitor/app.py" + sed -i 's/\r$//' "$WSL_REPO/status-monitor/app.py" + echo "[DEPLOY] OK - Source copied IDE -> WSL" fi -echo "[DEPLOY] Copie des fichiers..." -cp -f homelab-config/status-monitor/app.py . +# Etape 2: Deploiement WSL -> CT201 via CT200/CT100 (ProxyJump) +echo "[DEPLOY] Transfert WSL -> CT201 via CT200/CT100..." +scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 "$WSL_REPO/status-monitor/app.py" root@192.168.0.201:/tmp/app.py -# Copier le service systemd si présent -if [ -f "homelab-config/status-monitor/status-monitor.service" ]; then - cp -f homelab-config/status-monitor/status-monitor.service /etc/systemd/system/ - systemctl daemon-reload - echo "[DEPLOY] Service systemd mis à jour" -fi +# Etape 3: Installation et redemarrage sur CT201 +echo "[DEPLOY] Installation sur CT201..." +ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 "mv /tmp/app.py /opt/status-monitor/app.py && systemctl restart status-monitor && echo '[DEPLOY] OK - CT201 updated'" -echo "[DEPLOY] Redémarrage du service..." -systemctl restart status-monitor - -echo "[DEPLOY] ✅ Déploiement terminé !" -ENDSSH - -echo "[DEPLOY] Déploiement terminé avec succès !" +echo "[DEPLOY] OK - All done: IDE -> WSL -> CT200/CT100 -> CT201" From 84226f2416225f0334ae8dbbb1f1e187e1bae3cd Mon Sep 17 00:00:00 2001 From: Windsurf Date: Mon, 20 Jul 2026 20:28:54 +0200 Subject: [PATCH 3/6] =?UTF-8?q?docs(git):=20Mise=20=C3=A0=20jour=20authent?= =?UTF-8?q?ification=20Git=20vers=20Windsurf/3N14E7GWVSgi3K?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT_devops.md | 7 ++++--- INSTRUCTIONS.md | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/AGENT_devops.md b/AGENT_devops.md index b65f8a0..b9f4c8c 100755 --- a/AGENT_devops.md +++ b/AGENT_devops.md @@ -23,9 +23,10 @@ ``` 4. **Authentification Git** : - - URL : `https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git` - - Utilisateur : VisualPCI - - Mot de passe : 3N14E7GWVSgi3K + - URL : `https://Windsurf:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git` + - Utilisateur : Windsurf + - Mot de passe / token : 3N14E7GWVSgi3K + - Commit author : `Windsurf ` --- diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index 4c2dce5..653d8ea 100755 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -50,6 +50,13 @@ scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 ~/homelab-config/status-monitor/a ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 'mv /tmp/app.py /opt/status-monitor/app.py && systemctl restart status-monitor' ``` +## Authentification Git (à utiliser pour tous les commits) +- **Utilisateur** : `Windsurf` +- **Mot de passe / token** : `3N14E7GWVSgi3K` +- **URL du dépôt** : `https://Windsurf:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git` +- **Auteur des commits** : `Windsurf ` +- Le dépôt reste VisualPCI/homelab-config, l'authentification se fait avec Windsurf. + ## Sources de vérité - Dépôt Git : https://git.peis.fr/VisualPCI/homelab-config - Clone local : ~/homelab-config (WSL) From c63ff8a6a05202c08fa9c7f959b7e7e68ed3ff91 Mon Sep 17 00:00:00 2001 From: Windsurf Date: Mon, 20 Jul 2026 20:45:38 +0200 Subject: [PATCH 4/6] feat(status-monitor): v1.2.0 - Liens cliquables, filtres, public_url --- status-monitor/README.md | 616 +++++++++++++------------- status-monitor/app.py | 903 ++++++++++++++++++++------------------- 2 files changed, 781 insertions(+), 738 deletions(-) diff --git a/status-monitor/README.md b/status-monitor/README.md index c84c9fe..045e804 100644 --- a/status-monitor/README.md +++ b/status-monitor/README.md @@ -1,310 +1,306 @@ -# Status Monitor - Homelab - -**Application de monitoring des services Homelab** -**Version** : 1.1.0 -**Auteur** : Mistral Vibe -**Dernière mise à jour** : 19/07/2026 - ---- - -## 📋 Description - -Application Python de monitoring des services Homelab. Elle vérifie régulièrement l'état (online/offline) de tous les services configurés et affiche un tableau de bord web en temps réel. - -**Fonctionnalités** : -- Vérification SSH, HTTP, HTTPS -- Interface web responsive avec rafraîchissement automatique -- Historique des changements de statut -- Support des certificats SSL auto-signés (mode insecure) -- Tableau de bord avec statistiques globales - ---- - -## 🏗️ Architecture - -``` -Internet → Traefik (CT200) → status.peis.fr - ↓ - CT201 (192.168.0.201:80) - ↓ - Status Monitor (Python HTTP Server) - ↓ - Vérifie tous les services configurés -``` - ---- - -## 🚀 Déploiement - -### Pré-requis -- Python 3.x -- Accès réseau à tous les services à surveiller -- Port 80 disponible sur CT201 - -### Installation - -```bash -# Sur CT201 (192.168.0.201) -apt update && apt install -y python3 python3-pip - -# Créer le dossier -mkdir -p /opt/status-monitor -cd /opt/status-monitor - -# Copier les fichiers depuis ce dépôt -# (voir section Workflow Git ci-dessous) - -# Installer le service systemd -cp status-monitor.service /etc/systemd/system/ -systemctl daemon-reload -systemctl enable status-monitor -systemctl start status-monitor - -# Vérifier -systemctl status status-monitor -curl http://localhost -``` - ---- - -## 🔄 Workflow Git (Processus Standard) - -### Structure dans le dépôt -``` -homelab-config/ -└── status-monitor/ - ├── app.py # Application principale - ├── deploy.sh # Script de déploiement - ├── status-monitor.service # Service systemd - ├── VERSION # Version actuelle - └── README.md # Ce fichier -``` - -### 1. Développer / Modifier le code - -```bash -# Sur ta machine de développement (Windows) -# Modifier les fichiers dans : C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\ - -# Exemple : Modifier app.py -nano C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\app.py -``` - -### 2. Pousser vers Git (via CT200) - -```bash -# Copier les fichiers modifiés vers CT200 -scp -i C:\Users\theo\.ssh\id_ed25519 app.py root@192.168.0.100:/opt/git-repos/homelab-config/status-monitor/ - -# Sur CT200 : -ssh -i C:\Users\theo\.ssh\id_ed25519 root@192.168.0.100 -cd /opt/git-repos/homelab-config - -# Vérifier les changements -git status -git diff status-monitor/ - -# Ajouter, commiter, pousser -git add status-monitor/ -git commit -m "Update Status Monitor - - -Generated by Mistral Vibe. -Co-Authored-By: Mistral Vibe " -git push origin main -``` - -### 3. Déployer sur CT201 - -```bash -# Méthode A : Exécuter le script de déploiement -bash /opt/git-repos/homelab-config/status-monitor/deploy.sh - -# Méthode B : Manuellement -ssh root@192.168.0.201 -cd /opt/status-monitor - -# Cloner ou pull le dépôt -if [ ! -d "homelab-config" ]; then - git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git -else - cd homelab-config - git pull - cd .. -fi - -# Copier les fichiers -cp -f homelab-config/status-monitor/app.py . -cp -f homelab-config/status-monitor/status-monitor.service /etc/systemd/system/ - -# Redémarrer -systemctl daemon-reload -systemctl restart status-monitor -``` - ---- - -## 📦 Versioning - -### Règles de versioning -- Format : `MAJOR.MINOR.PATCH` (ex: 1.1.0) -- **MAJOR** : Changements incompatibles (refonte complète) -- **MINOR** : Nouvelles fonctionnalités (compatibles) -- **PATCH** : Corrections de bugs - -### Mettre à jour la version - -```bash -# 1. Modifier le fichier VERSION -nano status-monitor/VERSION - -# 2. Créer un tag Git (optionnel pour les releases) -git tag -a v1.1.0 -m "Release Status Monitor v1.1.0" -git push origin v1.1.0 - -# 3. Commit le changement de version -git add status-monitor/VERSION -git commit -m "Bump version to 1.1.0" -git push -``` - -### Historique des versions -| Version | Date | Changements | -|---------|------|-------------| -| 1.1.0 | 19/07/2026 | Version initiale dans Git | -| 1.0.0 | Avant | Version initiale (non versionnée) | - ---- - -## 📊 Configuration des Services - -Les services à surveiller sont définis dans le dictionnaire `SERVICES` dans `app.py`. - -**Format** : -```python -SERVICES = { - "service_id": { - "name": "Nom affiché", - "type": "ssh" | "http" | "https", - "host": "IP ou hostname", - "port": port_number, - "url": "/chemin" (optionnel, pour http/https), - "insecure": True | False (optionnel, pour https sans vérification SSL) - }, - ... -} -``` - -**Types disponibles** : -- `ssh` : Vérifie si le port SSH (22) est ouvert -- `http` : Vérifie la réponse HTTP sur un port -- `https` : Vérifie la réponse HTTPS sur un port - ---- - -## 🎨 API Endpoints - -| Endpoint | Méthode | Description | -|----------|---------|-------------| -| `/` | GET | Tableau de bord HTML | -| `/api/status` | GET | JSON de tous les statuts | -| `/api/refresh` | GET | Rafraîchit tous les services | -| `/api/check/` | GET | Vérifie un service spécifique | - ---- - -## 🔧 Configuration Traefik - -Le service est exposé via Traefik sur CT200 avec la configuration suivante dans `dynamic.yml` : - -```yaml -http: - routers: - status-monitor: - rule: "Host(`status.peis.fr`)" - entryPoints: - - "websecure" - tls: - certResolver: "letsencrypt" - service: status-monitor - - services: - status-monitor: - loadBalancer: - servers: - - url: "http://192.168.0.201:80" -``` - -**URL publique** : https://status.peis.fr - ---- - -## ⚡ Commandes Utiles - -### Sur CT201 - -```bash -# Voir les logs -journalctl -u status-monitor -f - -# Redémarrer -systemctl restart status-monitor - -# Tester localement -curl http://localhost -curl http://localhost/api/status | jq -``` - -### Depuis l'extérieur - -```bash -# Vérifier l'accès -curl -vk https://status.peis.fr -curl -vk https://status.peis.fr/api/status -``` - ---- - -## 🛠️ Développement - -### Ajouter un nouveau service - -1. Éditer `app.py` -2. Ajouter une entrée dans `SERVICES` -3. Tester localement -4. Pousser vers Git -5. Déployer - -### Tester localement - -```bash -# Sur ta machine de dev -python3 app.py -# Accéder à http://localhost:80 -``` - ---- - -## 📝 Credentials et Accès - -| Élément | Valeur | -|---------|--------| -| **Dépôt Git** | git.peis.fr/VisualPCI/homelab-config | -| **Username Git** | VisualPCI | -| **Token Git** | 3N14E7GWVSgi3K | -| **Serveur de déploiement** | CT201 (192.168.0.201) | -| **URL publique** | https://status.peis.fr | -| **Port** | 80 | - ---- - -## 🎯 Prochaines Évolutions - -- [ ] Ajouter l'authentification basique -- [ ] Ajouter des notifications (email, webhook) -- [ ] Intégrer avec Grafana/Prometheus -- [ ] Historique persistant en base de données -- [ ] Dashboard personnalisable - ---- - -*Documentation générée par Mistral Vibe - 19/07/2026* +# Status Monitor - Homelab + +**Application de monitoring des services Homelab** +**Version** : 1.2.0 +**Auteur** : Windsurf +**Dernière mise à jour** : 20/07/2026 + +--- + +## 📋 Description + +Application Python de monitoring des services Homelab. Elle vérifie régulièrement l'état (online/offline) de tous les services configurés et affiche un tableau de bord web en temps réel. + +**Fonctionnalités** : +- Vérification SSH, HTTP, HTTPS +- Interface web responsive avec rafraîchissement automatique +- **Liens cliquables vers chaque service (IP:port ou URL publique HTTPS)** +- **Filtres par type de service (SSH, HTTP, HTTPS, Others)** +- URLs publiques configurables par service (`public_url`) +- Historique des changements de statut +- Support des certificats SSL auto-signés (mode insecure) +- Tableau de bord avec statistiques globales + +--- + +## 🏗️ Architecture + +``` +Internet → Traefik (CT200) → status.peis.fr + ↓ + CT201 (192.168.0.201:80) + ↓ + Status Monitor (Python HTTP Server) + ↓ + Vérifie tous les services configurés +``` + +--- + +## 🚀 Déploiement + +### Pré-requis +- Python 3.x +- Accès réseau à tous les services à surveiller +- Port 80 disponible sur CT201 + +### Installation + +```bash +# Sur CT201 (192.168.0.201) +apt update && apt install -y python3 python3-pip + +# Créer le dossier +mkdir -p /opt/status-monitor +cd /opt/status-monitor + +# Copier les fichiers depuis ce dépôt +# (voir section Workflow Git ci-dessous) + +# Installer le service systemd +cp status-monitor.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable status-monitor +systemctl start status-monitor + +# Vérifier +systemctl status status-monitor +curl http://localhost +``` + +--- + +## 🔄 Workflow Git (Processus Standard) + +### Structure dans le dépôt +``` +homelab-config/ +└── status-monitor/ + ├── app.py # Application principale + ├── deploy.sh # Script de déploiement + ├── status-monitor.service # Service systemd + ├── VERSION # Version actuelle + └── README.md # Ce fichier +``` + +### 1. Développer / Modifier le code + +```bash +# Sur ta machine de développement (Windows) +# Modifier les fichiers dans : C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\ + +# Exemple : Modifier app.py +nano C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\app.py +``` + +### 2. Pousser vers Git depuis WSL + +```bash +# Synchroniser IDE -> WSL si nécessaire +cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py + +# Commiter et pousser avec l'utilisateur Windsurf +cd ~/homelab-config +git status +git diff status-monitor/ +git add status-monitor/ +git commit -m "Update Status Monitor - " +GIT_SSL_NO_VERIFY=true git push origin main +``` + +### 3. Déployer sur CT201 + +**Flux obligatoire : IDE (Windows) -> WSL -> CT200/CT100 (bastion 192.168.0.100) -> CT201 (192.168.0.201)** + +```bash +# Méthode A : Exécuter le script de déploiement depuis WSL +wsl bash ~/homelab-config/status-monitor/deploy.sh + +# Méthode B : Manuellement +# 1. IDE -> WSL +cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py + +# 2. WSL -> CT201 (rebond via CT200/CT100) +scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 ~/homelab-config/status-monitor/app.py root@192.168.0.201:/tmp/app.py + +# 3. Installation sur CT201 +ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 'mv /tmp/app.py /opt/status-monitor/app.py && systemctl restart status-monitor' +``` + +--- + +## 📦 Versioning + +### Règles de versioning +- Format : `MAJOR.MINOR.PATCH` (ex: 1.1.0) +- **MAJOR** : Changements incompatibles (refonte complète) +- **MINOR** : Nouvelles fonctionnalités (compatibles) +- **PATCH** : Corrections de bugs + +### Mettre à jour la version + +```bash +# 1. Modifier le fichier VERSION +nano status-monitor/VERSION + +# 2. Créer un tag Git (optionnel pour les releases) +git tag -a v1.1.0 -m "Release Status Monitor v1.1.0" +git push origin v1.1.0 + +# 3. Commit le changement de version +git add status-monitor/VERSION +git commit -m "Bump version to 1.1.0" +git push +``` + +### Historique des versions +| Version | Date | Changements | +|---------|------|-------------| +| 1.1.0 | 19/07/2026 | Version initiale dans Git | +| 1.0.0 | Avant | Version initiale (non versionnée) | + +--- + +## 📊 Configuration des Services + +Les services à surveiller sont définis dans le dictionnaire `SERVICES` dans `app.py`. + +**Format** : +```python +SERVICES = { + "service_id": { + "name": "Nom affiché", + "type": "ssh" | "http" | "https", + "host": "IP ou hostname", + "port": port_number, + "url": "/chemin" (optionnel, pour http/https), + "public_url": "https://mon-service.peis.fr/" (optionnel, lien cliquable affiché), + "insecure": True | False (optionnel, pour https sans vérification SSL) + }, + ... +} +``` + +**Types disponibles** : +- `ssh` : Vérifie si le port SSH (22) est ouvert +- `http` : Vérifie la réponse HTTP sur un port (lien cliquable http://host:port/url) +- `https` : Vérifie la réponse HTTPS sur un port (lien cliquable https://host:port/url ou public_url) + +--- + +## 🎨 API Endpoints + +| Endpoint | Méthode | Description | +|----------|---------|-------------| +| `/` | GET | Tableau de bord HTML | +| `/api/status` | GET | JSON de tous les statuts | +| `/api/refresh` | GET | Rafraîchit tous les services | +| `/api/check/` | GET | Vérifie un service spécifique | + +--- + +## 🔧 Configuration Traefik + +Le service est exposé via Traefik sur CT200 avec la configuration suivante dans `dynamic.yml` : + +```yaml +http: + routers: + status-monitor: + rule: "Host(`status.peis.fr`)" + entryPoints: + - "websecure" + tls: + certResolver: "letsencrypt" + service: status-monitor + + services: + status-monitor: + loadBalancer: + servers: + - url: "http://192.168.0.201:80" +``` + +**URL publique** : https://status.peis.fr + +--- + +## ⚡ Commandes Utiles + +### Sur CT201 + +```bash +# Voir les logs +journalctl -u status-monitor -f + +# Redémarrer +systemctl restart status-monitor + +# Tester localement +curl http://localhost +curl http://localhost/api/status | jq +``` + +### Depuis l'extérieur + +```bash +# Vérifier l'accès +curl -vk https://status.peis.fr +curl -vk https://status.peis.fr/api/status +``` + +--- + +## 🛠️ Développement + +### Ajouter un nouveau service + +1. Éditer `app.py` +2. Ajouter une entrée dans `SERVICES` +3. Tester localement +4. Pousser vers Git +5. Déployer + +### Tester localement + +```bash +# Sur ta machine de dev +python3 app.py +# Accéder à http://localhost:80 +``` + +--- + +## 📝 Credentials et Accès + +| Élément | Valeur | +|---------|--------| +| **Dépôt Git** | git.peis.fr/VisualPCI/homelab-config | +| **Username Git** | Windsurf | +| **Token Git** | 3N14E7GWVSgi3K | +| **Auteur des commits** | Windsurf | +| **Serveur de déploiement** | CT201 (192.168.0.201) | +| **Bastion SSH** | CT200/CT100 (192.168.0.100) | +| **URL publique** | https://status.peis.fr | +| **Port** | 80 | + +--- + +## 🎯 Prochaines Évolutions + +- [ ] Ajouter l'authentification basique +- [ ] Ajouter des notifications (email, webhook Discord/Slack) +- [ ] Intégrer avec Grafana/Prometheus +- [ ] Historique persistant en base de données (SQLite/InfluxDB) +- [ ] Dashboard personnalisable (drag & drop des cartes) +- [ ] Temps de réponse / latence par service +- [ ] Catégories/groupes de services (Proxmox, Containers, VM, Externe) +- [ ] Mode sombre/clair +- [ ] Export CSV/PDF des statuts +- [ ] Page d'historique détaillée avec graphiques +- [ ] Gestion des certificats SSL (expiration, renouvellement) +- [ ] Support du ping ICMP en plus de TCP + +--- + +*Documentation générée par Windsurf - 20/07/2026* diff --git a/status-monitor/app.py b/status-monitor/app.py index 85de485..7943e1f 100755 --- a/status-monitor/app.py +++ b/status-monitor/app.py @@ -1,428 +1,475 @@ -#!/usr/bin/env python3 -"""Homelab Status Monitor v1.1.1""" - -import socket -import ssl -import urllib.request -import json -import threading -from datetime import datetime -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer - -VERSION = "1.1.1" - -# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https) -SERVICES = { - # Containers LXC - "ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22}, - "ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"}, - - "ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22}, - "ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"}, - - "ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22}, - "ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"}, - "ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True}, - - "ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22}, - "ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"}, - - "ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22}, - "ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"}, - - "ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22}, - "ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"}, - "ct200-traefik-https": {"name": "Traefik HTTPS", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "insecure": True}, - - "ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22}, - "ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"}, - - "ct202-gitea": {"name": "Gitea", "type": "ssh", "host": "192.168.0.202", "port": 22}, - "ct202-gitea-http": {"name": "Gitea Web", "type": "http", "host": "192.168.0.202", "port": 3000, "url": "/"}, - - # VM - "vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22}, - "vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/"}, - - # Proxmox hosts - "prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22}, - "prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22}, - - # Raspberry Pi - "pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22}, - - # External HTTPS endpoints - "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "insecure": True}, - "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "insecure": True}, - "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "insecure": True}, - "status-peis-fr": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "insecure": True}, -} - -status_history = {} -status_cache = {} -lock = threading.Lock() - - -def check_ssh(host, port, timeout=3): - """Check if SSH port is open.""" - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - result = sock.connect_ex((host, port)) - sock.close() - if result == 0: - return {"status": "online", "error": None} - else: - return {"status": "offline", "error": f"Connection refused (code: {result})"} - except socket.timeout: - return {"status": "timeout", "error": "Connection timed out"} - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_http(host, port, url="/", timeout=5): - """Check HTTP endpoint.""" - try: - full_url = f"http://{host}:{port}{url}" - req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) - with urllib.request.urlopen(req, timeout=timeout) as r: - return {"status": "online", "status_code": r.status, "error": None} - except urllib.error.HTTPError as e: - return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_https(host, port, url="/", timeout=5, insecure=False): - """Check HTTPS endpoint.""" - try: - full_url = f"https://{host}:{port}{url}" - req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) - if insecure: - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - else: - ctx = ssl.create_default_context() - with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r: - return {"status": "online", "status_code": r.status, "error": None} - except ssl.SSLError as e: - return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"} - except urllib.error.HTTPError as e: - return {"status": "online", "status_code": e.code, "error": None} - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_service(service_id, config): - """Check a single service based on its type.""" - st = config["type"] - if st == "ssh": - return check_ssh(config["host"], config["port"]) - elif st == "http": - return check_http(config["host"], config["port"], config.get("url", "/")) - elif st == "https": - return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False)) - return {"status": "error", "error": f"Unknown type: {st}"} - - -def update_history(sid, old, new): - """Update status change history.""" - global status_history - if old != new: - status_history[sid] = { - "old_status": old, - "new_status": new, - "changed_at": datetime.now().isoformat() - } - - -def check_single(sid): - """Check a single service and return its status.""" - if sid not in SERVICES: - return None - result = check_service(sid, SERVICES[sid]) - return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()} - - -def check_all(): - """Check all services and update cache.""" - global status_cache, status_history - new_cache = {} - for sid, cfg in SERVICES.items(): - result = check_service(sid, cfg) - old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") - new = result["status"] - if old != new: - update_history(sid, old, new) - new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()} - with lock: - status_cache = new_cache - return new_cache - - -# HTML Template (using a function to avoid f-string escaping issues) -def generate_html(): - """Generate the HTML page.""" - return ''' - - - - - Homelab Status Monitor v''' + VERSION + ''' - - - -
-

Homelab Status Monitor

-
v''' + VERSION + '''
-
- - - - -
-
-
0
Total
-
0
Online
-
0
Offline
-
0
Errors
-
-
-
- - -''' - - -class Handler(BaseHTTPRequestHandler): - """HTTP request handler.""" - - def log_message(self, *args): - """Suppress default logging.""" - pass - - def do_GET(self): - """Handle GET requests.""" - try: - if self.path in ['/', '/index.html']: - self.send_response(200) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.end_headers() - self.wfile.write(generate_html().encode('utf-8')) - - elif self.path == '/api/status': - check_all() # Always refresh cache - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.send_header('Cache-Control', 'no-cache') - self.end_headers() - self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8')) - - elif self.path == '/api/refresh': - result = check_all() - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8')) - - elif self.path.startswith('/api/check/'): - sid = self.path.split('/')[-1] - if sid in SERVICES: - old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") - rd = check_single(sid) - new_st = rd["result"]["status"] - if old != new_st: - update_history(sid, old, new_st) - with lock: - status_cache[sid] = rd - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8')) - else: - self.send_response(404) - self.end_headers() - - else: - self.send_response(404) - self.end_headers() - - except Exception as e: - self.send_response(500) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8')) - - -def main(): - """Main entry point.""" - print(f"Starting Status Monitor v{VERSION}") - check_all() - server = ThreadingHTTPServer(('', 80), Handler) - print(f'Status Monitor v{VERSION} running on port 80') - server.serve_forever() - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +"""Homelab Status Monitor v1.2.0""" + +import socket +import ssl +import urllib.request +import json +import threading +from datetime import datetime +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +VERSION = "1.2.0" + +# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https) +# public_url: external HTTPS URL shown as clickable link (optional) +SERVICES = { + # Containers LXC + "ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22}, + "ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"}, + + "ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22}, + "ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"}, + + "ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22}, + "ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"}, + "ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True}, + + "ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22}, + "ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"}, + + "ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22}, + "ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"}, + + # Traefik (CT200/CT100) - reverse proxy, no other service mixed here + "ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22}, + "ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"}, + "ct200-traefik-https": {"name": "Traefik HTTPS", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "public_url": "https://traefik.peis.fr/dashboard/", "insecure": True}, + + # Status Monitor (CT201) + "ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22}, + "ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"}, + "ct201-status-https": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "public_url": "https://status.peis.fr/", "insecure": True}, + + # Gitea (CT202) - own dedicated service, not mixed with Traefik + "ct202-gitea": {"name": "Gitea", "type": "ssh", "host": "192.168.0.202", "port": 22}, + "ct202-gitea-http": {"name": "Gitea Web", "type": "http", "host": "192.168.0.202", "port": 3000, "url": "/", "public_url": "https://git.peis.fr/"}, + "ct202-gitea-https": {"name": "git.peis.fr", "type": "https", "host": "git.peis.fr", "port": 443, "url": "/", "public_url": "https://git.peis.fr/", "insecure": True}, + + # VM + "vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22}, + "vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/", "public_url": "https://ha.peis.fr/"}, + + # Proxmox hosts + "prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22}, + "prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22}, + + # Raspberry Pi + "pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22}, + + # External HTTPS endpoints + "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "public_url": "https://ha.peis.fr/", "insecure": True}, + "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "public_url": "https://traefik.peis.fr/dashboard/", "insecure": True}, + "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "public_url": "https://test.peis.fr/", "insecure": True}, +} + +status_history = {} +status_cache = {} +lock = threading.Lock() + + +def check_ssh(host, port, timeout=3): + """Check if SSH port is open.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((host, port)) + sock.close() + if result == 0: + return {"status": "online", "error": None} + else: + return {"status": "offline", "error": f"Connection refused (code: {result})"} + except socket.timeout: + return {"status": "timeout", "error": "Connection timed out"} + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_http(host, port, url="/", timeout=5): + """Check HTTP endpoint.""" + try: + full_url = f"http://{host}:{port}{url}" + req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return {"status": "online", "status_code": r.status, "error": None} + except urllib.error.HTTPError as e: + return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_https(host, port, url="/", timeout=5, insecure=False): + """Check HTTPS endpoint.""" + try: + full_url = f"https://{host}:{port}{url}" + req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) + if insecure: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + else: + ctx = ssl.create_default_context() + with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r: + return {"status": "online", "status_code": r.status, "error": None} + except ssl.SSLError as e: + return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"} + except urllib.error.HTTPError as e: + return {"status": "online", "status_code": e.code, "error": None} + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_service(service_id, config): + """Check a single service based on its type.""" + st = config["type"] + if st == "ssh": + return check_ssh(config["host"], config["port"]) + elif st == "http": + return check_http(config["host"], config["port"], config.get("url", "/")) + elif st == "https": + return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False)) + return {"status": "error", "error": f"Unknown type: {st}"} + + +def update_history(sid, old, new): + """Update status change history.""" + global status_history + if old != new: + status_history[sid] = { + "old_status": old, + "new_status": new, + "changed_at": datetime.now().isoformat() + } + + +def check_single(sid): + """Check a single service and return its status.""" + if sid not in SERVICES: + return None + result = check_service(sid, SERVICES[sid]) + return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()} + + +def check_all(): + """Check all services and update cache.""" + global status_cache, status_history + new_cache = {} + for sid, cfg in SERVICES.items(): + result = check_service(sid, cfg) + old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") + new = result["status"] + if old != new: + update_history(sid, old, new) + new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()} + with lock: + status_cache = new_cache + return new_cache + + +# HTML Template (using a function to avoid f-string escaping issues) +def generate_html(): + """Generate the HTML page.""" + return ''' + + + + + Homelab Status Monitor v''' + VERSION + ''' + + + +
+

Homelab Status Monitor

+
v''' + VERSION + '''
+
+ + + + +
+
+ + + + + +
+
+
0
Total
+
0
Online
+
0
Offline
+
0
Errors
+
+
+
+ + +''' + + +class Handler(BaseHTTPRequestHandler): + """HTTP request handler.""" + + def log_message(self, *args): + """Suppress default logging.""" + pass + + def do_GET(self): + """Handle GET requests.""" + try: + if self.path in ['/', '/index.html']: + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(generate_html().encode('utf-8')) + + elif self.path == '/api/status': + check_all() # Always refresh cache + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Cache-Control', 'no-cache') + self.end_headers() + self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8')) + + elif self.path == '/api/refresh': + result = check_all() + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8')) + + elif self.path.startswith('/api/check/'): + sid = self.path.split('/')[-1] + if sid in SERVICES: + old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") + rd = check_single(sid) + new_st = rd["result"]["status"] + if old != new_st: + update_history(sid, old, new_st) + with lock: + status_cache[sid] = rd + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + + else: + self.send_response(404) + self.end_headers() + + except Exception as e: + self.send_response(500) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8')) + + +def main(): + """Main entry point.""" + print(f"Starting Status Monitor v{VERSION}") + check_all() + server = ThreadingHTTPServer(('', 80), Handler) + print(f'Status Monitor v{VERSION} running on port 80') + server.serve_forever() + + +if __name__ == '__main__': + main() From be3359e52904e4c18155661bdd308346463c4bd6 Mon Sep 17 00:00:00 2001 From: Windsurf Date: Mon, 20 Jul 2026 21:05:22 +0200 Subject: [PATCH 5/6] feat(status-monitor): v1.3.0 - design moderne, waiting, reverse proxy, /apps --- status-monitor/README.md | 618 ++++++++++++++++++++------------------- status-monitor/app.py | 523 ++++++++++++++++++++++++--------- 2 files changed, 701 insertions(+), 440 deletions(-) diff --git a/status-monitor/README.md b/status-monitor/README.md index 045e804..30ede05 100644 --- a/status-monitor/README.md +++ b/status-monitor/README.md @@ -1,306 +1,312 @@ -# Status Monitor - Homelab - -**Application de monitoring des services Homelab** -**Version** : 1.2.0 -**Auteur** : Windsurf -**Dernière mise à jour** : 20/07/2026 - ---- - -## 📋 Description - -Application Python de monitoring des services Homelab. Elle vérifie régulièrement l'état (online/offline) de tous les services configurés et affiche un tableau de bord web en temps réel. - -**Fonctionnalités** : -- Vérification SSH, HTTP, HTTPS -- Interface web responsive avec rafraîchissement automatique -- **Liens cliquables vers chaque service (IP:port ou URL publique HTTPS)** -- **Filtres par type de service (SSH, HTTP, HTTPS, Others)** -- URLs publiques configurables par service (`public_url`) -- Historique des changements de statut -- Support des certificats SSL auto-signés (mode insecure) -- Tableau de bord avec statistiques globales - ---- - -## 🏗️ Architecture - -``` -Internet → Traefik (CT200) → status.peis.fr - ↓ - CT201 (192.168.0.201:80) - ↓ - Status Monitor (Python HTTP Server) - ↓ - Vérifie tous les services configurés -``` - ---- - -## 🚀 Déploiement - -### Pré-requis -- Python 3.x -- Accès réseau à tous les services à surveiller -- Port 80 disponible sur CT201 - -### Installation - -```bash -# Sur CT201 (192.168.0.201) -apt update && apt install -y python3 python3-pip - -# Créer le dossier -mkdir -p /opt/status-monitor -cd /opt/status-monitor - -# Copier les fichiers depuis ce dépôt -# (voir section Workflow Git ci-dessous) - -# Installer le service systemd -cp status-monitor.service /etc/systemd/system/ -systemctl daemon-reload -systemctl enable status-monitor -systemctl start status-monitor - -# Vérifier -systemctl status status-monitor -curl http://localhost -``` - ---- - -## 🔄 Workflow Git (Processus Standard) - -### Structure dans le dépôt -``` -homelab-config/ -└── status-monitor/ - ├── app.py # Application principale - ├── deploy.sh # Script de déploiement - ├── status-monitor.service # Service systemd - ├── VERSION # Version actuelle - └── README.md # Ce fichier -``` - -### 1. Développer / Modifier le code - -```bash -# Sur ta machine de développement (Windows) -# Modifier les fichiers dans : C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\ - -# Exemple : Modifier app.py -nano C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\app.py -``` - -### 2. Pousser vers Git depuis WSL - -```bash -# Synchroniser IDE -> WSL si nécessaire -cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py - -# Commiter et pousser avec l'utilisateur Windsurf -cd ~/homelab-config -git status -git diff status-monitor/ -git add status-monitor/ -git commit -m "Update Status Monitor - " -GIT_SSL_NO_VERIFY=true git push origin main -``` - -### 3. Déployer sur CT201 - -**Flux obligatoire : IDE (Windows) -> WSL -> CT200/CT100 (bastion 192.168.0.100) -> CT201 (192.168.0.201)** - -```bash -# Méthode A : Exécuter le script de déploiement depuis WSL -wsl bash ~/homelab-config/status-monitor/deploy.sh - -# Méthode B : Manuellement -# 1. IDE -> WSL -cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py - -# 2. WSL -> CT201 (rebond via CT200/CT100) -scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 ~/homelab-config/status-monitor/app.py root@192.168.0.201:/tmp/app.py - -# 3. Installation sur CT201 -ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 'mv /tmp/app.py /opt/status-monitor/app.py && systemctl restart status-monitor' -``` - ---- - -## 📦 Versioning - -### Règles de versioning -- Format : `MAJOR.MINOR.PATCH` (ex: 1.1.0) -- **MAJOR** : Changements incompatibles (refonte complète) -- **MINOR** : Nouvelles fonctionnalités (compatibles) -- **PATCH** : Corrections de bugs - -### Mettre à jour la version - -```bash -# 1. Modifier le fichier VERSION -nano status-monitor/VERSION - -# 2. Créer un tag Git (optionnel pour les releases) -git tag -a v1.1.0 -m "Release Status Monitor v1.1.0" -git push origin v1.1.0 - -# 3. Commit le changement de version -git add status-monitor/VERSION -git commit -m "Bump version to 1.1.0" -git push -``` - -### Historique des versions -| Version | Date | Changements | -|---------|------|-------------| -| 1.1.0 | 19/07/2026 | Version initiale dans Git | -| 1.0.0 | Avant | Version initiale (non versionnée) | - ---- - -## 📊 Configuration des Services - -Les services à surveiller sont définis dans le dictionnaire `SERVICES` dans `app.py`. - -**Format** : -```python -SERVICES = { - "service_id": { - "name": "Nom affiché", - "type": "ssh" | "http" | "https", - "host": "IP ou hostname", - "port": port_number, - "url": "/chemin" (optionnel, pour http/https), - "public_url": "https://mon-service.peis.fr/" (optionnel, lien cliquable affiché), - "insecure": True | False (optionnel, pour https sans vérification SSL) - }, - ... -} -``` - -**Types disponibles** : -- `ssh` : Vérifie si le port SSH (22) est ouvert -- `http` : Vérifie la réponse HTTP sur un port (lien cliquable http://host:port/url) -- `https` : Vérifie la réponse HTTPS sur un port (lien cliquable https://host:port/url ou public_url) - ---- - -## 🎨 API Endpoints - -| Endpoint | Méthode | Description | -|----------|---------|-------------| -| `/` | GET | Tableau de bord HTML | -| `/api/status` | GET | JSON de tous les statuts | -| `/api/refresh` | GET | Rafraîchit tous les services | -| `/api/check/` | GET | Vérifie un service spécifique | - ---- - -## 🔧 Configuration Traefik - -Le service est exposé via Traefik sur CT200 avec la configuration suivante dans `dynamic.yml` : - -```yaml -http: - routers: - status-monitor: - rule: "Host(`status.peis.fr`)" - entryPoints: - - "websecure" - tls: - certResolver: "letsencrypt" - service: status-monitor - - services: - status-monitor: - loadBalancer: - servers: - - url: "http://192.168.0.201:80" -``` - -**URL publique** : https://status.peis.fr - ---- - -## ⚡ Commandes Utiles - -### Sur CT201 - -```bash -# Voir les logs -journalctl -u status-monitor -f - -# Redémarrer -systemctl restart status-monitor - -# Tester localement -curl http://localhost -curl http://localhost/api/status | jq -``` - -### Depuis l'extérieur - -```bash -# Vérifier l'accès -curl -vk https://status.peis.fr -curl -vk https://status.peis.fr/api/status -``` - ---- - -## 🛠️ Développement - -### Ajouter un nouveau service - -1. Éditer `app.py` -2. Ajouter une entrée dans `SERVICES` -3. Tester localement -4. Pousser vers Git -5. Déployer - -### Tester localement - -```bash -# Sur ta machine de dev -python3 app.py -# Accéder à http://localhost:80 -``` - ---- - -## 📝 Credentials et Accès - -| Élément | Valeur | -|---------|--------| -| **Dépôt Git** | git.peis.fr/VisualPCI/homelab-config | -| **Username Git** | Windsurf | -| **Token Git** | 3N14E7GWVSgi3K | -| **Auteur des commits** | Windsurf | -| **Serveur de déploiement** | CT201 (192.168.0.201) | -| **Bastion SSH** | CT200/CT100 (192.168.0.100) | -| **URL publique** | https://status.peis.fr | -| **Port** | 80 | - ---- - -## 🎯 Prochaines Évolutions - -- [ ] Ajouter l'authentification basique -- [ ] Ajouter des notifications (email, webhook Discord/Slack) -- [ ] Intégrer avec Grafana/Prometheus -- [ ] Historique persistant en base de données (SQLite/InfluxDB) -- [ ] Dashboard personnalisable (drag & drop des cartes) -- [ ] Temps de réponse / latence par service -- [ ] Catégories/groupes de services (Proxmox, Containers, VM, Externe) -- [ ] Mode sombre/clair -- [ ] Export CSV/PDF des statuts -- [ ] Page d'historique détaillée avec graphiques -- [ ] Gestion des certificats SSL (expiration, renouvellement) -- [ ] Support du ping ICMP en plus de TCP - ---- - -*Documentation générée par Windsurf - 20/07/2026* +# Status Monitor - Homelab + +**Application de monitoring des services Homelab** +**Version** : 1.3.0 +**Auteur** : Windsurf +**Dernière mise à jour** : 20/07/2026 + +--- + +## 📋 Description + +Application Python de monitoring des services Homelab. Elle vérifie régulièrement l'état (online/offline) de tous les services configurés et affiche un tableau de bord web en temps réel. + +**Fonctionnalités** : +- Vérification SSH, HTTP, HTTPS +- Interface web responsive moderne (glassmorphism, gradient, rounded cards) +- **Liens cliquables vers chaque service (IP:port ou URL publique HTTPS)** +- **Filtres par type de service (SSH, HTTP, HTTPS, Reverse Proxy, Others)** +- **Affichage immédiat des tuiles au chargement (statut `waiting`)** +- **Label `reverse proxy` pour les services de routage Traefik** +- Page `/apps` listant les applications avec URL cliquables, ordre réordonnable et persistant (localStorage) +- URLs publiques configurables par service (`public_url`) +- Historique des changements de statut +- Support des certificats SSL auto-signés (mode insecure) +- Tableau de bord avec statistiques globales + +--- + +## 🏗️ Architecture + +``` +Internet → Traefik (CT200) → status.peis.fr + ↓ + CT201 (192.168.0.201:80) + ↓ + Status Monitor (Python HTTP Server) + ↓ + Vérifie tous les services configurés +``` + +--- + +## 🚀 Déploiement + +### Pré-requis +- Python 3.x +- Accès réseau à tous les services à surveiller +- Port 80 disponible sur CT201 + +### Installation + +```bash +# Sur CT201 (192.168.0.201) +apt update && apt install -y python3 python3-pip + +# Créer le dossier +mkdir -p /opt/status-monitor +cd /opt/status-monitor + +# Copier les fichiers depuis ce dépôt +# (voir section Workflow Git ci-dessous) + +# Installer le service systemd +cp status-monitor.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable status-monitor +systemctl start status-monitor + +# Vérifier +systemctl status status-monitor +curl http://localhost +``` + +--- + +## 🔄 Workflow Git (Processus Standard) + +### Structure dans le dépôt +``` +homelab-config/ +└── status-monitor/ + ├── app.py # Application principale + ├── deploy.sh # Script de déploiement + ├── status-monitor.service # Service systemd + ├── VERSION # Version actuelle + └── README.md # Ce fichier +``` + +### 1. Développer / Modifier le code + +```bash +# Sur ta machine de développement (Windows) +# Modifier les fichiers dans : C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\ + +# Exemple : Modifier app.py +nano C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\app.py +``` + +### 2. Pousser vers Git depuis WSL + +```bash +# Synchroniser IDE -> WSL si nécessaire +cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py + +# Commiter et pousser avec l'utilisateur Windsurf +cd ~/homelab-config +git status +git diff status-monitor/ +git add status-monitor/ +git commit -m "Update Status Monitor - " +GIT_SSL_NO_VERIFY=true git push origin main +``` + +### 3. Déployer sur CT201 + +**Flux obligatoire : IDE (Windows) -> WSL -> CT200/CT100 (bastion 192.168.0.100) -> CT201 (192.168.0.201)** + +```bash +# Méthode A : Exécuter le script de déploiement depuis WSL +wsl bash ~/homelab-config/status-monitor/deploy.sh + +# Méthode B : Manuellement +# 1. IDE -> WSL +cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py + +# 2. WSL -> CT201 (rebond via CT200/CT100) +scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 ~/homelab-config/status-monitor/app.py root@192.168.0.201:/tmp/app.py + +# 3. Installation sur CT201 +ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 'mv /tmp/app.py /opt/status-monitor/app.py && systemctl restart status-monitor' +``` + +--- + +## 📦 Versioning + +### Règles de versioning +- Format : `MAJOR.MINOR.PATCH` (ex: 1.1.0) +- **MAJOR** : Changements incompatibles (refonte complète) +- **MINOR** : Nouvelles fonctionnalités (compatibles) +- **PATCH** : Corrections de bugs + +### Mettre à jour la version + +```bash +# 1. Modifier le fichier VERSION +nano status-monitor/VERSION + +# 2. Créer un tag Git (optionnel pour les releases) +git tag -a v1.1.0 -m "Release Status Monitor v1.1.0" +git push origin v1.1.0 + +# 3. Commit le changement de version +git add status-monitor/VERSION +git commit -m "Bump version to 1.1.0" +git push +``` + +### Historique des versions +| Version | Date | Changements | +|---------|------|-------------| +| 1.3.0 | 20/07/2026 | Design modernisé, tuiles waiting, filtre Reverse Proxy, label reverse proxy, page `/apps` ordonnable | +| 1.2.0 | 20/07/2026 | Liens cliquables, `public_url`, filtres par type, Gitea séparé | +| 1.1.0 | 19/07/2026 | Version initiale dans Git | +| 1.0.0 | Avant | Version initiale (non versionnée) | + +--- + +## 📊 Configuration des Services + +Les services à surveiller sont définis dans le dictionnaire `SERVICES` dans `app.py`. + +**Format** : +```python +SERVICES = { + "service_id": { + "name": "Nom affiché", + "type": "ssh" | "http" | "https", + "host": "IP ou hostname", + "port": port_number, + "url": "/chemin" (optionnel, pour http/https), + "public_url": "https://mon-service.peis.fr/" (optionnel, lien cliquable affiché), + "insecure": True | False (optionnel, pour https sans vérification SSL) + }, + ... +} +``` + +**Types disponibles** : +- `ssh` : Vérifie si le port SSH (22) est ouvert +- `http` : Vérifie la réponse HTTP sur un port (lien cliquable http://host:port/url) +- `https` : Vérifie la réponse HTTPS sur un port (lien cliquable https://host:port/url ou public_url) + +--- + +## 🎨 API Endpoints + +| Endpoint | Méthode | Description | +|----------|---------|-------------| +| `/` | GET | Tableau de bord HTML | +| `/apps` | GET | Liste des applications (URL cliquables, ordre persistant côté client) | +| `/api/status` | GET | JSON de tous les statuts | +| `/api/refresh` | GET | Rafraîchit tous les services | +| `/api/check/` | GET | Vérifie un service spécifique | + +--- + +## 🔧 Configuration Traefik + +Le service est exposé via Traefik sur CT200 avec la configuration suivante dans `dynamic.yml` : + +```yaml +http: + routers: + status-monitor: + rule: "Host(`status.peis.fr`)" + entryPoints: + - "websecure" + tls: + certResolver: "letsencrypt" + service: status-monitor + + services: + status-monitor: + loadBalancer: + servers: + - url: "http://192.168.0.201:80" +``` + +**URL publique** : https://status.peis.fr + +--- + +## ⚡ Commandes Utiles + +### Sur CT201 + +```bash +# Voir les logs +journalctl -u status-monitor -f + +# Redémarrer +systemctl restart status-monitor + +# Tester localement +curl http://localhost +curl http://localhost/api/status | jq +``` + +### Depuis l'extérieur + +```bash +# Vérifier l'accès +curl -vk https://status.peis.fr +curl -vk https://status.peis.fr/api/status +``` + +--- + +## 🛠️ Développement + +### Ajouter un nouveau service + +1. Éditer `app.py` +2. Ajouter une entrée dans `SERVICES` +3. Tester localement +4. Pousser vers Git +5. Déployer + +### Tester localement + +```bash +# Sur ta machine de dev +python3 app.py +# Accéder à http://localhost:80 +``` + +--- + +## 📝 Credentials et Accès + +| Élément | Valeur | +|---------|--------| +| **Dépôt Git** | git.peis.fr/VisualPCI/homelab-config | +| **Username Git** | Windsurf | +| **Token Git** | 3N14E7GWVSgi3K | +| **Auteur des commits** | Windsurf | +| **Serveur de déploiement** | CT201 (192.168.0.201) | +| **Bastion SSH** | CT200/CT100 (192.168.0.100) | +| **URL publique** | https://status.peis.fr | +| **Port** | 80 | + +--- + +## 🎯 Prochaines Évolutions + +- [ ] Ajouter l'authentification basique +- [ ] Ajouter des notifications (email, webhook Discord/Slack) +- [ ] Intégrer avec Grafana/Prometheus +- [ ] Historique persistant en base de données (SQLite/InfluxDB) +- [ ] Dashboard personnalisable (drag & drop des cartes) +- [ ] Temps de réponse / latence par service +- [ ] Catégories/groupes de services (Proxmox, Containers, VM, Externe) +- [ ] Mode sombre/clair +- [ ] Export CSV/PDF des statuts +- [ ] Page d'historique détaillée avec graphiques +- [ ] Gestion des certificats SSL (expiration, renouvellement) +- [ ] Support du ping ICMP en plus de TCP + +--- + +*Documentation générée par Windsurf - 20/07/2026* diff --git a/status-monitor/app.py b/status-monitor/app.py index 7943e1f..dcb3d94 100755 --- a/status-monitor/app.py +++ b/status-monitor/app.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Homelab Status Monitor v1.2.0""" +"""Homelab Status Monitor v1.3.0""" import socket import ssl @@ -9,58 +9,59 @@ import threading from datetime import datetime from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -VERSION = "1.2.0" +VERSION = "1.3.0" # Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https) # public_url: external HTTPS URL shown as clickable link (optional) +# label: extra tag shown as badge on card, e.g. "reverse proxy" (optional) SERVICES = { # Containers LXC "ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22}, "ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"}, - + "ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22}, "ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"}, - + "ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22}, "ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"}, "ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True}, - + "ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22}, "ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"}, - + "ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22}, "ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"}, - + # Traefik (CT200/CT100) - reverse proxy, no other service mixed here "ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22}, "ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"}, - "ct200-traefik-https": {"name": "Traefik HTTPS", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "public_url": "https://traefik.peis.fr/dashboard/", "insecure": True}, - + "ct200-traefik-https": {"name": "Traefik Dashboard", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "public_url": "https://traefik.peis.fr/dashboard/", "insecure": True}, + # Status Monitor (CT201) "ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22}, "ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"}, - "ct201-status-https": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "public_url": "https://status.peis.fr/", "insecure": True}, - + "ct201-status-https": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "public_url": "https://status.peis.fr/", "label": "reverse proxy", "insecure": True}, + # Gitea (CT202) - own dedicated service, not mixed with Traefik "ct202-gitea": {"name": "Gitea", "type": "ssh", "host": "192.168.0.202", "port": 22}, "ct202-gitea-http": {"name": "Gitea Web", "type": "http", "host": "192.168.0.202", "port": 3000, "url": "/", "public_url": "https://git.peis.fr/"}, - "ct202-gitea-https": {"name": "git.peis.fr", "type": "https", "host": "git.peis.fr", "port": 443, "url": "/", "public_url": "https://git.peis.fr/", "insecure": True}, - + "ct202-gitea-https": {"name": "git.peis.fr", "type": "https", "host": "git.peis.fr", "port": 443, "url": "/", "public_url": "https://git.peis.fr/", "label": "reverse proxy", "insecure": True}, + # VM "vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22}, "vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/", "public_url": "https://ha.peis.fr/"}, - + # Proxmox hosts "prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22}, "prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22}, - + # Raspberry Pi "pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22}, - - # External HTTPS endpoints - "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "public_url": "https://ha.peis.fr/", "insecure": True}, - "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "public_url": "https://traefik.peis.fr/dashboard/", "insecure": True}, - "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "public_url": "https://test.peis.fr/", "insecure": True}, + + # External HTTPS endpoints (Traefik reverse proxy) + "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "public_url": "https://ha.peis.fr/", "label": "reverse proxy", "insecure": True}, + "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "public_url": "https://traefik.peis.fr/dashboard/", "label": "reverse proxy", "insecure": True}, + "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "public_url": "https://test.peis.fr/", "label": "reverse proxy", "insecure": True}, } status_history = {} @@ -166,67 +167,192 @@ def check_all(): return new_cache -# HTML Template (using a function to avoid f-string escaping issues) +# HTML Templates def generate_html(): - """Generate the HTML page.""" + """Generate the main dashboard HTML page.""" + services_json = json.dumps(SERVICES) return ''' - + Homelab Status Monitor v''' + VERSION + '''
-

Homelab Status Monitor

-
v''' + VERSION + '''
-
+
+

Homelab Status Monitor

+
v''' + VERSION + '''
+
+
+ Applications List
@@ -235,6 +361,7 @@ def generate_html(): +
@@ -246,19 +373,27 @@ def generate_html():
+ +''' + + +def generate_apps_html(): + """Generate the applications list page with sortable and persistent order.""" + apps = [] + for sid, cfg in SERVICES.items(): + url = None + if cfg.get("public_url"): + url = cfg["public_url"] + elif cfg.get("type") in ("http", "https"): + proto = cfg["type"] + url = f"{proto}://{cfg['host']}:{cfg['port']}{cfg.get('url', '/')}" + if url: + apps.append({"id": sid, "name": cfg["name"], "url": url}) + apps_json = json.dumps(apps) + return ''' + + + + + Applications - Homelab Status Monitor v''' + VERSION + ''' + + + +
+
+ ← Back to dashboard +

Applications

+
+
Drag items to reorder. The order is saved in your browser.
+
    +
    + ''' @@ -417,7 +666,13 @@ class Handler(BaseHTTPRequestHandler): self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() self.wfile.write(generate_html().encode('utf-8')) - + + elif self.path in ['/apps', '/apps.html']: + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(generate_apps_html().encode('utf-8')) + elif self.path == '/api/status': check_all() # Always refresh cache self.send_response(200) From 99bac436ba005de337cf7b7ebf1a576ad3588bb2 Mon Sep 17 00:00:00 2001 From: Windsurf Date: Mon, 20 Jul 2026 21:18:37 +0200 Subject: [PATCH 6/6] fix(status-monitor): corrige guillemets onclick refresh pour eviter erreur JS --- status-monitor/app.py | 1460 ++++++++++++++++++++--------------------- 1 file changed, 730 insertions(+), 730 deletions(-) diff --git a/status-monitor/app.py b/status-monitor/app.py index dcb3d94..7d2c50e 100755 --- a/status-monitor/app.py +++ b/status-monitor/app.py @@ -1,730 +1,730 @@ -#!/usr/bin/env python3 -"""Homelab Status Monitor v1.3.0""" - -import socket -import ssl -import urllib.request -import json -import threading -from datetime import datetime -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer - -VERSION = "1.3.0" - -# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https) -# public_url: external HTTPS URL shown as clickable link (optional) -# label: extra tag shown as badge on card, e.g. "reverse proxy" (optional) -SERVICES = { - # Containers LXC - "ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22}, - "ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"}, - - "ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22}, - "ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"}, - - "ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22}, - "ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"}, - "ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True}, - - "ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22}, - "ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"}, - - "ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22}, - "ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"}, - - # Traefik (CT200/CT100) - reverse proxy, no other service mixed here - "ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22}, - "ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"}, - "ct200-traefik-https": {"name": "Traefik Dashboard", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "public_url": "https://traefik.peis.fr/dashboard/", "insecure": True}, - - # Status Monitor (CT201) - "ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22}, - "ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"}, - "ct201-status-https": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "public_url": "https://status.peis.fr/", "label": "reverse proxy", "insecure": True}, - - # Gitea (CT202) - own dedicated service, not mixed with Traefik - "ct202-gitea": {"name": "Gitea", "type": "ssh", "host": "192.168.0.202", "port": 22}, - "ct202-gitea-http": {"name": "Gitea Web", "type": "http", "host": "192.168.0.202", "port": 3000, "url": "/", "public_url": "https://git.peis.fr/"}, - "ct202-gitea-https": {"name": "git.peis.fr", "type": "https", "host": "git.peis.fr", "port": 443, "url": "/", "public_url": "https://git.peis.fr/", "label": "reverse proxy", "insecure": True}, - - # VM - "vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22}, - "vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/", "public_url": "https://ha.peis.fr/"}, - - # Proxmox hosts - "prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22}, - "prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22}, - - # Raspberry Pi - "pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22}, - - # External HTTPS endpoints (Traefik reverse proxy) - "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "public_url": "https://ha.peis.fr/", "label": "reverse proxy", "insecure": True}, - "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "public_url": "https://traefik.peis.fr/dashboard/", "label": "reverse proxy", "insecure": True}, - "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "public_url": "https://test.peis.fr/", "label": "reverse proxy", "insecure": True}, -} - -status_history = {} -status_cache = {} -lock = threading.Lock() - - -def check_ssh(host, port, timeout=3): - """Check if SSH port is open.""" - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(timeout) - result = sock.connect_ex((host, port)) - sock.close() - if result == 0: - return {"status": "online", "error": None} - else: - return {"status": "offline", "error": f"Connection refused (code: {result})"} - except socket.timeout: - return {"status": "timeout", "error": "Connection timed out"} - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_http(host, port, url="/", timeout=5): - """Check HTTP endpoint.""" - try: - full_url = f"http://{host}:{port}{url}" - req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) - with urllib.request.urlopen(req, timeout=timeout) as r: - return {"status": "online", "status_code": r.status, "error": None} - except urllib.error.HTTPError as e: - return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_https(host, port, url="/", timeout=5, insecure=False): - """Check HTTPS endpoint.""" - try: - full_url = f"https://{host}:{port}{url}" - req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) - if insecure: - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - else: - ctx = ssl.create_default_context() - with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r: - return {"status": "online", "status_code": r.status, "error": None} - except ssl.SSLError as e: - return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"} - except urllib.error.HTTPError as e: - return {"status": "online", "status_code": e.code, "error": None} - except Exception as e: - return {"status": "offline", "error": str(e)[:100]} - - -def check_service(service_id, config): - """Check a single service based on its type.""" - st = config["type"] - if st == "ssh": - return check_ssh(config["host"], config["port"]) - elif st == "http": - return check_http(config["host"], config["port"], config.get("url", "/")) - elif st == "https": - return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False)) - return {"status": "error", "error": f"Unknown type: {st}"} - - -def update_history(sid, old, new): - """Update status change history.""" - global status_history - if old != new: - status_history[sid] = { - "old_status": old, - "new_status": new, - "changed_at": datetime.now().isoformat() - } - - -def check_single(sid): - """Check a single service and return its status.""" - if sid not in SERVICES: - return None - result = check_service(sid, SERVICES[sid]) - return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()} - - -def check_all(): - """Check all services and update cache.""" - global status_cache, status_history - new_cache = {} - for sid, cfg in SERVICES.items(): - result = check_service(sid, cfg) - old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") - new = result["status"] - if old != new: - update_history(sid, old, new) - new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()} - with lock: - status_cache = new_cache - return new_cache - - -# HTML Templates -def generate_html(): - """Generate the main dashboard HTML page.""" - services_json = json.dumps(SERVICES) - return ''' - - - - - Homelab Status Monitor v''' + VERSION + ''' - - - -
    -
    -

    Homelab Status Monitor

    -
    v''' + VERSION + '''
    -
    -
    - - - Applications List - - -
    -
    - - - - - - -
    -
    -
    0
    Total
    -
    0
    Online
    -
    0
    Offline
    -
    0
    Errors
    -
    -
    -
    - - -''' - - -def generate_apps_html(): - """Generate the applications list page with sortable and persistent order.""" - apps = [] - for sid, cfg in SERVICES.items(): - url = None - if cfg.get("public_url"): - url = cfg["public_url"] - elif cfg.get("type") in ("http", "https"): - proto = cfg["type"] - url = f"{proto}://{cfg['host']}:{cfg['port']}{cfg.get('url', '/')}" - if url: - apps.append({"id": sid, "name": cfg["name"], "url": url}) - apps_json = json.dumps(apps) - return ''' - - - - - Applications - Homelab Status Monitor v''' + VERSION + ''' - - - -
    -
    - ← Back to dashboard -

    Applications

    -
    -
    Drag items to reorder. The order is saved in your browser.
    -
      -
      - - -''' - - -class Handler(BaseHTTPRequestHandler): - """HTTP request handler.""" - - def log_message(self, *args): - """Suppress default logging.""" - pass - - def do_GET(self): - """Handle GET requests.""" - try: - if self.path in ['/', '/index.html']: - self.send_response(200) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.end_headers() - self.wfile.write(generate_html().encode('utf-8')) - - elif self.path in ['/apps', '/apps.html']: - self.send_response(200) - self.send_header('Content-type', 'text/html; charset=utf-8') - self.end_headers() - self.wfile.write(generate_apps_html().encode('utf-8')) - - elif self.path == '/api/status': - check_all() # Always refresh cache - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.send_header('Cache-Control', 'no-cache') - self.end_headers() - self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8')) - - elif self.path == '/api/refresh': - result = check_all() - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8')) - - elif self.path.startswith('/api/check/'): - sid = self.path.split('/')[-1] - if sid in SERVICES: - old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") - rd = check_single(sid) - new_st = rd["result"]["status"] - if old != new_st: - update_history(sid, old, new_st) - with lock: - status_cache[sid] = rd - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8')) - else: - self.send_response(404) - self.end_headers() - - else: - self.send_response(404) - self.end_headers() - - except Exception as e: - self.send_response(500) - self.send_header('Content-type', 'application/json') - self.end_headers() - self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8')) - - -def main(): - """Main entry point.""" - print(f"Starting Status Monitor v{VERSION}") - check_all() - server = ThreadingHTTPServer(('', 80), Handler) - print(f'Status Monitor v{VERSION} running on port 80') - server.serve_forever() - - -if __name__ == '__main__': - main() +#!/usr/bin/env python3 +"""Homelab Status Monitor v1.3.0""" + +import socket +import ssl +import urllib.request +import json +import threading +from datetime import datetime +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +VERSION = "1.3.0" + +# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https) +# public_url: external HTTPS URL shown as clickable link (optional) +# label: extra tag shown as badge on card, e.g. "reverse proxy" (optional) +SERVICES = { + # Containers LXC + "ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22}, + "ct101-mqtt-http": {"name": "MQTT HTTP", "type": "http", "host": "192.168.0.5", "port": 1883, "url": "/"}, + + "ct102-frigate": {"name": "Frigate", "type": "ssh", "host": "192.168.0.91", "port": 22}, + "ct102-frigate-http": {"name": "Frigate Web", "type": "http", "host": "192.168.0.91", "port": 5000, "url": "/"}, + + "ct103-adguard": {"name": "AdGuard", "type": "ssh", "host": "192.168.0.92", "port": 22}, + "ct103-adguard-http": {"name": "AdGuard Web", "type": "http", "host": "192.168.0.92", "port": 80, "url": "/"}, + "ct103-adguard-https": {"name": "AdGuard HTTPS", "type": "https", "host": "192.168.0.92", "port": 443, "url": "/", "insecure": True}, + + "ct105-alpine": {"name": "Alpine-Docker", "type": "ssh", "host": "192.168.0.155", "port": 22}, + "ct105-alpine-docker": {"name": "Alpine Docker API", "type": "http", "host": "192.168.0.155", "port": 2375, "url": "/"}, + + "ct106-zigbee": {"name": "Zigbee2MQTT", "type": "ssh", "host": "192.168.0.177", "port": 22}, + "ct106-zigbee-http": {"name": "Zigbee2MQTT Web", "type": "http", "host": "192.168.0.177", "port": 8081, "url": "/"}, + + # Traefik (CT200/CT100) - reverse proxy, no other service mixed here + "ct200-traefik": {"name": "Traefik Proxy", "type": "ssh", "host": "192.168.0.100", "port": 22}, + "ct200-traefik-http": {"name": "Traefik HTTP", "type": "http", "host": "192.168.0.100", "port": 80, "url": "/"}, + "ct200-traefik-https": {"name": "Traefik Dashboard", "type": "https", "host": "192.168.0.100", "port": 443, "url": "/", "public_url": "https://traefik.peis.fr/dashboard/", "insecure": True}, + + # Status Monitor (CT201) + "ct201-status": {"name": "Status Monitor", "type": "ssh", "host": "192.168.0.201", "port": 22}, + "ct201-status-http": {"name": "Status Monitor Web", "type": "http", "host": "192.168.0.201", "port": 80, "url": "/"}, + "ct201-status-https": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "public_url": "https://status.peis.fr/", "label": "reverse proxy", "insecure": True}, + + # Gitea (CT202) - own dedicated service, not mixed with Traefik + "ct202-gitea": {"name": "Gitea", "type": "ssh", "host": "192.168.0.202", "port": 22}, + "ct202-gitea-http": {"name": "Gitea Web", "type": "http", "host": "192.168.0.202", "port": 3000, "url": "/", "public_url": "https://git.peis.fr/"}, + "ct202-gitea-https": {"name": "git.peis.fr", "type": "https", "host": "git.peis.fr", "port": 443, "url": "/", "public_url": "https://git.peis.fr/", "label": "reverse proxy", "insecure": True}, + + # VM + "vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22}, + "vm100-ha-http": {"name": "Home Assistant Web", "type": "http", "host": "192.168.0.43", "port": 8123, "url": "/", "public_url": "https://ha.peis.fr/"}, + + # Proxmox hosts + "prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22}, + "prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22}, + + # Raspberry Pi + "pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22}, + + # External HTTPS endpoints (Traefik reverse proxy) + "ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "public_url": "https://ha.peis.fr/", "label": "reverse proxy", "insecure": True}, + "traefik-dash": {"name": "traefik.peis.fr", "type": "https", "host": "traefik.peis.fr", "port": 443, "url": "/dashboard/", "public_url": "https://traefik.peis.fr/dashboard/", "label": "reverse proxy", "insecure": True}, + "test-peis-fr": {"name": "test.peis.fr", "type": "https", "host": "test.peis.fr", "port": 443, "url": "/", "public_url": "https://test.peis.fr/", "label": "reverse proxy", "insecure": True}, +} + +status_history = {} +status_cache = {} +lock = threading.Lock() + + +def check_ssh(host, port, timeout=3): + """Check if SSH port is open.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + result = sock.connect_ex((host, port)) + sock.close() + if result == 0: + return {"status": "online", "error": None} + else: + return {"status": "offline", "error": f"Connection refused (code: {result})"} + except socket.timeout: + return {"status": "timeout", "error": "Connection timed out"} + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_http(host, port, url="/", timeout=5): + """Check HTTP endpoint.""" + try: + full_url = f"http://{host}:{port}{url}" + req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return {"status": "online", "status_code": r.status, "error": None} + except urllib.error.HTTPError as e: + return {"status": "online", "status_code": e.code, "error": None} # HTTP error still means service is up + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_https(host, port, url="/", timeout=5, insecure=False): + """Check HTTPS endpoint.""" + try: + full_url = f"https://{host}:{port}{url}" + req = urllib.request.Request(full_url, headers={"User-Agent": "StatusMon"}) + if insecure: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + else: + ctx = ssl.create_default_context() + with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r: + return {"status": "online", "status_code": r.status, "error": None} + except ssl.SSLError as e: + return {"status": "ssl_error", "error": f"SSL: {str(e)[:80]}"} + except urllib.error.HTTPError as e: + return {"status": "online", "status_code": e.code, "error": None} + except Exception as e: + return {"status": "offline", "error": str(e)[:100]} + + +def check_service(service_id, config): + """Check a single service based on its type.""" + st = config["type"] + if st == "ssh": + return check_ssh(config["host"], config["port"]) + elif st == "http": + return check_http(config["host"], config["port"], config.get("url", "/")) + elif st == "https": + return check_https(config["host"], config["port"], config.get("url", "/"), config.get("insecure", False)) + return {"status": "error", "error": f"Unknown type: {st}"} + + +def update_history(sid, old, new): + """Update status change history.""" + global status_history + if old != new: + status_history[sid] = { + "old_status": old, + "new_status": new, + "changed_at": datetime.now().isoformat() + } + + +def check_single(sid): + """Check a single service and return its status.""" + if sid not in SERVICES: + return None + result = check_service(sid, SERVICES[sid]) + return {"config": SERVICES[sid], "result": result, "time": datetime.now().isoformat()} + + +def check_all(): + """Check all services and update cache.""" + global status_cache, status_history + new_cache = {} + for sid, cfg in SERVICES.items(): + result = check_service(sid, cfg) + old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") + new = result["status"] + if old != new: + update_history(sid, old, new) + new_cache[sid] = {"config": cfg, "result": result, "time": datetime.now().isoformat()} + with lock: + status_cache = new_cache + return new_cache + + +# HTML Templates +def generate_html(): + """Generate the main dashboard HTML page.""" + services_json = json.dumps(SERVICES) + return ''' + + + + + Homelab Status Monitor v''' + VERSION + ''' + + + +
      +
      +

      Homelab Status Monitor

      +
      v''' + VERSION + '''
      +
      +
      + + + Applications List + + +
      +
      + + + + + + +
      +
      +
      0
      Total
      +
      0
      Online
      +
      0
      Offline
      +
      0
      Errors
      +
      +
      +
      + + +''' + + +def generate_apps_html(): + """Generate the applications list page with sortable and persistent order.""" + apps = [] + for sid, cfg in SERVICES.items(): + url = None + if cfg.get("public_url"): + url = cfg["public_url"] + elif cfg.get("type") in ("http", "https"): + proto = cfg["type"] + url = f"{proto}://{cfg['host']}:{cfg['port']}{cfg.get('url', '/')}" + if url: + apps.append({"id": sid, "name": cfg["name"], "url": url}) + apps_json = json.dumps(apps) + return ''' + + + + + Applications - Homelab Status Monitor v''' + VERSION + ''' + + + +
      +
      + ← Back to dashboard +

      Applications

      +
      +
      Drag items to reorder. The order is saved in your browser.
      +
        +
        + + +''' + + +class Handler(BaseHTTPRequestHandler): + """HTTP request handler.""" + + def log_message(self, *args): + """Suppress default logging.""" + pass + + def do_GET(self): + """Handle GET requests.""" + try: + if self.path in ['/', '/index.html']: + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(generate_html().encode('utf-8')) + + elif self.path in ['/apps', '/apps.html']: + self.send_response(200) + self.send_header('Content-type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write(generate_apps_html().encode('utf-8')) + + elif self.path == '/api/status': + check_all() # Always refresh cache + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.send_header('Cache-Control', 'no-cache') + self.end_headers() + self.wfile.write(json.dumps({"data": status_cache, "history": status_history}, default=str).encode('utf-8')) + + elif self.path == '/api/refresh': + result = check_all() + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"data": result, "history": status_history}, default=str).encode('utf-8')) + + elif self.path.startswith('/api/check/'): + sid = self.path.split('/')[-1] + if sid in SERVICES: + old = status_cache.get(sid, {}).get("result", {}).get("status", "unknown") + rd = check_single(sid) + new_st = rd["result"]["status"] + if old != new_st: + update_history(sid, old, new_st) + with lock: + status_cache[sid] = rd + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({**rd, "history": status_history.get(sid)}, default=str).encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + + else: + self.send_response(404) + self.end_headers() + + except Exception as e: + self.send_response(500) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps({"error": str(e)}).encode('utf-8')) + + +def main(): + """Main entry point.""" + print(f"Starting Status Monitor v{VERSION}") + check_all() + server = ThreadingHTTPServer(('', 80), Handler) + print(f'Status Monitor v{VERSION} running on port 80') + server.serve_forever() + + +if __name__ == '__main__': + main()