É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 | </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 | ||||