From c63ff8a6a05202c08fa9c7f959b7e7e68ed3ff91 Mon Sep 17 00:00:00 2001 From: Windsurf Date: Mon, 20 Jul 2026 20:45:38 +0200 Subject: [PATCH] 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()