← back to Designer Wallcoverings
auto-save: 2026-07-31T12:27:46 (1 files) — DW-Agents/vendor-command-center/server.js
2673366cb02c41881fd248b18bd6bcc98790b103 · 2026-07-31 12:27:54 -0700 · Steve Abrams
Files touched
M DW-Agents/vendor-command-center/server.js
Diff
commit 2673366cb02c41881fd248b18bd6bcc98790b103
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Jul 31 12:27:54 2026 -0700
auto-save: 2026-07-31T12:27:46 (1 files) — DW-Agents/vendor-command-center/server.js
---
DW-Agents/vendor-command-center/server.js | 112 ++++++++++++++++++++++++++++++
1 file changed, 112 insertions(+)
diff --git a/DW-Agents/vendor-command-center/server.js b/DW-Agents/vendor-command-center/server.js
index 09f16455..ba0d97dc 100644
--- a/DW-Agents/vendor-command-center/server.js
+++ b/DW-Agents/vendor-command-center/server.js
@@ -2596,6 +2596,84 @@ app.get('/api/vendors/:code/products', async (req, res) => {
}
});
+// --- API: Live Shopify verify — mirror-vs-live drift check (TK-10084) ---
+// READ-ONLY (productsCount only). Click-to-verify from the Vendors tab; never auto-run.
+app.get('/api/vendors/:code/live-verify', async (req, res) => {
+ try {
+ const code = req.params.code;
+ const { rows: vendorRows } = await pool.query(
+ 'SELECT vendor_name, vendor_code FROM vendor_registry WHERE vendor_code = $1',
+ [code]
+ );
+ if (!vendorRows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+
+ // Same pattern resolution as syncVendorCounts (map first, registry-name fallback)
+ const patterns = VENDOR_NAME_MAP[code] || (vendorRows[0].vendor_name ? [vendorRows[0].vendor_name + '%'] : null);
+ if (!patterns || !patterns.length) {
+ return res.status(400).json({ success: false, error: 'No Shopify vendor mapping configured' });
+ }
+
+ // Mirror side: same ILIKE patterns as syncVendorCounts, per-status
+ const conditions = patterns.map((_, i) => `vendor ILIKE $${i + 1}`).join(' OR ');
+ const mirRes = await pool.query(
+ `SELECT COUNT(CASE WHEN UPPER(status) = 'ACTIVE' THEN 1 END) AS active_cnt,
+ COUNT(CASE WHEN UPPER(status) = 'DRAFT' THEN 1 END) AS draft_cnt,
+ COUNT(CASE WHEN UPPER(status) = 'ARCHIVED' THEN 1 END) AS arch_cnt
+ FROM shopify_products WHERE ${conditions}`,
+ patterns
+ );
+ const mirror = {
+ active: parseInt(mirRes.rows[0].active_cnt) || 0,
+ draft: parseInt(mirRes.rows[0].draft_cnt) || 0,
+ archived: parseInt(mirRes.rows[0].arch_cnt) || 0
+ };
+
+ // Live side: productsCount per exact pattern base per status, summed across bases.
+ // Exact vendor-string match may undercount vs the mirror's ILIKE '<base>%' wildcards.
+ const bases = [...new Set(patterns.map(p => p.replace(/%+$/, '').trim()).filter(Boolean))];
+ const live = { active: 0, draft: 0, archived: 0 };
+ const gqlUrl = `https://${SHOPIFY_DOMAIN}/admin/api/2024-10/graphql.json`;
+ for (const base of bases) {
+ for (const status of ['active', 'draft', 'archived']) {
+ const q = `vendor:'${base.replace(/'/g, "\\'")}' status:${status}`;
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), 10000);
+ try {
+ const resp = await fetch(gqlUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_TOKEN
+ },
+ body: JSON.stringify({ query: `{ productsCount(query: ${JSON.stringify(q)}) { count } }` }),
+ signal: controller.signal
+ });
+ const j = await resp.json();
+ live[status] += (j.data && j.data.productsCount && j.data.productsCount.count) || 0;
+ } finally {
+ clearTimeout(timer);
+ }
+ await new Promise(r => setTimeout(r, 300)); // pace Shopify calls
+ }
+ }
+
+ const drift = live.active !== mirror.active || live.draft !== mirror.draft || live.archived !== mirror.archived;
+ res.json({
+ success: true,
+ vendor: vendorRows[0].vendor_name,
+ code,
+ patterns: bases,
+ live,
+ mirror,
+ drift,
+ note: 'live = exact vendor-string productsCount per pattern base; may undercount vs mirror ILIKE wildcard patterns',
+ checkedAt: new Date().toISOString()
+ });
+ } catch (err) {
+ res.status(500).json({ success: false, error: err.message });
+ }
+});
+
// --- Catalog Browse API (infinite scroll + phase progress) ---
app.get('/api/catalog-browse', async (req, res) => {
try {
@@ -7286,6 +7364,40 @@ function renderTable(list) {
shopTd.textContent = Number(shopCount).toLocaleString();
if (shopCount > 0) shopTd.style.color = '#60a5fa';
else shopTd.style.color = 'var(--text-muted)';
+ // Live-verify button (TK-10084): click-to-check mirror vs live Shopify productsCount
+ var lvBtn = document.createElement('button');
+ lvBtn.textContent = '✓';
+ lvBtn.title = 'Verify against live Shopify (productsCount, read-only)';
+ lvBtn.style.cssText = 'margin-left:6px;padding:0 5px;font-size:10px;cursor:pointer;background:transparent;border:1px solid var(--border);border-radius:4px;color:var(--text-muted);';
+ lvBtn.addEventListener('click', (function(code, btn) { return function(e) {
+ e.stopPropagation();
+ btn.disabled = true; btn.textContent = '…';
+ fetch('/api/vendors/' + code + '/live-verify').then(function(r) { return r.json(); }).then(function(j) {
+ var badge = document.createElement('span');
+ badge.style.cssText = 'margin-left:6px;padding:1px 6px;border-radius:8px;font-size:10px;font-weight:700;white-space:nowrap;';
+ if (!j.success) {
+ badge.textContent = 'ERR';
+ badge.style.background = '#7f1d1d'; badge.style.color = '#fca5a5';
+ badge.title = j.error || 'verify failed';
+ } else {
+ var delta = Math.abs(j.live.active - j.mirror.active) + Math.abs(j.live.draft - j.mirror.draft) + Math.abs(j.live.archived - j.mirror.archived);
+ badge.title = 'live active/draft/archived: ' + j.live.active + '/' + j.live.draft + '/' + j.live.archived +
+ ' | mirror: ' + j.mirror.active + '/' + j.mirror.draft + '/' + j.mirror.archived;
+ if (j.drift) {
+ badge.textContent = 'DRIFT ' + delta.toLocaleString();
+ badge.style.background = '#78350f'; badge.style.color = '#fbbf24';
+ } else {
+ badge.textContent = 'LIVE ✓';
+ badge.style.background = '#14532d'; badge.style.color = '#4ade80';
+ }
+ }
+ btn.replaceWith(badge);
+ }).catch(function(err) {
+ btn.disabled = false; btn.textContent = '✓';
+ btn.title = 'verify failed: ' + err;
+ });
+ }; })(v.vendor_code, lvBtn));
+ shopTd.appendChild(lvBtn);
tr.appendChild(shopTd);
// Not on Shopify (gap)
← cf9719e9 auto-save: 2026-07-31T11:57:35 (1 files) — DW-Agents/vendor-
·
back to Designer Wallcoverings
·
auto-save: 2026-07-31T14:58:42 (3 files) — DW-Agents/vendor- 73a7b19c →