[object Object]

← back to Marketing Command Center

Vendor IG Reporting panel: DW + 37 vendor Instagram accounts roster (33 found, ~4.5M combined reach), sortable, for social/competitive reporting

dc913ce0aceab3bc4d48606f1ca80a996ee3c7d9 · 2026-06-10 08:52:16 -0700 · Steve

Files touched

Diff

commit dc913ce0aceab3bc4d48606f1ca80a996ee3c7d9
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jun 10 08:52:16 2026 -0700

    Vendor IG Reporting panel: DW + 37 vendor Instagram accounts roster (33 found, ~4.5M combined reach), sortable, for social/competitive reporting
---
 data/vendor-instagram.json | 40 ++++++++++++++++++++++++++++++++++++++++
 modules/registry.js        |  1 +
 modules/vendors/index.js   | 39 +++++++++++++++++++++++++++++++++++++++
 public/panels/vendors.html | 14 ++++++++++++++
 public/panels/vendors.js   | 35 +++++++++++++++++++++++++++++++++++
 5 files changed, 129 insertions(+)

diff --git a/data/vendor-instagram.json b/data/vendor-instagram.json
new file mode 100644
index 0000000..9a984a7
--- /dev/null
+++ b/data/vendor-instagram.json
@@ -0,0 +1,40 @@
+[
+  { "brand": "Designer Wallcoverings", "vendorCode": "dw", "handle": "@designerwallcoverings", "url": "https://www.instagram.com/designerwallcoverings/", "followers": "15K", "verified": false, "note": "our account" },
+  { "brand": "1838 Wallcoverings", "vendorCode": "1838", "handle": "@1838_wallcoverings", "url": "https://www.instagram.com/1838_wallcoverings/", "followers": "20K", "verified": false, "note": "" },
+  { "brand": "Arteriors Home", "vendorCode": "arteriors", "handle": "@arteriorshome", "url": "https://www.instagram.com/arteriorshome/", "followers": "346K", "verified": false, "note": "" },
+  { "brand": "Atomic50 Ceilings", "vendorCode": "atomic50", "handle": "none found", "url": "", "followers": "", "verified": false, "note": "no official IG" },
+  { "brand": "Bespoke Wallcoverings", "vendorCode": "bespoke", "handle": "none found", "url": "", "followers": "", "verified": false, "note": "no official IG" },
+  { "brand": "BN Walls", "vendorCode": "bnwalls", "handle": "@bnwallsofficial", "url": "https://www.instagram.com/bnwallsofficial/", "followers": "12K", "verified": false, "note": "" },
+  { "brand": "Brewster Home Fashions", "vendorCode": "brewster", "handle": "@brewsterhome", "url": "https://www.instagram.com/brewsterhome/", "followers": "18K", "verified": false, "note": "" },
+  { "brand": "Colour & Design", "vendorCode": "colouranddesign", "handle": "@colour.design", "url": "https://www.instagram.com/colour.design/", "followers": "1.8K", "verified": false, "note": "" },
+  { "brand": "Contrado", "vendorCode": "contrado", "handle": "@contradouk", "url": "https://www.instagram.com/contradouk/", "followers": "47K", "verified": false, "note": "" },
+  { "brand": "Dedar", "vendorCode": "dedar", "handle": "@dedarmilano", "url": "https://www.instagram.com/dedarmilano/", "followers": "218K", "verified": false, "note": "" },
+  { "brand": "DeNovo Wall", "vendorCode": "denovowall", "handle": "@denovowall", "url": "https://www.instagram.com/denovowall/", "followers": "1.4K", "verified": false, "note": "" },
+  { "brand": "Designtex", "vendorCode": "designtex", "handle": "@designtex_inc", "url": "https://www.instagram.com/designtex_inc/", "followers": "30K", "verified": false, "note": "" },
+  { "brand": "Fabricut", "vendorCode": "fabricut", "handle": "@fabricut", "url": "https://www.instagram.com/fabricut/", "followers": "97K", "verified": false, "note": "" },
+  { "brand": "Folia Fabrics", "vendorCode": "folia", "handle": "none found", "url": "", "followers": "", "verified": false, "note": "no official IG" },
+  { "brand": "Hygge & West", "vendorCode": "hygge", "handle": "@hyggeandwest", "url": "https://www.instagram.com/hyggeandwest/", "followers": "219K", "verified": false, "note": "" },
+  { "brand": "Justin David Textiles", "vendorCode": "justindavid", "handle": "@justindavidhospitalitytextiles", "url": "https://www.instagram.com/justindavidhospitalitytextiles/", "followers": "1.9K", "verified": false, "note": "" },
+  { "brand": "Knoll Textiles", "vendorCode": "knoll", "handle": "@knolltextiles", "url": "https://www.instagram.com/knolltextiles/", "followers": "41K", "verified": false, "note": "" },
+  { "brand": "Maya Romanoff", "vendorCode": "maya-romanoff", "handle": "@mayaromanoff", "url": "https://www.instagram.com/mayaromanoff/", "followers": "26K", "verified": false, "note": "" },
+  { "brand": "MINDTHEGAP", "vendorCode": "mindthegap", "handle": "@mindtgap", "url": "https://www.instagram.com/mindtgap/", "followers": "83K", "verified": false, "note": "" },
+  { "brand": "Mural Source", "vendorCode": "muralsource", "handle": "@themuralsource", "url": "https://www.instagram.com/themuralsource/", "followers": "41K", "verified": false, "note": "" },
+  { "brand": "Naturale 54", "vendorCode": "naturale54", "handle": "none found", "url": "", "followers": "", "verified": false, "note": "no official IG" },
+  { "brand": "Osborne & Little", "vendorCode": "osborne", "handle": "@osborneandlittle", "url": "https://www.instagram.com/osborneandlittle/", "followers": "225K", "verified": false, "note": "" },
+  { "brand": "Rebel Walls", "vendorCode": "rebel-walls", "handle": "@rebelwalls", "url": "https://www.instagram.com/rebelwalls/", "followers": "357K", "verified": false, "note": "" },
+  { "brand": "Romo", "vendorCode": "romo", "handle": "@romo_fabrics", "url": "https://www.instagram.com/romo_fabrics/", "followers": "135K", "verified": false, "note": "" },
+  { "brand": "Stout Textiles", "vendorCode": "stout", "handle": "@stouttextiles", "url": "https://www.instagram.com/stouttextiles/", "followers": "23K", "verified": false, "note": "" },
+  { "brand": "Timorous Beasties", "vendorCode": "timorous", "handle": "@timorous_beasties", "url": "https://www.instagram.com/timorous_beasties/", "followers": "150K", "verified": false, "note": "" },
+  { "brand": "WallQuest", "vendorCode": "wallquest", "handle": "@wallquestofficial", "url": "https://www.instagram.com/wallquestofficial/", "followers": "10K", "verified": false, "note": "" },
+  { "brand": "York Wallcoverings", "vendorCode": "york", "handle": "@york_wallcoverings", "url": "https://www.instagram.com/york_wallcoverings/", "followers": "100K", "verified": false, "note": "" },
+  { "brand": "F. Schumacher", "vendorCode": "schumacher", "handle": "none found", "url": "", "followers": "", "verified": false, "note": "only personal accounts found — verify @schumacher1889" },
+  { "brand": "Thibaut", "vendorCode": "thibaut", "handle": "@thibaut_1886", "url": "https://www.instagram.com/thibaut_1886/", "followers": "294K", "verified": false, "note": "" },
+  { "brand": "Arte International", "vendorCode": "arte", "handle": "@artewalls", "url": "https://www.instagram.com/artewalls/", "followers": "363K", "verified": false, "note": "" },
+  { "brand": "Koroseal", "vendorCode": "koroseal", "handle": "@koroseal", "url": "https://www.instagram.com/koroseal/", "followers": "4.6K", "verified": false, "note": "" },
+  { "brand": "Wolf Gordon", "vendorCode": "wolf-gordon", "handle": "@wolfgordon", "url": "https://www.instagram.com/wolfgordon/", "followers": "11K", "verified": false, "note": "" },
+  { "brand": "Carnegie Fabrics", "vendorCode": "carnegie", "handle": "@carnegiefabrics", "url": "https://www.instagram.com/carnegiefabrics/", "followers": "13K", "verified": false, "note": "" },
+  { "brand": "Scalamandré", "vendorCode": "scalamandre", "handle": "@scalamandre", "url": "https://www.instagram.com/scalamandre/", "followers": "117K", "verified": false, "note": "" },
+  { "brand": "Sister Parish Design", "vendorCode": "sister-parish", "handle": "@sisterparishdesign1933", "url": "https://www.instagram.com/sisterparishdesign1933/", "followers": "125K", "verified": false, "note": "private-labeled by DW as Saybrook House" },
+  { "brand": "Cole & Son", "vendorCode": "cole-son", "handle": "@cole_and_son_wallpapers", "url": "https://www.instagram.com/cole_and_son_wallpapers/", "followers": "483K", "verified": false, "note": "" },
+  { "brand": "Phillip Jeffries", "vendorCode": "phillip-jeffries", "handle": "@phillipjeffriesltd", "url": "https://www.instagram.com/phillipjeffriesltd/", "followers": "524K", "verified": false, "note": "" }
+]
diff --git a/modules/registry.js b/modules/registry.js
index a06b19d..504c03a 100644
--- a/modules/registry.js
+++ b/modules/registry.js
@@ -15,4 +15,5 @@ module.exports = [
   'performance',        // performance dashboard: email + GA4 KPIs, charts, top campaigns
   'social',             // social scheduler: IG/TikTok queue board, gated publish
   'segments',           // audience segments: rule builder + live preview over contacts
+  'vendors',            // vendor IG reporting: DW + all vendor Instagram accounts roster
 ];
