"""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, Codex).
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", "has_io": True, "up_h": "378 Go", "down_h": "365 Go", "tokens": None},
{"ok": True, "label": "torr9", "ratio_h": "1,62", "has_io": True, "up_h": "226 Go", "down_h": "140 Go", "tokens": 2168, "tokens_h": "2 168", "tokens_label": "token"},
{"ok": True, "label": "tr4ker", "ratio_h": "5233,52", "has_io": True, "up_h": "5,62 To", "down_h": "1 Go", "tokens": 223, "tokens_h": "223", "tokens_label": "token"},
{"ok": True, "label": "yggreborn", "ratio_h": "7,63", "has_io": True, "up_h": "60,55 Go", "down_h": "7,94 Go", "tokens": None},
],
"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())