[object Object]

← back to Marketing Command Center

vendors panel: '👥 My vendor contacts (N)' button + /contacts + /contacts/sheet routes (internal contact sheet in the command center; hidden if no data)

f5354e1eca233b2687f5e186566f6e6db6ffc56a · 2026-08-31 13:28:20 -0700 · Steve Abrams

Files touched

Diff

commit f5354e1eca233b2687f5e186566f6e6db6ffc56a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 13:28:20 2026 -0700

    vendors panel: '👥 My vendor contacts (N)' button + /contacts + /contacts/sheet routes (internal contact sheet in the command center; hidden if no data)
---
 modules/vendors/index.js   | 14 ++++++++++++++
 public/panels/vendors.html |  9 ++++++---
 public/panels/vendors.js   | 12 ++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/modules/vendors/index.js b/modules/vendors/index.js
index 18a984a..fecdc07 100644
--- a/modules/vendors/index.js
+++ b/modules/vendors/index.js
@@ -302,5 +302,19 @@ module.exports = {
       }
       res.json({ ok: true, harvested: ok, failed, total: targets.length });
     });
+
+    // GET /contacts — "who I know inside each DW vendor" (LinkedIn connections ×
+    // vendor_registry, built by scripts/build-vendor-contact-sheet.mjs). INTERNAL/PII;
+    // this panel is Basic-Auth gated. Reference only — never a public surface.
+    const LI_CONTACTS_JSON = path.join(__dirname, '..', '..', 'data', 'vendor-contacts.json');
+    const LI_CONTACTS_HTML = path.join(__dirname, '..', '..', 'data', 'vendor-contacts.html');
+    router.get('/contacts', (_req, res) => {
+      try { res.json(JSON.parse(fs.readFileSync(LI_CONTACTS_JSON, 'utf8'))); }
+      catch { res.json({ vendors: 0, totalContacts: 0, rows: [], note: 'Run scripts/build-vendor-contact-sheet.mjs first.' }); }
+    });
+    router.get('/contacts/sheet', (_req, res) => {
+      try { res.type('html').send(fs.readFileSync(LI_CONTACTS_HTML, 'utf8')); }
+      catch { res.type('html').send('<p style="font:16px sans-serif;margin:40px">No contact sheet yet — run <code>scripts/build-vendor-contact-sheet.mjs</code>.</p>'); }
+    });
   },
 };
diff --git a/public/panels/vendors.html b/public/panels/vendors.html
index 47b6f5b..e2229e1 100644
--- a/public/panels/vendors.html
+++ b/public/panels/vendors.html
@@ -36,9 +36,12 @@
 <div class="card" id="vend-li-card">
   <div class="row" style="justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">
     <h2 style="margin:0">Vendor brands · LinkedIn</h2>
-    <div style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
-      <button id="vp-li-harvest" class="btn amp-strong" title="Public Open-Graph pull (thumbnail + text) of every mapped vendor LinkedIn company page — no API, no login">⬇ Harvest ALL LinkedIn pages</button>
-      <small class="muted" id="vp-li-when"></small>
+    <div style="display:flex;align-items:center;gap:8px">
+      <a id="vp-li-contacts" class="btn" target="_blank" rel="noopener" href="/api/vendors/contacts/sheet" title="Who I know inside each DW vendor — from my LinkedIn connections (internal)" style="text-decoration:none;display:none">👥 My vendor contacts <span id="vp-li-contacts-n"></span></a>
+      <div style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
+        <button id="vp-li-harvest" class="btn amp-strong" title="Public Open-Graph pull (thumbnail + text) of every mapped vendor LinkedIn company page — no API, no login">⬇ Harvest ALL LinkedIn pages</button>
+        <small class="muted" id="vp-li-when"></small>
+      </div>
     </div>
   </div>
   <div class="muted" style="margin:8px 0 4px">Vendor LinkedIn <b>company pages</b> (B2B/trade). Public Open-Graph pull only — <b>no LinkedIn API, no login</b>. <b>Reference + attribution-amplify only</b>: browse, copy the text, share with a link back to the vendor. <span style="color:#8a6d2f">✓ verified</span> = slug confirmed against the real company page; <span style="color:#a1341f">? unverified</span> = best guess, confirm before trusting.</div>
diff --git a/public/panels/vendors.js b/public/panels/vendors.js
index c1721ea..db0d344 100644
--- a/public/panels/vendors.js
+++ b/public/panels/vendors.js
@@ -17,6 +17,18 @@ window.MCC_PANELS['vendors'] = {
     };
     setStats();
 
+    // "My vendor contacts" — reveal the button + count if the internal contact sheet exists.
+    (async () => {
+      try {
+        const c = await (await fetch('/api/vendors/contacts')).json();
+        if (c && c.totalContacts > 0) {
+          const a = root.querySelector('#vp-li-contacts');
+          root.querySelector('#vp-li-contacts-n').textContent = `(${c.totalContacts})`;
+          if (a) a.style.display = '';
+        }
+      } catch { /* no sheet yet — button stays hidden */ }
+    })();
+
     // Owned · DW Fleet — all 35 DW-OWNED Instagram accounts from the canonical
     // source (window.MCC_ACCOUNTS), each showing its LAST 6 posts inline (thumb +
     // title + date · type), mirroring the external-vendor rows. The owned posts come

← 2e93a4c vendors: strict LinkedIn-connection x vendor_registry contac  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-31T13:51:21 (1 data files) — pub 67ef2f1 →