[object Object]

← back to Vendor Agents Viewer

Honest agent status: resolve real pm2 name per port, flag conflict (port reused by other app) vs true online. 0 vendor agents actually running.

913eff2516a12d0b79e6172bbc56b55ddc18ca24 · 2026-06-10 07:45:18 -0700 · Steve

Files touched

Diff

commit 913eff2516a12d0b79e6172bbc56b55ddc18ca24
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jun 10 07:45:18 2026 -0700

    Honest agent status: resolve real pm2 name per port, flag conflict (port reused by other app) vs true online. 0 vendor agents actually running.
---
 public/index.html | 11 +++++-----
 server.js         | 66 +++++++++++++++++++++++++++++++++++++++----------------
 2 files changed, 53 insertions(+), 24 deletions(-)

diff --git a/public/index.html b/public/index.html
index 4c4806d..6a82a58 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,7 +4,7 @@
 <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
 <title>Vendor Agents — Review · Launch · Restart</title>
 <style>
-  :root{--bg:#0f1115;--card:#1a1d24;--line:#2a2f3a;--txt:#e8eaed;--mut:#9aa0aa;--on:#34d399;--off:#6b7280;--accent:#34e2f4;--cols:4}
+  :root{--bg:#0f1115;--card:#1a1d24;--line:#2a2f3a;--txt:#e8eaed;--mut:#9aa0aa;--on:#34d399;--off:#6b7280;--conf:#f59e0b;--accent:#34e2f4;--cols:4}
   *{box-sizing:border-box}
   body{margin:0;background:var(--bg);color:var(--txt);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
   header{padding:18px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(15,17,21,.96);backdrop-filter:blur(8px);z-index:5}
@@ -17,11 +17,11 @@
   input[type=range]{accent-color:var(--accent)}
   .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px;padding:18px 22px}
   .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px;display:flex;flex-direction:column;gap:8px}
-  .card.on{border-left:3px solid var(--on)} .card.off{border-left:3px solid var(--off)}
+  .card.on{border-left:3px solid var(--on)} .card.off{border-left:3px solid var(--off)} .card.conflict{border-left:3px solid var(--conf)}
   .row1{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
   .nm{font-weight:650;font-size:15px} .ven{color:var(--mut);font-size:12px}
   .badge{font-size:11px;padding:2px 8px;border-radius:20px;font-weight:600;white-space:nowrap}
-  .badge.on{background:rgba(52,211,153,.15);color:var(--on)} .badge.off{background:rgba(107,114,128,.18);color:var(--off)}
+  .badge.on{background:rgba(52,211,153,.15);color:var(--on)} .badge.off{background:rgba(107,114,128,.18);color:var(--off)} .badge.conflict{background:rgba(245,158,11,.16);color:var(--conf)}
   .desc{color:var(--mut);font-size:12.5px;min-height:34px}
   .kv{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px}
   .kv span{color:var(--mut)} .kv b{color:var(--txt)}
@@ -50,7 +50,7 @@
         <option value="updated">Recently Updated</option>
       </select>
     </label>
-    <label>Show <select id="filter"><option value="all">All</option><option value="online">Online</option><option value="offline">Offline</option></select></label>
+    <label>Show <select id="filter"><option value="all">All</option><option value="online">Online</option><option value="offline">Offline</option><option value="conflict">Port Conflict</option></select></label>
     <label>Density <input type="range" id="dens" min="2" max="7" value="4"></label>
     <button class="act" onclick="load()">↻ Refresh</button>
   </div>
@@ -88,6 +88,7 @@ function render(){
         <span>Images <b>${(x.catalog_with_images||0).toLocaleString()}</b></span>
         ${x.website_score!=null?`<span>Site <b>${x.website_score}</b></span>`:''}
       </div>
+      ${x.conflict_owner?`<div class="when" style="color:var(--conf)">\u26a0 port :${x.agent_port} reused by <b>${x.conflict_owner}</b> \u2014 agent not running</div>`:""}
       <div class="when" title="${x.updated_at||''}">🕓 updated ${fmtDate(x.updated_at)}</div>
       <div class="btns">
         ${x.url?`<a class="act go" href="${x.url}" target="_blank">Open</a>`:''}
@@ -101,7 +102,7 @@ async function load(){
   const r=await fetch('/api/agents');const j=await r.json();
   if(!j.ok){$('#stats').textContent='error: '+j.error;return;}
   ALL=j.agents;const s=j.summary;
-  $('#stats').innerHTML=`<span><b>${s.total}</b> agents</span><span><b style="color:var(--on)">${s.online}</b> online</span><span><b>${s.offline}</b> offline</span><span><b>${s.totalCatalog.toLocaleString()}</b> catalog SKUs</span><span><b>${s.totalShopify.toLocaleString()}</b> on Shopify</span>`;
+  $('#stats').innerHTML=`<span><b>${s.total}</b> agents</span><span><b style="color:var(--on)">${s.online}</b> online</span><span><b>${s.offline}</b> offline</span><span><b style="color:#f59e0b">${s.conflict||0}</b> port-conflict</span><span><b>${s.totalCatalog.toLocaleString()}</b> catalog SKUs</span><span><b>${s.totalShopify.toLocaleString()}</b> on Shopify</span>`;
   render();
 }
 // persist controls
diff --git a/server.js b/server.js
index cb6362f..4e087f0 100644
--- a/server.js
+++ b/server.js
@@ -20,19 +20,28 @@ const pool = new Pool({
 app.use(express.json());
 app.use(express.static(__dirname + '/public'));
 
-// --- live listening ports on Kamatera (cached 30s) ---
-let portCache = { at: 0, ports: new Set() };
-function liveAgentPorts() {
-  return new Promise((resolve) => {
-    if (Date.now() - portCache.at < 30000) return resolve(portCache.ports);
-    execFile('ssh', ['-o', 'ConnectTimeout=12', SSH, "ss -tln 2>/dev/null | grep -oE ':(9[0-9]{3}) ' | tr -d ': '"],
-      { timeout: 20000 }, (err, stdout) => {
-        const ports = new Set();
-        if (!err && stdout) stdout.split('\n').forEach(l => { const n = parseInt(l.trim(), 10); if (n) ports.add(n); });
-        portCache = { at: Date.now(), ports };
-        resolve(ports);
-      });
+// --- live port -> real pm2 name map on Kamatera (cached 30s) ---
+// The registry's pm2_name is unreliable; resolve the ACTUAL pm2 process that
+// owns each agent_port so restart/launch hit the right process.
+let portCache = { at: 0, map: {} };
+function sshRaw(cmd) {
+  return new Promise((resolve) => execFile('ssh', ['-o', 'ConnectTimeout=15', SSH, cmd],
+    { timeout: 25000, maxBuffer: 8 * 1024 * 1024 }, (e, out) => resolve(out || '')));
+}
+async function livePortMap() {
+  if (Date.now() - portCache.at < 30000) return portCache.map;
+  // port -> pid  (from ss)   and   pid -> name (from pm2)
+  const ssTxt = await sshRaw("ss -tlnp 2>/dev/null | grep -oE ':(9[0-9]{3}) +.*pid=[0-9]+'");
+  const pm2Txt = await sshRaw("pm2 jlist 2>/dev/null");
+  const pidName = {};
+  try { JSON.parse(pm2Txt).forEach(p => { pidName[String(p.pid)] = p.name; }); } catch {}
+  const map = {};
+  ssTxt.split('\n').forEach(l => {
+    const m = l.match(/:(9\d{3})\s.*pid=(\d+)/);
+    if (m) map[m[1]] = pidName[m[2]] || ('pid:' + m[2]);
   });
+  portCache = { at: Date.now(), map };
+  return map;
 }
 
 function describe(a) {
@@ -45,7 +54,7 @@ function describe(a) {
 // --- API: all agents with status + usage ---
 app.get('/api/agents', async (req, res) => {
   try {
-    const live = await liveAgentPorts();
+    const live = await livePortMap();
     const { rows } = await pool.query(`
       SELECT vendor_code, vendor_name, agent_name, agent_port, pm2_name, website_url,
              catalog_count, shopify_count, catalog_with_images, catalog_on_shopify,
@@ -53,16 +62,35 @@ app.get('/api/agents', async (req, res) => {
       FROM vendor_registry
       WHERE agent_port IS NOT NULL OR agent_name IS NOT NULL OR pm2_name IS NOT NULL
       ORDER BY agent_port NULLS LAST, vendor_name`);
-    const agents = rows.map(a => ({
-      ...a,
-      status: a.agent_port && live.has(Number(a.agent_port)) ? 'online' : 'offline',
-      description: describe(a),
-      url: a.agent_port ? `http://45.61.58.125:${a.agent_port}/` : null,
-    }));
+    const slug = s => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+    const agents = rows.map(a => {
+      const realPm2 = a.agent_port ? live[String(a.agent_port)] : null;
+      // An agent is truly ONLINE only if the live process owning its port plausibly
+      // matches it. Many registry ports have been reused by unrelated apps.
+      const matches = realPm2 && (
+        realPm2 === a.pm2_name ||
+        (a.pm2_name && slug(realPm2) === slug(a.pm2_name)) ||
+        (a.vendor_name && slug(realPm2).includes(slug(a.vendor_name).slice(0, 8))) ||
+        (a.agent_name && slug(realPm2).includes(slug(a.agent_name)))
+      );
+      let status = 'offline';
+      if (matches) status = 'online';
+      else if (realPm2) status = 'conflict'; // port reused by a different app
+      return {
+        ...a,
+        status,
+        pm2_name: matches ? realPm2 : a.pm2_name,
+        registry_pm2: a.pm2_name,
+        conflict_owner: (!matches && realPm2) ? realPm2 : null,
+        description: describe(a),
+        url: a.agent_port ? `http://45.61.58.125:${a.agent_port}/` : null,
+      };
+    });
     const summary = {
       total: agents.length,
       online: agents.filter(x => x.status === 'online').length,
       offline: agents.filter(x => x.status === 'offline').length,
+      conflict: agents.filter(x => x.status === 'conflict').length,
       totalCatalog: agents.reduce((s, x) => s + (x.catalog_count || 0), 0),
       totalShopify: agents.reduce((s, x) => s + (x.shopify_count || 0), 0),
     };

← 338a1ba initial scaffold — vendor agents viewer (review/launch/resta  ·  back to Vendor Agents Viewer  ·  Launch infra: generic agent-host.js (per-vendor, env-paramet 1e2344e →