SPB Git

spb/admin-ka

Public

Écosystème + chat : intégration des 3 agents gardiens KA Guardian (ka2/ka4/ka6)

- PROJECTS: ka2/ka4/ka6@M4M36 (repo commun ~/cluster-projects/ka-guardian) → modifiables via le chat
- monitor.js: 3 sites www.kaX.bot + nœud M4M36 au sweep + statut/cpu/ram via launchd (champ unit)
- actions admin: restart/stop/start launchctl + logs tail pour les sites launchd (M4M36 sans pm2)
- app.js: libellés pm2 → génériques

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 23, 2026) parent bfc9c8e Browse files

3 changed files +48 −15

modified public/app.js +4 −4
@@ -1214,7 +1214,7 @@
1214 </div> 1214 </div>
1215 <div class="site-foot"> 1215 <div class="site-foot">
1216 <span>${esc(s.node)}</span> 1216 <span>${esc(s.node)}</span>
1217− ${s.proc?.status ? `<span class="${s.proc.status === 'online' ? '' : 'foot-bad'}">pm2 <b>${esc(s.proc.status)}</b></span>` : ''} 1217+ ${s.proc?.status ? `<span class="${s.proc.status === 'online' ? '' : 'foot-bad'}">proc <b>${esc(s.proc.status)}</b></span>` : ''}
1218 ${s.proc?.last_commit ? `<span>commit <b>${timeAgo(s.proc.last_commit)}</b></span>` : ''} 1218 ${s.proc?.last_commit ? `<span>commit <b>${timeAgo(s.proc.last_commit)}</b></span>` : ''}
1219 ${s.certExpires ? `<span>SSL <b>${Math.max(0, Math.round((s.certExpires - Date.now()) / 86400000))} j</b></span>` : ''} 1219 ${s.certExpires ? `<span>SSL <b>${Math.max(0, Math.round((s.certExpires - Date.now()) / 86400000))} j</b></span>` : ''}
1220 </div>`; 1220 </div>`;
@@ -1286,7 +1286,7 @@
1286 <h4>Service</h4> 1286 <h4>Service</h4>
1287 <div class="kv"><span>Domaine</span><b>${esc(d.domain)}</b></div> 1287 <div class="kv"><span>Domaine</span><b>${esc(d.domain)}</b></div>
1288 <div class="kv"><span>Nœud</span><b>${esc(d.node)}</b></div> 1288 <div class="kv"><span>Nœud</span><b>${esc(d.node)}</b></div>
1289− <div class="kv"><span>PM2</span><b>${esc(s.proc?.status || '—')}${s.proc?.restarts ? ' · ' + s.proc.restarts + ' restarts' : ''}</b></div> 1289+ <div class="kv"><span>Processus</span><b>${esc(s.proc?.status || '—')}${s.proc?.restarts ? ' · ' + s.proc.restarts + ' restarts' : ''}</b></div>
1290 <div class="kv"><span>CPU / RAM process</span><b>${s.proc ? s.proc.cpu + '% / ' + fmtMB(s.proc.mem) : '—'}</b></div> 1290 <div class="kv"><span>CPU / RAM process</span><b>${s.proc ? s.proc.cpu + '% / ' + fmtMB(s.proc.mem) : '—'}</b></div>
1291 <div class="kv"><span>Charge nœud</span><b>${s.nodeStat ? s.nodeStat.load1 + ' · RAM ' + Math.round(s.nodeStat.mem_used / s.nodeStat.mem_total * 100) + '% · disque ' + s.nodeStat.disk_pct + '%' : '—'}</b></div> 1291 <div class="kv"><span>Charge nœud</span><b>${s.nodeStat ? s.nodeStat.load1 + ' · RAM ' + Math.round(s.nodeStat.mem_used / s.nodeStat.mem_total * 100) + '% · disque ' + s.nodeStat.disk_pct + '%' : '—'}</b></div>
1292 <div class="kv"><span>Dernier commit</span><b>${s.proc?.last_commit ? timeAgo(s.proc.last_commit) : '—'}</b></div> 1292 <div class="kv"><span>Dernier commit</span><b>${s.proc?.last_commit ? timeAgo(s.proc.last_commit) : '—'}</b></div>
@@ -1301,7 +1301,7 @@
1301 <div class="sheet-sec"> 1301 <div class="sheet-sec">
1302 <h4>Actions d'administration</h4> 1302 <h4>Actions d'administration</h4>
1303 <div class="admin-actions"> 1303 <div class="admin-actions">
1304− <button class="btn-ghost" id="aRestart">↻ Redémarrer PM2</button> 1304+ <button class="btn-ghost" id="aRestart">↻ Redémarrer</button>
1305 <button class="btn-ghost" id="aLogs">📜 Logs récents</button> 1305 <button class="btn-ghost" id="aLogs">📜 Logs récents</button>
1306 <button class="btn-ghost" id="aCommits">⌥ Git & commits</button> 1306 <button class="btn-ghost" id="aCommits">⌥ Git & commits</button>
1307 <a class="btn-ghost" href="https://${esc(d.domain)}" target="_blank" rel="noopener">↗ Ouvrir le site</a> 1307 <a class="btn-ghost" href="https://${esc(d.domain)}" target="_blank" rel="noopener">↗ Ouvrir le site</a>
@@ -1318,7 +1318,7 @@
1318 const out = () => $('aOut'); 1318 const out = () => $('aOut');
1319 const showOut = (html) => { out().classList.remove('hidden'); out().innerHTML = html; }; 1319 const showOut = (html) => { out().classList.remove('hidden'); out().innerHTML = html; };
1320 $('aRestart').onclick = async () => { 1320 $('aRestart').onclick = async () => {
1321− if (!confirm(`Redémarrer ${d.label} (pm2 restart) sur ${d.node} ?`)) return; 1321+ if (!confirm(`Redémarrer ${d.label} sur ${d.node} ?`)) return;
1322 showOut('<div class="admin-load">redémarrage…</div>'); 1322 showOut('<div class="admin-load">redémarrage…</div>');
1323 const r = await fetch('/api/admin/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app: d.app, action: 'restart' }) }); 1323 const r = await fetch('/api/admin/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app: d.app, action: 'restart' }) });
1324 const j = await r.json(); 1324 const j = await r.json();
modified server/monitor.js +24 −8
@@ -22,9 +22,13 @@ export const SITES = [
22 { app: 'job-ka', label: 'Job·Ka', domain: 'www.job-ka.com', node: 'M3U96a', pm2: 'job-ka-web', dir: '~/apps/job-ka', deployed: '2026-08' }, 22 { app: 'job-ka', label: 'Job·Ka', domain: 'www.job-ka.com', node: 'M3U96a', pm2: 'job-ka-web', dir: '~/apps/job-ka', deployed: '2026-08' },
23 { app: 'trouve-ka', label: 'Trouve·Ka', domain: 'www.trouve-ka.com', node: 'M2M32', pm2: 'tk-web', dir: '~/trouve-ka', deployed: '2026-08' }, 23 { app: 'trouve-ka', label: 'Trouve·Ka', domain: 'www.trouve-ka.com', node: 'M2M32', pm2: 'tk-web', dir: '~/trouve-ka', deployed: '2026-08' },
24 { app: 'groupe-ka', label: 'Groupe·Ka', domain: 'www.groupe-ka.com', node: 'M3U96b', pm2: 'groupe-ka', dir: '~/apps/groupe-ka', deployed: '2026-08' }, 24 { app: 'groupe-ka', label: 'Groupe·Ka', domain: 'www.groupe-ka.com', node: 'M3U96b', pm2: 'groupe-ka', dir: '~/apps/groupe-ka', deployed: '2026-08' },
25+ // Agents gardiens KA Guardian — un seul repo sur M4M36, process launchd (unit), pas de pm2 sur ce nœud
26+ { app: 'ka2', label: 'KA·2 Guardian', domain: 'www.ka2.bot', node: 'M4M36', unit: 'com.ka2.guardian', log: '~/cluster-projects/ka-guardian/logs/ka2.log', dir: '~/cluster-projects/ka-guardian', deployed: '2026-08' },
27+ { app: 'ka4', label: 'KA·4 Guardian', domain: 'www.ka4.bot', node: 'M4M36', unit: 'com.ka4.guardian', log: '~/cluster-projects/ka-guardian/logs/ka4.log', dir: '~/cluster-projects/ka-guardian', deployed: '2026-08' },
28+ { app: 'ka6', label: 'KA·6 Guardian', domain: 'www.ka6.bot', node: 'M4M36', unit: 'com.ka6.guardian', log: '~/cluster-projects/ka-guardian/logs/ka6.log', dir: '~/cluster-projects/ka-guardian', deployed: '2026-08' },
25 ]; 29 ];
26 30
27−const NODES = ['M3U96a', 'M3U96b', 'M4M64a', 'M4M64b', 'M2M32']; 31+const NODES = ['M3U96a', 'M3U96b', 'M4M64a', 'M4M64b', 'M2M32', 'M4M36'];
28 const SELF_NODE = 'M3U96a'; 32 const SELF_NODE = 'M3U96a';
29 const CHECK_MS = 45_000; 33 const CHECK_MS = 45_000;
30 const SWEEP_MS = 5 * 60_000; 34 const SWEEP_MS = 5 * 60_000;
@@ -155,6 +159,9 @@ function sweepNode(node) {
155 return new Promise((resolve) => { 159 return new Promise((resolve) => {
156 const gitCmds = SITES.filter(s => s.node === node) 160 const gitCmds = SITES.filter(s => s.node === node)
157 .map(s => `echo "GIT ${s.app} $(git -C ${s.dir} log -1 --format=%ct 2>/dev/null || echo 0)"`).join('; '); 161 .map(s => `echo "GIT ${s.app} $(git -C ${s.dir} log -1 --format=%ct 2>/dev/null || echo 0)"`).join('; ');
162+ // sites sous launchd (KA Guardian) : PID + cpu/rss via ps
163+ const lchCmds = SITES.filter(s => s.node === node && s.unit)
164+ .map(s => `pid=$(launchctl list | awk '$3=="${s.unit}"{print $1}'); [ -z "$pid" ] && pid=-; echo "LCH ${s.app} $pid $(ps -o %cpu=,rss= -p $pid 2>/dev/null | tr -s ' ')"`).join('; ');
158 const script = `export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH 165 const script = `export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH
159 echo "LOAD $(sysctl -n vm.loadavg 2>/dev/null)" 166 echo "LOAD $(sysctl -n vm.loadavg 2>/dev/null)"
160 echo "MEMTOTAL $(sysctl -n hw.memsize 2>/dev/null)" 167 echo "MEMTOTAL $(sysctl -n hw.memsize 2>/dev/null)"
@@ -162,6 +169,7 @@ echo "PAGESIZE $(sysctl -n hw.pagesize 2>/dev/null)"
162 vm_stat 2>/dev/null | egrep "active|wired|occupied" | tr -d '.' 169 vm_stat 2>/dev/null | egrep "active|wired|occupied" | tr -d '.'
163 echo "DISK $(df -k / | tail -1 | awk '{print $5}' | tr -d '%')" 170 echo "DISK $(df -k / | tail -1 | awk '{print $5}' | tr -d '%')"
164 echo "PM2JSON $(pm2 jlist 2>/dev/null | tail -1)" 171 echo "PM2JSON $(pm2 jlist 2>/dev/null | tail -1)"
172+${lchCmds}
165 ${gitCmds}`; 173 ${gitCmds}`;
166 const args = node === SELF_NODE 174 const args = node === SELF_NODE
167 ? ['-c', script] 175 ? ['-c', script]
@@ -191,11 +199,21 @@ function parseSweep(node, out) {
191 for (const m of out.matchAll(/^GIT (\S+) (\d+)/gm)) commits[m[1]] = Number(m[2]) * 1000; 199 for (const m of out.matchAll(/^GIT (\S+) (\d+)/gm)) commits[m[1]] = Number(m[2]) * 1000;
192 200
193 const pm2raw = out.match(/^PM2JSON (\[.*\])\s*$/m); 201 const pm2raw = out.match(/^PM2JSON (\[.*\])\s*$/m);
202+ let list = [];
203+ if (pm2raw) { try { list = JSON.parse(pm2raw[1]); } catch {} }
204+ // sites launchd : "LCH <app> <pid|-> [<%cpu> <rss Ko>]"
205+ const lch = {};
206+ for (const m of out.matchAll(/^LCH (\S+) (\S+) ?([\d.]+)? ?(\d+)?/gm)) {
207+ lch[m[1]] = { pid: m[2], cpu: Number(m[3] || 0), rss: Number(m[4] || 0) };
208+ }
194 const ins = db.prepare('INSERT INTO appstats(app,ts,cpu,mem,status,restarts,last_commit) VALUES(?,?,?,?,?,?,?)'); 209 const ins = db.prepare('INSERT INTO appstats(app,ts,cpu,mem,status,restarts,last_commit) VALUES(?,?,?,?,?,?,?)');
195− if (pm2raw) { 210+ for (const site of SITES.filter(s => s.node === node)) {
196− let list = []; 211+ if (site.unit) {
197− try { list = JSON.parse(pm2raw[1]); } catch {} 212+ const l = lch[site.app];
198− for (const site of SITES.filter(s => s.node === node)) { 213+ const up = l && l.pid !== '-' && /^\d+$/.test(l.pid);
214+ ins.run(site.app, ts, l ? l.cpu : 0, l ? l.rss * 1024 : 0,
215+ l ? (up ? 'online' : 'arrêté') : 'inconnu', 0, commits[site.app] || 0);
216+ } else if (pm2raw) {
199 const p = list.find(x => x.name === site.pm2) || list.find(x => (x.name || '').startsWith(site.app)); 217 const p = list.find(x => x.name === site.pm2) || list.find(x => (x.name || '').startsWith(site.app));
200 ins.run(site.app, ts, 218 ins.run(site.app, ts,
201 p ? (p.monit?.cpu ?? 0) : 0, 219 p ? (p.monit?.cpu ?? 0) : 0,
@@ -203,9 +221,7 @@ function parseSweep(node, out) {
203 p ? (p.pm2_env?.status || 'inconnu') : 'introuvable', 221 p ? (p.pm2_env?.status || 'inconnu') : 'introuvable',
204 p ? (p.pm2_env?.restart_time ?? 0) : 0, 222 p ? (p.pm2_env?.restart_time ?? 0) : 0,
205 commits[site.app] || 0); 223 commits[site.app] || 0);
206− } 224+ } else {
207− } else {
208− for (const site of SITES.filter(s => s.node === node)) {
209 ins.run(site.app, ts, 0, 0, 'inconnu', 0, commits[site.app] || 0); 225 ins.run(site.app, ts, 0, 0, 'inconnu', 0, commits[site.app] || 0);
210 } 226 }
211 } 227 }
modified server/server.js +20 −3
@@ -60,7 +60,7 @@ const UPGRADER_MODEL = config.upgraderModel || 'claude-opus-4-8';
60 const KA_KNOWLEDGE = `Tu es l'"upgradeur de prompt" de la console administration-ka.com. Ta seule tâche : réécrire la demande brute de l'utilisateur en un prompt EXCELLENT destiné à Claude Code, qui s'exécute sur le nœud d'une app du Groupe KA. Tu ne réponds jamais à la demande toi-même, tu ne codes pas : tu produis UNIQUEMENT le prompt amélioré, prêt à être envoyé. 60 const KA_KNOWLEDGE = `Tu es l'"upgradeur de prompt" de la console administration-ka.com. Ta seule tâche : réécrire la demande brute de l'utilisateur en un prompt EXCELLENT destiné à Claude Code, qui s'exécute sur le nœud d'une app du Groupe KA. Tu ne réponds jamais à la demande toi-même, tu ne codes pas : tu produis UNIQUEMENT le prompt amélioré, prêt à être envoyé.
61 61
62 # Écosystème Groupe KA (12 apps web, stack Node/React, cluster MacLustr) 62 # Écosystème Groupe KA (12 apps web, stack Node/React, cluster MacLustr)
63−- lou-ka (lou-ka.com) : location immobilière/logements. immo-ka (immo-ka.com) : achat/vente immobilier. vrai-prix (vrai-prix.com) : comparateur de prix. valoplex : immobilier/valorisation. auto-ka (auto-ka.com) : véhicules. fabri-ka (fabri-ka.com) : fabricants/artisans/friperies. food-ka (food-ka.com) : alimentation/épicerie. resto-ka (resto-ka.com) : restaurants. sorti-ka (sorti-ka.com) : sorties/événements. crea-ka (crea-ka.com) : créateurs de contenu QC (multi-plateforme). job-ka (job-ka.com) : emplois. trouve-ka (trouve-ka.com) : recherche/annuaire. groupe-ka.com : hub + KA ID (SSO). Marché cible : Québec (fr-CA). 63+- lou-ka (lou-ka.com) : location immobilière/logements. immo-ka (immo-ka.com) : achat/vente immobilier. vrai-prix (vrai-prix.com) : comparateur de prix. valoplex : immobilier/valorisation. auto-ka (auto-ka.com) : véhicules. fabri-ka (fabri-ka.com) : fabricants/artisans/friperies. food-ka (food-ka.com) : alimentation/épicerie. resto-ka (resto-ka.com) : restaurants. sorti-ka (sorti-ka.com) : sorties/événements. crea-ka (crea-ka.com) : créateurs de contenu QC (multi-plateforme). job-ka (job-ka.com) : emplois. trouve-ka (trouve-ka.com) : recherche/annuaire. groupe-ka.com : hub + KA ID (SSO). ka2/ka4/ka6 (ka2.bot/ka4.bot/ka6.bot) : agents gardiens autonomes des connecteurs (KA Guardian — un seul repo Python/FastAPI sur M4M36, process launchd com.kaX.guardian, PAS pm2). Marché cible : Québec (fr-CA).
64 - Architecture commune : back Node (souvent Express/Fastify), front React/Next ou Vite, données via connecteurs (scrapers) + SQLite/Postgres, PM2 pour les process, ngrok pour les domaines. Beaucoup d'apps ont un backend "sync"/crawler et une couche qualité (published/quarantaine). 64 - Architecture commune : back Node (souvent Express/Fastify), front React/Next ou Vite, données via connecteurs (scrapers) + SQLite/Postgres, PM2 pour les process, ngrok pour les domaines. Beaucoup d'apps ont un backend "sync"/crawler et une couche qualité (published/quarantaine).
65 65
66 # Conventions NON négociables (rappelle-les dans le prompt quand c'est pertinent) 66 # Conventions NON négociables (rappelle-les dans le prompt quand c'est pertinent)
@@ -220,6 +220,10 @@ const PROJECTS = [
220 { id: 'crea-ka@M3U96b', name: 'Créa·Ka', node: 'M3U96b', dir: '~/apps/crea-ka' }, 220 { id: 'crea-ka@M3U96b', name: 'Créa·Ka', node: 'M3U96b', dir: '~/apps/crea-ka' },
221 { id: 'job-ka@M3U96a', name: 'Job·Ka', node: null, dir: path.join(HOME, 'apps', 'job-ka') }, 221 { id: 'job-ka@M3U96a', name: 'Job·Ka', node: null, dir: path.join(HOME, 'apps', 'job-ka') },
222 { id: 'trouve-ka@M2M32', name: 'Trouve·Ka', node: 'M2M32', dir: '~/trouve-ka' }, 222 { id: 'trouve-ka@M2M32', name: 'Trouve·Ka', node: 'M2M32', dir: '~/trouve-ka' },
223+ // Agents gardiens KA Guardian — 3 sites, 1 seul repo sur M4M36 (launchd, pas pm2)
224+ { id: 'ka2@M4M36', name: 'KA·2 Guardian', node: 'M4M36', dir: '~/cluster-projects/ka-guardian' },
225+ { id: 'ka4@M4M36', name: 'KA·4 Guardian', node: 'M4M36', dir: '~/cluster-projects/ka-guardian' },
226+ { id: 'ka6@M4M36', name: 'KA·6 Guardian', node: 'M4M36', dir: '~/cluster-projects/ka-guardian' },
223 ]; 227 ];
224 // Orchestrateur : une seule session sur M3U96a qui pilote les 13 apps via SSH, 228 // Orchestrateur : une seule session sur M3U96a qui pilote les 13 apps via SSH,
225 // en gardant le contexte complet de la tâche. 229 // en gardant le contexte complet de la tâche.
@@ -661,13 +665,26 @@ const server = http.createServer(async (req, res) => {
661 const action = ['restart', 'stop', 'start'].includes(body.action) ? body.action : null; 665 const action = ['restart', 'stop', 'start'].includes(body.action) ? body.action : null;
662 if (!site || !action) return json(res, 400, { error: 'app ou action invalide' }); 666 if (!site || !action) return json(res, 400, { error: 'app ou action invalide' });
663 audit({ kind: 'admin_action', app: site.app, node: site.node, action, ip: clientIp(req) }); 667 audit({ kind: 'admin_action', app: site.app, node: site.node, action, ip: clientIp(req) });
664− const out = await runOnNode(site.node, `pm2 ${action} ${site.pm2} && pm2 jlist | python3 -c "import json,sys; l=json.load(sys.stdin); p=[x for x in l if x['name']=='${site.pm2}']; print(p[0]['pm2_env']['status'] if p else 'introuvable')"`); 668+ let out;
669+ if (site.unit) {
670+ // sites sous launchd (KA Guardian sur M4M36) — pas de pm2 sur ce nœud
671+ const cmds = {
672+ restart: `launchctl kickstart -k gui/$(id -u)/${site.unit}`,
673+ stop: `launchctl unload ~/Library/LaunchAgents/${site.unit}.plist`,
674+ start: `launchctl load ~/Library/LaunchAgents/${site.unit}.plist`,
675+ };
676+ out = await runOnNode(site.node, `${cmds[action]} && sleep 1 && (launchctl list | awk '$3=="${site.unit}"{print ($1=="-")?"arrêté":"online"; f=1} END{if(!f)print "déchargé"}')`);
677+ } else {
678+ out = await runOnNode(site.node, `pm2 ${action} ${site.pm2} && pm2 jlist | python3 -c "import json,sys; l=json.load(sys.stdin); p=[x for x in l if x['name']=='${site.pm2}']; print(p[0]['pm2_env']['status'] if p else 'introuvable')"`);
679+ }
665 return json(res, 200, { ok: !out.error, output: (out.stdout || out.error || '').trim().split('\n').pop(), full: (out.stdout || '').slice(-1500) }); 680 return json(res, 200, { ok: !out.error, output: (out.stdout || out.error || '').trim().split('\n').pop(), full: (out.stdout || '').slice(-1500) });
666 } 681 }
667 if (p.startsWith('/api/admin/logs/')) { 682 if (p.startsWith('/api/admin/logs/')) {
668 const site = SITES.find(s => s.app === p.split('/')[4]); 683 const site = SITES.find(s => s.app === p.split('/')[4]);
669 if (!site) return json(res, 404, { error: 'site inconnu' }); 684 if (!site) return json(res, 404, { error: 'site inconnu' });
670− const out = await runOnNode(site.node, `pm2 logs ${site.pm2} --nostream --lines 60 2>&1 | tail -70`); 685+ const out = site.log
686+ ? await runOnNode(site.node, `tail -70 ${site.log} 2>&1`)
687+ : await runOnNode(site.node, `pm2 logs ${site.pm2} --nostream --lines 60 2>&1 | tail -70`);
671 return json(res, 200, { logs: out.stdout || out.error || '(vide)' }); 688 return json(res, 200, { logs: out.stdout || out.error || '(vide)' });
672 } 689 }
673 if (p.startsWith('/api/admin/commits/')) { 690 if (p.startsWith('/api/admin/commits/')) {
674 691
Menu

Theme