Remove HTML-to-Markdown parser and PDF generation functionality, clean up unused imports and whitespace
This commit is contained in:
parent
8db38ca75e
commit
6c83ada7d1
@ -31,13 +31,10 @@ import pytz
|
||||
from fastapi import FastAPI, HTTPException, Depends, Request, Form, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
|
||||
from fastapi.security import APIKeyHeader
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from html.parser import HTMLParser
|
||||
from io import BytesIO
|
||||
from xml.sax.saxutils import escape as _xml_escape
|
||||
import hashlib
|
||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy import select
|
||||
@ -48,8 +45,6 @@ from app.crud.log import LogRepository # type: ignore
|
||||
from app.crud.task import TaskRepository # type: ignore
|
||||
from app.crud.schedule import ScheduleRepository # type: ignore
|
||||
from app.crud.schedule_run import ScheduleRunRepository # type: ignore
|
||||
from app.models.database import init_db # type: ignore
|
||||
from app.services.notification_service import notification_service, send_notification # type: ignore
|
||||
from app.schemas.notification import NotificationRequest, NotificationResponse # type: ignore
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
@ -102,452 +97,6 @@ ACTION_PLAYBOOK_MAP = {
|
||||
# Gestionnaire de clés API
|
||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
|
||||
class _HelpHtmlToMarkdownParser(HTMLParser):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(convert_charrefs=True)
|
||||
self._in_help = False
|
||||
self._help_depth = 0
|
||||
self._lines: list[str] = []
|
||||
self._buf: list[str] = []
|
||||
self._list_stack: list[str] = []
|
||||
self._in_pre = False
|
||||
self._span_class: str = ""
|
||||
self._in_toc_link = False
|
||||
|
||||
def _flush(self) -> None:
|
||||
txt = "".join(self._buf).strip()
|
||||
self._buf = []
|
||||
if txt:
|
||||
self._lines.append(txt)
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
|
||||
attrs_dict = {k: (v or "") for k, v in attrs}
|
||||
|
||||
if tag == "section" and attrs_dict.get("id") == "page-help":
|
||||
self._in_help = True
|
||||
self._help_depth = 1
|
||||
return
|
||||
if not self._in_help:
|
||||
return
|
||||
|
||||
if tag == "section":
|
||||
self._help_depth += 1
|
||||
if tag in {"h1", "h2", "h3", "h4"}:
|
||||
self._flush()
|
||||
if tag in {"p", "div"}:
|
||||
self._flush()
|
||||
if tag in {"ul", "ol"}:
|
||||
self._flush()
|
||||
self._list_stack.append(tag)
|
||||
if tag == "li":
|
||||
self._flush()
|
||||
if tag == "pre":
|
||||
self._flush()
|
||||
self._in_pre = True
|
||||
self._lines.append("```")
|
||||
if tag == "code":
|
||||
self._buf.append("`")
|
||||
if tag == "span":
|
||||
self._span_class = attrs_dict.get("class") or ""
|
||||
if "help-code" in self._span_class:
|
||||
self._buf.append("`")
|
||||
|
||||
if tag == "a":
|
||||
cls = attrs_dict.get("class") or ""
|
||||
if "help-toc-item" in cls:
|
||||
self._flush()
|
||||
self._in_toc_link = True
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
if not self._in_help:
|
||||
return
|
||||
|
||||
if tag == "a" and self._in_toc_link:
|
||||
txt = "".join(self._buf).strip()
|
||||
self._buf = []
|
||||
if txt:
|
||||
self._lines.append(f"- {txt}")
|
||||
self._lines.append("")
|
||||
self._in_toc_link = False
|
||||
return
|
||||
|
||||
if tag == "section":
|
||||
self._help_depth -= 1
|
||||
if self._help_depth <= 0:
|
||||
self._flush()
|
||||
self._in_help = False
|
||||
return
|
||||
|
||||
if tag in {"h1", "h2", "h3", "h4"}:
|
||||
txt = "".join(self._buf).strip()
|
||||
self._buf = []
|
||||
if txt:
|
||||
level = {"h1": "#", "h2": "##", "h3": "###", "h4": "####"}[tag]
|
||||
self._lines.append(f"{level} {txt}")
|
||||
self._lines.append("")
|
||||
return
|
||||
|
||||
if tag == "li":
|
||||
txt = "".join(self._buf).strip()
|
||||
self._buf = []
|
||||
if txt:
|
||||
if self._list_stack and self._list_stack[-1] == "ol":
|
||||
self._lines.append(f"1. {txt}")
|
||||
else:
|
||||
self._lines.append(f"- {txt}")
|
||||
return
|
||||
|
||||
if tag in {"p", "div"}:
|
||||
self._flush()
|
||||
self._lines.append("")
|
||||
return
|
||||
|
||||
if tag in {"ul", "ol"}:
|
||||
self._flush()
|
||||
if self._list_stack:
|
||||
self._list_stack.pop()
|
||||
self._lines.append("")
|
||||
return
|
||||
|
||||
if tag == "pre":
|
||||
self._flush()
|
||||
self._in_pre = False
|
||||
self._lines.append("```")
|
||||
self._lines.append("")
|
||||
return
|
||||
|
||||
if tag == "code":
|
||||
self._buf.append("`")
|
||||
return
|
||||
|
||||
if tag == "span" and "help-code" in (self._span_class or ""):
|
||||
self._buf.append("`")
|
||||
self._span_class = ""
|
||||
return
|
||||
|
||||
def handle_data(self, data: str) -> None:
|
||||
if not self._in_help:
|
||||
return
|
||||
if not data:
|
||||
return
|
||||
txt = data
|
||||
if not self._in_pre:
|
||||
txt = re.sub(r"\s+", " ", txt)
|
||||
self._buf.append(txt)
|
||||
|
||||
def markdown(self) -> str:
|
||||
out = "\n".join(line.rstrip() for line in self._lines)
|
||||
out = re.sub(r"\n{3,}", "\n\n", out).strip() + "\n"
|
||||
return out
|
||||
|
||||
|
||||
def _build_help_markdown() -> str:
|
||||
html_path = BASE_DIR / "index.html"
|
||||
html = html_path.read_text(encoding="utf-8")
|
||||
parser = _HelpHtmlToMarkdownParser()
|
||||
parser.feed(html)
|
||||
md = parser.markdown()
|
||||
if len(md.strip()) < 200:
|
||||
raise HTTPException(status_code=500, detail="Extraction du contenu d'aide insuffisante")
|
||||
return md
|
||||
|
||||
|
||||
def _extract_leading_emojis(text: str) -> tuple[str, str]:
|
||||
if not text:
|
||||
return "", ""
|
||||
m = re.match(
|
||||
r"^(?P<emoji>[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF\U0001F1E0-\U0001F1FF\u2600-\u26FF\u2700-\u27BF\u200D\uFE0E\uFE0F]+)\s*(?P<rest>.*)$",
|
||||
text,
|
||||
)
|
||||
if not m:
|
||||
return "", text
|
||||
emoji = (m.group("emoji") or "").strip()
|
||||
rest = (m.group("rest") or "").lstrip()
|
||||
if not emoji:
|
||||
return "", text
|
||||
return emoji, rest
|
||||
|
||||
|
||||
_LAST_EMOJI_FONT_PATH = ""
|
||||
|
||||
|
||||
def _emoji_to_png_bytes(emoji: str, px: int = 64) -> bytes:
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
except ModuleNotFoundError:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Génération PDF avec emojis indisponible: dépendance 'pillow' manquante (installer pillow dans l'environnement runtime).",
|
||||
)
|
||||
|
||||
font_paths = [
|
||||
r"C:\\Windows\\Fonts\\seguiemj.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoEmoji-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
]
|
||||
|
||||
emoji_soft_fallback = {
|
||||
"❤️\u200d\U0001FA79": "❤️",
|
||||
"⚙️": "⚙",
|
||||
"🛠️": "🛠",
|
||||
"🏗️": "🏗",
|
||||
"⚡️": "⚡",
|
||||
}
|
||||
|
||||
global _LAST_EMOJI_FONT_PATH
|
||||
_LAST_EMOJI_FONT_PATH = ""
|
||||
|
||||
def _load_font():
|
||||
for fp in font_paths:
|
||||
try:
|
||||
try:
|
||||
_LAST_EMOJI_FONT_PATH = fp
|
||||
return ImageFont.truetype(fp, size=int(px * 0.75), embedded_color=True)
|
||||
except TypeError:
|
||||
_LAST_EMOJI_FONT_PATH = fp
|
||||
return ImageFont.truetype(fp, size=int(px * 0.75))
|
||||
except Exception:
|
||||
continue
|
||||
_LAST_EMOJI_FONT_PATH = "(default)"
|
||||
return ImageFont.load_default()
|
||||
|
||||
font = _load_font()
|
||||
|
||||
pad = max(2, int(px * 0.18))
|
||||
canvas = px + (2 * pad)
|
||||
img = Image.new("RGBA", (canvas, canvas), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
bbox = draw.textbbox((0, 0), emoji, font=font)
|
||||
w = bbox[2] - bbox[0]
|
||||
h = bbox[3] - bbox[1]
|
||||
x = ((canvas - w) // 2) - bbox[0]
|
||||
y = ((canvas - h) // 2) - bbox[1]
|
||||
|
||||
draw_kwargs = {"font": font, "fill": (0, 0, 0, 255)}
|
||||
try:
|
||||
draw.text((x, y), emoji, embedded_color=True, **draw_kwargs)
|
||||
except TypeError:
|
||||
draw.text((x, y), emoji, **draw_kwargs)
|
||||
|
||||
try:
|
||||
alpha = img.getchannel("A")
|
||||
nonzero = alpha.getbbox() is not None
|
||||
except Exception:
|
||||
nonzero = True
|
||||
if (not nonzero) and emoji in emoji_soft_fallback:
|
||||
fallback_emoji = emoji_soft_fallback[emoji]
|
||||
img = Image.new("RGBA", (canvas, canvas), (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
bbox = draw.textbbox((0, 0), fallback_emoji, font=font)
|
||||
w = bbox[2] - bbox[0]
|
||||
h = bbox[3] - bbox[1]
|
||||
x = ((canvas - w) // 2) - bbox[0]
|
||||
y = ((canvas - h) // 2) - bbox[1]
|
||||
try:
|
||||
draw.text((x, y), fallback_emoji, embedded_color=True, **draw_kwargs)
|
||||
except TypeError:
|
||||
draw.text((x, y), fallback_emoji, **draw_kwargs)
|
||||
|
||||
out = BytesIO()
|
||||
img.save(out, format="PNG")
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def _markdown_to_pdf_bytes(markdown: str) -> bytes:
|
||||
try:
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.platypus import Paragraph, Preformatted, SimpleDocTemplate, Spacer, Table, TableStyle, Image
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
except ModuleNotFoundError as e:
|
||||
if "reportlab" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Génération PDF indisponible: dépendance 'reportlab' manquante (installer reportlab dans l'environnement runtime).",
|
||||
)
|
||||
raise
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
mono_font_name = "Courier"
|
||||
mono_font_paths = [
|
||||
r"C:\\Windows\\Fonts\\consola.ttf",
|
||||
r"C:\\Windows\\Fonts\\lucon.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
|
||||
]
|
||||
for fp in mono_font_paths:
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont("MonoUnicode", fp))
|
||||
mono_font_name = "MonoUnicode"
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
normal = styles["BodyText"]
|
||||
h1 = styles["Heading1"]
|
||||
h2 = styles["Heading2"]
|
||||
h3 = styles["Heading3"]
|
||||
code_style = ParagraphStyle(
|
||||
"CodeBlock",
|
||||
parent=styles.get("Code", normal),
|
||||
fontName=mono_font_name,
|
||||
fontSize=9,
|
||||
leading=11,
|
||||
backColor=colors.whitesmoke,
|
||||
)
|
||||
|
||||
story: list[Any] = []
|
||||
in_code = False
|
||||
code_lines: list[str] = []
|
||||
lines = (markdown or "").splitlines()
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if line.strip().startswith("```"):
|
||||
if not in_code:
|
||||
in_code = True
|
||||
code_lines = []
|
||||
else:
|
||||
in_code = False
|
||||
story.append(Preformatted("\n".join(code_lines), code_style))
|
||||
story.append(Spacer(1, 0.35 * cm))
|
||||
i += 1
|
||||
continue
|
||||
if in_code:
|
||||
code_lines.append(line)
|
||||
i += 1
|
||||
continue
|
||||
if not line.strip():
|
||||
story.append(Spacer(1, 0.2 * cm))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if line.startswith("### "):
|
||||
raw = line[4:].strip()
|
||||
emoji, rest = _extract_leading_emojis(raw)
|
||||
if emoji:
|
||||
png = _emoji_to_png_bytes(emoji, px=56)
|
||||
img = Image(BytesIO(png), width=0.55 * cm, height=0.55 * cm)
|
||||
tbl = Table([[img, Paragraph(_xml_escape(rest), h3)]], colWidths=[0.7 * cm, None])
|
||||
tbl.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 1),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
|
||||
]
|
||||
)
|
||||
)
|
||||
story.append(tbl)
|
||||
else:
|
||||
story.append(Paragraph(_xml_escape(raw), h3))
|
||||
story.append(Spacer(1, 0.2 * cm))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if line.startswith("## "):
|
||||
raw = line[3:].strip()
|
||||
emoji, rest = _extract_leading_emojis(raw)
|
||||
if emoji:
|
||||
png = _emoji_to_png_bytes(emoji, px=64)
|
||||
img = Image(BytesIO(png), width=0.65 * cm, height=0.65 * cm)
|
||||
tbl = Table([[img, Paragraph(_xml_escape(rest), h2)]], colWidths=[0.8 * cm, None])
|
||||
tbl.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 1),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
|
||||
]
|
||||
)
|
||||
)
|
||||
story.append(tbl)
|
||||
else:
|
||||
story.append(Paragraph(_xml_escape(raw), h2))
|
||||
story.append(Spacer(1, 0.25 * cm))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if line.startswith("# "):
|
||||
raw = line[2:].strip()
|
||||
emoji, rest = _extract_leading_emojis(raw)
|
||||
if emoji:
|
||||
png = _emoji_to_png_bytes(emoji, px=72)
|
||||
img = Image(BytesIO(png), width=0.8 * cm, height=0.8 * cm)
|
||||
tbl = Table([[img, Paragraph(_xml_escape(rest), h1)]], colWidths=[1.0 * cm, None])
|
||||
tbl.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 1),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
|
||||
]
|
||||
)
|
||||
)
|
||||
story.append(tbl)
|
||||
else:
|
||||
story.append(Paragraph(_xml_escape(raw), h1))
|
||||
story.append(Spacer(1, 0.3 * cm))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if line.lstrip().startswith(("- ", "* ")):
|
||||
while i < len(lines) and lines[i].lstrip().startswith(("- ", "* ")):
|
||||
item_text = lines[i].lstrip()[2:].strip()
|
||||
emoji, rest = _extract_leading_emojis(item_text)
|
||||
if emoji:
|
||||
png = _emoji_to_png_bytes(emoji, px=52)
|
||||
img = Image(BytesIO(png), width=0.5 * cm, height=0.5 * cm)
|
||||
tbl = Table([[img, Paragraph(_xml_escape(rest), normal)]], colWidths=[0.75 * cm, None])
|
||||
tbl.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 12),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 6),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 1),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
|
||||
]
|
||||
)
|
||||
)
|
||||
story.append(tbl)
|
||||
else:
|
||||
story.append(Paragraph(_xml_escape(f"• {item_text}"), normal))
|
||||
story.append(Spacer(1, 0.12 * cm))
|
||||
i += 1
|
||||
story.append(Spacer(1, 0.15 * cm))
|
||||
continue
|
||||
|
||||
story.append(Paragraph(_xml_escape(line.strip()), normal))
|
||||
story.append(Spacer(1, 0.2 * cm))
|
||||
i += 1
|
||||
|
||||
buffer = BytesIO()
|
||||
doc = SimpleDocTemplate(
|
||||
buffer,
|
||||
pagesize=A4,
|
||||
leftMargin=2 * cm,
|
||||
rightMargin=2 * cm,
|
||||
topMargin=2 * cm,
|
||||
bottomMargin=2 * cm,
|
||||
title="Homelab Automation Dashboard — Centre d'Aide",
|
||||
)
|
||||
doc.build(story)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
# Modèles Pydantic améliorés
|
||||
class CommandResult(BaseModel):
|
||||
status: str
|
||||
@ -668,14 +217,12 @@ class HostRequest(BaseModel):
|
||||
env_group: str = Field(..., description="Groupe d'environnement (ex: env_homelab, env_prod)")
|
||||
role_groups: List[str] = Field(default=[], description="Groupes de rôles (ex: role_proxmox, role_sbc)")
|
||||
|
||||
|
||||
class HostUpdateRequest(BaseModel):
|
||||
"""Requête de mise à jour d'un hôte"""
|
||||
env_group: Optional[str] = Field(default=None, description="Nouveau groupe d'environnement")
|
||||
role_groups: Optional[List[str]] = Field(default=None, description="Nouveaux groupes de rôles")
|
||||
ansible_host: Optional[str] = Field(default=None, description="Nouvelle adresse ansible_host")
|
||||
|
||||
|
||||
class GroupRequest(BaseModel):
|
||||
"""Requête pour créer un groupe"""
|
||||
name: str = Field(..., min_length=3, max_length=50, description="Nom du groupe (ex: env_prod, role_web)")
|
||||
@ -696,7 +243,6 @@ class GroupRequest(BaseModel):
|
||||
raise ValueError("Le type doit être 'env' ou 'role'")
|
||||
return v
|
||||
|
||||
|
||||
class GroupUpdateRequest(BaseModel):
|
||||
"""Requête pour modifier un groupe"""
|
||||
new_name: str = Field(..., min_length=3, max_length=50, description="Nouveau nom du groupe")
|
||||
@ -709,12 +255,10 @@ class GroupUpdateRequest(BaseModel):
|
||||
raise ValueError('Le nom du groupe ne peut contenir que des lettres, chiffres, tirets et underscores')
|
||||
return v
|
||||
|
||||
|
||||
class GroupDeleteRequest(BaseModel):
|
||||
"""Requête pour supprimer un groupe"""
|
||||
move_hosts_to: Optional[str] = Field(default=None, description="Groupe vers lequel déplacer les hôtes")
|
||||
|
||||
|
||||
class AdHocCommandRequest(BaseModel):
|
||||
"""Requête pour exécuter une commande ad-hoc Ansible"""
|
||||
target: str = Field(..., description="Hôte ou groupe cible")
|
||||
@ -724,7 +268,6 @@ class AdHocCommandRequest(BaseModel):
|
||||
timeout: int = Field(default=60, ge=5, le=600, description="Timeout en secondes")
|
||||
category: Optional[str] = Field(default="default", description="Catégorie d'historique pour cette commande")
|
||||
|
||||
|
||||
class AdHocCommandResult(BaseModel):
|
||||
"""Résultat d'une commande ad-hoc"""
|
||||
target: str
|
||||
@ -736,7 +279,6 @@ class AdHocCommandResult(BaseModel):
|
||||
duration: float
|
||||
hosts_results: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class AdHocHistoryEntry(BaseModel):
|
||||
"""Entrée dans l'historique des commandes ad-hoc"""
|
||||
id: str
|
||||
@ -750,7 +292,6 @@ class AdHocHistoryEntry(BaseModel):
|
||||
last_used: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
use_count: int = 1
|
||||
|
||||
|
||||
class AdHocHistoryCategory(BaseModel):
|
||||
"""Catégorie pour organiser les commandes ad-hoc"""
|
||||
name: str
|
||||
@ -758,7 +299,6 @@ class AdHocHistoryCategory(BaseModel):
|
||||
color: str = "#7c3aed"
|
||||
icon: str = "fa-folder"
|
||||
|
||||
|
||||
class TaskLogFile(BaseModel):
|
||||
"""Représentation d'un fichier de log de tâche"""
|
||||
id: str
|
||||
@ -784,7 +324,6 @@ class TaskLogFile(BaseModel):
|
||||
target_type: Optional[str] = None # Type de cible: 'host', 'group', 'role'
|
||||
source_type: Optional[str] = None # Source: 'scheduled', 'manual', 'adhoc'
|
||||
|
||||
|
||||
class TasksFilterParams(BaseModel):
|
||||
"""Paramètres de filtrage des tâches"""
|
||||
status: Optional[str] = None # pending, running, completed, failed, all
|
||||
@ -799,7 +338,6 @@ class TasksFilterParams(BaseModel):
|
||||
limit: int = 50 # Pagination côté serveur
|
||||
offset: int = 0
|
||||
|
||||
|
||||
# ===== MODÈLES PLANIFICATEUR (SCHEDULER) =====
|
||||
|
||||
class ScheduleRecurrence(BaseModel):
|
||||
@ -810,7 +348,6 @@ class ScheduleRecurrence(BaseModel):
|
||||
day_of_month: Optional[int] = Field(default=None, ge=1, le=31, description="Jour du mois (1-31) pour monthly")
|
||||
cron_expression: Optional[str] = Field(default=None, description="Expression cron pour custom")
|
||||
|
||||
|
||||
class Schedule(BaseModel):
|
||||
"""Modèle d'un schedule de playbook"""
|
||||
id: str = Field(default_factory=lambda: f"sched_{uuid.uuid4().hex[:12]}")
|
||||
@ -850,7 +387,6 @@ class Schedule(BaseModel):
|
||||
# Si schedule_type est 'once', recurrence n'est pas obligatoire
|
||||
return v
|
||||
|
||||
|
||||
class ScheduleRun(BaseModel):
|
||||
"""Historique d'une exécution de schedule"""
|
||||
id: str = Field(default_factory=lambda: f"run_{uuid.uuid4().hex[:12]}")
|
||||
@ -869,7 +405,6 @@ class ScheduleRun(BaseModel):
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
|
||||
|
||||
class ScheduleCreateRequest(BaseModel):
|
||||
"""Requête de création d'un schedule"""
|
||||
name: str = Field(..., min_length=3, max_length=100)
|
||||
@ -898,7 +433,6 @@ class ScheduleCreateRequest(BaseModel):
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
raise ValueError(f"Fuseau horaire invalide: {v}")
|
||||
|
||||
|
||||
class ScheduleUpdateRequest(BaseModel):
|
||||
"""Requête de mise à jour d'un schedule"""
|
||||
name: Optional[str] = Field(default=None, min_length=3, max_length=100)
|
||||
@ -918,7 +452,6 @@ class ScheduleUpdateRequest(BaseModel):
|
||||
notification_type: Optional[Literal["none", "all", "errors"]] = Field(default=None)
|
||||
tags: Optional[List[str]] = Field(default=None)
|
||||
|
||||
|
||||
class ScheduleStats(BaseModel):
|
||||
"""Statistiques globales des schedules"""
|
||||
total: int = 0
|
||||
@ -931,7 +464,6 @@ class ScheduleStats(BaseModel):
|
||||
executions_24h: int = 0
|
||||
success_rate_7d: float = 0.0
|
||||
|
||||
|
||||
# ===== SERVICE DE LOGGING MARKDOWN =====
|
||||
|
||||
class TaskLogService:
|
||||
@ -4095,51 +3627,6 @@ async def verify_api_key(api_key: str = Depends(api_key_header)) -> bool:
|
||||
raise HTTPException(status_code=401, detail="Clé API invalide ou manquante")
|
||||
return True
|
||||
|
||||
@app.get("/api/help/documentation.md")
|
||||
async def download_help_markdown(api_key_valid: bool = Depends(verify_api_key)):
|
||||
"""Télécharge la documentation du centre d'aide en format Markdown."""
|
||||
markdown = _build_help_markdown()
|
||||
digest = hashlib.sha256(markdown.encode("utf-8")).hexdigest()[:16]
|
||||
etag = f'W/"md-{digest}"'
|
||||
filename = f"homelab-documentation-{digest}.md"
|
||||
return Response(
|
||||
content=markdown,
|
||||
media_type="text/markdown; charset=utf-8",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}",
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0",
|
||||
"ETag": etag,
|
||||
"X-Help-Doc-Generator": "app_optimized._build_help_markdown",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/help/documentation.pdf")
|
||||
async def download_help_pdf(api_key_valid: bool = Depends(verify_api_key)):
|
||||
"""Télécharge la documentation du centre d'aide en format PDF."""
|
||||
markdown = _build_help_markdown()
|
||||
pdf_bytes = _markdown_to_pdf_bytes(markdown)
|
||||
digest = hashlib.sha256(bytes(pdf_bytes)).hexdigest()[:16]
|
||||
etag = f'W/"pdf-{digest}"'
|
||||
filename = f"homelab-documentation-{digest}.pdf"
|
||||
emoji_font = globals().get("_LAST_EMOJI_FONT_PATH", "")
|
||||
return Response(
|
||||
content=pdf_bytes,
|
||||
media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={filename}",
|
||||
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0",
|
||||
"ETag": etag,
|
||||
"X-Help-Doc-Generator": "app_optimized._markdown_to_pdf_bytes",
|
||||
"X-Help-Emoji-Font": emoji_font,
|
||||
},
|
||||
)
|
||||
|
||||
# Routes API
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root(request: Request):
|
||||
"""Page principale du dashboard"""
|
||||
|
||||
776
app/index.html
776
app/index.html
@ -3727,724 +3727,26 @@
|
||||
|
||||
<!-- Layout avec Table des Matières -->
|
||||
<div class="flex gap-8">
|
||||
<!-- Table des Matières (sidebar gauche) -->
|
||||
<!-- Table des Matières (sidebar gauche) - Chargée dynamiquement -->
|
||||
<aside class="hidden lg:block w-64 flex-shrink-0">
|
||||
<div class="help-toc glass-card p-4">
|
||||
<div class="help-toc glass-card p-4 sticky top-24">
|
||||
<div class="help-toc-title">Table des Matières</div>
|
||||
<nav>
|
||||
<a href="#help-quickstart" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-quickstart')">
|
||||
<span class="mr-2">⚡️</span>Démarrage Rapide
|
||||
</a>
|
||||
<a href="#help-indicators" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-indicators')">
|
||||
<span class="mr-2">❤️🩹</span>Indicateurs de Santé
|
||||
</a>
|
||||
<a href="#help-architecture" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-architecture')">
|
||||
<span class="mr-2">🏗️</span>Architecture
|
||||
</a>
|
||||
<a href="#help-features" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-features')">
|
||||
<span class="mr-2">⚙️</span>Fonctionnalités
|
||||
</a>
|
||||
<a href="#help-notifications" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-notifications')">
|
||||
<span class="mr-2">🔔</span>Notifications
|
||||
</a>
|
||||
<a href="#help-playbooks" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-playbooks')">
|
||||
<span class="mr-2">📖</span>Playbooks Ansible
|
||||
</a>
|
||||
<a href="#help-api" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-api')">
|
||||
<span class="mr-2">🔗</span>Référence API
|
||||
</a>
|
||||
<a href="#help-troubleshooting" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-troubleshooting')">
|
||||
<span class="mr-2">🛠️</span>Dépannage
|
||||
</a>
|
||||
<a href="#help-shortcuts" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-shortcuts')">
|
||||
<span class="mr-2">✨</span>Raccourcis & Astuces
|
||||
</a>
|
||||
<nav id="help-toc-nav">
|
||||
<!-- TOC chargée dynamiquement depuis help.md -->
|
||||
<div class="text-gray-500 text-sm py-2">Chargement...</div>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Contenu Principal -->
|
||||
<div class="flex-1 help-main-content">
|
||||
|
||||
<!-- Quick Start -->
|
||||
<div id="help-quickstart" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">⚡️</span>
|
||||
Démarrage Rapide
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="help-card">
|
||||
<div class="text-3xl mb-4 text-green-400"><i class="fas fa-1"></i></div>
|
||||
<h3 class="font-semibold mb-2">Ajouter vos Hosts</h3>
|
||||
<p class="text-gray-400 text-sm">
|
||||
Commencez par ajouter vos serveurs dans la section <span class="help-code">Hosts</span>.
|
||||
Chaque host nécessite un nom, une adresse IP et un système d'exploitation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="help-card">
|
||||
<div class="text-3xl mb-4 text-blue-400"><i class="fas fa-2"></i></div>
|
||||
<h3 class="font-semibold mb-2">Bootstrap Ansible</h3>
|
||||
<p class="text-gray-400 text-sm">
|
||||
Exécutez le <span class="help-code">Bootstrap</span> sur chaque host pour configurer
|
||||
l'accès SSH et les prérequis Ansible.
|
||||
</p>
|
||||
</div>
|
||||
<div class="help-card">
|
||||
<div class="text-3xl mb-4 text-purple-400"><i class="fas fa-3"></i></div>
|
||||
<h3 class="font-semibold mb-2">Automatiser</h3>
|
||||
<p class="text-gray-400 text-sm">
|
||||
Utilisez les <span class="help-code">Actions Rapides</span> ou exécutez des playbooks
|
||||
personnalisés pour automatiser vos tâches.
|
||||
</p>
|
||||
<!-- Contenu Principal - Chargé dynamiquement depuis help.md -->
|
||||
<div id="help-dynamic-content" class="flex-1 help-main-content">
|
||||
<!-- Placeholder pendant le chargement -->
|
||||
<div class="glass-card p-8 mb-8 text-center">
|
||||
<div class="loading-spinner mx-auto mb-4"></div>
|
||||
<p class="text-gray-400">Chargement de la documentation...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Indicateurs de Santé -->
|
||||
<div id="help-indicators" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">❤️🩹</span>
|
||||
Indicateurs de Santé des Hosts
|
||||
</h2>
|
||||
<p class="text-gray-400 mb-6">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Explication des barres -->
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4 text-purple-400">Comprendre l'Indicateur</h3>
|
||||
<div class="space-y-4">
|
||||
<!-- Niveau Excellent -->
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="health-indicator-demo">
|
||||
<div class="health-bar bg-green-500"></div>
|
||||
<div class="health-bar bg-green-500"></div>
|
||||
<div class="health-bar bg-green-500"></div>
|
||||
<div class="health-bar bg-green-500"></div>
|
||||
<div class="health-bar bg-green-500"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-green-400">Excellent</span>
|
||||
<span class="text-gray-400 text-sm ml-2">(5 barres vertes)</span>
|
||||
<p class="text-gray-500 text-xs mt-1">Host en ligne, bootstrap OK, vérifié récemment</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Niveau Bon -->
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="health-indicator-demo">
|
||||
<div class="health-bar bg-yellow-500"></div>
|
||||
<div class="health-bar bg-yellow-500"></div>
|
||||
<div class="health-bar bg-yellow-500"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-yellow-400">Bon</span>
|
||||
<span class="text-gray-400 text-sm ml-2">(3-4 barres jaunes)</span>
|
||||
<p class="text-gray-500 text-xs mt-1">Host fonctionnel mais certains aspects à améliorer</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Niveau Moyen -->
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="health-indicator-demo">
|
||||
<div class="health-bar bg-orange-500"></div>
|
||||
<div class="health-bar bg-orange-500"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-orange-400">Moyen</span>
|
||||
<span class="text-gray-400 text-sm ml-2">(2 barres oranges)</span>
|
||||
<p class="text-gray-500 text-xs mt-1">Attention requise - vérification recommandée</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Niveau Faible -->
|
||||
<div class="flex items-center gap-4 p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="health-indicator-demo">
|
||||
<div class="health-bar bg-red-500"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
<div class="health-bar bg-gray-600"></div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-semibold text-red-400">Faible</span>
|
||||
<span class="text-gray-400 text-sm ml-2">(1 barre rouge)</span>
|
||||
<p class="text-gray-500 text-xs mt-1">Host hors ligne ou non configuré</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Facteurs de calcul -->
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4 text-purple-400">Facteurs de Calcul du Score</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="fas fa-circle text-green-400 text-xs"></i>
|
||||
<span class="font-medium">Statut en ligne</span>
|
||||
<span class="text-green-400 text-sm ml-auto">+2 points</span>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs">Le host répond aux requêtes réseau</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="fas fa-check-circle text-blue-400 text-xs"></i>
|
||||
<span class="font-medium">Bootstrap Ansible OK</span>
|
||||
<span class="text-blue-400 text-sm ml-auto">+1 point</span>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs">SSH et prérequis Ansible configurés</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="fas fa-clock text-purple-400 text-xs"></i>
|
||||
<span class="font-medium">Vérifié récemment (<1h)</span>
|
||||
<span class="text-purple-400 text-sm ml-auto">+2 points</span>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs">Dernière vérification il y a moins d'une heure</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-800/50 rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i class="fas fa-clock text-yellow-400 text-xs"></i>
|
||||
<span class="font-medium">Vérifié aujourd'hui</span>
|
||||
<span class="text-yellow-400 text-sm ml-auto">+1 point</span>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs">Dernière vérification dans les 24 dernières heures</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>Astuce:</strong> Exécutez régulièrement un <span class="help-code">Health Check</span>
|
||||
pour maintenir un score de santé élevé.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statuts Bootstrap -->
|
||||
<div class="mt-8">
|
||||
<h3 class="font-semibold mb-4 text-purple-400">Statuts Bootstrap Ansible</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="p-4 bg-green-900/20 border border-green-600/30 rounded-lg">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="px-2 py-0.5 bg-green-600/30 text-green-400 text-xs rounded-full flex items-center">
|
||||
<i class="fas fa-check-circle mr-1"></i>Ansible Ready
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-yellow-900/20 border border-yellow-600/30 rounded-lg">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="px-2 py-0.5 bg-yellow-600/30 text-yellow-400 text-xs rounded-full flex items-center">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>Non configuré
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
Le bootstrap n'a pas encore été exécuté sur ce host. Cliquez sur le bouton
|
||||
<span class="help-code">Bootstrap</span> pour configurer l'accès Ansible.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Texte "Jamais vérifié" -->
|
||||
<div class="mt-6 p-4 bg-gray-800/50 rounded-lg">
|
||||
<h4 class="font-semibold mb-2 flex items-center gap-2">
|
||||
<i class="fas fa-question-circle text-gray-400"></i>
|
||||
Que signifie "Jamais vérifié" ?
|
||||
</h4>
|
||||
<p class="text-gray-400 text-sm">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Architecture -->
|
||||
<div id="help-architecture" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">🏗️</span>
|
||||
Architecture de la Solution
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4 text-purple-400">Stack Technologique</h3>
|
||||
<ul class="help-list">
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fas fa-server text-green-400"></i>
|
||||
<span><strong>Backend:</strong> FastAPI (Python) - API REST haute performance</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fas fa-cogs text-orange-400"></i>
|
||||
<span><strong>Automation:</strong> Ansible - Gestion de configuration</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fas fa-desktop text-blue-400"></i>
|
||||
<span><strong>Frontend:</strong> HTML/CSS/JS avec TailwindCSS</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fab fa-docker text-cyan-400"></i>
|
||||
<span><strong>Déploiement:</strong> Docker & Docker Compose</span>
|
||||
</li>
|
||||
<li class="flex items-center gap-3">
|
||||
<i class="fas fa-plug text-yellow-400"></i>
|
||||
<span><strong>Temps réel:</strong> WebSocket pour les mises à jour live</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4 text-purple-400">Structure des Fichiers</h3>
|
||||
<div class="bg-black/40 p-4 rounded-lg font-mono text-sm">
|
||||
<pre class="text-gray-300">
|
||||
homelab-automation/
|
||||
├── app/
|
||||
│ ├── app_optimized.py # API FastAPI
|
||||
│ ├── index.html # Interface web
|
||||
│ └── main.js # Logique frontend
|
||||
├── ansible/
|
||||
│ ├── inventory/
|
||||
│ │ ├── hosts.yml # Inventaire des hosts
|
||||
│ │ └── group_vars/ # Variables par groupe
|
||||
│ └── playbooks/ # Playbooks Ansible
|
||||
├── tasks_logs/ # Logs des tâches
|
||||
├── docker-compose.yml
|
||||
└── Dockerfile</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fonctionnalités -->
|
||||
<div id="help-features" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">⚙️</span>
|
||||
Fonctionnalités Détaillées par Section
|
||||
</h2>
|
||||
|
||||
<!-- Accordéon -->
|
||||
<div class="space-y-2">
|
||||
<!-- Dashboard -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-tachometer-alt text-purple-400"></i>
|
||||
<strong>Dashboard</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">Vue d'ensemble centralisée de votre infrastructure homelab.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Métriques en temps réel:</strong> État des hosts (Online/Offline), statistiques des tâches (Succès/Échec).</li>
|
||||
<li><strong>Actions Rapides:</strong> Accès immédiat aux opérations courantes (Mise à jour globale, Health Check général).</li>
|
||||
<li><strong>Aperçu des Hosts:</strong> Liste condensée avec indicateurs de statut et OS pour une surveillance rapide.</li>
|
||||
<li><strong>Notifications:</strong> Alertes visuelles sur les dernières activités importantes.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hosts -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-server text-blue-400"></i>
|
||||
<strong>Hosts</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">Gestion complète du cycle de vie de vos serveurs.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Inventaire:</strong> Ajout, modification et suppression de hosts avec détection d'OS.</li>
|
||||
<li><strong>Bootstrap Ansible:</strong> Préparation automatique des serveurs (User, Clés SSH, Sudo).</li>
|
||||
<li><strong>Indicateurs de Santé:</strong> Score de santé détaillé basé sur la connectivité et la configuration.</li>
|
||||
<li><strong>Actions Individuelles:</strong> Exécution de playbooks spécifiques (Upgrade, Backup, Reboot) sur un host.</li>
|
||||
<li><strong>Détails Avancés:</strong> Vue détaillée avec historique des tâches et logs spécifiques au host.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playbooks -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-book text-pink-400"></i>
|
||||
<strong>Playbooks</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">Bibliothèque et exécution de playbooks Ansible personnalisés.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Catalogue:</strong> Liste de tous les playbooks disponibles dans votre répertoire.</li>
|
||||
<li><strong>Exécution Ciblée:</strong> Lancement de playbooks sur des hosts spécifiques ou des groupes.</li>
|
||||
<li><strong>Logs en Direct:</strong> Suivi temps réel de l'exécution Ansible (console output).</li>
|
||||
<li><strong>Historique:</strong> Accès rapide aux résultats des exécutions précédentes.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tasks -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-tasks text-green-400"></i>
|
||||
<strong>Tasks</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">Traçabilité complète de toutes les opérations d'automatisation.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Suivi d'État:</strong> Visualisation instantanée (En cours, Succès, Échec).</li>
|
||||
<li><strong>Filtrage Avancé:</strong> Recherche par statut, par date ou par type d'action.</li>
|
||||
<li><strong>Logs Détaillés:</strong> Accès aux sorties standard et d'erreur pour le débogage.</li>
|
||||
<li><strong>Auto-refresh:</strong> Mise à jour automatique des tâches en cours d'exécution.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedules -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-calendar-alt text-indigo-400"></i>
|
||||
<strong>Schedules</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">Planification automatisée des tâches récurrentes.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Planification Cron:</strong> Configuration flexible de la fréquence d'exécution.</li>
|
||||
<li><strong>Tâches Récurrentes:</strong> Backups quotidiens, Mises à jour hebdomadaires, Health Checks horaires.</li>
|
||||
<li><strong>Ciblage:</strong> Définition des hosts ou groupes cibles pour chaque planification.</li>
|
||||
<li><strong>Gestion:</strong> Activation, désactivation ou modification des planifications existantes.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-file-alt text-orange-400"></i>
|
||||
<strong>Logs</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">Journal technique des événements système.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Streaming WebSocket:</strong> Arrivée des logs en temps réel sans rechargement de page.</li>
|
||||
<li><strong>Niveaux de Log:</strong> Distinction claire entre Info, Warning et Error.</li>
|
||||
<li><strong>Export:</strong> Possibilité de télécharger les logs pour analyse externe.</li>
|
||||
<li><strong>Rétention:</strong> Gestion de l'historique et nettoyage des logs anciens.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alertes -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-bell text-red-400"></i>
|
||||
<strong>Alertes</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">Centre de messages pour les événements importants.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Suivi:</strong> Consultez les alertes récentes (succès/échec, changements d'état).</li>
|
||||
<li><strong>Lecture:</strong> Les alertes peuvent être marquées comme lues pour garder une boîte de réception propre.</li>
|
||||
<li><strong>Notifications:</strong> Certaines alertes peuvent déclencher des notifications ntfy (si activé).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span class="flex items-center gap-3">
|
||||
<i class="fas fa-cog text-cyan-400"></i>
|
||||
<strong>Configuration</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">Paramètres de l'application et intégrations.</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Paramètres applicatifs:</strong> Options persistées (ex: collecte des métriques).</li>
|
||||
<li><strong>Notifications:</strong> Configuration et test du service ntfy.</li>
|
||||
<li><strong>Sécurité:</strong> Gestion du compte utilisateur (mot de passe) via l'écran utilisateur.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notifications -->
|
||||
<div id="help-notifications" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">🔔</span>
|
||||
Système de Notifications (ntfy)
|
||||
</h2>
|
||||
<p class="text-gray-400 mb-6">
|
||||
Restez informé de l'état de votre infrastructure grâce au système de notifications intégré basé sur <strong>ntfy</strong>.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="help-card">
|
||||
<h3 class="font-semibold mb-3 text-purple-400">Canaux & Configuration</h3>
|
||||
<p class="text-gray-400 text-sm mb-4">
|
||||
Les notifications sont envoyées via le service ntfy, permettant de recevoir des alertes push sur mobile et desktop.
|
||||
</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><strong>Push Mobile:</strong> Via l'application ntfy (Android/iOS).</li>
|
||||
<li><strong>Web Push:</strong> Notifications navigateur sur desktop.</li>
|
||||
<li><strong>Priorité:</strong> Gestion des niveaux d'urgence (Low à High).</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="help-card">
|
||||
<h3 class="font-semibold mb-3 text-blue-400">Types d'Alertes</h3>
|
||||
<p class="text-gray-400 text-sm mb-4">
|
||||
Vous recevez des notifications pour les événements critiques :
|
||||
</p>
|
||||
<ul class="help-list text-sm">
|
||||
<li><span class="mr-2">✅</span>Succès des Backups</li>
|
||||
<li><span class="mr-2">❌</span>Échecs de Tâches</li>
|
||||
<li><span class="mr-2">⚠️</span>Changements de Santé Host</li>
|
||||
<li><span class="mr-2">🛠️</span>Fin de Bootstrap</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Playbooks Ansible -->
|
||||
<div id="help-playbooks" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">📖</span>
|
||||
Playbooks Ansible Disponibles
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="help-card">
|
||||
<h3 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-tools text-yellow-400"></i>
|
||||
bootstrap-host.yml
|
||||
</h3>
|
||||
<p class="text-gray-400 text-sm mb-2">
|
||||
Configure un nouveau host pour Ansible: création utilisateur, clé SSH, sudo sans mot de passe.
|
||||
</p>
|
||||
<span class="text-xs text-purple-400">Requis avant toute autre opération</span>
|
||||
</div>
|
||||
<div class="help-card">
|
||||
<h3 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-heartbeat text-green-400"></i>
|
||||
health-check.yml
|
||||
</h3>
|
||||
<p class="text-gray-400 text-sm mb-2">
|
||||
Vérifie l'état de santé: CPU, RAM, disque, services critiques.
|
||||
</p>
|
||||
<span class="text-xs text-purple-400">Exécution rapide, non destructif</span>
|
||||
</div>
|
||||
<div class="help-card">
|
||||
<h3 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-arrow-up text-blue-400"></i>
|
||||
system-upgrade.yml
|
||||
</h3>
|
||||
<p class="text-gray-400 text-sm mb-2">
|
||||
Met à jour tous les paquets système (apt/yum/dnf selon l'OS).
|
||||
</p>
|
||||
<span class="text-xs text-orange-400">Peut nécessiter un redémarrage</span>
|
||||
</div>
|
||||
<div class="help-card">
|
||||
<h3 class="font-semibold mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-save text-cyan-400"></i>
|
||||
backup-config.yml
|
||||
</h3>
|
||||
<p class="text-gray-400 text-sm mb-2">
|
||||
Sauvegarde les fichiers de configuration importants (/etc, configs apps).
|
||||
</p>
|
||||
<span class="text-xs text-purple-400">Stockage local ou distant</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Reference -->
|
||||
<div id="help-api" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">🔗</span>
|
||||
Référence API
|
||||
</h2>
|
||||
<p class="text-gray-400 mb-6">
|
||||
L'API REST est accessible sur le port configuré. Authentification via header <span class="help-code">Authorization: Bearer <token></span>.
|
||||
</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-700">
|
||||
<th class="text-left py-3 px-4 text-purple-400">Endpoint</th>
|
||||
<th class="text-left py-3 px-4 text-purple-400">Méthode</th>
|
||||
<th class="text-left py-3 px-4 text-purple-400">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-gray-300">
|
||||
<tr class="border-b border-gray-800">
|
||||
<td class="py-3 px-4"><span class="help-code">/api/hosts</span></td>
|
||||
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
|
||||
<td class="py-3 px-4">Liste tous les hosts</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-800">
|
||||
<td class="py-3 px-4"><span class="help-code">/api/hosts</span></td>
|
||||
<td class="py-3 px-4"><span class="text-blue-400">POST</span></td>
|
||||
<td class="py-3 px-4">Ajoute un nouveau host</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-800">
|
||||
<td class="py-3 px-4"><span class="help-code">/api/tasks/logs</span></td>
|
||||
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
|
||||
<td class="py-3 px-4">Récupère les logs de tâches</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-800">
|
||||
<td class="py-3 px-4"><span class="help-code">/api/ansible/playbooks</span></td>
|
||||
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
|
||||
<td class="py-3 px-4">Liste les playbooks disponibles</td>
|
||||
</tr>
|
||||
<tr class="border-b border-gray-800">
|
||||
<td class="py-3 px-4"><span class="help-code">/api/ansible/execute</span></td>
|
||||
<td class="py-3 px-4"><span class="text-blue-400">POST</span></td>
|
||||
<td class="py-3 px-4">Exécute un playbook</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-3 px-4"><span class="help-code">/api/metrics</span></td>
|
||||
<td class="py-3 px-4"><span class="text-green-400">GET</span></td>
|
||||
<td class="py-3 px-4">Métriques du dashboard</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Troubleshooting -->
|
||||
<div id="help-troubleshooting" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">🛠️</span>
|
||||
Dépannage
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span>Le bootstrap échoue avec "Permission denied"</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="mb-2"><strong>Cause:</strong> Les identifiants SSH fournis sont incorrects ou l'utilisateur n'a pas les droits sudo.</p>
|
||||
<p><strong>Solution:</strong> Vérifiez le nom d'utilisateur et mot de passe. Assurez-vous que l'utilisateur peut exécuter <span class="help-code">sudo</span> sur le host cible.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span>Les hosts apparaissent "offline" alors qu'ils sont accessibles</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="mb-2"><strong>Cause:</strong> Le health check n'a pas été exécuté ou la clé SSH n'est pas configurée.</p>
|
||||
<p><strong>Solution:</strong> Exécutez le bootstrap si ce n'est pas fait, puis lancez un Health Check.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span>Les tâches restent bloquées "En cours"</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="mb-2"><strong>Cause:</strong> Le processus Ansible peut être bloqué ou le host ne répond plus.</p>
|
||||
<p><strong>Solution:</strong> Vérifiez la connectivité réseau. Consultez les logs système pour plus de détails. Redémarrez le conteneur Docker si nécessaire.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item" onclick="toggleAccordion(this)">
|
||||
<div class="accordion-header">
|
||||
<span>L'interface ne se met pas à jour en temps réel</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="mb-2"><strong>Cause:</strong> La connexion WebSocket est interrompue.</p>
|
||||
<p><strong>Solution:</strong> Rafraîchissez la page. Vérifiez que le port WebSocket n'est pas bloqué par un firewall ou proxy.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard Shortcuts -->
|
||||
<div id="help-shortcuts" class="glass-card p-8 fade-in help-section-anchor">
|
||||
<h2 class="help-section-title">
|
||||
<span class="text-2xl mr-2">✨</span>
|
||||
Raccourcis & Astuces
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4 text-purple-400">Navigation</h3>
|
||||
<ul class="help-list text-sm">
|
||||
<li>Cliquez sur le logo pour revenir au Dashboard</li>
|
||||
<li>Utilisez les onglets du menu pour naviguer</li>
|
||||
<li>Le thème clair/sombre est persistant</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4 text-purple-400">Productivité</h3>
|
||||
<ul class="help-list text-sm">
|
||||
<li>Filtrez les hosts par groupe pour des actions groupées</li>
|
||||
<li>Utilisez les filtres de date pour retrouver des tâches</li>
|
||||
<li>Exportez les logs avant de les effacer</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- Fin help-main-content -->
|
||||
</div><!-- Fin flex container -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -4532,6 +3834,62 @@ homelab-automation/
|
||||
targetPage.querySelectorAll('.fade-in').forEach(el => {
|
||||
el.classList.add('visible');
|
||||
});
|
||||
|
||||
// Charger dynamiquement le contenu d'aide si nécessaire
|
||||
if (pageName === 'help') {
|
||||
loadHelpContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chargement dynamique du contenu d'aide depuis help.md
|
||||
let helpContentLoaded = false;
|
||||
async function loadHelpContent() {
|
||||
if (helpContentLoaded) return;
|
||||
|
||||
const contentContainer = document.getElementById('help-dynamic-content');
|
||||
const tocNav = document.getElementById('help-toc-nav');
|
||||
|
||||
if (!contentContainer) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/help/content', {
|
||||
headers: window.dashboard ? window.dashboard.getAuthHeaders() : {}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erreur de chargement');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Injecter le contenu
|
||||
contentContainer.innerHTML = data.content;
|
||||
|
||||
// Injecter la TOC
|
||||
if (tocNav && data.toc) {
|
||||
tocNav.innerHTML = data.toc;
|
||||
}
|
||||
|
||||
// Marquer comme chargé
|
||||
helpContentLoaded = true;
|
||||
|
||||
// Réinitialiser les animations fade-in
|
||||
contentContainer.querySelectorAll('.fade-in, .glass-card').forEach(el => {
|
||||
el.classList.add('visible');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement aide:', error);
|
||||
contentContainer.innerHTML = `
|
||||
<div class="glass-card p-8 mb-8 text-center">
|
||||
<i class="fas fa-exclamation-triangle text-4xl text-yellow-400 mb-4"></i>
|
||||
<p class="text-gray-400 mb-4">Impossible de charger la documentation.</p>
|
||||
<button onclick="helpContentLoaded = false; loadHelpContent();" class="px-4 py-2 bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors">
|
||||
<i class="fas fa-redo mr-2"></i>Réessayer
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,30 +1,46 @@
|
||||
"""
|
||||
Routes API pour l'aide et la documentation.
|
||||
Source unique: app/static/help.md
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import Response
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.dependencies import verify_api_key
|
||||
from app.utils.markdown_parser import build_help_markdown
|
||||
from app.utils.help_renderer import render_help_page, get_raw_markdown
|
||||
from app.utils.pdf_generator import markdown_to_pdf_bytes
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Chemin vers le fichier source Markdown
|
||||
HELP_MD_PATH = settings.base_dir / "static" / "help.md"
|
||||
|
||||
|
||||
@router.get("/content")
|
||||
async def get_help_content(api_key_valid: bool = Depends(verify_api_key)):
|
||||
"""
|
||||
Retourne le contenu HTML de la page d'aide généré depuis help.md.
|
||||
Utilisé pour le chargement dynamique de la page d'aide.
|
||||
"""
|
||||
html_content, toc_html = render_help_page(HELP_MD_PATH)
|
||||
|
||||
return JSONResponse({
|
||||
"content": html_content,
|
||||
"toc": toc_html
|
||||
})
|
||||
|
||||
|
||||
@router.get("/documentation.md")
|
||||
async def download_help_markdown(api_key_valid: bool = Depends(verify_api_key)):
|
||||
"""Télécharge la documentation d'aide en format Markdown."""
|
||||
# Essayer de charger depuis index.html
|
||||
html_path = settings.base_dir / "index.html"
|
||||
markdown_content = build_help_markdown(html_path=html_path)
|
||||
markdown_content = get_raw_markdown(HELP_MD_PATH)
|
||||
|
||||
return Response(
|
||||
content=markdown_content,
|
||||
media_type="text/markdown",
|
||||
media_type="text/markdown; charset=utf-8",
|
||||
headers={
|
||||
"Content-Disposition": "attachment; filename=homelab-automation-help.md"
|
||||
}
|
||||
@ -34,9 +50,7 @@ async def download_help_markdown(api_key_valid: bool = Depends(verify_api_key)):
|
||||
@router.get("/documentation.pdf")
|
||||
async def download_help_pdf(api_key_valid: bool = Depends(verify_api_key)):
|
||||
"""Télécharge la documentation d'aide en format PDF."""
|
||||
# Essayer de charger depuis index.html
|
||||
html_path = settings.base_dir / "index.html"
|
||||
markdown_content = build_help_markdown(html_path=html_path)
|
||||
markdown_content = get_raw_markdown(HELP_MD_PATH)
|
||||
|
||||
pdf_bytes = markdown_to_pdf_bytes(
|
||||
markdown_content,
|
||||
|
||||
317
app/static/help.md
Normal file
317
app/static/help.md
Normal file
@ -0,0 +1,317 @@
|
||||
# 🚀 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.
|
||||
|
||||
---
|
||||
|
||||
## ⚡️ Démarrage Rapide {#help-quickstart}
|
||||
|
||||
<!-- quickstart-cards -->
|
||||
:::card green fa-1
|
||||
### Ajouter vos Hosts
|
||||
Commencez par ajouter vos serveurs dans la section `Hosts`. Chaque host nécessite un nom, une adresse IP et un système d'exploitation.
|
||||
:::
|
||||
|
||||
:::card blue fa-2
|
||||
### Bootstrap Ansible
|
||||
Exécutez le `Bootstrap` sur chaque host pour configurer l'accès SSH et les prérequis Ansible.
|
||||
:::
|
||||
|
||||
:::card purple fa-3
|
||||
### Automatiser
|
||||
Utilisez les `Actions Rapides` ou exécutez des playbooks personnalisés pour automatiser vos tâches.
|
||||
:::
|
||||
<!-- /quickstart-cards -->
|
||||
|
||||
---
|
||||
|
||||
## ❤️🩹 Indicateurs de Santé des Hosts {#help-indicators}
|
||||
|
||||
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
|
||||
|
||||
:::health-level excellent 5 green
|
||||
**Excellent** (5 barres vertes)
|
||||
Host en ligne, bootstrap OK, vérifié récemment
|
||||
:::
|
||||
|
||||
:::health-level good 3 yellow
|
||||
**Bon** (3-4 barres jaunes)
|
||||
Host fonctionnel mais certains aspects à améliorer
|
||||
:::
|
||||
|
||||
:::health-level medium 2 orange
|
||||
**Moyen** (2 barres oranges)
|
||||
Attention requise - vérification recommandée
|
||||
:::
|
||||
|
||||
:::health-level low 1 red
|
||||
**Faible** (1 barre rouge)
|
||||
Host hors ligne ou non configuré
|
||||
:::
|
||||
|
||||
### Facteurs de Calcul du Score
|
||||
|
||||
:::score-factor fa-circle green +2 points
|
||||
**Statut en ligne**
|
||||
Le host répond aux requêtes réseau
|
||||
:::
|
||||
|
||||
:::score-factor fa-check-circle blue +1 point
|
||||
**Bootstrap Ansible OK**
|
||||
SSH et prérequis Ansible configurés
|
||||
:::
|
||||
|
||||
:::score-factor fa-clock purple +2 points
|
||||
**Vérifié récemment (<1h)**
|
||||
Dernière vérification il y a moins d'une heure
|
||||
:::
|
||||
|
||||
:::score-factor fa-clock yellow +1 point
|
||||
**Vérifié aujourd'hui**
|
||||
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
|
||||
|
||||
:::status-badge green fa-check-circle 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.
|
||||
:::
|
||||
|
||||
:::status-badge yellow fa-exclamation-triangle 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 {#help-architecture}
|
||||
|
||||
### Stack Technologique
|
||||
|
||||
- **Backend:** FastAPI (Python) - API REST haute performance {icon:fa-server color:green}
|
||||
- **Automation:** Ansible - Gestion de configuration {icon:fa-cogs color:orange}
|
||||
- **Frontend:** HTML/CSS/JS avec TailwindCSS {icon:fa-desktop color:blue}
|
||||
- **Déploiement:** Docker & Docker Compose {icon:fab fa-docker color:cyan}
|
||||
- **Temps réel:** WebSocket pour les mises à jour live {icon:fa-plug color:yellow}
|
||||
|
||||
### 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 Détaillées par Section {#help-features}
|
||||
|
||||
:::accordion fa-tachometer-alt purple 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.
|
||||
:::
|
||||
|
||||
:::accordion fa-server blue 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.
|
||||
:::
|
||||
|
||||
:::accordion fa-book pink 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.
|
||||
:::
|
||||
|
||||
:::accordion fa-tasks green 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.
|
||||
:::
|
||||
|
||||
:::accordion fa-calendar-alt indigo 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.
|
||||
:::
|
||||
|
||||
:::accordion fa-file-alt orange 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.
|
||||
:::
|
||||
|
||||
:::accordion fa-bell red 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é).
|
||||
:::
|
||||
|
||||
:::accordion fa-cog cyan 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) {#help-notifications}
|
||||
|
||||
Restez informé de l'état de votre infrastructure grâce au système de notifications intégré basé sur **ntfy**.
|
||||
|
||||
### Canaux & 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 {#help-playbooks}
|
||||
|
||||
:::playbook fa-tools yellow bootstrap-host.yml
|
||||
Configure un nouveau host pour Ansible: création utilisateur, clé SSH, sudo sans mot de passe.
|
||||
**Requis avant toute autre opération**
|
||||
:::
|
||||
|
||||
:::playbook fa-heartbeat green health-check.yml
|
||||
Vérifie l'état de santé: CPU, RAM, disque, services critiques.
|
||||
**Exécution rapide, non destructif**
|
||||
:::
|
||||
|
||||
:::playbook fa-arrow-up blue system-upgrade.yml
|
||||
Met à jour tous les paquets système (apt/yum/dnf selon l'OS).
|
||||
**Peut nécessiter un redémarrage**
|
||||
:::
|
||||
|
||||
:::playbook fa-save cyan backup-config.yml
|
||||
Sauvegarde les fichiers de configuration importants (/etc, configs apps).
|
||||
**Stockage local ou distant**
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Référence API {#help-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 {#help-troubleshooting}
|
||||
|
||||
:::troubleshoot 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.
|
||||
:::
|
||||
|
||||
:::troubleshoot 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.
|
||||
:::
|
||||
|
||||
:::troubleshoot 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.
|
||||
:::
|
||||
|
||||
:::troubleshoot 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 & Astuces {#help-shortcuts}
|
||||
|
||||
### 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*
|
||||
@ -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",
|
||||
]
|
||||
|
||||
520
app/utils/help_renderer.py
Normal file
520
app/utils/help_renderer.py
Normal file
@ -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*
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
@ -193,7 +193,7 @@ projet/
|
||||
│ ├── utils/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── pdf_generator.py # _markdown_to_pdf_bytes
|
||||
│ │ ├── markdown_parser.py # _HelpHtmlToMarkdownParser
|
||||
│ │ ├── help_renderer.py # help.md -> HTML + markdown clean
|
||||
│ │ ├── ssh_utils.py # find_ssh_private_key, run_ssh_command
|
||||
│ │ └── helpers.py # Fonctions utilitaires diverses
|
||||
│ │
|
||||
@ -239,7 +239,7 @@ projet/
|
||||
| Endpoints Health | 5902-6112 | `app/routes/health.py` |
|
||||
| Endpoints Schedules | 6388-6949 | `app/routes/schedules.py` |
|
||||
| Endpoints Notifications | 6952-7022 | `app/routes/notifications.py` |
|
||||
| PDF/Markdown utils | 105-548 | `app/utils/pdf_generator.py`, `app/utils/markdown_parser.py` |
|
||||
| PDF/Markdown utils | 105-548 | `app/utils/pdf_generator.py`, `app/utils/help_renderer.py` |
|
||||
| SSH utils | 3581-3673 | `app/utils/ssh_utils.py` |
|
||||
| Startup/Shutdown | 7025-7098 | `app/__init__.py` (create_app) |
|
||||
|
||||
@ -276,7 +276,7 @@ projet/
|
||||
### Étape 4: Utils
|
||||
1. Créer `app/utils/__init__.py`
|
||||
2. Créer `app/utils/pdf_generator.py`
|
||||
3. Créer `app/utils/markdown_parser.py`
|
||||
3. Créer `app/utils/help_renderer.py`
|
||||
4. Créer `app/utils/ssh_utils.py`
|
||||
5. Créer `app/utils/helpers.py`
|
||||
|
||||
@ -392,7 +392,7 @@ projet/
|
||||
│ ├── __init__.py # ✅
|
||||
│ ├── ssh_utils.py # ✅ SSH & Bootstrap
|
||||
│ ├── pdf_generator.py # ✅ Markdown to PDF
|
||||
│ └── markdown_parser.py # ✅ HTML to Markdown
|
||||
│ └── help_renderer.py # ✅ help.md -> HTML + markdown clean
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -6,11 +6,11 @@ and that the PDF generator returns a non-empty PDF payload.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# Ensure project root on path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
@ -18,10 +18,16 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_help_markdown_builder_exists_and_non_empty():
|
||||
from app import app_optimized # type: ignore
|
||||
from app import create_app
|
||||
|
||||
assert hasattr(app_optimized, "_build_help_markdown")
|
||||
md = app_optimized._build_help_markdown()
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
resp = client.get(
|
||||
"/api/help/documentation.md",
|
||||
headers={"X-API-Key": "dev-key-1234567890"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
md = resp.text
|
||||
assert isinstance(md, str)
|
||||
assert len(md) > 100
|
||||
assert "Guide d'Utilisation" in md or "Démarrage Rapide" in md
|
||||
@ -31,19 +37,42 @@ async def test_help_markdown_builder_exists_and_non_empty():
|
||||
async def test_help_pdf_generator_returns_pdf_bytes():
|
||||
pytest.importorskip("reportlab")
|
||||
pytest.importorskip("PIL")
|
||||
from app import app_optimized # type: ignore
|
||||
|
||||
md = app_optimized._build_help_markdown()
|
||||
pdf_bytes = app_optimized._markdown_to_pdf_bytes(md)
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
resp = client.get(
|
||||
"/api/help/documentation.pdf",
|
||||
headers={"X-API-Key": "dev-key-1234567890"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
pdf_bytes = resp.content
|
||||
assert isinstance(pdf_bytes, (bytes, bytearray))
|
||||
assert len(pdf_bytes) > 1000
|
||||
assert bytes(pdf_bytes[:4]) == b"%PDF"
|
||||
|
||||
|
||||
def test_help_routes_registered():
|
||||
from app.app_optimized import app # type: ignore
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
paths = {r.path for r in app.routes}
|
||||
assert "/api/help/content" in paths
|
||||
assert "/api/help/documentation.md" in paths
|
||||
assert "/api/help/documentation.pdf" in paths
|
||||
|
||||
|
||||
def test_help_content_endpoint_returns_html_and_toc():
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
client = TestClient(app)
|
||||
resp = client.get(
|
||||
"/api/help/content",
|
||||
headers={"X-API-Key": "dev-key-1234567890"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data.get("content"), str)
|
||||
assert isinstance(data.get("toc"), str)
|
||||
assert "help-quickstart" in data["toc"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user