Remove HTML-to-Markdown parser and PDF generation functionality, clean up unused imports and whitespace
This commit is contained in:
@@ -4,12 +4,10 @@ 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 build_help_markdown
|
||||
|
||||
__all__ = [
|
||||
"find_ssh_private_key",
|
||||
"run_ssh_command",
|
||||
"bootstrap_host",
|
||||
"markdown_to_pdf_bytes",
|
||||
"build_help_markdown",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,520 @@
|
||||
"""
|
||||
Renderer de documentation Markdown vers HTML avec styles TailwindCSS.
|
||||
Convertit le fichier help.md en HTML pour la page d'aide du dashboard.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
|
||||
class HelpMarkdownRenderer:
|
||||
"""Convertit le Markdown personnalisé en HTML avec styles TailwindCSS."""
|
||||
|
||||
def __init__(self, markdown_content: str):
|
||||
self.content = markdown_content
|
||||
self.toc_items: List[Tuple[str, str, str]] = [] # (id, emoji, title)
|
||||
|
||||
def render(self) -> Tuple[str, str]:
|
||||
"""
|
||||
Rend le Markdown en HTML.
|
||||
|
||||
Returns:
|
||||
Tuple (html_content, toc_html)
|
||||
"""
|
||||
html = self.content
|
||||
|
||||
# Extraire et traiter les sections avec IDs pour la TOC
|
||||
html = self._process_sections(html)
|
||||
|
||||
# Traiter les blocs personnalisés
|
||||
html = self._process_quickstart_cards(html)
|
||||
html = self._process_health_levels(html)
|
||||
html = self._process_score_factors(html)
|
||||
html = self._process_status_badges(html)
|
||||
html = self._process_accordions(html)
|
||||
html = self._process_playbooks(html)
|
||||
html = self._process_troubleshoot(html)
|
||||
|
||||
# Traiter le Markdown standard
|
||||
html = self._process_blockquotes(html)
|
||||
html = self._process_tables(html)
|
||||
html = self._process_code_blocks(html)
|
||||
html = self._process_inline_code(html)
|
||||
html = self._process_lists(html)
|
||||
html = self._process_headings(html)
|
||||
html = self._process_paragraphs(html)
|
||||
html = self._process_bold_italic(html)
|
||||
html = self._process_horizontal_rules(html)
|
||||
|
||||
# Générer la TOC
|
||||
toc_html = self._generate_toc()
|
||||
|
||||
return html, toc_html
|
||||
|
||||
def _process_sections(self, html: str) -> str:
|
||||
"""Extrait les sections H2 avec IDs pour la TOC."""
|
||||
pattern = r'^## ([^\n{]+)\s*\{#([^}]+)\}'
|
||||
|
||||
def replace_section(match):
|
||||
title = match.group(1).strip()
|
||||
section_id = match.group(2).strip()
|
||||
|
||||
# Extraire l'emoji du titre
|
||||
emoji_match = re.match(r'^(\S+)\s+(.+)$', title)
|
||||
if emoji_match:
|
||||
emoji = emoji_match.group(1)
|
||||
text = emoji_match.group(2)
|
||||
else:
|
||||
emoji = ""
|
||||
text = title
|
||||
|
||||
self.toc_items.append((section_id, emoji, text))
|
||||
|
||||
return f'</div><div id="{section_id}" class="glass-card p-8 mb-8 fade-in help-section-anchor">\n<h2 class="help-section-title"><span class="text-2xl mr-2">{emoji}</span>{text}</h2>'
|
||||
|
||||
html = re.sub(pattern, replace_section, html, flags=re.MULTILINE)
|
||||
|
||||
# Nettoyer le premier </div> en trop
|
||||
html = html.replace('</div><div id="help-', '<div id="help-', 1)
|
||||
|
||||
# Ajouter la fermeture finale
|
||||
html += '\n</div>'
|
||||
|
||||
return html
|
||||
|
||||
def _process_quickstart_cards(self, html: str) -> str:
|
||||
"""Traite les cartes de démarrage rapide."""
|
||||
# Trouver le bloc quickstart-cards
|
||||
pattern = r'<!-- quickstart-cards -->(.*?)<!-- /quickstart-cards -->'
|
||||
|
||||
def process_cards(match):
|
||||
cards_content = match.group(1)
|
||||
|
||||
# Parser chaque carte
|
||||
card_pattern = r':::card\s+(\w+)\s+(fa-\d+)\s*\n###\s+([^\n]+)\n([^:]+):::'
|
||||
cards = re.findall(card_pattern, cards_content, re.DOTALL)
|
||||
|
||||
cards_html = '<div class="grid grid-cols-1 md:grid-cols-3 gap-6">'
|
||||
|
||||
for color, icon, title, description in cards:
|
||||
color_class = f"text-{color}-400"
|
||||
desc = self._process_inline_code(description.strip())
|
||||
cards_html += f'''
|
||||
<div class="help-card">
|
||||
<div class="text-3xl mb-4 {color_class}"><i class="fas {icon}"></i></div>
|
||||
<h3 class="font-semibold mb-2">{title}</h3>
|
||||
<p class="text-gray-400 text-sm">{desc}</p>
|
||||
</div>'''
|
||||
|
||||
cards_html += '</div>'
|
||||
return cards_html
|
||||
|
||||
return re.sub(pattern, process_cards, html, flags=re.DOTALL)
|
||||
|
||||
def _process_health_levels(self, html: str) -> str:
|
||||
"""Traite les indicateurs de niveau de santé."""
|
||||
pattern = r':::health-level\s+(\w+)\s+(\d+)\s+(\w+)\s*\n\*\*([^*]+)\*\*\s*\(([^)]+)\)\s*\n([^:]+):::'
|
||||
|
||||
def replace_health(match):
|
||||
level = match.group(1)
|
||||
bars = int(match.group(2))
|
||||
color = match.group(3)
|
||||
title = match.group(4)
|
||||
subtitle = match.group(5)
|
||||
description = match.group(6).strip()
|
||||
|
||||
bars_html = ''
|
||||
for i in range(5):
|
||||
if i < bars:
|
||||
bars_html += f'<div class="health-bar bg-{color}-500"></div>'
|
||||
else:
|
||||
bars_html += '<div class="health-bar bg-gray-600"></div>'
|
||||
|
||||
return f'''
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="health-indicator-demo">{bars_html}</div>
|
||||
<div>
|
||||
<span class="font-semibold text-{color}-400">{title}</span>
|
||||
<span class="text-gray-400 text-sm ml-2">({subtitle})</span>
|
||||
<p class="text-gray-500 text-xs mt-1">{description}</p>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
return re.sub(pattern, replace_health, html, flags=re.DOTALL)
|
||||
|
||||
def _process_score_factors(self, html: str) -> str:
|
||||
"""Traite les facteurs de score."""
|
||||
pattern = r':::score-factor\s+(fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n\*\*([^*]+)\*\*\s*\n([^:]+):::'
|
||||
|
||||
def replace_factor(match):
|
||||
icon = match.group(1)
|
||||
color = match.group(2)
|
||||
points = match.group(3).strip()
|
||||
title = match.group(4)
|
||||
description = match.group(5).strip()
|
||||
|
||||
return f'''
|
||||
<div class="p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="fas {icon} text-{color}-400 text-xs"></i>
|
||||
<span class="font-medium">{title}</span>
|
||||
<span class="text-{color}-400 text-sm ml-auto">{points}</span>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs">{description}</p>
|
||||
</div>'''
|
||||
|
||||
return re.sub(pattern, replace_factor, html, flags=re.DOTALL)
|
||||
|
||||
def _process_status_badges(self, html: str) -> str:
|
||||
"""Traite les badges de statut."""
|
||||
pattern = r':::status-badge\s+(\w+)\s+(fa-[\w-]+)\s+([^\n]+)\n([^:]+):::'
|
||||
|
||||
def replace_badge(match):
|
||||
color = match.group(1)
|
||||
icon = match.group(2)
|
||||
badge_text = match.group(3).strip()
|
||||
description = match.group(4).strip()
|
||||
description = self._process_inline_code(description)
|
||||
|
||||
return f'''
|
||||
<div class="p-4 bg-{color}-900/20 border border-{color}-600/30 rounded-lg">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="px-2 py-0.5 bg-{color}-600/30 text-{color}-400 text-xs rounded-full flex items-center">
|
||||
<i class="fas {icon} mr-1"></i>{badge_text}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">{description}</p>
|
||||
</div>'''
|
||||
|
||||
return re.sub(pattern, replace_badge, html, flags=re.DOTALL)
|
||||
|
||||
def _process_accordions(self, html: str) -> str:
|
||||
"""Traite les accordéons."""
|
||||
pattern = r':::accordion\s+(fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n(.*?):::'
|
||||
|
||||
def replace_accordion(match):
|
||||
icon = match.group(1)
|
||||
color = match.group(2)
|
||||
title = match.group(3).strip()
|
||||
content = match.group(4).strip()
|
||||
|
||||
# Traiter les listes dans le contenu
|
||||
content = self._process_lists(content)
|
||||
content = self._process_bold_italic(content)
|
||||
|
||||
# Séparer description et liste
|
||||
parts = content.split('\n\n', 1)
|
||||
description = parts[0] if parts else ''
|
||||
list_content = parts[1] if len(parts) > 1 else ''
|
||||
|
||||
return f'''
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas {icon} text-{color}-400"></i>
|
||||
<strong>{title}</strong>
|
||||
</span>
|
||||
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
<div class="p-4">
|
||||
<p class="text-gray-400 mb-4">{description}</p>
|
||||
{list_content}
|
||||
</div>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
return re.sub(pattern, replace_accordion, html, flags=re.DOTALL)
|
||||
|
||||
def _process_playbooks(self, html: str) -> str:
|
||||
"""Traite les cartes de playbooks."""
|
||||
pattern = r':::playbook\s+(fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n([^*]+)\*\*([^*]+)\*\*\s*:::'
|
||||
|
||||
def replace_playbook(match):
|
||||
icon = match.group(1)
|
||||
color = match.group(2)
|
||||
filename = match.group(3).strip()
|
||||
description = match.group(4).strip()
|
||||
note = match.group(5).strip()
|
||||
|
||||
# Déterminer la couleur de la note
|
||||
note_color = "purple"
|
||||
if "redémarrage" in note.lower() or "attention" in note.lower():
|
||||
note_color = "orange"
|
||||
|
||||
return f'''
|
||||
<div class="help-card">
|
||||
<h3 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fas {icon} text-{color}-400"></i>
|
||||
{filename}
|
||||
</h3>
|
||||
<p class="text-gray-400 text-sm mb-2">{description}</p>
|
||||
<span class="text-xs text-{note_color}-400">{note}</span>
|
||||
</div>'''
|
||||
|
||||
return re.sub(pattern, replace_playbook, html, flags=re.DOTALL)
|
||||
|
||||
def _process_troubleshoot(self, html: str) -> str:
|
||||
"""Traite les sections de dépannage."""
|
||||
pattern = r':::troubleshoot\s+([^\n]+)\n(.*?):::'
|
||||
|
||||
def replace_troubleshoot(match):
|
||||
title = match.group(1).strip()
|
||||
content = match.group(2).strip()
|
||||
|
||||
# Traiter le contenu
|
||||
content = self._process_bold_italic(content)
|
||||
content = self._process_inline_code(content)
|
||||
content = content.replace('\n\n', '</p><p class="text-gray-400 text-sm">')
|
||||
|
||||
return f'''
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span>{title}</span>
|
||||
<i class="fas fa-chevron-down accordion-icon text-gray-400"></i>
|
||||
</div>
|
||||
<div class="accordion-content">
|
||||
<div class="p-4 text-gray-400 text-sm">
|
||||
<p class="text-gray-400 text-sm">{content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>'''
|
||||
|
||||
return re.sub(pattern, replace_troubleshoot, html, flags=re.DOTALL)
|
||||
|
||||
def _process_blockquotes(self, html: str) -> str:
|
||||
"""Traite les citations (blockquotes)."""
|
||||
pattern = r'^>\s*\*\*([^:]+):\*\*\s*(.+)$'
|
||||
|
||||
def replace_quote(match):
|
||||
label = match.group(1)
|
||||
content = match.group(2).strip()
|
||||
content = self._process_inline_code(content)
|
||||
|
||||
return f'''
|
||||
<div class="mt-4 p-3 bg-purple-900/20 border border-purple-600/30 rounded-lg">
|
||||
<p class="text-sm text-purple-300">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<strong>{label}:</strong> {content}
|
||||
</p>
|
||||
</div>'''
|
||||
|
||||
return re.sub(pattern, replace_quote, html, flags=re.MULTILINE)
|
||||
|
||||
def _process_tables(self, html: str) -> str:
|
||||
"""Traite les tableaux Markdown."""
|
||||
# Trouver les tableaux
|
||||
table_pattern = r'(\|[^\n]+\|\n)+(\|[-:| ]+\|\n)(\|[^\n]+\|\n)+'
|
||||
|
||||
def replace_table(match):
|
||||
table_text = match.group(0)
|
||||
lines = [l.strip() for l in table_text.strip().split('\n') if l.strip()]
|
||||
|
||||
if len(lines) < 3:
|
||||
return table_text
|
||||
|
||||
# En-têtes
|
||||
headers = [h.strip() for h in lines[0].split('|') if h.strip()]
|
||||
# Ignorer la ligne de séparation (lines[1])
|
||||
# Données
|
||||
rows = []
|
||||
for line in lines[2:]:
|
||||
cells = [c.strip() for c in line.split('|') if c.strip()]
|
||||
rows.append(cells)
|
||||
|
||||
# Construire le HTML
|
||||
html_table = '<div class="overflow-x-auto"><table class="w-full text-sm">'
|
||||
html_table += '<thead><tr class="border-b border-gray-700">'
|
||||
for h in headers:
|
||||
html_table += f'<th class="text-left py-3 px-4 text-purple-400">{h}</th>'
|
||||
html_table += '</tr></thead>'
|
||||
|
||||
html_table += '<tbody class="text-gray-300">'
|
||||
for row in rows:
|
||||
html_table += '<tr class="border-b border-gray-800">'
|
||||
for cell in row:
|
||||
cell = self._process_inline_code(cell)
|
||||
html_table += f'<td class="py-3 px-4">{cell}</td>'
|
||||
html_table += '</tr>'
|
||||
html_table += '</tbody></table></div>'
|
||||
|
||||
return html_table
|
||||
|
||||
return re.sub(table_pattern, replace_table, html)
|
||||
|
||||
def _process_code_blocks(self, html: str) -> str:
|
||||
"""Traite les blocs de code."""
|
||||
pattern = r'```(\w*)\n(.*?)```'
|
||||
|
||||
def replace_code(match):
|
||||
lang = match.group(1) or ''
|
||||
code = match.group(2)
|
||||
return f'<div class="bg-black/40 p-4 rounded-lg font-mono text-sm"><pre class="text-gray-300">{code}</pre></div>'
|
||||
|
||||
return re.sub(pattern, replace_code, html, flags=re.DOTALL)
|
||||
|
||||
def _process_inline_code(self, html: str) -> str:
|
||||
"""Traite le code inline."""
|
||||
return re.sub(r'`([^`]+)`', r'<span class="help-code">\1</span>', html)
|
||||
|
||||
def _process_lists(self, html: str) -> str:
|
||||
"""Traite les listes à puces."""
|
||||
lines = html.split('\n')
|
||||
result = []
|
||||
in_list = False
|
||||
|
||||
for line in lines:
|
||||
if line.strip().startswith('- '):
|
||||
if not in_list:
|
||||
result.append('<ul class="help-list text-sm">')
|
||||
in_list = True
|
||||
item = line.strip()[2:]
|
||||
item = self._process_bold_italic(item)
|
||||
item = self._process_inline_code(item)
|
||||
result.append(f'<li>{item}</li>')
|
||||
else:
|
||||
if in_list:
|
||||
result.append('</ul>')
|
||||
in_list = False
|
||||
result.append(line)
|
||||
|
||||
if in_list:
|
||||
result.append('</ul>')
|
||||
|
||||
return '\n'.join(result)
|
||||
|
||||
def _process_headings(self, html: str) -> str:
|
||||
"""Traite les titres H3 et H4."""
|
||||
# H3
|
||||
html = re.sub(
|
||||
r'^### ([^\n{]+)$',
|
||||
r'<h3 class="font-semibold mb-4 text-purple-400">\1</h3>',
|
||||
html,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
# H4
|
||||
html = re.sub(
|
||||
r'^#### ([^\n]+)$',
|
||||
r'<h4 class="font-semibold mb-2 flex items-center gap-2"><i class="fas fa-question-circle text-gray-400"></i>\1</h4>',
|
||||
html,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
return html
|
||||
|
||||
def _process_paragraphs(self, html: str) -> str:
|
||||
"""Traite les paragraphes."""
|
||||
# Remplacer les lignes vides multiples par des paragraphes
|
||||
html = re.sub(r'\n\n+', '\n\n', html)
|
||||
|
||||
# Envelopper les paragraphes isolés
|
||||
lines = html.split('\n\n')
|
||||
result = []
|
||||
|
||||
for block in lines:
|
||||
block = block.strip()
|
||||
if not block:
|
||||
continue
|
||||
# Ne pas envelopper si c'est déjà du HTML
|
||||
if block.startswith('<') or block.startswith('|'):
|
||||
result.append(block)
|
||||
elif not any(block.startswith(x) for x in ['#', '-', '>', '```', ':::', '<!--']):
|
||||
result.append(f'<p class="text-gray-400 mb-6">{block}</p>')
|
||||
else:
|
||||
result.append(block)
|
||||
|
||||
return '\n\n'.join(result)
|
||||
|
||||
def _process_bold_italic(self, html: str) -> str:
|
||||
"""Traite le gras et l'italique."""
|
||||
# Gras
|
||||
html = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', html)
|
||||
# Italique
|
||||
html = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', html)
|
||||
return html
|
||||
|
||||
def _process_horizontal_rules(self, html: str) -> str:
|
||||
"""Traite les lignes horizontales."""
|
||||
return re.sub(r'^---+$', '', html, flags=re.MULTILINE)
|
||||
|
||||
def _generate_toc(self) -> str:
|
||||
"""Génère le HTML de la table des matières."""
|
||||
toc_html = ''
|
||||
for section_id, emoji, title in self.toc_items:
|
||||
toc_html += f'''
|
||||
<a href="#{section_id}" class="help-toc-item" onclick="scrollToHelpSection(event, '{section_id}')">
|
||||
<span class="mr-2">{emoji}</span>{title}
|
||||
</a>'''
|
||||
return toc_html
|
||||
|
||||
|
||||
def render_help_page(markdown_path: Path) -> Tuple[str, str]:
|
||||
"""
|
||||
Rend la page d'aide depuis le fichier Markdown.
|
||||
|
||||
Args:
|
||||
markdown_path: Chemin vers le fichier help.md
|
||||
|
||||
Returns:
|
||||
Tuple (html_content, toc_html)
|
||||
"""
|
||||
if not markdown_path.exists():
|
||||
return "<p>Documentation non disponible.</p>", ""
|
||||
|
||||
content = markdown_path.read_text(encoding='utf-8')
|
||||
renderer = HelpMarkdownRenderer(content)
|
||||
return renderer.render()
|
||||
|
||||
|
||||
def get_raw_markdown(markdown_path: Path) -> str:
|
||||
"""
|
||||
Retourne le contenu Markdown brut nettoyé pour le téléchargement.
|
||||
|
||||
Args:
|
||||
markdown_path: Chemin vers le fichier help.md
|
||||
|
||||
Returns:
|
||||
Contenu Markdown nettoyé (sans syntaxe personnalisée)
|
||||
"""
|
||||
if not markdown_path.exists():
|
||||
return "# Documentation non disponible"
|
||||
|
||||
content = markdown_path.read_text(encoding='utf-8')
|
||||
|
||||
# Nettoyer la syntaxe personnalisée pour un Markdown standard
|
||||
# Supprimer les IDs de section
|
||||
content = re.sub(r'\s*\{#[^}]+\}', '', content)
|
||||
|
||||
# Convertir les cartes en texte simple
|
||||
content = re.sub(r'<!-- quickstart-cards -->', '', content)
|
||||
content = re.sub(r'<!-- /quickstart-cards -->', '', content)
|
||||
content = re.sub(r':::card\s+\w+\s+fa-\d+\s*\n', '', content)
|
||||
|
||||
# Convertir les health-levels
|
||||
content = re.sub(r':::health-level\s+\w+\s+\d+\s+\w+\s*\n', '', content)
|
||||
|
||||
# Convertir les score-factors
|
||||
content = re.sub(r':::score-factor\s+[^\n]+\n', '', content)
|
||||
|
||||
# Convertir les status-badges
|
||||
content = re.sub(r':::status-badge\s+[^\n]+\n', '', content)
|
||||
|
||||
# Convertir les accordions
|
||||
content = re.sub(r':::accordion\s+[^\n]+\n', '### ', content)
|
||||
|
||||
# Convertir les playbooks
|
||||
content = re.sub(r':::playbook\s+[^\n]+\n', '### ', content)
|
||||
|
||||
# Convertir les troubleshoot
|
||||
content = re.sub(r':::troubleshoot\s+', '### ', content)
|
||||
|
||||
# Supprimer les fermetures :::
|
||||
content = re.sub(r'^:::$', '', content, flags=re.MULTILINE)
|
||||
|
||||
# Supprimer les attributs {icon:...}
|
||||
content = re.sub(r'\s*\{icon:[^}]+\}', '', content)
|
||||
|
||||
# Nettoyer les lignes vides multiples
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
return content.strip()
|
||||
@@ -1,332 +0,0 @@
|
||||
"""
|
||||
Générateur de documentation Markdown pour l'aide Homelab Automation.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def build_help_markdown(html_path: Path = None, html_content: str = None) -> str:
|
||||
"""Génère le contenu Markdown d'aide professionnel.
|
||||
|
||||
Args:
|
||||
html_path: Non utilisé (conservé pour compatibilité)
|
||||
html_content: Non utilisé (conservé pour compatibilité)
|
||||
|
||||
Returns:
|
||||
Contenu Markdown formaté professionnellement
|
||||
"""
|
||||
return _get_professional_help_markdown()
|
||||
|
||||
|
||||
def _get_professional_help_markdown() -> str:
|
||||
"""Retourne la documentation d'aide complète et professionnelle."""
|
||||
return """# Homelab Automation Dashboard
|
||||
|
||||
## Guide d'Utilisation
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Table des Matières
|
||||
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
## Démarrage Rapide
|
||||
|
||||
### Étape 1 : Ajouter vos Hosts
|
||||
|
||||
Commencez par ajouter vos serveurs dans la section **Hosts**. Chaque host nécessite :
|
||||
- Un nom unique
|
||||
- Une adresse IP
|
||||
- Un système d'exploitation
|
||||
|
||||
### Étape 2 : Bootstrap Ansible
|
||||
|
||||
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)
|
||||
|
||||
### Étape 3 : Automatiser
|
||||
|
||||
Utilisez les **Actions Rapides** ou exécutez des playbooks personnalisés pour automatiser vos tâches récurrentes.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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*
|
||||
"""
|
||||
Reference in New Issue
Block a user