Remove HTML-to-Markdown parser and replace with comprehensive professional documentation generator featuring detailed guides for health indicators, architecture, features, notifications, playbooks, API reference, troubleshooting, and best practices

This commit is contained in:
2025-12-14 17:55:35 -05:00
parent 0030fcc101
commit 8db38ca75e
9 changed files with 331 additions and 2795 deletions
+1 -2
View File
@@ -4,13 +4,12 @@ Utilitaires pour l'API Homelab Automation.
from app.utils.ssh_utils import find_ssh_private_key, run_ssh_command, bootstrap_host
from app.utils.pdf_generator import markdown_to_pdf_bytes
from app.utils.markdown_parser import HelpHtmlToMarkdownParser, build_help_markdown
from app.utils.markdown_parser import build_help_markdown
__all__ = [
"find_ssh_private_key",
"run_ssh_command",
"bootstrap_host",
"markdown_to_pdf_bytes",
"HelpHtmlToMarkdownParser",
"build_help_markdown",
]
+298 -253
View File
@@ -1,287 +1,332 @@
"""
Parser HTML vers Markdown pour la documentation d'aide.
Générateur de documentation Markdown pour l'aide Homelab Automation.
"""
import re
from html.parser import HTMLParser
from pathlib import Path
from typing import List, Tuple
class HelpHtmlToMarkdownParser(HTMLParser):
"""Parser pour convertir le HTML de documentation en Markdown."""
def __init__(self):
super().__init__()
self.markdown_lines: List[str] = []
self.current_text = ""
self.in_code = False
self.in_pre = False
self.in_list = False
self.list_type = "ul"
self.list_level = 0
self.in_table = False
self.table_row: List[str] = []
self.in_th = False
self.ignore_content = False
self.tag_stack: List[str] = []
def handle_starttag(self, tag: str, attrs: List[Tuple[str, str]]):
self.tag_stack.append(tag)
attrs_dict = dict(attrs)
if tag in ["script", "style", "nav", "footer"]:
self.ignore_content = True
return
if tag == "h1":
self._flush_text()
self.markdown_lines.append("\n# ")
elif tag == "h2":
self._flush_text()
self.markdown_lines.append("\n## ")
elif tag == "h3":
self._flush_text()
self.markdown_lines.append("\n### ")
elif tag == "h4":
self._flush_text()
self.markdown_lines.append("\n#### ")
elif tag == "p":
self._flush_text()
self.markdown_lines.append("\n")
elif tag == "br":
self.markdown_lines.append("\n")
elif tag == "strong" or tag == "b":
self.current_text += "**"
elif tag == "em" or tag == "i":
self.current_text += "*"
elif tag == "code":
if not self.in_pre:
self.current_text += "`"
self.in_code = True
elif tag == "pre":
self._flush_text()
self.in_pre = True
self.markdown_lines.append("\n```\n")
elif tag == "ul":
self._flush_text()
self.in_list = True
self.list_type = "ul"
self.list_level += 1
elif tag == "ol":
self._flush_text()
self.in_list = True
self.list_type = "ol"
self.list_level += 1
elif tag == "li":
self._flush_text()
indent = " " * (self.list_level - 1)
if self.list_type == "ul":
self.markdown_lines.append(f"\n{indent}- ")
else:
self.markdown_lines.append(f"\n{indent}1. ")
elif tag == "a":
href = attrs_dict.get("href", "")
self.current_text += "["
self._href_pending = href
elif tag == "table":
self._flush_text()
self.in_table = True
self.markdown_lines.append("\n")
elif tag == "tr":
self.table_row = []
elif tag == "th":
self.in_th = True
elif tag == "td":
pass
elif tag == "hr":
self._flush_text()
self.markdown_lines.append("\n---\n")
elif tag == "blockquote":
self._flush_text()
self.markdown_lines.append("\n> ")
def handle_endtag(self, tag: str):
if self.tag_stack and self.tag_stack[-1] == tag:
self.tag_stack.pop()
if tag in ["script", "style", "nav", "footer"]:
self.ignore_content = False
return
if tag in ["h1", "h2", "h3", "h4"]:
self._flush_text()
self.markdown_lines.append("\n")
elif tag == "p":
self._flush_text()
self.markdown_lines.append("\n")
elif tag == "strong" or tag == "b":
self.current_text += "**"
elif tag == "em" or tag == "i":
self.current_text += "*"
elif tag == "code":
if not self.in_pre:
self.current_text += "`"
self.in_code = False
elif tag == "pre":
self._flush_text()
self.in_pre = False
self.markdown_lines.append("```\n")
elif tag == "ul" or tag == "ol":
self._flush_text()
self.list_level -= 1
if self.list_level == 0:
self.in_list = False
self.markdown_lines.append("\n")
elif tag == "li":
self._flush_text()
elif tag == "a":
href = getattr(self, "_href_pending", "")
self.current_text += f"]({href})"
self._href_pending = ""
elif tag == "tr":
if self.table_row:
self.markdown_lines.append("| " + " | ".join(self.table_row) + " |\n")
if self.in_th:
# Ajouter la ligne de séparation après les en-têtes
self.markdown_lines.append("|" + "|".join(["---"] * len(self.table_row)) + "|\n")
self.in_th = False
elif tag == "th" or tag == "td":
self._flush_text()
self.table_row.append(self.current_text.strip())
self.current_text = ""
elif tag == "table":
self.in_table = False
self.markdown_lines.append("\n")
elif tag == "blockquote":
self._flush_text()
self.markdown_lines.append("\n")
def handle_data(self, data: str):
if self.ignore_content:
return
if self.in_pre:
self.markdown_lines.append(data)
else:
# Normaliser les espaces
text = " ".join(data.split())
if text:
self.current_text += text
def _flush_text(self):
if self.current_text.strip():
self.markdown_lines.append(self.current_text.strip())
self.current_text = ""
def get_markdown(self) -> str:
self._flush_text()
content = "".join(self.markdown_lines)
# Nettoyer les lignes vides multiples
content = re.sub(r'\n{3,}', '\n\n', content)
return content.strip()
def build_help_markdown(html_path: Path = None, html_content: str = None) -> str:
"""Construit le contenu Markdown d'aide depuis un fichier HTML.
"""Génère le contenu Markdown d'aide professionnel.
Args:
html_path: Chemin vers le fichier HTML source
html_content: Contenu HTML direct (prioritaire sur html_path)
html_path: Non utilisé (conservé pour compatibilité)
html_content: Non utilisé (conservé pour compatibilité)
Returns:
Contenu Markdown formaté
Contenu Markdown formaté professionnellement
"""
if html_content:
content = html_content
elif html_path and html_path.exists():
content = html_path.read_text(encoding='utf-8')
else:
return _get_default_help_markdown()
# Extraire uniquement la section d'aide si présente
help_section_match = re.search(
r'<section[^>]*id=["\']help["\'][^>]*>(.*?)</section>',
content,
re.DOTALL | re.IGNORECASE
)
if help_section_match:
help_html = help_section_match.group(1)
else:
# Utiliser tout le body si pas de section help
body_match = re.search(r'<body[^>]*>(.*?)</body>', content, re.DOTALL | re.IGNORECASE)
help_html = body_match.group(1) if body_match else content
# Parser le HTML
parser = HelpHtmlToMarkdownParser()
parser.feed(help_html)
return parser.get_markdown()
return _get_professional_help_markdown()
def _get_default_help_markdown() -> str:
"""Retourne le contenu d'aide par défaut."""
return """# 📚 Documentation Homelab Automation
def _get_professional_help_markdown() -> str:
"""Retourne la documentation d'aide complète et professionnelle."""
return """# Homelab Automation Dashboard
## 🎯 Introduction
## Guide d'Utilisation
Bienvenue dans le **Homelab Automation Dashboard**, une plateforme complète pour gérer votre infrastructure homelab avec Ansible.
Bienvenue dans le guide officiel de votre **Homelab Automation Dashboard** !
Découvrez comment gérer et automatiser efficacement votre infrastructure grâce à cette solution puissante et centralisée.
## 🖥️ Dashboard
---
Le dashboard principal affiche:
- **Hôtes en ligne**: Nombre d'hôtes actuellement accessibles
- **Tâches**: Nombre total de tâches exécutées
- **Taux de succès**: Pourcentage de tâches réussies
- **Uptime**: Disponibilité globale du système
## Table des Matières
## 🖧 Gestion des Hôtes
1. [Démarrage Rapide](#démarrage-rapide)
2. [Indicateurs de Santé des Hosts](#indicateurs-de-santé-des-hosts)
3. [Architecture de la Solution](#architecture-de-la-solution)
4. [Fonctionnalités par Section](#fonctionnalités-par-section)
5. [Système de Notifications](#système-de-notifications-ntfy)
6. [Playbooks Ansible Disponibles](#playbooks-ansible-disponibles)
7. [Référence API](#référence-api)
8. [Dépannage](#dépannage)
9. [Raccourcis et Astuces](#raccourcis-et-astuces)
### Ajouter un hôte
1. Cliquez sur "Nouvel hôte"
2. Renseignez le nom et l'adresse IP
3. Sélectionnez le groupe d'environnement
4. Choisissez les groupes de rôles
5. Cliquez sur "Ajouter"
---
### Bootstrap
Le bootstrap configure un hôte pour Ansible:
- Création de l'utilisateur d'automatisation
- Configuration des clés SSH
- Installation de sudo et Python3
## Démarrage Rapide
## 📋 Playbooks
### Étape 1 : Ajouter vos Hosts
Les playbooks Ansible sont organisés par catégorie:
- **Général**: Playbooks de base
- **Maintenance**: Mises à jour, redémarrages
- **Sécurité**: Configurations de sécurité
- **Monitoring**: Surveillance et alertes
Commencez par ajouter vos serveurs dans la section **Hosts**. Chaque host nécessite :
- Un nom unique
- Une adresse IP
- Un système d'exploitation
## ⏰ Schedules
### Étape 2 : Bootstrap Ansible
Planifiez l'exécution automatique de playbooks:
- **Une fois**: Exécution unique à une date/heure
- **Quotidien**: Tous les jours à une heure fixe
- **Hebdomadaire**: Certains jours de la semaine
- **Mensuel**: Un jour spécifique du mois
- **Personnalisé**: Expression cron
Exécutez le **Bootstrap** sur chaque host pour configurer :
- L'accès SSH avec clé publique
- L'utilisateur d'automatisation
- Les prérequis Ansible (Python, sudo)
## 🔔 Notifications
### Étape 3 : Automatiser
Les notifications sont envoyées via ntfy:
- Démarrage/arrêt de l'application
- Succès/échec des tâches
- Bootstrap réussi/échoué
Utilisez les **Actions Rapides** ou exécutez des playbooks personnalisés pour automatiser vos tâches récurrentes.
## 🔑 API
---
## Indicateurs de Santé des Hosts
Chaque host affiche un indicateur visuel de santé représenté par des barres colorées. Cet indicateur combine plusieurs facteurs pour évaluer l'état global de votre serveur.
### Comprendre l'Indicateur
| Niveau | Barres | Description |
|--------|--------|-------------|
| **Excellent** | 5 barres vertes | Host en ligne, bootstrap OK, vérifié récemment |
| **Bon** | 3-4 barres jaunes | Host fonctionnel mais certains aspects à améliorer |
| **Moyen** | 2 barres oranges | Attention requise - vérification recommandée |
| **Faible** | 1 barre rouge | Host hors ligne ou non configuré |
### Facteurs de Calcul du Score
| Facteur | Points | Description |
|---------|--------|-------------|
| Statut en ligne | +2 | Le host répond aux requêtes réseau |
| Bootstrap Ansible OK | +1 | SSH et prérequis Ansible configurés |
| Vérifié récemment (<1h) | +2 | Dernière vérification il y a moins d'une heure |
| Vérifié aujourd'hui | +1 | Dernière vérification dans les 24 dernières heures |
> **Astuce :** Exécutez régulièrement un `Health Check` pour maintenir un score de santé élevé.
### Statuts Bootstrap Ansible
- **Ansible Ready** : Le host est entièrement configuré pour Ansible. L'utilisateur automation existe, la clé SSH est déployée et sudo est configuré sans mot de passe.
- **Non configuré** : Le bootstrap n'a pas encore été exécuté sur ce host. Cliquez sur le bouton **Bootstrap** pour configurer l'accès Ansible.
#### Que signifie "Jamais vérifié" ?
Ce message apparaît lorsqu'aucun Health Check n'a été exécuté sur le host depuis son ajout. Le système ne peut pas déterminer l'état réel du serveur. Lancez un Health Check pour mettre à jour le statut et obtenir un score de santé précis.
---
## Architecture de la Solution
### Stack Technologique
| Composant | Technologie | Description |
|-----------|-------------|-------------|
| **Backend** | FastAPI (Python) | API REST haute performance |
| **Automation** | Ansible | Gestion de configuration |
| **Frontend** | HTML/CSS/JS + TailwindCSS | Interface web moderne |
| **Déploiement** | Docker & Docker Compose | Conteneurisation |
| **Temps réel** | WebSocket | Mises à jour live |
### Structure des Fichiers
L'API REST est accessible avec une clé API:
```
Header: X-API-Key: votre-cle-api
homelab-automation/
├── app/
│ ├── routes/ # Routes API FastAPI
│ ├── models/ # Modèles SQLAlchemy
│ ├── schemas/ # Schémas Pydantic
│ ├── services/ # Logique métier
│ ├── index.html # Interface web
│ └── main.js # Logique frontend
├── ansible/
│ ├── inventory/
│ │ ├── hosts.yml # Inventaire des hosts
│ │ └── group_vars/ # Variables par groupe
│ └── playbooks/ # Playbooks Ansible
├── alembic/ # Migrations de base de données
├── data/ # Base de données SQLite
├── tasks_logs/ # Logs des tâches
├── docker-compose.yml
└── Dockerfile
```
---
*Généré par Homelab Automation Dashboard*
## Fonctionnalités par Section
### Dashboard
Vue d'ensemble centralisée de votre infrastructure homelab.
- **Métriques en temps réel** : État des hosts (Online/Offline), statistiques des tâches (Succès/Échec)
- **Actions Rapides** : Accès immédiat aux opérations courantes (Mise à jour globale, Health Check général)
- **Aperçu des Hosts** : Liste condensée avec indicateurs de statut et OS pour une surveillance rapide
- **Notifications** : Alertes visuelles sur les dernières activités importantes
### Hosts
Gestion complète du cycle de vie de vos serveurs.
- **Inventaire** : Ajout, modification et suppression de hosts avec détection d'OS
- **Bootstrap Ansible** : Préparation automatique des serveurs (User, Clés SSH, Sudo)
- **Indicateurs de Santé** : Score de santé détaillé basé sur la connectivité et la configuration
- **Actions Individuelles** : Exécution de playbooks spécifiques (Upgrade, Backup, Reboot) sur un host
- **Détails Avancés** : Vue détaillée avec historique des tâches et logs spécifiques au host
### Playbooks
Bibliothèque et exécution de playbooks Ansible personnalisés.
- **Catalogue** : Liste de tous les playbooks disponibles dans votre répertoire
- **Exécution Ciblée** : Lancement de playbooks sur des hosts spécifiques ou des groupes
- **Logs en Direct** : Suivi temps réel de l'exécution Ansible (console output)
- **Historique** : Accès rapide aux résultats des exécutions précédentes
### Tasks
Traçabilité complète de toutes les opérations d'automatisation.
- **Suivi d'État** : Visualisation instantanée (En cours, Succès, Échec)
- **Filtrage Avancé** : Recherche par statut, par date ou par type d'action
- **Logs Détaillés** : Accès aux sorties standard et d'erreur pour le débogage
- **Auto-refresh** : Mise à jour automatique des tâches en cours d'exécution
### Schedules
Planification automatisée des tâches récurrentes.
- **Planification Cron** : Configuration flexible de la fréquence d'exécution
- **Tâches Récurrentes** : Backups quotidiens, Mises à jour hebdomadaires, Health Checks horaires
- **Ciblage** : Définition des hosts ou groupes cibles pour chaque planification
- **Gestion** : Activation, désactivation ou modification des planifications existantes
### Logs
Journal technique des événements système.
- **Streaming WebSocket** : Arrivée des logs en temps réel sans rechargement de page
- **Niveaux de Log** : Distinction claire entre Info, Warning et Error
- **Export** : Possibilité de télécharger les logs pour analyse externe
- **Rétention** : Gestion de l'historique et nettoyage des logs anciens
### Alertes
Centre de messages pour les événements importants.
- **Suivi** : Consultez les alertes récentes (succès/échec, changements d'état)
- **Lecture** : Les alertes peuvent être marquées comme lues pour garder une boîte de réception propre
- **Notifications** : Certaines alertes peuvent déclencher des notifications ntfy (si activé)
### Configuration
Paramètres de l'application et intégrations.
- **Paramètres applicatifs** : Options persistées (ex: collecte des métriques)
- **Notifications** : Configuration et test du service ntfy
- **Sécurité** : Gestion du compte utilisateur (mot de passe) via l'écran utilisateur
---
## Système de Notifications (ntfy)
Restez informé de l'état de votre infrastructure grâce au système de notifications intégré basé sur **ntfy**.
### Canaux et Configuration
Les notifications sont envoyées via le service ntfy, permettant de recevoir des alertes push sur mobile et desktop.
- **Push Mobile** : Via l'application ntfy (Android/iOS)
- **Web Push** : Notifications navigateur sur desktop
- **Priorité** : Gestion des niveaux d'urgence (Low à High)
### Types d'Alertes
Vous recevez des notifications pour les événements critiques :
- ✅ Succès des Backups
- ❌ Échecs de Tâches
- ⚠️ Changements de Santé Host
- 🛠️ Fin de Bootstrap
---
## Playbooks Ansible Disponibles
### bootstrap-host.yml
Configure un nouveau host pour Ansible : création utilisateur, clé SSH, sudo sans mot de passe.
> **Note :** Requis avant toute autre opération
### health-check.yml
Vérifie l'état de santé : CPU, RAM, disque, services critiques.
> **Note :** Exécution rapide, non destructif
### system-upgrade.yml
Met à jour tous les paquets système (apt/yum/dnf selon l'OS).
> **Attention :** Peut nécessiter un redémarrage
### backup-config.yml
Sauvegarde les fichiers de configuration importants (/etc, configs apps).
> **Note :** Stockage local ou distant
---
## Référence API
L'API REST est accessible sur le port configuré. Authentification via header `Authorization: Bearer <token>`.
| Endpoint | Méthode | Description |
|----------|---------|-------------|
| `/api/hosts` | GET | Liste tous les hosts |
| `/api/hosts` | POST | Ajoute un nouveau host |
| `/api/hosts/{id}` | PUT | Modifie un host existant |
| `/api/hosts/{id}` | DELETE | Supprime un host |
| `/api/tasks/logs` | GET | Récupère les logs de tâches |
| `/api/ansible/playbooks` | GET | Liste les playbooks disponibles |
| `/api/ansible/execute` | POST | Exécute un playbook |
| `/api/schedules` | GET | Liste les planifications |
| `/api/schedules` | POST | Crée une planification |
| `/api/metrics` | GET | Métriques du dashboard |
| `/api/auth/login` | POST | Authentification utilisateur |
| `/api/auth/me` | GET | Informations utilisateur courant |
---
## Dépannage
### Le bootstrap échoue avec "Permission denied"
**Cause :** Les identifiants SSH fournis sont incorrects ou l'utilisateur n'a pas les droits sudo.
**Solution :** Vérifiez le nom d'utilisateur et mot de passe. Assurez-vous que l'utilisateur peut exécuter `sudo` sur le host cible.
### Les hosts apparaissent "offline" alors qu'ils sont accessibles
**Cause :** Le health check n'a pas été exécuté ou la clé SSH n'est pas configurée.
**Solution :** Exécutez le bootstrap si ce n'est pas fait, puis lancez un Health Check.
### Les tâches restent bloquées "En cours"
**Cause :** Le processus Ansible peut être bloqué ou le host ne répond plus.
**Solution :** Vérifiez la connectivité réseau. Consultez les logs système pour plus de détails. Redémarrez le conteneur Docker si nécessaire.
### L'interface ne se met pas à jour en temps réel
**Cause :** La connexion WebSocket est interrompue.
**Solution :** Rafraîchissez la page. Vérifiez que le port WebSocket n'est pas bloqué par un firewall ou proxy.
---
## Raccourcis et Astuces
### Navigation
- Cliquez sur le logo pour revenir au Dashboard
- Utilisez les onglets du menu pour naviguer
- Le thème clair/sombre est persistant
### Productivité
- Filtrez les hosts par groupe pour des actions groupées
- Utilisez les filtres de date pour retrouver des tâches
- Exportez les logs avant de les effacer
---
*Documentation générée par Homelab Automation Dashboard v1.0*
"""
+32 -1
View File
@@ -197,6 +197,29 @@ def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> byt
bulletIndent=10,
)
quote_style = ParagraphStyle(
'CustomQuote',
parent=body_style,
leftIndent=20,
borderColor=colors.HexColor('#7c3aed'),
borderWidth=2,
borderPadding=8,
backColor=colors.HexColor('#f5f3ff'),
textColor=colors.HexColor('#5b21b6'),
fontSize=9,
spaceBefore=8,
spaceAfter=8,
)
h4_style = ParagraphStyle(
'CustomH4',
parent=styles['Heading4'],
fontSize=11,
spaceBefore=8,
spaceAfter=4,
textColor=colors.HexColor('#374151'),
)
# Parser le Markdown
elements = []
elements.append(Paragraph(title, title_style))
@@ -272,7 +295,7 @@ def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> byt
elements.append(Paragraph(display_text, h3_style))
elif line.startswith('#### '):
text = line[5:].strip()
elements.append(Paragraph(f"<b>{text}</b>", body_style))
elements.append(Paragraph(text, h4_style))
# Listes à puces
elif line.strip().startswith('- ') or line.strip().startswith('* '):
@@ -291,6 +314,14 @@ def markdown_to_pdf_bytes(markdown_content: str, title: str = "Document") -> byt
text = re.sub(r'`(.+?)`', r'<font face="Courier">\1</font>', text)
elements.append(Paragraph(f"{num}. {text}", bullet_style))
# Blockquotes (citations)
elif line.strip().startswith('> '):
text = line.strip()[2:]
# Convertir le markdown basique
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
text = re.sub(r'`(.+?)`', r'<font face="Courier">\1</font>', text)
elements.append(Paragraph(text, quote_style))
# Ligne horizontale
elif line.strip() in ['---', '***', '___']:
elements.append(Spacer(1, 10))