merge origin/main and resolve conflicts
This commit is contained in:
+278
-263
@@ -1,263 +1,278 @@
|
|||||||
# AGENTS.md - Instructions pour Mistral Vibe et autres agents
|
# AGENTS.md - Instructions pour Mistral Vibe et autres agents
|
||||||
|
|
||||||
## 🎯 Workflow DevOps Homelab
|
## 🎯 Workflow DevOps Homelab
|
||||||
|
|
||||||
### ✅ RÈGLES FONDAMENTALES
|
### ✅ RÈGLES FONDAMENTALES
|
||||||
|
|
||||||
1. **TOUJOURS travailler depuis le dépôt Git**
|
1. **TOUJOURS travailler depuis le dépôt Git**
|
||||||
- Dépôt principal : https://git.peis.fr/VisualPCI/homelab-config
|
- Dépôt principal : https://git.peis.fr/VisualPCI/homelab-config
|
||||||
- Clone local : `~/homelab-config` (WSL)
|
- Clone local : `~/homelab-config` (WSL)
|
||||||
- **JAMAIS** modifier les fichiers locaux sur `C:\Users\theo\.vibe\Proxmox\inventair\`
|
- **JAMAIS** modifier les fichiers locaux sur `C:\Users\theo\.vibe\Proxmox\inventair\`
|
||||||
|
|
||||||
2. **Avant TOUTE action** :
|
2. **Avant TOUTE action** :
|
||||||
```bash
|
```bash
|
||||||
cd ~/homelab-config
|
cd ~/homelab-config
|
||||||
GIT_SSL_NO_VERIFY=true git pull origin main
|
GIT_SSL_NO_VERIFY=true git pull origin main
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Après TOUTE modification** :
|
3. **Après TOUTE modification** :
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add .
|
||||||
git commit -m "description claire de la modification"
|
git commit -m "description claire de la modification"
|
||||||
GIT_SSL_NO_VERIFY=true git push origin main
|
GIT_SSL_NO_VERIFY=true git push origin main
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Authentification Git** :
|
4. **Authentification Git** :
|
||||||
- URL : `https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git`
|
- URL : `https://Windsurf:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git`
|
||||||
- Utilisateur : VisualPCI
|
- Utilisateur : Windsurf
|
||||||
- Mot de passe : 3N14E7GWVSgi3K
|
- Mot de passe / token : 3N14E7GWVSgi3K
|
||||||
|
- Commit author : `Windsurf <windsurf@peis.fr>`
|
||||||
---
|
|
||||||
|
---
|
||||||
## 📁 Structure du dépôt
|
|
||||||
|
## 📁 Structure du dépôt
|
||||||
```
|
|
||||||
homelab-config/
|
```
|
||||||
├── AGENTS.md ← Ce fichier
|
homelab-config/
|
||||||
├── INSTRUCTIONS.md ← Instructions générales
|
├── AGENTS.md ← Ce fichier
|
||||||
├── homelab-inventaire.md ← Inventaire principal
|
├── INSTRUCTIONS.md ← Instructions générales
|
||||||
├── status-monitor/ ← Service de monitoring
|
├── homelab-inventaire.md ← Inventaire principal
|
||||||
│ ├── app.py ← Code principal (corrigé)
|
├── status-monitor/ ← Service de monitoring
|
||||||
│ ├── app_v1.py ← Backup ancienne version
|
│ ├── app.py ← Code principal (corrigé)
|
||||||
│ ├── app_v2.py ← Backup ancienne version
|
│ ├── app_v1.py ← Backup ancienne version
|
||||||
│ ├── deploy.sh ← Script de déploiement
|
│ ├── app_v2.py ← Backup ancienne version
|
||||||
│ └── dynamic_traefik.yml ← Configuration Traefik
|
│ ├── deploy.sh ← Script de déploiement
|
||||||
├── traefik-config/ ← Configurations Traefik
|
│ └── dynamic_traefik.yml ← Configuration Traefik
|
||||||
└── procedure-*.md ← Documentations
|
├── traefik-config/ ← Configurations Traefik
|
||||||
```
|
└── procedure-*.md ← Documentations
|
||||||
|
```
|
||||||
---
|
|
||||||
|
---
|
||||||
## 🚀 Déploiement Status Monitor (CT201)
|
|
||||||
|
## 🚀 Déploiement Status Monitor (CT201)
|
||||||
### Serveur cible
|
|
||||||
- **IP** : 192.168.0.201
|
### Serveur cible
|
||||||
- **Service** : `status-monitor` (systemd)
|
- **IP** : 192.168.0.201
|
||||||
- **Dossier** : `/opt/status-monitor/`
|
- **Service** : `status-monitor` (systemd)
|
||||||
- **URL** : https://status.peis.fr
|
- **Dossier** : `/opt/status-monitor/`
|
||||||
|
- **URL** : https://status.peis.fr
|
||||||
### Workflow de déploiement
|
|
||||||
|
### Workflow de déploiement
|
||||||
#### 1. Modifier le code
|
|
||||||
```bash
|
**⚠️ Flux obligatoire : IDE (Windows) -> WSL -> CT200/CT100 (bastion 192.168.0.100) -> CT201 (192.168.0.201)**
|
||||||
cd ~/homelab-config
|
|
||||||
# Modifier les fichiers dans status-monitor/
|
```
|
||||||
git add status-monitor/
|
IDE (C:\Users\theo\homelab_ia\homelab-config)
|
||||||
git commit -m "Fix: description de la correction"
|
└── copy ───> WSL (~/homelab-config)
|
||||||
git push origin main
|
└── scp -J root@192.168.0.100 ───> CT201 (192.168.0.201)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Déployer sur CT201
|
#### 1. Modifier le code dans l'IDE
|
||||||
**Option A - Déploiement automatique :**
|
Modifier `status-monitor/app.py` dans l'IDE Windows.
|
||||||
```bash
|
|
||||||
wsl bash ~/homelab-config/status-monitor/deploy.sh
|
#### 2. Synchroniser IDE -> WSL
|
||||||
```
|
```bash
|
||||||
|
cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py
|
||||||
**Option B - Déploiement manuel :**
|
```
|
||||||
```bash
|
|
||||||
# Copier les fichiers
|
#### 3. Commiter et pusher
|
||||||
scp ~/homelab-config/status-monitor/app.py root@192.168.0.201:/opt/status-monitor/
|
```bash
|
||||||
scp ~/homelab-config/status-monitor/deploy.sh root@192.168.0.201:/opt/status-monitor/
|
cd ~/homelab-config
|
||||||
|
GIT_SSL_NO_VERIFY=true git add status-monitor/
|
||||||
# Redémarrer le service
|
GIT_SSL_NO_VERIFY=true git commit -m "Fix: description de la correction"
|
||||||
ssh root@192.168.0.201 "chmod +x /opt/status-monitor/deploy.sh"
|
GIT_SSL_NO_VERIFY=true git push origin main
|
||||||
ssh root@192.168.0.201 "systemctl restart status-monitor"
|
```
|
||||||
```
|
|
||||||
|
#### 4. Déployer sur CT201
|
||||||
#### 3. Vérifier le déploiement
|
**Option A - Déploiement automatique (recommandé) :**
|
||||||
```bash
|
```bash
|
||||||
# Vérifier l'API
|
wsl bash ~/homelab-config/status-monitor/deploy.sh
|
||||||
curl -k -s https://status.peis.fr/api/status | python3 -m json.tool
|
```
|
||||||
|
Ce script synchronise automatiquement l'IDE -> WSL puis déploie via CT200/CT100.
|
||||||
# Ou via navigateur
|
|
||||||
https://status.peis.fr
|
**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
|
||||||
|
|
||||||
## 🐞 Corrections courantes
|
# 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'
|
||||||
### Problème : Cache non rafraîchi
|
```
|
||||||
**Symptôme** : La page status affiche des données anciennes
|
|
||||||
|
#### 5. Vérifier le déploiement
|
||||||
**Cause** : `status_cache` n'était pas rafraîchi après le premier chargement
|
```bash
|
||||||
|
# Vérifier l'API
|
||||||
**Solution appliquée** :
|
curl -k -s https://status.peis.fr/api/status | python3 -m json.tool
|
||||||
```python
|
|
||||||
# AVANT (bug) :
|
# Ou via navigateur
|
||||||
if not status_cache:
|
https://status.peis.fr
|
||||||
check_all()
|
```
|
||||||
|
|
||||||
# APRÈS (corrigé) :
|
---
|
||||||
check_all() # Toujours rafraîchir à chaque appel
|
|
||||||
```
|
## 🐞 Corrections courantes
|
||||||
|
|
||||||
**Fichier** : `status-monitor/app.py` (ligne 372)
|
### 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
|
||||||
## 📋 Commandes utiles
|
|
||||||
|
**Solution appliquée** :
|
||||||
### Git
|
```python
|
||||||
```bash
|
# AVANT (bug) :
|
||||||
# État
|
if not status_cache:
|
||||||
cd ~/homelab-config && git status
|
check_all()
|
||||||
|
|
||||||
# Pull
|
# APRÈS (corrigé) :
|
||||||
cd ~/homelab-config && git pull
|
check_all() # Toujours rafraîchir à chaque appel
|
||||||
|
```
|
||||||
# Commit
|
|
||||||
cd ~/homelab-config && git add . && git commit -m "message" && git push
|
**Fichier** : `status-monitor/app.py` (ligne 372)
|
||||||
|
|
||||||
# Voir l'historique
|
---
|
||||||
cd ~/homelab-config && git log --oneline --graph
|
|
||||||
```
|
## 📋 Commandes utiles
|
||||||
|
|
||||||
### SSH CT201 (via rebond CT200)
|
### Git
|
||||||
```bash
|
```bash
|
||||||
# Se connecter (CT200 est le bastion)
|
# État
|
||||||
ssh -J root@192.168.0.100 root@192.168.0.201
|
cd ~/homelab-config && git status
|
||||||
|
|
||||||
# Voir les logs du service
|
# Pull
|
||||||
ssh -J root@192.168.0.100 root@192.168.0.201 "journalctl -u status-monitor -f"
|
cd ~/homelab-config && git pull
|
||||||
|
|
||||||
# État du service
|
# Commit
|
||||||
ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl status status-monitor"
|
cd ~/homelab-config && git add . && git commit -m "message" && git push
|
||||||
|
|
||||||
# Redémarrer
|
# Voir l'historique
|
||||||
ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl restart status-monitor"
|
cd ~/homelab-config && git log --oneline --graph
|
||||||
```
|
```
|
||||||
|
|
||||||
### SSH CT200 (direct)
|
### SSH CT201 (via rebond CT200)
|
||||||
```bash
|
```bash
|
||||||
# Se connecter directement
|
# Se connecter (CT200 est le bastion)
|
||||||
ssh -i ~/.ssh/id_ed25519 root@192.168.0.100
|
ssh -J root@192.168.0.100 root@192.168.0.201
|
||||||
|
|
||||||
# Copier des fichiers vers CT200
|
# Voir les logs du service
|
||||||
scp -i ~/.ssh/id_ed25519 fichier root@192.168.0.100:/destination/
|
ssh -J root@192.168.0.100 root@192.168.0.201 "journalctl -u status-monitor -f"
|
||||||
|
|
||||||
# Copier des fichiers de CT200 vers local
|
# État du service
|
||||||
scp -i ~/.ssh/id_ed25519 root@192.168.0.100:/source/fichier ./
|
ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl status status-monitor"
|
||||||
```
|
|
||||||
|
# Redémarrer
|
||||||
### Gitea
|
ssh -J root@192.168.0.100 root@192.168.0.201 "systemctl restart status-monitor"
|
||||||
- **URL** : https://git.peis.fr
|
```
|
||||||
- **Compte** : VisualPCI
|
|
||||||
- **Dépôt** : https://git.peis.fr/VisualPCI/homelab-config
|
### SSH CT200 (direct)
|
||||||
|
```bash
|
||||||
### Workflow complet utilisé pour la correction du 20/07/2026
|
# Se connecter directement
|
||||||
|
ssh -i ~/.ssh/id_ed25519 root@192.168.0.100
|
||||||
**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.
|
# Copier des fichiers vers CT200
|
||||||
|
scp -i ~/.ssh/id_ed25519 fichier root@192.168.0.100:/destination/
|
||||||
**Workflow appliqué** :
|
|
||||||
```bash
|
# Copier des fichiers de CT200 vers local
|
||||||
# 1. Cloner depuis WSL (Ubuntu)
|
scp -i ~/.ssh/id_ed25519 root@192.168.0.100:/source/fichier ./
|
||||||
cd ~/homelab-config
|
```
|
||||||
git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git
|
|
||||||
|
### Gitea
|
||||||
# 2. Récupérer le fichier buggé depuis CT201 (via CT200)
|
- **URL** : https://git.peis.fr
|
||||||
ssh -J root@192.168.0.100 root@192.168.0.201 cat /opt/status-monitor/app.py > status-monitor/app_from_ct201.py
|
- **Compte** : VisualPCI
|
||||||
|
- **Dépôt** : https://git.peis.fr/VisualPCI/homelab-config
|
||||||
# 3. Corriger le bug avec Python (remplacer \" par ')
|
|
||||||
python3 -c "
|
### Workflow complet utilisé pour la correction du 20/07/2026
|
||||||
with open('status-monitor/app.py', 'r') as f:
|
|
||||||
content = f.read()
|
**Problème** : SyntaxError: Unexpected identifier 'card' (at index.html:101:35)
|
||||||
content = content.replace('\\\"', \"'\")
|
**Cause** : Dans `status-monitor/app.py`, les strings JavaScript utilisaient `class=\"` qui générait du code JS invalide.
|
||||||
with open('status-monitor/app.py', 'w') as f:
|
|
||||||
f.write(content)
|
**Workflow appliqué** :
|
||||||
"
|
```bash
|
||||||
|
# 1. Cloner depuis WSL (Ubuntu)
|
||||||
# 4. Vérifier la syntaxe Python
|
cd ~/homelab-config
|
||||||
python3 -m py_compile status-monitor/app.py
|
git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git
|
||||||
|
|
||||||
# 5. Commiter avec GIT_SSL_NO_VERIFY
|
# 2. Récupérer le fichier buggé depuis CT201 (via CT200)
|
||||||
GIT_SSL_NO_VERIFY=true git add status-monitor/app.py
|
ssh -J root@192.168.0.100 root@192.168.0.201 cat /opt/status-monitor/app.py > status-monitor/app_from_ct201.py
|
||||||
GIT_SSL_NO_VERIFY=true git commit -m "Fix(status-monitor): Correction des guillemets dans le JavaScript - Resout SyntaxError loading"
|
|
||||||
|
# 3. Corriger le bug avec Python (remplacer \" par ')
|
||||||
# 6. Pull avec rebase (conflit détecté)
|
python3 -c "
|
||||||
GIT_SSL_NO_VERIFY=true git pull --rebase origin main
|
with open('status-monitor/app.py', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
# 7. Pousser vers git.peis.fr
|
content = content.replace('\\\"', \"'\")
|
||||||
GIT_SSL_NO_VERIFY=true git push origin main
|
with open('status-monitor/app.py', 'w') as f:
|
||||||
|
f.write(content)
|
||||||
# 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'
|
# 4. Vérifier la syntaxe Python
|
||||||
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'
|
python3 -m py_compile status-monitor/app.py
|
||||||
ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'ssh root@192.168.0.201 systemctl restart status-monitor'
|
|
||||||
|
# 5. Commiter avec GIT_SSL_NO_VERIFY
|
||||||
# Méthode 2 : Direct avec -J (bastion)
|
GIT_SSL_NO_VERIFY=true git add status-monitor/app.py
|
||||||
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
|
GIT_SSL_NO_VERIFY=true git commit -m "Fix(status-monitor): Correction des guillemets dans le JavaScript - Resout SyntaxError loading"
|
||||||
ssh -J root@192.168.0.100 root@192.168.0.201 'chmod +x /opt/status-monitor/app.py && systemctl restart status-monitor'
|
|
||||||
```
|
# 6. Pull avec rebase (conflit détecté)
|
||||||
|
GIT_SSL_NO_VERIFY=true git pull --rebase origin main
|
||||||
**Résultat** :
|
|
||||||
- Commit `685060c` poussé vers git.peis.fr
|
# 7. Pousser vers git.peis.fr
|
||||||
- Fichier corrigé déployé sur CT201
|
GIT_SSL_NO_VERIFY=true git push origin main
|
||||||
- Service redémarré avec succès
|
|
||||||
|
# 8. Deployer sur CT201 (via CT200 comme bastion)
|
||||||
**Leçons apprises** :
|
# Méthode 1 : Copier via CT200
|
||||||
- Toujours utiliser `-J` pour le rebond SSH vers CT201
|
cat status-monitor/app.py | ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'cat > /tmp/app_new.py'
|
||||||
- `GIT_SSL_NO_VERIFY=true` est nécessaire pour contourner les problèmes de certificat
|
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'
|
||||||
- Utiliser `python3` pour les scripts de correction complexes
|
ssh -i ~/.ssh/id_ed25519 root@192.168.0.100 'ssh root@192.168.0.201 systemctl restart status-monitor'
|
||||||
- Tester la syntaxe Python avant de deployer
|
|
||||||
|
# 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'
|
||||||
## 🚨 Erreurs à éviter
|
```
|
||||||
|
|
||||||
- ❌ Modifier directement les fichiers sur CT201 sans passer par Git
|
**Résultat** :
|
||||||
- ❌ Oublier de faire `git pull` avant de modifier un fichier
|
- Commit `685060c` poussé vers git.peis.fr
|
||||||
- ❌ Oublier de commiter et pusher après une modification
|
- Fichier corrigé déployé sur CT201
|
||||||
- ❌ Travailler depuis plusieurs emplacements différents
|
- Service redémarré avec succès
|
||||||
- ❌ Modifier les fichiers dans `C:\Users\theo\.vibe\Proxmox\inventair\`
|
|
||||||
|
**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
|
||||||
## ✅ Checklist avant déploiement
|
- Utiliser `python3` pour les scripts de correction complexes
|
||||||
|
- Tester la syntaxe Python avant de deployer
|
||||||
- [ ] `git pull` exécuté
|
|
||||||
- [ ] Modifications testées localement
|
---
|
||||||
- [ ] `git add .` exécuté
|
|
||||||
- [ ] `git commit -m "..."` exécuté
|
## 🚨 Erreurs à éviter
|
||||||
- [ ] `git push origin main` exécuté
|
|
||||||
- [ ] Déploiement vérifié sur https://status.peis.fr
|
- ❌ 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
|
||||||
## 📊 Historique des modifications
|
- ❌ Modifier les fichiers dans `C:\Users\theo\.vibe\Proxmox\inventair\`
|
||||||
|
|
||||||
| Date | Commit | Description |
|
---
|
||||||
|------|--------|-------------|
|
|
||||||
| 2026-07-20 | `685060c` | Fix(status-monitor): Correction des guillemets dans le JavaScript - Resout SyntaxError loading |
|
## ✅ Checklist avant déploiement
|
||||||
| 2026-07-19 | `6b77f49` | Fix: Force cache refresh on /api/status endpoint |
|
|
||||||
| 2026-07-19 | `3ed1d96` | Add: deploy.sh script for DevOps workflow |
|
- [ ] `git pull` exécuté
|
||||||
| 2026-07-19 | `da6b794` | Ajout: INSTRUCTIONS.md pour workflow Git |
|
- [ ] Modifications testées localement
|
||||||
|
- [ ] `git add .` exécuté
|
||||||
---
|
- [ ] `git commit -m "..."` exécuté
|
||||||
|
- [ ] `git push origin main` exécuté
|
||||||
*Dernière mise à jour : 2026-07-20*
|
- [ ] Déploiement vérifié sur https://status.peis.fr
|
||||||
|
|
||||||
*Dernière mise à jour : 2026-07-19*
|
---
|
||||||
*Pour : Mistral Vibe et agents homelab*
|
|
||||||
|
## 📊 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*
|
||||||
|
|||||||
@@ -25,6 +25,38 @@
|
|||||||
git commit -m "description claire"
|
git commit -m "description claire"
|
||||||
GIT_SSL_NO_VERIFY=true git push origin main
|
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'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 <windsurf@peis.fr>`
|
||||||
|
- Le dépôt reste VisualPCI/homelab-config, l'authentification se fait avec Windsurf.
|
||||||
|
|
||||||
## Sources de vérité
|
## Sources de vérité
|
||||||
- Dépôt Git : https://git.peis.fr/VisualPCI/homelab-config
|
- Dépôt Git : https://git.peis.fr/VisualPCI/homelab-config
|
||||||
- Clone local : ~/homelab-config (WSL)
|
- Clone local : ~/homelab-config (WSL)
|
||||||
|
|||||||
+47
-45
@@ -1,9 +1,9 @@
|
|||||||
# Status Monitor - Homelab
|
# Status Monitor - Homelab
|
||||||
|
|
||||||
**Application de monitoring des services Homelab**
|
**Application de monitoring des services Homelab**
|
||||||
**Version** : 1.1.0
|
**Version** : 1.3.0
|
||||||
**Auteur** : Mistral Vibe
|
**Auteur** : Windsurf
|
||||||
**Dernière mise à jour** : 19/07/2026
|
**Dernière mise à jour** : 20/07/2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -13,7 +13,13 @@ Application Python de monitoring des services Homelab. Elle vérifie régulière
|
|||||||
|
|
||||||
**Fonctionnalités** :
|
**Fonctionnalités** :
|
||||||
- Vérification SSH, HTTP, HTTPS
|
- Vérification SSH, HTTP, HTTPS
|
||||||
- Interface web responsive avec rafraîchissement automatique
|
- 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
|
- Historique des changements de statut
|
||||||
- Support des certificats SSL auto-signés (mode insecure)
|
- Support des certificats SSL auto-signés (mode insecure)
|
||||||
- Tableau de bord avec statistiques globales
|
- Tableau de bord avec statistiques globales
|
||||||
@@ -90,55 +96,38 @@ homelab-config/
|
|||||||
nano C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\app.py
|
nano C:\Users\theo\.vibe\Proxmox\inventair\status-monitor\app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Pousser vers Git (via CT200)
|
### 2. Pousser vers Git depuis WSL
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Copier les fichiers modifiés vers CT200
|
# Synchroniser IDE -> WSL si nécessaire
|
||||||
scp -i C:\Users\theo\.ssh\id_ed25519 app.py root@192.168.0.100:/opt/git-repos/homelab-config/status-monitor/
|
cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py
|
||||||
|
|
||||||
# Sur CT200 :
|
# Commiter et pousser avec l'utilisateur Windsurf
|
||||||
ssh -i C:\Users\theo\.ssh\id_ed25519 root@192.168.0.100
|
cd ~/homelab-config
|
||||||
cd /opt/git-repos/homelab-config
|
|
||||||
|
|
||||||
# Vérifier les changements
|
|
||||||
git status
|
git status
|
||||||
git diff status-monitor/
|
git diff status-monitor/
|
||||||
|
|
||||||
# Ajouter, commiter, pousser
|
|
||||||
git add status-monitor/
|
git add status-monitor/
|
||||||
git commit -m "Update Status Monitor - <description des changements>
|
git commit -m "Update Status Monitor - <description des changements>"
|
||||||
|
GIT_SSL_NO_VERIFY=true git push origin main
|
||||||
Generated by Mistral Vibe.
|
|
||||||
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>"
|
|
||||||
git push origin main
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Déployer sur CT201
|
### 3. Déployer sur CT201
|
||||||
|
|
||||||
|
**Flux obligatoire : IDE (Windows) -> WSL -> CT200/CT100 (bastion 192.168.0.100) -> CT201 (192.168.0.201)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Méthode A : Exécuter le script de déploiement
|
# Méthode A : Exécuter le script de déploiement depuis WSL
|
||||||
bash /opt/git-repos/homelab-config/status-monitor/deploy.sh
|
wsl bash ~/homelab-config/status-monitor/deploy.sh
|
||||||
|
|
||||||
# Méthode B : Manuellement
|
# Méthode B : Manuellement
|
||||||
ssh root@192.168.0.201
|
# 1. IDE -> WSL
|
||||||
cd /opt/status-monitor
|
cp /mnt/c/Users/theo/homelab_ia/homelab-config/status-monitor/app.py ~/homelab-config/status-monitor/app.py
|
||||||
|
|
||||||
# Cloner ou pull le dépôt
|
# 2. WSL -> CT201 (rebond via CT200/CT100)
|
||||||
if [ ! -d "homelab-config" ]; then
|
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
|
||||||
git clone https://VisualPCI:3N14E7GWVSgi3K@git.peis.fr/VisualPCI/homelab-config.git
|
|
||||||
else
|
|
||||||
cd homelab-config
|
|
||||||
git pull
|
|
||||||
cd ..
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copier les fichiers
|
# 3. Installation sur CT201
|
||||||
cp -f homelab-config/status-monitor/app.py .
|
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'
|
||||||
cp -f homelab-config/status-monitor/status-monitor.service /etc/systemd/system/
|
|
||||||
|
|
||||||
# Redémarrer
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart status-monitor
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -170,6 +159,8 @@ git push
|
|||||||
### Historique des versions
|
### Historique des versions
|
||||||
| Version | Date | Changements |
|
| 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.1.0 | 19/07/2026 | Version initiale dans Git |
|
||||||
| 1.0.0 | Avant | Version initiale (non versionnée) |
|
| 1.0.0 | Avant | Version initiale (non versionnée) |
|
||||||
|
|
||||||
@@ -188,6 +179,7 @@ SERVICES = {
|
|||||||
"host": "IP ou hostname",
|
"host": "IP ou hostname",
|
||||||
"port": port_number,
|
"port": port_number,
|
||||||
"url": "/chemin" (optionnel, pour http/https),
|
"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)
|
"insecure": True | False (optionnel, pour https sans vérification SSL)
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
@@ -196,8 +188,8 @@ SERVICES = {
|
|||||||
|
|
||||||
**Types disponibles** :
|
**Types disponibles** :
|
||||||
- `ssh` : Vérifie si le port SSH (22) est ouvert
|
- `ssh` : Vérifie si le port SSH (22) est ouvert
|
||||||
- `http` : Vérifie la réponse HTTP sur un port
|
- `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
|
- `https` : Vérifie la réponse HTTPS sur un port (lien cliquable https://host:port/url ou public_url)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -206,6 +198,7 @@ SERVICES = {
|
|||||||
| Endpoint | Méthode | Description |
|
| Endpoint | Méthode | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `/` | GET | Tableau de bord HTML |
|
| `/` | 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/status` | GET | JSON de tous les statuts |
|
||||||
| `/api/refresh` | GET | Rafraîchit tous les services |
|
| `/api/refresh` | GET | Rafraîchit tous les services |
|
||||||
| `/api/check/<service_id>` | GET | Vérifie un service spécifique |
|
| `/api/check/<service_id>` | GET | Vérifie un service spécifique |
|
||||||
@@ -289,9 +282,11 @@ python3 app.py
|
|||||||
| Élément | Valeur |
|
| Élément | Valeur |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| **Dépôt Git** | git.peis.fr/VisualPCI/homelab-config |
|
| **Dépôt Git** | git.peis.fr/VisualPCI/homelab-config |
|
||||||
| **Username Git** | VisualPCI |
|
| **Username Git** | Windsurf |
|
||||||
| **Token Git** | 3N14E7GWVSgi3K |
|
| **Token Git** | 3N14E7GWVSgi3K |
|
||||||
|
| **Auteur des commits** | Windsurf <windsurf@peis.fr> |
|
||||||
| **Serveur de déploiement** | CT201 (192.168.0.201) |
|
| **Serveur de déploiement** | CT201 (192.168.0.201) |
|
||||||
|
| **Bastion SSH** | CT200/CT100 (192.168.0.100) |
|
||||||
| **URL publique** | https://status.peis.fr |
|
| **URL publique** | https://status.peis.fr |
|
||||||
| **Port** | 80 |
|
| **Port** | 80 |
|
||||||
|
|
||||||
@@ -300,11 +295,18 @@ python3 app.py
|
|||||||
## 🎯 Prochaines Évolutions
|
## 🎯 Prochaines Évolutions
|
||||||
|
|
||||||
- [ ] Ajouter l'authentification basique
|
- [ ] Ajouter l'authentification basique
|
||||||
- [ ] Ajouter des notifications (email, webhook)
|
- [ ] Ajouter des notifications (email, webhook Discord/Slack)
|
||||||
- [ ] Intégrer avec Grafana/Prometheus
|
- [ ] Intégrer avec Grafana/Prometheus
|
||||||
- [ ] Historique persistant en base de données
|
- [ ] Historique persistant en base de données (SQLite/InfluxDB)
|
||||||
- [ ] Dashboard personnalisable
|
- [ ] 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 Mistral Vibe - 19/07/2026*
|
*Documentation générée par Windsurf - 20/07/2026*
|
||||||
|
|||||||
+421
-119
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Homelab Status Monitor v1.1.1"""
|
"""Homelab Status Monitor v1.3.0"""
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
@@ -9,53 +9,59 @@ import threading
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
VERSION = "1.1.1"
|
VERSION = "1.3.0"
|
||||||
|
|
||||||
# Service configuration: name, type (ssh/http/https), host, port, url (for http/https), insecure (for https)
|
# 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 = {
|
SERVICES = {
|
||||||
# Containers LXC
|
# Containers LXC
|
||||||
"ct101-mqtt": {"name": "MQTT", "type": "ssh", "host": "192.168.0.5", "port": 22},
|
"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": "/"},
|
"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": {"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": "/"},
|
"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": {"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-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},
|
"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": {"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": "/"},
|
"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": {"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": "/"},
|
"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": {"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-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},
|
"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": {"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-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": {"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": "/"},
|
"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
|
# VM
|
||||||
"vm100-ha": {"name": "Home Assistant", "type": "ssh", "host": "192.168.0.43", "port": 22},
|
"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": "/"},
|
"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
|
# Proxmox hosts
|
||||||
"prox": {"name": "Proxmox (prox)", "type": "ssh", "host": "192.168.0.94", "port": 22},
|
"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},
|
"prox2": {"name": "Proxmox (prox2)", "type": "ssh", "host": "192.168.0.95", "port": 22},
|
||||||
|
|
||||||
# Raspberry Pi
|
# Raspberry Pi
|
||||||
"pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22},
|
"pi3": {"name": "Raspberry Pi 3", "type": "ssh", "host": "192.168.0.156", "port": 22},
|
||||||
|
|
||||||
# External HTTPS endpoints
|
# External HTTPS endpoints (Traefik reverse proxy)
|
||||||
"ha-peis-fr": {"name": "ha.peis.fr", "type": "https", "host": "ha.peis.fr", "port": 443, "url": "/", "insecure": True},
|
"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/", "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": "/", "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-peis-fr": {"name": "status.peis.fr", "type": "https", "host": "status.peis.fr", "port": 443, "url": "/", "insecure": True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status_history = {}
|
status_history = {}
|
||||||
@@ -161,64 +167,203 @@ def check_all():
|
|||||||
return new_cache
|
return new_cache
|
||||||
|
|
||||||
|
|
||||||
# HTML Template (using a function to avoid f-string escaping issues)
|
# HTML Templates
|
||||||
def generate_html():
|
def generate_html():
|
||||||
"""Generate the HTML page."""
|
"""Generate the main dashboard HTML page."""
|
||||||
|
services_json = json.dumps(SERVICES)
|
||||||
return '''<!DOCTYPE html>
|
return '''<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Homelab Status Monitor v''' + VERSION + '''</title>
|
<title>Homelab Status Monitor v''' + VERSION + '''</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 20px; }
|
:root {
|
||||||
|
--bg: #0b0f19;
|
||||||
|
--surface: rgba(30, 41, 59, 0.65);
|
||||||
|
--surface-solid: #151d2a;
|
||||||
|
--text: #e2e8f0;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
--accent: #60a5fa;
|
||||||
|
--accent-2: #818cf8;
|
||||||
|
--online: #22c55e;
|
||||||
|
--offline: #ef4444;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--waiting: #64748b;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
|
background: radial-gradient(circle at top left, #172033 0%, var(--bg) 40%), var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
.container { max-width: 1400px; margin: 0 auto; }
|
.container { max-width: 1400px; margin: 0 auto; }
|
||||||
h1 { color: #60a5fa; text-align: center; margin-bottom: 5px; }
|
header { text-align: center; margin-bottom: 24px; position: relative; }
|
||||||
.version { text-align: center; color: #64748b; font-size: 0.85em; margin-bottom: 15px; }
|
h1 {
|
||||||
.controls { text-align: center; margin: 20px 0; position: relative; }
|
font-size: 2.4em;
|
||||||
button { padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9em; }
|
font-weight: 800;
|
||||||
button:hover { background: #2563eb; }
|
margin: 0;
|
||||||
button:disabled { opacity: 0.7; cursor: not-allowed; }
|
background: linear-gradient(90deg, var(--accent), var(--accent-2));
|
||||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 15px; margin-top: 20px; }
|
-webkit-background-clip: text;
|
||||||
.card { background: #1e293b; padding: 15px; border-radius: 8px; border-left: 4px solid; position: relative; }
|
-webkit-text-fill-color: transparent;
|
||||||
.card.online { border-left-color: #22c55e; }
|
letter-spacing: -0.02em;
|
||||||
.card.offline { border-left-color: #ef4444; }
|
}
|
||||||
.card.timeout { border-left-color: #f97316; }
|
.version { color: var(--muted); font-size: 0.9em; margin-top: 6px; }
|
||||||
.card.ssl_error { border-left-color: #f97316; }
|
.topbar {
|
||||||
.card.error { border-left-color: #ef4444; }
|
display: flex;
|
||||||
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
justify-content: center;
|
||||||
.name { font-weight: bold; font-size: 1.1em; flex: 1; }
|
align-items: center;
|
||||||
.type { font-size: 0.8em; background: #334155; padding: 2px 8px; border-radius: 12px; color: #94a3b8; }
|
gap: 12px;
|
||||||
.badge { padding: 4px 12px; border-radius: 12px; font-size: 0.85em; font-weight: bold; }
|
flex-wrap: wrap;
|
||||||
.badge.online { background: #166534; color: #dcfce7; }
|
margin: 20px 0;
|
||||||
.badge.offline { background: #991b1b; color: #fca5a5; }
|
}
|
||||||
.badge.timeout { background: #92400e; color: #fde68a; }
|
.topbar a, .topbar button {
|
||||||
.badge.ssl_error { background: #92400e; color: #fde68a; }
|
padding: 10px 18px;
|
||||||
.badge.error { background: #991b1b; color: #fca5a5; }
|
border-radius: 999px;
|
||||||
.details { margin-top: 10px; font-size: 0.85em; color: #94a3b8; }
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
.error { color: #fca5a5; margin-top: 5px; font-size: 0.85em; word-break: break-word; }
|
background: var(--surface);
|
||||||
.summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
|
color: var(--text);
|
||||||
.card-sum { background: #1e293b; padding: 15px; border-radius: 8px; text-align: center; }
|
font-size: 0.9em;
|
||||||
.card-sum .count { font-size: 2em; font-weight: bold; color: #60a5fa; }
|
cursor: pointer;
|
||||||
.card-sum .label { color: #94a3b8; }
|
text-decoration: none;
|
||||||
.history-note { font-size: 0.75em; color: #64748b; margin-top: 5px; }
|
backdrop-filter: blur(12px);
|
||||||
.refresh-btn { position: absolute; top: 10px; right: 10px; background: transparent; border: none; cursor: pointer; font-size: 1.2em; padding: 0; line-height: 1; }
|
transition: transform 0.15s, background 0.2s, box-shadow 0.2s;
|
||||||
.refresh-btn:hover { opacity: 0.8; }
|
}
|
||||||
.refresh-btn:disabled { cursor: not-allowed; opacity: 0.5; }
|
.topbar a:hover, .topbar button:hover:not(:disabled) {
|
||||||
|
background: rgba(96, 165, 250, 0.18);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 20px rgba(96, 165, 250, 0.12);
|
||||||
|
}
|
||||||
|
.topbar button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
.filters { text-align: center; margin: 18px 0; }
|
||||||
|
.filter-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 5px 8px 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 8px 18px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.filter-btn.active { background: linear-gradient(90deg, var(--accent), var(--accent-2)); color: #fff; border-color: transparent; font-weight: 600; }
|
||||||
|
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin: 22px 0; }
|
||||||
|
.card-sum {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 18px;
|
||||||
|
text-align: center;
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
.card-sum .count { font-size: 2.2em; font-weight: 800; color: var(--accent); }
|
||||||
|
.card-sum .label { color: var(--muted); font-size: 0.9em; margin-top: 4px; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 18px; margin-top: 24px; }
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.12);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 18px;
|
||||||
|
position: relative;
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; height: 4px;
|
||||||
|
background: var(--waiting);
|
||||||
|
}
|
||||||
|
.card.online::before { background: var(--online); box-shadow: 0 0 14px var(--online); }
|
||||||
|
.card.offline::before { background: var(--offline); box-shadow: 0 0 14px var(--offline); }
|
||||||
|
.card.timeout::before, .card.ssl_error::before { background: var(--warning); box-shadow: 0 0 14px var(--warning); }
|
||||||
|
.card.error::before { background: var(--offline); box-shadow: 0 0 14px var(--offline); }
|
||||||
|
.card.waiting::before { background: var(--waiting); }
|
||||||
|
.card:hover { transform: translateY(-3px); box-shadow: 0 16px 48px rgba(0,0,0,0.28); }
|
||||||
|
.card-header { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||||
|
.name { font-weight: 700; font-size: 1.15em; flex: 1; }
|
||||||
|
.badges-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||||
|
.tag {
|
||||||
|
font-size: 0.72em;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.tag.type { background: rgba(148, 163, 184, 0.18); color: var(--muted); }
|
||||||
|
.tag.label { background: linear-gradient(90deg, rgba(96,165,250,0.2), rgba(129,140,248,0.2)); color: var(--accent); border: 1px solid rgba(96,165,250,0.25); }
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.badge::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
|
||||||
|
.badge.online { background: rgba(34, 197, 94, 0.12); color: var(--online); }
|
||||||
|
.badge.offline { background: rgba(239, 68, 68, 0.12); color: var(--offline); }
|
||||||
|
.badge.timeout, .badge.ssl_error { background: rgba(245, 158, 11, 0.12); color: var(--warning); }
|
||||||
|
.badge.error { background: rgba(239, 68, 68, 0.12); color: var(--offline); }
|
||||||
|
.badge.waiting { background: rgba(100, 116, 139, 0.12); color: var(--waiting); }
|
||||||
|
.details { margin-top: 14px; font-size: 0.88em; color: var(--muted); }
|
||||||
|
.details a { color: var(--accent); text-decoration: none; }
|
||||||
|
.details a:hover { text-decoration: underline; }
|
||||||
|
.error { color: #fca5a5; margin-top: 8px; font-size: 0.85em; word-break: break-word; }
|
||||||
|
.history-note { font-size: 0.78em; color: var(--muted); margin-top: 10px; }
|
||||||
|
.refresh-btn {
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
background: var(--surface-solid);
|
||||||
|
color: var(--text);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95em;
|
||||||
|
transition: background 0.2s, transform 0.15s;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.refresh-btn:hover:not(:disabled) { background: rgba(96, 165, 250, 0.22); transform: rotate(90deg); }
|
||||||
|
.refresh-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||||
.loading-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid #334155; border-top-color: #60a5fa; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 8px; }
|
.loading-spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(148,163,184,0.25); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
||||||
|
.loading { width: 16px; height: 16px; border-radius: 50%; border: 2px solid rgba(148,163,184,0.25); border-top-color: var(--accent); animation: spin 0.9s linear infinite; }
|
||||||
|
#ts { color: var(--muted); font-size: 0.85em; margin-left: 10px; }
|
||||||
|
#global-loading { margin-left: 8px; vertical-align: middle; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Homelab Status Monitor</h1>
|
<header>
|
||||||
<div class="version">v''' + VERSION + '''</div>
|
<h1>Homelab Status Monitor</h1>
|
||||||
<div class="controls">
|
<div class="version">v''' + VERSION + '''</div>
|
||||||
|
</header>
|
||||||
|
<div class="topbar">
|
||||||
<button class="refresh-all" onclick="refreshAll()">Refresh All</button>
|
<button class="refresh-all" onclick="refreshAll()">Refresh All</button>
|
||||||
<button onclick="location.reload()">Reload Page</button>
|
<button onclick="location.reload()">Reload Page</button>
|
||||||
|
<a href="/apps">Applications List</a>
|
||||||
<span id="ts"></span>
|
<span id="ts"></span>
|
||||||
<span id="global-loading"></span>
|
<span id="global-loading"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filters" id="filters">
|
||||||
|
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All</button>
|
||||||
|
<button class="filter-btn" data-filter="ssh" onclick="setFilter('ssh')">SSH</button>
|
||||||
|
<button class="filter-btn" data-filter="http" onclick="setFilter('http')">HTTP</button>
|
||||||
|
<button class="filter-btn" data-filter="https" onclick="setFilter('https')">HTTPS</button>
|
||||||
|
<button class="filter-btn" data-filter="reverse" onclick="setFilter('reverse')">Reverse Proxy</button>
|
||||||
|
<button class="filter-btn" data-filter="others" onclick="setFilter('others')">Others</button>
|
||||||
|
</div>
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<div class="card-sum"><div class="count" id="total">0</div><div class="label">Total</div></div>
|
<div class="card-sum"><div class="count" id="total">0</div><div class="label">Total</div></div>
|
||||||
<div class="card-sum"><div class="count" id="ok">0</div><div class="label">Online</div></div>
|
<div class="card-sum"><div class="count" id="ok">0</div><div class="label">Online</div></div>
|
||||||
@@ -228,18 +373,27 @@ def generate_html():
|
|||||||
<div class="grid" id="grid"></div>
|
<div class="grid" id="grid"></div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
const SERVICE_DEFS = ''' + services_json + ''';
|
||||||
let data = {};
|
let data = {};
|
||||||
let history = {};
|
let history = {};
|
||||||
let refreshInProgress = false;
|
let refreshInProgress = false;
|
||||||
|
let activeFilter = 'all';
|
||||||
|
|
||||||
|
function initWaiting() {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
for (const [id, cfg] of Object.entries(SERVICE_DEFS)) {
|
||||||
|
data[id] = {config: cfg, result: {status: "waiting"}, time: now};
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
function getClass(s) {
|
function getClass(s) {
|
||||||
var classes = {"online": "online", "offline": "offline", "timeout": "timeout", "error": "error", "ssl_error": "ssl_error"};
|
return {"online": "online", "offline": "offline", "timeout": "timeout", "error": "error", "ssl_error": "ssl_error", "waiting": "waiting"}[s] || "offline";
|
||||||
return classes[s] || "offline";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtHist(sid) {
|
function fmtHist(sid) {
|
||||||
if (history[sid]) {
|
if (history[sid]) {
|
||||||
var h = history[sid];
|
const h = history[sid];
|
||||||
return "Changed: " + h.old_status + " -> " + h.new_status + " at " + new Date(h.changed_at).toLocaleTimeString();
|
return "Changed: " + h.old_status + " -> " + h.new_status + " at " + new Date(h.changed_at).toLocaleTimeString();
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -247,8 +401,27 @@ def generate_html():
|
|||||||
|
|
||||||
function setGlobalLoad(b) {
|
function setGlobalLoad(b) {
|
||||||
refreshInProgress = b;
|
refreshInProgress = b;
|
||||||
var elem = document.getElementById("global-loading");
|
document.getElementById("global-loading").innerHTML = b ? '<span class="loading-spinner"></span>' : '';
|
||||||
elem.innerHTML = b ? '<span class="loading-spinner"></span>' : '';
|
}
|
||||||
|
|
||||||
|
function setFilter(type) {
|
||||||
|
activeFilter = type;
|
||||||
|
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.filter === type));
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLink(c) {
|
||||||
|
if (c.public_url) return "<a href='" + c.public_url + "' target='_blank' rel='noopener noreferrer'>" + c.public_url + "</a>";
|
||||||
|
if (c.type === 'http') return "<a href='http://" + c.host + ":" + c.port + c.url + "' target='_blank' rel='noopener noreferrer'>" + c.host + ":" + c.port + c.url + "</a>";
|
||||||
|
if (c.type === 'https') return "<a href='https://" + c.host + ":" + c.port + c.url + "' target='_blank' rel='noopener noreferrer'>" + c.host + ":" + c.port + c.url + "</a>";
|
||||||
|
return c.host + ":" + c.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilter(c) {
|
||||||
|
if (activeFilter === 'all') return true;
|
||||||
|
if (activeFilter === 'others') return c.type !== 'ssh' && c.type !== 'http' && c.type !== 'https';
|
||||||
|
if (activeFilter === 'reverse') return c.label === 'reverse proxy';
|
||||||
|
return c.type === activeFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
@@ -256,28 +429,32 @@ def generate_html():
|
|||||||
let t = 0, ok = 0, ko = 0, err = 0;
|
let t = 0, ok = 0, ko = 0, err = 0;
|
||||||
for (const [id, v] of Object.entries(data)) {
|
for (const [id, v] of Object.entries(data)) {
|
||||||
const c = v.config, r = v.result, s = r.status;
|
const c = v.config, r = v.result, s = r.status;
|
||||||
|
if (!matchesFilter(c)) continue;
|
||||||
t++;
|
t++;
|
||||||
if (s === "online") ok++;
|
if (s === "online") ok++;
|
||||||
else if (s === "offline" || s === "error" || s === "timeout" || s === "ssl_error") ko++;
|
else if (s === "offline" || s === "error" || s === "timeout" || s === "ssl_error") ko++;
|
||||||
else err++;
|
else if (s !== "waiting") err++;
|
||||||
|
|
||||||
const bc = getClass(s);
|
const bc = getClass(s);
|
||||||
const pi = c.url ? c.host + ":" + c.port + c.url : c.host + ":" + c.port;
|
const label = c.label ? "<span class='tag label'>" + c.label + "</span>" : "";
|
||||||
|
const link = s === 'waiting' ? "<span class='loading'></span> Checking..." : buildLink(c);
|
||||||
|
|
||||||
h += "<div class='card ' + bc + '>";
|
h += "<div class='card " + bc + "'>";
|
||||||
h += "<button class='refresh-btn' onclick='refreshService(\'" + id + "\')' title='Refresh'>🔄</button>";
|
h += "<div class='card-header'>";
|
||||||
h += "<div class='header'><div class='name'>" + c.name + "</div><span class='type'>" + c.type + "</span></div>";
|
h += "<div class='name'>" + c.name + "</div>";
|
||||||
|
h += "<div class='badges-row'>" + label + "<span class='tag type'>" + c.type + "</span><button class='refresh-btn' onclick='refreshService("" + id + "")' title='Refresh'>↻</button></div>";
|
||||||
|
h += "</div>";
|
||||||
h += "<span class='badge " + bc + "'>" + s.replace(/_/g, " ") + "</span>";
|
h += "<span class='badge " + bc + "'>" + s.replace(/_/g, " ") + "</span>";
|
||||||
h += "<div class='details'>" + pi + "</div>";
|
h += "<div class='details'>" + link + "</div>";
|
||||||
if (r.error) h += "<div class='error'>" + r.error + "</div>";
|
if (r.error && s !== 'waiting') h += "<div class='error'>" + r.error + "</div>";
|
||||||
if (r.status_code) h += "<div class='details'>Status: " + r.status_code + "</div>";
|
if (r.status_code) h += "<div class='details'>Status: " + r.status_code + "</div>";
|
||||||
h += "<div class='details'>Checked: " + new Date(v.time).toLocaleTimeString() + "</div>";
|
if (s !== 'waiting') h += "<div class='details'>Checked: " + new Date(v.time).toLocaleTimeString() + "</div>";
|
||||||
const hist = fmtHist(id);
|
const hist = fmtHist(id);
|
||||||
if (hist) h += "<div class='history-note'>" + hist + "</div>";
|
if (hist) h += "<div class='history-note'>" + hist + "</div>";
|
||||||
h += "</div>";
|
h += "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("grid").innerHTML = h || "<p style='color:#64748b;text-align:center;'>No services to display</p>";
|
document.getElementById("grid").innerHTML = h || "<p style='color:#64748b;text-align:center;'>No services to display for this filter</p>";
|
||||||
document.getElementById("total").textContent = t;
|
document.getElementById("total").textContent = t;
|
||||||
document.getElementById("ok").textContent = ok;
|
document.getElementById("ok").textContent = ok;
|
||||||
document.getElementById("ko").textContent = ko;
|
document.getElementById("ko").textContent = ko;
|
||||||
@@ -286,21 +463,15 @@ def generate_html():
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refreshService(sid) {
|
function refreshService(sid) {
|
||||||
const btn = event.target;
|
const btn = event.currentTarget;
|
||||||
|
const original = btn.innerHTML;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<span class="loading-spinner"></span>';
|
btn.innerHTML = '<span class="loading-spinner"></span>';
|
||||||
fetch("/api/check/" + encodeURIComponent(sid))
|
fetch("/api/check/" + encodeURIComponent(sid))
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => {
|
.then(d => { data[sid] = d; if (d.history) history[sid] = d.history; render(); })
|
||||||
data[sid] = d;
|
|
||||||
if (d.history) history[sid] = d.history;
|
|
||||||
render();
|
|
||||||
})
|
|
||||||
.catch(err => console.error("Error:", err))
|
.catch(err => console.error("Error:", err))
|
||||||
.finally(() => {
|
.finally(() => { btn.disabled = false; btn.innerHTML = original; });
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = "🔄";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshAll() {
|
function refreshAll() {
|
||||||
@@ -310,46 +481,171 @@ def generate_html():
|
|||||||
btn.innerHTML = '<span class="loading-spinner"></span> Refreshing...';
|
btn.innerHTML = '<span class="loading-spinner"></span> Refreshing...';
|
||||||
fetch("/api/refresh")
|
fetch("/api/refresh")
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => {
|
.then(d => { data = d.data; history = d.history || {}; render(); })
|
||||||
data = d.data;
|
|
||||||
history = d.history || {};
|
|
||||||
render();
|
|
||||||
})
|
|
||||||
.catch(err => console.error("Error:", err))
|
.catch(err => console.error("Error:", err))
|
||||||
.finally(() => {
|
.finally(() => { setGlobalLoad(false); btn.disabled = false; btn.innerHTML = "Refresh All"; });
|
||||||
setGlobalLoad(false);
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = "Refresh All";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial load
|
function fetchStatus() {
|
||||||
fetch("/api/status")
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(d => {
|
|
||||||
data = d.data;
|
|
||||||
history = d.history || {};
|
|
||||||
render();
|
|
||||||
})
|
|
||||||
.catch(err => console.error("Error:", err));
|
|
||||||
|
|
||||||
// Auto-refresh every 30 seconds
|
|
||||||
setInterval(function() {
|
|
||||||
setGlobalLoad(true);
|
setGlobalLoad(true);
|
||||||
fetch("/api/status")
|
fetch("/api/status")
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(function(d) {
|
.then(d => { data = d.data; history = d.history || {}; render(); })
|
||||||
data = d.data;
|
.catch(err => console.error("Error:", err))
|
||||||
history = d.history || {};
|
.finally(() => setGlobalLoad(false));
|
||||||
render();
|
}
|
||||||
})
|
|
||||||
.catch(function(err) {
|
initWaiting();
|
||||||
console.error("Error:", err);
|
fetchStatus();
|
||||||
})
|
setInterval(fetchStatus, 30000);
|
||||||
.finally(function() {
|
</script>
|
||||||
setGlobalLoad(false);
|
</body>
|
||||||
|
</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 '''<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Applications - Homelab Status Monitor v''' + VERSION + '''</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0b0f19;
|
||||||
|
--surface: rgba(30, 41, 59, 0.65);
|
||||||
|
--text: #e2e8f0;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
--accent: #60a5fa;
|
||||||
|
--accent-2: #818cf8;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||||
|
background: radial-gradient(circle at top left, #172033 0%, var(--bg) 40%), var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
margin: 0; padding: 24px; min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container { max-width: 900px; margin: 0 auto; }
|
||||||
|
header { text-align: center; margin-bottom: 24px; }
|
||||||
|
h1 {
|
||||||
|
font-size: 2.2em; font-weight: 800; margin: 0;
|
||||||
|
background: linear-gradient(90deg, var(--accent), var(--accent-2));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
.back { display: inline-block; margin-bottom: 20px; color: var(--accent); text-decoration: none; }
|
||||||
|
.back:hover { text-decoration: underline; }
|
||||||
|
.hint { color: var(--muted); font-size: 0.9em; margin-bottom: 18px; text-align: center; }
|
||||||
|
.app-list { list-style: none; padding: 0; margin: 0; }
|
||||||
|
.app-item {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
cursor: grab;
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
|
||||||
|
transition: transform 0.15s, box-shadow 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.app-item:active { cursor: grabbing; }
|
||||||
|
.app-item.dragging { opacity: 0.5; transform: scale(0.98); }
|
||||||
|
.app-item:hover { box-shadow: 0 12px 40px rgba(0,0,0,0.25); transform: translateY(-2px); }
|
||||||
|
.drag-handle {
|
||||||
|
width: 24px; height: 24px;
|
||||||
|
display: flex; flex-direction: column; justify-content: center; gap: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.drag-handle span { display: block; height: 2px; background: currentColor; border-radius: 2px; }
|
||||||
|
.app-name { font-weight: 700; font-size: 1.05em; flex: 1; }
|
||||||
|
.app-url { color: var(--accent); text-decoration: none; word-break: break-all; }
|
||||||
|
.app-url:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<a href="/" class="back">← Back to dashboard</a>
|
||||||
|
<h1>Applications</h1>
|
||||||
|
</header>
|
||||||
|
<div class="hint">Drag items to reorder. The order is saved in your browser.</div>
|
||||||
|
<ul class="app-list" id="app-list"></ul>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const APPS = ''' + apps_json + ''';
|
||||||
|
const STORAGE_KEY = "statusmonitor_apps_order";
|
||||||
|
|
||||||
|
function getOrderedApps() {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!saved) return APPS.slice();
|
||||||
|
let order;
|
||||||
|
try { order = JSON.parse(saved); } catch (e) { return APPS.slice(); }
|
||||||
|
const map = new Map(APPS.map(a => [a.id, a]));
|
||||||
|
const ordered = order.map(id => map.get(id)).filter(a => a);
|
||||||
|
const remaining = APPS.filter(a => !order.includes(a.id));
|
||||||
|
return ordered.concat(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveOrder(order) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(order));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
const list = document.getElementById("app-list");
|
||||||
|
const apps = getOrderedApps();
|
||||||
|
list.innerHTML = apps.map(a =>
|
||||||
|
"<li class='app-item' draggable='true' data-id='" + a.id + "'>" +
|
||||||
|
"<div class='drag-handle'><span></span><span></span><span></span></div>" +
|
||||||
|
"<div class='app-name'>" + a.name + "</div>" +
|
||||||
|
"<a class='app-url' href='" + a.url + "' target='_blank' rel='noopener noreferrer'>" + a.url + "</a>" +
|
||||||
|
"</li>"
|
||||||
|
).join("");
|
||||||
|
initDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initDrag() {
|
||||||
|
const list = document.getElementById("app-list");
|
||||||
|
let dragged = null;
|
||||||
|
list.querySelectorAll(".app-item").forEach(item => {
|
||||||
|
item.addEventListener("dragstart", function(e) {
|
||||||
|
dragged = item;
|
||||||
|
item.classList.add("dragging");
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
});
|
});
|
||||||
}, 30000);
|
item.addEventListener("dragend", function() {
|
||||||
|
item.classList.remove("dragging");
|
||||||
|
dragged = null;
|
||||||
|
const order = Array.from(list.children).map(li => li.dataset.id);
|
||||||
|
saveOrder(order);
|
||||||
|
});
|
||||||
|
item.addEventListener("dragover", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!dragged || dragged === item) return;
|
||||||
|
const rect = item.getBoundingClientRect();
|
||||||
|
const offset = e.clientY - rect.top - rect.height / 2;
|
||||||
|
if (offset < 0) list.insertBefore(dragged, item);
|
||||||
|
else list.insertBefore(dragged, item.nextSibling);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>'''
|
</html>'''
|
||||||
@@ -370,7 +666,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header('Content-type', 'text/html; charset=utf-8')
|
self.send_header('Content-type', 'text/html; charset=utf-8')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(generate_html().encode('utf-8'))
|
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':
|
elif self.path == '/api/status':
|
||||||
check_all() # Always refresh cache
|
check_all() # Always refresh cache
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
|
|||||||
+16
-11
@@ -1,22 +1,27 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Script de déploiement DevOps pour status-monitor
|
# 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
|
# Utilisation: wsl bash ~/homelab-config/status-monitor/deploy.sh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
WSL_REPO="$HOME/homelab-config"
|
||||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
IDE_REPO="/mnt/c/Users/theo/homelab_ia/homelab-config"
|
||||||
|
|
||||||
echo "[DEPLOY] Début du déploiement depuis Git..."
|
# 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] Transfert des fichiers vers CT201..."
|
# Etape 2: Deploiement WSL -> CT201 via CT200/CT100 (ProxyJump)
|
||||||
scp -J root@192.168.0.100 -i ~/.ssh/id_ed25519 "$SCRIPT_DIR/app.py" root@192.168.0.201:/tmp/app.py
|
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
|
||||||
|
|
||||||
|
# Etape 3: Installation et redemarrage sur CT201
|
||||||
echo "[DEPLOY] Installation sur CT201..."
|
echo "[DEPLOY] Installation sur CT201..."
|
||||||
ssh -J root@192.168.0.100 -i ~/.ssh/id_ed25519 root@192.168.0.201 << 'ENDSSH'
|
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'"
|
||||||
mv /tmp/app.py /opt/status-monitor/app.py
|
|
||||||
systemctl restart status-monitor
|
|
||||||
echo "[DEPLOY] ✅ Déploiement terminé sur CT201 !"
|
|
||||||
ENDSSH
|
|
||||||
|
|
||||||
echo "[DEPLOY] Déploiement terminé avec succès !"
|
echo "[DEPLOY] OK - All done: IDE -> WSL -> CT200/CT100 -> CT201"
|
||||||
|
|||||||
Reference in New Issue
Block a user