SPB Git

spb/admin-ka

Public
22.5 KB · 396 lines javascript
1// registry.js — topologie VIVANTE de la console admin-ka.2// « Quelle app tourne sur quel nœud ? » n'est plus codé ici : la réponse vient3// du registre de la passerelle mld (M1M32:~/dispatch/registry.json), que4// l'orchestrateur maclustr-dispatch met à jour à chaque deploy/move/retire.5//   • poussé par mld dans data/registry.json (abonné « admin-ka », suit les migrations) ;6//   • tiré toutes les 2 min par ssh (M1M32 / gitsrv) en ceinture-et-bretelles ;7//   • le fichier local est surveillé : un push mld est appliqué en quelques secondes.8// Ce module ne garde en dur que la CONNAISSANCE Ka (libellés, exceptions launchd,9// process PM2 non déductibles) — jamais un emplacement.10import fs from 'node:fs';11import os from 'node:os';12import path from 'node:path';13import { execFile } from 'node:child_process';14import { fileURLToPath } from 'node:url';1516const __dirname = path.dirname(fileURLToPath(import.meta.url));17const HOME = os.homedir();18const USER = 'simon-pierreboucher';19const GATEWAY_HOSTS = ['M1M32', 'gitsrv'];          // alias ssh essayés pour tirer le registre20const PULL_MS = 2 * 60_000;21const WATCH_MS = 5_000;22const STALE_MS = 3 * 3600_000;                       // registre plus vieux → « périmé » (mld en panne ?)23const ENV_PATH = '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';2425// ---------- connaissance Ka (PAS d'emplacement ici) ----------26// label : nom affiché ; pm2 : process web quand la règle « *-web sinon 1er » ne suffit pas ;27// unit/log : apps sous launchd (pas de pm2) ; repoDir : repo de travail ≠ dir du manifeste ;28// monitor:false / project:false : exclure du monitoring / des projets Claude Code.29export const KA_CATALOG = {30  'lou-ka':    { label: 'Lou·Ka' },31  'rent-ka':   { label: 'Rent·Ka' },32  'immo-ka':   { label: 'Immo·Ka' },33  'house-ka':  { label: 'House·Ka' },34  'vrai-prix': { label: 'Vrai-Prix' },35  'auto-ka':   { label: 'Auto·Ka' },36  'fabri-ka':  { label: 'Fabri·Ka' },37  'food-ka':   { label: 'Food·Ka' },38  'resto-ka':  { label: 'Resto·Ka' },39  'sorti-ka':  { label: 'Sorti·Ka' },40  'crea-ka':   { label: 'Créa·Ka' },41  'job-ka':    { label: 'Job·Ka' },42  'trouve-ka': { label: 'Trouve·Ka', pm2: 'tk-web' },43  'groupe-ka': { label: 'Groupe·Ka' },44  'api-ka':    { label: 'API·Ka', pm2: 'apika-api' },45  'ka-stats':  { label: 'Ka·Stats' },46  // Agents gardiens KA Guardian — un seul repo, launchd (pas de pm2)47  'ka2': { label: 'KA·2 Guardian', unit: 'com.ka2.guardian', log: '~/cluster-projects/ka-guardian/logs/ka2.log', repoDir: '~/cluster-projects/ka-guardian' },48  'ka4': { label: 'KA·4 Guardian', unit: 'com.ka4.guardian', log: '~/cluster-projects/ka-guardian/logs/ka4.log', repoDir: '~/cluster-projects/ka-guardian' },49  'ka6': { label: 'KA·6 Guardian', unit: 'com.ka6.guardian', log: '~/cluster-projects/ka-guardian/logs/ka6.log', repoDir: '~/cluster-projects/ka-guardian' },50  // la console elle-même : sert à détecter SON nœud, ni surveillée ni projet51  'admin-ka':  { label: 'Administration·Ka', monitor: false, project: false },52};53// toute app du registre au nom « Ka » (ex. un futur toit-ka) est incluse automatiquement54const KA_NAME_RE = /(^|-)ka(\d|-|$)/i;5556// ---------- état ----------57export const SITES = [];   // rempli EN PLACE (les importateurs gardent leur référence)58let REG = { apps: {}, updated: null, gateway: null, fetchedAt: 0, source: null, error: null, mtime: 0 };59let SELF = 'M3U96a';       // repli tant que le registre n'a pas dit où tourne admin-ka60let cachePath = null;61let onChange = () => {};62let sshHosts = new Set();63const toolingChecked = {}; // node -> ts du dernier contrôle outillage6465function expandHome(p) { return p && p.startsWith('~') ? path.join(HOME, p.slice(1)) : p; }66function loadSshHosts() {67  try {68    const txt = fs.readFileSync(path.join(HOME, '.ssh', 'config'), 'utf8');69    sshHosts = new Set([...txt.matchAll(/^Host\s+(\S+)\s*$/gm)].map(m => m[1]).filter(h => h !== '*'));70  } catch { sshHosts = new Set(); }71}7273// ---------- construction des sites depuis le registre ----------74function webProcess(app, processes, cat) {75  if (cat.pm2) return cat.pm2;76  const biz = processes.filter(p => !/(^|-)ngrok$/.test(p));77  return biz.find(p => /-web$/.test(p)) || biz.find(p => p === app) || biz[0] || null;78}7980function buildSites() {81  const apps = REG.apps || {};82  const names = [83    ...Object.keys(KA_CATALOG).filter(a => apps[a]),84    ...Object.keys(apps).filter(a => !KA_CATALOG[a] && KA_NAME_RE.test(a)).sort(),85  ];86  const next = [];87  for (const app of names) {88    const cat = KA_CATALOG[app] || {};89    const e = apps[app];90    if (cat.monitor === false && cat.project === false) continue;91    const processes = Array.isArray(e.processes) ? e.processes : [];92    const launchd = Array.isArray(e.launchd) ? e.launchd : [];93    const unit = cat.unit || (processes.length === 0 && launchd.length ? launchd[0] : null);94    next.push({95      app, label: cat.label || e.label || app,96      domain: e.domain || null, node: e.node, ip: e.ip || null, port: e.port || null,97      dir: e.dir, repoDir: cat.repoDir || e.dir,98      pm2: unit ? null : webProcess(app, processes, cat),99      unit, log: cat.log || null,100      processes, launchd,101      deployed: e.deployed || null, status: e.status || null, updated: e.updated || null,102      monitor: cat.monitor !== false, project: cat.project !== false,103    });104  }105  return next;106}107108function diffSites(before, after) {109  const ev = [];110  const b = new Map(before.map(s => [s.app, s]));111  const a = new Map(after.map(s => [s.app, s]));112  for (const [app, s] of a) {113    const o = b.get(app);114    if (!o) ev.push({ kind: 'added', app, label: s.label, to: s.node });115    else if (o.node !== s.node) ev.push({ kind: 'moved', app, label: s.label, from: o.node, to: s.node });116    else if (o.dir !== s.dir || o.port !== s.port) ev.push({ kind: 'changed', app, label: s.label, node: s.node, dir: s.dir, port: s.port });117  }118  for (const [app, o] of b) if (!a.has(app)) ev.push({ kind: 'removed', app, label: o.label, from: o.node });119  return ev;120}121122function applyRegistry(data, source, mtime) {123  const apps = data && data.apps;124  if (!apps || typeof apps !== 'object' || !Object.keys(apps).length) throw new Error('registre sans apps');125  const before = SITES.slice();126  const prevSelf = SELF;127  REG = { apps, updated: data.updated || null, gateway: data.gateway || null, fetchedAt: Date.now(), source, error: null, mtime: mtime || Date.now() };128  if (apps['admin-ka'] && apps['admin-ka'].node) SELF = apps['admin-ka'].node;129  const next = buildSites();130  SITES.length = 0; SITES.push(...next);131  const events = diffSites(before, next);132  if (prevSelf !== SELF) events.push({ kind: 'self', from: prevSelf, to: SELF });133  return events;134}135136// ---------- cache local (poussé par mld) ----------137function readCache() {138  if (!cachePath || !fs.existsSync(cachePath)) return false;139  const st = fs.statSync(cachePath);140  if (st.mtimeMs === REG.mtime && SITES.length) return false;141  const data = JSON.parse(fs.readFileSync(cachePath, 'utf8'));142  const events = applyRegistry(data, 'cache:' + cachePath, st.mtimeMs);143  return events;144}145function writeCache(text) {146  const tmp = cachePath + '.tmp';147  fs.writeFileSync(tmp, text);148  fs.renameSync(tmp, cachePath);149}150151// ---------- tirage ssh ----------152function sshCat(host) {153  return new Promise((resolve) => {154    execFile('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', host, 'cat ~/dispatch/registry.json'],155      { timeout: 25_000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, PATH: ENV_PATH, HOME } },156      (err, stdout, stderr) => resolve(err ? { error: (stderr || err.message).trim().slice(0, 200) } : { text: String(stdout) }));157  });158}159160export async function refreshRegistry(reason = 'periodic') {161  let lastErr = null;162  for (const host of GATEWAY_HOSTS) {163    if (!sshHosts.has(host) && host !== 'gitsrv') continue;164    const r = await sshCat(host);165    if (r.error) { lastErr = `${host}: ${r.error}`; continue; }166    let data;167    try { data = JSON.parse(r.text); } catch (e) { lastErr = `${host}: JSON invalide (${e.message})`; continue; }168    try {169      const events = applyRegistry(data, `ssh:${host}`, Date.now());170      if (cachePath) { try { writeCache(JSON.stringify(data, null, 2)); REG.mtime = fs.statSync(cachePath).mtimeMs; } catch {} }171      finish(events, reason);172      return { ok: true, source: `ssh:${host}`, events };173    } catch (e) { lastErr = `${host}: ${e.message}`; }174  }175  REG.error = lastErr || 'aucun hôte passerelle';176  return { ok: false, error: REG.error };177}178179function finish(events, reason) {180  if (events && events.length) {181    for (const ev of events) console.log(`[registre] ${describe(ev)} (${reason})`);182    try { onChange(events); } catch {}183  }184}185186export function describe(ev) {187  switch (ev.kind) {188    case 'moved': return `${ev.label} a déménagé ${ev.from} → ${ev.to}`;189    case 'added': return `${ev.label} apparaît dans le registre (${ev.to})`;190    case 'removed': return `${ev.label} a quitté le registre (était sur ${ev.from})`;191    case 'changed': return `${ev.label} : répertoire/port changés sur ${ev.node} (${ev.dir}:${ev.port})`;192    case 'self': return `la console admin-ka tourne maintenant sur ${ev.to} (avant ${ev.from})`;193    default: return JSON.stringify(ev);194  }195}196197// ---------- API pour le reste du backend ----------198export function selfNode() { return SELF; }199export function getNodes() {200  const set = new Set(SITES.filter(s => s.monitor).map(s => s.node));201  set.add(SELF);202  return [...set].sort();203}204export function ipOf(node) {205  for (const s of SITES) if (s.node === node && s.ip) return s.ip;206  for (const e of Object.values(REG.apps || {})) if (e.node === node && e.ip) return e.ip;207  return null;208}209// cible ssh d'un nœud : l'alias de ~/.ssh/config s'il existe (noms Bonjour, résistants au210// DHCP), sinon user@IP LAN du registre (un nœud jamais vu par cette config reste joignable).211export function sshTarget(node) {212  if (sshHosts.has(node)) return node;213  const ip = ipOf(node);214  return ip ? `${USER}@${ip}` : node;215}216export function getSites() { return SITES.filter(s => s.monitor); }217export function getProjects() {218  return SITES.filter(s => s.project).map(s => ({219    id: `${s.app}@${s.node}`, app: s.app, name: s.label,220    node: s.node === SELF ? null : s.node,221    dir: s.node === SELF ? expandHome(s.repoDir) : s.repoDir,222    nodeLabel: s.node,223  }));224}225// accepte les anciens ids « app@nœud » même si l'app a déménagé depuis226export function findProject(id) {227  if (!id) return null;228  const app = String(id).split('@')[0];229  return getProjects().find(p => p.app === app) || null;230}231export function getRegistry() {232  const age = REG.fetchedAt ? Date.now() - REG.fetchedAt : null;233  const missing = Object.keys(KA_CATALOG).filter(a => KA_CATALOG[a].monitor !== false && !(REG.apps || {})[a]);234  return {235    updated: REG.updated, gateway: REG.gateway, fetchedAt: REG.fetchedAt, source: REG.source, error: REG.error,236    ageMs: age, stale: age == null || age > STALE_MS, nApps: Object.keys(REG.apps || {}).length,237    self: SELF, nodes: getNodes(), missing, cache: cachePath,238    sites: SITES.map(s => ({ app: s.app, label: s.label, node: s.node, ip: s.ip, port: s.port, dir: s.dir, repoDir: s.repoDir, pm2: s.pm2, unit: s.unit, processes: s.processes, status: s.status, deployed: s.deployed, updated: s.updated, domain: s.domain })),239  };240}241242// Bloc texte injecté dans les prompts système (upgradeur, orchestrateur) : jamais de table figée.243export function topologyText() {244  const when = REG.updated ? REG.updated.replace('T', ' ') : 'inconnu';245  const lines = SITES.map(s => `- ${s.app} → nœud ${s.node}${s.node === SELF ? ' (local)' : ''}, repo ${s.repoDir}${s.pm2 ? `, pm2 ${s.processes.filter(p => !/ngrok$/.test(p)).join('/')}` : s.unit ? `, launchd ${s.unit}` : ''}${s.port ? `, port ${s.port}` : ''}`);246  return `Emplacements ACTUELS des apps (registre mld de la passerelle M1M32, mis à jour ${when} — les apps peuvent être déplacées par \`mld move\`, ne jamais supposer un nœud de mémoire ; pour vérifier : \`ssh gitsrv cat ~/dispatch/registry.json\`) :\n${lines.join('\n')}`;247}248249// ---------- CLAUDE.md de l'orchestrateur multi-sites (régénéré à chaque changement) ----------250export function orchestratorDoc() {251  const when = REG.updated ? REG.updated.replace('T', ' ') : 'inconnu';252  const rows = SITES.map(s => `| ${s.app.padEnd(10)} | ${s.node.padEnd(7)} | ${s.repoDir.padEnd(34)} | ${s.unit ? `launchd ${s.unit}` : s.processes.filter(p => !/ngrok$/.test(p)).join(', ')} |`).join('\n');253  const locals = SITES.filter(s => s.node === SELF).map(s => s.app).join(', ') || '(aucune)';254  const nodes = getNodes().filter(n => n !== SELF);255  return `# 🎛️ Orchestrateur multi-sites Groupe KA256257Tu es une session Claude Code **orchestratrice** lancée sur le nœud **${SELF}** (là où tourne la console administration-ka.com). Ton rôle : réaliser une tâche qui touche **plusieurs apps du Groupe KA à la fois**, en gardant le **contexte complet** d'un bout à l'autre (contrairement à la diffusion qui lance une session isolée par app). Tu travailles sur les repos de PROD **à distance via SSH** sur le nœud de chaque app.258259## Table de déploiement (GÉNÉRÉE depuis le registre mld — ne pas éditer à la main)260261Source de vérité : \`M1M32:~/dispatch/registry.json\` (mis à jour ${when}). Ce fichier est régénéré par admin-ka à chaque changement du registre ; en cas de doute : \`ssh gitsrv cat ~/dispatch/registry.json\` ou \`ssh M1M32 ~/maclustr-dispatch/bin/mld status\`.262263| App        | Nœud    | Répertoire de prod                 | Process |264|------------|---------|------------------------------------|---------|265${rows}266267- Les apps sur **${SELF}** (${locals}) : tu es déjà dessus → travaille en local (\`cd <dir>\`), pas de SSH.268- Les autres : \`ssh <nœud> 'cd <dir> && …'\`. Alias SSH configurés sur ${SELF} pour ${nodes.join(', ')} (noms Bonjour .local en interne). ⚠️ *.maclustr.io est bloqué entre nœuds ; utilise les alias, ou \`simon-pierreboucher@<IP LAN>\` du registre pour un nœud sans alias.269- Pour copier un fichier vers un nœud : \`scp fichier <nœud>:<dir>/\` ou édite via un heredoc en SSH.270- Une app peut être **déplacée par \`mld move\`** entre deux de tes tours : relis cette table (elle est réécrite) plutôt que de te fier à ta mémoire.271272## Méthode d'orchestration (garde le contexte de TOUTE la tâche)2732741. **Planifie d'abord** : liste les apps concernées et ce qui doit changer sur chacune. Utilise TodoWrite pour suivre l'avancement app par app.2752. **Explore avant d'agir** : sur 1-2 apps représentatives, lis le code pour comprendre le pattern commun (les apps KA partagent beaucoup : ka-ui, ecosystem.json, structure back/front). Décide d'une approche cohérente réutilisable partout.2763. **Applique app par app** (ou par nœud, en parallèle avec \`&\`+\`wait\` quand c'est sûr). Pour chaque app : édite le repo de prod, build si besoin, \`pm2 restart <process>\`, vérifie le healthcheck/site, puis \`git add/commit/push origin main\` (origin = spbgit).2774. **Rapporte un bilan final** : tableau des apps modifiées, commits, et ce qui reste.278279## Conventions (rappel)280- Remote-first, origin = **spbgit** (git perso sur la passerelle M1M32, alias \`gitsrv\` — PAS GitHub). Standards UI KA (header/nav opaques, --z-*, mobile-first, ordre DOM=visuel des fiches). i18n fr-CA.281- Ne jamais laisser une app avec des modifs non commitées / non redéployées.282- Outils dispo sur chaque nœud (dans ~/.claude/.env) : Serper, Tavily, Scrapfly, Bright Data, Oxylabs, Apify — pour les tâches de connecteurs.283- Sudo si besoin : \`echo '492592' | sudo -S <cmd>\`.284285## Process (colonne « Process » ci-dessus : pm2 sauf mention launchd)286Les tunnels \`<app>-ngrok\` ne sont jamais redémarrés (ils portent le domaine). En cas de doute : \`pm2 jlist\` sur le nœud. EXCEPTION KA Guardian (ka2/ka4/ka6) : launchd, pas pm2 — restart via \`launchctl kickstart -k gui/$(id -u)/com.ka2.guardian\` (idem ka4/ka6), health \`curl localhost:8799|8899|8999/health\` ; voir le CLAUDE.md du repo ka-guardian.287288Travaille avec rigueur : une tâche multi-sites bien orchestrée > 13 modifs incohérentes.289`;290}291export function writeOrchestratorDoc(file) {292  try {293    const next = orchestratorDoc();294    let cur = null;295    try { cur = fs.readFileSync(file, 'utf8'); } catch {}296    if (cur === next) return false;297    fs.mkdirSync(path.dirname(file), { recursive: true });298    fs.writeFileSync(file, next);299    console.log(`[registre] ${file} régénéré`);300    return true;301  } catch (e) { console.error('[registre] CLAUDE.md orchestrateur :', e.message); return false; }302}303304// ---------- outillage des nœuds distants (perm-mcp + CLAUDE.md contextuel) ----------305function nodeClaudeMd(node) {306  const hosted = SITES.filter(s => s.node === node).map(s => `${s.app} (${s.repoDir}${s.unit ? `, launchd ${s.unit}` : s.processes.length ? `, pm2 ${s.processes.filter(p => !/ngrok$/.test(p)).join('/')}` : ''})`);307  return `# ⚠️ TU ES SUR LE NŒUD ${node} DU CLUSTER MACLUSTR (pas le laptop)308309Tu es Claude Code exécuté **sur ${node}**, lancé à distance depuis administration-ka.com (console sur ${SELF}). Le répertoire courant est un **repo de production déployé sur CE nœud** : c'est la source de vérité (doctrine remote-first Groupe Ka).310311- Après toute modification : build si nécessaire, \`pm2 restart <app>\`, vérifier le healthcheck, puis \`git add/commit/push origin main\` (origin = gitsrv/spbgit, le git perso sur la passerelle M1M32 — PAS GitHub).312- **Où sont les autres apps ?** Le registre mld fait foi, jamais une table en dur : \`ssh gitsrv cat ~/dispatch/registry.json\` (nœud, IP LAN, dir, port, process PM2 de chaque app).313- ⚠️ Depuis un nœud, SSH vers *.maclustr.io est BLOQUÉ (ACL Tailscale). Seul l'alias \`gitsrv\` (M1M32) est garanti ; les autres nœuds par leur IP LAN 192.168.2.x du registre, si la clé locale y est autorisée.314- Clés API (Anthropic, Serper, Tavily, Scrapfly, Bright Data, Oxylabs, Apify) : \`~/.claude/.env\`, exportées dans tes sessions. Ne jamais installer de paquets globaux : venv / node_modules du repo.315- Sudo local : \`echo '492592' | sudo -S <cmd>\`.316- Apps hébergées ici d'après le registre (${REG.updated || 'date inconnue'}) : ${hosted.join(' ; ') || '(aucune connue)'}.317`;318}319function sshExec(target, script, timeoutMs = 30_000) {320  return new Promise((resolve) => {321    execFile('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=12', target, script],322      { timeout: timeoutMs, maxBuffer: 2 * 1024 * 1024, env: { ...process.env, PATH: ENV_PATH, HOME } },323      (err, stdout, stderr) => resolve({ rc: err ? (err.code ?? 1) : 0, out: String(stdout || ''), err: String(stderr || '') }));324  });325}326const toolingInflight = {}; // node -> Promise (évite deux contrôles concurrents au démarrage)327export function ensureNodeTooling(node, opts = {}) {328  if (toolingInflight[node]) return toolingInflight[node];329  const p = _ensureNodeTooling(node, opts).finally(() => { delete toolingInflight[node]; });330  toolingInflight[node] = p;331  return p;332}333async function _ensureNodeTooling(node, { force = false } = {}) {334  if (node === SELF) return { node, local: true };335  if (!force && Date.now() - (toolingChecked[node] || 0) < 6 * 3600_000) return { node, cached: true };336  const target = sshTarget(node);337  const local = path.join(__dirname, 'perm-mcp.js');338  const probe = await sshExec(target, `md5 -q ~/.adminka/perm-mcp.js 2>/dev/null || echo none; test -s ~/.claude/CLAUDE.md && echo claude-md:ok || echo claude-md:none; test -x /opt/homebrew/bin/claude && echo claude:ok || echo claude:none; test -s ~/.claude/.env && echo env:ok || echo env:none`);339  if (probe.rc !== 0) return { node, error: `ssh ${target} : ${probe.err.slice(0, 160)}` };340  toolingChecked[node] = Date.now();341  const lines = probe.out.trim().split('\n');342  const result = { node, target, actions: [] };343  const localMd5 = await new Promise(r => execFile('md5', ['-q', local], (e, o) => r(e ? null : String(o).trim())));344  if (localMd5 && lines[0] !== localMd5) {345    const src = fs.readFileSync(local, 'utf8');346    const w = await sshExec(target, `mkdir -p ~/.adminka && cat > ~/.adminka/perm-mcp.js <<'__ADMINKA_EOF__'\n${src}\n__ADMINKA_EOF__\necho ok`);347    result.actions.push(w.out.includes('ok') ? 'perm-mcp.js installé/mis à jour' : `perm-mcp.js ÉCHEC ${w.err.slice(0, 100)}`);348  }349  if (lines.includes('claude-md:none')) {350    const md = nodeClaudeMd(node);351    const w = await sshExec(target, `mkdir -p ~/.claude && cat > ~/.claude/CLAUDE.md <<'__ADMINKA_EOF__'\n${md}\n__ADMINKA_EOF__\necho ok`);352    result.actions.push(w.out.includes('ok') ? 'CLAUDE.md contextuel créé' : `CLAUDE.md ÉCHEC ${w.err.slice(0, 100)}`);353  }354  result.claude = lines.includes('claude:ok');355  result.env = lines.includes('env:ok');356  if (!result.claude) result.actions.push('⚠ /opt/homebrew/bin/claude ABSENT — les sessions Claude Code y échoueront (npm i -g @anthropic-ai/claude-code)');357  if (!result.env) result.actions.push('⚠ ~/.claude/.env ABSENT — copier les clés API');358  if (result.actions.length) console.log(`[registre] outillage ${node} : ${result.actions.join(' ; ')}`);359  return result;360}361export async function ensureAllNodesTooling(opts) {362  const out = [];363  for (const n of getNodes()) out.push(await ensureNodeTooling(n, opts));364  return out;365}366367// ---------- démarrage ----------368export async function initRegistry({ dataDir, onChange: cb, orchestratorDocPath } = {}) {369  cachePath = path.join(dataDir, 'registry.json');370  onChange = (events) => {371    if (orchestratorDocPath) writeOrchestratorDoc(orchestratorDocPath);372    ensureAllNodesTooling().catch(() => {});373    cb && cb(events);374  };375  loadSshHosts();376  let events = [];377  try { events = readCache() || []; } catch (e) { console.error('[registre] cache illisible :', e.message); }378  if (!SITES.length) {379    const r = await refreshRegistry('startup');380    if (!r.ok) console.error('[registre] INDISPONIBLE au démarrage :', r.error, '— aucun site connu tant que le registre ne répond pas');381  } else {382    console.log(`[registre] ${SITES.length} apps Ka depuis le cache (registre ${REG.updated}), nœud local ${SELF}`);383    refreshRegistry('startup-refresh').catch(() => {});384  }385  if (orchestratorDocPath) writeOrchestratorDoc(orchestratorDocPath);386  ensureAllNodesTooling().catch(() => {});387  if (events.length) finish(events, 'startup');388  // 1) le push mld modifie data/registry.json → application en quelques secondes389  fs.watchFile(cachePath, { interval: WATCH_MS }, () => {390    try { const ev = readCache(); if (ev) finish(ev, 'push mld'); } catch (e) { console.error('[registre] cache :', e.message); }391  });392  // 2) tirage périodique (si un push a raté ou si mld tourne sans abonné)393  setInterval(() => { refreshRegistry('periodic').catch(() => {}); }, PULL_MS);394  setInterval(loadSshHosts, 30 * 60_000);395}396
Files · ee1f78227355f7efe4a3ebffbf93acfb5e6404a1
Menu

Theme