SPB Git forge

spb/ka-guardian

Public
26commits 1branches 0releases
4.5 MBsize
maindefault branch
19 days agolast push
Python 57.1% Shell 13.8% CSS 12% JavaScript 10% HTML 7.2%

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 1 mo ago (Aug 23, 2026) parent 1c9b76c

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.