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 import FastAPI, HTTPException, Depends, Request, Form, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
|
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
|
||||||
from fastapi.security import APIKeyHeader
|
from fastapi.security import APIKeyHeader
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from html.parser import HTMLParser
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from xml.sax.saxutils import escape as _xml_escape
|
from xml.sax.saxutils import escape as _xml_escape
|
||||||
import hashlib
|
|
||||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||||
from sqlalchemy import select
|
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.task import TaskRepository # type: ignore
|
||||||
from app.crud.schedule import ScheduleRepository # type: ignore
|
from app.crud.schedule import ScheduleRepository # type: ignore
|
||||||
from app.crud.schedule_run import ScheduleRunRepository # 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
|
from app.schemas.notification import NotificationRequest, NotificationResponse # type: ignore
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
@ -102,452 +97,6 @@ ACTION_PLAYBOOK_MAP = {
|
|||||||
# Gestionnaire de clés API
|
# Gestionnaire de clés API
|
||||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
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
|
# Modèles Pydantic améliorés
|
||||||
class CommandResult(BaseModel):
|
class CommandResult(BaseModel):
|
||||||
status: str
|
status: str
|
||||||
@ -668,14 +217,12 @@ class HostRequest(BaseModel):
|
|||||||
env_group: str = Field(..., description="Groupe d'environnement (ex: env_homelab, env_prod)")
|
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)")
|
role_groups: List[str] = Field(default=[], description="Groupes de rôles (ex: role_proxmox, role_sbc)")
|
||||||
|
|
||||||
|
|
||||||
class HostUpdateRequest(BaseModel):
|
class HostUpdateRequest(BaseModel):
|
||||||
"""Requête de mise à jour d'un hôte"""
|
"""Requête de mise à jour d'un hôte"""
|
||||||
env_group: Optional[str] = Field(default=None, description="Nouveau groupe d'environnement")
|
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")
|
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")
|
ansible_host: Optional[str] = Field(default=None, description="Nouvelle adresse ansible_host")
|
||||||
|
|
||||||
|
|
||||||
class GroupRequest(BaseModel):
|
class GroupRequest(BaseModel):
|
||||||
"""Requête pour créer un groupe"""
|
"""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)")
|
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'")
|
raise ValueError("Le type doit être 'env' ou 'role'")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class GroupUpdateRequest(BaseModel):
|
class GroupUpdateRequest(BaseModel):
|
||||||
"""Requête pour modifier un groupe"""
|
"""Requête pour modifier un groupe"""
|
||||||
new_name: str = Field(..., min_length=3, max_length=50, description="Nouveau nom du 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')
|
raise ValueError('Le nom du groupe ne peut contenir que des lettres, chiffres, tirets et underscores')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class GroupDeleteRequest(BaseModel):
|
class GroupDeleteRequest(BaseModel):
|
||||||
"""Requête pour supprimer un groupe"""
|
"""Requête pour supprimer un groupe"""
|
||||||
move_hosts_to: Optional[str] = Field(default=None, description="Groupe vers lequel déplacer les hôtes")
|
move_hosts_to: Optional[str] = Field(default=None, description="Groupe vers lequel déplacer les hôtes")
|
||||||
|
|
||||||
|
|
||||||
class AdHocCommandRequest(BaseModel):
|
class AdHocCommandRequest(BaseModel):
|
||||||
"""Requête pour exécuter une commande ad-hoc Ansible"""
|
"""Requête pour exécuter une commande ad-hoc Ansible"""
|
||||||
target: str = Field(..., description="Hôte ou groupe cible")
|
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")
|
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")
|
category: Optional[str] = Field(default="default", description="Catégorie d'historique pour cette commande")
|
||||||
|
|
||||||
|
|
||||||
class AdHocCommandResult(BaseModel):
|
class AdHocCommandResult(BaseModel):
|
||||||
"""Résultat d'une commande ad-hoc"""
|
"""Résultat d'une commande ad-hoc"""
|
||||||
target: str
|
target: str
|
||||||
@ -736,7 +279,6 @@ class AdHocCommandResult(BaseModel):
|
|||||||
duration: float
|
duration: float
|
||||||
hosts_results: Optional[Dict[str, Any]] = None
|
hosts_results: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
class AdHocHistoryEntry(BaseModel):
|
class AdHocHistoryEntry(BaseModel):
|
||||||
"""Entrée dans l'historique des commandes ad-hoc"""
|
"""Entrée dans l'historique des commandes ad-hoc"""
|
||||||
id: str
|
id: str
|
||||||
@ -750,7 +292,6 @@ class AdHocHistoryEntry(BaseModel):
|
|||||||
last_used: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
last_used: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
use_count: int = 1
|
use_count: int = 1
|
||||||
|
|
||||||
|
|
||||||
class AdHocHistoryCategory(BaseModel):
|
class AdHocHistoryCategory(BaseModel):
|
||||||
"""Catégorie pour organiser les commandes ad-hoc"""
|
"""Catégorie pour organiser les commandes ad-hoc"""
|
||||||
name: str
|
name: str
|
||||||
@ -758,7 +299,6 @@ class AdHocHistoryCategory(BaseModel):
|
|||||||
color: str = "#7c3aed"
|
color: str = "#7c3aed"
|
||||||
icon: str = "fa-folder"
|
icon: str = "fa-folder"
|
||||||
|
|
||||||
|
|
||||||
class TaskLogFile(BaseModel):
|
class TaskLogFile(BaseModel):
|
||||||
"""Représentation d'un fichier de log de tâche"""
|
"""Représentation d'un fichier de log de tâche"""
|
||||||
id: str
|
id: str
|
||||||
@ -784,7 +324,6 @@ class TaskLogFile(BaseModel):
|
|||||||
target_type: Optional[str] = None # Type de cible: 'host', 'group', 'role'
|
target_type: Optional[str] = None # Type de cible: 'host', 'group', 'role'
|
||||||
source_type: Optional[str] = None # Source: 'scheduled', 'manual', 'adhoc'
|
source_type: Optional[str] = None # Source: 'scheduled', 'manual', 'adhoc'
|
||||||
|
|
||||||
|
|
||||||
class TasksFilterParams(BaseModel):
|
class TasksFilterParams(BaseModel):
|
||||||
"""Paramètres de filtrage des tâches"""
|
"""Paramètres de filtrage des tâches"""
|
||||||
status: Optional[str] = None # pending, running, completed, failed, all
|
status: Optional[str] = None # pending, running, completed, failed, all
|
||||||
@ -799,7 +338,6 @@ class TasksFilterParams(BaseModel):
|
|||||||
limit: int = 50 # Pagination côté serveur
|
limit: int = 50 # Pagination côté serveur
|
||||||
offset: int = 0
|
offset: int = 0
|
||||||
|
|
||||||
|
|
||||||
# ===== MODÈLES PLANIFICATEUR (SCHEDULER) =====
|
# ===== MODÈLES PLANIFICATEUR (SCHEDULER) =====
|
||||||
|
|
||||||
class ScheduleRecurrence(BaseModel):
|
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")
|
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")
|
cron_expression: Optional[str] = Field(default=None, description="Expression cron pour custom")
|
||||||
|
|
||||||
|
|
||||||
class Schedule(BaseModel):
|
class Schedule(BaseModel):
|
||||||
"""Modèle d'un schedule de playbook"""
|
"""Modèle d'un schedule de playbook"""
|
||||||
id: str = Field(default_factory=lambda: f"sched_{uuid.uuid4().hex[:12]}")
|
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
|
# Si schedule_type est 'once', recurrence n'est pas obligatoire
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class ScheduleRun(BaseModel):
|
class ScheduleRun(BaseModel):
|
||||||
"""Historique d'une exécution de schedule"""
|
"""Historique d'une exécution de schedule"""
|
||||||
id: str = Field(default_factory=lambda: f"run_{uuid.uuid4().hex[:12]}")
|
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
|
datetime: lambda v: v.isoformat() if v else None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ScheduleCreateRequest(BaseModel):
|
class ScheduleCreateRequest(BaseModel):
|
||||||
"""Requête de création d'un schedule"""
|
"""Requête de création d'un schedule"""
|
||||||
name: str = Field(..., min_length=3, max_length=100)
|
name: str = Field(..., min_length=3, max_length=100)
|
||||||
@ -898,7 +433,6 @@ class ScheduleCreateRequest(BaseModel):
|
|||||||
except pytz.exceptions.UnknownTimeZoneError:
|
except pytz.exceptions.UnknownTimeZoneError:
|
||||||
raise ValueError(f"Fuseau horaire invalide: {v}")
|
raise ValueError(f"Fuseau horaire invalide: {v}")
|
||||||
|
|
||||||
|
|
||||||
class ScheduleUpdateRequest(BaseModel):
|
class ScheduleUpdateRequest(BaseModel):
|
||||||
"""Requête de mise à jour d'un schedule"""
|
"""Requête de mise à jour d'un schedule"""
|
||||||
name: Optional[str] = Field(default=None, min_length=3, max_length=100)
|
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)
|
notification_type: Optional[Literal["none", "all", "errors"]] = Field(default=None)
|
||||||
tags: Optional[List[str]] = Field(default=None)
|
tags: Optional[List[str]] = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class ScheduleStats(BaseModel):
|
class ScheduleStats(BaseModel):
|
||||||
"""Statistiques globales des schedules"""
|
"""Statistiques globales des schedules"""
|
||||||
total: int = 0
|
total: int = 0
|
||||||
@ -931,7 +464,6 @@ class ScheduleStats(BaseModel):
|
|||||||
executions_24h: int = 0
|
executions_24h: int = 0
|
||||||
success_rate_7d: float = 0.0
|
success_rate_7d: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
# ===== SERVICE DE LOGGING MARKDOWN =====
|
# ===== SERVICE DE LOGGING MARKDOWN =====
|
||||||
|
|
||||||
class TaskLogService:
|
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")
|
raise HTTPException(status_code=401, detail="Clé API invalide ou manquante")
|
||||||
return True
|
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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def root(request: Request):
|
async def root(request: Request):
|
||||||
"""Page principale du dashboard"""
|
"""Page principale du dashboard"""
|
||||||
|
|||||||
776
app/index.html
776
app/index.html
@ -3727,726 +3727,28 @@
|
|||||||
|
|
||||||
<!-- Layout avec Table des Matières -->
|
<!-- Layout avec Table des Matières -->
|
||||||
<div class="flex gap-8">
|
<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">
|
<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>
|
<div class="help-toc-title">Table des Matières</div>
|
||||||
<nav>
|
<nav id="help-toc-nav">
|
||||||
<a href="#help-quickstart" class="help-toc-item" onclick="scrollToHelpSection(event, 'help-quickstart')">
|
<!-- TOC chargée dynamiquement depuis help.md -->
|
||||||
<span class="mr-2">⚡️</span>Démarrage Rapide
|
<div class="text-gray-500 text-sm py-2">Chargement...</div>
|
||||||
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Contenu Principal -->
|
<!-- Contenu Principal - Chargé dynamiquement depuis help.md -->
|
||||||
<div class="flex-1 help-main-content">
|
<div id="help-dynamic-content" class="flex-1 help-main-content">
|
||||||
|
<!-- Placeholder pendant le chargement -->
|
||||||
<!-- Quick Start -->
|
<div class="glass-card p-8 mb-8 text-center">
|
||||||
<div id="help-quickstart" class="glass-card p-8 mb-8 fade-in help-section-anchor">
|
<div class="loading-spinner mx-auto mb-4"></div>
|
||||||
<h2 class="help-section-title">
|
<p class="text-gray-400">Chargement de la documentation...</p>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
<!-- END PAGE: AIDE -->
|
<!-- END PAGE: AIDE -->
|
||||||
|
|
||||||
@ -4532,6 +3834,62 @@ homelab-automation/
|
|||||||
targetPage.querySelectorAll('.fade-in').forEach(el => {
|
targetPage.querySelectorAll('.fade-in').forEach(el => {
|
||||||
el.classList.add('visible');
|
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.
|
Routes API pour l'aide et la documentation.
|
||||||
|
Source unique: app/static/help.md
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
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.config import settings
|
||||||
from app.core.dependencies import verify_api_key
|
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
|
from app.utils.pdf_generator import markdown_to_pdf_bytes
|
||||||
|
|
||||||
router = APIRouter()
|
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")
|
@router.get("/documentation.md")
|
||||||
async def download_help_markdown(api_key_valid: bool = Depends(verify_api_key)):
|
async def download_help_markdown(api_key_valid: bool = Depends(verify_api_key)):
|
||||||
"""Télécharge la documentation d'aide en format Markdown."""
|
"""Télécharge la documentation d'aide en format Markdown."""
|
||||||
# Essayer de charger depuis index.html
|
markdown_content = get_raw_markdown(HELP_MD_PATH)
|
||||||
html_path = settings.base_dir / "index.html"
|
|
||||||
markdown_content = build_help_markdown(html_path=html_path)
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
content=markdown_content,
|
content=markdown_content,
|
||||||
media_type="text/markdown",
|
media_type="text/markdown; charset=utf-8",
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": "attachment; filename=homelab-automation-help.md"
|
"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")
|
@router.get("/documentation.pdf")
|
||||||
async def download_help_pdf(api_key_valid: bool = Depends(verify_api_key)):
|
async def download_help_pdf(api_key_valid: bool = Depends(verify_api_key)):
|
||||||
"""Télécharge la documentation d'aide en format PDF."""
|
"""Télécharge la documentation d'aide en format PDF."""
|
||||||
# Essayer de charger depuis index.html
|
markdown_content = get_raw_markdown(HELP_MD_PATH)
|
||||||
html_path = settings.base_dir / "index.html"
|
|
||||||
markdown_content = build_help_markdown(html_path=html_path)
|
|
||||||
|
|
||||||
pdf_bytes = markdown_to_pdf_bytes(
|
pdf_bytes = markdown_to_pdf_bytes(
|
||||||
markdown_content,
|
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.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.pdf_generator import markdown_to_pdf_bytes
|
||||||
from app.utils.markdown_parser import build_help_markdown
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"find_ssh_private_key",
|
"find_ssh_private_key",
|
||||||
"run_ssh_command",
|
"run_ssh_command",
|
||||||
"bootstrap_host",
|
"bootstrap_host",
|
||||||
"markdown_to_pdf_bytes",
|
"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/
|
│ ├── utils/
|
||||||
│ │ ├── __init__.py
|
│ │ ├── __init__.py
|
||||||
│ │ ├── pdf_generator.py # _markdown_to_pdf_bytes
|
│ │ ├── 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
|
│ │ ├── ssh_utils.py # find_ssh_private_key, run_ssh_command
|
||||||
│ │ └── helpers.py # Fonctions utilitaires diverses
|
│ │ └── helpers.py # Fonctions utilitaires diverses
|
||||||
│ │
|
│ │
|
||||||
@ -239,7 +239,7 @@ projet/
|
|||||||
| Endpoints Health | 5902-6112 | `app/routes/health.py` |
|
| Endpoints Health | 5902-6112 | `app/routes/health.py` |
|
||||||
| Endpoints Schedules | 6388-6949 | `app/routes/schedules.py` |
|
| Endpoints Schedules | 6388-6949 | `app/routes/schedules.py` |
|
||||||
| Endpoints Notifications | 6952-7022 | `app/routes/notifications.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` |
|
| SSH utils | 3581-3673 | `app/utils/ssh_utils.py` |
|
||||||
| Startup/Shutdown | 7025-7098 | `app/__init__.py` (create_app) |
|
| Startup/Shutdown | 7025-7098 | `app/__init__.py` (create_app) |
|
||||||
|
|
||||||
@ -276,7 +276,7 @@ projet/
|
|||||||
### Étape 4: Utils
|
### Étape 4: Utils
|
||||||
1. Créer `app/utils/__init__.py`
|
1. Créer `app/utils/__init__.py`
|
||||||
2. Créer `app/utils/pdf_generator.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`
|
4. Créer `app/utils/ssh_utils.py`
|
||||||
5. Créer `app/utils/helpers.py`
|
5. Créer `app/utils/helpers.py`
|
||||||
|
|
||||||
@ -392,7 +392,7 @@ projet/
|
|||||||
│ ├── __init__.py # ✅
|
│ ├── __init__.py # ✅
|
||||||
│ ├── ssh_utils.py # ✅ SSH & Bootstrap
|
│ ├── ssh_utils.py # ✅ SSH & Bootstrap
|
||||||
│ ├── pdf_generator.py # ✅ Markdown to PDF
|
│ ├── 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
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
# Ensure project root on path
|
# Ensure project root on path
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_help_markdown_builder_exists_and_non_empty():
|
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")
|
app = create_app()
|
||||||
md = app_optimized._build_help_markdown()
|
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 isinstance(md, str)
|
||||||
assert len(md) > 100
|
assert len(md) > 100
|
||||||
assert "Guide d'Utilisation" in md or "Démarrage Rapide" in md
|
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():
|
async def test_help_pdf_generator_returns_pdf_bytes():
|
||||||
pytest.importorskip("reportlab")
|
pytest.importorskip("reportlab")
|
||||||
pytest.importorskip("PIL")
|
pytest.importorskip("PIL")
|
||||||
from app import app_optimized # type: ignore
|
from app import create_app
|
||||||
|
|
||||||
md = app_optimized._build_help_markdown()
|
|
||||||
pdf_bytes = app_optimized._markdown_to_pdf_bytes(md)
|
|
||||||
|
|
||||||
|
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 isinstance(pdf_bytes, (bytes, bytearray))
|
||||||
assert len(pdf_bytes) > 1000
|
assert len(pdf_bytes) > 1000
|
||||||
assert bytes(pdf_bytes[:4]) == b"%PDF"
|
assert bytes(pdf_bytes[:4]) == b"%PDF"
|
||||||
|
|
||||||
|
|
||||||
def test_help_routes_registered():
|
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}
|
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.md" in paths
|
||||||
assert "/api/help/documentation.pdf" 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