Bannières de partage (OG) par agent + rendu des pages par agent + CLAUDE.md prod
- og-ka2/ka4/ka6.png (2400×1260, identité Groupe KA) générées par deploy/make_og_images.py - balises Open Graph/Twitter dans index.html et commander.html (jetons __NUM__/__DOMAIN__/__TAGLINE__) - main.py: _render_page() au démarrage + route /og.png - CLAUDE.md: guide de prod M4M36 (launchd, restart, gotchas) pour les sessions admin Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8 changed files +204 −5
added
CLAUDE.md
+30 −0
@@ -0,0 +1,30 @@ | ||
| 1 | +# KA Guardian — ka2 · ka4 · ka6 (agents gardiens des connecteurs) | |
| 2 | + | |
| 3 | +Un seul repo pour les **3 sites gardiens** : www.ka2.bot, www.ka4.bot, www.ka6.bot. | |
| 4 | +Chaque agent est une instance de `orchestrator/main.py` (env `AGENT=ka2|ka4|ka6`), | |
| 5 | +identité (accent, tagline, services surveillés) dans `topology.json`. | |
| 6 | + | |
| 7 | +## Production (nœud M4M36) | |
| 8 | + | |
| 9 | +- Répertoire : `~/cluster-projects/ka-guardian` (ce repo, origin = gitsrv/spbgit `ka-guardian.git`). | |
| 10 | +- Process : **launchd** (PAS pm2) — `com.ka2.guardian` (:8799), `com.ka4.guardian` (:8899), `com.ka6.guardian` (:8999). Tunnels ngrok `com.kaX.ngrok` (ne pas toucher). | |
| 11 | +- Pas d'étape de build : Python servi en place (`.venv` local au nœud). | |
| 12 | + | |
| 13 | +## Après toute modification (sur M4M36) | |
| 14 | + | |
| 15 | +1. Redémarrer le ou les agents concernés : | |
| 16 | + `launchctl kickstart -k gui/$(id -u)/com.ka2.guardian` (idem ka4/ka6 — les 3 partagent le code, redémarre les 3 si le changement est commun). | |
| 17 | +2. Vérifier : `curl -sf localhost:8799/health` (8899, 8999) puis le site public. | |
| 18 | +3. `git add … && git commit && git push origin main` (origin = gitsrv/spbgit, PAS GitHub). | |
| 19 | + | |
| 20 | +⚠️ NE JAMAIS redémarrer les **runners** (`com.ka.guardian-runner` sur M3U96a/b, M4M64a/b) : ça tue la mission Claude en vol. Ils ne vivent pas sur ce nœud de toute façon. | |
| 21 | +⚠️ Le trafic LAN direct est bloqué par macOS 26 (Local Network Privacy) — le courrier zsh `com.ka.guardian-courier` s'en occupe ; ne pas le contourner. | |
| 22 | + | |
| 23 | +## Pages & image de marque | |
| 24 | + | |
| 25 | +- `orchestrator/web/index.html` + `commander.html` : les jetons `__NUM__`, `__DOMAIN__`, `__TAGLINE__` (balises OG) sont rendus par agent au démarrage dans `main.py` (`_render_page`). | |
| 26 | +- Bannières de partage `orchestrator/web/og-ka{2,4,6}.png` (servies à `/og.png`) : régénérer sur le laptop avec `python3 deploy/make_og_images.py` (Playwright/Chromium) si l'identité visuelle change. | |
| 27 | + | |
| 28 | +## Déploiement complet depuis le laptop | |
| 29 | + | |
| 30 | +`deploy/deploy.sh orchestrators` (tar-over-ssh + vérif md5 + relance launchd). Source laptop : `~/Desktop/Groupe-KA/apps-web/ka-guardian`. | |
added
deploy/make_og_images.py
+118 −0
@@ -0,0 +1,118 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +"""Génère les bannières de partage (Open Graph) des 3 sites gardiens. | |
| 3 | + | |
| 4 | +Rendu HTML → Chromium (Playwright) → orchestrator/web/og-ka{2,4,6}.png | |
| 5 | +(2400×1260, ratio 1200:630). Langage visuel Groupe KA : papier, encre, | |
| 6 | +lime, ombres décalées dures + accent/battement ECG propres à chaque agent. | |
| 7 | + | |
| 8 | +Usage (laptop) : python3 deploy/make_og_images.py | |
| 9 | +""" | |
| 10 | +import json | |
| 11 | +import tempfile | |
| 12 | +from pathlib import Path | |
| 13 | + | |
| 14 | +from playwright.sync_api import sync_playwright | |
| 15 | + | |
| 16 | +ROOT = Path(__file__).resolve().parent.parent | |
| 17 | +WEB = ROOT / "orchestrator" / "web" | |
| 18 | +TOPO = json.loads((ROOT / "topology.json").read_text()) | |
| 19 | + | |
| 20 | +# couleur du battement ECG (celle des favicons, plus dense que l'accent pastel) | |
| 21 | +ECG = {"ka2": "#5b9df5", "ka4": "#ff9f2e", "ka6": "#a78bfa"} | |
| 22 | +SERVICES_LABEL = { | |
| 23 | + "ka2": "lou·ka · immo·ka · resto·ka", | |
| 24 | + "ka4": "auto·ka · food·ka · sorti·ka", | |
| 25 | + "ka6": "fabri·ka · job·ka · créa·ka", | |
| 26 | +} | |
| 27 | + | |
| 28 | +TPL = """<!doctype html> | |
| 29 | +<html lang="fr-CA"><head><meta charset="utf-8"> | |
| 30 | +<link rel="preconnect" href="https://fonts.googleapis.com"> | |
| 31 | +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| 32 | +<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet"> | |
| 33 | +<style> | |
| 34 | + * { box-sizing: border-box; margin: 0; } | |
| 35 | + html, body { width: 1200px; height: 630px; overflow: hidden; } | |
| 36 | + body { background: #f5f3ee; color: #141814; font-family: "Inter", system-ui, sans-serif; | |
| 37 | + -webkit-font-smoothing: antialiased; position: relative; } | |
| 38 | + .frame { position: absolute; inset: 26px; border: 3px solid #141814; border-radius: 22px; | |
| 39 | + background: #fff; box-shadow: 14px 14px 0 #141814; padding: 52px 60px 46px; overflow: hidden; } | |
| 40 | + .dots { position: absolute; inset: 0; border-radius: 20px; | |
| 41 | + background-image: radial-gradient(#1418141a 1.6px, transparent 1.6px); background-size: 26px 26px; } | |
| 42 | + .top { display: flex; justify-content: space-between; align-items: flex-start; position: relative; } | |
| 43 | + .wordmark { font-family: "Space Grotesk", sans-serif; font-weight: 700; font-size: 120px; | |
| 44 | + letter-spacing: -0.05em; line-height: 1; display: flex; align-items: center; } | |
| 45 | + .wordmark .num { color: %%ACCENT2%%; } | |
| 46 | + .wordmark .ka { background: #141814; color: #d9f26b; border-radius: 14px; margin-left: 20px; | |
| 47 | + padding: 6px 24px 14px; display: inline-block; transform: rotate(-2deg); font-size: 0.42em; } | |
| 48 | + .gk { font-family: "Space Grotesk", sans-serif; font-weight: 700; font-size: 30px; display: flex; | |
| 49 | + align-items: center; gap: 14px; margin-top: 14px; } | |
| 50 | + .gk .badge { background: #141814; color: #d9f26b; border-radius: 10px; padding: 2px 14px 6px; | |
| 51 | + transform: rotate(-2deg); display: inline-block; } | |
| 52 | + .pastille { width: 22px; height: 22px; border-radius: 50%; background: #d9f26b; border: 3px solid #141814; } | |
| 53 | + .klabel { font-family: "JetBrains Mono", monospace; text-transform: uppercase; letter-spacing: 0.14em; | |
| 54 | + color: #4d5551; font-size: 21px; font-weight: 700; margin-top: 26px; position: relative; } | |
| 55 | + .tagline { font-family: "Space Grotesk", sans-serif; font-weight: 700; font-size: 62px; | |
| 56 | + letter-spacing: -0.02em; margin-top: 12px; position: relative; line-height: 1.12; } | |
| 57 | + .tagline .hl { background: %%ACCENT%%; border-radius: 12px; padding: 0 16px 4px; display: inline-block; | |
| 58 | + transform: rotate(-1deg); } | |
| 59 | + svg.ecg { position: absolute; left: 0; right: 0; bottom: 118px; width: 100%; } | |
| 60 | + .bottom { position: absolute; left: 60px; right: 60px; bottom: 42px; display: flex; | |
| 61 | + justify-content: space-between; align-items: center; } | |
| 62 | + .chip { font-family: "JetBrains Mono", monospace; font-weight: 700; font-size: 26px; | |
| 63 | + text-transform: uppercase; letter-spacing: 0.06em; border: 3px solid #141814; border-radius: 999px; | |
| 64 | + background: #f5f3ee; padding: 10px 26px; box-shadow: 6px 6px 0 #141814; } | |
| 65 | + .services { font-family: "JetBrains Mono", monospace; font-weight: 600; font-size: 22px; color: #4d5551; } | |
| 66 | +</style></head> | |
| 67 | +<body> | |
| 68 | +<div class="frame"> | |
| 69 | + <div class="dots"></div> | |
| 70 | + <div class="top"> | |
| 71 | + <div class="wordmark">ka·<span class="num">%%NUM%%</span><span class="ka">guardian</span></div> | |
| 72 | + <div class="gk">groupe<span class="badge">·KA</span><span class="pastille"></span></div> | |
| 73 | + </div> | |
| 74 | + <div class="klabel">agent gardien autonome · connecteurs du groupe ka</div> | |
| 75 | + <div class="tagline">%%TAGLINE_HTML%%</div> | |
| 76 | + <svg class="ecg" viewBox="0 0 1200 90" preserveAspectRatio="none"> | |
| 77 | + <polyline points="0,58 260,58 320,32 400,82 470,44 520,58 700,58 760,36 830,78 895,48 940,58 1200,58" | |
| 78 | + fill="none" stroke="%%ECG%%" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/> | |
| 79 | + </svg> | |
| 80 | + <div class="bottom"> | |
| 81 | + <div class="chip">%%DOMAIN%%</div> | |
| 82 | + <div class="services">%%SERVICES%%</div> | |
| 83 | + </div> | |
| 84 | +</div> | |
| 85 | +</body></html>""" | |
| 86 | + | |
| 87 | +# mot à surligner dans la tagline (image de marque : pastille accent de l'agent) | |
| 88 | +def tagline_html(agent: str, tagline: str) -> str: | |
| 89 | + first, _, rest = tagline.partition(" ") | |
| 90 | + return f'<span class="hl">{first}</span> {rest}' | |
| 91 | + | |
| 92 | + | |
| 93 | +def main() -> None: | |
| 94 | + with sync_playwright() as p: | |
| 95 | + browser = p.chromium.launch() | |
| 96 | + page = browser.new_page(viewport={"width": 1200, "height": 630}, device_scale_factor=2) | |
| 97 | + for agent, me in TOPO["agents"].items(): | |
| 98 | + html = (TPL | |
| 99 | + .replace("%%NUM%%", agent[-1]) | |
| 100 | + .replace("%%ACCENT%%", me["accent"]) | |
| 101 | + .replace("%%ACCENT2%%", me["accent2"]) | |
| 102 | + .replace("%%ECG%%", ECG[agent]) | |
| 103 | + .replace("%%DOMAIN%%", me["domain"]) | |
| 104 | + .replace("%%SERVICES%%", SERVICES_LABEL[agent]) | |
| 105 | + .replace("%%TAGLINE_HTML%%", tagline_html(agent, me["tagline"]))) | |
| 106 | + tmp = Path(tempfile.gettempdir()) / f"og-{agent}.html" | |
| 107 | + tmp.write_text(html) | |
| 108 | + page.goto(tmp.as_uri()) | |
| 109 | + page.evaluate("document.fonts.ready.then(() => {})") | |
| 110 | + page.wait_for_timeout(900) | |
| 111 | + out = WEB / f"og-{agent}.png" | |
| 112 | + page.screenshot(path=str(out)) | |
| 113 | + print(f"✓ {out.relative_to(ROOT)}") | |
| 114 | + browser.close() | |
| 115 | + | |
| 116 | + | |
| 117 | +if __name__ == "__main__": | |
| 118 | + main() | |
modified
orchestrator/main.py
+28 −5
@@ -22,7 +22,7 @@ from typing import Any | ||
| 22 | 22 | |
| 23 | 23 | import httpx |
| 24 | 24 | from fastapi import FastAPI, Header, HTTPException, Request |
| 25 | −from fastapi.responses import FileResponse, StreamingResponse | |
| 25 | +from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse | |
| 26 | 26 | from fastapi.staticfiles import StaticFiles |
| 27 | 27 | |
| 28 | 28 | BASE = pathlib.Path(__file__).resolve().parent |
@@ -863,14 +863,37 @@ def favicon() -> FileResponse: | ||
| 863 | 863 | return FileResponse(BASE / "web" / f"favicon-{AGENT}.svg", media_type="image/svg+xml") |
| 864 | 864 | |
| 865 | 865 | |
| 866 | +@app.get("/og.png") | |
| 867 | +def og_image() -> FileResponse: | |
| 868 | + """Bannière de partage (Open Graph) propre à l'agent.""" | |
| 869 | + return FileResponse(BASE / "web" / f"og-{AGENT}.png", media_type="image/png") | |
| 870 | + | |
| 871 | + | |
| 872 | +def _render_page(name: str) -> str: | |
| 873 | + """Les pages sont statiques mais servies par agent : on remplace au | |
| 874 | + démarrage les jetons de partage (OG) par l'identité de l'agent — | |
| 875 | + les robots des réseaux sociaux n'exécutent pas le JS.""" | |
| 876 | + html = (BASE / "web" / name).read_text(encoding="utf-8") | |
| 877 | + for token, value in { | |
| 878 | + "__NUM__": AGENT[-1], | |
| 879 | + "__DOMAIN__": ME["domain"], | |
| 880 | + "__TAGLINE__": ME.get("tagline", ""), | |
| 881 | + }.items(): | |
| 882 | + html = html.replace(token, value) | |
| 883 | + return html | |
| 884 | + | |
| 885 | + | |
| 886 | +PAGES = {name: _render_page(name) for name in ("index.html", "commander.html")} | |
| 887 | + | |
| 888 | + | |
| 866 | 889 | @app.get("/") |
| 867 | −def index() -> FileResponse: | |
| 868 | − return FileResponse(BASE / "web" / "index.html") | |
| 890 | +def index() -> HTMLResponse: | |
| 891 | + return HTMLResponse(PAGES["index.html"]) | |
| 869 | 892 | |
| 870 | 893 | |
| 871 | 894 | @app.get("/commander") |
| 872 | −def commander() -> FileResponse: | |
| 873 | − return FileResponse(BASE / "web" / "commander.html") | |
| 895 | +def commander() -> HTMLResponse: | |
| 896 | + return HTMLResponse(PAGES["commander.html"]) | |
| 874 | 897 | |
| 875 | 898 | |
| 876 | 899 | app.mount("/static", StaticFiles(directory=BASE / "web"), name="static") |
modified
orchestrator/web/commander.html
+14 −0
@@ -5,6 +5,20 @@ | ||
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> |
| 6 | 6 | <title>Commander — KA Guardian</title> |
| 7 | 7 | <meta name="description" content="Poste de commande de l'agent gardien : lancer une session Claude Code 100 % autonome — nouveau connecteur, enrichissement ou inspection des connecteurs dégradés — avec plafonds de durée et de coût."> |
| 8 | +<!-- Partage (Open Graph / Twitter) — les jetons __NUM__/__DOMAIN__/__TAGLINE__ sont rendus par agent dans main.py --> | |
| 9 | +<meta property="og:type" content="website"> | |
| 10 | +<meta property="og:site_name" content="KA Guardian — Groupe KA"> | |
| 11 | +<meta property="og:title" content="Commander — ka·__NUM__ guardian"> | |
| 12 | +<meta property="og:description" content="Donner du travail à l'agent gardien : une session Claude Code 100 % autonome, avec plafonds de durée et de coût."> | |
| 13 | +<meta property="og:url" content="https://__DOMAIN__/commander"> | |
| 14 | +<meta property="og:image" content="https://__DOMAIN__/og.png"> | |
| 15 | +<meta property="og:image:width" content="2400"> | |
| 16 | +<meta property="og:image:height" content="1260"> | |
| 17 | +<meta property="og:locale" content="fr_CA"> | |
| 18 | +<meta name="twitter:card" content="summary_large_image"> | |
| 19 | +<meta name="twitter:title" content="Commander — ka·__NUM__ guardian"> | |
| 20 | +<meta name="twitter:description" content="Donner du travail à l'agent gardien — plafonds de durée et de coût."> | |
| 21 | +<meta name="twitter:image" content="https://__DOMAIN__/og.png"> | |
| 8 | 22 | <link rel="icon" type="image/svg+xml" href="/favicon.svg"> |
| 9 | 23 | <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 10 | 24 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
modified
orchestrator/web/index.html
+14 −0
@@ -5,6 +5,20 @@ | ||
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> |
| 6 | 6 | <title>Guardian — Groupe KA</title> |
| 7 | 7 | <meta name="description" content="Agent gardien autonome du Groupe KA : il détecte les connecteurs en panne, les répare avec Claude, surveille la guérison et revient en arrière si ça empire. Tout est public, en direct."> |
| 8 | +<!-- Partage (Open Graph / Twitter) — les jetons __NUM__/__DOMAIN__/__TAGLINE__ sont rendus par agent dans main.py --> | |
| 9 | +<meta property="og:type" content="website"> | |
| 10 | +<meta property="og:site_name" content="KA Guardian — Groupe KA"> | |
| 11 | +<meta property="og:title" content="ka·__NUM__ guardian — __TAGLINE__"> | |
| 12 | +<meta property="og:description" content="Agent gardien autonome des connecteurs du Groupe KA : il détecte les pannes, répare avec Claude, surveille la guérison. Tout est public, en direct."> | |
| 13 | +<meta property="og:url" content="https://__DOMAIN__/"> | |
| 14 | +<meta property="og:image" content="https://__DOMAIN__/og.png"> | |
| 15 | +<meta property="og:image:width" content="2400"> | |
| 16 | +<meta property="og:image:height" content="1260"> | |
| 17 | +<meta property="og:locale" content="fr_CA"> | |
| 18 | +<meta name="twitter:card" content="summary_large_image"> | |
| 19 | +<meta name="twitter:title" content="ka·__NUM__ guardian — __TAGLINE__"> | |
| 20 | +<meta name="twitter:description" content="Agent gardien autonome des connecteurs du Groupe KA — public, en direct."> | |
| 21 | +<meta name="twitter:image" content="https://__DOMAIN__/og.png"> | |
| 8 | 22 | <link rel="icon" type="image/svg+xml" href="/favicon.svg"> |
| 9 | 23 | <link rel="apple-touch-icon" href="/favicon.svg"> |
| 10 | 24 | <link rel="preconnect" href="https://fonts.googleapis.com"> |
added
orchestrator/web/og-ka2.png
+0 −0
Binary file not shown.
added
orchestrator/web/og-ka4.png
+0 −0
Binary file not shown.
added
orchestrator/web/og-ka6.png
+0 −0
Binary file not shown.