diff --git a/modules/vendors/index.js b/modules/vendors/index.js
new file mode 100644
index 0000000..2f3b525
--- /dev/null
+++ b/modules/vendors/index.js
@@ -0,0 +1,39 @@
+// Vendor IG Reporting — directory of DW's + all vendor Instagram accounts, for
+// competitive/social reporting. Reads data/vendor-instagram.json (curated; refresh
+// via research). Read-only. Pairs with the advertising-signals tooling for deeper
+// per-vendor ad/social intel later.
+const fs = require('fs');
+const path = require('path');
+
+const DATA = path.join(__dirname, '..', '..', 'data', 'vendor-instagram.json');
+function load() { try { return JSON.parse(fs.readFileSync(DATA, 'utf8')); } catch { return []; } }
+function followersToNum(f) {
+  if (!f) return 0;
+  const m = String(f).trim().match(/^([\d.]+)\s*([kKmM]?)/);
+  if (!m) return 0;
+  const n = parseFloat(m[1]); const u = m[2].toLowerCase();
+  return Math.round(n * (u === 'm' ? 1e6 : u === 'k' ? 1e3 : 1));
+}
+
+module.exports = {
+  id: 'vendors',
+  title: 'Vendor IG Reporting',
+  icon: '📷',
+  mount(router) {
+    router.get('/accounts', (_req, res) => {
+      const rows = load().map(r => ({ ...r, followersNum: followersToNum(r.followers), hasIG: r.handle && r.handle !== 'none found' }));
+      const withIG = rows.filter(r => r.hasIG);
+      const reach = withIG.reduce((s, r) => s + r.followersNum, 0);
+      res.json({
+        accounts: rows,
+        stats: {
+          total: rows.length,
+          withIG: withIG.length,
+          missing: rows.length - withIG.length,
+          totalReach: reach,
+          dw: rows.find(r => r.vendorCode === 'dw') || null,
+        },
+      });
+    });
+  },
+};
diff --git a/public/panels/vendors.html b/public/panels/vendors.html
new file mode 100644
index 0000000..ec73221
--- /dev/null
+++ b/public/panels/vendors.html
@@ -0,0 +1,14 @@
+<div id="vend-banner"></div>
+<div class="card" id="vend-stats" style="display:flex;gap:30px;flex-wrap:wrap;align-items:center">
+  <div><div style="font:600 26px/1 'Cormorant Garamond',serif" id="vs-total">…</div><small class="muted">vendor brands</small></div>
+  <div><div style="font:600 26px/1 'Cormorant Garamond',serif" id="vs-ig">…</div><small class="muted">with Instagram</small></div>
+  <div><div style="font:600 26px/1 'Cormorant Garamond',serif" id="vs-reach">…</div><small class="muted">combined reach</small></div>
+  <div><div style="font:600 26px/1 'Cormorant Garamond',serif" id="vs-missing">…</div><small class="muted">no IG found</small></div>
+  <div style="margin-left:auto"><label>Sort</label>
+    <select id="vs-sort"><option value="followers">Followers ↓</option><option value="brand">Brand A→Z</option><option value="code">Vendor code</option></select></div>
+</div>
+<div class="card">
+  <h2>DW + Vendor Instagram accounts</h2>
+  <div class="muted" style="margin-bottom:10px">Click any handle to open the account. Use this roster for competitive/social reporting.</div>
+  <div id="vend-rows"></div>
+</div>
diff --git a/public/panels/vendors.js b/public/panels/vendors.js
new file mode 100644
index 0000000..d52b858
--- /dev/null
+++ b/public/panels/vendors.js
@@ -0,0 +1,35 @@
+window.MCC_PANELS = window.MCC_PANELS || {};
+window.MCC_PANELS['vendors'] = {
+  async init(root) {
+    const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
+    const fmt = n => n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? Math.round(n / 1e3) + 'K' : (n || '—');
+    let data;
+    try { data = await (await fetch('/api/vendors/accounts')).json(); } catch { root.querySelector('#vend-rows').innerHTML = '<div class="muted">Failed to load.</div>'; return; }
+    const s = data.stats || {};
+    root.querySelector('#vs-total').textContent = s.total || 0;
+    root.querySelector('#vs-ig').textContent = s.withIG || 0;
+    root.querySelector('#vs-reach').textContent = fmt(s.totalReach || 0);
+    root.querySelector('#vs-missing').textContent = s.missing || 0;
+    const render = (sort) => {
+      let rows = [...data.accounts];
+      if (sort === 'brand') rows.sort((a, b) => a.brand.localeCompare(b.brand));
+      else if (sort === 'code') rows.sort((a, b) => (a.vendorCode || '').localeCompare(b.vendorCode || ''));
+      else rows.sort((a, b) => (b.followersNum || 0) - (a.followersNum || 0));
+      root.querySelector('#vend-rows').innerHTML = rows.map(r => {
+        const dw = r.vendorCode === 'dw';
+        const handle = r.hasIG
+          ? `<a class="lnk" href="${esc(r.url)}" target="_blank" rel="noopener noreferrer">${esc(r.handle)} ↗</a>`
+          : `<span class="muted">no IG</span>`;
+        return `<div class="row" style="justify-content:space-between;border-bottom:1px solid var(--line);padding:9px 2px;align-items:center${dw ? ';background:#faf6ee' : ''}">
+          <div style="min-width:200px"><b>${esc(r.brand)}</b>${dw ? ' <span class="pill">OURS</span>' : ''}${r.note && !dw ? ` <span class="muted" style="font-size:11px">${esc(r.note)}</span>` : ''}</div>
+          <div style="flex:1">${handle}</div>
+          <div style="min-width:70px;text-align:right;font-variant-numeric:tabular-nums">${r.hasIG ? fmt(r.followersNum) : ''}</div>
+        </div>`;
+      }).join('');
+    };
+    render('followers');
+    root.querySelector('#vs-sort').onchange = e => render(e.target.value);
+    root.querySelector('#vend-banner').innerHTML = s.missing
+      ? `<div class="muted-banner">${s.withIG} of ${s.total} brands have an official Instagram — ${s.missing} have none found (Atomic50, Bespoke, Folia, Naturale 54, Schumacher). Combined vendor reach ≈ ${fmt(s.totalReach)} followers.</div>` : '';
+  },
+};

← d54875f 3 more panels (parallel agents): Performance dashboard (KPIs  ·  back to Marketing Command Center  ·  Channels publishing engine: FB/IG/TikTok/YouTube adapters (r a85f5b2 →