É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>
3 changed files +48 −15
modified
public/app.js
+4 −4
@@ -1214,7 +1214,7 @@ | ||
| 1214 | 1214 | </div> |
| 1215 | 1215 | <div class="site-foot"> |
| 1216 | 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 | 1218 | ${s.proc?.last_commit ? `<span>commit <b>${timeAgo(s.proc.last_commit)}</b></span>` : ''} |
| 1219 | 1219 | ${s.certExpires ? `<span>SSL <b>${Math.max(0, Math.round((s.certExpires - Date.now()) / 86400000))} j</b></span>` : ''} |
| 1220 | 1220 | </div>`; |
@@ -1286,7 +1286,7 @@ | ||
| 1286 | 1286 | <h4>Service</h4> |
| 1287 | 1287 | <div class="kv"><span>Domaine</span><b>${esc(d.domain)}</b></div> |
| 1288 | 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 | 1290 | <div class="kv"><span>CPU / RAM process</span><b>${s.proc ? s.proc.cpu + '% / ' + fmtMB(s.proc.mem) : '—'}</b></div> |
| 1291 | 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 | 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 | 1301 | <div class="sheet-sec"> |
| 1302 | 1302 | <h4>Actions d'administration</h4> |
| 1303 | 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 | 1305 | <button class="btn-ghost" id="aLogs">📜 Logs récents</button> |
| 1306 | 1306 | <button class="btn-ghost" id="aCommits">⌥ Git & commits</button> |
| 1307 | 1307 | <a class="btn-ghost" href="https://${esc(d.domain)}" target="_blank" rel="noopener">↗ Ouvrir le site</a> |
@@ -1318,7 +1318,7 @@ | ||
| 1318 | 1318 | const out = () => $('aOut'); |
| 1319 | 1319 | const showOut = (html) => { out().classList.remove('hidden'); out().innerHTML = html; }; |
| 1320 | 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 | 1322 | showOut('<div class="admin-load">redémarrage…</div>'); |
| 1323 | 1323 | const r = await fetch('/api/admin/action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ app: d.app, action: 'restart' }) }); |
| 1324 | 1324 | const j = await r.json(); |
modified
server/monitor.js
+24 −8
@@ -22,9 +22,13 @@ export const SITES = [ | ||
| 22 | 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 | 23 | { app: 'trouve-ka', label: 'Trouve·Ka', domain: 'www.trouve-ka.com', node: 'M2M32', pm2: 'tk-web', dir: '~/trouve-ka', deployed: '2026-08' }, |
| 24 | 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 | 32 | const SELF_NODE = 'M3U96a'; |
| 29 | 33 | const CHECK_MS = 45_000; |
| 30 | 34 | const SWEEP_MS = 5 * 60_000; |
@@ -155,6 +159,9 @@ function sweepNode(node) { | ||
| 155 | 159 | return new Promise((resolve) => { |
| 156 | 160 | const gitCmds = SITES.filter(s => s.node === node) |
| 157 | 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 | 165 | const script = `export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH |
| 159 | 166 | echo "LOAD $(sysctl -n vm.loadavg 2>/dev/null)" |
| 160 | 167 | echo "MEMTOTAL $(sysctl -n hw.memsize 2>/dev/null)" |
@@ -162,6 +169,7 @@ echo "PAGESIZE $(sysctl -n hw.pagesize 2>/dev/null)" | ||
| 162 | 169 | vm_stat 2>/dev/null | egrep "active|wired|occupied" | tr -d '.' |
| 163 | 170 | echo "DISK $(df -k / | tail -1 | awk '{print $5}' | tr -d '%')" |
| 164 | 171 | echo "PM2JSON $(pm2 jlist 2>/dev/null | tail -1)" |
| 172 | +${lchCmds} | |
| 165 | 173 | ${gitCmds}`; |
| 166 | 174 | const args = node === SELF_NODE |
| 167 | 175 | ? ['-c', script] |
@@ -191,11 +199,21 @@ function parseSweep(node, out) { | ||
| 191 | 199 | for (const m of out.matchAll(/^GIT (\S+) (\d+)/gm)) commits[m[1]] = Number(m[2]) * 1000; |
| 192 | 200 | |
| 193 | 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 | 209 | const ins = db.prepare('INSERT INTO appstats(app,ts,cpu,mem,status,restarts,last_commit) VALUES(?,?,?,?,?,?,?)'); |
| 195 | − if (pm2raw) { | |
| 196 | − let list = []; | |
| 197 | − try { list = JSON.parse(pm2raw[1]); } catch {} | |
| 198 | − for (const site of SITES.filter(s => s.node === node)) { | |
| 210 | + for (const site of SITES.filter(s => s.node === node)) { | |
| 211 | + if (site.unit) { | |
| 212 | + const l = lch[site.app]; | |
| 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 | 217 | const p = list.find(x => x.name === site.pm2) || list.find(x => (x.name || '').startsWith(site.app)); |
| 200 | 218 | ins.run(site.app, ts, |
| 201 | 219 | p ? (p.monit?.cpu ?? 0) : 0, |
@@ -203,9 +221,7 @@ function parseSweep(node, out) { | ||
| 203 | 221 | p ? (p.pm2_env?.status || 'inconnu') : 'introuvable', |
| 204 | 222 | p ? (p.pm2_env?.restart_time ?? 0) : 0, |
| 205 | 223 | commits[site.app] || 0); |
| 206 | − } | |
| 207 | − } else { | |
| 208 | − for (const site of SITES.filter(s => s.node === node)) { | |
| 224 | + } else { | |
| 209 | 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 | 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 | 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 | 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 | 66 | # Conventions NON négociables (rappelle-les dans le prompt quand c'est pertinent) |
@@ -220,6 +220,10 @@ const PROJECTS = [ | ||
| 220 | 220 | { id: 'crea-ka@M3U96b', name: 'Créa·Ka', node: 'M3U96b', dir: '~/apps/crea-ka' }, |
| 221 | 221 | { id: 'job-ka@M3U96a', name: 'Job·Ka', node: null, dir: path.join(HOME, 'apps', 'job-ka') }, |
| 222 | 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 | 228 | // Orchestrateur : une seule session sur M3U96a qui pilote les 13 apps via SSH, |
| 225 | 229 | // en gardant le contexte complet de la tâche. |
@@ -661,13 +665,26 @@ const server = http.createServer(async (req, res) => { | ||
| 661 | 665 | const action = ['restart', 'stop', 'start'].includes(body.action) ? body.action : null; |
| 662 | 666 | if (!site || !action) return json(res, 400, { error: 'app ou action invalide' }); |
| 663 | 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 | 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 | 682 | if (p.startsWith('/api/admin/logs/')) { |
| 668 | 683 | const site = SITES.find(s => s.app === p.split('/')[4]); |
| 669 | 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 | 688 | return json(res, 200, { logs: out.stdout || out.error || '(vide)' }); |
| 672 | 689 | } |
| 673 | 690 | if (p.startsWith('/api/admin/commits/')) { |
| 674 | 691 | |