"""Aperçu design hors-ligne : rend dashboard.html avec des données fictives en PNG paysage (1680x1264, niveaux de gris, sans rotation), pour itérer sur le design sans dépendre des intégrations réseau (Claude, météo, NAS, HA). Usage: .venv/bin/python ../dev/preview.py [sortie.png] """ from __future__ import annotations import asyncio import io import sys from pathlib import Path from jinja2 import Environment, FileSystemLoader, select_autoescape from PIL import Image from playwright.async_api import async_playwright BACKEND = Path(__file__).resolve().parent.parent / "backend" TEMPLATES = BACKEND / "templates" WIDTH, HEIGHT = 1680, 1264 sys.path.insert(0, str(BACKEND)) from fonts import font_face_css # noqa: E402 env = Environment( loader=FileSystemLoader(str(TEMPLATES)), autoescape=select_autoescape(["html"]), ) CTX = { "width": WIDTH, "height": HEIGHT, "fonts": font_face_css(), "time": "14:32", "dow": "lundi", "date": "15 juin", "updated": "14:32", "stale": False, "weather": { "ok": True, "kind": "partly", "temp": 21.4, "feels_like": 20.1, "temp_min": 14.0, "temp_max": 24.0, "precip_prob": 20, "label": "Peu nuageux", }, "claude": { "ok": True, "extra": {"label": "Crédits API : 38 % restant"}, "burn_rate": 1820, }, "gauges": [ {"name": "Session (5 h)", "remaining": 64, "resets_in": "2 h 10", "extra": ""}, {"name": "Hebdo (7 j)", "remaining": 12, "resets_in": "3 j", "extra": "Opus 41% rest."}, ], "codex": { "ok": True, "plan_type": "plus", "limited": False, "gauges": [ {"name": "Session (5 h)", "remaining": 78, "resets_in": "1 h 40"}, {"name": "Hebdo (7 j)", "remaining": 33, "resets_in": "4 j"}, ], }, "nas": { "ok": True, "disks": [ {"label": "Volume 1", "percent": 72, "free_human": "1,4 To"}, {"label": "Volume 2", "percent": 41, "free_human": "5,8 To"}, ], "docker_running": 18, "docker_total": 19, "docker_unhealthy": 1, "vpn_ok": True, "vpn_port": 51820, }, "trackers": [ {"ok": True, "label": "c411", "ratio_h": "1,04", "up_bytes": 1, "up_h": "378 Go", "down_h": "365 Go", "tokens": None}, {"ok": True, "label": "torr9", "ratio_h": "1,62", "up_bytes": 1, "up_h": "226 Go", "down_h": "140 Go", "tokens": 2168, "tokens_h": "2 168", "tokens_label": "jetons"}, {"ok": True, "label": "tr4ker", "ratio_h": "5233,52", "up_bytes": 1, "up_h": "5,62 To", "down_h": "1 Go", "tokens": 223, "tokens_h": "223", "tokens_label": "Crédit"}, {"ok": True, "label": "yggreborn", "ratio_h": "7,63", "up_bytes": 0, "down_bytes": 0, "tokens": None}, ], "ha_states": [ {"label": "Salon", "display": "21°"}, {"label": "Chambre", "display": "19°"}, {"label": "Humidité", "display": "48 %"}, {"label": "Porte garage", "display": "Fermée"}, ], "kobo": {"ok": True, "percent": 63, "charging": False, "low": False, "stale": False}, } async def main() -> None: out = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent / "preview.png" html = env.get_template("dashboard.html").render(**CTX) async with async_playwright() as p: browser = await p.chromium.launch(args=["--no-sandbox", "--disable-dev-shm-usage"]) page = await browser.new_page(viewport={"width": WIDTH, "height": HEIGHT}, device_scale_factor=1) await page.set_content(html, wait_until="networkidle") png = await page.screenshot(type="png", clip={"x": 0, "y": 0, "width": WIDTH, "height": HEIGHT}) await browser.close() Image.open(io.BytesIO(png)).convert("L").save(out) print(f"écrit {out}") if __name__ == "__main__": asyncio.run(main())