SPB Git

spb/ka-guardian

Public

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>
Simon-Pierre Boucher committed (Aug 23, 2026) parent 1c9b76c Browse files

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 import httpx 23 import httpx
24 from fastapi import FastAPI, Header, HTTPException, Request 24 from fastapi import FastAPI, Header, HTTPException, Request
25−from fastapi.responses import FileResponse, StreamingResponse 25+from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
26 from fastapi.staticfiles import StaticFiles 26 from fastapi.staticfiles import StaticFiles
27 27
28 BASE = pathlib.Path(__file__).resolve().parent 28 BASE = pathlib.Path(__file__).resolve().parent
@@ -863,14 +863,37 @@ def favicon() -> FileResponse:
863 return FileResponse(BASE / "web" / f"favicon-{AGENT}.svg", media_type="image/svg+xml") 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 @app.get("/") 889 @app.get("/")
867−def index() -> FileResponse: 890+def index() -> HTMLResponse:
868− return FileResponse(BASE / "web" / "index.html") 891+ return HTMLResponse(PAGES["index.html"])
869 892
870 893
871 @app.get("/commander") 894 @app.get("/commander")
872−def commander() -> FileResponse: 895+def commander() -> HTMLResponse:
873− return FileResponse(BASE / "web" / "commander.html") 896+ return HTMLResponse(PAGES["commander.html"])
874 897
875 898
876 app.mount("/static", StaticFiles(directory=BASE / "web"), name="static") 899 app.mount("/static", StaticFiles(directory=BASE / "web"), name="static")
modified orchestrator/web/commander.html +14 −0
@@ -5,6 +5,20 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6 <title>Commander — KA Guardian</title> 6 <title>Commander — KA Guardian</title>
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."> 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 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 22 <link rel="icon" type="image/svg+xml" href="/favicon.svg">
9 <link rel="preconnect" href="https://fonts.googleapis.com"> 23 <link rel="preconnect" href="https://fonts.googleapis.com">
10 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 24 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
modified orchestrator/web/index.html +14 −0
@@ -5,6 +5,20 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6 <title>Guardian — Groupe KA</title> 6 <title>Guardian — Groupe KA</title>
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."> 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 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 22 <link rel="icon" type="image/svg+xml" href="/favicon.svg">
9 <link rel="apple-touch-icon" href="/favicon.svg"> 23 <link rel="apple-touch-icon" href="/favicon.svg">
10 <link rel="preconnect" href="https://fonts.googleapis.com"> 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.

Menu

Theme