[object Object]

← back to Designer Wallcoverings

auto-save: 2026-06-24T07:25:27 (2 files) — vendor-scrapers/china-seas-refresh DW-Agents/vendor-command-center/server.js.backup-20260624-072033

fa89fca73d6fc39e9badbe2b9838edbae58bed6a · 2026-06-24 07:25:33 -0700 · Steve Abrams

Files touched

Diff

commit fa89fca73d6fc39e9badbe2b9838edbae58bed6a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jun 24 07:25:33 2026 -0700

    auto-save: 2026-06-24T07:25:27 (2 files) — vendor-scrapers/china-seas-refresh DW-Agents/vendor-command-center/server.js.backup-20260624-072033
---
 .../server.js.backup-20260624-072033               | 11907 +++++++++++++++++++
 1 file changed, 11907 insertions(+)

diff --git a/DW-Agents/vendor-command-center/server.js.backup-20260624-072033 b/DW-Agents/vendor-command-center/server.js.backup-20260624-072033
new file mode 100644
index 00000000..696176c4
--- /dev/null
+++ b/DW-Agents/vendor-command-center/server.js.backup-20260624-072033
@@ -0,0 +1,11907 @@
+/**
+ * Victor - Vendor Command Center Dashboard
+ * Central vendor management hub for Designer Wallcoverings
+ * Port: 9660
+ */
+
+require('dotenv').config({ path: '/root/.env' });
+if (!process.env.SHOPIFY_ADMIN_TOKEN || !process.env.SHOPIFY_ORDERS_TOKEN) {
+  console.error('FATAL: SHOPIFY_ADMIN_TOKEN / SHOPIFY_ORDERS_TOKEN not set in /root/.env');
+  process.exit(1);
+}
+
+// Global crash handlers — surface errors to PM2 error log instead of silent exit
+process.on('uncaughtException', (err) => {
+  console.error('[Victor] UNCAUGHT EXCEPTION:', err.stack || err.message);
+  process.exit(1);
+});
+process.on('unhandledRejection', (reason) => {
+  console.error('[Victor] UNHANDLED REJECTION:', reason);
+});
+
+const express = require('express');
+const { Pool } = require('pg');
+const http = require('http');
+const { scoreWebsite } = require('./website-scorer');
+const stdWorker = require('./standardization-worker');
+const { processImage, uploadCroppedToShopify } = require('./logo-cropper');
+const { validateVendor, getDiscontinuationSummary } = require('./discontinuation-validator');
+
+const app = express();
+const PORT = 9660;
+
+// --- Database ---
+const pool = new Pool({
+  connectionString: process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp',
+  max: 10,
+  idleTimeoutMillis: 30000,
+});
+
+// --- Live Activity Log (ring buffer, max 200) ---
+const activityLog = [];
+const MAX_ACTIVITY = 200;
+function logActivity(type, message, details) {
+  const entry = {
+    ts: new Date().toISOString(),
+    type: type,     // 'sync', 'health', 'api', 'agent', 'error', 'info'
+    message: message,
+    details: details || null
+  };
+  activityLog.unshift(entry);
+  if (activityLog.length > MAX_ACTIVITY) activityLog.length = MAX_ACTIVITY;
+}
+logActivity('info', 'Victor starting up on port ' + PORT);
+
+// --- Agent Health Cache (30s TTL) ---
+const healthCache = {};
+const HEALTH_CACHE_TTL = 30000;
+
+function checkAgentHealthCached(port) {
+  const now = Date.now();
+  if (healthCache[port] && (now - healthCache[port].ts) < HEALTH_CACHE_TTL) {
+    return Promise.resolve(healthCache[port].online);
+  }
+  const prevState = healthCache[port] ? healthCache[port].online : null;
+  return new Promise((resolve) => {
+    const authHeader = 'Basic ' + Buffer.from(process.env.GEORGE_BASIC_AUTH || '').toString('base64');
+    const req = http.get({
+      hostname: '127.0.0.1', port: port, path: '/health', timeout: 3000,
+      headers: { 'Authorization': authHeader }
+    }, (res) => {
+      let data = '';
+      res.on('data', (chunk) => { data += chunk; });
+      res.on('end', () => {
+        const ok = res.statusCode >= 200 && res.statusCode < 400;
+        if (prevState !== null && prevState !== ok) {
+          logActivity('health', `Agent on port ${port} went ${ok ? 'ONLINE' : 'OFFLINE'}`);
+        }
+        healthCache[port] = { online: ok, ts: now };
+        resolve(ok);
+      });
+    });
+    req.on('error', () => {
+      if (prevState === true) logActivity('health', `Agent on port ${port} went OFFLINE (unreachable)`);
+      healthCache[port] = { online: false, ts: now }; resolve(false);
+    });
+    req.on('timeout', () => {
+      req.destroy();
+      if (prevState === true) logActivity('health', `Agent on port ${port} went OFFLINE (timeout)`);
+      healthCache[port] = { online: false, ts: now }; resolve(false);
+    });
+  });
+}
+
+// --- Middleware ---
+app.use(express.json());
+
+// Auth middleware — session-cookie first, basic-auth fallback.
+// 2026-06-12 refactor: the browser used to embed btoa('admin:<password>') in
+// every client fetch (the password shipped to the client). Now a successful
+// basic-auth mints an httpOnly `vcc_session` cookie derived from the admin
+// password via HMAC; same-origin fetches authenticate via that cookie, so NO
+// credential is ever embedded in browser JS. Rotating the password rotates the
+// HMAC secret, which invalidates every old cookie automatically.
+const crypto = require('crypto');
+function vccSessionToken() {
+  const secret = process.env.VCC_ADMIN_PASS || process.env.GEORGE_BASIC_AUTH_PASS || '';
+  if (!secret) return '';
+  return crypto.createHmac('sha256', secret).update('vcc-session-v1').digest('hex');
+}
+function vccParseCookies(req) {
+  const out = {};
+  (req.headers.cookie || '').split(';').forEach(function (p) {
+    const i = p.indexOf('='); if (i > 0) out[p.slice(0, i).trim()] = p.slice(i + 1).trim();
+  });
+  return out;
+}
+function requireAuth(req, res, next) {
+  const expected = vccSessionToken();
+  // 1. Valid session cookie → in (constant-time compare; length-guarded).
+  if (expected) {
+    const got = vccParseCookies(req).vcc_session || '';
+    if (got.length === expected.length &&
+        crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) return next();
+  }
+  // 2. Basic-auth fallback (browser dialog / page load) — on success mint the cookie.
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) {
+    res.setHeader('WWW-Authenticate', 'Basic realm="Vendor Command Center"');
+    return res.status(401).send('Authentication required');
+  }
+  const decoded = Buffer.from(auth.split(' ')[1], 'base64').toString();
+  const idx = decoded.indexOf(':');
+  const user = decoded.slice(0, idx), pass = decoded.slice(idx + 1);
+  const ok = (process.env.VCC_ADMIN_PASS && user === 'admin' && pass === process.env.VCC_ADMIN_PASS) ||
+             (process.env.GEORGE_BASIC_AUTH_PASS && user === 'admin' && pass === process.env.GEORGE_BASIC_AUTH_PASS);
+  if (ok) {
+    if (expected) res.setHeader('Set-Cookie', 'vcc_session=' + expected + '; HttpOnly; SameSite=Strict; Path=/; Max-Age=43200');
+    return next();
+  }
+  res.setHeader('WWW-Authenticate', 'Basic realm="Vendor Command Center"');
+  return res.status(401).send('Invalid credentials');
+}
+
+// --- Health Check (no auth) ---
+app.get('/health', async (req, res) => {
+  try {
+    const result = await pool.query('SELECT COUNT(*) as cnt FROM vendor_registry');
+    res.json({
+      status: 'healthy',
+      service: 'vendor-command-center',
+      codename: 'Victor',
+      port: PORT,
+      vendors: parseInt(result.rows[0].cnt),
+      timestamp: new Date().toISOString(),
+      uptime: process.uptime()
+    });
+  } catch (err) {
+    res.status(500).json({ status: 'error', error: err.message });
+  }
+});
+
+// Apply auth to all other routes
+app.use(requireAuth);
+
+// --- API: Get all vendors (with agent health status + Nora mailer counts) ---
+app.get('/api/vendors', async (req, res) => {
+  try {
+    const result = await pool.query(
+      'SELECT * FROM vendor_registry ORDER BY vendor_name ASC'
+    );
+    // Get Nora pipeline run counts per vendor
+    let noraMap = {};
+    try {
+      const noraRes = await pool.query(`
+        SELECT vendor_name,
+          COUNT(*) as total_mailers,
+          COUNT(*) FILTER (WHERE status IN ('completed','awaiting_approval','needs_confirmation')) as active_mailers,
+          MAX(created_at) as latest_mailer
+        FROM nora_pipeline_runs
+        WHERE vendor_name IS NOT NULL AND vendor_name != '' AND vendor_name != 'Unknown Vendor'
+        GROUP BY vendor_name
+      `);
+      for (const r of noraRes.rows) {
+        noraMap[r.vendor_name] = { total: parseInt(r.total_mailers), active: parseInt(r.active_mailers), latest: r.latest_mailer };
+      }
+    } catch (e) { /* nora table may not exist */ }
+
+    // Check health for all agents in parallel (cached 30s)
+    const vendors = await Promise.all(result.rows.map(async (v) => {
+      const nora = noraMap[v.vendor_name] || { total: 0, active: 0, latest: null };
+      if (!v.agent_port) return { ...v, agent_status: 'no-port', nora_mailers: nora.total, nora_active: nora.active, nora_latest: nora.latest };
+      const online = await checkAgentHealthCached(v.agent_port);
+      return { ...v, agent_status: online ? 'online' : 'offline', nora_mailers: nora.total, nora_active: nora.active, nora_latest: nora.latest };
+    }));
+    res.json({ success: true, data: vendors, count: vendors.length });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Get single vendor ---
+app.get('/api/vendors/:code', async (req, res) => {
+  try {
+    const result = await pool.query(
+      'SELECT * FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (result.rows.length === 0) {
+      return res.status(404).json({ success: false, error: 'Vendor not found' });
+    }
+    res.json({ success: true, data: result.rows[0] });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Update vendor fields ---
+app.put('/api/vendors/:code', async (req, res) => {
+  try {
+    const allowedFields = [
+      'is_private_label', 'private_label_name', 'learnings', 'crawl_cron',
+      'crawl_priority', 'is_active', 'has_credentials', 'login_url',
+      'last_product_update', 'last_price_update', 'last_image_update',
+      'last_spec_update', 'last_discontinued_check', 'last_blog_post',
+      'last_instagram_post', 'next_scheduled_crawl',
+      'display_prices', 'sample_price', 'skip_shopify',
+      'vendor_discount_pct', 'pricing_unit', 'pricing_notes',
+      'show_on_brands_page'
+    ];
+    const updates = [];
+    const values = [];
+    let idx = 1;
+    for (const field of allowedFields) {
+      if (req.body[field] !== undefined) {
+        updates.push(field + ' = $' + idx);
+        values.push(req.body[field]);
+        idx++;
+      }
+    }
+    if (updates.length === 0) {
+      return res.status(400).json({ success: false, error: 'No valid fields to update' });
+    }
+    updates.push('updated_at = NOW()');
+    values.push(req.params.code);
+    const sql = 'UPDATE vendor_registry SET ' + updates.join(', ') + ' WHERE vendor_code = $' + idx + ' RETURNING *';
+    const result = await pool.query(sql, values);
+    if (result.rows.length === 0) {
+      return res.status(404).json({ success: false, error: 'Vendor not found' });
+    }
+    res.json({ success: true, data: result.rows[0] });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- Proxy helper ---
+function proxyToAgent(agentPort, path, method, body) {
+  return new Promise((resolve) => {
+    const postData = body ? JSON.stringify(body) : '';
+    const options = {
+      hostname: '127.0.0.1',
+      port: agentPort,
+      path: path,
+      method: method,
+      headers: {
+        'Content-Type': 'application/json',
+        'Content-Length': Buffer.byteLength(postData),
+        'Authorization': 'Basic ' + Buffer.from(process.env.GEORGE_BASIC_AUTH || '').toString('base64')
+      },
+      timeout: 10000
+    };
+    const proxyReq = http.request(options, (proxyRes) => {
+      let respBody = '';
+      proxyRes.on('data', (chunk) => { respBody += chunk; });
+      proxyRes.on('end', () => {
+        try { resolve({ success: true, status: proxyRes.statusCode, data: JSON.parse(respBody) }); }
+        catch (_e) { resolve({ success: true, status: proxyRes.statusCode, data: respBody }); }
+      });
+    });
+    proxyReq.on('error', (err) => {
+      resolve({ success: false, error: 'Agent not reachable: ' + err.message });
+    });
+    proxyReq.on('timeout', () => {
+      proxyReq.destroy();
+      resolve({ success: false, error: 'Agent request timed out' });
+    });
+    if (postData) proxyReq.write(postData);
+    proxyReq.end();
+  });
+}
+
+// --- API: Proxy scan to vendor agent ---
+app.post('/api/vendors/:code/scan', async (req, res) => {
+  try {
+    const vendor = await pool.query(
+      'SELECT agent_port, agent_name FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (vendor.rows.length === 0) {
+      return res.status(404).json({ success: false, error: 'Vendor not found' });
+    }
+    const { agent_port, agent_name } = vendor.rows[0];
+    if (!agent_port) {
+      return res.status(400).json({ success: false, error: 'No agent port configured' });
+    }
+    logActivity('agent', `Scan triggered for ${agent_name} (port ${agent_port})`);
+    const result = await proxyToAgent(agent_port, '/api/scan', 'POST', req.body || {});
+    res.json({ agent: agent_name, port: agent_port, ...result });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Trigger price update ---
+app.post('/api/vendors/:code/update-prices', async (req, res) => {
+  try {
+    const vendor = await pool.query(
+      'SELECT agent_port, agent_name FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (vendor.rows.length === 0) {
+      return res.status(404).json({ success: false, error: 'Vendor not found' });
+    }
+    const { agent_port, agent_name } = vendor.rows[0];
+    if (!agent_port) {
+      return res.status(400).json({ success: false, error: 'No agent port configured' });
+    }
+    const result = await proxyToAgent(agent_port, '/api/update-prices', 'POST', { action: 'update-prices' });
+    res.json({ agent: agent_name, port: agent_port, ...result });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Calendar data ---
+app.get('/api/calendar', async (req, res) => {
+  try {
+    const result = await pool.query(
+      'SELECT vendor_name, vendor_code, agent_name, crawl_cron, crawl_priority, next_scheduled_crawl, last_product_update, is_active FROM vendor_registry ORDER BY crawl_priority DESC, vendor_name ASC'
+    );
+    res.json({ success: true, data: result.rows });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Update cron schedule ---
+app.put('/api/calendar/:code', async (req, res) => {
+  try {
+    const { crawl_cron, next_scheduled_crawl } = req.body;
+    const result = await pool.query(
+      'UPDATE vendor_registry SET crawl_cron = COALESCE($1, crawl_cron), next_scheduled_crawl = COALESCE($2, next_scheduled_crawl), updated_at = NOW() WHERE vendor_code = $3 RETURNING *',
+      [crawl_cron || null, next_scheduled_crawl || null, req.params.code]
+    );
+    if (result.rows.length === 0) {
+      return res.status(404).json({ success: false, error: 'Vendor not found' });
+    }
+    res.json({ success: true, data: result.rows[0] });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Global stats ---
+app.get('/api/stats', async (req, res) => {
+  try {
+    const result = await pool.query(`
+      SELECT
+        COUNT(*) as total_vendors,
+        COUNT(*) FILTER (WHERE is_active) as active_vendors,
+        COUNT(*) FILTER (WHERE is_private_label) as private_label_count,
+        COUNT(*) FILTER (WHERE skip_shopify) as skip_shopify_count,
+        COUNT(*) FILTER (WHERE has_credentials) as with_credentials,
+        COUNT(*) FILTER (WHERE crawl_priority = 'HIGH') as high_priority,
+        COUNT(*) FILTER (WHERE crawl_priority = 'MEDIUM') as medium_priority,
+        COUNT(*) FILTER (WHERE crawl_priority = 'LOW') as low_priority,
+        COALESCE(SUM(total_products), 0) as total_products,
+        COALESCE(SUM(catalog_count), 0) as total_catalog,
+        COALESCE(SUM(shopify_count), 0) as total_shopify,
+        COALESCE(SUM(products_with_cost), 0) as total_with_cost,
+        COALESCE(SUM(products_with_images), 0) as total_with_images,
+        COALESCE(SUM(discontinued_count), 0) as total_discontinued,
+        COALESCE(SUM(catalog_not_on_shopify), 0) as total_not_on_shopify,
+        COALESCE(SUM(total_products) - SUM(products_with_images), 0) as missing_images,
+        COALESCE(SUM(catalog_with_width), 0) as total_with_width,
+        COALESCE(SUM(catalog_with_repeat), 0) as total_with_repeat,
+        COALESCE(SUM(catalog_with_about_vendor), 0) as total_with_about_vendor,
+        COALESCE(SUM(catalog_with_all_images), 0) as total_with_all_images,
+        COALESCE(SUM(catalog_with_spec_sheet), 0) as total_with_spec_sheet,
+        COALESCE(SUM(catalog_with_body_html), 0) as total_with_body_html,
+        COALESCE(SUM(shopify_draft), 0) as total_draft,
+        COALESCE(SUM(shopify_archived), 0) as total_archived,
+        COALESCE(SUM(shopify_draft), 0) as drafts_needing_review,
+        COALESCE(SUM(shopify_archived), 0) as already_archived,
+        MIN(next_scheduled_crawl) FILTER (WHERE next_scheduled_crawl > NOW()) as next_crawl,
+        ROUND(AVG(spec_completeness) FILTER (WHERE catalog_table IS NOT NULL), 1) as avg_spec_completeness,
+        COUNT(*) FILTER (WHERE spec_completeness < 50 AND catalog_table IS NOT NULL) as vendors_below_50_spec
+      FROM vendor_registry
+    `);
+    const stats = result.rows[0];
+    stats.cost_coverage_pct = stats.total_products > 0
+      ? ((stats.total_with_cost / stats.total_products) * 100).toFixed(1)
+      : '0.0';
+    stats.image_coverage_pct = stats.total_products > 0
+      ? ((stats.total_with_images / stats.total_products) * 100).toFixed(1)
+      : '0.0';
+    res.json({ success: true, data: stats });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: System status (vendor counts, catalog counts, Shopify counts, health) ---
+app.get('/api/status', async (req, res) => {
+  try {
+    const statsResult = await pool.query(`
+      SELECT
+        COUNT(*) as total_vendors,
+        COUNT(*) FILTER (WHERE is_active) as active_vendors,
+        COUNT(*) FILTER (WHERE is_private_label) as private_label_count,
+        COALESCE(SUM(catalog_count), 0) as total_catalog,
+        COALESCE(SUM(shopify_count), 0) as total_shopify,
+        COALESCE(SUM(products_with_cost), 0) as total_with_cost,
+        COALESCE(SUM(products_with_images), 0) as total_with_images,
+        COALESCE(SUM(discontinued_count), 0) as total_discontinued,
+        COALESCE(SUM(catalog_not_on_shopify), 0) as total_not_on_shopify,
+        COALESCE(SUM(catalog_with_width), 0) as total_with_width,
+        COALESCE(SUM(catalog_with_repeat), 0) as total_with_repeat,
+        ROUND(AVG(spec_completeness) FILTER (WHERE catalog_table IS NOT NULL), 1) as avg_spec_completeness
+      FROM vendor_registry
+    `);
+    const stats = statsResult.rows[0];
+
+    // Top 10 vendors by catalog count
+    const topVendors = await pool.query(`
+      SELECT vendor_name, vendor_code, catalog_count, shopify_count, agent_port
+      FROM vendor_registry
+      WHERE catalog_count > 0
+      ORDER BY catalog_count DESC
+      LIMIT 10
+    `);
+
+    res.json({
+      success: true,
+      status: 'operational',
+      service: 'vendor-command-center',
+      port: 9660,
+      uptime: process.uptime(),
+      timestamp: new Date().toISOString(),
+      vendors: {
+        total: parseInt(stats.total_vendors),
+        active: parseInt(stats.active_vendors),
+        private_label: parseInt(stats.private_label_count)
+      },
+      catalog: {
+        total_products: parseInt(stats.total_catalog),
+        on_shopify: parseInt(stats.total_shopify),
+        not_on_shopify: parseInt(stats.total_not_on_shopify),
+        with_cost: parseInt(stats.total_with_cost),
+        with_images: parseInt(stats.total_with_images),
+        with_width: parseInt(stats.total_with_width),
+        with_repeat: parseInt(stats.total_with_repeat),
+        discontinued: parseInt(stats.total_discontinued),
+        avg_spec_completeness: parseFloat(stats.avg_spec_completeness) || 0
+      },
+      top_vendors: topVendors.rows
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, status: 'error', error: err.message });
+  }
+});
+
+// --- API: Live catalog totals (real-time from pg_stat, no sync needed) ---
+app.get('/api/live-totals', async (req, res) => {
+  try {
+    // Get approximate live row counts from PostgreSQL stats (instant, no table scan)
+    // Exclude non-vendor catalogs (connie = competitive intel, vendor_catalog = generic)
+    const liveRes = await pool.query(`
+      SELECT relname, n_live_tup as approx_count
+      FROM pg_stat_user_tables
+      WHERE relname LIKE '%_catalog'
+        AND relname NOT IN ('connie_our_catalog', 'connie_competitor_catalog', 'vendor_catalog')
+      ORDER BY n_live_tup DESC
+    `);
+    const catalogTotal = liveRes.rows.reduce((sum, r) => sum + parseInt(r.approx_count || 0), 0);
+
+    // Also get shopify live count
+    const shopRes = await pool.query(`
+      SELECT n_live_tup as approx_count FROM pg_stat_user_tables WHERE relname = 'shopify_products'
+    `);
+    const shopifyTotal = shopRes.rows.length > 0 ? parseInt(shopRes.rows[0].approx_count || 0) : 0;
+
+    // Get vendor_registry totals (pre-computed, for specs/cost/images)
+    const regRes = await pool.query(`
+      SELECT
+        COALESCE(SUM(products_with_cost), 0) as with_cost,
+        COALESCE(SUM(products_with_images), 0) as with_images,
+        COALESCE(SUM(catalog_with_width), 0) as with_width,
+        COALESCE(SUM(catalog_with_repeat), 0) as with_repeat,
+        COALESCE(SUM(catalog_not_on_shopify), 0) as not_on_shopify,
+        COALESCE(SUM(shopify_draft), 0) as drafts_needing_review,
+        COALESCE(SUM(discontinued_count), 0) as discontinued_in_catalog
+      FROM vendor_registry
+    `);
+    const reg = regRes.rows[0];
+
+    res.json({
+      success: true,
+      catalog_total: catalogTotal,
+      shopify_total: shopifyTotal,
+      with_cost: parseInt(reg.with_cost),
+      with_images: parseInt(reg.with_images),
+      with_width: parseInt(reg.with_width),
+      with_repeat: parseInt(reg.with_repeat),
+      not_on_shopify: parseInt(reg.not_on_shopify),
+      to_discontinue: parseInt(reg.drafts_needing_review),
+      tables: liveRes.rows.map(r => ({ name: r.relname, count: parseInt(r.approx_count) })),
+      ts: new Date().toISOString()
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Spec completeness detail for a vendor ---
+app.get('/api/spec-completeness/:code', async (req, res) => {
+  try {
+    const { rows } = await pool.query(
+      'SELECT vendor_name, catalog_table, spec_completeness, spec_fields_present, spec_fields_total FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (rows.length === 0) return res.status(404).json({ success: false, error: 'Vendor not found' });
+
+    const v = rows[0];
+    const catTable = v.catalog_table;
+    if (!catTable) {
+      return res.json({ success: true, vendor: v.vendor_name, pct: 0, present: 0, total: REQUIRED_CATALOG_FIELDS.length, fields: {} });
+    }
+
+    // Get actual columns
+    const colRes = await pool.query(
+      `SELECT column_name FROM information_schema.columns WHERE table_name = $1`,
+      [catTable]
+    );
+    const actualCols = new Set(colRes.rows.map(r => r.column_name));
+
+    // Build per-field detail
+    const fields = {};
+    for (const field of REQUIRED_CATALOG_FIELDS) {
+      const aliases = FIELD_ALIASES[field] || [field];
+      const matchedCol = aliases.find(a => actualCols.has(a));
+      if (matchedCol) {
+        // Count non-null/non-empty values
+        try {
+          const countRes = await pool.query(
+            `SELECT COUNT(*) as filled FROM ${catTable} WHERE ${matchedCol} IS NOT NULL AND ${matchedCol}::text != ''`
+          );
+          const totalRes = await pool.query(`SELECT COUNT(*) as total FROM ${catTable}`);
+          fields[field] = {
+            status: 'present',
+            column: matchedCol,
+            filled: parseInt(countRes.rows[0].filled) || 0,
+            total: parseInt(totalRes.rows[0].total) || 0
+          };
+        } catch (_) {
+          fields[field] = { status: 'present', column: matchedCol, filled: 0, total: 0 };
+        }
+      } else {
+        fields[field] = { status: 'missing', column: null, filled: 0, total: 0 };
+      }
+    }
+
+    res.json({
+      success: true,
+      vendor: v.vendor_name,
+      pct: parseFloat(v.spec_completeness) || 0,
+      present: v.spec_fields_present || 0,
+      total: v.spec_fields_total || REQUIRED_CATALOG_FIELDS.length,
+      fields
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Spec completeness summary (all vendors) ---
+app.get('/api/spec-completeness', async (req, res) => {
+  try {
+    const { rows } = await pool.query(
+      `SELECT vendor_code, vendor_name, catalog_table, spec_completeness, spec_fields_present, spec_fields_total
+       FROM vendor_registry ORDER BY spec_completeness ASC`
+    );
+    const below50 = rows.filter(r => parseFloat(r.spec_completeness) < 50 && r.catalog_table);
+    res.json({
+      success: true,
+      data: rows,
+      below_50_count: below50.length,
+      below_50: below50.map(r => ({ code: r.vendor_code, name: r.vendor_name, pct: parseFloat(r.spec_completeness) })),
+      required_fields: REQUIRED_CATALOG_FIELDS
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Bulk action ---
+app.post('/api/bulk-action', async (req, res) => {
+  try {
+    const { action, vendorCodes } = req.body;
+    if (!action || !vendorCodes || !Array.isArray(vendorCodes) || vendorCodes.length === 0) {
+      return res.status(400).json({ success: false, error: 'action and vendorCodes[] required' });
+    }
+
+    const results = [];
+    for (const code of vendorCodes) {
+      const vendor = await pool.query(
+        'SELECT agent_port, agent_name FROM vendor_registry WHERE vendor_code = $1',
+        [code]
+      );
+      if (vendor.rows.length === 0) {
+        results.push({ code, success: false, error: 'Not found' });
+        continue;
+      }
+      const { agent_port, agent_name } = vendor.rows[0];
+
+      let path = '/api/scan';
+      if (action === 'update-prices') path = '/api/update-prices';
+      else if (action === 'update-images') path = '/api/update-images';
+      else if (action === 'check-discontinued') path = '/api/check-discontinued';
+      else if (action === 'update-specs') path = '/api/update-specs';
+
+      const agentResult = await proxyToAgent(agent_port, path, 'POST', { action });
+      results.push({ code, agent: agent_name, ...agentResult });
+    }
+    res.json({ success: true, action, results });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Check agent health ---
+app.get('/api/vendors/:code/agent-health', async (req, res) => {
+  try {
+    const vendor = await pool.query(
+      'SELECT agent_port, agent_name FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (vendor.rows.length === 0) {
+      return res.status(404).json({ success: false, error: 'Vendor not found' });
+    }
+    const { agent_port, agent_name } = vendor.rows[0];
+
+    const agentHealth = await new Promise((resolve) => {
+      const options = {
+        hostname: '127.0.0.1',
+        port: agent_port,
+        path: '/health',
+        method: 'GET',
+        timeout: 3000
+      };
+      const proxyReq = http.request(options, (proxyRes) => {
+        let body = '';
+        proxyRes.on('data', (chunk) => { body += chunk; });
+        proxyRes.on('end', () => {
+          try { resolve({ online: true, data: JSON.parse(body) }); }
+          catch (_e) { resolve({ online: true, data: body }); }
+        });
+      });
+      proxyReq.on('error', () => resolve({ online: false }));
+      proxyReq.on('timeout', () => { proxyReq.destroy(); resolve({ online: false }); });
+      proxyReq.end();
+    });
+
+    res.json({ success: true, agent: agent_name, port: agent_port, ...agentHealth });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Bulk scan (POST /api/bulk-scan) ---
+app.post('/api/bulk-scan', async (req, res) => {
+  try {
+    const { codes } = req.body;
+    if (!codes || !Array.isArray(codes) || codes.length === 0) {
+      return res.status(400).json({ success: false, error: 'codes[] required' });
+    }
+    logActivity('agent', `Bulk scan triggered for ${codes.length} vendors`);
+    const { rows } = await pool.query(
+      'SELECT vendor_code, vendor_name, agent_port FROM vendor_registry WHERE vendor_code = ANY($1)',
+      [codes]
+    );
+    const results = await Promise.allSettled(rows.map(v => {
+      return new Promise((resolve) => {
+        if (!v.agent_port) return resolve({ vendor: v.vendor_name, code: v.vendor_code, status: 'no-port' });
+        const postData = JSON.stringify({});
+        const options = {
+          hostname: '127.0.0.1', port: v.agent_port, path: '/api/scan',
+          method: 'POST', timeout: 10000,
+          headers: {
+            'Content-Type': 'application/json',
+            'Content-Length': Buffer.byteLength(postData),
+            'Authorization': 'Basic ' + Buffer.from(process.env.GEORGE_BASIC_AUTH || '').toString('base64')
+          }
+        };
+        const proxyReq = http.request(options, (proxyRes) => {
+          let data = '';
+          proxyRes.on('data', (c) => { data += c; });
+          proxyRes.on('end', () => resolve({ vendor: v.vendor_name, code: v.vendor_code, status: 'triggered' }));
+        });
+        proxyReq.on('error', () => resolve({ vendor: v.vendor_name, code: v.vendor_code, status: 'unreachable' }));
+        proxyReq.on('timeout', () => { proxyReq.destroy(); resolve({ vendor: v.vendor_name, code: v.vendor_code, status: 'timeout' }); });
+        proxyReq.write(postData);
+        proxyReq.end();
+      });
+    }));
+    res.json({
+      success: true,
+      results: results.map(r => r.status === 'fulfilled' ? r.value : { status: 'error', error: r.reason?.message })
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Get agent observations (proxy) ---
+app.get('/api/vendors/:code/observations', async (req, res) => {
+  try {
+    const { rows } = await pool.query(
+      'SELECT agent_port, vendor_name FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (!rows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+    const port = rows[0].agent_port;
+    if (!port) return res.json({ success: true, data: [], message: 'No agent port configured' });
+
+    const proxyReq = http.get('http://127.0.0.1:' + port + '/api/observations', {
+      timeout: 5000,
+      headers: { 'Authorization': 'Basic ' + Buffer.from(process.env.GEORGE_BASIC_AUTH || '').toString('base64') }
+    }, (proxyRes) => {
+      let data = '';
+      proxyRes.on('data', (chunk) => { data += chunk; });
+      proxyRes.on('end', () => {
+        try { res.json({ success: true, data: JSON.parse(data) }); }
+        catch (_e) { res.json({ success: true, data: [], raw: data }); }
+      });
+    });
+    proxyReq.on('error', () => res.json({ success: true, data: [], message: 'Agent unreachable' }));
+    proxyReq.on('timeout', () => { proxyReq.destroy(); res.json({ success: true, data: [], message: 'Timeout' }); });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- SKU Prefix Generator (compliant, obfuscated) ---
+// Rules: DW + 2 random letters from allowed set. Never reference vendor name.
+// Forbidden: B, D, S, F, M, N, V (hard to hear on phone)
+// Allowed: A, C, E, G, H, I, J, K, L, O, P, Q, R, T, U, W, X, Y, Z
+async function generateCompliantPrefix(pool) {
+  const ALLOWED = 'ACEGHIJKLOPQRTUWXYZ';
+  // Get all existing prefixes from vendor_registry
+  const { rows: existing } = await pool.query(
+    "SELECT DISTINCT sku_prefix FROM vendor_registry WHERE sku_prefix IS NOT NULL AND sku_prefix <> ''"
+  );
+  const usedPrefixes = new Set(existing.map(r => r.sku_prefix.replace(/-$/, '').toUpperCase()));
+
+  // Also check shopify_products for any DW prefixes in the wild
+  try {
+    const { rows: shopPrefixes } = await pool.query(
+      "SELECT DISTINCT SUBSTRING(dw_sku FROM 1 FOR 4) AS pfx FROM shopify_products WHERE dw_sku IS NOT NULL AND dw_sku <> '' AND dw_sku LIKE 'DW%'"
+    );
+    shopPrefixes.forEach(r => usedPrefixes.add(r.pfx.toUpperCase()));
+  } catch (_e) { /* shopify_products may not exist */ }
+
+  // Generate random 2-letter combos until we find one not in use
+  const maxAttempts = 500;
+  for (let i = 0; i < maxAttempts; i++) {
+    const c1 = ALLOWED[Math.floor(Math.random() * ALLOWED.length)];
+    const c2 = ALLOWED[Math.floor(Math.random() * ALLOWED.length)];
+    const candidate = 'DW' + c1 + c2;
+    if (!usedPrefixes.has(candidate)) {
+      return candidate + '-';
+    }
+  }
+  throw new Error('Could not generate unique SKU prefix after ' + maxAttempts + ' attempts');
+}
+
+// --- API: Find next available sku_range_start (5000 blocks) ---
+async function getNextSkuRangeStart(pool) {
+  const { rows } = await pool.query(
+    "SELECT COALESCE(MAX(sku_range_start), 195000) AS max_start FROM vendor_registry WHERE sku_range_start IS NOT NULL"
+  );
+  const maxStart = parseInt(rows[0].max_start) || 195000;
+  // Round up to next 5000 block
+  return Math.ceil((maxStart + 1) / 5000) * 5000;
+}
+
+// --- API: Assign DW SKUs to vendor catalog products ---
+app.post('/api/vendors/:code/assign-skus', async (req, res) => {
+  try {
+    const { rows: vendorRows } = await pool.query(
+      'SELECT vendor_code, vendor_name, catalog_table, sku_prefix, sku_range_start, private_label, skip_catalog_scrape FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (!vendorRows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+
+    const v = vendorRows[0];
+    if (!v.catalog_table) {
+      return res.json({ success: false, error: 'Vendor missing catalog_table' });
+    }
+
+    // SAFETY: Block SKU assignment for skip_catalog_scrape vendors (e.g., Cowtan & Tout)
+    // Private labels WITH a documented sku_prefix (rule 20/21: DWJS Jeffrey Stevens,
+    // DWPR Phillipe Romano, DWWC Architectural) ARE allowed — they must get their own DW SKUs.
+    if (v.skip_catalog_scrape) {
+      return res.status(400).json({
+        success: false,
+        error: `${v.vendor_name} is flagged skip_catalog_scrape. SKU assignment blocked.`
+      });
+    }
+    if (v.private_label && !(v.sku_prefix && v.sku_prefix.trim())) {
+      return res.status(400).json({
+        success: false,
+        error: `${v.vendor_name} is a private label brand without a documented sku_prefix. Set sku_prefix in vendor_registry first (see CLAUDE.md rule 21 for the Private Label Vendor Map).`
+      });
+    }
+
+    const catTable = v.catalog_table.replace(/[^a-z0-9_]/gi, '');
+    let prefix = (v.sku_prefix || '').trim();
+    let rangeStart = parseInt(v.sku_range_start) || 0;
+
+    // Auto-generate compliant prefix if vendor has none
+    if (!prefix) {
+      prefix = await generateCompliantPrefix(pool);
+      rangeStart = await getNextSkuRangeStart(pool);
+      await pool.query(
+        'UPDATE vendor_registry SET sku_prefix = $1, sku_range_start = $2 WHERE vendor_code = $3',
+        [prefix, rangeStart, req.params.code]
+      );
+      console.log('[Victor] Auto-generated prefix ' + prefix + ' (range ' + rangeStart + ') for ' + v.vendor_name);
+    }
+
+    // Check if dw_sku column exists
+    const colCheck = await pool.query(
+      "SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = 'dw_sku'",
+      [catTable]
+    );
+    if (!colCheck.rows.length) {
+      await pool.query(`ALTER TABLE ${catTable} ADD COLUMN IF NOT EXISTS dw_sku text DEFAULT ''`);
+    }
+
+    // Find current max DW SKU number for this prefix across ALL tables
+    // Check the catalog table itself
+    const maxInCatalog = await pool.query(
+      `SELECT MAX(CAST(NULLIF(regexp_replace(dw_sku, $1, ''), '') AS INTEGER)) as max_num FROM ${catTable} WHERE dw_sku LIKE $2`,
+      ['^' + prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), prefix + '%']
+    );
+    // Also check shopify_products for any existing SKUs with this prefix
+    let maxInShopify = { rows: [{ max_num: null }] };
+    try {
+      maxInShopify = await pool.query(
+        "SELECT MAX(CAST(NULLIF(regexp_replace(dw_sku, $1, ''), '') AS INTEGER)) as max_num FROM shopify_products WHERE dw_sku LIKE $2",
+        ['^' + prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), prefix + '%']
+      );
+    } catch (e) { /* shopify_products may not exist */ }
+
+    const maxCat = parseInt(maxInCatalog.rows[0]?.max_num) || 0;
+    const maxShop = parseInt(maxInShopify.rows[0]?.max_num) || 0;
+    let nextNum = Math.max(maxCat, maxShop, rangeStart);
+    if (nextNum < rangeStart) nextNum = rangeStart;
+
+    // Get products missing dw_sku, ordered by mfr_sku for consistency
+    const missing = await pool.query(
+      `SELECT id, mfr_sku FROM ${catTable} WHERE dw_sku IS NULL OR dw_sku = '' ORDER BY mfr_sku ASC`
+    );
+
+    if (missing.rows.length === 0) {
+      return res.json({ success: true, assigned: 0, message: 'All products already have DW SKUs' });
+    }
+
+    // Assign sequential SKUs — but if mfr_sku is already a DW SKU, reuse it
+    let assigned = 0;
+    let reused = 0;
+    for (const row of missing.rows) {
+      let dwSku;
+      if (row.mfr_sku && /^DW[A-Z]{1,3}-\d+/.test(row.mfr_sku)) {
+        // mfr_sku is already a DW SKU — reuse it, don't mint a new one
+        dwSku = row.mfr_sku;
+        reused++;
+      } else {
+        nextNum++;
+        dwSku = prefix + nextNum;
+      }
+      await pool.query(`UPDATE ${catTable} SET dw_sku = $1 WHERE id = $2`, [dwSku, row.id]);
+      assigned++;
+    }
+
+    console.log(`[Victor] Assigned ${assigned} DW SKUs to ${v.vendor_name} (${prefix}${rangeStart + 1} - ${prefix}${nextNum}, ${reused} reused existing DW SKUs)`);
+
+    res.json({
+      success: true,
+      vendor: v.vendor_name,
+      reused,
+      prefix,
+      assigned,
+      range: prefix + (nextNum - assigned + 1) + ' to ' + prefix + nextNum,
+      message: assigned + ' DW SKUs assigned to ' + v.vendor_name
+    });
+  } catch (err) {
+    console.error('[Victor] SKU assignment error:', err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Create/Find Shopify inquiry page for vendor with 0 products ---
+const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const SHOPIFY_TOKEN = process.env.SHOPIFY_ORDERS_TOKEN;
+
+// ─── Launch Test: Full enrichment pipeline — 1 newest SKU per vendor to Shopify ──
+app.post('/api/launch-test', async (req, res) => {
+  try {
+    const { vendor_codes } = req.body;
+    if (!vendor_codes || !vendor_codes.length) return res.status(400).json({ error: 'No vendor_codes provided' });
+
+    const results = [];
+    const PRODUCT_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+    const GEMINI_KEY = process.env.GEMINI_API_KEY || '';
+    const GRAPHQL_URL = `https://${SHOPIFY_DOMAIN}/admin/api/2024-10/graphql.json`;
+
+    // Helper: POST JSON to Silas via http module
+    function silasPost(path, body) {
+      return new Promise((resolve, reject) => {
+        const postData = JSON.stringify(body);
+        const r = http.request({
+          hostname: '127.0.0.1', port: 9674, path, method: 'POST',
+          headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) }
+        }, (resp) => {
+          let d = '';
+          resp.on('data', c => d += c);
+          resp.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } });
+        });
+        r.on('error', reject);
+        r.setTimeout(10000, () => { r.destroy(); reject(new Error('timeout')); });
+        r.write(postData);
+        r.end();
+      });
+    }
+
+    // Helper: Gemini AI description
+    async function generateDescription(imageUrl, title) {
+      try {
+        const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=${GEMINI_KEY}`;
+        const geminiBody = {
+          contents: [{
+            parts: [
+              { text: `You are a luxury wallcovering copywriter. Write a 2-3 sentence commercial product description for this wallcovering titled "${title}". The product image is at: ${imageUrl}\nFocus on texture, design, and the atmosphere it creates in a room. Never use the word "wallpaper" — always say "wallcovering". Be elegant and concise.` }
+            ]
+          }]
+        };
+        const controller = new AbortController();
+        const timeout = setTimeout(() => controller.abort(), 15000);
+        const resp = await fetch(geminiUrl, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify(geminiBody),
+          signal: controller.signal
+        });
+        clearTimeout(timeout);
+        const data = await resp.json();
+        if (data.candidates && data.candidates[0] && data.candidates[0].content) {
+          return data.candidates[0].content.parts[0].text.trim();
+        }
+        return null;
+      } catch (e) {
+        console.error('[VCC] Gemini description error:', e.message);
+        return null;
+      }
+    }
+
+    // Helper: GraphQL metafieldsSet
+    async function setMetafields(shopifyProductId, metafields) {
+      const mutation = `mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
+        metafieldsSet(metafields: $metafields) {
+          metafields { namespace key }
+          userErrors { field message }
+        }
+      }`;
+      const ownerId = `gid://shopify/Product/${shopifyProductId}`;
+      const variables = {
+        metafields: metafields.map(m => ({
+          ownerId,
+          namespace: m.namespace,
+          key: m.key,
+          value: m.value,
+          type: 'single_line_text_field'
+        }))
+      };
+      const resp = await fetch(GRAPHQL_URL, {
+        method: 'POST',
+        headers: { 'X-Shopify-Access-Token': PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+        body: JSON.stringify({ query: mutation, variables })
+      });
+      return await resp.json();
+    }
+
+    for (const code of vendor_codes) {
+      const steps = {};
+
+      // ── Lookup vendor ──
+      const { rows: vRows } = await pool.query(
+        'SELECT vendor_code, vendor_name, catalog_table, sku_prefix FROM vendor_registry WHERE vendor_code = $1', [code]
+      );
+      if (!vRows.length) { results.push({ vendor: code, status: 'error', reason: 'Vendor not found' }); continue; }
+      const v = vRows[0];
+      if (!v.catalog_table) { results.push({ vendor: v.vendor_name, status: 'error', reason: 'No catalog table' }); continue; }
+
+      const catTable = v.catalog_table.replace(/[^a-z0-9_]/gi, '');
+
+      // ── Check available columns ──
+      const { rows: cols } = await pool.query(
+        "SELECT column_name FROM information_schema.columns WHERE table_name=$1 AND column_name IN ('dw_sku','mfr_sku','pattern_name','color_name','width','image_url','material','collection','pattern_repeat','repeat_v','repeat_h','fire_rating','finish','application','design_style','design','match_type','roll_length','coverage','features','brand','rooms','color_primary','about_vendor','product_url')", [catTable]
+      );
+      const colSet = new Set(cols.map(c => c.column_name));
+      if (!colSet.has('dw_sku') || !colSet.has('mfr_sku')) {
+        results.push({ vendor: v.vendor_name, status: 'error', reason: 'Missing dw_sku or mfr_sku column' }); continue;
+      }
+
+      // ── STEP 1: Get newest eligible product ──
+      const widthFilter = colSet.has('width') ? "AND width IS NOT NULL AND width <> ''" : '';
+      const imageFilter = colSet.has('image_url') ? "AND image_url IS NOT NULL AND image_url <> ''" : '';
+      const colorFilter = colSet.has('color_name') ? "AND color_name IS NOT NULL AND color_name <> ''" : '';
+      const nameCol = colSet.has('pattern_name') ? 'pattern_name' : "'Product'";
+      const colorCol = colSet.has('color_name') ? 'color_name' : "''";
+
+      const selectCols = [
+        'mfr_sku', `${nameCol} as pattern_name`, `${colorCol} as color_name`, 'dw_sku',
+        colSet.has('width') ? 'width' : "'' as width",
+        colSet.has('image_url') ? 'image_url' : "'' as image_url",
+        colSet.has('material') ? 'material' : "'Non-woven' as material",
+        colSet.has('collection') ? 'collection' : `'${v.vendor_name.replace(/'/g, "''")}' as collection`,
+        colSet.has('pattern_repeat') ? 'pattern_repeat' : (colSet.has('repeat_v') ? 'repeat_v as pattern_repeat' : "NULL as pattern_repeat"),
+        colSet.has('repeat_h') ? 'repeat_h' : "NULL as repeat_h",
+        colSet.has('fire_rating') ? 'fire_rating' : "NULL as fire_rating",
+        colSet.has('finish') ? 'finish' : "NULL as finish",
+        colSet.has('application') ? 'application' : "NULL as application",
+        colSet.has('design_style') ? 'design_style' : (colSet.has('design') ? 'design as design_style' : "NULL as design_style"),
+        colSet.has('match_type') ? 'match_type' : "NULL as match_type",
+        colSet.has('roll_length') ? 'roll_length' : "NULL as roll_length",
+        colSet.has('coverage') ? 'coverage' : "NULL as coverage",
+        colSet.has('features') ? 'features' : "NULL as features",
+        colSet.has('brand') ? 'brand' : "NULL as brand",
+        colSet.has('rooms') ? 'rooms' : "NULL as rooms",
+        colSet.has('about_vendor') ? 'about_vendor' : "NULL as about_vendor"
+      ];
+
+      const { rows: products } = await pool.query(
+        `SELECT ${selectCols.join(', ')}
+         FROM "${catTable}"
+         WHERE dw_sku IS NOT NULL AND shopify_product_id IS NULL
+           ${widthFilter} ${imageFilter} ${colorFilter}
+         ORDER BY id DESC LIMIT 1`
+      );
+
+      if (!products.length) {
+        results.push({ vendor: v.vendor_name, status: 'skipped', reason: 'No eligible products (all on Shopify or missing data)' }); continue;
+      }
+
+      const p = products[0];
+      // Private label: use private_label_name, hide real vendor
+      const displayVendor = v.is_private_label && v.private_label_name ? v.private_label_name : v.vendor_name;
+      const title = [p.pattern_name, p.color_name].filter(Boolean).join(' - ') + ' Wallcovering | ' + displayVendor;
+
+      // ── STEP 2: Silas validate-push ──
+      try {
+        const silasResult = await silasPost('/api/validate-push', {
+          dw_sku: p.dw_sku, vendor_code: code, vendor_name: v.vendor_name,
+          mfr_sku: p.mfr_sku, title, image_url: p.image_url, width: p.width
+        });
+        if (!silasResult.approved) {
+          steps.silas_push = 'rejected: ' + (silasResult.reason || 'unknown');
+          results.push({ vendor: v.vendor_name, sku: p.dw_sku, mfr: p.mfr_sku, status: 'blocked_silas', reason: silasResult.reason || 'Silas rejected', steps }); continue;
+        }
+        steps.silas_push = 'approved';
+      } catch (e) {
+        steps.silas_push = 'error: ' + e.message;
+        results.push({ vendor: v.vendor_name, sku: p.dw_sku, status: 'error', reason: 'Silas unavailable: ' + e.message, steps }); continue;
+      }
+
+      // ── STEP 3: Generate AI description via Gemini ──
+      let aiDescription = null;
+      try {
+        aiDescription = await generateDescription(p.image_url, title);
+        steps.description = aiDescription ? 'generated' : 'failed (proceeding without)';
+      } catch (e) {
+        steps.description = 'error: ' + e.message + ' (proceeding without)';
+      }
+
+      // Build body_html — AI description only, no specs table
+      const bodyHtml = aiDescription
+        ? `<p>${aiDescription.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</p>`
+        : `<p>${title}</p>`;
+
+      // ── STEP 4: Create product on Shopify as DRAFT ──
+      // Private label: hide real vendor name, collection, and MFR SKU from public fields
+      const isPL = !!(v.is_private_label && v.private_label_name);
+      const tags = [displayVendor, p.collection, p.pattern_name, 'Luxury', 'Wallcovering'].filter(Boolean);
+      if (p.material) tags.push(p.material);
+      if (p.fire_rating) tags.push('Fire Rated');
+      if (p.design_style) tags.push(p.design_style);
+      // For private label, also add MFR SKU as tag (searchable but not customer-visible in title)
+      if (!isPL && p.mfr_sku) tags.push(p.mfr_sku);
+
+      const shopifyPayload = {
+        product: {
+          title,
+          vendor: displayVendor,
+          product_type: 'Wallcovering',
+          status: 'draft',
+          body_html: bodyHtml,
+          tags: [...new Set(tags)].join(', '),
+          images: (p.image_url && !p._croppedImage) ? [{ src: p.image_url, alt: title }] : [],
+          variants: [{
+            title: 'Sample', option1: 'Sample', price: '4.25',
+            sku: p.dw_sku + '-Sample', barcode: isPL ? '' : p.mfr_sku,
+            inventory_management: null
+          }]
+        }
+      };
+
+      let shopifyProductId = null;
+      try {
+        const shopResp = await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-10/products.json`, {
+          method: 'POST',
+          headers: { 'X-Shopify-Access-Token': PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+          body: JSON.stringify(shopifyPayload)
+        });
+        const shopData = await shopResp.json();
+        if (shopData.product) {
+          shopifyProductId = shopData.product.id;
+          steps.shopify_create = 'draft created';
+        } else {
+          steps.shopify_create = 'failed: ' + JSON.stringify(shopData.errors || shopData).substring(0, 200);
+          results.push({ vendor: v.vendor_name, sku: p.dw_sku, mfr: p.mfr_sku, status: 'error', reason: steps.shopify_create, steps }); continue;
+        }
+      } catch (e) {
+        steps.shopify_create = 'error: ' + e.message;
+        results.push({ vendor: v.vendor_name, sku: p.dw_sku, mfr: p.mfr_sku, status: 'error', reason: e.message, steps }); continue;
+      }
+
+      // Rate limit after Shopify REST call
+      await new Promise(r => setTimeout(r, 600));
+
+      // ── STEP 4b: Logo crop — detect and remove vendor logos from product image ──
+      if (p.image_url && shopifyProductId) {
+        try {
+          const cropResult = await processImage(p.image_url);
+          if (cropResult.cropped && cropResult.buffer) {
+            // Delete the original image, upload cropped version
+            const existingImages = shopData?.product?.images || [];
+            if (existingImages.length > 0) {
+              await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-10/products/${shopifyProductId}/images/${existingImages[0].id}.json`, {
+                method: 'DELETE',
+                headers: { 'X-Shopify-Access-Token': PRODUCT_TOKEN }
+              });
+              await new Promise(r => setTimeout(r, 500));
+            }
+            const imgResp = await uploadCroppedToShopify(shopifyProductId, cropResult.buffer, title, SHOPIFY_DOMAIN, PRODUCT_TOKEN);
+            steps.logo_crop = cropResult.details;
+            if (imgResp.image) steps.logo_crop += ` → uploaded (${imgResp.image.id})`;
+            await new Promise(r => setTimeout(r, 500));
+          } else {
+            steps.logo_crop = cropResult.details;
+          }
+        } catch (cropErr) {
+          steps.logo_crop = 'error: ' + cropErr.message;
+          console.error('[Victor] Logo crop error:', cropErr.message);
+        }
+      }
+
+      // ── STEP 5: Push ALL metafields via GraphQL ──
+      const isTexture = (p.design_style || '').toLowerCase().includes('texture') && !(p.design_style || '').toLowerCase().includes('floral');
+      const metafields = [
+        // Core specs
+        { namespace: 'global', key: 'width', value: p.width },
+        // Private label: hide MFR SKU from customer-visible metafield, store real vendor in private_label namespace
+        { namespace: 'global', key: 'manufacturer_sku', value: isPL ? '' : p.mfr_sku },
+        { namespace: 'global', key: 'Brand', value: displayVendor },
+        { namespace: 'global', key: 'Collection', value: p.collection },
+        // Private label: store real vendor info in hidden namespace
+        isPL ? { namespace: 'private_label', key: 'real_vendor_name', value: v.vendor_name } : null,
+        isPL ? { namespace: 'private_label', key: 'real_mfr_sku', value: p.mfr_sku } : null,
+        { namespace: 'global', key: 'Contents', value: p.material },
+        { namespace: 'global', key: 'application', value: p.application || 'Paste the wall' },
+        // Repeat (patterns only, not textures)
+        !isTexture ? { namespace: 'global', key: 'repeat', value: p.pattern_repeat } : null,
+        !isTexture ? { namespace: 'global', key: 'Vert-Rpt', value: p.pattern_repeat } : null,
+        p.repeat_h ? { namespace: 'global', key: 'Horz-Rpt', value: p.repeat_h } : null,
+        // Match type
+        { namespace: 'global', key: 'MATCH', value: p.match_type },
+        // Fire rating
+        { namespace: 'global', key: 'fire_rating', value: p.fire_rating },
+        // Finish
+        { namespace: 'global', key: 'Finish', value: p.finish },
+        // Roll length
+        { namespace: 'global', key: 'length', value: p.roll_length },
+        // Design style
+        { namespace: 'global', key: 'design', value: p.design_style },
+        { namespace: 'global', key: 'Style', value: p.design_style ? 'Luxury' : null },
+        // Coverage
+        { namespace: 'global', key: 'coverage', value: p.coverage },
+        // Features
+        { namespace: 'global', key: 'features', value: p.features },
+        // Color
+        { namespace: 'global', key: 'Color-Way', value: p.color_name },
+        // Unit of measure
+        { namespace: 'global', key: 'unit_of_measure', value: 'Priced Per Single Roll' },
+        // Country
+        { namespace: 'global', key: 'Country', value: p.about_vendor ? null : null },
+      ].filter(m => m && m.value && String(m.value).trim() && String(m.value).toLowerCase() !== 'null');
+
+      try {
+        const gqlResult = await setMetafields(shopifyProductId, metafields);
+        const userErrors = gqlResult?.data?.metafieldsSet?.userErrors || [];
+        if (userErrors.length > 0) {
+          steps.metafields = 'partial: ' + userErrors.map(e => e.message).join('; ');
+        } else {
+          steps.metafields = metafields.length + ' set';
+        }
+      } catch (e) {
+        steps.metafields = 'error: ' + e.message;
+      }
+
+      // Rate limit after GraphQL call
+      await new Promise(r => setTimeout(r, 600));
+
+      // ── STEP 5.5: Interior Design Tagger (Gemini AI) ──
+      // Analyze image for colors, styles, patterns → tags + hex → metafields + Shopify tags
+      steps.id_tagger = 'skipped';
+      if (p.image_url) {
+        try {
+          const idPrompt = `Analyze this wallcovering image. Return ONLY valid JSON:\n{"dominantColor":"single color","hexCode":"#XXXXXX","allColors":["every visible color"],"styles":["1-2 from: Traditional, Contemporary, Art Deco, Mid-Century, Victorian, Baroque, Luxury, Bohemian, Coastal, Tropical, Minimalist"],"patterns":["1-2 from: Damask, Floral, Geometric, Botanical, Textured, Abstract, Stripe, Medallion, Trellis, Toile, Chinoiserie, Scenic"]}`;
+          const geminiIdResp = await fetch(
+            `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=${GEMINI_KEY}`,
+            { method: 'POST', headers: { 'Content-Type': 'application/json' },
+              body: JSON.stringify({
+                contents: [{ parts: [
+                  { text: `Image URL: ${p.image_url}\n\nProduct: ${title}\n\n${idPrompt}` }
+                ]}],
+                generationConfig: { temperature: 0.1, maxOutputTokens: 400, thinkingConfig: { thinkingBudget: 0 } }
+              }),
+              signal: AbortSignal.timeout(15000)
+            }
+          );
+          const geminiIdData = await geminiIdResp.json();
+          const idText = geminiIdData?.candidates?.[0]?.content?.parts?.[0]?.text || '';
+          const jsonMatch = idText.match(/\{[\s\S]*\}/);
+
+          if (jsonMatch) {
+            const aiAnalysis = JSON.parse(jsonMatch[0]);
+
+            // Build tag set
+            const tagSet = new Set();
+            tagSet.add(v.vendor_name);
+            tagSet.add('Wallcovering');
+            tagSet.add('Commercial');
+            tagSet.add('Architectural');
+            if (p.pattern_name) tagSet.add(p.pattern_name);
+            if (p.color_name) tagSet.add(p.color_name);
+            if (p.collection) tagSet.add(p.collection);
+            if (p.material) tagSet.add(p.material);
+
+            // ALL colors (no "Background Color" prefix)
+            (aiAnalysis.allColors || []).forEach(c => { if (c) tagSet.add(c.trim()); });
+            if (aiAnalysis.dominantColor) tagSet.add(aiAnalysis.dominantColor.trim());
+            (aiAnalysis.styles || []).forEach(s => { if (s) tagSet.add(s.trim()); });
+            (aiAnalysis.patterns || []).forEach(p2 => { if (p2) tagSet.add(p2.trim()); });
+            tagSet.delete('');
+
+            const tagsStr = Array.from(tagSet).join(', ');
+
+            // Update product tags on Shopify
+            await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-10/products/${shopifyProductId}.json`, {
+              method: 'PUT',
+              headers: { 'X-Shopify-Access-Token': PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+              body: JSON.stringify({ product: { id: shopifyProductId, tags: tagsStr } })
+            });
+            await new Promise(r => setTimeout(r, 600));
+
+            // Push hex + AI data to metafields
+            const idMetafields = [];
+            const gid = `gid://shopify/Product/${shopifyProductId}`;
+            if (aiAnalysis.hexCode) {
+              idMetafields.push({ ownerId: gid, namespace: 'global', key: 'color_hex', value: aiAnalysis.hexCode, type: 'single_line_text_field' });
+              idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'color_hex', value: aiAnalysis.hexCode, type: 'single_line_text_field' });
+            }
+            if (aiAnalysis.allColors) {
+              idMetafields.push({ ownerId: gid, namespace: 'global', key: 'ai_color_tags', value: aiAnalysis.allColors.join(', '), type: 'single_line_text_field' });
+              idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'ai_color_tags', value: aiAnalysis.allColors.join(', '), type: 'single_line_text_field' });
+            }
+            if (aiAnalysis.dominantColor) {
+              idMetafields.push({ ownerId: gid, namespace: 'global', key: 'background_color', value: aiAnalysis.dominantColor, type: 'single_line_text_field' });
+              idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'background_color', value: aiAnalysis.dominantColor, type: 'single_line_text_field' });
+            }
+            // Push custom.* text fields too
+            if (aiAnalysis.dominantColor) {
+              idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'background_color', value: aiAnalysis.dominantColor, type: 'single_line_text_field' });
+              idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'color', value: p.color_name || aiAnalysis.dominantColor, type: 'single_line_text_field' });
+            }
+            idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'brand', value: v.vendor_name, type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'manufacturer_sku', value: p.mfr_sku, type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'collection_name', value: p.collection || v.vendor_name, type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'custom', key: 'material', value: p.material || 'Non-woven', type: 'multi_line_text_field' });
+            // dwc.* fields
+            idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'pattern_name', value: p.pattern_name || '', type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'color', value: p.color_name || aiAnalysis.dominantColor, type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'width', value: p.width || '', type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'manufacturer_sku', value: p.mfr_sku, type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'real_vendor', value: v.vendor_name, type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'brand', value: v.vendor_name, type: 'single_line_text_field' });
+            idMetafields.push({ ownerId: gid, namespace: 'dwc', key: 'dw_sku', value: p.dw_sku, type: 'single_line_text_field' });
+
+            const validMf = idMetafields.filter(m => m.value && m.value.trim());
+            if (validMf.length > 0) {
+              await setMetafields(shopifyProductId, validMf);
+            }
+            await new Promise(r => setTimeout(r, 600));
+
+            // Publish to all 14 channels
+            const PUBS = [
+              '22208643184','22497296496','29646651457','29739483201','29776969793',
+              '37538791489','37904089153','43657658419','44234276915','44317474867',
+              '44317507635','71898464307','115856375859','140027723827'
+            ].map(id => ({ publicationId: `gid://shopify/Publication/${id}` }));
+
+            await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-10/graphql.json`, {
+              method: 'POST',
+              headers: { 'X-Shopify-Access-Token': PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+              body: JSON.stringify({
+                query: 'mutation publishablePublish($id: ID!, $input: [PublicationInput!]!) { publishablePublish(id: $id, input: $input) { userErrors { message } } }',
+                variables: { id: `gid://shopify/Product/${shopifyProductId}`, input: PUBS }
+              })
+            });
+
+            steps.id_tagger = `${tagSet.size} tags, hex=${aiAnalysis.hexCode || '?'}, ${validMf.length} mf, 14ch`;
+          }
+        } catch (e) {
+          steps.id_tagger = 'error: ' + e.message;
+        }
+      }
+
+      // ── STEP 6: Silas validate-activation ──
+      let silasActivationOk = false;
+      try {
+        const activationResult = await silasPost('/api/validate-activation', {
+          shopify_product_id: shopifyProductId
+        });
+        silasActivationOk = !!activationResult.approved;
+        steps.silas_activation = silasActivationOk ? 'approved' : ('rejected: ' + (activationResult.reason || 'unknown'));
+      } catch (e) {
+        steps.silas_activation = 'error: ' + e.message;
+      }
+
+      // ── STEP 7: Activate product (only if Silas approves) ──
+      if (silasActivationOk) {
+        try {
+          const activateResp = await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-10/products/${shopifyProductId}.json`, {
+            method: 'PUT',
+            headers: { 'X-Shopify-Access-Token': PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+            body: JSON.stringify({ product: { id: shopifyProductId, status: 'active' } })
+          });
+          const activateData = await activateResp.json();
+          steps.activated = !!(activateData.product && activateData.product.status === 'active');
+        } catch (e) {
+          steps.activated = false;
+          steps.activate_error = e.message;
+        }
+        // Rate limit after activation PUT
+        await new Promise(r => setTimeout(r, 600));
+      } else {
+        steps.activated = false;
+      }
+
+      // Room setting — skipped for Phase 2
+      steps.room_setting = 'skipped (Phase 2)';
+
+      // ── STEP 8: Update catalog with shopify_product_id ──
+      try {
+        await pool.query(`UPDATE "${catTable}" SET shopify_product_id = $1 WHERE dw_sku = $2`, [String(shopifyProductId), p.dw_sku]);
+      } catch (e) {
+        console.error('[VCC] Catalog update error for', p.dw_sku, e.message);
+      }
+
+      results.push({
+        vendor: v.vendor_name,
+        sku: p.dw_sku,
+        mfr: p.mfr_sku,
+        shopify_id: shopifyProductId,
+        title,
+        status: steps.activated ? 'launched' : 'draft',
+        steps
+      });
+    }
+
+    res.json({
+      success: true,
+      launched: results.filter(r => r.status === 'launched').length,
+      total: results.length,
+      results
+    });
+  } catch (err) {
+    console.error('[VCC] Launch test error:', err);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+app.post('/api/vendors/:code/inquiry-page', async (req, res) => {
+  try {
+    const { rows } = await pool.query(
+      'SELECT vendor_code, vendor_name, website_url, website_summary FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (!rows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+    const v = rows[0];
+    const handle = 'vendor-' + v.vendor_code.replace(/_/g, '-');
+
+    // Check if page already exists
+    const checkUrl = 'https://' + SHOPIFY_DOMAIN + '/admin/api/2024-01/pages.json?handle=' + handle;
+    const checkRes = await fetch(checkUrl, {
+      headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' }
+    });
+    const checkData = await checkRes.json();
+    if (checkData.pages && checkData.pages.length > 0) {
+      return res.json({
+        success: true,
+        existing: true,
+        page_id: checkData.pages[0].id,
+        handle: checkData.pages[0].handle,
+        url: 'https://' + SHOPIFY_DOMAIN + '/pages/' + checkData.pages[0].handle,
+        message: 'Inquiry page already exists for ' + v.vendor_name
+      });
+    }
+
+    // Build inquiry page HTML — skip website_summary if it looks like an internal score
+    const rawSummary = v.website_summary || '';
+    const isInternalScore = /shopify|seo|lazy loading|mobile support|site with/i.test(rawSummary);
+    const vendorDescription = (!isInternalScore && rawSummary.length > 20) ? rawSummary : (v.vendor_name + ' offers premium wallcoverings and textiles. Contact us to request samples, pricing, and availability.');
+    const bodyHtml = '<div style="max-width:800px;margin:0 auto;padding:20px;">'
+      + '<h1 style="font-size:28px;margin-bottom:10px;">' + v.vendor_name + ' Wallcoverings</h1>'
+      + '<p style="font-size:16px;line-height:1.6;color:#555;margin-bottom:30px;">'
+      + vendorDescription + '</p>'
+      + '<div style="background:#f8f8f8;border:1px solid #e0e0e0;border-radius:8px;padding:30px;margin-bottom:30px;">'
+      + '<h2 style="font-size:22px;margin-bottom:8px;">Inquire About ' + v.vendor_name + ' Products</h2>'
+      + '<p style="color:#666;margin-bottom:20px;">Interested in ' + v.vendor_name + '? Fill out the form below and our team will get back to you with pricing, samples, and availability.</p>'
+      + '<form action="/contact#contact_form" method="post">'
+      + '<input type="hidden" name="contact[subject]" value="Inquiry: ' + v.vendor_name + '">'
+      + '<div style="margin-bottom:15px;">'
+      + '<label style="display:block;font-weight:600;margin-bottom:5px;">Name</label>'
+      + '<input type="text" name="contact[name]" required style="width:100%;padding:10px;border:1px solid #ccc;border-radius:4px;font-size:15px;">'
+      + '</div>'
+      + '<div style="margin-bottom:15px;">'
+      + '<label style="display:block;font-weight:600;margin-bottom:5px;">Email</label>'
+      + '<input type="email" name="contact[email]" required style="width:100%;padding:10px;border:1px solid #ccc;border-radius:4px;font-size:15px;">'
+      + '</div>'
+      + '<div style="margin-bottom:15px;">'
+      + '<label style="display:block;font-weight:600;margin-bottom:5px;">Phone (optional)</label>'
+      + '<input type="tel" name="contact[phone]" style="width:100%;padding:10px;border:1px solid #ccc;border-radius:4px;font-size:15px;">'
+      + '</div>'
+      + '<div style="margin-bottom:15px;">'
+      + '<label style="display:block;font-weight:600;margin-bottom:5px;">Message</label>'
+      + '<textarea name="contact[body]" rows="5" required style="width:100%;padding:10px;border:1px solid #ccc;border-radius:4px;font-size:15px;" placeholder="Tell us what you are looking for — specific patterns, colors, quantities, or samples."></textarea>'
+      + '</div>'
+      + '<button type="submit" style="background:#1a1a2e;color:#fff;padding:12px 30px;border:none;border-radius:4px;font-size:16px;cursor:pointer;">Send Inquiry</button>'
+      + '</form>'
+      + '</div>'
+      + (v.website_url ? '<p style="color:#888;font-size:14px;">Visit <a href="' + v.website_url + '" target="_blank" rel="noopener">' + v.vendor_name + ' official site</a> for more information.</p>' : '')
+      + '</div>';
+
+    // Create Shopify page
+    const createUrl = 'https://' + SHOPIFY_DOMAIN + '/admin/api/2024-01/pages.json';
+    const createRes = await fetch(createUrl, {
+      method: 'POST',
+      headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        page: {
+          title: v.vendor_name + ' Wallcoverings — Inquiry',
+          handle: handle,
+          body_html: bodyHtml,
+          published: true,
+          template_suffix: ''
+        }
+      })
+    });
+    const createData = await createRes.json();
+
+    if (createData.page) {
+      console.log('[Victor] Created Shopify inquiry page for ' + v.vendor_name + ': /pages/' + handle);
+      res.json({
+        success: true,
+        existing: false,
+        page_id: createData.page.id,
+        handle: createData.page.handle,
+        url: 'https://' + SHOPIFY_DOMAIN + '/pages/' + createData.page.handle,
+        message: 'Inquiry page created for ' + v.vendor_name
+      });
+    } else {
+      const errMsg = typeof createData.errors === 'string' ? createData.errors : JSON.stringify(createData.errors || 'Failed to create page');
+      if (errMsg.includes('write_content')) {
+        res.json({ success: false, error: 'Shopify token needs write_content scope. Approve in Shopify Admin > Apps > Custom app settings.' });
+      } else {
+        res.json({ success: false, error: errMsg });
+      }
+    }
+  } catch (err) {
+    console.error('[Victor] Inquiry page error:', err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Discontinuation suggestions (match catalog vs Shopify) ---
+app.get('/api/vendors/:code/disc-suggestions', async (req, res) => {
+  try {
+    const { rows: vendorRows } = await pool.query(
+      'SELECT vendor_code, vendor_name, catalog_table FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (!vendorRows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+
+    const code = req.params.code;
+    const patterns = VENDOR_NAME_MAP[code];
+    if (!patterns || patterns.length === 0) {
+      return res.json({ success: true, data: [], count: 0, message: 'No Shopify vendor mapping' });
+    }
+
+    const limit = Math.min(parseInt(req.query.limit) || 200, 1000);
+    const conditions = patterns.map((_, i) => `vendor ILIKE $${i + 1}`).join(' OR ');
+
+    // Find draft + archived Shopify products for this vendor
+    const query = `
+      SELECT shopify_id, title, vendor, sku, dw_sku, mfr_sku, image_url, status, price, cost_price,
+        CASE
+          WHEN UPPER(status) = 'ARCHIVED' THEN 'ARCHIVED'
+          WHEN UPPER(status) = 'DRAFT' THEN 'DRAFT'
+        END as disc_status
+      FROM shopify_products
+      WHERE (${conditions}) AND UPPER(status) IN ('DRAFT', 'ARCHIVED')
+      ORDER BY UPPER(status) DESC, title ASC
+      LIMIT $${patterns.length + 1}
+    `;
+
+    const countQuery = `
+      SELECT
+        COUNT(*) FILTER (WHERE UPPER(status) = 'DRAFT') as draft_count,
+        COUNT(*) FILTER (WHERE UPPER(status) = 'ARCHIVED') as archived_count,
+        COUNT(*) as total
+      FROM shopify_products
+      WHERE (${conditions}) AND UPPER(status) IN ('DRAFT', 'ARCHIVED')
+    `;
+
+    const [dataResult, countResult] = await Promise.all([
+      pool.query(query, [...patterns, limit]),
+      pool.query(countQuery, patterns)
+    ]);
+
+    res.json({
+      success: true,
+      vendor: vendorRows[0].vendor_name,
+      data: dataResult.rows,
+      counts: countResult.rows[0],
+      suggestion: 'These products are DRAFT or ARCHIVED on Shopify and may be candidates for permanent removal.'
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Global discontinuation report ---
+app.get('/api/disc-report', async (req, res) => {
+  try {
+    const result = await pool.query(`
+      SELECT vendor_code, vendor_name, shopify_draft, shopify_archived,
+        (COALESCE(shopify_draft, 0) + COALESCE(shopify_archived, 0)) as total_disc,
+        shopify_count, catalog_count
+      FROM vendor_registry
+      WHERE (COALESCE(shopify_draft, 0) + COALESCE(shopify_archived, 0)) > 0
+      ORDER BY (COALESCE(shopify_draft, 0) + COALESCE(shopify_archived, 0)) DESC
+    `);
+
+    const totalDraft = result.rows.reduce((s, r) => s + (parseInt(r.shopify_draft) || 0), 0);
+    const totalArchived = result.rows.reduce((s, r) => s + (parseInt(r.shopify_archived) || 0), 0);
+
+    res.json({
+      success: true,
+      data: result.rows,
+      totals: { draft: totalDraft, archived: totalArchived, total: totalDraft + totalArchived },
+      vendors_with_disc: result.rows.length
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: 3-Point Discontinuation Validation ---
+
+// Summary: real cleanup numbers (not the fake 95K)
+app.get('/api/discontinuation/summary', async (req, res) => {
+  try {
+    const summary = await getDiscontinuationSummary();
+    res.json({ success: true, ...summary });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// Validate a vendor's products for discontinuation
+app.get('/api/discontinuation/validate/:vendor_code', async (req, res) => {
+  try {
+    const { vendor_code } = req.params;
+    const statusFilter = req.query.status || 'DRAFT';
+    const limit = Math.min(parseInt(req.query.limit) || 500, 2000);
+    const results = await validateVendor(vendor_code, { statusFilter, limit });
+    res.json({ success: true, ...results });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// Delete draft products from Shopify (dry-run by default)
+app.post('/api/discontinuation/delete-drafts', async (req, res) => {
+  try {
+    const { vendor_code, dry_run = true, limit = 100 } = req.body || {};
+    const isDryRun = dry_run !== false && dry_run !== 'false';
+
+    // Build query for draft products
+    let query, params;
+    if (vendor_code) {
+      const { rows: vendorRows } = await pool.query(
+        'SELECT vendor_name FROM vendor_registry WHERE vendor_code = $1', [vendor_code]
+      );
+      if (!vendorRows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+      query = `
+        SELECT shopify_id, title, vendor, dw_sku, mfr_sku, image_url
+        FROM shopify_products
+        WHERE vendor ILIKE $1 AND UPPER(status) = 'DRAFT'
+        ORDER BY title ASC
+        LIMIT $2
+      `;
+      params = [vendorRows[0].vendor_name, Math.min(parseInt(limit), 500)];
+    } else {
+      query = `
+        SELECT shopify_id, title, vendor, dw_sku, mfr_sku, image_url
+        FROM shopify_products
+        WHERE UPPER(status) = 'DRAFT'
+        ORDER BY vendor ASC, title ASC
+        LIMIT $1
+      `;
+      params = [Math.min(parseInt(limit), 500)];
+    }
+
+    const { rows: drafts } = await pool.query(query, params);
+
+    if (isDryRun) {
+      // Cross-reference with catalog to show what exists in PostgreSQL
+      const withCatalog = [];
+      const noCatalog = [];
+      for (const d of drafts) {
+        const { rows: catalogMatch } = await pool.query(`
+          SELECT id, sku, name, vendor_id FROM products
+          WHERE sku = $1 OR sku = $2
+          LIMIT 1
+        `, [d.mfr_sku || '', d.dw_sku || '']);
+        if (catalogMatch.length > 0) {
+          withCatalog.push({ ...d, catalog_id: catalogMatch[0].id, catalog_name: catalogMatch[0].name });
+        } else {
+          noCatalog.push(d);
+        }
+      }
+
+      return res.json({
+        success: true,
+        dry_run: true,
+        vendor: vendor_code || 'ALL',
+        total_drafts: drafts.length,
+        in_catalog: withCatalog.length,
+        not_in_catalog: noCatalog.length,
+        action_plan: {
+          delete_from_shopify: drafts.length,
+          data_safe_in_postgresql: withCatalog.length,
+          junk_no_catalog: noCatalog.length
+        },
+        products_with_catalog: withCatalog.slice(0, 20),
+        products_no_catalog: noCatalog.slice(0, 20),
+        message: 'DRY RUN — no changes made. Set dry_run: false to execute.'
+      });
+    }
+
+    // LIVE MODE — Queue deletions via Quinn (Shopify GraphQL)
+    const SHOPIFY_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+    const PRODUCT_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+
+    let deleted = 0, failed = 0;
+    const errors = [];
+
+    // Batch delete via GraphQL (25 per call)
+    for (let i = 0; i < drafts.length; i += 25) {
+      const batch = drafts.slice(i, i + 25);
+      const mutations = batch.map((d, idx) =>
+        `p${idx}: productDelete(input: {id: "${String(d.shopify_id).startsWith('gid://') ? d.shopify_id : 'gid://shopify/Product/' + d.shopify_id}"}) { deletedProductId userErrors { field message } }`
+      ).join('\n');
+
+      try {
+        const gqlResp = await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-10/graphql.json`, {
+          method: 'POST',
+          headers: { 'X-Shopify-Access-Token': PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+          body: JSON.stringify({ query: `mutation { ${mutations} }` })
+        });
+        const gqlData = await gqlResp.json();
+
+        for (let j = 0; j < batch.length; j++) {
+          const result = gqlData?.data?.[`p${j}`];
+          if (result?.deletedProductId) {
+            deleted++;
+            // Update shopify_products status
+            await pool.query(
+              "UPDATE shopify_products SET status = 'DELETED_FROM_SHOPIFY' WHERE shopify_id = $1",
+              [batch[j].shopify_id]
+            );
+          } else {
+            const userErrors = result?.userErrors || [];
+            const notExists = userErrors.some(e => e.message && e.message.includes('does not exist'));
+            if (notExists) {
+              // Product already gone from Shopify — clean up our DB record
+              deleted++;
+              await pool.query(
+                "UPDATE shopify_products SET status = 'DELETED_FROM_SHOPIFY' WHERE shopify_id = $1",
+                [batch[j].shopify_id]
+              );
+            } else {
+              failed++;
+              errors.push({ shopify_id: batch[j].shopify_id, error: userErrors.length ? userErrors : 'unknown' });
+            }
+          }
+        }
+
+        // Rate limit between batches
+        await new Promise(r => setTimeout(r, 1000));
+      } catch (err) {
+        failed += batch.length;
+        errors.push({ batch_start: i, error: err.message });
+      }
+    }
+
+    res.json({
+      success: true,
+      dry_run: false,
+      vendor: vendor_code || 'ALL',
+      deleted,
+      failed,
+      total_attempted: drafts.length,
+      errors: errors.slice(0, 10)
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- Vendor Name Mapping (Shopify vendor names → vendor_registry codes) ---
+const VENDOR_NAME_MAP = {
+  // --- Original 33 vendors ---
+  'schumacher': ['Schumacher%', 'F. Schumacher%'],
+  'kravet': ['Kravet%'],
+  'thibaut': ['Thibaut%', 'THIBAUT AT DW%', 'Anna French%', 'ANNA FRENCH%'],
+  'cole_son': ['Cole & Son%', 'Cole and Son%'],
+  'elitis': ['Elitis%'],
+  'arte': ['Arte%', 'ARTE%'],
+  'koroseal': ['Koroseal%'],
+  'maya': ['Maya Romanoff%'],
+  'phillip_jeffries': ['Phillip Jeffries%'],
+  'innovations': ['Innovations%', 'Innovationsusa%'],
+  'ralph_lauren': ['Ralph Lauren%'],
+  'brewster_york': ['Brewster%', 'Ronald Redding%', 'DAISY BENNETT%', 'Daisy Bennett%', 'A-Street%'],
+  'york_contract': ['York%'],
+  'graham_brown': ['Graham & Brown%', 'Graham and Brown%', 'Grahambrown%'],
+  'romo': ['Romo%'],
+  'osborne': ['Osborne & Little%'],
+  'mindthegap': ['Mind the Gap%'],
+  'hygge': ['Hygge%'],
+  'wallquest': ['Wallquest%', 'WallQuest%'],
+  'glass_bead': ['Glass Bead%', 'Glambeads%'],
+  'bespoke': ['DW Bespoke%'],
+  'w1838': ['1838%'],
+  'contrado': ['Contrado%'],
+  'fabricut': ['Fabricut%'],
+  'dedar': ['Dedar%'],
+  'designtex': ['Designtex%'],
+  'knoll': ['Knoll%'],
+  'bnwalls': ['BN Walls%'],
+  'stout': ['Stout%'],
+  'timorous': ['Timorous%'],
+  'arteriors': ['Arteriors%'],
+  'folia': ['Folia%'],
+  'muralsource': ['Mural Source%'],
+  'spoonflower': ['Spoonflower%'],
+  // --- Newly added vendors ---
+  'scalamandre': ['Scalamandre%'],
+  'hollywood': ['Hollywood%', 'Momentumtextilesandwalls%'],
+  'phillipe_romano': ['Phillipe Romano%', 'PR Faux Leather%', 'Velvet Walls by Phillipe Romano%'],
+  'clarke_clarke': ['Clarke And Clarke%'],
+  'brunschwig': ['Brunschwig%'],
+  'jf_fabrics': ['JF FABRICS%', 'JF Fabrics%'],
+  'jeffrey_stevens': ['Jeffrey Stevens%'],
+  'lee_jofa': ['Lee Jofa%'],
+  'rebel_walls': ['Rebel Walls%', 'Rebel Studio%'],
+  'fentucci': ['Fentucci%'],
+  'gaston_daniela': ['Gaston Y Daniela%'],
+  'malibu': ['Malibu Wall%'],
+  'holly_hunt': ['Holly Hunt%'],
+  'sandberg': ['Sandberg%'],
+  'china_seas': ['China Seas%'],
+  'designers_guild': ['Designers Guild%', "Designer's Guild%", 'Designersguild%', 'Christian Lacroix%'],
+  'wolf_gordon': ['Wolf Gordon%'],
+  'paul_montgomery': ['Paul Montgomery%'],
+  // Anna French is a Thibaut sub-brand — map to thibaut
+  'mulberry': ['Mulberry%'],
+  'harlequin': ['Harlequin%'],
+  'newmor': ['Newmor%'],
+  'william_morris': ['William Morris%', 'Morris and Company%'],
+  'british_walls': ['British Walls%'],
+  'et_cie': ['Et Cie%'],
+  'cowtan_tout': ['Cowtan & Tout%', 'Cowtan %'],
+  'nina_campbell': ['Nina Campbell%'],
+  'catchii': ['Catchii%'],
+  'versace': ['Versace%'],
+  'colefax_fowler': ['Colefax%'],
+  'coordonne': ['Coordonn%'],
+  'relativity_textiles': ['Retro Walls%', 'Retro DW%'],
+  'surface_stick': ['Surface Stick%'],
+  'la_walls': ['LA Walls%'],
+  'gp_baker': ['G P & J Baker%', 'GP & J Baker%'],
+  'roberto_cavalli': ['Roberto Cavalli%'],
+  'armani_casa': ['Armani Casa%'],
+  'nicolette_mayer': ['Nicolette Mayer%'],
+  'nlxl': ['NLXL%'],
+  'laura_ashley': ['Laura Ashley%'],
+  'missoni': ['Missoni%'],
+  'marburg': ['Marburg%'],
+  'manuel_canovas': ['Manuel Canovas%'],
+  'mark_alexander': ['Mark Alexander%'],
+  'carlisle': ['Carlisle%'],
+  'baker_lifestyle': ['Baker Lifestyle%'],
+  'threads': ['Threads%'],
+  'glitter_walls': ['Glitter Walls%'],
+  'primo_leathers': ['Primo Leathers%'],
+  'leed_walls': ['LEED Walls%'],
+  'showroom_line': ['Showroom Line%'],
+
+  'lincrusta': ['Lincrusta%'],
+  'tres_tintas': ['Tres Tintas%'],
+  'brand_mckenzie': ['Brand McKenzie%'],
+  'borastapeter': ['Bor%stapeter%'],
+  'iksel': ['IKSEL%', 'Iksel%'],
+  'hermes': ['Hermes%'],
+  'dolce_gabbana': ['Dolce & Gabbana%'],
+  'ultrasuede': ['Ultrasuede%'],
+  'novasuede': ['Novasuede%'],
+  // --- Sub-brands mapped to parent ---
+  // Christian Lacroix is a Designers Guild sub-brand
+  // Velvet Walls is a Phillipe Romano sub-brand (name doesn't start with PR)
+  // Momentumtextilesandwalls is Hollywood/Momentum
+
+  // --- New vendors (confirmed by Steve 2026-03-16) ---
+  'andrew_martin': ['Andrew Martin%'],
+  'as_creation': ['AS Creation%'],
+  'backdrop': ['Backdrop%'],
+  'clarence_house': ['Clarence House%'],
+  'donghia': ['Donghia%'],
+  'emiliana_parati': ['Emiliana Parati%'],
+  'erika_wakerly': ['Erika Wakerly%'],
+  'florence_broadhurst': ['Florence Broadhurst%'],
+  'graduate_collection': ['Graduate Collection%', 'The Graduate Collection%'],
+  'holland_sherry': ['Holland and Sherry%'],
+  'martyn_lawrence_bullard': ['Martyn Lawrence Bullard%'],
+  'mc_escher': ['MC Escher%'],
+  'nobilis': ['Nobilis%'],
+  'peg_norriss': ['Peg Norriss%'],
+  'phyllis_morris': ['Phyllis Morris%'],
+  'pierre_frey': ['Pierre Frey%'],
+  'sacha_walckhoff': ['Sacha Walckhoff%'],
+  'sugarboo': ['SUGARBOO%'],
+  'telefina': ['Telefina%'],
+  'witch_watchman': ['Witch & Watchman%', 'Witch And Watchman%'],
+  'zeuxis': ['Zeuxis Parrhasius%'],
+  'patty_madden': ['Patty Madden%'],
+  'kelly_wearstler': ['Kelly Wearstler%'],
+  'zoffany': ['Zoffany%'],
+  'studio_ditte': ['Studio Ditte%'],
+  'milton_king': ['Milton & King%', 'Milton and King%'],
+  'celerie_kemble': ['Celerie Kemble%'],
+  'hinson': ['Hinson%'],
+  'farrow_ball': ['Farrow%ball%'],
+  'winfield_thybony': ['Winfield Thybony%'],
+  'sancar': ['Sancar%'],
+  'edge_wallcovering': ['Edge Wallcovering%'],
+  'ananbo': ['Ananbo%'],
+  'rebecca_moses': ['Rebecca Moses%'],
+  'happy_menocal': ['Happy Menocal%'],
+  'sara_bergqvist': ['Sara Bergqvist%'],
+  'sarah_bartholomew': ['Sarah Bartholomew%'],
+  'olivia_popp': ['OLIVIA AND POPP%', 'Olivia and Popp%'],
+  'linherr': ['Linherr Hollingsworth%'],
+  'vahallan': ['Vahallan%'],
+  // --- Internal / House brands ---
+  'dw_house': ['Designer Wallcoverings%', 'DW Home%', 'DW%Exclusive%', 'Hand Crafted%', 'Traditional Whimsy%', 'Mid Century Modern%', 'Apartment Wallpaper%', 'Hospitality Wallcoverings%', 'Wallpaper NYC%', 'Peel and Stick%', 'PS Removable%', 'Limited Stock%', 'Madagascar%', 'Florentina%', 'Steve Abrams%', 'Steven Abrams%', 'Architectural Fabrics%', 'Luxury Murals%', 'Trikes-DL Couch-Eykon-Tower%', 'Marimekko Exclusive%', 'Natural Paint%', 'Pixels%', 'French Market%', 'Grasscloth WallPaper%', 'Designer Laboratory%', 'Porsche%Paint%', 'Printy6%', 'TEST']
+};
+
+// Catalog table mapping for scraped vendor data
+const CATALOG_TABLE_MAP = {
+  'arte': 'arte_catalog',
+  'brewster_york': 'brewster_catalog',
+  'cole_son': 'coleson_catalog',
+  'elitis': 'elitis_catalog',
+  'innovations': 'innovations_catalog',
+  'koroseal': 'koroseal_catalog',
+  'kravet': 'kravet_catalog',
+  'maya': 'maya_catalog',
+  'phillip_jeffries': 'pj_catalog',
+  'ralph_lauren': 'rl_catalog',
+  'schumacher': 'schumacher_catalog',
+  'thibaut': 'thibaut_catalog',
+  'york_contract': 'york_catalog',
+  'graham_brown': 'graham_brown_catalog',
+  'romo': 'romo_catalog',
+  'bespoke': 'bespoke_catalog',
+  'china_seas': 'china_seas_catalog',
+  'cowtan_tout': 'cowtan_catalog',
+  // New scraped tables
+  'w1838': 'w1838_catalog',
+  'dedar': 'dedar_catalog',
+  'designtex': 'designtex_catalog',
+  'fabricut': 'fabricut_catalog',
+  'knoll': 'knoll_catalog',
+  'hygge': 'hygge_catalog',
+  'bnwalls': 'bnwalls_catalog',
+  'mindthegap': 'mindthegap_catalog',
+  'osborne': 'osborne_catalog',
+  'stout': 'stout_catalog',
+  'wallquest': 'wallquest_catalog',
+  'contrado': 'contrado_catalog',
+  'muralsource': 'muralsource_catalog',
+  'timorous': 'timorous_catalog',
+  'arteriors': 'arteriors_catalog',
+  'folia': 'folia_catalog',
+  'spoonflower': 'spoonflower_catalog'
+};
+
+// --- Catalog column mapping (price/image column names per table) ---
+const CATALOG_COLUMNS = {
+  'arte_catalog':          { price: 'price',       image: 'image_url' },
+  'bespoke_catalog':       { price: null,          image: 'image_url' },
+  'brewster_catalog':      { price: null,          image: 'image_url' },
+  'china_seas_catalog':    { price: null,          image: 'image_url' },
+  'coleson_catalog':       { price: null,          image: 'image_url' },
+  'cowtan_catalog':        { price: null,          image: 'image_url' },
+  'elitis_catalog':        { price: null,          image: 'image_url' },
+  'graham_brown_catalog':  { price: 'trade_price', image: 'image_url' },
+  'innovations_catalog':   { price: null,          image: 'image_url' },
+  'koroseal_catalog':      { price: null,          image: 'image_url' },
+  'kravet_catalog':        { price: null,          image: 'image_url' },
+  'maya_catalog':          { price: null,          image: 'image_url' },
+  'pj_catalog':            { price: null,          image: 'image_url' },
+  'rl_catalog':            { price: 'price',       image: 'image_url' },
+  'romo_catalog':          { price: 'price',       image: 'image_url' },
+  'schumacher_catalog':    { price: 'trade_price', image: 'image_url' },
+  'thibaut_catalog':       { price: 'your_cost',   image: 'image_url' },
+  'york_catalog':          { price: null,          image: 'image_url' },
+  'york_contract_catalog': { price: 'retail_price', image: 'image_url' },
+  // New scraped tables (all use standard column names)
+  'w1838_catalog':         { price: 'price_retail', image: 'image_url' },
+  'dedar_catalog':         { price: 'price_retail', image: 'image_url' },
+  'designtex_catalog':     { price: 'price_retail', image: 'image_url' },
+  'fabricut_catalog':      { price: 'price_retail', image: 'image_url' },
+  'knoll_catalog':         { price: 'price_retail', image: 'image_url' },
+  'hygge_catalog':         { price: 'price_retail', image: 'image_url' },
+  'bnwalls_catalog':       { price: 'price_retail', image: 'image_url' },
+  'mindthegap_catalog':    { price: 'price_retail', image: 'image_url' },
+  'osborne_catalog':       { price: 'price_retail', image: 'image_url' },
+  'stout_catalog':         { price: 'price_retail', image: 'image_url' },
+  'wallquest_catalog':     { price: 'price_retail', image: 'image_url' },
+  'contrado_catalog':      { price: 'price_retail', image: 'image_url' },
+  'muralsource_catalog':   { price: 'price_retail', image: 'image_url' },
+  'timorous_catalog':      { price: 'price_retail', image: 'image_url' },
+  'arteriors_catalog':     { price: 'price_retail', image: 'image_url' },
+  'folia_catalog':         { price: 'price_retail', image: 'image_url' },
+  'spoonflower_catalog':   { price: 'price_retail', image: 'image_url' },
+  // Romo Group sub-brands
+  'black_edition_catalog': { price: null, image: 'image_url' },
+  'kirkby_catalog':        { price: null, image: 'image_url' },
+  'zinc_catalog':          { price: null, image: 'image_url' },
+  'villa_nova_catalog':    { price: null, image: 'image_url' },
+  'mark_alexander_catalog':{ price: null, image: 'image_url' },
+  // Non-standard column names
+  'franquemont_catalog':   { price: null, image: "image_urls[1]" },
+  'versace_catalog':       { price: null, image: 'image_url' },
+  // More vendors
+  'coordonne_catalog':     { price: null, image: 'image_url' },
+  'harlequin_catalog':     { price: null, image: 'image_url' },
+  'scalamandre_catalog':   { price: null, image: 'image_url' },
+  'as_creation_catalog':   { price: null, image: 'image_url' },
+  'marburg_catalog':       { price: null, image: 'image_url' },
+  'newwall_catalog':       { price: null, image: 'image_url' },
+  'nicolette_mayer_catalog': { price: null, image: 'image_url' },
+  'malibu_catalog':        { price: null, image: 'image_url' },
+  'hollywood_catalog':     { price: null, image: 'image_url' },
+  'jf_fabrics_catalog':    { price: null, image: 'image_url' },
+  'jeffrey_stevens_catalog': { price: null, image: 'image_url' },
+  'nobilis_catalog':       { price: null, image: 'image_url' }
+};
+
+// --- Spec column mapping (width & repeat column names per catalog table) ---
+const SPEC_COLUMNS = {
+  // Old-style tables: width + pattern_repeat
+  'arte_catalog':          { width: 'width', repeat: 'pattern_repeat' },
+  'bespoke_catalog':       { width: 'width', repeat: 'pattern_repeat' },
+  'brewster_catalog':      { width: 'width', repeat: 'pattern_repeat' },
+  'china_seas_catalog':    { width: 'width', repeat: 'pattern_repeat' },
+  'coleson_catalog':       { width: 'width', repeat: 'pattern_repeat' },
+  'cowtan_catalog':        { width: 'width', repeat: 'pattern_repeat' },
+  'elitis_catalog':        { width: 'width', repeat: 'pattern_repeat' },
+  'graham_brown_catalog':  { width: 'width', repeat: 'pattern_repeat' },
+  'innovations_catalog':   { width: 'width', repeat: 'pattern_repeat' },
+  'koroseal_catalog':      { width: 'width', repeat: 'pattern_repeat' },
+  'kravet_catalog':        { width: 'width', repeat: 'pattern_repeat' },
+  'maya_catalog':          { width: 'width', repeat: 'pattern_repeat' },
+  'pj_catalog':            { width: 'width', repeat: 'pattern_repeat' },
+  'rl_catalog':            { width: 'width', repeat: 'pattern_repeat' },
+  'romo_catalog':          { width: 'width', repeat: 'pattern_repeat' },
+  'schumacher_catalog':    { width: 'width', repeat: 'pattern_repeat' },
+  'thibaut_catalog':       { width: 'width', repeat: 'pattern_repeat' },
+  'york_catalog':          { width: 'width', repeat: 'pattern_repeat' },
+  'york_contract_catalog': { width: 'width', repeat: 'pattern_repeat' },
+  // New-style tables: width + repeat_v
+  'w1838_catalog':         { width: 'width', repeat: 'repeat_v' },
+  'dedar_catalog':         { width: 'width', repeat: 'repeat_v' },
+  'designtex_catalog':     { width: 'width', repeat: 'repeat_v' },
+  'fabricut_catalog':      { width: 'width', repeat: 'repeat_v' },
+  'knoll_catalog':         { width: 'width', repeat: 'repeat_v' },
+  'hygge_catalog':         { width: 'width', repeat: 'repeat_v' },
+  'bnwalls_catalog':       { width: 'width', repeat: 'repeat_v' },
+  'mindthegap_catalog':    { width: 'width', repeat: 'repeat_v' },
+  'osborne_catalog':       { width: 'width', repeat: 'repeat_v' },
+  'stout_catalog':         { width: 'width', repeat: 'repeat_v' },
+  'wallquest_catalog':     { width: 'width', repeat: 'repeat_v' },
+  'contrado_catalog':      { width: 'width', repeat: 'repeat_v' },
+  'muralsource_catalog':   { width: 'width', repeat: 'repeat_v' },
+  'timorous_catalog':      { width: 'width', repeat: 'repeat_v' },
+  'arteriors_catalog':     { width: 'width', repeat: 'repeat_v' },
+  'folia_catalog':         { width: 'width', repeat: 'repeat_v' },
+  'spoonflower_catalog':   { width: 'width', repeat: 'repeat_v' },
+  // Romo Group sub-brands
+  'black_edition_catalog': { width: 'width', repeat: 'pattern_repeat' },
+  'kirkby_catalog':        { width: 'width', repeat: 'pattern_repeat' },
+  'zinc_catalog':          { width: 'width', repeat: 'pattern_repeat' },
+  'villa_nova_catalog':    { width: 'width', repeat: 'pattern_repeat' },
+  'mark_alexander_catalog':{ width: 'width', repeat: 'pattern_repeat' },
+  // Non-standard column names
+  'franquemont_catalog':   { width: 'roll_width', repeat: 'vertical_repeat' },
+  'versace_catalog':       { width: 'width', repeat: 'pattern_repeat' },
+  // More vendors
+  'coordonne_catalog':     { width: 'width', repeat: 'pattern_repeat' },
+  'harlequin_catalog':     { width: 'width', repeat: 'pattern_repeat' },
+  'scalamandre_catalog':   { width: 'width', repeat: 'pattern_repeat' },
+  'as_creation_catalog':   { width: 'width', repeat: 'pattern_repeat' },
+  'marburg_catalog':       { width: 'width', repeat: 'pattern_repeat' },
+  'newwall_catalog':       { width: 'width', repeat: 'pattern_repeat' },
+  'nicolette_mayer_catalog': { width: 'width', repeat: 'pattern_repeat' },
+  'malibu_catalog':        { width: 'width', repeat: 'pattern_repeat' },
+  'hollywood_catalog':     { width: 'width', repeat: 'repeat_v' },
+  'jf_fabrics_catalog':    { width: 'width', repeat: 'pattern_repeat' },
+  'jeffrey_stevens_catalog': { width: 'width', repeat: 'pattern_repeat' },
+  'nobilis_catalog':       { width: 'width', repeat: 'pattern_repeat' }
+};
+
+// --- Required Catalog Fields (ALL vendors must capture these) ---
+const REQUIRED_CATALOG_FIELDS = [
+  'mfr_sku', 'dw_sku', 'pattern_name', 'color_name', 'collection',
+  'product_type', 'width', 'length', 'repeat_v', 'repeat_h',
+  'match_type', 'material', 'finish', 'application', 'coverage',
+  'features', 'fire_rating', 'design', 'rooms', 'color_primary',
+  'color_secondary', 'price_retail', 'price_currency', 'image_url',
+  'all_images', 'product_url', 'in_stock', 'vendor_name', 'brand',
+  'tags', 'body_html', 'short_description', 'about_vendor',
+  'us_distributor', 'showroom_locations'
+];
+
+// Map required field names to possible column aliases per catalog table
+// Old-style tables use different column names for the same data
+const FIELD_ALIASES = {
+  'mfr_sku':    ['mfr_sku', 'arte_sku', 'sku', 'item_number', 'product_sku', 'vendor_sku'],
+  'repeat_v':   ['repeat_v', 'pattern_repeat', 'vertical_repeat'],
+  'repeat_h':   ['repeat_h', 'horizontal_repeat'],
+  'price_retail': ['price_retail', 'price', 'retail_price', 'trade_price', 'your_cost', 'msrp'],
+  'length':     ['length', 'roll_length'],
+  'fire_rating': ['fire_rating', 'fire_rating_us', 'fire_rating_eu'],
+  'body_html':  ['body_html', 'description', 'ai_description'],
+  'tags':       ['tags', 'ai_tags'],
+  'design':     ['design', 'ai_patterns', 'ai_styles'],
+  'color_primary': ['color_primary', 'ai_background_color'],
+  'color_secondary': ['color_secondary', 'ai_colors'],
+  'rooms':      ['rooms'],
+  'application': ['application', 'installation', 'adhesive', 'how_to_paste']
+};
+
+// --- Compute spec completeness for a catalog table ---
+async function computeSpecCompleteness(catTable) {
+  if (!catTable) return { pct: 0, present: 0, total: REQUIRED_CATALOG_FIELDS.length };
+  try {
+    // Get actual columns in this table
+    const colRes = await pool.query(
+      `SELECT column_name FROM information_schema.columns WHERE table_name = $1`,
+      [catTable]
+    );
+    const actualCols = new Set(colRes.rows.map(r => r.column_name));
+    if (actualCols.size === 0) return { pct: 0, present: 0, total: REQUIRED_CATALOG_FIELDS.length };
+
+    let fieldsPresent = 0;
+
+    for (const field of REQUIRED_CATALOG_FIELDS) {
+      // Check if any alias of this field exists as a column
+      const aliases = FIELD_ALIASES[field] || [field];
+      const matchedCol = aliases.find(a => actualCols.has(a));
+      if (matchedCol) {
+        fieldsPresent++;
+      }
+    }
+
+    const pct = REQUIRED_CATALOG_FIELDS.length > 0
+      ? parseFloat(((fieldsPresent / REQUIRED_CATALOG_FIELDS.length) * 100).toFixed(1))
+      : 0;
+
+    return { pct, present: fieldsPresent, total: REQUIRED_CATALOG_FIELDS.length };
+  } catch (_) {
+    return { pct: 0, present: 0, total: REQUIRED_CATALOG_FIELDS.length };
+  }
+}
+
+// --- Sync: Populate vendor_registry — CATALOG tables are source of truth ---
+async function syncVendorCounts() {
+  console.log('[Victor] Syncing vendor counts — PostgreSQL catalogs = source of truth, Shopify = published...');
+  logActivity('sync', 'Sync started — scanning all vendor catalogs');
+  const startTime = Date.now();
+  let updated = 0;
+
+  try {
+    // Get all vendors with current counts for change detection
+    const { rows: allVendors } = await pool.query('SELECT vendor_code, vendor_name, catalog_table, catalog_count as old_cat, shopify_count as old_shop, total_products as old_total FROM vendor_registry');
+
+    for (const v of allVendors) {
+      const code = v.vendor_code;
+      const catTable = v.catalog_table;
+      let catalogCount = 0, catWithCost = 0, catWithImages = 0, catOnShopify = 0;
+      let catWithWidth = 0, catWithRepeat = 0;
+      let catWithAboutVendor = 0, catWithAllImages = 0, catWithSpecSheet = 0, catWithBodyHtml = 0;
+      let shopifyCount = 0, shopifyWithImages = 0;
+      let shopifyDraft = 0, shopifyArchived = 0;
+      let parkerCost = 0;
+
+      // --- PRIMARY: Catalog table (source of truth) ---
+      if (catTable) {
+        try {
+          const cols = CATALOG_COLUMNS[catTable] || { price: null, image: 'image_url' };
+
+          // When table is vendor_catalog (shared), filter by vendor_code
+          const isShared = catTable === 'vendor_catalog';
+          const vendorFilter = isShared ? ` WHERE vendor_code = '${code.replace(/'/g, "''")}'` : '';
+          const vendorAnd = isShared ? ` AND vendor_code = '${code.replace(/'/g, "''")}'` : '';
+
+          // Total catalog products
+          const totalRes = await pool.query(`SELECT COUNT(*) as cnt FROM ${catTable}${vendorFilter}`);
+          catalogCount = parseInt(totalRes.rows[0].cnt) || 0;
+
+          // Catalog products with cost/price
+          if (cols.price) {
+            const costRes = await pool.query(
+              `SELECT COUNT(*) as cnt FROM ${catTable} WHERE ${cols.price} IS NOT NULL AND ${cols.price}::text != '' AND ${cols.price}::text != '0'${vendorAnd}`
+            );
+            catWithCost = parseInt(costRes.rows[0].cnt) || 0;
+          }
+
+          // Catalog products with images (handle array columns like image_urls)
+          if (cols.image) {
+            let imgQuery;
+            if (cols.image.includes('[')) {
+              // Array column like image_urls[1]
+              const arrCol = cols.image.split('[')[0];
+              imgQuery = `SELECT COUNT(*) as cnt FROM ${catTable} WHERE ${arrCol} IS NOT NULL AND array_length(${arrCol},1) > 0${vendorAnd}`;
+            } else {
+              imgQuery = `SELECT COUNT(*) as cnt FROM ${catTable} WHERE ${cols.image} IS NOT NULL AND ${cols.image} != ''${vendorAnd}`;
+            }
+            const imgRes = await pool.query(imgQuery);
+            catWithImages = parseInt(imgRes.rows[0].cnt) || 0;
+          }
+
+          // Catalog products with width spec
+          const specCols = SPEC_COLUMNS[catTable];
+          if (specCols) {
+            if (specCols.width) {
+              try {
+                const widthRes = await pool.query(
+                  `SELECT COUNT(*) as cnt FROM ${catTable} WHERE ${specCols.width} IS NOT NULL AND ${specCols.width}::text != '' AND ${specCols.width}::text != '0'${vendorAnd}`
+                );
+                catWithWidth = parseInt(widthRes.rows[0].cnt) || 0;
+              } catch (_) {}
+            }
+            if (specCols.repeat) {
+              try {
+                const repeatRes = await pool.query(
+                  `SELECT COUNT(*) as cnt FROM ${catTable} WHERE ${specCols.repeat} IS NOT NULL AND ${specCols.repeat}::text != '' AND ${specCols.repeat}::text != '0'${vendorAnd}`
+                );
+                catWithRepeat = parseInt(repeatRes.rows[0].cnt) || 0;
+              } catch (_) {}
+            }
+          }
+
+          // Catalog products with about_vendor, all_images, spec_sheet, body_html
+          const assetFields = [
+            { col: 'about_vendor', setter: (v) => catWithAboutVendor = v },
+            { col: 'all_images', setter: (v) => catWithAllImages = v },
+            { col: 'spec_sheet', setter: (v) => catWithSpecSheet = v },
+            { col: 'body_html', setter: (v) => catWithBodyHtml = v }
+          ];
+          for (const af of assetFields) {
+            try {
+              const afRes = await pool.query(
+                `SELECT COUNT(*) as cnt FROM ${catTable} WHERE ${af.col} IS NOT NULL AND ${af.col}::text != '' AND ${af.col}::text != '[]'${vendorAnd}`
+              );
+              af.setter(parseInt(afRes.rows[0].cnt) || 0);
+            } catch (_) { /* column may not exist */ }
+          }
+
+          // Catalog products already on Shopify
+          try {
+            const shopRes = await pool.query(
+              `SELECT COUNT(*) as cnt FROM ${catTable} WHERE shopify_product_id IS NOT NULL AND shopify_product_id != ''${vendorAnd}`
+            );
+            catOnShopify = parseInt(shopRes.rows[0].cnt) || 0;
+          } catch (_) { /* no shopify_product_id column */ }
+        } catch (_) { /* table may not exist yet */ }
+      }
+
+      // --- Spec Completeness ---
+      const specResult = await computeSpecCompleteness(catTable);
+      const specPct = specResult.pct;
+      const specPresent = specResult.present;
+      const specTotal = specResult.total;
+
+      // --- SECONDARY: Shopify products (what's published) ---
+      const patterns = VENDOR_NAME_MAP[code];
+      if (patterns && patterns.length > 0) {
+        try {
+          const conditions = patterns.map((_, i) => `vendor ILIKE $${i + 1}`).join(' OR ');
+          const shopRes = await pool.query(
+            `SELECT COUNT(DISTINCT shopify_id) as cnt,
+              COUNT(CASE WHEN image_url IS NOT NULL AND image_url != '' THEN 1 END) as with_img,
+              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
+          );
+          shopifyCount = parseInt(shopRes.rows[0].cnt) || 0;
+          shopifyWithImages = parseInt(shopRes.rows[0].with_img) || 0;
+          shopifyDraft = parseInt(shopRes.rows[0].draft_cnt) || 0;
+          shopifyArchived = parseInt(shopRes.rows[0].arch_cnt) || 0;
+        } catch (_) {}
+      }
+
+      // --- Parker cost data ---
+      if (patterns && patterns.length > 0) {
+        try {
+          const parkerPatterns = patterns.map(p => '%' + p.replace(/%/g, '') + '%');
+          const parkerConditions = parkerPatterns.map((_, i) => `vendor_name ILIKE $${i + 1}`).join(' OR ');
+          const parkerRes = await pool.query(
+            `SELECT COUNT(*) as cnt FROM parker_products WHERE vendor_cost > 0 AND (${parkerConditions})`,
+            parkerPatterns
+          );
+          parkerCost = parseInt(parkerRes.rows[0].cnt) || 0;
+        } catch (_) {}
+      }
+
+      // Determine best product count: catalog (primary) > shopify (secondary)
+      const totalProducts = catalogCount > 0 ? catalogCount : shopifyCount;
+      const totalWithCost = Math.max(catWithCost, parkerCost);
+      const totalWithImages = catalogCount > 0 ? catWithImages : shopifyWithImages;
+      const notOnShopify = catalogCount > 0 ? Math.max(0, catalogCount - catOnShopify) : 0;
+
+      const discCount = shopifyDraft + shopifyArchived;
+
+      await pool.query(
+        `UPDATE vendor_registry SET
+          total_products = $1, products_with_cost = $2, products_with_images = $3,
+          catalog_count = $4, shopify_count = $5, catalog_with_cost = $6,
+          catalog_with_images = $7, catalog_on_shopify = $8, catalog_not_on_shopify = $9,
+          catalog_with_width = $10, catalog_with_repeat = $11,
+          shopify_draft = $12, shopify_archived = $13, discontinued_count = $14,
+          spec_completeness = $15, spec_fields_present = $16, spec_fields_total = $17,
+          catalog_with_about_vendor = $19, catalog_with_all_images = $20,
+          catalog_with_spec_sheet = $21, catalog_with_body_html = $22,
+          updated_at = NOW()
+        WHERE vendor_code = $18`,
+        [totalProducts, totalWithCost, totalWithImages,
+         catalogCount, shopifyCount, catWithCost,
+         catWithImages, catOnShopify, notOnShopify,
+         catWithWidth, catWithRepeat,
+         shopifyDraft, shopifyArchived, discCount,
+         specPct, specPresent, specTotal, code,
+         catWithAboutVendor, catWithAllImages, catWithSpecSheet, catWithBodyHtml]
+      );
+      updated++;
+
+      // Log significant changes
+      const oldCat = parseInt(v.old_cat) || 0;
+      const oldShop = parseInt(v.old_shop) || 0;
+      if (catalogCount > 0 && catalogCount !== oldCat) {
+        const diff = catalogCount - oldCat;
+        const arrow = diff > 0 ? '+' : '';
+        logActivity('sync', `${v.vendor_name}: catalog ${oldCat} -> ${catalogCount} (${arrow}${diff})`, { code, diff });
+      }
+      if (shopifyCount > 0 && shopifyCount !== oldShop) {
+        const diff = shopifyCount - oldShop;
+        const arrow = diff > 0 ? '+' : '';
+        logActivity('sync', `${v.vendor_name}: Shopify ${oldShop} -> ${shopifyCount} (${arrow}${diff})`, { code, diff });
+      }
+    }
+
+    const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+    console.log(`[Victor] Sync complete: ${updated} vendors updated in ${elapsed}s`);
+    logActivity('sync', `Sync complete: ${updated} vendors updated in ${elapsed}s`);
+    return { success: true, updated, elapsed };
+  } catch (err) {
+    console.error('[Victor] Sync error:', err.message);
+    logActivity('error', 'Sync failed: ' + err.message);
+    return { success: false, error: err.message };
+  }
+}
+
+// --- API: Sample product from vendor catalog (best-populated row) ---
+app.get('/api/vendors/:code/sample-product', async (req, res) => {
+  try {
+    const { rows: vendorRows } = await pool.query(
+      'SELECT catalog_table, vendor_name FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (!vendorRows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+    const table = vendorRows[0].catalog_table;
+    if (!table) return res.json({ success: true, data: null, message: 'No catalog table configured' });
+    // Sanitize table name
+    if (!/^[a-z0-9_]+$/.test(table)) return res.status(400).json({ success: false, error: 'Invalid table name' });
+    // Get columns
+    const colResult = await pool.query(
+      `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1 ORDER BY ordinal_position`, [table]
+    );
+    const columns = colResult.rows.map(r => r.column_name);
+    // Get best-populated product (most non-null/non-empty fields, with image)
+    const sample = await pool.query(
+      `SELECT * FROM ${table} WHERE image_url IS NOT NULL AND image_url != '' ORDER BY updated_at DESC NULLS LAST LIMIT 1`
+    );
+    if (!sample.rows.length) {
+      // Fallback: any product
+      const fallback = await pool.query(`SELECT * FROM ${table} ORDER BY id LIMIT 1`);
+      return res.json({ success: true, data: fallback.rows[0] || null, columns, schema: colResult.rows, vendor: vendorRows[0].vendor_name });
+    }
+    res.json({ success: true, data: sample.rows[0], columns, schema: colResult.rows, vendor: vendorRows[0].vendor_name });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Chat with vendor agent (data-driven answers) ---
+app.post('/api/vendors/:code/chat', async (req, res) => {
+  try {
+    const { message } = req.body;
+    if (!message) return res.status(400).json({ success: false, error: 'Message required' });
+    const { rows: vendorRows } = await pool.query(
+      'SELECT * FROM vendor_registry WHERE vendor_code = $1', [req.params.code]
+    );
+    if (!vendorRows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+    const v = vendorRows[0];
+    const table = v.catalog_table;
+    let catalogStats = null;
+    if (table && /^[a-z0-9_]+$/.test(table)) {
+      const stats = await pool.query(`
+        SELECT
+          COUNT(*) as total,
+          COUNT(*) FILTER (WHERE image_url IS NOT NULL AND image_url != '') as with_images,
+          COUNT(*) FILTER (WHERE width IS NOT NULL AND width != '') as with_width,
+          COUNT(*) FILTER (WHERE repeat_v IS NOT NULL AND repeat_v != '') as with_repeat,
+          COUNT(*) FILTER (WHERE discontinued = true) as discontinued,
+          COUNT(*) FILTER (WHERE shopify_product_id IS NOT NULL AND shopify_product_id != '') as on_shopify,
+          COUNT(*) FILTER (WHERE price_retail IS NOT NULL) as with_price,
+          COUNT(*) FILTER (WHERE material IS NOT NULL AND material != '') as with_material,
+          MIN(last_scraped) as oldest_scrape,
+          MAX(last_scraped) as newest_scrape,
+          COUNT(*) FILTER (WHERE last_scraped IS NULL) as never_scraped
+        FROM ${table}
+      `);
+      catalogStats = stats.rows[0];
+    }
+    // Build context for response
+    const context = {
+      vendor: v.vendor_name,
+      code: v.vendor_code,
+      agent: v.agent_name,
+      port: v.agent_port,
+      catalog_table: table,
+      catalog_count: v.catalog_count,
+      shopify_count: v.shopify_count,
+      not_on_shopify: v.catalog_not_on_shopify,
+      website_score: v.website_score,
+      website_url: v.website_url,
+      has_credentials: v.has_credentials,
+      crawl_priority: v.crawl_priority,
+      last_product_update: v.last_product_update,
+      learnings: v.learnings,
+      catalogStats
+    };
+    // Try to proxy to agent first
+    if (v.agent_port) {
+      try {
+        const agentResp = await fetch(`http://127.0.0.1:${v.agent_port}/api/chat`, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json', 'Authorization': 'Basic ' + Buffer.from(process.env.GEORGE_BASIC_AUTH || '').toString('base64') },
+          body: JSON.stringify({ message, context }),
+          signal: AbortSignal.timeout(5000)
+        });
+        if (agentResp.ok) {
+          const agentData = await agentResp.json();
+          return res.json({ success: true, source: 'agent', response: agentData.response || agentData.message, context });
+        }
+      } catch (e) { /* Agent doesn't have chat — fall through to data-driven response */ }
+    }
+    // Data-driven response
+    let answer = `${v.vendor_name} (${v.vendor_code}) — Agent: ${v.agent_name || '?'} on port ${v.agent_port || '?'}\n`;
+    if (catalogStats) {
+      answer += `\nCatalog: ${catalogStats.total} products in ${table}\n`;
+      answer += `• With images: ${catalogStats.with_images}\n`;
+      answer += `• With width: ${catalogStats.with_width}\n`;
+      answer += `• With repeat: ${catalogStats.with_repeat}\n`;
+      answer += `• With price: ${catalogStats.with_price}\n`;
+      answer += `• With material: ${catalogStats.with_material}\n`;
+      answer += `• On Shopify: ${catalogStats.on_shopify}\n`;
+      answer += `• Discontinued: ${catalogStats.discontinued}\n`;
+      answer += `• Never scraped: ${catalogStats.never_scraped}\n`;
+      answer += `\nLast scrape: ${catalogStats.newest_scrape ? new Date(catalogStats.newest_scrape).toLocaleDateString() : 'never'}\n`;
+    }
+    answer += `\nShopify: ${v.shopify_count || 0} listed | Not listed: ${v.catalog_not_on_shopify || 0}`;
+    if (v.learnings) answer += `\n\nNotes: ${v.learnings}`;
+    res.json({ success: true, source: 'data', response: answer, context });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Trigger sync ---
+app.post('/api/sync', async (req, res) => {
+  logActivity('api', 'Manual sync triggered via API');
+  const result = await syncVendorCounts();
+  res.json(result);
+});
+
+// --- API: Live Activity Feed ---
+app.get('/api/activity', (req, res) => {
+  const limit = Math.min(parseInt(req.query.limit) || 50, 200);
+  const since = req.query.since || null;
+  let entries = activityLog;
+  if (since) {
+    entries = entries.filter(e => e.ts > since);
+  }
+  res.json({ entries: entries.slice(0, limit), total: activityLog.length });
+});
+
+// --- API: Products list for a vendor (from shopify_products) ---
+app.get('/api/vendors/:code/products', async (req, res) => {
+  try {
+    const { rows: vendorRows } = await pool.query(
+      'SELECT vendor_name, vendor_code FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (!vendorRows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+
+    const patterns = VENDOR_NAME_MAP[req.params.code];
+    if (!patterns || patterns.length === 0) {
+      return res.json({ success: true, data: [], count: 0, message: 'No Shopify vendor mapping configured' });
+    }
+
+    const limit = Math.min(parseInt(req.query.limit) || 100, 500);
+    const offset = parseInt(req.query.offset) || 0;
+    const search = req.query.search || '';
+
+    const conditions = patterns.map((_, i) => `vendor ILIKE $${i + 1}`).join(' OR ');
+    const values = [...patterns];
+    let whereExtra = '';
+    if (search) {
+      values.push('%' + search + '%');
+      whereExtra = ` AND (title ILIKE $${values.length} OR sku ILIKE $${values.length} OR dw_sku ILIKE $${values.length})`;
+    }
+    values.push(limit, offset);
+
+    const query = `
+      SELECT shopify_id, title, vendor, sku, dw_sku, mfr_sku, image_url, cost_price, retail_price, price, status, tags
+      FROM shopify_products
+      WHERE (${conditions})${whereExtra}
+      ORDER BY title ASC
+      LIMIT $${values.length - 1} OFFSET $${values.length}
+    `;
+
+    const countQuery = `SELECT COUNT(DISTINCT shopify_id) as cnt FROM shopify_products WHERE (${conditions})${whereExtra}`;
+    const countValues = values.slice(0, values.length - 2);
+
+    const [dataResult, countResult] = await Promise.all([
+      pool.query(query, values),
+      pool.query(countQuery, countValues)
+    ]);
+
+    res.json({
+      success: true,
+      vendor: vendorRows[0].vendor_name,
+      data: dataResult.rows,
+      count: parseInt(countResult.rows[0].cnt),
+      limit,
+      offset
+    });
+  } 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 {
+    const limit = Math.min(parseInt(req.query.limit) || 50, 200);
+    const offset = parseInt(req.query.offset) || 0;
+    const vendor = req.query.vendor || '';
+    const search = req.query.search || '';
+
+    const conditions = [];
+    const values = [];
+    let idx = 1;
+
+    if (vendor) {
+      conditions.push(`vc.vendor_code = $${idx++}`);
+      values.push(vendor);
+    }
+    if (search) {
+      conditions.push(`(vc.dw_sku ILIKE $${idx} OR vc.mfr_sku ILIKE $${idx} OR vc.pattern_name ILIKE $${idx})`);
+      values.push('%' + search + '%');
+      idx++;
+    }
+
+    // Optional brand-flag filters (apply via JOIN on vendor_registry)
+    if (req.query.pl === '1')    conditions.push(`vr.is_private_label = true`);
+    if (req.query.brand === '1') conditions.push(`vr.show_on_brands_page = true`);
+
+    const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
+
+    const dataQuery = `
+      SELECT vc.dw_sku, vc.mfr_sku, vc.pattern_name, vc.color_name, vc.vendor_code,
+             vc.image_url, vc.on_shopify,
+             vr.is_private_label, vr.show_on_brands_page, vr.private_label_name, vr.vendor_name,
+             et.phase1_scraped_at IS NOT NULL AS p1,
+             et.phase2_specs_at IS NOT NULL AS p2,
+             et.phase3_ai_at IS NOT NULL AS p3,
+             et.phase4_silas_at IS NOT NULL AS p4,
+             et.phase5_vcc_at IS NOT NULL AS p5,
+             et.phase6_rooms_at IS NOT NULL AS p6,
+             et.phase7_spin_at IS NOT NULL AS p7,
+             et.phase8_shopify_at IS NOT NULL AS p8,
+             (CASE WHEN et.phase1_scraped_at IS NOT NULL THEN 1 ELSE 0 END +
+              CASE WHEN et.phase2_specs_at IS NOT NULL THEN 1 ELSE 0 END +
+              CASE WHEN et.phase3_ai_at IS NOT NULL THEN 1 ELSE 0 END +
+              CASE WHEN et.phase4_silas_at IS NOT NULL THEN 1 ELSE 0 END +
+              CASE WHEN et.phase5_vcc_at IS NOT NULL THEN 1 ELSE 0 END +
+              CASE WHEN et.phase6_rooms_at IS NOT NULL THEN 1 ELSE 0 END +
+              CASE WHEN et.phase7_spin_at IS NOT NULL THEN 1 ELSE 0 END +
+              CASE WHEN et.phase8_shopify_at IS NOT NULL THEN 1 ELSE 0 END) AS phases_done
+      FROM vendor_catalog vc
+      LEFT JOIN enrichment_tracking et ON et.vendor_code = vc.vendor_code AND et.mfr_sku = vc.mfr_sku
+      LEFT JOIN vendor_registry vr ON vr.vendor_code = vc.vendor_code
+      ${where}
+      ORDER BY vc.vendor_code, vc.dw_sku
+      LIMIT $${idx++} OFFSET $${idx++}
+    `;
+    values.push(limit, offset);
+
+    const countQuery = `
+      SELECT COUNT(*) AS cnt
+      FROM vendor_catalog vc
+      LEFT JOIN vendor_registry vr ON vr.vendor_code = vc.vendor_code
+      ${where}
+    `;
+    const countValues = values.slice(0, values.length - 2);
+
+    const [dataResult, countResult] = await Promise.all([
+      pool.query(dataQuery, values),
+      pool.query(countQuery, countValues)
+    ]);
+
+    res.json({
+      success: true,
+      data: dataResult.rows,
+      total: parseInt(countResult.rows[0].cnt),
+      limit,
+      offset
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+app.get('/api/catalog-browse/vendors', async (req, res) => {
+  try {
+    const { rows } = await pool.query(`
+      SELECT vendor_code, COUNT(*) AS product_count
+      FROM vendor_catalog
+      GROUP BY vendor_code
+      ORDER BY product_count DESC
+    `);
+    res.json({ success: true, data: rows });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// === [ADMIN MODAL] Catalog product detail / cover / collections (added 2026-05-05) ===
+const VCC_SHOP_DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const VCC_SHOP_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const VCC_SHOP_API = 'https://' + VCC_SHOP_DOMAIN + '/admin/api/2024-10';
+
+// In-memory 5-min cache for collection list
+let _vccCollectionsCache = { ts: 0, data: null };
+
+function _vccNumericId(gidOrNum) {
+  if (!gidOrNum) return null;
+  const s = String(gidOrNum);
+  const m = s.match(/(\d+)$/);
+  return m ? m[1] : null;
+}
+
+async function _vccShopifyFetch(path, opts) {
+  opts = opts || {};
+  opts.headers = Object.assign({
+    'X-Shopify-Access-Token': VCC_SHOP_TOKEN,
+    'Content-Type': 'application/json'
+  }, opts.headers || {});
+  const r = await fetch(VCC_SHOP_API + path, opts);
+  const text = await r.text();
+  let body = null;
+  try { body = text ? JSON.parse(text) : null; } catch { body = { raw: text }; }
+  return { ok: r.ok, status: r.status, body };
+}
+
+// Resolve a vendor_catalog row (vendor_code + mfr_sku) to a numeric Shopify product id.
+// We accept either the numeric id directly (preferred) OR a "vendor:mfr_sku" pair as fallback.
+async function _vccResolveProductId(idParam, vendor, mfrSku) {
+  const direct = _vccNumericId(idParam);
+  if (direct && /^\d{6,}$/.test(direct)) return direct;
+  if (vendor && mfrSku) {
+    const { rows } = await pool.query(
+      `SELECT shopify_id FROM shopify_products
+        WHERE LOWER(mfr_sku) = LOWER($1) AND vendor ILIKE $2 AND status='ACTIVE'
+        ORDER BY id DESC LIMIT 1`,
+      [mfrSku, vendor]
+    );
+    if (rows.length) return _vccNumericId(rows[0].shopify_id);
+  }
+  return null;
+}
+
+// GET /api/catalog-browse/product/:id
+// :id may be a numeric Shopify product id, or query ?vendor=&mfr_sku= as fallback lookup.
+app.get('/api/catalog-browse/product/:id', async (req, res) => {
+  try {
+    const pid = await _vccResolveProductId(req.params.id, req.query.vendor, req.query.mfr_sku);
+    if (!pid) return res.status(404).json({ success: false, error: 'Product not found in shopify_products' });
+
+    const [prod, imgs, collects] = await Promise.all([
+      _vccShopifyFetch('/products/' + pid + '.json?fields=id,title,handle,vendor,status,image'),
+      _vccShopifyFetch('/products/' + pid + '/images.json'),
+      _vccShopifyFetch('/collects.json?product_id=' + pid + '&limit=250&fields=id,collection_id')
+    ]);
+    if (!prod.ok) return res.status(prod.status).json({ success: false, error: 'Shopify product fetch failed', detail: prod.body });
+
+    const product = prod.body && prod.body.product;
+    const images = (imgs.body && imgs.body.images) || [];
+    const collectsArr = (collects.body && collects.body.collects) || [];
+
+    res.json({
+      success: true,
+      product: {
+        id: product.id,
+        title: product.title,
+        handle: product.handle,
+        vendor: product.vendor,
+        status: product.status,
+        cover_image_id: product.image ? product.image.id : null
+      },
+      images: images.map(i => ({ id: i.id, src: i.src, position: i.position, alt: i.alt, width: i.width, height: i.height })),
+      collection_ids: collectsArr.map(c => c.collection_id),
+      collects: collectsArr
+    });
+  } catch (err) {
+    console.error('[vcc product detail]', err);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// POST /api/catalog-browse/product/:id/cover-photo
+// body: { image_id }  -> set that image to position 1 (REST PUT /images/:id)
+//   OR  { upload: <base64>, filename, alt } -> upload as new image at position 1
+app.post('/api/catalog-browse/product/:id/cover-photo', async (req, res) => {
+  try {
+    const pid = await _vccResolveProductId(req.params.id, req.body.vendor, req.body.mfr_sku);
+    if (!pid) return res.status(404).json({ success: false, error: 'Product not found' });
+    const { image_id, upload, filename, alt } = req.body || {};
+
+    if (upload) {
+      // Strip data: URL prefix if present
+      const b64 = String(upload).replace(/^data:image\/[a-zA-Z+]+;base64,/, '');
+      const r = await _vccShopifyFetch('/products/' + pid + '/images.json', {
+        method: 'POST',
+        body: JSON.stringify({ image: { attachment: b64, filename: filename || 'cover.jpg', position: 1, alt: alt || '' } })
+      });
+      if (!r.ok) return res.status(r.status).json({ success: false, error: 'Image upload failed', detail: r.body });
+      return res.json({ success: true, action: 'uploaded', image: r.body.image });
+    }
+
+    if (!image_id) return res.status(400).json({ success: false, error: 'image_id or upload required' });
+    const r = await _vccShopifyFetch('/products/' + pid + '/images/' + image_id + '.json', {
+      method: 'PUT',
+      body: JSON.stringify({ image: { id: Number(image_id), position: 1 } })
+    });
+    if (!r.ok) return res.status(r.status).json({ success: false, error: 'Reorder failed', detail: r.body });
+    res.json({ success: true, action: 'promoted', image: r.body.image });
+  } catch (err) {
+    console.error('[vcc cover-photo]', err);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// GET /api/shopify/collections  (5-min cache)
+// Returns merged custom + smart collections: [{id, title, handle, type}]
+app.get('/api/shopify/collections', async (req, res) => {
+  try {
+    const now = Date.now();
+    if (_vccCollectionsCache.data && (now - _vccCollectionsCache.ts) < 5 * 60 * 1000) {
+      return res.json({ success: true, cached: true, count: _vccCollectionsCache.data.length, data: _vccCollectionsCache.data });
+    }
+    async function pageAll(kind) {
+      const out = [];
+      let url = '/' + kind + '.json?limit=250&fields=id,title,handle';
+      // Shopify REST cursor pagination via Link header — fetch full URL when present
+      let nextFull = null;
+      while (true) {
+        const r = nextFull
+          ? await fetch(nextFull, { headers: { 'X-Shopify-Access-Token': VCC_SHOP_TOKEN } })
+          : await fetch(VCC_SHOP_API + url, { headers: { 'X-Shopify-Access-Token': VCC_SHOP_TOKEN } });
+        if (!r.ok) break;
+        const j = await r.json();
+        const arr = j[kind] || [];
+        for (const c of arr) out.push({ id: c.id, title: c.title, handle: c.handle, type: kind === 'custom_collections' ? 'custom' : 'smart' });
+        const link = r.headers.get('link') || r.headers.get('Link');
+        if (!link) break;
+        const m = link.match(/<([^>]+)>;\s*rel="next"/);
+        if (!m) break;
+        nextFull = m[1];
+      }
+      return out;
+    }
+    const [custom, smart] = await Promise.all([pageAll('custom_collections'), pageAll('smart_collections')]);
+    const all = custom.concat(smart).sort((a, b) => (a.title || '').localeCompare(b.title || ''));
+    _vccCollectionsCache = { ts: now, data: all };
+    res.json({ success: true, cached: false, count: all.length, data: all });
+  } catch (err) {
+    console.error('[vcc collections list]', err);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// POST /api/catalog-browse/product/:id/collections
+// body: { add: [collection_id...], remove: [collection_id...] }
+// Smart collections cannot be modified via Collect API — those are silently skipped & reported.
+app.post('/api/catalog-browse/product/:id/collections', async (req, res) => {
+  try {
+    const pid = await _vccResolveProductId(req.params.id, req.body.vendor, req.body.mfr_sku);
+    if (!pid) return res.status(404).json({ success: false, error: 'Product not found' });
+    const add = Array.isArray(req.body.add) ? req.body.add : [];
+    const remove = Array.isArray(req.body.remove) ? req.body.remove : [];
+
+    const results = { added: [], removed: [], skipped_smart: [], errors: [] };
+
+    // Build smart-collection set so we can skip them (Collect API only works on custom_collections)
+    const colList = (_vccCollectionsCache.data) || [];
+    const smartIds = new Set(colList.filter(c => c.type === 'smart').map(c => String(c.id)));
+
+    for (const cid of add) {
+      if (smartIds.has(String(cid))) { results.skipped_smart.push(cid); continue; }
+      const r = await _vccShopifyFetch('/collects.json', {
+        method: 'POST',
+        body: JSON.stringify({ collect: { product_id: Number(pid), collection_id: Number(cid) } })
+      });
+      if (r.ok) results.added.push(cid);
+      else results.errors.push({ op: 'add', collection_id: cid, status: r.status, detail: r.body });
+    }
+
+    if (remove.length) {
+      // Need collect_id per (product, collection) to DELETE
+      const lookup = await _vccShopifyFetch('/collects.json?product_id=' + pid + '&limit=250&fields=id,collection_id');
+      const map = {};
+      ((lookup.body && lookup.body.collects) || []).forEach(c => { map[String(c.collection_id)] = c.id; });
+      for (const cid of remove) {
+        if (smartIds.has(String(cid))) { results.skipped_smart.push(cid); continue; }
+        const collectId = map[String(cid)];
+        if (!collectId) { results.errors.push({ op: 'remove', collection_id: cid, error: 'not currently a member' }); continue; }
+        const r = await _vccShopifyFetch('/collects/' + collectId + '.json', { method: 'DELETE' });
+        if (r.ok || r.status === 404) results.removed.push(cid);
+        else results.errors.push({ op: 'remove', collection_id: cid, status: r.status, detail: r.body });
+      }
+    }
+
+    res.json({ success: true, product_id: pid, ...results });
+  } catch (err) {
+    console.error('[vcc collections update]', err);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+// === [/ADMIN MODAL] ===
+
+// Serve catalog browse page
+app.get('/catalog-browse', (req, res) => {
+  res.sendFile(__dirname + '/public/catalog-browse.html');
+});
+
+// Run sync on startup (after 5s delay to let DB connect) and every 5 minutes
+setTimeout(() => syncVendorCounts(), 5000);
+setInterval(() => syncVendorCounts(), 300000);
+
+// --- Utility: Escape HTML to prevent XSS ---
+function escapeHtml(str) {
+  if (!str) return '';
+  return String(str)
+    .replace(/&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
+// --- API: Score a single vendor website ---
+app.post('/api/vendors/:code/score', async (req, res) => {
+  try {
+    const { rows } = await pool.query(
+      'SELECT vendor_code, vendor_name, website_url FROM vendor_registry WHERE vendor_code = $1',
+      [req.params.code]
+    );
+    if (!rows.length) return res.status(404).json({ success: false, error: 'Vendor not found' });
+    const v = rows[0];
+    if (!v.website_url) return res.json({ success: false, error: 'No website URL configured' });
+
+    console.log(`[Victor] Scoring website for ${v.vendor_name}: ${v.website_url}`);
+    const result = await scoreWebsite(v.website_url);
+
+    await pool.query(
+      `UPDATE vendor_registry SET website_score = $1, website_summary = $2, website_tech_stack = $3, website_scored_at = NOW() WHERE vendor_code = $4`,
+      [result.score, result.summary, result.tech_stack || '', v.vendor_code]
+    );
+
+    res.json({ success: true, data: { vendor_code: v.vendor_code, ...result } });
+  } catch (err) {
+    console.error(`[Victor] Score error:`, err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Score all vendor websites (batch, throttled) ---
+app.post('/api/score-all', async (req, res) => {
+  try {
+    const staleOnly = req.body.stale_only !== false; // default: only score stale/unscored
+    let query = 'SELECT vendor_code, vendor_name, website_url FROM vendor_registry WHERE website_url IS NOT NULL';
+    if (staleOnly) {
+      query += ` AND (website_scored_at IS NULL OR website_scored_at < NOW() - INTERVAL '7 days')`;
+    }
+    query += ' ORDER BY website_score ASC NULLS FIRST';
+    const { rows } = await pool.query(query);
+
+    res.json({ success: true, message: `Scoring ${rows.length} vendors in background`, count: rows.length });
+
+    // Background scoring with 2s delay between requests
+    (async () => {
+      let scored = 0;
+      for (const v of rows) {
+        try {
+          const result = await scoreWebsite(v.website_url);
+          await pool.query(
+            `UPDATE vendor_registry SET website_score = $1, website_summary = $2, website_tech_stack = $3, website_scored_at = NOW() WHERE vendor_code = $4`,
+            [result.score, result.summary, result.tech_stack || '', v.vendor_code]
+          );
+          scored++;
+          console.log(`[Victor] Scored ${v.vendor_name}: ${result.score}/10 (${scored}/${rows.length})`);
+        } catch (err) {
+          console.log(`[Victor] Score failed for ${v.vendor_name}: ${err.message}`);
+        }
+        // Throttle: 2s between requests to avoid rate limits
+        await new Promise(r => setTimeout(r, 2000));
+      }
+      console.log(`[Victor] Batch scoring complete: ${scored}/${rows.length} vendors scored`);
+    })();
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// ============================================================================
+// DATA QUALITY (Strategy 3) - products table + catalog audit
+// ============================================================================
+
+// --- API: Data quality summary for products table ---
+app.get('/api/data-quality', async (req, res) => {
+  try {
+    // Products table quality
+    const pq = await pool.query(`
+      SELECT
+        COUNT(*) as total,
+        COUNT(CASE WHEN image_urls IS NOT NULL AND array_length(image_urls, 1) > 0 THEN 1 END) as has_images,
+        COUNT(CASE WHEN width_in IS NOT NULL AND width_in > 0 THEN 1 END) as has_width,
+        COUNT(CASE WHEN material IS NOT NULL AND material != '' THEN 1 END) as has_material,
+        COUNT(CASE WHEN repeat_vertical_in IS NOT NULL THEN 1 END) as has_repeat_v,
+        COUNT(CASE WHEN repeat_horizontal_in IS NOT NULL THEN 1 END) as has_repeat_h,
+        COUNT(CASE WHEN match_type IS NOT NULL AND match_type != '' THEN 1 END) as has_match,
+        COUNT(CASE WHEN content IS NOT NULL AND content != '' THEN 1 END) as has_content,
+        COUNT(CASE WHEN price IS NOT NULL AND price > 0 THEN 1 END) as has_price,
+        COUNT(CASE WHEN backing IS NOT NULL AND backing != '' THEN 1 END) as has_backing,
+        COUNT(CASE WHEN fire_rating IS NOT NULL AND fire_rating != '' THEN 1 END) as has_fire_rating,
+        COUNT(CASE WHEN thumbnail_url IS NOT NULL AND thumbnail_url != '' THEN 1 END) as has_thumbnail
+      FROM products
+    `);
+
+    // Per-vendor breakdown
+    const vq = await pool.query(`
+      SELECT
+        vendor_id,
+        COUNT(*) as total,
+        COUNT(CASE WHEN image_urls IS NOT NULL AND array_length(image_urls, 1) > 0 THEN 1 END) as has_images,
+        COUNT(CASE WHEN width_in IS NOT NULL AND width_in > 0 THEN 1 END) as has_width,
+        COUNT(CASE WHEN material IS NOT NULL AND material != '' THEN 1 END) as has_material,
+        COUNT(CASE WHEN content IS NOT NULL AND content != '' THEN 1 END) as has_content,
+        COUNT(CASE WHEN price IS NOT NULL AND price > 0 THEN 1 END) as has_price,
+        COUNT(CASE WHEN repeat_vertical_in IS NOT NULL THEN 1 END) as has_repeat_v
+      FROM products
+      GROUP BY vendor_id
+      ORDER BY total DESC
+    `);
+
+    // Open issues count
+    const iq = await pool.query(`
+      SELECT COUNT(*) as open_issues FROM data_quality_issues WHERE resolved = false
+    `);
+
+    res.json({
+      success: true,
+      summary: pq.rows[0],
+      vendors: vq.rows,
+      open_issues: parseInt(iq.rows[0].open_issues),
+      ts: new Date().toISOString()
+    });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Run data quality audit (populate data_quality_issues) ---
+app.post('/api/data-quality/audit', async (req, res) => {
+  try {
+    const client = await pool.connect();
+    let created = 0;
+
+    // Find all products missing critical fields
+    const { rows } = await client.query(`
+      SELECT id, vendor_id, sku, name,
+        (image_urls IS NULL OR array_length(image_urls, 1) IS NULL) as missing_image,
+        (width_in IS NULL OR width_in = 0) as missing_width,
+        (material IS NULL OR material = '') as missing_material,
+        (content IS NULL OR content = '') as missing_content,
+        (price IS NULL OR price = 0) as missing_price,
+        (repeat_vertical_in IS NULL) as missing_repeat
+      FROM products
+    `);
+
+    // Clear old unresolved issues
+    await client.query(`DELETE FROM data_quality_issues WHERE resolved = false`);
+
+    for (const p of rows) {
+      const issues = [];
+      if (p.missing_image) issues.push({ type: 'missing_image', severity: 'critical' });
+      if (p.missing_width) issues.push({ type: 'missing_width', severity: 'high' });
+      if (p.missing_material) issues.push({ type: 'missing_material', severity: 'medium' });
+      if (p.missing_content) issues.push({ type: 'missing_content', severity: 'medium' });
+      if (p.missing_price) issues.push({ type: 'missing_price', severity: 'high' });
+      if (p.missing_repeat) issues.push({ type: 'missing_repeat', severity: 'low' });
+
+      for (const issue of issues) {
+        await client.query(
+          `INSERT INTO data_quality_issues (issue_type, severity, vendor_id, product_id, details)
+           VALUES ($1, $2, $3, $4, $5)`,
+          [issue.type, issue.severity, p.vendor_id, p.id, JSON.stringify({ sku: p.sku, name: p.name })]
+        );
+        created++;
+      }
+    }
+
+    client.release();
+    logActivity('data-quality', `Audit complete: ${created} issues found across ${rows.length} products`);
+    res.json({ success: true, issues_created: created, products_audited: rows.length });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Data quality issues list ---
+app.get('/api/data-quality/issues', async (req, res) => {
+  try {
+    const vendor = req.query.vendor || null;
+    const severity = req.query.severity || null;
+    let query = `SELECT issue_type, severity, vendor_id, COUNT(*) as cnt
+                 FROM data_quality_issues WHERE resolved = false`;
+    const params = [];
+    if (vendor) { params.push(vendor); query += ` AND vendor_id = $${params.length}`; }
+    if (severity) { params.push(severity); query += ` AND severity = $${params.length}`; }
+    query += ` GROUP BY issue_type, severity, vendor_id ORDER BY cnt DESC`;
+    const { rows } = await pool.query(query, params);
+    res.json({ success: true, issues: rows });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Catalog-level data quality (for VCC main dashboard) ---
+app.get('/api/data-quality/catalog', async (req, res) => {
+  try {
+    const result = await pool.query(`
+      SELECT
+        vendor_code,
+        vendor_name,
+        catalog_count,
+        products_with_images,
+        catalog_with_width,
+        catalog_with_repeat,
+        products_with_cost,
+        spec_completeness,
+        CASE WHEN catalog_count > 0 THEN ROUND(products_with_images::numeric / catalog_count * 100, 1) ELSE 0 END as image_pct,
+        CASE WHEN catalog_count > 0 THEN ROUND(catalog_with_width::numeric / catalog_count * 100, 1) ELSE 0 END as width_pct,
+        CASE WHEN catalog_count > 0 THEN ROUND(catalog_with_repeat::numeric / catalog_count * 100, 1) ELSE 0 END as repeat_pct,
+        CASE WHEN catalog_count > 0 THEN ROUND(products_with_cost::numeric / catalog_count * 100, 1) ELSE 0 END as cost_pct
+      FROM vendor_registry
+      WHERE catalog_count > 0
+      ORDER BY catalog_count DESC
+    `);
+    res.json({ success: true, vendors: result.rows });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: Slack alert for data quality drops ---
+const SLACK_WEBHOOK = 'https://hooks.slack.com/services/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';
+
+app.post('/api/data-quality/slack-alert', async (req, res) => {
+  try {
+    const threshold = parseInt(req.body.threshold) || 80;
+    const result = await pool.query(`
+      SELECT vendor_code, vendor_name, catalog_count,
+        CASE WHEN catalog_count > 0 THEN ROUND(products_with_images::numeric / catalog_count * 100, 1) ELSE 0 END as image_pct,
+        CASE WHEN catalog_count > 0 THEN ROUND(catalog_with_width::numeric / catalog_count * 100, 1) ELSE 0 END as width_pct,
+        CASE WHEN catalog_count > 0 THEN ROUND(catalog_with_repeat::numeric / catalog_count * 100, 1) ELSE 0 END as repeat_pct,
+        spec_completeness
+      FROM vendor_registry
+      WHERE catalog_count > 50
+      ORDER BY spec_completeness ASC NULLS FIRST
+    `);
+
+    const flagged = result.rows.filter(v => {
+      const img = parseFloat(v.image_pct) || 0;
+      const width = parseFloat(v.width_pct) || 0;
+      const spec = parseFloat(v.spec_completeness) || 0;
+      return img < threshold || width < threshold || spec < threshold;
+    });
+
+    if (flagged.length === 0) {
+      return res.json({ success: true, message: 'All vendors above threshold', flagged: 0 });
+    }
+
+    const lines = flagged.slice(0, 15).map(v =>
+      `• *${v.vendor_name}* (${Number(v.catalog_count).toLocaleString()}) — Img: ${v.image_pct}% | Width: ${v.width_pct}% | Spec: ${v.spec_completeness}%`
+    );
+
+    const slackMsg = {
+      text: `:warning: *Data Quality Alert* — ${flagged.length} vendors below ${threshold}% threshold\n\n${lines.join('\n')}${flagged.length > 15 ? `\n...and ${flagged.length - 15} more` : ''}\n\n<http://45.61.58.125:9660/|View in VCC>`
+    };
+
+    await fetch(SLACK_WEBHOOK, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(slackMsg)
+    });
+
+    logActivity('data-quality', `Slack alert sent: ${flagged.length} vendors below ${threshold}%`);
+    res.json({ success: true, flagged: flagged.length, threshold });
+  } catch (err) {
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- Discontinued Products API ---
+app.get('/api/discontinued', async (req, res) => {
+  try {
+    const { vendor, page = 1, limit = 50, status: filterStatus } = req.query;
+    const offset = (parseInt(page) - 1) * parseInt(limit);
+
+    // Summary stats
+    const summaryQ = await pool.query(`
+      SELECT
+        COUNT(*) as total_discontinued,
+        COUNT(DISTINCT d.vendor_name) as vendor_count,
+        COUNT(sp.id) as matched_shopify,
+        COUNT(CASE WHEN sp.status = 'active' THEN 1 END) as still_active,
+        COUNT(CASE WHEN sp.status = 'archived' THEN 1 END) as already_archived,
+        COUNT(CASE WHEN sp.status = 'draft' THEN 1 END) as in_draft,
+        COUNT(CASE WHEN sp.id IS NULL THEN 1 END) as not_on_shopify
+      FROM discontinued_skus d
+      LEFT JOIN shopify_products sp ON d.dw_sku = sp.dw_sku
+    `);
+
+    // Vendor breakdown
+    const vendorQ = await pool.query(`
+      SELECT
+        COALESCE(NULLIF(d.vendor_name,''), 'Unknown') as vendor_name,
+        COUNT(*) as total,
+        COUNT(sp.id) as on_shopify,
+        COUNT(CASE WHEN sp.status = 'active' THEN 1 END) as active,
+        COUNT(CASE WHEN sp.status = 'archived' THEN 1 END) as archived,
+        COUNT(CASE WHEN sp.status = 'draft' THEN 1 END) as draft
+      FROM discontinued_skus d
+      LEFT JOIN shopify_products sp ON d.dw_sku = sp.dw_sku
+      GROUP BY COALESCE(NULLIF(d.vendor_name,''), 'Unknown')
+      ORDER BY COUNT(*) DESC
+    `);
+
+    // Product list (paginated, filterable)
+    let whereClause = '';
+    const params = [];
+    if (vendor && vendor !== 'all') {
+      params.push(vendor);
+      whereClause += ` AND COALESCE(NULLIF(d.vendor_name,''), 'Unknown') = $${params.length}`;
+    }
+    if (filterStatus === 'active') {
+      whereClause += ` AND sp.status = 'active'`;
+    } else if (filterStatus === 'archived') {
+      whereClause += ` AND sp.status = 'archived'`;
+    } else if (filterStatus === 'not_on_shopify') {
+      whereClause += ` AND sp.id IS NULL`;
+    }
+
+    params.push(parseInt(limit), offset);
+    const productsQ = await pool.query(`
+      SELECT
+        d.id, d.dw_handle, d.dw_sku, d.mfr_sku, d.vendor_code, d.vendor_name,
+        d.archive_tag, d.archived_date, d.notes,
+        sp.shopify_id, sp.title, sp.status as shopify_status, sp.image_url, sp.vendor as shopify_vendor
+      FROM discontinued_skus d
+      LEFT JOIN shopify_products sp ON d.dw_sku = sp.dw_sku
+      WHERE 1=1 ${whereClause}
+      ORDER BY d.vendor_name ASC, d.dw_sku ASC
+      LIMIT $${params.length - 1} OFFSET $${params.length}
+    `, params);
+
+    const countParams = params.slice(0, -2);
+    const countQ = await pool.query(`
+      SELECT COUNT(*) as total
+      FROM discontinued_skus d
+      LEFT JOIN shopify_products sp ON d.dw_sku = sp.dw_sku
+      WHERE 1=1 ${whereClause}
+    `, countParams);
+
+    res.json({
+      summary: summaryQ.rows[0],
+      vendors: vendorQ.rows,
+      products: productsQ.rows,
+      pagination: {
+        page: parseInt(page),
+        limit: parseInt(limit),
+        total: parseInt(countQ.rows[0].total),
+        pages: Math.ceil(parseInt(countQ.rows[0].total) / parseInt(limit))
+      }
+    });
+  } catch (err) {
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// --- Line Manager: Proxy to Brewster/York catalog (9600) ---
+function proxyTo9600(method, apiPath, body) {
+  return new Promise((resolve, reject) => {
+    const bodyStr = body ? JSON.stringify(body) : '';
+    const authHeader = 'Basic ' + Buffer.from(process.env.GEORGE_BASIC_AUTH || '').toString('base64');
+    const options = {
+      hostname: '127.0.0.1', port: 9600, path: apiPath, method,
+      headers: { 'Authorization': authHeader, 'Content-Type': 'application/json' },
+      timeout: 30000
+    };
+    if (bodyStr) options.headers['Content-Length'] = Buffer.byteLength(bodyStr);
+    const req = http.request(options, (res) => {
+      let data = '';
+      res.on('data', c => data += c);
+      res.on('end', () => {
+        try { resolve(JSON.parse(data)); }
+        catch (e) { resolve({ raw: data }); }
+      });
+    });
+    req.on('error', (e) => reject(e));
+    req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
+    if (bodyStr) req.write(bodyStr);
+    req.end();
+  });
+}
+
+app.get('/api/lm/lines', async (req, res) => {
+  try {
+    const data = await proxyTo9600('GET', '/api/lines');
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: 'Brewster catalog unavailable: ' + err.message }); }
+});
+
+app.post('/api/lm/push/start', async (req, res) => {
+  try {
+    const data = await proxyTo9600('POST', '/api/push/start', req.body);
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: err.message }); }
+});
+
+app.get('/api/lm/push/status', async (req, res) => {
+  try {
+    const data = await proxyTo9600('GET', '/api/push/status');
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: err.message }); }
+});
+
+app.post('/api/lm/push/stop', async (req, res) => {
+  try {
+    const data = await proxyTo9600('POST', '/api/push/stop');
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: err.message }); }
+});
+
+app.get('/api/lm/push/cron', async (req, res) => {
+  try {
+    const data = await proxyTo9600('GET', '/api/push/cron');
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: err.message }); }
+});
+
+app.post('/api/lm/push/cron', async (req, res) => {
+  try {
+    const data = await proxyTo9600('POST', '/api/push/cron', req.body);
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: err.message }); }
+});
+
+app.delete('/api/lm/push/cron', async (req, res) => {
+  try {
+    const data = await proxyTo9600('DELETE', '/api/push/cron');
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: err.message }); }
+});
+
+app.post('/api/lm/crawl/:brand', async (req, res) => {
+  try {
+    const data = await proxyTo9600('POST', '/api/crawl/' + encodeURIComponent(req.params.brand));
+    res.json(data);
+  } catch (err) { res.status(502).json({ success: false, error: err.message }); }
+});
+
+// ============================================================
+// TAG MANAGER ENDPOINTS
+// ============================================================
+
+const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
+
+// Comprehensive color word set from interior-design-tagger skill
+const TAG_COLOR_WORDS = new Set([
+  'Basil','Beige','Black','Blue','Blush','Bronze','Brown','Burgundy','Buff',
+  'Camel','Canary','Cerulean','Champagne','Charcoal','Cherry','Chestnut','Chocolate',
+  'Cobalt','Copper','Coral','Cream','Crimson',
+  'Ebony','Ecru','Emerald','Espresso',
+  'Fawn','Forest','Fuchsia',
+  'Gold','Golden','Graphite','Gray','Green','Grey','Gunmetal',
+  'Honey','Hunter',
+  'Indigo','Ivory',
+  'Jet',
+  'Kelly','Khaki',
+  'Lavender','Lilac','Lime',
+  'Magenta','Maroon','Mauve','Metallic','Mint','Mocha','Moss','Mustard',
+  'Navy','Nude',
+  'Ochre','Olive','Onyx','Orange','Orchid',
+  'Peach','Pearl','Pewter','Pink','Platinum','Plum','Purple',
+  'Red','Rose','Ruby','Rust',
+  'Saffron','Sage','Salmon','Sand','Sapphire','Scarlet','Seafoam','Sienna','Silver','Sky','Slate',
+  'Stone','Tan','Tangerine','Taupe','Teal','Terracotta','Thyme','Turquoise',
+  'Umber','Vermillion','Violet',
+  'White','Wine',
+  'Yellow'
+]);
+
+const TAG_JUNK_PATTERNS = [
+  /^display_variant$/i,
+  /^LIGHT\s+\w+\/\w+$/,
+  /^Color-/i,
+  /^Color\s/i,
+  /^Background Color/i,
+  /Trending.*Collection.*20[12]\d/i,
+  /^[A-Z\s]{3,}\/[A-Z\s]{3,}$/
+];
+
+const PARENT_COLOR_MAP = {
+  'Indigo':'Blue','Navy':'Blue','Cobalt':'Blue','Sky':'Blue','Teal':'Blue','Cerulean':'Blue','Aqua':'Blue','Azure':'Blue','Sapphire':'Blue',
+  'Olive':'Green','Sage':'Green','Mint':'Green','Forest':'Green','Emerald':'Green','Seafoam':'Green','Lime':'Green','Kelly':'Green','Hunter':'Green',
+  'Coral':'Pink','Salmon':'Pink','Blush':'Pink','Rose':'Pink','Fuchsia':'Pink',
+  'Rust':'Orange','Terracotta':'Orange','Sienna':'Orange','Copper':'Orange','Amber':'Orange','Tangerine':'Orange','Apricot':'Orange',
+  'Burgundy':'Red','Maroon':'Red','Wine':'Red','Crimson':'Red','Scarlet':'Red','Cherry':'Red','Ruby':'Red','Vermillion':'Red',
+  'Lavender':'Purple','Plum':'Purple','Violet':'Purple','Mauve':'Purple','Amethyst':'Purple','Lilac':'Purple','Orchid':'Purple','Magenta':'Purple',
+  'Gold':'Yellow','Mustard':'Yellow','Canary':'Yellow','Honey':'Yellow','Saffron':'Yellow',
+  'Tan':'Beige','Khaki':'Beige','Sand':'Beige','Camel':'Beige','Taupe':'Beige','Buff':'Beige','Fawn':'Beige','Ecru':'Beige',
+  'Charcoal':'Gray','Slate':'Gray','Pewter':'Gray','Ash':'Gray','Stone':'Gray','Graphite':'Gray','Gunmetal':'Gray',
+  'Chocolate':'Brown','Espresso':'Brown','Mocha':'Brown','Chestnut':'Brown','Umber':'Brown',
+  'Ivory':'White','Cream':'White','Pearl':'White','Bone':'White','Eggshell':'White',
+  'Onyx':'Black','Ebony':'Black','Jet':'Black'
+};
+
+// In-memory state for background jobs
+let tagScanState = { running: false, scanned: 0, flagged: 0, total: 0, byVendor: {} };
+let tagApplyState = { running: false, applied: 0, errors: 0, total: 0, currentVendor: '' };
+let tagBatchState = { status: 'idle', batchId: null, analyzed: 0, retrying: 0, total: 0 };
+
+// GET /api/tags/stats
+app.get('/api/tags/stats', async (req, res) => {
+  try {
+    const { rows } = await pool.query(`
+      SELECT vendor,
+        COUNT(*) as total,
+        COUNT(*) FILTER (WHERE status='pending') as pending,
+        COUNT(*) FILTER (WHERE status='retry') as retry,
+        COUNT(*) FILTER (WHERE status='analyzed') as analyzed,
+        COUNT(*) FILTER (WHERE status='applied') as applied
+      FROM tag_analysis GROUP BY vendor ORDER BY vendor
+    `);
+    res.json({ vendors: rows });
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// GET /api/tags/scan — start background scan
+app.get('/api/tags/scan', async (req, res) => {
+  if (tagScanState.running) return res.json({ message: 'Scan already running', ...tagScanState });
+  tagScanState = { running: true, scanned: 0, flagged: 0, total: 0, byVendor: {} };
+  res.json({ message: 'Scan started' });
+  logActivity('info', 'Tag Manager: Shopify scan started');
+
+  // Background scan
+  (async () => {
+    try {
+      let sinceId = 0;
+      while (true) {
+        await new Promise(r => setTimeout(r, 520));
+        const url = `https://${SHOPIFY_DOMAIN}/admin/api/2024-10/products.json?limit=250&since_id=${sinceId}&fields=id,title,vendor,tags,status,images`;
+        const resp = await fetch(url, { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } });
+        const text = await resp.text();
+        let d;
+        try { d = JSON.parse(text); } catch(e) { break; }
+        if (!d.products || d.products.length === 0) break;
+
+        const values = [];
+        for (const p of d.products) {
+          if (p.status === 'archived') continue;
+          tagScanState.scanned++;
+          const tags = (p.tags || '').split(',').map(t => t.trim()).filter(Boolean);
+          const colorTags = tags.filter(t => TAG_COLOR_WORDS.has(t));
+          const junkTags = tags.filter(t => TAG_JUNK_PATTERNS.some(pat => pat.test(t)));
+          const isFlagged = colorTags.length > 5 || tags.length <= 1 || junkTags.length > 0;
+
+          if (isFlagged) {
+            tagScanState.flagged++;
+            if (!tagScanState.byVendor[p.vendor]) tagScanState.byVendor[p.vendor] = 0;
+            tagScanState.byVendor[p.vendor]++;
+            const imageUrl = (p.images && p.images[0]) ? p.images[0].src : '';
+            values.push([String(p.id), p.vendor, p.title, imageUrl, p.tags || '']);
+          }
+        }
+
+        // Batch upsert flagged products
+        for (const v of values) {
+          await pool.query(`
+            INSERT INTO tag_analysis (shopify_product_id, vendor, title, image_url, current_tags, status)
+            VALUES ($1, $2, $3, $4, $5, 'pending')
+            ON CONFLICT (shopify_product_id) DO UPDATE SET
+              vendor=EXCLUDED.vendor, title=EXCLUDED.title, image_url=EXCLUDED.image_url,
+              current_tags=EXCLUDED.current_tags
+            WHERE tag_analysis.status NOT IN ('analyzed','applied')
+          `, v);
+        }
+
+        sinceId = d.products[d.products.length - 1].id;
+        tagScanState.total = tagScanState.scanned;
+        if (d.products.length < 250) break;
+      }
+      logActivity('info', `Tag Manager: Scan complete. ${tagScanState.flagged} flagged of ${tagScanState.scanned} scanned`);
+    } catch (err) {
+      logActivity('error', 'Tag Manager scan error: ' + err.message);
+    }
+    tagScanState.running = false;
+  })();
+});
+
+// GET /api/tags/scan-status
+app.get('/api/tags/scan-status', (req, res) => {
+  res.json(tagScanState);
+});
+
+// POST /api/tags/batch-submit — Submit Gemini batch analysis
+app.post('/api/tags/batch-submit', async (req, res) => {
+  const { vendors, limit } = req.body || {};
+  if (!vendors || !vendors.length) return res.status(400).json({ error: 'vendors required' });
+  const maxLimit = limit || 5000;
+
+  try {
+    const { rows } = await pool.query(`
+      SELECT shopify_product_id, vendor, title, image_url, current_tags
+      FROM tag_analysis
+      WHERE status IN ('pending','retry') AND vendor = ANY($1)
+      ORDER BY created_at ASC LIMIT $2
+    `, [vendors, maxLimit]);
+
+    if (rows.length === 0) return res.json({ message: 'No products to analyze', productCount: 0 });
+
+    tagBatchState = { status: 'processing', batchId: null, analyzed: 0, retrying: 0, total: rows.length };
+    res.json({ message: 'Batch processing started', productCount: rows.length });
+    logActivity('info', `Tag Manager: Gemini batch started for ${rows.length} products`);
+
+    // Process products with real-time Gemini calls (batch API alternative)
+    // Using sync calls with rate limiting since Batch API requires file upload setup
+    (async () => {
+      for (let i = 0; i < rows.length; i++) {
+        const row = rows[i];
+        try {
+          // Download image
+          let imageBase64 = null;
+          if (row.image_url) {
+            try {
+              const imgResp = await fetch(row.image_url);
+              if (imgResp.ok) {
+                const buf = Buffer.from(await imgResp.arrayBuffer());
+                imageBase64 = buf.toString('base64');
+              }
+            } catch(e) { /* skip image */ }
+          }
+
+          if (!imageBase64) {
+            await pool.query(`UPDATE tag_analysis SET status='retry', retry_count=retry_count+1 WHERE shopify_product_id=$1`, [row.shopify_product_id]);
+            tagBatchState.retrying++;
+            continue;
+          }
+
+          // Call Gemini
+          const geminiResp = await fetch(
+            `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=${GEMINI_API_KEY}`,
+            {
+              method: 'POST',
+              headers: { 'Content-Type': 'application/json' },
+              body: JSON.stringify({
+                contents: [{ parts: [
+                  { inlineData: { mimeType: 'image/jpeg', data: imageBase64 } },
+                  { text: `Analyze this wallcovering/fabric image. Return ONLY valid JSON (no markdown, no backticks):
+{
+  "backgroundColor": "single dominant color word",
+  "relevantColors": ["max 3 colors ACTUALLY visible in image"],
+  "styles": ["1-2 from: Traditional, Contemporary, Art Deco, Mid-Century Modern, Victorian, Chinoiserie, Minimalist, Bohemian, Coastal, Tropical, Scandinavian, Industrial, Farmhouse, Transitional, Mediterranean, Arts & Crafts, Art Nouveau, Maximalist"],
+  "patterns": ["1-2 from: Damask, Floral, Geometric, Botanical, Toile, Trellis, Abstract, Stripe, Solid, Textured, Paisley, Medallion, Chevron, Herringbone, Ikat, Scenic, Tropical, Vine, Leaf, Ogee, Quatrefoil, Fretwork"],
+  "material": "one of: Vinyl, Paper, Non-woven, Grasscloth, Sisal, Silk, Fabric, Linen, Velvet, Metal Leaf, Foil"
+}` }
+                ]}],
+                generationConfig: { temperature: 0.1, maxOutputTokens: 500, thinkingConfig: { thinkingBudget: 0 } }
+              })
+            }
+          );
+
+          const geminiData = await geminiResp.json();
+          let aiResult = null;
+          try {
+            const textContent = geminiData.candidates?.[0]?.content?.parts?.[0]?.text || '';
+            const cleaned = textContent.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
+            aiResult = JSON.parse(cleaned);
+          } catch(e) { /* parse failed */ }
+
+          if (!aiResult || !aiResult.backgroundColor) {
+            await pool.query(`UPDATE tag_analysis SET status='retry', retry_count=retry_count+1 WHERE shopify_product_id=$1`, [row.shopify_product_id]);
+            tagBatchState.retrying++;
+            continue;
+          }
+
+          // Build suggested tags
+          const existingTags = (row.current_tags || '').split(',').map(t => t.trim()).filter(Boolean);
+          const keepTags = existingTags.filter(t => {
+            if (TAG_COLOR_WORDS.has(t)) return false;
+            if (TAG_JUNK_PATTERNS.some(p => p.test(t))) return false;
+            if (/^Background Color/i.test(t)) return false;
+            if (/^Multi$/i.test(t)) return false;
+            return true;
+          });
+
+          // Preserve display_variant
+          const hadDisplayVariant = existingTags.some(t => t.toLowerCase() === 'display_variant');
+
+          const newTags = new Set(keepTags);
+          if (hadDisplayVariant) newTags.add('display_variant');
+
+          // Add background color
+          newTags.add('Background Color ' + aiResult.backgroundColor);
+
+          // Add AI colors with parent mapping
+          const parentFamilies = new Set();
+          const allColors = [aiResult.backgroundColor, ...(aiResult.relevantColors || [])];
+          for (const c of allColors) {
+            const cap = c.charAt(0).toUpperCase() + c.slice(1).toLowerCase();
+            newTags.add(cap);
+            const parent = PARENT_COLOR_MAP[cap];
+            if (parent) { newTags.add(parent); parentFamilies.add(parent); }
+            else { parentFamilies.add(cap); }
+          }
+          if (parentFamilies.size > 3) newTags.add('Multi');
+
+          // Add styles, patterns, material
+          for (const s of (aiResult.styles || [])) newTags.add(s);
+          for (const p of (aiResult.patterns || [])) newTags.add(p);
+          if (aiResult.material) {
+            newTags.add(aiResult.material);
+            const natMaterials = ['grasscloth','sisal','jute','raffia','hemp','seagrass','silk','linen','cotton','paper'];
+            if (!natMaterials.includes(aiResult.material.toLowerCase())) {
+              newTags.add('Class A Fire Rated');
+            }
+          }
+
+          const suggestedTags = [...newTags].join(', ');
+
+          await pool.query(`
+            UPDATE tag_analysis SET
+              ai_background_color=$2, ai_colors=$3, ai_styles=$4, ai_patterns=$5, ai_material=$6,
+              suggested_tags=$7, status='analyzed', analyzed_at=NOW()
+            WHERE shopify_product_id=$1
+          `, [
+            row.shopify_product_id,
+            aiResult.backgroundColor,
+            JSON.stringify(aiResult.relevantColors || []),
+            JSON.stringify(aiResult.styles || []),
+            JSON.stringify(aiResult.patterns || []),
+            aiResult.material || null,
+            suggestedTags
+          ]);
+          tagBatchState.analyzed++;
+
+          // Rate limit: ~1 req/s for Gemini
+          await new Promise(r => setTimeout(r, 1100));
+
+        } catch (err) {
+          await pool.query(`UPDATE tag_analysis SET status='retry', retry_count=retry_count+1 WHERE shopify_product_id=$1`, [row.shopify_product_id]).catch(() => {});
+          tagBatchState.retrying++;
+        }
+      }
+      tagBatchState.status = 'complete';
+      logActivity('info', `Tag Manager: Gemini batch complete. ${tagBatchState.analyzed} analyzed, ${tagBatchState.retrying} retrying`);
+    })();
+
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// GET /api/tags/batch-status
+app.get('/api/tags/batch-status', (req, res) => {
+  res.json(tagBatchState);
+});
+
+// GET /api/tags/preview
+app.get('/api/tags/preview', async (req, res) => {
+  const vendor = req.query.vendor;
+  const limit = parseInt(req.query.limit) || 50;
+  const offset = parseInt(req.query.offset) || 0;
+  if (!vendor) return res.status(400).json({ error: 'vendor required' });
+  try {
+    const { rows } = await pool.query(`
+      SELECT shopify_product_id as id, title, current_tags as "currentTags",
+        suggested_tags as "suggestedTags", ai_colors as "aiColors", ai_styles as "aiStyles", status
+      FROM tag_analysis WHERE vendor=$1 ORDER BY status DESC, created_at ASC LIMIT $2 OFFSET $3
+    `, [vendor, limit, offset]);
+    res.json({ products: rows });
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// POST /api/tags/apply
+app.post('/api/tags/apply', async (req, res) => {
+  const { vendor, productIds } = req.body || {};
+  if (!vendor) return res.status(400).json({ error: 'vendor required' });
+  if (tagApplyState.running) return res.json({ message: 'Apply already running', ...tagApplyState });
+
+  let whereClause = `vendor=$1 AND status='analyzed'`;
+  const params = [vendor];
+  if (productIds && productIds.length > 0) {
+    whereClause += ` AND shopify_product_id = ANY($2)`;
+    params.push(productIds.map(String));
+  }
+
+  try {
+    const { rows } = await pool.query(`SELECT shopify_product_id, suggested_tags FROM tag_analysis WHERE ${whereClause}`, params);
+    tagApplyState = { running: true, applied: 0, errors: 0, total: rows.length, currentVendor: vendor };
+    res.json({ message: 'Applying tags', total: rows.length });
+    logActivity('info', `Tag Manager: Applying tags to ${rows.length} ${vendor} products`);
+
+    (async () => {
+      for (const row of rows) {
+        try {
+          await new Promise(r => setTimeout(r, 520));
+          const url = `https://${SHOPIFY_DOMAIN}/admin/api/2024-10/products/${row.shopify_product_id}.json`;
+          const resp = await fetch(url, {
+            method: 'PUT',
+            headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' },
+            body: JSON.stringify({ product: { id: parseInt(row.shopify_product_id), tags: row.suggested_tags } })
+          });
+          if (resp.status === 429) {
+            const ra = parseFloat(resp.headers.get('Retry-After') || '2');
+            await new Promise(r => setTimeout(r, ra * 1000));
+            // Retry once
+            await fetch(url, {
+              method: 'PUT',
+              headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' },
+              body: JSON.stringify({ product: { id: parseInt(row.shopify_product_id), tags: row.suggested_tags } })
+            });
+          }
+          await pool.query(`UPDATE tag_analysis SET status='applied', applied_at=NOW() WHERE shopify_product_id=$1`, [row.shopify_product_id]);
+          tagApplyState.applied++;
+        } catch (err) {
+          tagApplyState.errors++;
+        }
+      }
+      tagApplyState.running = false;
+      logActivity('info', `Tag Manager: Apply complete for ${vendor}. ${tagApplyState.applied} applied, ${tagApplyState.errors} errors`);
+    })();
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// GET /api/tags/apply-status
+app.get('/api/tags/apply-status', (req, res) => {
+  res.json(tagApplyState);
+});
+
+// =============================================================
+// PRODUCT STANDARDIZATION ENGINE API
+// =============================================================
+
+// GET /api/standardize/status — engine state
+app.get('/api/standardize/status', (req, res) => {
+  res.json(stdWorker.getEngineState());
+});
+
+// GET /api/standardize/progress — per-vendor progress
+app.get('/api/standardize/progress', async (req, res) => {
+  try {
+    const data = await stdWorker.getVendorProgress();
+    res.json(data);
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// POST /api/standardize/start — start engine for a vendor
+app.post('/api/standardize/start', async (req, res) => {
+  const { vendor, analyzeOnly, limit } = req.body || {};
+  if (!vendor) return res.status(400).json({ error: 'vendor required' });
+  try {
+    const result = await stdWorker.runVendorStandardization(vendor, {
+      analyzeOnly: analyzeOnly || false,
+      limit: limit || 99999
+    });
+    res.json(result);
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// POST /api/post-push-pipeline — Full post-push enrichment pipeline for a vendor
+// Runs: 1) AI enrichment (Gemini) → 2) Metafield push → 3) Body HTML rewrite → 4) Interior designer tags → 5) Architectural (if private label)
+app.post('/api/post-push-pipeline', async (req, res) => {
+  const { vendor_code, limit } = req.body || {};
+  if (!vendor_code) return res.status(400).json({ error: 'vendor_code required' });
+
+  const vc = vendor_code.toUpperCase();
+  const lim = limit || 99999;
+
+  // Check if vendor is private label (for architectural skill)
+  let isPrivateLabel = false;
+  try {
+    const { rows } = await pool.query('SELECT is_private_label FROM vendor_registry WHERE vendor_code = $1', [vc]);
+    isPrivateLabel = rows[0]?.is_private_label || false;
+  } catch (e) { /* ignore */ }
+
+  const { execFile } = require('child_process');
+  const scriptsDir = '/root/DW-Agents/scripts';
+
+  const runScript = (cmd, args) => new Promise((resolve) => {
+    execFile(cmd, args, { timeout: 600000, cwd: scriptsDir }, (err, stdout, stderr) => {
+      resolve({ success: !err, output: (stdout || '').slice(-500), error: err?.message || '' });
+    });
+  });
+
+  // Return immediately, run pipeline in background
+  res.json({ status: 'started', vendor_code: vc, is_private_label: isPrivateLabel, steps: [
+    'ai-enrichment', 'metafield-push', 'body-html-rewrite', 'interior-design-tags',
+    ...(isPrivateLabel ? ['architectural-update'] : [])
+  ]});
+
+  console.log(`[VCC] Post-push pipeline started for ${vc} (private_label=${isPrivateLabel})`);
+
+  // Step 1: AI Enrichment (Gemini vision for colors, description)
+  console.log(`[VCC] [${vc}] Step 1/5: AI Enrichment...`);
+  await runScript('python3', [`${scriptsDir}/enrich-ai-fields.py`, '--vendor', vc, '--limit', String(lim)]);
+
+  // Step 2: Push all metafields to Shopify
+  console.log(`[VCC] [${vc}] Step 2/5: Metafield Push...`);
+  await runScript('python3', [`${scriptsDir}/push-metafields-vendor.py`, 'vendor_catalog', '--vendor', vc, '--force']);
+
+  // Step 3: Rewrite body_html (description only, mfr desc priority, no specs)
+  console.log(`[VCC] [${vc}] Step 3/5: Body HTML Rewrite...`);
+  await runScript('python3', [`${scriptsDir}/rewrite-body-html.py`, '--vendor', vc]);
+
+  // Step 4: Interior Designer Tags (ALWAYS runs — additive)
+  console.log(`[VCC] [${vc}] Step 4/5: Interior Designer Tags...`);
+  await runScript('python3', [`${scriptsDir}/tag-interior-design.py`, '--vendor', vc]);
+
+  // Step 5: Private Label cleanup (only if private label)
+  if (isPrivateLabel) {
+    console.log(`[VCC] [${vc}] Step 5/5: Private Label Fix (Caribbean names, our colors, metafields)...`);
+    await runScript('python3', [`${scriptsDir}/fix-pr-private-label.py`]);
+  }
+
+  console.log(`[VCC] Post-push pipeline COMPLETE for ${vc}`);
+});
+
+// POST /api/sync-metafield-namespaces — Copy global.*/dwc.* → custom.*/specs.* for a vendor's products
+app.post('/api/sync-metafield-namespaces', async (req, res) => {
+  const { vendor, limit } = req.body || {};
+  if (!vendor) return res.status(400).json({ error: 'vendor required (Shopify vendor name)' });
+  const lim = limit || 250;
+
+  res.json({ status: 'started', vendor, limit: lim });
+
+  const { syncNamespaces } = require('/root/DW-Agents/shared/metafield-map.js');
+
+  (async () => {
+    let synced = 0, skipped = 0, errors = 0;
+    let sinceId = 0;
+
+    while (true) {
+      try {
+        const pResp = await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-01/products.json?vendor=${encodeURIComponent(vendor)}&status=active&limit=250&fields=id&since_id=${sinceId}`, {
+          headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN }
+        });
+        if (pResp.status === 429) { await new Promise(r => setTimeout(r, 4000)); continue; }
+        const pData = await pResp.json();
+        if (!pData.products?.length) break;
+
+        for (const p of pData.products) {
+          if (synced + skipped >= lim) break;
+          try {
+            const mfResp = await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-01/products/${p.id}/metafields.json?limit=250`, {
+              headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN }
+            });
+            if (mfResp.status === 429) { await new Promise(r => setTimeout(r, 4000)); continue; }
+            const { metafields = [] } = await mfResp.json();
+            const newMfs = syncNamespaces(metafields);
+
+            if (newMfs.length > 0) {
+              const putResp = await fetch(`https://${SHOPIFY_DOMAIN}/admin/api/2024-01/products/${p.id}.json`, {
+                method: 'PUT',
+                headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN, 'Content-Type': 'application/json' },
+                body: JSON.stringify({ product: { id: p.id, metafields: newMfs } })
+              });
+              if (putResp.status === 429) { await new Promise(r => setTimeout(r, 4000)); }
+              synced++;
+            } else {
+              skipped++;
+            }
+          } catch { errors++; }
+          await new Promise(r => setTimeout(r, 600));
+        }
+
+        sinceId = pData.products[pData.products.length - 1].id;
+        if (synced + skipped >= lim) break;
+        await new Promise(r => setTimeout(r, 500));
+      } catch { break; }
+    }
+
+    console.log(`[VCC] Namespace sync DONE for ${vendor}: ${synced} synced, ${skipped} already set, ${errors} errors`);
+  })();
+});
+
+// POST /api/update-metafields — Push specs from DB to Shopify metafields for a vendor
+// Uses shared metafield mapping: custom.*, specs.*, global.*, dwc.*
+app.post('/api/update-metafields', async (req, res) => {
+  const { vendor_code, catalog_table, limit, force } = req.body || {};
+  if (!vendor_code && !catalog_table) return res.status(400).json({ error: 'vendor_code or catalog_table required' });
+
+  const table = catalog_table || (vendor_code ? vendor_code.toLowerCase() + '_catalog' : null);
+  const vc = (vendor_code || table.replace('_catalog', '')).toUpperCase();
+  const lim = limit || 99999;
+
+  res.json({ status: 'started', vendor: vc, table, limit: lim, force: !!force });
+
+  const { execFile } = require('child_process');
+  console.log(`[VCC] Metafield push started for ${vc} (table=${table}, limit=${lim}, force=${!!force})`);
+
+  const args = ['push-metafields-vendor.py', table];
+  if (vendor_code) args.push('--vendor', vc);
+  if (force) args.push('--force');
+  if (limit) args.push('--limit', String(lim));
+
+  execFile('python3', args, { timeout: 3600000, cwd: '/root/DW-Agents/scripts' }, (err, stdout, stderr) => {
+    if (err) console.log(`[VCC] Metafield push ERROR for ${vc}:`, err.message);
+    else console.log(`[VCC] Metafield push COMPLETE for ${vc}:`, stdout.slice(-300));
+  });
+});
+
+// POST /api/standardize/stop — stop engine
+app.post('/api/standardize/stop', (req, res) => {
+  res.json(stdWorker.stopEngine());
+});
+
+// POST /api/standardize/bulk — run bulk analysis on all pre-loaded pending products (skip Shopify scan)
+app.post('/api/standardize/bulk', async (req, res) => {
+  const { batchSize, apply } = req.body || {};
+  try {
+    const result = await stdWorker.runBulkAnalysis({
+      batchSize: batchSize || 500,
+      apply: apply !== false
+    });
+    res.json(result);
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// GET /api/standardize/vendors — get Shopify vendor list with product counts
+app.get('/api/standardize/vendors', async (req, res) => {
+  try {
+    const { rows } = await pool.query(`
+      SELECT vr.vendor_name, vr.vendor_code, vr.shopify_count, vr.is_private_label,
+        vr.vendor_discount_pct, vr.discount_confirmed,
+        COALESCE(ps.total, 0) as std_total,
+        COALESCE(ps.analyzed, 0) as std_analyzed,
+        COALESCE(ps.applied, 0) as std_applied,
+        COALESCE(ps.errors, 0) as std_errors,
+        ps.last_analyzed, ps.last_applied
+      FROM vendor_registry vr
+      LEFT JOIN (
+        SELECT vendor,
+          COUNT(*) as total,
+          COUNT(*) FILTER (WHERE status='analyzed') as analyzed,
+          COUNT(*) FILTER (WHERE status='applied') as applied,
+          COUNT(*) FILTER (WHERE status='error') as errors,
+          MAX(analyzed_at) as last_analyzed,
+          MAX(applied_at) as last_applied
+        FROM product_standardization
+        GROUP BY vendor
+      ) ps ON vr.vendor_name = ps.vendor
+      WHERE vr.shopify_count > 0
+      ORDER BY vr.shopify_count DESC
+    `);
+    res.json({ vendors: rows });
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// POST /api/vendor/discount — save confirmed vendor discount and cascade to
+// every product_pricing row for that vendor so the generated cost flips.
+app.post('/api/vendor/discount', express.json(), async (req, res) => {
+  try {
+    const { vendor_code, discount_pct, confirmed } = req.body || {};
+    if (!vendor_code) return res.status(400).json({ error: 'vendor_code required' });
+    const pct = (discount_pct === null || discount_pct === '' || discount_pct === undefined) ? null : Number(discount_pct);
+    await pool.query(
+      `UPDATE vendor_registry
+       SET vendor_discount_pct   = $2,
+           discount_confirmed    = $3,
+           discount_confirmed_at = CASE WHEN $3 THEN NOW() ELSE discount_confirmed_at END
+       WHERE vendor_code = $1`,
+      [vendor_code, pct, !!confirmed]);
+    await pool.query(
+      `UPDATE product_pricing SET discount_pct = $2, updated_at = NOW() WHERE vendor_code = $1`,
+      [vendor_code, pct]);
+    const r = await pool.query(
+      `SELECT vendor_code, vendor_discount_pct, discount_confirmed FROM vendor_registry WHERE vendor_code = $1`,
+      [vendor_code]);
+    res.json({ ok: true, vendor: r.rows[0] });
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// GET /api/standardize/preview/:vendor — preview analyzed products
+app.get('/api/standardize/preview/:vendor', async (req, res) => {
+  try {
+    const { rows } = await pool.query(`
+      SELECT shopify_product_id as id, title, title_standardized, tags_standardized,
+        ai_colors, ai_hex_codes, ai_styles, ai_patterns, ai_material, ai_is_mural,
+        ai_audience, ai_description, ai_room_type, status, analyzed_at, applied_at
+      FROM product_standardization
+      WHERE vendor=$1 ORDER BY analyzed_at DESC NULLS LAST LIMIT 50
+    `, [req.params.vendor]);
+    res.json({ products: rows });
+  } catch (err) { res.status(500).json({ error: err.message }); }
+});
+
+// --- Product Actions API ---
+const PA_PRODUCT_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const PA_CONTENT_TOKEN = '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';
+const PA_SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const PA_GQL = `https://${PA_SHOP}/admin/api/2024-07/graphql.json`;
+
+app.get('/api/product-actions/search', async (req, res) => {
+  try {
+    const q = req.query.q || '';
+    const cursor = req.query.cursor || null;
+    const after = cursor ? `, after: "${cursor}"` : '';
+
+    const gqlQuery = `{ products(first: 50, query: "${q.replace(/"/g, '\\"')}"${after}) { edges { cursor node { id title handle status vendor productType featuredImage { url } variants(first:1) { edges { node { sku } } } updatedAt } } pageInfo { hasNextPage } } }`;
+
+    const resp = await fetch(PA_GQL, {
+      method: 'POST',
+      headers: { 'X-Shopify-Access-Token': PA_PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+      body: JSON.stringify({ query: gqlQuery })
+    });
+    const data = await resp.json();
+    const edges = data?.data?.products?.edges || [];
+    const hasMore = data?.data?.products?.pageInfo?.hasNextPage || false;
+    const lastCursor = edges.length ? edges[edges.length - 1].cursor : null;
+
+    const products = edges.map(e => {
+      const n = e.node;
+      return {
+        id: n.id.split('/').pop(),
+        title: n.title,
+        handle: n.handle,
+        status: n.status,
+        vendor: n.vendor || '',
+        type: n.productType || '',
+        sku: n.variants?.edges?.[0]?.node?.sku || '',
+        image: n.featuredImage?.url ? n.featuredImage.url.replace(/\?.*/, '?width=80') : '',
+        updated: n.updatedAt ? new Date(n.updatedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : ''
+      };
+    });
+
+    res.json({ products, cursor: lastCursor, hasMore });
+  } catch (err) {
+    res.status(500).json({ error: err.message });
+  }
+});
+
+app.post('/api/product-actions/bulk-status', async (req, res) => {
+  try {
+    const { ids, status } = req.body;
+    if (!ids || !status) return res.status(400).json({ error: 'ids and status required' });
+
+    let success = 0, errors = 0;
+    for (const id of ids) {
+      try {
+        const resp = await fetch(`https://${PA_SHOP}/admin/api/2024-07/products/${id}.json`, {
+          method: 'PUT',
+          headers: { 'X-Shopify-Access-Token': PA_PRODUCT_TOKEN, 'Content-Type': 'application/json' },
+          body: JSON.stringify({ product: { id: parseInt(id), status } })
+        });
+        if (resp.ok) success++;
+        else errors++;
+        await new Promise(r => setTimeout(r, 300));
+      } catch (e) {
+        errors++;
+      }
+    }
+    res.json({ success, errors, total: ids.length });
+  } catch (err) {
+    res.status(500).json({ error: err.message });
+  }
+});
+
+app.post('/api/product-actions/bulk-redirect', async (req, res) => {
+  try {
+    const { handles, target } = req.body;
+    if (!handles || !target) return res.status(400).json({ error: 'handles and target required' });
+
+    let created = 0, skipped = 0, errors = 0;
+    for (const handle of handles) {
+      try {
+        const resp = await fetch(`https://${PA_SHOP}/admin/api/2024-07/redirects.json`, {
+          method: 'POST',
+          headers: { 'X-Shopify-Access-Token': PA_CONTENT_TOKEN, 'Content-Type': 'application/json' },
+          body: JSON.stringify({ redirect: { path: `/products/${handle}`, target } })
+        });
+        if (resp.ok) created++;
+        else if (resp.status === 422) skipped++;
+        else errors++;
+        await new Promise(r => setTimeout(r, 250));
+      } catch (e) {
+        errors++;
+      }
+    }
+    res.json({ created, skipped, errors, total: handles.length });
+  } catch (err) {
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// --- New Launches API ---
+app.get('/api/new-launches', async (req, res) => {
+  try {
+    const { days = '7' } = req.query;
+    const daysNum = parseInt(days) || 7;
+    const since = new Date(Date.now() - daysNum * 86400000).toISOString();
+
+    const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+    const TOKEN = process.env.SHOPIFY_ORDERS_TOKEN;
+
+    const products = [];
+    let cursor = null;
+
+    for (let page = 0; page < 20; page++) {
+      await new Promise(r => setTimeout(r, 350));
+      const after = cursor ? `, after: "${cursor}"` : '';
+      const q = `{
+        products(first: 50, query: "created_at:>'${since}' status:active"${after}, sortKey: CREATED_AT, reverse: true) {
+          pageInfo { hasNextPage endCursor }
+          edges { node {
+            id title vendor productType status createdAt
+            variants(first: 1) { edges { node { sku price } } }
+            images(first: 1) { edges { node { src } } }
+          }}
+        }
+      }`;
+
+      const r = await fetch(`https://${SHOP}/admin/api/2024-10/graphql.json`, {
+        method: 'POST',
+        headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+        body: JSON.stringify({ query: q })
+      });
+      const d = await r.json();
+      if (!d.data) break;
+
+      for (const e of d.data.products.edges) {
+        const n = e.node;
+        products.push({
+          id: n.id.split('/').pop(),
+          title: n.title,
+          vendor: n.vendor,
+          type: n.productType,
+          sku: n.variants?.edges?.[0]?.node?.sku || '',
+          price: n.variants?.edges?.[0]?.node?.price || '',
+          image: n.images?.edges?.[0]?.node?.src || '',
+          created: n.createdAt
+        });
+      }
+      if (!d.data.products.pageInfo.hasNextPage) break;
+      cursor = d.data.products.pageInfo.endCursor;
+    }
+
+    // Group by date
+    const grouped = {};
+    for (const p of products) {
+      const dateKey = new Date(p.created).toLocaleDateString('en-US', { timeZone: 'America/Los_Angeles', year: 'numeric', month: 'short', day: 'numeric' });
+      if (!grouped[dateKey]) grouped[dateKey] = [];
+      grouped[dateKey].push(p);
+    }
+
+    res.json({ success: true, total: products.length, days: daysNum, grouped, products });
+  } catch (err) {
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// --- Catalog Column Cache (refreshed every 5 minutes) ---
+const catalogColumnCache = { data: null, ts: 0, TTL: 300000 };
+async function getCatalogColumnMap() {
+  const now = Date.now();
+  if (catalogColumnCache.data && (now - catalogColumnCache.ts) < catalogColumnCache.TTL) {
+    return catalogColumnCache.data;
+  }
+  // Get all vendor catalog tables
+  const vendorRes = await pool.query(
+    "SELECT vendor_code, vendor_name, catalog_table FROM vendor_registry WHERE catalog_table IS NOT NULL AND catalog_table != '' AND catalog_table != 'vendor_catalog' ORDER BY vendor_name"
+  );
+  // Batch query all columns for all catalog tables at once
+  const tableNames = vendorRes.rows.map(v => v.catalog_table).filter(t => /^[a-z0-9_]+$/i.test(t));
+  const colRes = await pool.query(
+    "SELECT table_name, column_name FROM information_schema.columns WHERE table_name = ANY($1)",
+    [tableNames]
+  );
+  // Build map: tableName -> Set of columns
+  const colMap = {};
+  for (const row of colRes.rows) {
+    if (!colMap[row.table_name]) colMap[row.table_name] = new Set();
+    colMap[row.table_name].add(row.column_name);
+  }
+  const result = { vendors: vendorRes.rows, colMap };
+  catalogColumnCache.data = result;
+  catalogColumnCache.ts = now;
+  return result;
+}
+
+// --- API: Global Catalog Search (searches across ALL vendor catalog tables) ---
+app.get('/api/catalog-search', async (req, res) => {
+  try {
+    const q = (req.query.q || '').trim();
+    const page = Math.max(1, parseInt(req.query.page) || 1);
+    const limit = Math.min(100, Math.max(10, parseInt(req.query.limit) || 50));
+    const offset = (page - 1) * limit;
+
+    if (!q) {
+      return res.json({ success: true, data: [], total: 0, page, limit, pages: 0 });
+    }
+
+    const { vendors: vendorRows, colMap } = await getCatalogColumnMap();
+
+    const searchCols = ['pattern_name', 'color_name', 'mfr_sku', 'collection', 'material', 'product_type', 'dw_sku'];
+    const selectCols = ['pattern_name', 'color_name', 'mfr_sku', 'collection', 'image_url', 'width', 'shopify_product_id', 'product_type', 'dw_sku', 'product_url', 'repeat_v', 'repeat_h', 'material', 'match_type', 'all_images', 'spec_sheet', 'about_vendor'];
+
+    const searchPattern = '%' + q.replace(/[%_\\]/g, '\\$&') + '%';
+    const unionParts = [];
+
+    for (const v of vendorRows) {
+      const tbl = v.catalog_table;
+      if (!/^[a-z0-9_]+$/i.test(tbl)) continue;
+      const existingCols = colMap[tbl];
+      if (!existingCols) continue;
+
+      const selParts = selectCols.map(col => existingCols.has(col) ? `CAST(${col} AS TEXT) as ${col}` : `NULL::text as ${col}`);
+      const whereParts = searchCols
+        .filter(col => existingCols.has(col))
+        .map(col => `CAST(${col} AS TEXT) ILIKE $1`);
+
+      if (whereParts.length === 0) continue;
+
+      unionParts.push(
+        `SELECT ${selParts.join(', ')}, '${v.vendor_code.replace(/'/g, "''")}' as vendor_code, '${v.vendor_name.replace(/'/g, "''")}' as vendor_name FROM ${tbl} WHERE (${whereParts.join(' OR ')})`
+      );
+    }
+
+    if (unionParts.length === 0) {
+      return res.json({ success: true, data: [], total: 0, page, limit, pages: 0 });
+    }
+
+    // Count total matches first
+    const countSQL = `SELECT COUNT(*) as total FROM (${unionParts.join(' UNION ALL ')}) AS search_results`;
+    const countRes = await pool.query(countSQL, [searchPattern]);
+    const total = parseInt(countRes.rows[0].total);
+
+    // Fetch paginated results
+    const dataSQL = `SELECT * FROM (${unionParts.join(' UNION ALL ')}) AS search_results ORDER BY vendor_name, pattern_name LIMIT ${limit} OFFSET ${offset}`;
+    const dataRes = await pool.query(dataSQL, [searchPattern]);
+
+    res.json({
+      success: true,
+      data: dataRes.rows,
+      total,
+      page,
+      limit,
+      pages: Math.ceil(total / limit),
+      query: q
+    });
+  } catch (err) {
+    console.error('[catalog-search] Error:', err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- API: View All Catalog Items (paginated across ALL vendor catalog tables) ---
+app.get('/api/catalog-all', async (req, res) => {
+  try {
+    const page = Math.max(1, parseInt(req.query.page) || 1);
+    const limit = Math.min(100, Math.max(10, parseInt(req.query.limit) || 50));
+    const offset = (page - 1) * limit;
+    const vendorFilter = (req.query.vendor || '').trim();
+
+    const { vendors: vendorRows, colMap } = await getCatalogColumnMap();
+
+    const selectCols = ['pattern_name', 'color_name', 'mfr_sku', 'collection', 'image_url', 'width', 'shopify_product_id', 'product_type', 'dw_sku', 'product_url', 'repeat_v', 'repeat_h', 'material', 'match_type', 'all_images', 'spec_sheet', 'about_vendor'];
+    const unionParts = [];
+
+    for (const v of vendorRows) {
+      if (vendorFilter && v.vendor_code !== vendorFilter) continue;
+      const tbl = v.catalog_table;
+      if (!/^[a-z0-9_]+$/i.test(tbl)) continue;
+      const existingCols = colMap[tbl];
+      if (!existingCols) continue;
+
+      const selParts = selectCols.map(col => existingCols.has(col) ? `CAST(${col} AS TEXT) as ${col}` : `NULL::text as ${col}`);
+
+      unionParts.push(
+        `SELECT ${selParts.join(', ')}, '${v.vendor_code.replace(/'/g, "''")}' as vendor_code, '${v.vendor_name.replace(/'/g, "''")}' as vendor_name FROM ${tbl}`
+      );
+    }
+
+    if (unionParts.length === 0) {
+      return res.json({ success: true, data: [], total: 0, page, limit, pages: 0 });
+    }
+
+    // Use pg_stat for fast approximate total count when no vendor filter
+    let total;
+    if (!vendorFilter) {
+      const approxRes = await pool.query(`
+        SELECT SUM(n_live_tup) as total FROM pg_stat_user_tables
+        WHERE relname IN (${vendorRows.map((_, i) => `$${i+1}`).join(',')})
+      `, vendorRows.map(v => v.catalog_table));
+      total = parseInt(approxRes.rows[0].total) || 0;
+    } else {
+      const countSQL = `SELECT COUNT(*) as total FROM (${unionParts.join(' UNION ALL ')}) AS all_items`;
+      const countRes = await pool.query(countSQL);
+      total = parseInt(countRes.rows[0].total);
+    }
+
+    // Fetch paginated results
+    const dataSQL = `SELECT * FROM (${unionParts.join(' UNION ALL ')}) AS all_items ORDER BY vendor_name, pattern_name LIMIT ${limit} OFFSET ${offset}`;
+    const dataRes = await pool.query(dataSQL);
+
+    res.json({
+      success: true,
+      data: dataRes.rows,
+      total,
+      page,
+      limit,
+      pages: Math.ceil(total / limit),
+      vendor: vendorFilter || 'all'
+    });
+  } catch (err) {
+    console.error('[catalog-all] Error:', err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- Sample Variant Health Check ---
+// Shopify Product API token (read/write products scope)
+const SHOPIFY_PRODUCT_TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
+const SHOPIFY_GRAPHQL_URL = 'https://' + SHOPIFY_DOMAIN + '/admin/api/2024-01/graphql.json';
+
+// In-memory cache for sample variant audit results (1 hour TTL)
+let sampleVariantCache = null;
+let sampleVariantCacheTime = 0;
+const SAMPLE_CACHE_TTL = 60 * 60 * 1000; // 1 hour
+
+// Excluded vendors for push recommendations
+const EXCLUDED_VENDOR_NOTES = [
+  { name: 'Cowtan & Tout', code: 'cowtan_tout', reason: 'Images restricted - specs only' },
+  { name: 'Scalamandre (Fabrics)', code: 'scalamandre', reason: 'Fabric products excluded from push' },
+  { name: 'Bespoke', code: 'bespoke', reason: 'Custom/bespoke products - excluded from standard push' }
+];
+
+/**
+ * Fetch all active products with their variants from Shopify via GraphQL (paginated).
+ * Returns array of { id, title, vendor, variants: [{ id, title, price }] }
+ */
+async function fetchAllShopifyProductsWithVariants(progressCb) {
+  const products = [];
+  let hasNextPage = true;
+  let cursor = null;
+  let pageNum = 0;
+
+  while (hasNextPage) {
+    pageNum++;
+    const afterClause = cursor ? `, after: "${cursor}"` : '';
+    const query = `{
+      products(first: 250, query: "status:active"${afterClause}) {
+        edges {
+          cursor
+          node {
+            id
+            title
+            vendor
+            variants(first: 100) {
+              edges {
+                node {
+                  id
+                  title
+                  price
+                }
+              }
+            }
+          }
+        }
+        pageInfo {
+          hasNextPage
+        }
+      }
+    }`;
+
+    const response = await fetch(SHOPIFY_GRAPHQL_URL, {
+      method: 'POST',
+      headers: {
+        'X-Shopify-Access-Token': SHOPIFY_PRODUCT_TOKEN,
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({ query })
+    });
+
+    if (!response.ok) {
+      const errText = await response.text();
+      throw new Error(`Shopify GraphQL error (${response.status}): ${errText}`);
+    }
+
+    const data = await response.json();
+
+    if (data.errors) {
+      throw new Error('Shopify GraphQL errors: ' + JSON.stringify(data.errors));
+    }
+
+    const edges = data.data.products.edges;
+    for (const edge of edges) {
+      const node = edge.node;
+      products.push({
+        id: node.id.replace('gid://shopify/Product/', ''),
+        gid: node.id,
+        title: node.title,
+        vendor: node.vendor,
+        variants: node.variants.edges.map(ve => ({
+          id: ve.node.id.replace('gid://shopify/ProductVariant/', ''),
+          gid: ve.node.id,
+          title: ve.node.title,
+          price: parseFloat(ve.node.price)
+        }))
+      });
+      cursor = edge.cursor;
+    }
+
+    hasNextPage = data.data.products.pageInfo.hasNextPage;
+
+    if (progressCb) progressCb(pageNum, products.length, hasNextPage);
+
+    // Respect rate limits: Shopify GraphQL has 1000 point bucket, this query costs ~10-15 points
+    // Small delay to avoid throttling
+    if (hasNextPage) {
+      await new Promise(r => setTimeout(r, 200));
+    }
+  }
+
+  return products;
+}
+
+/**
+ * Build a vendor lookup map: vendor_name (lowercase) -> { vendor_code, sample_price, skip_shopify }
+ */
+async function getVendorSamplePriceMap() {
+  const { rows } = await pool.query(
+    'SELECT vendor_code, vendor_name, sample_price, skip_shopify FROM vendor_registry'
+  );
+  const map = {};
+  for (const r of rows) {
+    map[r.vendor_name.toLowerCase()] = {
+      vendor_code: r.vendor_code,
+      sample_price: parseFloat(r.sample_price) || 4.25,
+      skip_shopify: r.skip_shopify || false
+    };
+  }
+  return map;
+}
+
+// GET /api/audit/sample-variants — Check all active products for Sample variant
+app.get('/api/audit/sample-variants', async (req, res) => {
+  try {
+    const forceRefresh = req.query.refresh === 'true';
+    const now = Date.now();
+
+    // Return cached results if fresh
+    if (!forceRefresh && sampleVariantCache && (now - sampleVariantCacheTime) < SAMPLE_CACHE_TTL) {
+      return res.json({
+        success: true,
+        cached: true,
+        cached_at: new Date(sampleVariantCacheTime).toISOString(),
+        ...sampleVariantCache
+      });
+    }
+
+    logActivity('api', 'Sample variant audit started');
+    const startTime = Date.now();
+
+    // Get vendor price map
+    const vendorMap = await getVendorSamplePriceMap();
+
+    // Fetch all products with variants from Shopify
+    let lastLog = Date.now();
+    const products = await fetchAllShopifyProductsWithVariants((page, count, more) => {
+      if (Date.now() - lastLog > 5000) {
+        console.log(`[Victor] Sample audit: page ${page}, ${count} products fetched...`);
+        lastLog = Date.now();
+      }
+    });
+
+    // Analyze each product
+    let total = 0;
+    let hasSample = 0;
+    let missingSample = 0;
+    let wrongPrice = 0;
+    const byVendor = {};
+    const missingList = [];
+    const wrongPriceList = [];
+
+    for (const product of products) {
+      total++;
+      const vendorLower = (product.vendor || '').toLowerCase();
+      const vendorInfo = vendorMap[vendorLower] || { vendor_code: 'unknown', sample_price: 4.25, skip_shopify: false };
+      const expectedPrice = vendorInfo.sample_price;
+      const vendorCode = vendorInfo.vendor_code;
+
+      // Initialize vendor bucket
+      if (!byVendor[vendorCode]) {
+        byVendor[vendorCode] = {
+          vendor_name: product.vendor,
+          total: 0,
+          has_sample: 0,
+          missing_sample: 0,
+          wrong_price: 0,
+          skip_shopify: vendorInfo.skip_shopify,
+          expected_sample_price: expectedPrice
+        };
+      }
+      byVendor[vendorCode].total++;
+
+      // Check for Sample variant
+      const sampleVariant = product.variants.find(v =>
+        v.title && v.title.toLowerCase().includes('sample')
+      );
+
+      if (sampleVariant) {
+        if (Math.abs(sampleVariant.price - expectedPrice) > 0.01) {
+          wrongPrice++;
+          byVendor[vendorCode].wrong_price++;
+          wrongPriceList.push({
+            product_id: product.id,
+            title: product.title,
+            vendor: product.vendor,
+            vendor_code: vendorCode,
+            variant_id: sampleVariant.id,
+            current_price: sampleVariant.price,
+            expected_price: expectedPrice
+          });
+        } else {
+          hasSample++;
+          byVendor[vendorCode].has_sample++;
+        }
+      } else {
+        missingSample++;
+        byVendor[vendorCode].missing_sample++;
+        if (missingList.length < 500) { // Cap the list to avoid huge responses
+          missingList.push({
+            product_id: product.id,
+            title: product.title,
+            vendor: product.vendor,
+            vendor_code: vendorCode
+          });
+        }
+      }
+    }
+
+    const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+
+    const result = {
+      total,
+      has_sample: hasSample,
+      missing_sample: missingSample,
+      wrong_price: wrongPrice,
+      pct_compliant: total > 0 ? ((hasSample / total) * 100).toFixed(1) + '%' : '0%',
+      by_vendor: byVendor,
+      excluded_vendors: EXCLUDED_VENDOR_NOTES,
+      sample_missing_list: missingList,
+      sample_wrong_price_list: wrongPriceList.slice(0, 200),
+      scan_duration_sec: parseFloat(elapsed),
+      scanned_at: new Date().toISOString()
+    };
+
+    // Cache results
+    sampleVariantCache = result;
+    sampleVariantCacheTime = Date.now();
+
+    logActivity('api', `Sample variant audit complete: ${total} products, ${missingSample} missing, ${wrongPrice} wrong price (${elapsed}s)`);
+
+    res.json({
+      success: true,
+      cached: false,
+      ...result
+    });
+  } catch (err) {
+    console.error('[Victor] Sample variant audit error:', err.message);
+    logActivity('error', 'Sample variant audit failed: ' + err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// POST /api/fix/sample-variants — Fix missing or wrong-priced Sample variants
+app.post('/api/fix/sample-variants', async (req, res) => {
+  try {
+    const { vendor_code, dry_run = true } = req.body;
+
+    if (!vendor_code) {
+      return res.status(400).json({ success: false, error: 'vendor_code is required' });
+    }
+
+    // Get vendor info
+    const { rows: vendorRows } = await pool.query(
+      'SELECT vendor_code, vendor_name, sample_price, skip_shopify FROM vendor_registry WHERE vendor_code = $1',
+      [vendor_code]
+    );
+
+    if (!vendorRows.length) {
+      return res.status(404).json({ success: false, error: 'Vendor not found: ' + vendor_code });
+    }
+
+    const vendor = vendorRows[0];
+    if (vendor.skip_shopify) {
+      return res.status(400).json({
+        success: false,
+        error: `Vendor ${vendor.vendor_name} has skip_shopify=true. Cannot fix sample variants for excluded vendors.`
+      });
+    }
+
+    const expectedPrice = parseFloat(vendor.sample_price) || 4.25;
+
+    logActivity('api', `Sample variant fix started for ${vendor.vendor_name} (dry_run=${dry_run})`);
+
+    // If we have cached audit data AND the cached lists are complete for this vendor, use cache;
+    // otherwise run a targeted live scan via GraphQL.
+    let productsToFix = [];
+    let useLiveScan = true;
+
+    if (sampleVariantCache && sampleVariantCache.by_vendor[vendor_code]) {
+      const vendorStats = sampleVariantCache.by_vendor[vendor_code];
+      const cachedMissing = sampleVariantCache.sample_missing_list
+        .filter(p => p.vendor_code === vendor_code);
+      const cachedWrongPrice = sampleVariantCache.sample_wrong_price_list
+        .filter(p => p.vendor_code === vendor_code);
+
+      // Only use cache if the cached lists contain ALL items for this vendor
+      // (i.e., counts match what the by_vendor stats say)
+      const expectedMissing = vendorStats.missing_sample || 0;
+      const expectedWrong = vendorStats.wrong_price || 0;
+      if (cachedMissing.length >= expectedMissing && cachedWrongPrice.length >= expectedWrong) {
+        useLiveScan = false;
+        for (const p of cachedMissing) {
+          productsToFix.push({ product_id: p.product_id, title: p.title, action: 'add_sample', variant_id: null });
+        }
+        for (const p of cachedWrongPrice) {
+          productsToFix.push({ product_id: p.product_id, title: p.title, action: 'fix_price', variant_id: p.variant_id, current_price: p.current_price });
+        }
+      }
+    }
+
+    if (useLiveScan) {
+      // Live scan — fetch products for this vendor from Shopify via GraphQL
+      const vendorProducts = [];
+      let hasNextPage = true;
+      let cursor = null;
+
+      while (hasNextPage) {
+        const afterClause = cursor ? `, after: "${cursor}"` : '';
+        const vendorEscaped = vendor.vendor_name.replace(/"/g, '\\"');
+        const query = `{
+          products(first: 250, query: "status:active vendor:'${vendorEscaped}'"${afterClause}) {
+            edges {
+              cursor
+              node {
+                id
+                title
+                variants(first: 100) {
+                  edges {
+                    node {
+                      id
+                      title
+                      price
+                    }
+                  }
+                }
+              }
+            }
+            pageInfo { hasNextPage }
+          }
+        }`;
+
+        const response = await fetch(SHOPIFY_GRAPHQL_URL, {
+          method: 'POST',
+          headers: {
+            'X-Shopify-Access-Token': SHOPIFY_PRODUCT_TOKEN,
+            'Content-Type': 'application/json'
+          },
+          body: JSON.stringify({ query })
+        });
+
+        if (!response.ok) throw new Error(`Shopify GraphQL error: ${response.status}`);
+        const data = await response.json();
+        if (data.errors) throw new Error('GraphQL: ' + JSON.stringify(data.errors));
+
+        for (const edge of data.data.products.edges) {
+          const node = edge.node;
+          const pid = node.id.replace('gid://shopify/Product/', '');
+          const variants = node.variants.edges.map(ve => ({
+            id: ve.node.id.replace('gid://shopify/ProductVariant/', ''),
+            title: ve.node.title,
+            price: parseFloat(ve.node.price)
+          }));
+
+          const sampleV = variants.find(v => v.title && v.title.toLowerCase().includes('sample'));
+          if (!sampleV) {
+            productsToFix.push({ product_id: pid, title: node.title, action: 'add_sample', variant_id: null });
+          } else if (Math.abs(sampleV.price - expectedPrice) > 0.01) {
+            productsToFix.push({ product_id: pid, title: node.title, action: 'fix_price', variant_id: sampleV.id, current_price: sampleV.price });
+          }
+          cursor = edge.cursor;
+        }
+
+        hasNextPage = data.data.products.pageInfo.hasNextPage;
+        if (hasNextPage) await new Promise(r => setTimeout(r, 200));
+      }
+    }
+
+    if (productsToFix.length === 0) {
+      return res.json({
+        success: true,
+        message: `All products for ${vendor.vendor_name} already have correct Sample variants.`,
+        fixed: 0,
+        errors: 0
+      });
+    }
+
+    // Dry run — just report what would be fixed
+    if (dry_run) {
+      return res.json({
+        success: true,
+        dry_run: true,
+        vendor: vendor.vendor_name,
+        vendor_code: vendor.vendor_code,
+        expected_price: expectedPrice,
+        would_fix: productsToFix.length,
+        add_sample: productsToFix.filter(p => p.action === 'add_sample').length,
+        fix_price: productsToFix.filter(p => p.action === 'fix_price').length,
+        products: productsToFix.slice(0, 100) // Show first 100
+      });
+    }
+
+    // Live fix — apply changes via Shopify REST API (rate-limited at 2 req/s)
+    let fixed = 0;
+    let errors = 0;
+    const errorDetails = [];
+    const SHOPIFY_REST_BASE = 'https://' + SHOPIFY_DOMAIN + '/admin/api/2024-01';
+
+    for (const item of productsToFix) {
+      try {
+        if (item.action === 'add_sample') {
+          // Add a new Sample variant to the product
+          const variantPayload = {
+            variant: {
+              product_id: parseInt(item.product_id),
+              title: 'Sample',
+              option1: 'Sample',
+              price: expectedPrice.toFixed(2),
+              requires_shipping: true,
+              taxable: true,
+              inventory_management: null,
+              inventory_policy: 'deny'
+            }
+          };
+
+          const addRes = await fetch(
+            `${SHOPIFY_REST_BASE}/products/${item.product_id}/variants.json`,
+            {
+              method: 'POST',
+              headers: {
+                'X-Shopify-Access-Token': SHOPIFY_PRODUCT_TOKEN,
+                'Content-Type': 'application/json'
+              },
+              body: JSON.stringify(variantPayload)
+            }
+          );
+
+          if (!addRes.ok) {
+            const errBody = await addRes.text();
+            throw new Error(`Add variant failed (${addRes.status}): ${errBody}`);
+          }
+          fixed++;
+        } else if (item.action === 'fix_price') {
+          // Update existing variant price
+          const updatePayload = {
+            variant: {
+              id: parseInt(item.variant_id),
+              price: expectedPrice.toFixed(2)
+            }
+          };
+
+          const updateRes = await fetch(
+            `${SHOPIFY_REST_BASE}/variants/${item.variant_id}.json`,
+            {
+              method: 'PUT',
+              headers: {
+                'X-Shopify-Access-Token': SHOPIFY_PRODUCT_TOKEN,
+                'Content-Type': 'application/json'
+              },
+              body: JSON.stringify(updatePayload)
+            }
+          );
+
+          if (!updateRes.ok) {
+            const errBody = await updateRes.text();
+            throw new Error(`Update variant failed (${updateRes.status}): ${errBody}`);
+          }
+          fixed++;
+        }
+      } catch (err) {
+        errors++;
+        errorDetails.push({ product_id: item.product_id, title: item.title, error: err.message });
+        console.error(`[Victor] Sample fix error for product ${item.product_id}: ${err.message}`);
+      }
+
+      // Rate limit: 2 requests per second
+      await new Promise(r => setTimeout(r, 500));
+    }
+
+    // Invalidate cache after fixes
+    sampleVariantCache = null;
+    sampleVariantCacheTime = 0;
+
+    logActivity('api', `Sample variant fix for ${vendor.vendor_name}: ${fixed} fixed, ${errors} errors`);
+
+    res.json({
+      success: true,
+      dry_run: false,
+      vendor: vendor.vendor_name,
+      vendor_code: vendor.vendor_code,
+      expected_price: expectedPrice,
+      fixed,
+      errors,
+      error_details: errorDetails.slice(0, 50)
+    });
+  } catch (err) {
+    console.error('[Victor] Sample variant fix error:', err.message);
+    logActivity('error', 'Sample variant fix failed: ' + err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// GET /api/vendor-exclusions — List vendors excluded from Shopify push
+app.get('/api/vendor-exclusions', async (req, res) => {
+  try {
+    const { rows } = await pool.query(
+      `SELECT vendor_code, vendor_name, skip_shopify, sample_price, notes
+       FROM vendor_registry
+       WHERE skip_shopify = true
+       ORDER BY vendor_name`
+    );
+
+    res.json({
+      success: true,
+      excluded_vendors: rows,
+      static_notes: EXCLUDED_VENDOR_NOTES,
+      count: rows.length
+    });
+  } catch (err) {
+    console.error('[Victor] Vendor exclusions error:', err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- Dashboard HTML ---
+app.get('/', (req, res) => {
+  res.setHeader('Content-Type', 'text/html');
+  res.send(getDashboardHTML());
+});
+
+function getDashboardHTML() {
+  return `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Victor - Vendor Command Center</title>
+<style>
+* { margin: 0; padding: 0; box-sizing: border-box; }
+:root {
+  --bg-primary: #0a0a14;
+  --bg-card: #13152a;
+  --bg-card-hover: #1a1d35;
+  --bg-input: #1e2140;
+  --border: #2a2d45;
+  --text-primary: #e8e8f0;
+  --text-secondary: #8888a8;
+  --text-muted: #55557a;
+  --accent: #7c3aed;
+  --accent-hover: #6d28d9;
+  --accent-light: rgba(124, 58, 237, 0.15);
+  --green: #22c55e;
+  --green-bg: rgba(34, 197, 94, 0.12);
+  --red: #ef4444;
+  --red-bg: rgba(239, 68, 68, 0.12);
+  --orange: #f59e0b;
+  --orange-bg: rgba(245, 158, 11, 0.12);
+  --blue: #3b82f6;
+  --blue-bg: rgba(59, 130, 246, 0.12);
+  --yellow: #eab308;
+  --pink: #ec4899;
+  --pink-bg: rgba(236, 72, 153, 0.12);
+  --cyan: #06b6d4;
+  --cyan-bg: rgba(6, 182, 212, 0.12);
+}
+.asset-badge {
+  display: inline-block;
+  padding: 2px 6px;
+  border-radius: 3px;
+  font-size: 9px;
+  font-weight: 600;
+  margin: 2px 3px 2px 0;
+  white-space: nowrap;
+}
+.asset-badge.missing { background: var(--red-bg); color: var(--red); }
+.asset-badge.partial { background: var(--orange-bg); color: var(--orange); }
+.asset-badge.good { background: var(--green-bg); color: var(--green); }
+.asset-badge.na { background: var(--bg-input); color: var(--text-muted); }
+.score-cell { text-align: center; cursor: default; }
+.score-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; min-width: 28px; text-align: center; }
+.score-badge.score-high { background: var(--green-bg); color: var(--green); }
+.score-badge.score-mid { background: var(--orange-bg); color: var(--orange); }
+.score-badge.score-low { background: var(--red-bg); color: var(--red); }
+.score-badge.score-none { background: var(--bg-input); color: var(--text-muted); font-weight: 400; font-size: 10px; }
+.website-info { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; padding: 8px 12px; background: var(--bg-input); border-radius: 6px; border: 1px solid var(--border); }
+.website-info .wi-row { display: flex; gap: 8px; align-items: center; }
+.website-info .wi-label { font-size: 10px; color: var(--text-muted); min-width: 60px; }
+.website-info .wi-value { font-size: 11px; color: var(--text-primary); }
+.asset-badge.na { background: rgba(255,255,255,0.05); color: var(--text-muted); }
+.assets-cell { position: relative; white-space: nowrap; text-align: center; cursor: pointer; }
+.dq-summary { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; cursor: pointer; user-select: none; }
+.dq-summary:hover { filter: brightness(1.2); }
+.dq-summary.dq-good { background: var(--green-bg); color: var(--green); }
+.dq-summary.dq-partial { background: var(--orange-bg); color: var(--orange); }
+.dq-summary.dq-bad { background: var(--red-bg); color: var(--red); }
+.dq-summary.dq-na { background: var(--bg-input); color: var(--text-muted); font-weight: 400; }
+.dq-popover { display: none; position: absolute; top: 100%; right: 0; z-index: 1000; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px; min-width: 180px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
+.dq-popover.show { display: block; }
+.dq-popover .dq-row { display: flex; justify-content: space-between; align-items: center; padding: 3px 0; font-size: 11px; }
+.dq-popover .dq-label { color: var(--text-secondary); }
+.dq-popover .dq-val { font-weight: 600; }
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  background: var(--bg-primary);
+  color: var(--text-primary);
+  min-height: 100vh;
+}
+.header {
+  background: linear-gradient(135deg, #13152a 0%, #1a1040 100%);
+  border-bottom: 1px solid var(--border);
+  padding: 16px 24px;
+  position: sticky;
+  top: 0;
+  z-index: 200;
+}
+.header-top {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+.header-title {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.header-title h1 {
+  font-size: 26px;
+  font-weight: 700;
+  background: linear-gradient(135deg, #7c3aed, #a78bfa);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+.header-title .codename {
+  font-size: 14px;
+  color: var(--text-secondary);
+  background: var(--bg-input);
+  padding: 3px 10px;
+  border-radius: 4px;
+}
+.header-meta {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  font-size: 12px;
+  color: var(--text-muted);
+}
+.refresh-ts { color: var(--text-secondary); }
+.stats-bar {
+  display: flex;
+  gap: 16px;
+  flex-wrap: wrap;
+}
+.stat-card {
+  background: var(--bg-input);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  padding: 8px 16px;
+  display: flex;
+  flex-direction: column;
+  min-width: 120px;
+  cursor: pointer;
+  transition: transform 0.15s, box-shadow 0.15s;
+}
+.stat-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+}
+.stat-card:active { transform: translateY(0); }
+/* Stat Drilldown Modal */
+.stat-modal-overlay {
+  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.7); z-index: 1000;
+  display: flex; align-items: center; justify-content: center;
+  backdrop-filter: blur(4px);
+}
+.stat-modal {
+  background: var(--bg-secondary); border: 1px solid var(--border);
+  border-radius: 12px; width: 90vw; max-width: 900px; max-height: 80vh;
+  display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
+}
+.stat-modal-header {
+  padding: 16px 20px; border-bottom: 1px solid var(--border);
+  display: flex; justify-content: space-between; align-items: center;
+}
+.stat-modal-header h3 { margin: 0; font-size: 16px; color: var(--text-primary); }
+.stat-modal-close { background: none; border: none; color: var(--text-muted); font-size: 20px; cursor: pointer; padding: 4px 8px; }
+.stat-modal-close:hover { color: var(--text-primary); }
+.stat-modal-body { padding: 16px 20px; overflow-y: auto; flex: 1; }
+.stat-modal-body table { width: 100%; border-collapse: collapse; }
+.stat-modal-body th { text-align: left; padding: 6px 10px; font-size: 11px; text-transform: uppercase; color: var(--text-muted); border-bottom: 1px solid var(--border); cursor: pointer; }
+.stat-modal-body th:hover { color: var(--accent); }
+.stat-modal-body td { padding: 6px 10px; font-size: 13px; border-bottom: 1px solid rgba(42,45,69,0.3); }
+.stat-modal-body tr:hover { background: var(--bg-card-hover); }
+.stat-card .stat-value {
+  font-size: 24px;
+  font-weight: 700;
+  color: var(--text-primary);
+}
+.stat-card .stat-label {
+  font-size: 12px;
+  color: var(--text-secondary);
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  margin-top: 2px;
+}
+.stat-card.accent .stat-value { color: var(--accent); }
+.stat-card.green .stat-value { color: var(--green); }
+.stat-card.blue .stat-value { color: var(--blue); }
+.stat-card.orange .stat-value { color: var(--orange); }
+.controls {
+  padding: 12px 24px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  flex-wrap: wrap;
+  border-bottom: 1px solid var(--border);
+  background: var(--bg-primary);
+}
+.filter-group {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+}
+.filter-btn {
+  padding: 5px 12px;
+  font-size: 12px;
+  border: 1px solid var(--border);
+  background: var(--bg-card);
+  color: var(--text-secondary);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.filter-btn:hover { border-color: var(--accent); color: var(--text-primary); }
+.filter-btn.active {
+  background: var(--accent);
+  border-color: var(--accent);
+  color: #fff;
+}
+.controls-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.sort-select {
+  padding: 5px 10px;
+  font-size: 12px;
+  background: var(--bg-card);
+  color: var(--text-primary);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  outline: none;
+}
+.sort-select option { background: var(--bg-card); }
+.bulk-btn {
+  padding: 5px 12px;
+  font-size: 12px;
+  border: 1px solid var(--accent);
+  background: var(--accent-light);
+  color: var(--accent);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.bulk-btn:hover { background: var(--accent); color: #fff; }
+.bulk-btn:disabled { opacity: 0.4; cursor: not-allowed; }
+.tabs {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0;
+  padding: 0 24px;
+  background: var(--bg-primary);
+  border-bottom: 1px solid var(--border);
+}
+.tab-btn {
+  padding: 10px 20px;
+  font-size: 13px;
+  color: var(--text-muted);
+  background: none;
+  border: none;
+  border-bottom: 2px solid transparent;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.tab-btn:hover { color: var(--text-secondary); }
+.tab-btn.active {
+  color: var(--accent);
+  border-bottom-color: var(--accent);
+}
+.table-container {
+  padding: 0 24px 24px;
+  overflow-x: auto;
+}
+table {
+  width: 100%;
+  border-collapse: collapse;
+  margin-top: 12px;
+}
+thead th {
+  padding: 8px 10px;
+  font-size: 11px;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  color: var(--text-muted);
+  border-bottom: 1px solid var(--border);
+  text-align: left;
+  white-space: nowrap;
+  background: var(--bg-primary);
+}
+thead th[title] {
+  cursor: help;
+  border-bottom: 1px dotted var(--text-muted);
+}
+tbody tr {
+  border-bottom: 1px solid rgba(42, 45, 69, 0.5);
+  cursor: pointer;
+  transition: background 0.1s;
+}
+tbody tr:hover { background: var(--bg-card-hover); }
+tbody tr.expanded { background: var(--bg-card); }
+td {
+  padding: 8px 10px;
+  font-size: 13px;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+.cb-cell { width: 36px; text-align: center; }
+.cb-cell input[type="checkbox"] {
+  accent-color: var(--accent);
+  width: 15px;
+  height: 15px;
+  cursor: pointer;
+}
+.vendor-name {
+  font-weight: 600;
+  color: var(--text-primary);
+}
+.agent-code {
+  font-size: 11px;
+  color: var(--text-muted);
+  margin-left: 6px;
+}
+.port-badge {
+  font-size: 10px;
+  background: var(--bg-input);
+  color: var(--text-secondary);
+  padding: 1px 6px;
+  border-radius: 4px;
+  margin-left: 4px;
+}
+.sku-badge {
+  font-size: 11px;
+  background: var(--accent-light);
+  color: var(--accent);
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-family: monospace;
+}
+.priority-badge {
+  font-size: 10px;
+  font-weight: 600;
+  padding: 2px 8px;
+  border-radius: 10px;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+.priority-HIGH { background: var(--red-bg); color: var(--red); }
+.priority-MEDIUM { background: var(--orange-bg); color: var(--orange); }
+.priority-LOW { background: var(--green-bg); color: var(--green); }
+.status-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: 4px;
+}
+.dot-green { background: var(--green); box-shadow: 0 0 4px var(--green); }
+.dot-red { background: var(--red); }
+.dot-gray { background: #555; }
+.icon-status {
+  font-size: 14px;
+  margin-right: 4px;
+}
+.product-count {
+  font-family: monospace;
+  font-size: 12px;
+}
+.date-cell {
+  font-size: 11px;
+  color: var(--text-muted);
+}
+.expand-btn {
+  background: none;
+  border: none;
+  color: var(--text-muted);
+  cursor: pointer;
+  font-size: 16px;
+  padding: 2px 6px;
+  border-radius: 4px;
+  transition: all 0.15s;
+}
+.expand-btn:hover { background: var(--bg-input); color: var(--text-primary); }
+.pl-checkbox {
+  accent-color: var(--accent);
+  width: 14px;
+  height: 14px;
+  cursor: pointer;
+}
+.pl-name-input {
+  background: var(--bg-input);
+  border: 1px solid var(--border);
+  color: var(--text-primary);
+  font-size: 11px;
+  padding: 2px 6px;
+  border-radius: 4px;
+  width: 100px;
+  outline: none;
+}
+.pl-name-input:focus { border-color: var(--accent); }
+.drill-down {
+  display: none;
+  background: var(--bg-card);
+  border-bottom: 2px solid var(--accent);
+}
+.drill-down.show { display: table-row; }
+.drill-content {
+  padding: 16px 20px;
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+  min-width: 0;
+  overflow: hidden;
+  white-space: normal;
+}
+.drill-section {
+  background: var(--bg-input);
+  border-radius: 8px;
+  padding: 12px;
+  border: 1px solid var(--border);
+  min-width: 0;
+  overflow: hidden;
+}
+.drill-section h4 {
+  font-size: 12px;
+  color: var(--text-muted);
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  margin-bottom: 8px;
+}
+.drill-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+.action-btn {
+  padding: 6px 12px;
+  font-size: 11px;
+  border: 1px solid var(--border);
+  background: var(--bg-card);
+  color: var(--text-secondary);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.15s;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.action-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-light); }
+.action-btn.loading { opacity: 0.6; pointer-events: none; }
+.action-btn .spinner {
+  display: none;
+  width: 12px;
+  height: 12px;
+  border: 2px solid var(--text-muted);
+  border-top-color: var(--accent);
+  border-radius: 50%;
+  animation: spin 0.6s linear infinite;
+}
+.action-btn.loading .spinner { display: inline-block; }
+@keyframes spin { to { transform: rotate(360deg); } }
+.learnings-textarea {
+  width: 100%;
+  min-height: 80px;
+  background: var(--bg-card);
+  color: var(--text-primary);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  padding: 8px;
+  font-size: 12px;
+  font-family: inherit;
+  resize: vertical;
+  outline: none;
+}
+.learnings-textarea:focus { border-color: var(--accent); }
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+  gap: 10px 16px;
+}
+.detail-item {
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+  overflow: hidden;
+}
+.detail-item .label {
+  font-size: 10px;
+  color: var(--text-muted);
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  margin-bottom: 2px;
+}
+.detail-item .value {
+  font-size: 13px;
+  color: var(--text-primary);
+  word-break: break-word;
+  overflow-wrap: break-word;
+  line-height: 1.4;
+}
+.agent-link {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  color: var(--accent);
+  font-size: 12px;
+  text-decoration: none;
+}
+.agent-link:hover { text-decoration: underline; }
+.calendar-container { padding: 16px 24px; display: none; }
+.calendar-container.show { display: block; }
+.calendar-grid {
+  display: grid;
+  grid-template-columns: repeat(7, 1fr);
+  gap: 4px;
+}
+.cal-header {
+  padding: 6px;
+  font-size: 11px;
+  color: var(--text-muted);
+  text-align: center;
+  font-weight: 600;
+  text-transform: uppercase;
+}
+.cal-day {
+  min-height: 70px;
+  background: var(--bg-card);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  padding: 4px;
+  font-size: 10px;
+}
+.cal-day .day-num {
+  font-size: 12px;
+  color: var(--text-secondary);
+  font-weight: 600;
+  margin-bottom: 2px;
+}
+.cal-day .cal-vendor {
+  padding: 1px 4px;
+  border-radius: 3px;
+  margin-bottom: 2px;
+  font-size: 9px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.cal-day.empty { opacity: 0.3; }
+.toast-container {
+  position: fixed;
+  top: 16px;
+  right: 16px;
+  z-index: 10000;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.toast {
+  padding: 10px 16px;
+  border-radius: 8px;
+  font-size: 13px;
+  animation: slideIn 0.2s ease-out;
+  max-width: 360px;
+}
+.toast.success { background: var(--green-bg); color: var(--green); border: 1px solid var(--green); }
+.toast.error { background: var(--red-bg); color: var(--red); border: 1px solid var(--red); }
+.toast.info { background: var(--blue-bg); color: var(--blue); border: 1px solid var(--blue); }
+@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
+.search-input {
+  padding: 5px 10px;
+  font-size: 12px;
+  background: var(--bg-card);
+  color: var(--text-primary);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  outline: none;
+  width: 180px;
+}
+.search-input:focus { border-color: var(--accent); }
+.search-input::placeholder { color: var(--text-muted); }
+@media (max-width: 1200px) {
+  .drill-content { grid-template-columns: 1fr; }
+  .stats-bar { gap: 8px; }
+  .stat-card { min-width: 90px; padding: 6px 10px; }
+}
+@media (max-width: 768px) {
+  .controls { flex-direction: column; align-items: flex-start; }
+  .header { padding: 12px 16px; }
+  .table-container { padding: 0 8px 16px; }
+  td, th { padding: 6px 6px; font-size: 11px; }
+}
+/* --- Live Activity Ticker --- */
+.live-ticker {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 32px;
+  background: linear-gradient(135deg, #0d0d1a 0%, #13152a 100%);
+  border-top: 1px solid var(--border);
+  display: flex;
+  align-items: center;
+  z-index: 300;
+  overflow: hidden;
+  font-size: 11px;
+  padding: 0 12px;
+  gap: 8px;
+}
+.ticker-label {
+  background: var(--accent);
+  color: #fff;
+  padding: 2px 8px;
+  border-radius: 3px;
+  font-weight: 700;
+  font-size: 10px;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+.ticker-pulse {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: var(--green);
+  box-shadow: 0 0 6px var(--green);
+  animation: pulse 2s infinite;
+  flex-shrink: 0;
+}
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.3; }
+}
+.ticker-scroll {
+  flex: 1;
+  overflow: hidden;
+  white-space: nowrap;
+  mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent);
+  -webkit-mask-image: linear-gradient(to right, transparent, black 20px, black calc(100% - 20px), transparent);
+}
+.ticker-entries {
+  display: inline-flex;
+  gap: 24px;
+  animation: ticker-scroll 40s linear infinite;
+  color: var(--text-secondary);
+}
+.ticker-entries:hover { animation-play-state: paused; }
+@keyframes ticker-scroll {
+  0% { transform: translateX(0); }
+  100% { transform: translateX(-50%); }
+}
+.ticker-entry { white-space: nowrap; }
+.ticker-entry .ticker-time { color: var(--text-muted); margin-right: 6px; font-family: monospace; font-size: 10px; }
+.ticker-entry .ticker-type { font-weight: 600; margin-right: 4px; }
+.ticker-type.sync { color: var(--blue); }
+.ticker-type.agent { color: var(--green); }
+.ticker-type.api { color: var(--accent); }
+.ticker-type.error { color: var(--red); }
+.ticker-type.info { color: var(--cyan); }
+.ticker-type.health { color: var(--orange); }
+.ticker-count {
+  color: var(--text-muted);
+  font-size: 10px;
+  flex-shrink: 0;
+  white-space: nowrap;
+}
+body { padding-bottom: 36px; }
+/* Stat value flash animation */
+@keyframes flash-green {
+  0% { color: var(--green); text-shadow: 0 0 8px var(--green); }
+  100% { text-shadow: none; }
+}
+@keyframes flash-red {
+  0% { color: var(--red); text-shadow: 0 0 8px var(--red); }
+  100% { text-shadow: none; }
+}
+.stat-flash-up { animation: flash-green 1.5s ease-out; }
+.stat-flash-down { animation: flash-red 1.5s ease-out; }
+/* Sortable column headers */
+th[data-sort] {
+  cursor: pointer;
+  user-select: none;
+  position: relative;
+  padding-right: 18px !important;
+  white-space: nowrap;
+  transition: color 0.15s ease;
+}
+th[data-sort]:hover { color: var(--accent); }
+th[data-sort]::after {
+  content: '\u2195';
+  position: absolute;
+  right: 4px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 10px;
+  opacity: 0.3;
+}
+th[data-sort].sort-asc::after { content: '\u25B2'; opacity: 1; color: var(--accent); }
+th[data-sort].sort-desc::after { content: '\u25BC'; opacity: 1; color: var(--accent); }
+th[data-sort].sort-asc, th[data-sort].sort-desc { color: var(--accent); }
+/* Missing Specs Alert Banner */
+.missing-specs-alert {
+  display: none;
+  background: rgba(239,68,68,0.12);
+  border: 1px solid var(--red);
+  border-radius: 6px;
+  padding: 8px 16px;
+  margin: 8px 0;
+  color: var(--red);
+  font-size: 12px;
+  font-weight: 600;
+}
+/* Spec Details Modal */
+.spec-modal-overlay {
+  display: none;
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  background: rgba(0,0,0,0.7);
+  z-index: 10000;
+  justify-content: center;
+  align-items: center;
+}
+.spec-modal-overlay.show { display: flex; }
+.spec-modal {
+  background: var(--card-bg, #1e1e2e);
+  border: 1px solid var(--border, #333);
+  border-radius: 12px;
+  padding: 24px;
+  max-width: 700px;
+  width: 90%;
+  max-height: 80vh;
+  overflow-y: auto;
+  color: var(--text, #e0e0e0);
+}
+.spec-modal h3 { margin: 0 0 16px; color: var(--accent); }
+.spec-field-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 4px 8px;
+  border-bottom: 1px solid rgba(255,255,255,0.05);
+  font-size: 12px;
+}
+.spec-field-row:nth-child(even) { background: rgba(255,255,255,0.02); }
+.spec-field-name { font-family: monospace; }
+.spec-field-status { font-weight: 600; }
+.spec-field-status.present { color: var(--green); }
+.spec-field-status.missing { color: var(--red); }
+.spec-field-status.empty { color: var(--accent); }
+/* --- Global Catalog Search Bar --- */
+.global-search-bar {
+  background: linear-gradient(135deg, #1a1040 0%, #0d1a2a 100%);
+  border-bottom: 2px solid var(--accent);
+  padding: 12px 24px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.global-search-bar .search-icon {
+  font-size: 20px;
+  color: var(--accent);
+  flex-shrink: 0;
+}
+.global-search-bar .search-wrapper {
+  flex: 1;
+  position: relative;
+}
+.global-search-bar input[type="text"] {
+  width: 100%;
+  padding: 10px 16px 10px 40px;
+  font-size: 15px;
+  background: var(--bg-input);
+  color: var(--text-primary);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  outline: none;
+  transition: border-color 0.2s, box-shadow 0.2s;
+  font-family: inherit;
+}
+.global-search-bar input[type="text"]:focus {
+  border-color: var(--accent);
+  box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2);
+}
+.global-search-bar input[type="text"]::placeholder {
+  color: var(--text-muted);
+}
+.global-search-bar .search-magnifier {
+  position: absolute;
+  left: 12px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 16px;
+  color: var(--text-muted);
+  pointer-events: none;
+}
+.global-search-bar .search-actions {
+  display: flex;
+  gap: 8px;
+  flex-shrink: 0;
+}
+.global-search-bar .gs-btn {
+  padding: 8px 16px;
+  font-size: 13px;
+  font-weight: 600;
+  border: 1px solid var(--accent);
+  background: var(--accent);
+  color: #fff;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.15s;
+  white-space: nowrap;
+}
+.global-search-bar .gs-btn:hover { background: var(--accent-hover); }
+.global-search-bar .gs-btn.gs-secondary {
+  background: transparent;
+  color: var(--accent);
+}
+.global-search-bar .gs-btn.gs-secondary:hover {
+  background: var(--accent-light);
+}
+.global-search-bar .gs-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+/* Search Results Panel */
+.search-results-panel {
+  display: none;
+  background: var(--bg-primary);
+  border-bottom: 1px solid var(--border);
+  max-height: 70vh;
+  overflow-y: auto;
+  position: relative;
+}
+.search-results-panel.active { display: block; }
+.search-results-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 24px;
+  border-bottom: 1px solid var(--border);
+  background: var(--bg-card);
+  position: sticky;
+  top: 0;
+  z-index: 10;
+}
+.search-results-header .sr-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+.search-results-header .sr-count {
+  font-size: 13px;
+  color: var(--accent);
+  font-weight: 600;
+}
+.search-results-header .sr-close {
+  background: none;
+  border: none;
+  color: var(--text-muted);
+  font-size: 22px;
+  cursor: pointer;
+  padding: 2px 8px;
+  border-radius: 4px;
+}
+.search-results-header .sr-close:hover { color: var(--text-primary); background: var(--bg-input); }
+.search-results-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+.search-results-table thead th {
+  padding: 8px 12px;
+  font-size: 11px;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  color: var(--text-muted);
+  border-bottom: 1px solid var(--border);
+  text-align: left;
+  white-space: nowrap;
+  background: var(--bg-primary);
+  position: sticky;
+  top: 42px;
+  z-index: 5;
+}
+.search-results-table tbody tr {
+  border-bottom: 1px solid rgba(42, 45, 69, 0.5);
+  transition: background 0.1s;
+}
+.search-results-table tbody tr:hover { background: var(--bg-card-hover); }
+.search-results-table td {
+  padding: 6px 12px;
+  font-size: 13px;
+  vertical-align: middle;
+}
+.sr-thumb {
+  width: 44px;
+  height: 44px;
+  object-fit: cover;
+  border-radius: 4px;
+  border: 1px solid var(--border);
+  background: var(--bg-input);
+}
+.sr-thumb-placeholder {
+  width: 44px;
+  height: 44px;
+  border-radius: 4px;
+  background: var(--bg-input);
+  border: 1px solid var(--border);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 18px;
+  color: var(--text-muted);
+}
+.sr-shopify-dot {
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+}
+.sr-shopify-dot.on-shopify {
+  background: var(--green);
+  box-shadow: 0 0 6px var(--green);
+}
+.sr-shopify-dot.not-on-shopify {
+  background: #555;
+}
+/* FULLPRODUCT Gate Indicators */
+.gate-bar {
+  display: flex;
+  gap: 2px;
+  align-items: center;
+  flex-wrap: nowrap;
+}
+.gate-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 2px;
+  flex-shrink: 0;
+  cursor: default;
+}
+.gate-dot.pass {
+  background: var(--green);
+}
+.gate-dot.fail-hard {
+  background: var(--red);
+  box-shadow: 0 0 4px var(--red);
+}
+.gate-dot.fail-soft {
+  background: #555;
+}
+.gate-score {
+  font-size: 10px;
+  font-weight: 700;
+  margin-left: 3px;
+  font-family: monospace;
+}
+.gate-score.ready { color: var(--green); }
+.gate-score.partial { color: var(--orange); }
+.gate-score.blocked { color: var(--red); }
+.sr-pagination {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 12px 24px;
+  border-top: 1px solid var(--border);
+  background: var(--bg-card);
+  position: sticky;
+  bottom: 0;
+}
+.sr-pagination button {
+  padding: 6px 14px;
+  font-size: 12px;
+  font-weight: 600;
+  border: 1px solid var(--border);
+  background: var(--bg-input);
+  color: var(--text-primary);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.sr-pagination button:hover { border-color: var(--accent); color: var(--accent); }
+.sr-pagination button:disabled { opacity: 0.3; cursor: not-allowed; }
+.sr-pagination .sr-page-info {
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+.sr-vendor-badge {
+  display: inline-block;
+  padding: 2px 8px;
+  border-radius: 4px;
+  font-size: 11px;
+  font-weight: 600;
+  background: var(--accent-light);
+  color: var(--accent);
+  white-space: nowrap;
+}
+.sr-sku {
+  font-family: monospace;
+  font-size: 11px;
+  color: var(--text-secondary);
+}
+.sr-loading {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+  color: var(--text-muted);
+  font-size: 14px;
+}
+.sr-loading .spinner-ring {
+  width: 24px;
+  height: 24px;
+  border: 3px solid var(--border);
+  border-top-color: var(--accent);
+  border-radius: 50%;
+  animation: spin 0.6s linear infinite;
+  margin-right: 12px;
+}
+.sr-empty {
+  text-align: center;
+  padding: 40px;
+  color: var(--text-muted);
+  font-size: 14px;
+}
+</style>
+</head>
+<body>
+
+<div class="toast-container" id="toasts"></div>
+
+<!-- Global Catalog Search Bar -->
+<div class="global-search-bar">
+  <div class="search-wrapper">
+    <span class="search-magnifier">&#128269;</span>
+    <input type="text" id="globalSearchInput" placeholder="Search all products across every vendor... (pattern, color, SKU, collection)" autocomplete="off">
+  </div>
+  <div class="search-actions">
+    <button class="gs-btn" id="globalSearchBtn" onclick="performGlobalSearch()">Search</button>
+    <button class="gs-btn gs-secondary" id="viewAllBtn" onclick="viewAllProducts()">View All Items</button>
+  </div>
+</div>
+
+<!-- Search Results Panel -->
+<div class="search-results-panel" id="searchResultsPanel">
+  <div class="search-results-header">
+    <div style="display:flex;align-items:center;gap:16px">
+      <span class="sr-title" id="srTitle">Search Results</span>
+      <span class="sr-count" id="srCount"></span>
+      <select id="srVendorFilter" onchange="filterByVendor()" style="background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:4px;padding:3px 8px;font-size:11px;display:none;">
+        <option value="">All Vendors</option>
+      </select>
+    </div>
+    <button class="sr-close" onclick="closeSearchResults()">&times;</button>
+  </div>
+  <div id="srBody">
+    <table class="search-results-table">
+      <thead>
+        <tr>
+          <th>Image</th>
+          <th>Vendor</th>
+          <th>Pattern</th>
+          <th>Color</th>
+          <th>MFR SKU</th>
+          <th>DW SKU</th>
+          <th>Collection</th>
+          <th>Type</th>
+          <th>Width</th>
+          <th title="FULLPRODUCT Quality Gate: Image, Width, MFR SKU (hard), Repeat, Material, All Images, Spec Sheet, About Vendor (soft)">Gate</th>
+          <th>Shopify</th>
+        </tr>
+      </thead>
+      <tbody id="srTableBody"></tbody>
+    </table>
+  </div>
+  <div class="sr-pagination" id="srPagination" style="display:none">
+    <button id="srPrevBtn" onclick="srPrevPage()" disabled>&laquo; Previous</button>
+    <span class="sr-page-info" id="srPageInfo">Page 1 of 1</span>
+    <button id="srNextBtn" onclick="srNextPage()" disabled>Next &raquo;</button>
+  </div>
+</div>
+
+<div class="header">
+  <div class="header-top">
+    <div class="header-title">
+      <h1>Vendor Command Center</h1>
+      <span class="codename">Victor</span>
+    </div>
+    <div class="header-meta">
+      <span class="refresh-ts" id="lastRefresh">Loading...</span>
+      <span style="color:var(--text-muted);font-size:11px;margin:0 6px;">|</span>
+      <span style="color:var(--text-secondary);font-size:11px;">Last Updated: <span id="lastUpdatedTs">${new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true })}</span></span>
+      <button onclick="syncNow()" id="syncNowBtn" style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;font-size:11px;cursor:pointer;font-weight:600;">Sync Now</button>
+      <span style="color:var(--green)">&#9679;</span>
+      <span>Port 9660</span>
+    </div>
+  </div>
+  <div class="stats-bar" id="statsBar">
+    <div class="stat-card accent" onclick="openStatModal('total_vendors','Total Vendors','catalog_count')">
+      <span class="stat-value" id="statTotalVendors">--</span>
+      <span class="stat-label">Total Vendors</span>
+    </div>
+    <div class="stat-card green" onclick="openStatModal('agents_online','Agents Online','agent_status')">
+      <span class="stat-value" id="statAgentsOnline">--</span>
+      <span class="stat-label">Agents Online</span>
+    </div>
+    <div class="stat-card blue" onclick="openStatModal('catalog','Catalog (DB)','catalog_count')">
+      <span class="stat-value" id="statTotalProducts">--</span>
+      <span class="stat-label">Catalog (DB)</span>
+    </div>
+    <div class="stat-card" style="border-color:#60a5fa" onclick="openStatModal('shopify','On Shopify','shopify_count')">
+      <span class="stat-value" id="statShopifyCount" style="color:#60a5fa">--</span>
+      <span class="stat-label">On Shopify</span>
+    </div>
+    <div class="stat-card green" onclick="openStatModal('with_cost','With Cost','products_with_cost')">
+      <span class="stat-value" id="statWithCost">--</span>
+      <span class="stat-label">With Cost</span>
+    </div>
+    <div class="stat-card orange" onclick="openStatModal('coverage','Cost Coverage','products_with_cost')">
+      <span class="stat-value" id="statCoverage">--%</span>
+      <span class="stat-label">Cost Coverage</span>
+    </div>
+    <div class="stat-card" onclick="openStatModal('with_images','With Images','products_with_images')">
+      <span class="stat-value" id="statWithImages">--</span>
+      <span class="stat-label">With Images</span>
+    </div>
+    <div class="stat-card" style="border-color:var(--red)" onclick="openStatModal('not_on_shopify','Not on Shopify','catalog_not_on_shopify')">
+      <span class="stat-value" id="statMissingImages" style="color:var(--red)">--</span>
+      <span class="stat-label">Not on Shopify</span>
+    </div>
+    <div class="stat-card" style="border-color:var(--orange)" onclick="openStatModal('to_disc','Drafts to Review','shopify_draft')">
+      <span class="stat-value" id="statToDisc" style="color:var(--orange)">--</span>
+      <span class="stat-label">Drafts to Review</span>
+    </div>
+    <div class="stat-card" style="border-color:var(--cyan)" onclick="openStatModal('with_width','With Width','catalog_with_width')">
+      <span class="stat-value" id="statWithWidth" style="color:var(--cyan)">--</span>
+      <span class="stat-label">With Width</span>
+    </div>
+    <div class="stat-card" style="border-color:var(--pink)" onclick="openStatModal('with_repeat','With Repeat','catalog_with_repeat')">
+      <span class="stat-value" id="statWithRepeat" style="color:var(--pink)">--</span>
+      <span class="stat-label">With Repeat</span>
+    </div>
+    <div class="stat-card" style="border-color:var(--purple, #a78bfa)" onclick="openStatModal('spec_fill','Avg Spec Fill','spec_completeness')">
+      <span class="stat-value" id="statSpecCompleteness" style="color:var(--purple, #a78bfa)">--%</span>
+      <span class="stat-label">Avg Spec Fill</span>
+    </div>
+    <div class="stat-card">
+      <span class="stat-value" id="statCreds">--</span>
+      <span class="stat-label">With Creds</span>
+    </div>
+    <div class="stat-card">
+      <span class="stat-value" id="statPL">--</span>
+      <span class="stat-label">Private Label</span>
+    </div>
+    <div class="stat-card" style="border-left:3px solid #e74c3c">
+      <span class="stat-value" id="statSkipShopify" style="color:#e74c3c">--</span>
+      <span class="stat-label">Skip Shopify</span>
+    </div>
+    <div class="stat-card">
+      <span class="stat-value" id="statNextCrawl" style="font-size:12px;">--</span>
+      <span class="stat-label">Next Crawl</span>
+    </div>
+  </div>
+</div>
+
+<div class="missing-specs-alert" id="missingSpecsAlert"></div>
+
+<!-- POLICY: $4.25 sample-only products (Steve, 2026-06-12) -->
+<div style="margin:10px 0;padding:11px 14px;border-left:3px solid var(--orange,#f39c12);background:rgba(243,156,18,0.08);border-radius:6px;font-size:13px;line-height:1.5;color:var(--text,#ddd)">
+  <strong>📋 Pricing policy — $4.25 = sample, not a bug.</strong>
+  A <code>$4.25</code> price is the memo-<b>sample</b> variant (normal). A product with <b>only</b> a $4.25 sample
+  (no roll/yard variant) is <b>OK to leave live as-is — do NOT draft or archive it</b>.
+  But it <b>still needs a vendor cost recorded</b> → sample-only-without-cost counts against <b>Cost Coverage</b> above until costed.
+</div>
+
+<!-- Spec Details Modal -->
+<div class="spec-modal-overlay" id="specModalOverlay" onclick="closeSpecModal(event)">
+  <div class="spec-modal" onclick="event.stopPropagation()">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
+      <h3 id="specModalTitle">Spec Completeness</h3>
+      <button onclick="document.getElementById('specModalOverlay').classList.remove('show')" style="background:none;border:none;color:var(--text-muted);font-size:20px;cursor:pointer">&times;</button>
+    </div>
+    <div id="specModalSummary" style="margin-bottom:12px;font-size:13px;color:var(--text-secondary)"></div>
+    <div id="specModalBody"></div>
+  </div>
+</div>
+
+<div class="tabs">
+  <button class="tab-btn active" data-tab="vendors" onclick="switchTab('vendors')">Vendors</button>
+  <button class="tab-btn" data-tab="calendar" onclick="switchTab('calendar')">Calendar</button>
+  <button class="tab-btn" data-tab="quality" onclick="switchTab('quality')">Data Quality</button>
+  <button class="tab-btn" data-tab="discontinued" onclick="switchTab('discontinued')">Discontinued</button>
+  <button class="tab-btn" data-tab="linemanager" onclick="switchTab('linemanager')">Line Manager</button>
+  <button class="tab-btn" data-tab="tagmanager" onclick="switchTab('tagmanager')">Tag Manager</button>
+  <button class="tab-btn" data-tab="standardize" onclick="switchTab('standardize')" style="background:linear-gradient(135deg,#7c3aed,#2563eb);color:#fff;">Standardize</button>
+  <button class="tab-btn" data-tab="newlaunches" onclick="switchTab('newlaunches')" style="background:linear-gradient(135deg,#f59e0b,#ef4444);color:#fff;">New Launches</button>
+  <button class="tab-btn" onclick="window.open('http://45.61.58.125:9846/launch-pad','_blank','noopener')" style="background:linear-gradient(135deg,#00f5d4,#00d4aa);color:#000;" title="Open Launch Pad — products validated and ready for Shopify push">&#128640; Ready to Launch</button>
+  <button class="tab-btn" data-tab="productactions" onclick="switchTab('productactions')" style="background:linear-gradient(135deg,#06b6d4,#8b5cf6);color:#fff;">Product Actions</button>
+</div>
+
+<div class="controls" id="vendorControls">
+  <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
+    <div class="filter-group" id="filterGroup">
+      <button class="filter-btn active" data-filter="all">All</button>
+      <button class="filter-btn" data-filter="private_label">Private Label</button>
+      <button class="filter-btn" data-filter="skip_shopify" style="color:#e74c3c">Skip Shopify</button>
+      <button class="filter-btn" data-filter="has_creds">Has Credentials</button>
+      <button class="filter-btn" data-filter="active">Active</button>
+      <button class="filter-btn" data-filter="HIGH">HIGH</button>
+      <button class="filter-btn" data-filter="MEDIUM">MEDIUM</button>
+      <button class="filter-btn" data-filter="LOW">LOW</button>
+    </div>
+    <input type="text" class="search-input" id="searchInput" placeholder="Search vendors..." oninput="applyFilters()">
+  </div>
+  <div class="controls-right">
+    <select class="sort-select" id="sortSelect" onchange="currentSort=this.value;currentSortDir='asc';updateSortHeaders();applyFilters()">
+      <option value="name">Sort: Name</option>
+      <option value="products">Sort: Products</option>
+      <option value="updated">Sort: Last Updated</option>
+      <option value="priority">Sort: Priority</option>
+      <option value="specs">Sort: Spec Fill (Low→High)</option>
+    </select>
+    <button class="bulk-btn" id="bulkCrawlBtn" disabled onclick="bulkAction('scan')">Crawl Selected</button>
+    <button class="bulk-btn" onclick="refreshAll()">Refresh Stats</button>
+    <button class="bulk-btn" style="background:var(--accent)" onclick="syncData()">Sync Product Counts</button>
+    <button class="bulk-btn" style="background:var(--orange-bg);color:var(--orange);border:1px solid var(--orange)" onclick="scoreAllWebsites()">Score All Websites</button>
+    <button class="bulk-btn" id="launchTestBtn" disabled style="background:linear-gradient(135deg,#22c55e,#16a34a);color:#fff;border:none;font-weight:600" onclick="launchTestSelected()">Launch 1 Test SKU</button>
+    <span id="selectedCount" style="font-size:11px;color:var(--text-muted)"></span>
+  </div>
+</div>
+
+<div class="table-container" id="vendorTab">
+  <table>
+    <thead>
+      <tr>
+        <th class="cb-cell"><input type="checkbox" id="selectAll" onchange="toggleSelectAll(this.checked)"></th>
+        <th data-sort="brands" title="Show on /pages/brands">Brands</th>
+        <th data-sort="pl" title="Private Label — resell under our brand">PL</th>
+        <th data-sort="prices" title="Show prices on Shopify listings">Show $</th>
+        <th data-sort="skipshopify" title="Do NOT push products to Shopify" style="color:#e74c3c">Skip S</th>
+        <th data-sort="sample" title="Sample order price per unit">Sample $</th>
+        <th data-sort="name" class="sort-asc">Vendor</th>
+        <th data-sort="sku" title="SKU prefix for this vendor">SKU</th>
+        <th data-sort="priority" title="Crawl priority level">Priority</th>
+        <th data-sort="status" title="Agent online/offline · Credentials · Active state">Agent</th>
+        <th data-sort="site" title="Website scrapability score (1-10)">Site Score</th>
+        <th data-sort="catalog" title="Total products in our catalog DB">In Catalog</th>
+        <th data-sort="shopify" title="Products published on Shopify">On Shopify</th>
+        <th data-sort="notlisted" title="In catalog but not yet on Shopify">Not Listed</th>
+        <th data-sort="disc" title="Draft products needing review — archive or activate">Drafts</th>
+        <th data-sort="cost" title="Products that have cost/pricing data">Has Cost</th>
+        <th data-sort="images" title="Products that have at least one image">Has Images</th>
+        <th data-sort="assets" title="Data quality: Width, Repeat, Images, About, Gallery, Spec Sheet coverage">Data Quality</th>
+        <th data-sort="specs" title="Percentage of spec fields populated across catalog">Spec %</th>
+        <th data-sort="nora" title="Nora mailer pipeline — emails received from this vendor">Nora</th>
+        <th data-sort="products_upd" title="Last time product data was updated">Products</th>
+        <th data-sort="prices_upd" title="Last time pricing was updated">Prices</th>
+        <th data-sort="images_upd" title="Last time images were updated">Images</th>
+        <th></th>
+      </tr>
+    </thead>
+    <tbody id="vendorTableBody"></tbody>
+  </table>
+
+  <!-- Private Label Section -->
+  <div id="privateLabelSection" style="margin-top:32px;border-top:2px solid var(--border);padding-top:16px;display:none;">
+    <h3 style="color:var(--pink);margin:0 0 12px;">
+      <span style="font-size:16px;">Private Label Lines</span>
+      <span id="plCount" style="font-size:12px;color:var(--text-secondary);margin-left:8px;"></span>
+    </h3>
+    <p style="color:var(--text-secondary);font-size:12px;margin:0 0 12px;">These are DW-owned brands. Products already exist in Shopify — scrapers should only enrich with images/specs from real manufacturers. No new SKU assignment.</p>
+    <table class="vendor-table" style="width:100%">
+      <thead><tr>
+        <th>Brand Name</th>
+        <th>Code</th>
+        <th>SKU Prefix</th>
+        <th>Catalog</th>
+        <th>Shopify</th>
+        <th>Images</th>
+        <th>Real MFR</th>
+      </tr></thead>
+      <tbody id="plTableBody"></tbody>
+    </table>
+  </div>
+</div>
+
+<div class="calendar-container" id="calendarTab">
+  <div id="calendarContent"></div>
+</div>
+
+<div id="qualityTab" style="display:none;padding:16px 0;">
+  <div style="display:flex;gap:12px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
+    <h3 style="color:var(--text-primary);margin:0">Catalog Data Quality Heatmap</h3>
+    <button class="bulk-btn" style="background:var(--accent);font-size:12px;padding:6px 12px" onclick="runQualityAudit()">Run Audit</button>
+    <button class="bulk-btn" style="background:#e67e22;font-size:12px;padding:6px 12px" onclick="sendQualitySlackAlert()">Slack Alert</button>
+    <select id="qualityFilter" onchange="filterQualityHeatmap()" style="background:var(--card-bg);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:12px">
+      <option value="all">All Vendors</option>
+      <option value="critical">Critical (any field &lt;50%)</option>
+      <option value="warning">Warning (any field &lt;80%)</option>
+      <option value="good">Good (all fields &ge;80%)</option>
+    </select>
+    <span id="qualityIssueCount" style="font-size:12px;color:var(--text-muted)"></span>
+  </div>
+  <div id="qualitySummary" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:16px"></div>
+  <div id="qualityHeatmap" style="overflow-x:auto"></div>
+  <div id="qualityLoading" style="color:var(--text-muted);padding:20px;text-align:center">Loading quality data...</div>
+</div>
+
+<!-- Discontinued Tab -->
+<div id="discontinuedTab" style="display:none;padding:16px 0;">
+  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px">
+    <h3 style="margin:0;color:var(--text-primary);font-size:16px">Discontinuation Queue — READ ONLY</h3>
+    <div style="display:flex;gap:8px;align-items:center">
+      <select id="discVendorFilter" onchange="loadDiscontinuedData()" style="background:var(--card-bg);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:12px">
+        <option value="all">All Vendors</option>
+      </select>
+      <select id="discStatusFilter" onchange="loadDiscontinuedData()" style="background:var(--card-bg);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:12px">
+        <option value="">All Statuses</option>
+        <option value="active">Still Active on Shopify</option>
+        <option value="archived">Already Archived</option>
+        <option value="not_on_shopify">Not on Shopify</option>
+      </select>
+      <span id="discPageInfo" style="font-size:11px;color:var(--text-muted)"></span>
+      <button class="bulk-btn" style="font-size:11px;padding:4px 8px" onclick="discPrevPage()">Prev</button>
+      <button class="bulk-btn" style="font-size:11px;padding:4px 8px" onclick="discNextPage()">Next</button>
+    </div>
+  </div>
+  <div id="discSummary" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:16px"></div>
+  <div id="discVendorBreakdown" style="margin-bottom:16px"></div>
+  <div id="discProducts" style="overflow-x:auto"></div>
+  <div id="discLoading" style="color:var(--text-muted);padding:20px;text-align:center">Loading discontinued data...</div>
+</div>
+
+<!-- Line Manager Tab -->
+<div id="lineManagerTab" style="display:none;padding:16px 0;">
+  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;flex-wrap:wrap;gap:8px;">
+    <h3 style="margin:0;color:var(--text-primary);font-size:16px">Brewster/York — Brand Line Manager</h3>
+    <div style="display:flex;gap:8px;">
+      <button class="bulk-btn" onclick="lmSelectAll()">Select All</button>
+      <button class="bulk-btn" onclick="lmDeselectAll()">Deselect All</button>
+      <span style="color:var(--text-muted);font-size:12px;line-height:28px;" id="lmSelectedInfo">0 brands selected</span>
+    </div>
+  </div>
+  <div style="overflow-x:auto;margin-bottom:16px;">
+    <table style="width:100%;border-collapse:collapse;font-size:13px;">
+      <thead>
+        <tr style="background:var(--bg-input);border-bottom:2px solid var(--border);">
+          <th style="padding:10px 12px;text-align:left;color:var(--text-secondary);font-size:11px;text-transform:uppercase;letter-spacing:.5px;width:40px;">&#9745;</th>
+          <th style="padding:10px 12px;text-align:left;color:var(--text-secondary);font-size:11px;text-transform:uppercase;letter-spacing:.5px;">Brand</th>
+          <th style="padding:10px 12px;text-align:left;color:var(--text-secondary);font-size:11px;text-transform:uppercase;letter-spacing:.5px;">Products</th>
+          <th style="padding:10px 12px;text-align:left;color:var(--text-secondary);font-size:11px;text-transform:uppercase;letter-spacing:.5px;">Ready</th>
+          <th style="padding:10px 12px;text-align:left;color:var(--text-secondary);font-size:11px;text-transform:uppercase;letter-spacing:.5px;">Pushed</th>
+          <th style="padding:10px 12px;text-align:left;color:var(--text-secondary);font-size:11px;text-transform:uppercase;letter-spacing:.5px;">Last Crawled</th>
+          <th style="padding:10px 12px;text-align:left;color:var(--text-secondary);font-size:11px;text-transform:uppercase;letter-spacing:.5px;">Actions</th>
+        </tr>
+      </thead>
+      <tbody id="lmTableBody">
+        <tr><td colspan="7" style="text-align:center;padding:30px;color:var(--text-muted);">Click tab to load...</td></tr>
+      </tbody>
+    </table>
+  </div>
+
+  <!-- Push Controls -->
+  <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px;">
+    <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;">
+      <div style="font-size:12px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;">Launch Mode</div>
+      <label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:13px;color:var(--text-primary);cursor:pointer;">
+        <input type="radio" name="lmPushMode" value="all" checked style="accent-color:var(--accent);"> Push ALL selected now
+      </label>
+      <label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-primary);cursor:pointer;">
+        <input type="radio" name="lmPushMode" value="batched" style="accent-color:var(--accent);"> Push in batches of
+        <select id="lmBatchSize" style="padding:4px 8px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px;">
+          <option value="10">10</option><option value="20" selected>20</option><option value="30">30</option><option value="40">40</option><option value="50">50</option>
+        </select>
+        every
+        <select id="lmDelayHours" style="padding:4px 8px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px;">
+          <option value="1">1h</option><option value="2" selected>2h</option><option value="4">4h</option><option value="6">6h</option><option value="8">8h</option><option value="12">12h</option><option value="24">24h</option>
+        </select>
+      </label>
+    </div>
+    <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;">
+      <div style="font-size:12px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;">Workflow</div>
+      <div style="font-size:12px;color:var(--text-secondary);line-height:1.8;">
+        <div><span style="color:var(--accent);font-weight:600;">1.</span> Select brands &amp; configure batching</div>
+        <div><span style="color:var(--accent);font-weight:600;">2.</span> Click <strong style="color:var(--green);">Queue for Push</strong></div>
+        <div><span style="color:var(--accent);font-weight:600;">3.</span> Review &amp; click <strong style="color:var(--green);">Start Push</strong></div>
+        <div><span style="color:var(--accent);font-weight:600;">4.</span> Schedule <strong style="color:var(--pink);">Monthly Cron</strong></div>
+      </div>
+    </div>
+  </div>
+
+  <div style="display:flex;gap:10px;align-items:center;margin-bottom:16px;">
+    <button id="lmBtnQueue" onclick="lmQueuePush()" style="padding:12px 28px;background:linear-gradient(135deg,#1b5e20,#2e7d32);color:#66bb6a;border:1px solid #2e7d32;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer;">&#128230; Queue for Push</button>
+    <button id="lmBtnStart" onclick="lmStartPush()" style="display:none;padding:12px 28px;background:linear-gradient(135deg,#1b5e20,#2e7d32);color:#66bb6a;border:1px solid #2e7d32;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer;">&#9654; Start Push to Shopify</button>
+    <button id="lmBtnStop" onclick="lmStopPush()" style="display:none;padding:12px 28px;background:linear-gradient(135deg,#b71c1c,#c62828);color:#ef5350;border:1px solid #c62828;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer;">&#9724; Stop</button>
+    <span style="color:var(--text-muted);font-size:13px;margin-left:auto;" id="lmPushInfo"></span>
+  </div>
+
+  <!-- Queue Review -->
+  <div id="lmQueuePanel" style="display:none;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:16px;">
+    <div style="font-size:14px;font-weight:700;color:var(--orange);margin-bottom:8px;">&#128230; Queued for Push</div>
+    <div id="lmQueueDetails" style="font-size:12px;color:var(--text-secondary);"></div>
+  </div>
+
+  <!-- Progress -->
+  <div id="lmProgressPanel" style="display:none;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:16px;">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
+      <span id="lmProgressTitle" style="font-size:14px;font-weight:700;color:var(--accent);">Push in Progress...</span>
+      <div style="display:flex;gap:12px;font-size:12px;">
+        <span><strong style="color:var(--green);" id="lmPsPushed">0</strong> pushed</span>
+        <span><strong style="color:var(--red);" id="lmPsErrors">0</strong> errors</span>
+        <span><strong style="color:var(--orange);" id="lmPsSkipped">0</strong> skipped</span>
+      </div>
+    </div>
+    <div style="height:14px;background:var(--bg-primary);border-radius:7px;overflow:hidden;margin-bottom:10px;">
+      <div id="lmProgressBar" style="height:100%;background:linear-gradient(90deg,#1b5e20,#66bb6a);border-radius:7px;transition:width .5s;width:0%;"></div>
+    </div>
+    <div id="lmProgressLog" style="max-height:180px;overflow-y:auto;background:var(--bg-primary);border-radius:6px;padding:10px;font-family:monospace;font-size:11px;color:var(--text-muted);line-height:1.6;"></div>
+  </div>
+
+  <!-- Cron Schedule -->
+  <div id="lmCronPanel" style="display:none;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
+      <span style="font-size:14px;font-weight:700;color:var(--pink);">&#128197; Monthly Schedule</span>
+      <span id="lmCronBadge" style="font-size:11px;padding:3px 10px;border-radius:10px;font-weight:600;"></span>
+    </div>
+    <div id="lmCronContent"></div>
+  </div>
+</div>
+
+<div id="tagManagerTab" style="display:none;padding:16px 0;">
+  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
+    <h2 style="margin:0;font-size:18px;color:var(--text-primary)">Tag Manager</h2>
+    <div style="display:flex;gap:8px;">
+      <button onclick="tmScan()" style="background:var(--blue);color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:12px;">Scan Store</button>
+      <button onclick="tmBatchSubmit()" style="background:var(--accent);color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:12px;">Submit Batch</button>
+      <button onclick="tmApplyAll()" style="background:var(--green);color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:12px;">Apply Selected</button>
+    </div>
+  </div>
+  <div id="tmStatus" style="background:var(--bg-card);border-radius:8px;padding:12px 16px;margin-bottom:16px;display:flex;gap:24px;font-size:13px;">
+    <span>Flagged: <strong id="tmFlagged">--</strong></span>
+    <span>Pending: <strong id="tmPending" style="color:var(--orange)">--</strong></span>
+    <span>Retry: <strong id="tmRetry" style="color:var(--red)">--</strong></span>
+    <span>Analyzed: <strong id="tmAnalyzed" style="color:var(--blue)">--</strong></span>
+    <span>Applied: <strong id="tmApplied" style="color:var(--green)">--</strong></span>
+  </div>
+  <div id="tmProgress" style="display:none;background:var(--bg-card);border-radius:8px;padding:12px 16px;margin-bottom:16px;">
+    <div style="display:flex;justify-content:space-between;margin-bottom:6px;">
+      <span id="tmProgressLabel" style="font-size:12px;color:var(--text-secondary)">Working...</span>
+      <span id="tmProgressPct" style="font-size:12px;color:var(--text-primary)">0%</span>
+    </div>
+    <div style="background:var(--bg-input);border-radius:4px;height:6px;overflow:hidden;">
+      <div id="tmProgressBar" style="background:var(--accent);height:100%;width:0%;transition:width 0.3s;"></div>
+    </div>
+  </div>
+  <div style="display:flex;gap:16px;">
+    <div style="width:320px;flex-shrink:0;">
+      <table style="width:100%;border-collapse:collapse;">
+        <thead><tr style="border-bottom:1px solid var(--border);">
+          <th style="text-align:left;padding:8px;font-size:11px;color:var(--text-muted);width:30px;"><input type="checkbox" id="tmSelectAll" onchange="tmToggleAll(this.checked)"></th>
+          <th style="text-align:left;padding:8px;font-size:11px;color:var(--text-muted);">Vendor</th>
+          <th style="text-align:right;padding:8px;font-size:11px;color:var(--text-muted);">Flagged</th>
+          <th style="text-align:right;padding:8px;font-size:11px;color:var(--text-muted);">Done</th>
+        </tr></thead>
+        <tbody id="tmVendorBody"></tbody>
+      </table>
+    </div>
+    <div style="flex:1;min-width:0;">
+      <div id="tmPreviewHeader" style="font-size:13px;color:var(--text-secondary);margin-bottom:8px;">Select a vendor to preview tags</div>
+      <div id="tmPreviewBody" style="max-height:600px;overflow-y:auto;"></div>
+    </div>
+  </div>
+</div>
+
+<!-- STANDARDIZATION ENGINE TAB -->
+<div id="standardizeTab" style="display:none;padding:16px 0;">
+  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
+    <div>
+      <h2 style="margin:0;font-size:18px;color:var(--text-primary);">Product Standardization Engine</h2>
+      <p style="margin:4px 0 0;font-size:12px;color:var(--text-muted);">Gemini Vision Analysis &rarr; Metafields &rarr; Tags &rarr; Title Case &rarr; SEO</p>
+    </div>
+    <div style="display:flex;gap:8px;align-items:center;">
+      <span id="stdEngineStatus" style="padding:4px 12px;border-radius:12px;font-size:11px;font-weight:700;background:var(--bg-input);color:var(--text-muted);">IDLE</span>
+    </div>
+  </div>
+
+  <!-- Engine Controls -->
+  <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:16px;">
+    <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
+      <select id="stdVendorSelect" style="padding:8px 12px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;font-size:13px;min-width:250px;">
+        <option value="">Select vendor...</option>
+      </select>
+      <label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text-secondary);cursor:pointer;">
+        <input type="checkbox" id="stdAnalyzeOnly"> Analyze Only (don't apply)
+      </label>
+      <label style="display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text-secondary);cursor:pointer;">
+        Limit: <input type="number" id="stdLimit" value="2" min="1" max="50000" style="width:70px;padding:4px 8px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:4px;font-size:12px;">
+      </label>
+      <button id="stdStartBtn" onclick="stdStart()" style="padding:8px 20px;background:linear-gradient(135deg,#1b5e20,#2e7d32);color:#66bb6a;border:1px solid #2e7d32;border-radius:6px;font-size:13px;font-weight:700;cursor:pointer;">&#9654; START</button>
+      <button id="stdStopBtn" onclick="stdStop()" style="display:none;padding:8px 20px;background:linear-gradient(135deg,#b71c1c,#c62828);color:#ef5350;border:1px solid #c62828;border-radius:6px;font-size:13px;font-weight:700;cursor:pointer;">&#9724; STOP</button>
+    </div>
+    <!-- Progress Bar -->
+    <div id="stdProgressWrap" style="display:none;margin-top:12px;">
+      <div style="display:flex;justify-content:space-between;margin-bottom:4px;">
+        <span id="stdProgressLabel" style="font-size:12px;color:var(--text-secondary);">Processing...</span>
+        <span id="stdProgressPct" style="font-size:12px;color:var(--text-primary);">0%</span>
+      </div>
+      <div style="background:var(--bg-input);border-radius:4px;height:8px;overflow:hidden;">
+        <div id="stdProgressBar" style="background:linear-gradient(90deg,#7c3aed,#2563eb);height:100%;width:0%;transition:width 0.5s;border-radius:4px;"></div>
+      </div>
+      <div style="display:flex;gap:20px;margin-top:8px;font-size:12px;">
+        <span>Analyzed: <strong id="stdAnalyzedCount" style="color:var(--blue);">0</strong></span>
+        <span>Applied: <strong id="stdAppliedCount" style="color:var(--green);">0</strong></span>
+        <span>Errors: <strong id="stdErrorCount" style="color:var(--red);">0</strong></span>
+        <span>Skipped: <strong id="stdSkippedCount" style="color:var(--text-muted);">0</strong></span>
+        <span>Current: <em id="stdCurrentProduct" style="color:var(--text-secondary);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;">--</em></span>
+      </div>
+    </div>
+  </div>
+
+  <!-- Vendor Progress Table -->
+  <div style="display:flex;gap:16px;">
+    <div style="flex:1;min-width:0;">
+      <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:10px;overflow:hidden;">
+        <table style="width:100%;border-collapse:collapse;">
+          <thead>
+            <tr style="border-bottom:1px solid var(--border);">
+              <th id="stdTh-vendor"   onclick="stdSortBy('vendor')"   style="text-align:left;padding:10px 12px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">Vendor <span class="std-sort-ind"></span></th>
+              <th id="stdTh-shopify"  onclick="stdSortBy('shopify')"  style="text-align:right;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">Shopify <span class="std-sort-ind"></span></th>
+              <th id="stdTh-scanned"  onclick="stdSortBy('scanned')"  style="text-align:right;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">Scanned <span class="std-sort-ind"></span></th>
+              <th id="stdTh-analyzed" onclick="stdSortBy('analyzed')" style="text-align:center;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">Analyzed <span class="std-sort-ind"></span></th>
+              <th id="stdTh-applied"  onclick="stdSortBy('applied')"  style="text-align:center;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">Applied <span class="std-sort-ind"></span></th>
+              <th id="stdTh-pct"      onclick="stdSortBy('pct')"      style="text-align:center;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">% <span class="std-sort-ind"></span></th>
+              <th id="stdTh-lastrun"  onclick="stdSortBy('lastrun')"  style="text-align:right;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">Last Run <span class="std-sort-ind"></span></th>
+              <th id="stdTh-status"   onclick="stdSortBy('status')"   style="text-align:center;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;">Status <span class="std-sort-ind"></span></th>
+              <th id="stdTh-discount" onclick="stdSortBy('discount')" style="text-align:center;padding:10px 8px;font-size:11px;color:var(--text-muted);font-weight:600;cursor:pointer;user-select:none;" title="Our confirmed vendor discount. Green = confirmed.">Disc % <span class="std-sort-ind"></span></th>
+            </tr>
+          </thead>
+          <tbody id="stdVendorBody"></tbody>
+        </table>
+      </div>
+    </div>
+
+    <!-- Live Log -->
+    <div style="width:360px;flex-shrink:0;">
+      <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:12px;height:500px;overflow-y:auto;font-family:monospace;font-size:11px;line-height:1.6;" id="stdLogPanel">
+        <div style="color:var(--text-muted);">Engine log will appear here...</div>
+      </div>
+    </div>
+  </div>
+
+  <!-- Preview Panel -->
+  <div id="stdPreviewPanel" style="display:none;margin-top:16px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;">
+    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
+      <h3 id="stdPreviewTitle" style="margin:0;font-size:15px;color:var(--text-primary);">Preview</h3>
+      <button onclick="document.getElementById('stdPreviewPanel').style.display='none'" style="background:none;border:none;color:var(--text-muted);font-size:18px;cursor:pointer;">&times;</button>
+    </div>
+    <div id="stdPreviewBody" style="max-height:400px;overflow-y:auto;"></div>
+  </div>
+</div>
+
+<!-- New Launches Tab -->
+<div id="newLaunchesTab" style="display:none;padding:16px 0;">
+  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
+    <div>
+      <h2 style="margin:0;font-size:18px;color:var(--text-primary);">New Product Launches</h2>
+      <p style="margin:4px 0 0;font-size:12px;color:var(--text-muted);">Recently created active products on Shopify, grouped by date</p>
+    </div>
+    <div style="display:flex;gap:10px;align-items:center;">
+      <select id="nlDateRange" style="padding:8px 12px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;font-size:13px;">
+        <option value="1">Today</option>
+        <option value="3">Last 3 Days</option>
+        <option value="7" selected>Last 7 Days</option>
+        <option value="14">Last 14 Days</option>
+        <option value="30">Last 30 Days</option>
+        <option value="60">Last 60 Days</option>
+        <option value="90">Last 90 Days</option>
+      </select>
+      <span id="nlCount" style="font-size:13px;color:var(--accent);font-weight:700;"></span>
+      <span id="nlLoading" style="display:none;font-size:12px;color:var(--text-muted);">Loading...</span>
+    </div>
+  </div>
+  <div id="nlContent"></div>
+</div>
+
+<!-- Product Actions Tab -->
+<div id="productActionsTab" style="display:none;padding:16px 0;">
+  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
+    <div>
+      <h2 style="margin:0;font-size:18px;color:var(--text-primary);">Product Bulk Actions</h2>
+      <p style="margin:4px 0 0;font-size:12px;color:var(--text-muted);">Search products, select, and apply bulk actions (Archive, Draft, Active, Redirect)</p>
+    </div>
+  </div>
+
+  <!-- Search Bar -->
+  <div style="display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap;">
+    <input type="text" id="paSearchInput" placeholder="Search by title, vendor, handle, SKU..." style="flex:1;min-width:300px;padding:10px 14px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;font-size:13px;" onkeydown="if(event.key==='Enter')paSearch()">
+    <select id="paStatusFilter" style="padding:10px 12px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;font-size:13px;">
+      <option value="">All Statuses</option>
+      <option value="ACTIVE">Active</option>
+      <option value="DRAFT">Draft</option>
+      <option value="ARCHIVED">Archived</option>
+    </select>
+    <button onclick="paSearch()" style="padding:10px 20px;background:var(--accent);color:#fff;border:none;border-radius:6px;font-weight:600;cursor:pointer;font-size:13px;">Search</button>
+    <span id="paResultCount" style="font-size:12px;color:var(--text-muted);"></span>
+    <span id="paSearching" style="display:none;font-size:12px;color:var(--text-muted);">Searching...</span>
+  </div>
+
+  <!-- Bulk Action Bar (sticky) -->
+  <div id="paBulkBar" style="display:none;position:sticky;top:0;z-index:20;background:var(--bg-surface);border:1px solid var(--accent);border-radius:8px;padding:10px 16px;margin-bottom:12px;display:none;align-items:center;gap:12px;flex-wrap:wrap;">
+    <span id="paSelectedCount" style="font-size:13px;font-weight:600;color:var(--accent);min-width:100px;"></span>
+    <div style="display:flex;gap:8px;flex-wrap:wrap;">
+      <button onclick="paBulkStatus('archived')" style="padding:6px 14px;background:rgba(248,81,73,0.12);color:#f85149;border:1px solid rgba(248,81,73,0.3);border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;">Archive Selected</button>
+      <button onclick="paBulkStatus('draft')" style="padding:6px 14px;background:rgba(210,153,34,0.12);color:#d29922;border:1px solid rgba(210,153,34,0.3);border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;">Draft Selected</button>
+      <button onclick="paBulkStatus('active')" style="padding:6px 14px;background:rgba(63,185,80,0.12);color:#3fb950;border:1px solid rgba(63,185,80,0.3);border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;">Activate Selected</button>
+      <div style="display:flex;gap:4px;align-items:center;">
+        <input type="text" id="paRedirectTarget" placeholder="Redirect URL (e.g. /collections/all)" style="padding:6px 10px;background:var(--bg-input);color:var(--text-primary);border:1px solid var(--border);border-radius:6px;font-size:12px;width:250px;">
+        <button onclick="paBulkRedirect()" style="padding:6px 14px;background:rgba(188,140,255,0.12);color:#bc8cff;border:1px solid rgba(188,140,255,0.3);border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;">Redirect Selected</button>
+      </div>
+    </div>
+    <span id="paBulkProgress" style="font-size:11px;color:var(--text-muted);margin-left:auto;"></span>
+  </div>
+
+  <!-- Results Table -->
+  <div style="overflow-x:auto;">
+    <table style="width:100%;border-collapse:collapse;font-size:13px;">
+      <thead>
+        <tr style="border-bottom:2px solid var(--border);">
+          <th style="padding:8px;text-align:left;width:30px;"><input type="checkbox" id="paSelectAll" onchange="paToggleAll(this.checked)"></th>
+          <th style="padding:8px;text-align:left;width:60px;">Image</th>
+          <th style="padding:8px;text-align:left;">Title</th>
+          <th style="padding:8px;text-align:left;">Vendor</th>
+          <th style="padding:8px;text-align:left;width:90px;">Status</th>
+          <th style="padding:8px;text-align:left;width:120px;">SKU</th>
+          <th style="padding:8px;text-align:left;width:100px;">Type</th>
+          <th style="padding:8px;text-align:left;width:130px;">Updated</th>
+        </tr>
+      </thead>
+      <tbody id="paTableBody"></tbody>
+    </table>
+  </div>
+
+  <!-- Pagination -->
+  <div id="paPagination" style="display:flex;justify-content:center;gap:8px;margin-top:16px;"></div>
+</div>
+
+<script>
+// State
+var vendors = [];
+var stats = {};
+var activeFilter = 'all';
+var expandedRow = null;
+var selectedVendors = {};
+var agentHealthCache = {};
+var currentSort = 'name';
+var currentSortDir = 'asc';
+
+// Close data quality popovers when clicking outside
+document.addEventListener('click', function() {
+  var pops = document.querySelectorAll('.dq-popover.show');
+  for (var i = 0; i < pops.length; i++) pops[i].classList.remove('show');
+});
+
+function updateSortHeaders() {
+  var allTh = document.querySelectorAll('#vendorTab th[data-sort]');
+  for (var i = 0; i < allTh.length; i++) {
+    allTh[i].classList.remove('sort-asc', 'sort-desc');
+    if (allTh[i].getAttribute('data-sort') === currentSort) {
+      allTh[i].classList.add(currentSortDir === 'asc' ? 'sort-asc' : 'sort-desc');
+    }
+  }
+}
+
+// Escape HTML for safe DOM insertion
+function esc(s) {
+  if (s === null || s === undefined) return '';
+  var d = document.createElement('div');
+  d.appendChild(document.createTextNode(String(s)));
+  return d.innerHTML;
+}
+
+// Toast notifications
+// Stat card click — open modal with vendor breakdown sorted by that stat
+function openStatModal(statKey, title, sortField) {
+  // Remove any existing modal
+  var existing = document.querySelector('.stat-modal-overlay');
+  if (existing) existing.remove();
+
+  // Build modal
+  var overlay = document.createElement('div');
+  overlay.className = 'stat-modal-overlay';
+  overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
+
+  var modal = document.createElement('div');
+  modal.className = 'stat-modal';
+
+  var header = document.createElement('div');
+  header.className = 'stat-modal-header';
+  var h3 = document.createElement('h3');
+  h3.textContent = title + ' — Vendor Breakdown';
+  header.appendChild(h3);
+  var closeBtn = document.createElement('button');
+  closeBtn.className = 'stat-modal-close';
+  closeBtn.textContent = '\u2715';
+  closeBtn.addEventListener('click', function() { overlay.remove(); });
+  header.appendChild(closeBtn);
+  modal.appendChild(header);
+
+  var body = document.createElement('div');
+  body.className = 'stat-modal-body';
+
+  // Sort vendors by the relevant field
+  var sorted = (vendors || []).slice().sort(function(a, b) {
+    var va = parseFloat(a[sortField]) || 0;
+    var vb = parseFloat(b[sortField]) || 0;
+    return vb - va;
+  });
+
+  // Build table
+  var table = document.createElement('table');
+  var thead = document.createElement('thead');
+  var headerRow = document.createElement('tr');
+  var cols = [
+    { key: 'vendor_name', label: 'Vendor' },
+    { key: 'catalog_count', label: 'Catalog' },
+    { key: 'shopify_count', label: 'On Shopify' },
+    { key: 'catalog_not_on_shopify', label: 'Not Listed' },
+    { key: 'products_with_cost', label: 'Has Cost' },
+    { key: 'products_with_images', label: 'Has Images' },
+    { key: 'catalog_with_width', label: 'Width' },
+    { key: 'catalog_with_repeat', label: 'Repeat' },
+    { key: 'spec_completeness', label: 'Spec %' }
+  ];
+  // Add special columns based on stat
+  if (statKey === 'to_disc') {
+    cols.splice(3, 0, { key: 'shopify_draft', label: 'Draft' }, { key: 'shopify_archived', label: 'Archived' });
+  }
+
+  var currentSort = { key: sortField, desc: true };
+
+  function renderModalTable() {
+    // Clear existing rows
+    var existingTbody = table.querySelector('tbody');
+    if (existingTbody) existingTbody.remove();
+
+    var tbody = document.createElement('tbody');
+    var total = 0;
+    for (var i = 0; i < sorted.length; i++) {
+      var v = sorted[i];
+      var primaryVal = parseFloat(v[sortField]) || 0;
+      if (statKey !== 'total_vendors' && statKey !== 'agents_online' && primaryVal === 0) continue;
+      total += primaryVal;
+      var tr = document.createElement('tr');
+      tr.style.cursor = 'pointer';
+      tr.addEventListener('click', (function(code) {
+        return function() { overlay.remove(); scrollToVendor(code); };
+      })(v.vendor_code));
+      for (var c = 0; c < cols.length; c++) {
+        var td = document.createElement('td');
+        var val = v[cols[c].key];
+        if (cols[c].key === 'vendor_name') {
+          td.textContent = val;
+          td.style.fontWeight = '600';
+        } else if (cols[c].key === 'spec_completeness') {
+          td.textContent = (parseFloat(val) || 0).toFixed(1) + '%';
+        } else {
+          td.textContent = Number(val || 0).toLocaleString();
+          if (parseFloat(val) > 0) td.style.color = 'var(--accent)';
+          else td.style.color = 'var(--text-muted)';
+        }
+        tr.appendChild(td);
+      }
+      tbody.appendChild(tr);
+    }
+    // Total row
+    var totalTr = document.createElement('tr');
+    totalTr.style.cssText = 'font-weight:700;border-top:2px solid var(--border);';
+    for (var c2 = 0; c2 < cols.length; c2++) {
+      var totalTd = document.createElement('td');
+      if (c2 === 0) totalTd.textContent = 'TOTAL';
+      else if (cols[c2].key === sortField) { totalTd.textContent = Number(total).toLocaleString(); totalTd.style.color = 'var(--accent)'; }
+      else totalTd.textContent = '';
+      totalTr.appendChild(totalTd);
+    }
+    tbody.appendChild(totalTr);
+    table.appendChild(tbody);
+  }
+
+  // Column header clicks for re-sorting
+  for (var c = 0; c < cols.length; c++) {
+    var th = document.createElement('th');
+    th.textContent = cols[c].label;
+    th.setAttribute('data-key', cols[c].key);
+    if (cols[c].key === sortField) th.style.color = 'var(--accent)';
+    th.addEventListener('click', (function(key) {
+      return function() {
+        if (currentSort.key === key) currentSort.desc = !currentSort.desc;
+        else { currentSort.key = key; currentSort.desc = true; }
+        sorted.sort(function(a, b) {
+          var va = key === 'vendor_name' ? (a[key] || '') : (parseFloat(a[key]) || 0);
+          var vb = key === 'vendor_name' ? (b[key] || '') : (parseFloat(b[key]) || 0);
+          if (key === 'vendor_name') return currentSort.desc ? vb.localeCompare(va) : va.localeCompare(vb);
+          return currentSort.desc ? vb - va : va - vb;
+        });
+        // Update header highlights
+        headerRow.querySelectorAll('th').forEach(function(t) { t.style.color = t.getAttribute('data-key') === key ? 'var(--accent)' : ''; });
+        renderModalTable();
+      };
+    })(cols[c].key));
+    headerRow.appendChild(th);
+  }
+  thead.appendChild(headerRow);
+  table.appendChild(thead);
+  renderModalTable();
+
+  body.appendChild(table);
+  modal.appendChild(body);
+  overlay.appendChild(modal);
+  document.body.appendChild(overlay);
+}
+
+// Scroll to vendor row in table
+function scrollToVendor(code) {
+  // Find the row and expand it
+  var rows = document.querySelectorAll('#vendorTab tbody tr:not(.drill-down)');
+  for (var i = 0; i < rows.length; i++) {
+    var cb = rows[i].querySelector('input[data-code="' + code + '"]');
+    if (cb) {
+      rows[i].scrollIntoView({ behavior: 'smooth', block: 'center' });
+      rows[i].style.background = 'rgba(139,92,246,0.15)';
+      setTimeout(function(r) { return function() { r.style.background = ''; }; }(rows[i]), 2000);
+      toggleExpand(code);
+      return;
+    }
+  }
+}
+
+function showToast(msg, type) {
+  type = type || 'info';
+  var container = document.getElementById('toasts');
+  var el = document.createElement('div');
+  el.className = 'toast ' + type;
+  el.textContent = msg;
+  container.appendChild(el);
+  setTimeout(function() { el.remove(); }, 4000);
+}
+
+// Sync Now button handler
+function syncNow() {
+  var btn = document.getElementById('syncNowBtn');
+  btn.textContent = 'Syncing...';
+  btn.disabled = true;
+  btn.style.opacity = '0.6';
+  fetch('/api/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
+    .then(function(r) { return r.json(); })
+    .then(function(res) {
+      btn.textContent = 'Sync Now';
+      btn.disabled = false;
+      btn.style.opacity = '1';
+      if (res.success) {
+        showToast('Sync complete: ' + res.updated + ' vendors updated in ' + res.elapsed + 's', 'success');
+        // Reload data immediately
+        Promise.all([loadVendors(), loadStats()]).then(function() {
+          var now = formatTimePT(new Date());
+          document.getElementById('lastRefresh').textContent = 'Last refresh: ' + now;
+          document.getElementById('lastUpdatedTs').textContent = now;
+        });
+      } else {
+        showToast('Sync failed: ' + (res.error || 'unknown'), 'error');
+      }
+    })
+    .catch(function(err) {
+      btn.textContent = 'Sync Now';
+      btn.disabled = false;
+      btn.style.opacity = '1';
+      showToast('Sync error: ' + err.message, 'error');
+    });
+}
+
+// API fetch helper — adds explicit Basic Auth to avoid Chrome credential-in-URL blocking
+function apiCall(path, options) {
+  options = options || {};
+  options.headers = options.headers || {};
+  options.headers['Content-Type'] = 'application/json';
+  options.headers['Authorization'] = ''  /* auth via httpOnly vcc_session cookie — no embedded credential */;
+  // Use clean origin (without embedded credentials) for absolute URL construction
+  var cleanUrl = window.location.protocol + '//' + window.location.host + path;
+  return fetch(cleanUrl, options).then(function(r) { return r.json(); });
+}
+
+// Load vendors
+function loadVendors() {
+  return apiCall('/api/vendors').then(function(res) {
+    if (res.success) {
+      vendors = res.data;
+      applyFilters();
+    }
+  }).catch(function(err) {
+    showToast('Failed to load vendors: ' + err.message, 'error');
+  });
+}
+
+// Load stats
+function loadStats() {
+  return apiCall('/api/stats').then(function(res) {
+    if (res.success) {
+      stats = res.data;
+      document.getElementById('statTotalVendors').textContent = stats.total_vendors;
+      document.getElementById('statTotalProducts').textContent = Number(stats.total_catalog || stats.total_products).toLocaleString();
+      document.getElementById('statShopifyCount').textContent = Number(stats.total_shopify || 0).toLocaleString();
+      document.getElementById('statWithCost').textContent = Number(stats.total_with_cost).toLocaleString();
+      document.getElementById('statCoverage').textContent = stats.cost_coverage_pct + '%';
+      document.getElementById('statWithImages').textContent = Number(stats.total_with_images).toLocaleString();
+      document.getElementById('statMissingImages').textContent = Number(stats.total_not_on_shopify || stats.missing_images || 0).toLocaleString();
+      document.getElementById('statToDisc').textContent = Number(stats.drafts_needing_review || 0).toLocaleString();
+      document.getElementById('statWithWidth').textContent = Number(stats.total_with_width || 0).toLocaleString();
+      document.getElementById('statWithRepeat').textContent = Number(stats.total_with_repeat || 0).toLocaleString();
+      document.getElementById('statSpecCompleteness').textContent = (stats.avg_spec_completeness || 0) + '%';
+      document.getElementById('statCreds').textContent = stats.with_credentials;
+      // Missing specs alert
+      var specAlertEl = document.getElementById('missingSpecsAlert');
+      var below50 = parseInt(stats.vendors_below_50_spec) || 0;
+      if (below50 > 0) {
+        specAlertEl.style.display = 'block';
+        specAlertEl.textContent = '\u26A0 ' + below50 + ' vendor' + (below50 > 1 ? 's' : '') + ' below 50% spec fill rate — click Specs column for details';
+      } else {
+        specAlertEl.style.display = 'none';
+      }
+      document.getElementById('statPL').textContent = stats.private_label_count;
+      document.getElementById('statSkipShopify').textContent = stats.skip_shopify_count || 0;
+      document.getElementById('statNextCrawl').textContent = stats.next_crawl ? fmtDateFull(stats.next_crawl) : 'None';
+      // Agents online count is updated from vendors data
+      var onlineCount = vendors.filter(function(v) { return v.agent_status === 'online'; }).length;
+      document.getElementById('statAgentsOnline').textContent = onlineCount + ' / ' + stats.total_vendors;
+    }
+  }).catch(function() {});
+}
+
+function refreshAll() {
+  showToast('Refreshing data...', 'info');
+  Promise.all([loadVendors(), loadStats()]).then(function() {
+    document.getElementById('lastRefresh').textContent = 'Last refresh: ' + formatTimePT(new Date());
+    showToast('Data refreshed', 'success');
+  });
+}
+
+function syncData() {
+  showToast('Syncing product counts from Shopify + catalog tables...', 'info');
+  apiCall('/api/sync', { method: 'POST' }).then(function(res) {
+    if (res.success) {
+      showToast('Sync complete: ' + res.updated + ' vendors updated in ' + res.elapsed + 's', 'success');
+      refreshAll();
+    } else {
+      showToast('Sync failed: ' + (res.error || 'Unknown error'), 'error');
+    }
+  });
+}
+
+// Spec details modal
+function showSpecDetails(code) {
+  var overlay = document.getElementById('specModalOverlay');
+  var title = document.getElementById('specModalTitle');
+  var summary = document.getElementById('specModalSummary');
+  var body = document.getElementById('specModalBody');
+  title.textContent = 'Loading...';
+  summary.textContent = '';
+  while (body.firstChild) body.removeChild(body.firstChild);
+  overlay.classList.add('show');
+  apiCall('/api/spec-completeness/' + encodeURIComponent(code)).then(function(res) {
+    if (!res.success) { title.textContent = 'Error'; summary.textContent = res.error; return; }
+    title.textContent = res.vendor + ' — Spec Completeness';
+    summary.textContent = res.present + ' of ' + res.total + ' required fields present (' + res.pct + '%)';
+    var fields = res.fields || {};
+    var fieldNames = Object.keys(fields);
+    for (var i = 0; i < fieldNames.length; i++) {
+      var f = fieldNames[i];
+      var info = fields[f];
+      var statusClass = info.status === 'present'
+        ? (info.filled === 0 ? 'empty' : 'present')
+        : 'missing';
+      var statusText = info.status === 'present'
+        ? (info.filled + '/' + info.total + ' filled' + (info.column !== f ? ' (as ' + info.column + ')' : ''))
+        : 'Column missing';
+      var row = document.createElement('div');
+      row.className = 'spec-field-row';
+      var nameSpan = document.createElement('span');
+      nameSpan.className = 'spec-field-name';
+      nameSpan.textContent = f;
+      var statusSpan = document.createElement('span');
+      statusSpan.className = 'spec-field-status ' + statusClass;
+      statusSpan.textContent = statusText;
+      row.appendChild(nameSpan);
+      row.appendChild(statusSpan);
+      body.appendChild(row);
+    }
+  }).catch(function(err) {
+    title.textContent = 'Error';
+    summary.textContent = err.message;
+  });
+}
+
+function closeSpecModal(e) {
+  if (e.target === document.getElementById('specModalOverlay')) {
+    document.getElementById('specModalOverlay').classList.remove('show');
+  }
+}
+
+function scoreAllWebsites() {
+  showToast('Scoring all vendor websites in background (stale/unscored only)...', 'info');
+  apiCall('/api/score-all', { method: 'POST', body: JSON.stringify({ stale_only: true }) }).then(function(res) {
+    if (res.success) {
+      showToast('Scoring ' + res.count + ' vendors in background. Refresh in a few minutes.', 'success');
+    } else {
+      showToast('Score-all failed: ' + (res.error || 'Unknown error'), 'error');
+    }
+  });
+}
+
+// Filters
+function applyFilters() {
+  var search = document.getElementById('searchInput').value.toLowerCase();
+
+  var filtered = vendors.filter(function(v) {
+    if (activeFilter === 'all') return true;
+    if (activeFilter === 'private_label') return v.is_private_label;
+    if (activeFilter === 'skip_shopify') return v.skip_shopify;
+    if (activeFilter === 'has_creds') return v.has_credentials;
+    if (activeFilter === 'active') return v.is_active;
+    if (activeFilter === 'HIGH' || activeFilter === 'MEDIUM' || activeFilter === 'LOW') return v.crawl_priority === activeFilter;
+    return true;
+  });
+
+  if (search) {
+    filtered = filtered.filter(function(v) {
+      return v.vendor_name.toLowerCase().indexOf(search) >= 0 ||
+        v.vendor_code.toLowerCase().indexOf(search) >= 0 ||
+        (v.agent_name || '').toLowerCase().indexOf(search) >= 0 ||
+        (v.sku_prefix || '').toLowerCase().indexOf(search) >= 0;
+    });
+  }
+
+  // Use column-header sort (currentSort/currentSortDir) or fallback to dropdown
+  var sort = currentSort || document.getElementById('sortSelect').value;
+  var dir = currentSortDir || 'asc';
+  var priorityOrder = { HIGH: 0, MEDIUM: 1, LOW: 2 };
+  var statusOrder = { online: 0, offline: 1, 'no-port': 2 };
+
+  filtered.sort(function(a, b) {
+    var cmp = 0;
+    switch (sort) {
+      case 'name': cmp = (a.vendor_name || '').localeCompare(b.vendor_name || ''); break;
+      case 'brands': cmp = (a.show_on_brands_page ? 1 : 0) - (b.show_on_brands_page ? 1 : 0); break;
+      case 'pl': cmp = (a.is_private_label ? 1 : 0) - (b.is_private_label ? 1 : 0); break;
+      case 'prices': cmp = (a.display_prices !== false ? 1 : 0) - (b.display_prices !== false ? 1 : 0); break;
+      case 'skipshopify': cmp = (a.skip_shopify ? 1 : 0) - (b.skip_shopify ? 1 : 0); break;
+      case 'sample': cmp = (parseFloat(a.sample_price) || 4.25) - (parseFloat(b.sample_price) || 4.25); break;
+      case 'sku': cmp = (a.sku_prefix || '').localeCompare(b.sku_prefix || ''); break;
+      case 'priority': cmp = (priorityOrder[a.crawl_priority] || 1) - (priorityOrder[b.crawl_priority] || 1); break;
+      case 'status': cmp = (statusOrder[a.agent_status] || 2) - (statusOrder[b.agent_status] || 2); break;
+      case 'site': cmp = (a.website_score || 0) - (b.website_score || 0); break;
+      case 'catalog':
+      case 'products': cmp = (a.catalog_count || a.total_products || 0) - (b.catalog_count || b.total_products || 0); break;
+      case 'shopify': cmp = (a.shopify_count || 0) - (b.shopify_count || 0); break;
+      case 'notlisted': cmp = (a.catalog_not_on_shopify || 0) - (b.catalog_not_on_shopify || 0); break;
+      case 'disc': cmp = (a.shopify_draft || 0) - (b.shopify_draft || 0); break;
+      case 'cost': cmp = (a.products_with_cost || 0) - (b.products_with_cost || 0); break;
+      case 'images': cmp = (a.products_with_images || 0) - (b.products_with_images || 0); break;
+      case 'assets':
+        var aTotal = a.catalog_count || a.total_products || 1;
+        var bTotal = b.catalog_count || b.total_products || 1;
+        var aScore = ((a.catalog_with_width || 0) + (a.catalog_with_repeat || 0) + (a.products_with_images || 0)) / aTotal;
+        var bScore = ((b.catalog_with_width || 0) + (b.catalog_with_repeat || 0) + (b.products_with_images || 0)) / bTotal;
+        cmp = aScore - bScore;
+        break;
+      case 'specs': cmp = (parseFloat(a.spec_completeness) || 0) - (parseFloat(b.spec_completeness) || 0); break;
+      case 'nora': cmp = (a.nora_mailers || 0) - (b.nora_mailers || 0); break;
+      case 'products_upd':
+      case 'updated':
+        var dpa = a.last_product_update ? new Date(a.last_product_update).getTime() : 0;
+        var dpb = b.last_product_update ? new Date(b.last_product_update).getTime() : 0;
+        cmp = dpa - dpb;
+        break;
+      case 'prices_upd':
+        var dpra = a.last_price_update ? new Date(a.last_price_update).getTime() : 0;
+        var dprb = b.last_price_update ? new Date(b.last_price_update).getTime() : 0;
+        cmp = dpra - dprb;
+        break;
+      case 'images_upd':
+        var dia = a.last_image_update ? new Date(a.last_image_update).getTime() : 0;
+        var dib = b.last_image_update ? new Date(b.last_image_update).getTime() : 0;
+        cmp = dia - dib;
+        break;
+      default: cmp = 0;
+    }
+    return dir === 'desc' ? -cmp : cmp;
+  });
+
+  renderTable(filtered);
+}
+
+// Filter buttons
+document.getElementById('filterGroup').addEventListener('click', function(e) {
+  if (e.target.classList.contains('filter-btn')) {
+    var btns = document.querySelectorAll('.filter-btn');
+    for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
+    e.target.classList.add('active');
+    activeFilter = e.target.getAttribute('data-filter');
+    applyFilters();
+  }
+});
+
+// Sortable column headers — click any th[data-sort] to sort
+document.querySelector('#vendorTab thead').addEventListener('click', function(e) {
+  var th = e.target.closest('th[data-sort]');
+  if (!th) return;
+  var sortKey = th.getAttribute('data-sort');
+  if (currentSort === sortKey) {
+    currentSortDir = currentSortDir === 'asc' ? 'desc' : 'asc';
+  } else {
+    currentSort = sortKey;
+    currentSortDir = 'asc';
+  }
+  // Update header classes
+  var allTh = document.querySelectorAll('#vendorTab th[data-sort]');
+  for (var i = 0; i < allTh.length; i++) {
+    allTh[i].classList.remove('sort-asc', 'sort-desc');
+  }
+  th.classList.add(currentSortDir === 'asc' ? 'sort-asc' : 'sort-desc');
+  // Sync dropdown if matching
+  var sel = document.getElementById('sortSelect');
+  if (sel) {
+    for (var j = 0; j < sel.options.length; j++) {
+      if (sel.options[j].value === sortKey) { sel.selectedIndex = j; break; }
+    }
+  }
+  applyFilters();
+});
+
+// Render table
+function renderTable(list) {
+  var tbody = document.getElementById('vendorTableBody');
+  // Clear using safe method
+  while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
+
+  // Separate private label vendors
+  var regularVendors = [];
+  var plVendors = [];
+  for (var pi = 0; pi < list.length; pi++) {
+    if (list[pi].is_private_label) plVendors.push(list[pi]);
+    else regularVendors.push(list[pi]);
+  }
+
+  // Render private label section
+  var plBody = document.getElementById('plTableBody');
+  var plSection = document.getElementById('privateLabelSection');
+  if (plBody) {
+    while (plBody.firstChild) plBody.removeChild(plBody.firstChild);
+    document.getElementById('plCount').textContent = '(' + plVendors.length + ' brands)';
+    plSection.style.display = plVendors.length > 0 ? 'block' : 'none';
+    for (var pli = 0; pli < plVendors.length; pli++) {
+      var pv = plVendors[pli];
+      var plTr = document.createElement('tr');
+      plTr.style.cursor = 'pointer';
+      plTr.addEventListener('click', (function(code) { return function() { toggleExpand(code); }; })(pv.vendor_code));
+      var imgPct = pv.total_products > 0 ? Math.round((pv.products_with_images || 0) / pv.total_products * 100) : 0;
+
+      // Build PL row cells using DOM methods — SKU cell shows prefix + series range
+      var plSeriesText = pv.sku_prefix || '\u2014';
+      if (pv.sku_range_start != null) {
+        var plRangeStart = parseInt(pv.sku_range_start, 10);
+        var plItemCount = parseInt(pv.catalog_count || pv.total_products || 0, 10);
+        plSeriesText = (pv.sku_prefix || '') + plRangeStart.toLocaleString() + ' \u2013 ' + (plRangeStart + 4999).toLocaleString();
+        if (plItemCount > 0) plSeriesText += ' (' + plItemCount.toLocaleString() + ')';
+      }
+      var plCells = [
+        { text: pv.vendor_name, style: 'padding:8px 12px;font-weight:500;' },
+        { text: pv.vendor_code, style: 'padding:8px 12px;color:var(--text-secondary);font-size:12px;' },
+        { text: plSeriesText, style: 'padding:8px 12px;font-family:monospace;font-size:11px;white-space:nowrap;' },
+        { text: (pv.catalog_count || pv.total_products || 0).toLocaleString(), style: 'padding:8px 12px;text-align:right;' },
+        { text: (pv.shopify_count || 0).toLocaleString(), style: 'padding:8px 12px;text-align:right;' },
+        { text: (pv.products_with_images || 0).toLocaleString() + ' (' + imgPct + '%)', style: 'padding:8px 12px;text-align:right;' }
+      ];
+      for (var ci = 0; ci < plCells.length; ci++) {
+        var plTd = document.createElement('td');
+        plTd.style.cssText = plCells[ci].style;
+        plTd.textContent = plCells[ci].text;
+        plTr.appendChild(plTd);
+      }
+
+      // Real MFR input cell
+      var mfrTd = document.createElement('td');
+      mfrTd.style.cssText = 'padding:8px 12px;';
+      var mfrInput = document.createElement('input');
+      mfrInput.type = 'text';
+      mfrInput.value = pv.private_label_name || '';
+      mfrInput.placeholder = 'Real manufacturer...';
+      mfrInput.style.cssText = 'background:var(--bg-input,#1a1a2e);border:1px solid var(--border);color:var(--text-primary);padding:4px 8px;border-radius:4px;width:150px;font-size:12px;';
+      mfrInput.setAttribute('data-code', pv.vendor_code);
+      mfrInput.addEventListener('click', function(e) { e.stopPropagation(); });
+      mfrInput.addEventListener('blur', (function(code) {
+        return function(e) { updatePLName(code, e.target.value); };
+      })(pv.vendor_code));
+      mfrTd.appendChild(mfrInput);
+      plTr.appendChild(mfrTd);
+
+      plBody.appendChild(plTr);
+    }
+  }
+
+  // Now render only regular vendors in main table
+  list = regularVendors;
+
+  for (var i = 0; i < list.length; i++) {
+    var v = list[i];
+    var tr = document.createElement('tr');
+    tr.id = 'row-' + v.vendor_code;
+    tr.setAttribute('data-code', v.vendor_code);
+
+    if (v.skip_shopify) {
+      tr.style.background = 'rgba(231,76,60,0.08)';
+    }
+    var isSelected = selectedVendors[v.vendor_code] || false;
+    var agentOnline = v.agent_status === 'online';
+    var agentDot = v.agent_status === 'online' ? 'dot-green' : (v.agent_status === 'no-port' ? 'dot-gray' : 'dot-red');
+    var agentTitle = agentOnline ? 'Agent online' : (v.agent_status === 'no-port' ? 'No port configured' : 'Agent offline');
+
+    // Build cells using DOM methods
+    // Checkbox cell
+    var cbTd = document.createElement('td');
+    cbTd.className = 'cb-cell';
+    var cbInput = document.createElement('input');
+    cbInput.type = 'checkbox';
+    cbInput.checked = isSelected;
+    cbInput.setAttribute('data-code', v.vendor_code);
+    cbInput.addEventListener('change', (function(code) {
+      return function(e) {
+        e.stopPropagation();
+        toggleSelect(code, e.target.checked);
+      };
+    })(v.vendor_code));
+    cbInput.addEventListener('click', function(e) { e.stopPropagation(); });
+    cbTd.appendChild(cbInput);
+    tr.appendChild(cbTd);
+
+    // Brands page cell
+    var brandsTd = document.createElement('td');
+    var brandsCb = document.createElement('input');
+    brandsCb.type = 'checkbox';
+    brandsCb.className = 'pl-checkbox';
+    brandsCb.checked = v.show_on_brands_page;
+    brandsCb.title = 'Show on /pages/brands';
+    brandsCb.setAttribute('data-code', v.vendor_code);
+    brandsCb.addEventListener('change', (function(code) {
+      return function(e) {
+        e.stopPropagation();
+        updateBrandsPage(code, e.target.checked);
+      };
+    })(v.vendor_code));
+    brandsCb.addEventListener('click', function(e) { e.stopPropagation(); });
+    brandsTd.appendChild(brandsCb);
+    tr.appendChild(brandsTd);
+
+    // PL cell
+    var plTd = document.createElement('td');
+    var plCb = document.createElement('input');
+    plCb.type = 'checkbox';
+    plCb.className = 'pl-checkbox';
+    plCb.checked = v.is_private_label;
+    plCb.title = 'Private Label';
+    plCb.setAttribute('data-code', v.vendor_code);
+    plCb.addEventListener('change', (function(code) {
+      return function(e) {
+        e.stopPropagation();
+        updatePL(code, e.target.checked);
+      };
+    })(v.vendor_code));
+    plCb.addEventListener('click', function(e) { e.stopPropagation(); });
+    plTd.appendChild(plCb);
+    if (v.is_private_label) {
+      var plInput = document.createElement('input');
+      plInput.type = 'text';
+      plInput.className = 'pl-name-input';
+      plInput.value = v.private_label_name || '';
+      plInput.placeholder = 'Label name...';
+      plInput.setAttribute('data-code', v.vendor_code);
+      plInput.addEventListener('blur', (function(code) {
+        return function(e) { updatePLName(code, e.target.value); };
+      })(v.vendor_code));
+      plInput.addEventListener('click', function(e) { e.stopPropagation(); });
+      plTd.appendChild(plInput);
+    }
+    tr.appendChild(plTd);
+
+    // Display Prices checkbox cell
+    var pricesTd = document.createElement('td');
+    var pricesCb = document.createElement('input');
+    pricesCb.type = 'checkbox';
+    pricesCb.className = 'pl-checkbox';
+    pricesCb.checked = v.display_prices !== false;
+    pricesCb.title = 'Display Prices';
+    pricesCb.setAttribute('data-code', v.vendor_code);
+    pricesCb.addEventListener('change', (function(code) {
+      return function(e) {
+        e.stopPropagation();
+        updateField(code, 'display_prices', e.target.checked);
+      };
+    })(v.vendor_code));
+    pricesCb.addEventListener('click', function(e) { e.stopPropagation(); });
+    pricesTd.appendChild(pricesCb);
+    tr.appendChild(pricesTd);
+
+    // Skip Shopify checkbox cell
+    var skipTd = document.createElement('td');
+    var skipCb = document.createElement('input');
+    skipCb.type = 'checkbox';
+    skipCb.className = 'pl-checkbox';
+    skipCb.checked = !!v.skip_shopify;
+    skipCb.title = 'Do NOT push to Shopify';
+    skipCb.style.accentColor = '#e74c3c';
+    skipCb.setAttribute('data-code', v.vendor_code);
+    skipCb.addEventListener('change', (function(code) {
+      return function(e) {
+        e.stopPropagation();
+        updateField(code, 'skip_shopify', e.target.checked);
+      };
+    })(v.vendor_code));
+    skipCb.addEventListener('click', function(e) { e.stopPropagation(); });
+    skipTd.appendChild(skipCb);
+    tr.appendChild(skipTd);
+
+    // Sample Price cell
+    var sampleTd = document.createElement('td');
+    sampleTd.className = 'sample-price';
+    sampleTd.textContent = '$' + (v.sample_price || '4.25');
+    sampleTd.style.color = 'var(--green)';
+    sampleTd.style.fontWeight = '600';
+    sampleTd.style.fontSize = '12px';
+    tr.appendChild(sampleTd);
+
+    // Vendor name cell
+    var nameTd = document.createElement('td');
+    var nameSpan = document.createElement('span');
+    nameSpan.className = 'vendor-name';
+    nameSpan.textContent = v.vendor_name;
+    nameTd.appendChild(nameSpan);
+    var agentSpan = document.createElement('span');
+    agentSpan.className = 'agent-code';
+    agentSpan.textContent = '(' + (v.agent_name || '?') + ')';
+    nameTd.appendChild(agentSpan);
+    var portSpan = document.createElement('span');
+    portSpan.className = 'port-badge';
+    portSpan.textContent = v.agent_port || '?';
+    nameTd.appendChild(portSpan);
+    tr.appendChild(nameTd);
+
+    // SKU cell — just prefix if no items, prefix + range + count if items exist
+    var skuTd = document.createElement('td');
+    skuTd.style.cssText = 'white-space:nowrap;';
+    var itemCount = parseInt(v.catalog_count || 0, 10);
+    var skuSpan = document.createElement('span');
+    skuSpan.className = 'sku-badge';
+    skuSpan.textContent = v.sku_prefix || '?';
+    skuTd.appendChild(skuSpan);
+    if (itemCount > 0 && v.sku_range_start != null) {
+      var rangeStart = parseInt(v.sku_range_start, 10);
+      var seriesDiv = document.createElement('div');
+      seriesDiv.style.cssText = 'font-family:monospace;font-size:10px;color:var(--text-secondary);margin-top:2px;';
+      seriesDiv.textContent = (v.sku_prefix || '') + rangeStart + ' \u2013 ' + (rangeStart + itemCount - 1);
+      skuTd.appendChild(seriesDiv);
+      var countDiv = document.createElement('div');
+      countDiv.style.cssText = 'font-size:9px;color:var(--green);margin-top:1px;';
+      countDiv.textContent = itemCount.toLocaleString() + ' items';
+      skuTd.appendChild(countDiv);
+    }
+    tr.appendChild(skuTd);
+
+    // Priority cell
+    var priTd = document.createElement('td');
+    var priSpan = document.createElement('span');
+    priSpan.className = 'priority-badge priority-' + (v.crawl_priority || 'MEDIUM');
+    priSpan.textContent = v.crawl_priority || 'MEDIUM';
+    priTd.appendChild(priSpan);
+    tr.appendChild(priTd);
+
+    // Agent status cell — shows online/offline dot, credential status, active state
+    var statusTd = document.createElement('td');
+    statusTd.style.cssText = 'white-space:nowrap;font-size:11px;';
+    var dotSpan = document.createElement('span');
+    dotSpan.className = 'status-dot ' + agentDot;
+    dotSpan.title = agentTitle;
+    statusTd.appendChild(dotSpan);
+    var statusLabel = document.createElement('span');
+    statusLabel.style.cssText = 'margin-right:6px;color:var(--text-secondary);';
+    statusLabel.textContent = agentOnline ? 'On' : (v.agent_status === 'no-port' ? '--' : 'Off');
+    statusTd.appendChild(statusLabel);
+    var credSpan = document.createElement('span');
+    credSpan.className = 'icon-status';
+    credSpan.style.cssText = 'cursor:help;';
+    if (v.has_credentials) {
+      credSpan.textContent = '\uD83D\uDD12';
+      credSpan.title = 'Login credentials configured — can access trade portal';
+    } else {
+      credSpan.textContent = '\uD83D\uDD13';
+      credSpan.title = 'No login credentials — cannot access trade portal';
+      credSpan.style.opacity = '0.3';
+    }
+    statusTd.appendChild(credSpan);
+    if (!v.is_active) {
+      var warnSpan = document.createElement('span');
+      warnSpan.className = 'icon-status';
+      warnSpan.style.color = 'var(--red)';
+      warnSpan.title = 'Vendor marked inactive — not being crawled';
+      warnSpan.textContent = '\u26A0';
+      statusTd.appendChild(warnSpan);
+    }
+    tr.appendChild(statusTd);
+
+    // Website score cell
+    var scoreTd = document.createElement('td');
+    scoreTd.className = 'score-cell';
+    var scoreBadge = document.createElement('span');
+    if (v.website_score) {
+      scoreBadge.className = 'score-badge ' + (v.website_score >= 7 ? 'score-high' : v.website_score >= 4 ? 'score-mid' : 'score-low');
+      scoreBadge.textContent = v.website_score;
+      scoreBadge.title = (v.website_summary || '') + ' | ' + (v.website_tech_stack || '') + (v.website_scored_at ? ' | Scored: ' + new Date(v.website_scored_at).toLocaleDateString() : '');
+    } else {
+      scoreBadge.className = 'score-badge score-none';
+      scoreBadge.textContent = '--';
+      scoreBadge.title = v.website_url ? 'Not scored yet' : 'No website URL';
+    }
+    scoreTd.appendChild(scoreBadge);
+    tr.appendChild(scoreTd);
+
+    // Product count cells — Catalog (DB) is source of truth
+    // Catalog count
+    var catTd = document.createElement('td');
+    catTd.className = 'product-count';
+    var catCount = v.catalog_count || v.total_products || 0;
+    catTd.textContent = Number(catCount).toLocaleString();
+    if (catCount > 0) catTd.style.color = 'var(--accent)';
+    else catTd.style.color = 'var(--text-muted)';
+    tr.appendChild(catTd);
+
+    // On Shopify count
+    var shopTd = document.createElement('td');
+    shopTd.className = 'product-count';
+    var shopCount = v.shopify_count || 0;
+    shopTd.textContent = Number(shopCount).toLocaleString();
+    if (shopCount > 0) shopTd.style.color = '#60a5fa';
+    else shopTd.style.color = 'var(--text-muted)';
+    tr.appendChild(shopTd);
+
+    // Not on Shopify (gap)
+    var gapTd = document.createElement('td');
+    gapTd.className = 'product-count';
+    var gapCount = v.catalog_not_on_shopify || 0;
+    gapTd.textContent = Number(gapCount).toLocaleString();
+    if (gapCount > 0) gapTd.style.color = 'var(--orange)';
+    else gapTd.style.color = 'var(--text-muted)';
+    tr.appendChild(gapTd);
+
+    // Drafts needing review (only drafts — archived are already handled)
+    var discTd = document.createElement('td');
+    discTd.className = 'product-count';
+    var draftCount = v.shopify_draft || 0;
+    discTd.textContent = Number(draftCount).toLocaleString();
+    if (draftCount > 100) { discTd.style.color = 'var(--red)'; discTd.style.fontWeight = '700'; }
+    else if (draftCount > 0) discTd.style.color = 'var(--orange)';
+    else discTd.style.color = 'var(--text-muted)';
+    discTd.title = 'Drafts: ' + draftCount + ' (Archived: ' + (v.shopify_archived || 0) + ')';
+    tr.appendChild(discTd);
+
+    // With cost
+    var costTd = document.createElement('td');
+    costTd.className = 'product-count';
+    var costCount = v.products_with_cost || 0;
+    costTd.textContent = Number(costCount).toLocaleString();
+    if (costCount > 0) costTd.style.color = 'var(--green)';
+    else costTd.style.color = 'var(--text-muted)';
+    tr.appendChild(costTd);
+
+    // With images
+    var imgTd = document.createElement('td');
+    imgTd.className = 'product-count';
+    var imgCount = v.products_with_images || 0;
+    imgTd.textContent = Number(imgCount).toLocaleString();
+    if (imgCount > 0) imgTd.style.color = 'var(--green)';
+    else imgTd.style.color = 'var(--text-muted)';
+    tr.appendChild(imgTd);
+
+    // Assets / Data Quality cell — compact summary badge with popover detail
+    var assetTd = document.createElement('td');
+    assetTd.className = 'assets-cell';
+    var totalCat = v.catalog_count || v.total_products || 0;
+    var withWidth = v.catalog_with_width || 0;
+    var withRepeat = v.catalog_with_repeat || 0;
+    var withImg = v.products_with_images || 0;
+    var aboutVendor = v.catalog_with_about_vendor || 0;
+    var allImg = v.catalog_with_all_images || 0;
+    var specSh = v.catalog_with_spec_sheet || 0;
+
+    if (totalCat === 0) {
+      var naBadge = document.createElement('span');
+      naBadge.className = 'dq-summary dq-na';
+      naBadge.textContent = 'N/A';
+      assetTd.appendChild(naBadge);
+    } else {
+      // Count how many of the 6 fields pass their threshold
+      var goodCount = 0;
+      var fields = [
+        { label: 'Width', val: withWidth, thresh: 0.8 },
+        { label: 'Repeat', val: withRepeat, thresh: 0.8 },
+        { label: 'Images', val: withImg, thresh: 0.8 },
+        { label: 'About', val: aboutVendor, thresh: 0.5 },
+        { label: 'Gallery', val: allImg, thresh: 0.5 },
+        { label: 'Spec Sheet', val: specSh, thresh: 0.5 }
+      ];
+      for (var fi = 0; fi < fields.length; fi++) {
+        if (fields[fi].val >= totalCat * fields[fi].thresh) goodCount++;
+      }
+      var summaryClass = goodCount >= 5 ? 'dq-good' : (goodCount >= 3 ? 'dq-partial' : 'dq-bad');
+      var summaryBadge = document.createElement('span');
+      summaryBadge.className = 'dq-summary ' + summaryClass;
+      summaryBadge.textContent = goodCount + '/6';
+      summaryBadge.title = 'Click for data quality detail';
+
+      // Build popover
+      var popover = document.createElement('div');
+      popover.className = 'dq-popover';
+      for (var fi2 = 0; fi2 < fields.length; fi2++) {
+        var f = fields[fi2];
+        var pct = Math.round(f.val / totalCat * 100);
+        var ok = f.val >= totalCat * f.thresh;
+        var row = document.createElement('div');
+        row.className = 'dq-row';
+        var lbl = document.createElement('span');
+        lbl.className = 'dq-label';
+        lbl.textContent = f.label;
+        var val = document.createElement('span');
+        val.className = 'dq-val';
+        val.style.color = f.val === 0 ? 'var(--red)' : (ok ? 'var(--green)' : 'var(--orange)');
+        val.textContent = f.val === 0 ? 'None' : (ok ? pct + '% OK' : pct + '%');
+        row.appendChild(lbl);
+        row.appendChild(val);
+        popover.appendChild(row);
+      }
+
+      assetTd.appendChild(summaryBadge);
+      assetTd.appendChild(popover);
+
+      // Click anywhere in the cell to toggle popover
+      assetTd.addEventListener('click', function(pop) {
+        return function(e) {
+          e.stopPropagation();
+          // Close any other open popovers
+          var allPops = document.querySelectorAll('.dq-popover.show');
+          for (var p = 0; p < allPops.length; p++) {
+            if (allPops[p] !== pop) allPops[p].classList.remove('show');
+          }
+          pop.classList.toggle('show');
+        };
+      }(popover));
+    }
+    tr.appendChild(assetTd);
+
+    // Spec Completeness cell
+    var specTd = document.createElement('td');
+    specTd.className = 'spec-cell';
+    var specPct = parseFloat(v.spec_completeness) || 0;
+    var specPresent = v.spec_fields_present || 0;
+    var specTotalFields = v.spec_fields_total || 35;
+    if (!v.catalog_table) {
+      var specNa = document.createElement('span');
+      specNa.className = 'asset-badge na';
+      specNa.textContent = 'N/A';
+      specTd.appendChild(specNa);
+    } else {
+      var specBadge = document.createElement('span');
+      specBadge.style.cssText = 'display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;cursor:pointer;';
+      if (specPct >= 80) {
+        specBadge.style.background = 'rgba(34,197,94,0.15)';
+        specBadge.style.color = 'var(--green)';
+      } else if (specPct >= 50) {
+        specBadge.style.background = 'rgba(251,191,36,0.15)';
+        specBadge.style.color = 'var(--orange)';
+      } else {
+        specBadge.style.background = 'rgba(239,68,68,0.15)';
+        specBadge.style.color = 'var(--red)';
+      }
+      specBadge.textContent = specPct + '%';
+      specBadge.title = specPresent + '/' + specTotalFields + ' fields present\\nClick for details';
+      specBadge.addEventListener('click', (function(code) {
+        return function(e) { e.stopPropagation(); showSpecDetails(code); };
+      })(v.vendor_code));
+      specTd.appendChild(specBadge);
+    }
+    tr.appendChild(specTd);
+
+    // Nora mailer cell
+    var noraTd = document.createElement('td');
+    noraTd.style.cssText = 'text-align:center;font-size:11px;';
+    var noraCount = v.nora_mailers || 0;
+    if (noraCount > 0) {
+      var noraBadge = document.createElement('span');
+      noraBadge.style.cssText = 'display:inline-block;padding:2px 8px;border-radius:4px;font-weight:700;background:rgba(168,85,247,0.15);color:#a855f7;cursor:pointer;';
+      noraBadge.textContent = noraCount;
+      noraBadge.title = noraCount + ' mailer(s) from ' + v.vendor_name + (v.nora_active ? ' (' + v.nora_active + ' active)' : '') + (v.nora_latest ? '\\nLatest: ' + new Date(v.nora_latest).toLocaleDateString() : '');
+      noraBadge.addEventListener('click', function(e) {
+        e.stopPropagation();
+        window.open('http://45.61.58.125:9812', '_blank');
+      });
+      noraTd.appendChild(noraBadge);
+    } else {
+      noraTd.textContent = '--';
+      noraTd.style.color = 'var(--text-muted)';
+    }
+    tr.appendChild(noraTd);
+
+    // Date cells
+    var dateFields = ['last_product_update', 'last_price_update', 'last_image_update'];
+    for (var d = 0; d < dateFields.length; d++) {
+      var dateTd = document.createElement('td');
+      dateTd.className = 'date-cell';
+      setDateCellContent(dateTd, v[dateFields[d]]);
+      tr.appendChild(dateTd);
+    }
+
+    // Expand button cell
+    var expTd = document.createElement('td');
+    var expBtn = document.createElement('button');
+    expBtn.className = 'expand-btn';
+    expBtn.textContent = expandedRow === v.vendor_code ? '\u25B2' : '\u25BC';
+    expBtn.setAttribute('data-code', v.vendor_code);
+    expBtn.addEventListener('click', (function(code) {
+      return function(e) { e.stopPropagation(); toggleExpand(code); };
+    })(v.vendor_code));
+    expTd.appendChild(expBtn);
+    tr.appendChild(expTd);
+
+    // Row click handler
+    tr.addEventListener('click', (function(code) {
+      return function(e) {
+        if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'TEXTAREA') return;
+        if (e.target.closest && e.target.closest('.action-btn')) return;
+        if (e.target.closest && (e.target.closest('.dq-summary') || e.target.closest('.dq-popover') || e.target.closest('.assets-cell'))) return;
+        toggleExpand(code);
+      };
+    })(v.vendor_code));
+
+    if (expandedRow === v.vendor_code) tr.classList.add('expanded');
+
+    tbody.appendChild(tr);
+
+    // Drill-down row
+    var drillTr = document.createElement('tr');
+    drillTr.id = 'drill-' + v.vendor_code;
+    drillTr.className = 'drill-down' + (expandedRow === v.vendor_code ? ' show' : '');
+    var drillTd = document.createElement('td');
+    drillTd.colSpan = 21;
+    buildDrillDown(drillTd, v);
+    drillTr.appendChild(drillTd);
+    tbody.appendChild(drillTr);
+  }
+}
+
+function setDateCellContent(td, dateStr) {
+  if (!dateStr) {
+    var span = document.createElement('span');
+    span.style.color = 'var(--text-muted)';
+    span.textContent = '--';
+    td.appendChild(span);
+    return;
+  }
+  var dt = new Date(dateStr);
+  var now = new Date();
+  var diff = now - dt;
+  var days = Math.floor(diff / 86400000);
+  var span = document.createElement('span');
+  if (days === 0) {
+    span.style.color = 'var(--green)';
+    span.textContent = 'Today';
+  } else if (days === 1) {
+    span.style.color = 'var(--green)';
+    span.textContent = '1d ago';
+  } else if (days < 7) {
+    span.style.color = 'var(--text-secondary)';
+    span.textContent = days + 'd ago';
+  } else if (days < 30) {
+    span.style.color = 'var(--orange)';
+    span.textContent = Math.floor(days/7) + 'w ago';
+  } else if (days < 365) {
+    span.style.color = 'var(--red)';
+    span.textContent = Math.floor(days/30) + 'mo ago';
+  } else {
+    span.style.color = 'var(--red)';
+    span.textContent = Math.floor(days/365) + 'y ago';
+  }
+  td.appendChild(span);
+}
+
+function buildDrillDown(container, v) {
+  var content = document.createElement('div');
+  content.className = 'drill-content';
+
+  // Actions section
+  var actionsSection = document.createElement('div');
+  actionsSection.className = 'drill-section';
+  var actionsTitle = document.createElement('h4');
+  actionsTitle.textContent = 'Actions';
+  actionsSection.appendChild(actionsTitle);
+
+  var actionsDiv = document.createElement('div');
+  actionsDiv.className = 'drill-actions';
+  var actionDefs = [
+    { label: 'Update Products', action: 'scan' },
+    { label: 'Update Prices', action: 'update-prices' },
+    { label: 'Update Images', action: 'update-images' },
+    { label: 'Update Specs', action: 'update-specs' },
+    { label: 'Check Discontinued', action: 'check-discontinued' },
+    { label: 'Schedule Blog', action: 'blog' },
+    { label: 'Schedule Instagram', action: 'instagram' },
+    { label: 'Score Website', action: 'score-website' }
+  ];
+  for (var a = 0; a < actionDefs.length; a++) {
+    var btn = document.createElement('button');
+    btn.className = 'action-btn';
+    var spinner = document.createElement('span');
+    spinner.className = 'spinner';
+    btn.appendChild(spinner);
+    btn.appendChild(document.createTextNode(' ' + actionDefs[a].label));
+    btn.setAttribute('data-action', actionDefs[a].action);
+    btn.setAttribute('data-code', v.vendor_code);
+    btn.addEventListener('click', (function(code, action) {
+      return function(e) {
+        e.stopPropagation();
+        vendorAction(code, action, e.currentTarget);
+      };
+    })(v.vendor_code, actionDefs[a].action));
+    actionsDiv.appendChild(btn);
+  }
+
+  // Assign DW SKUs button (separate — uses different endpoint)
+  var skuBtn = document.createElement('button');
+  skuBtn.className = 'action-btn';
+  skuBtn.style.background = 'rgba(124,58,237,0.15)';
+  skuBtn.style.borderColor = 'rgba(124,58,237,0.3)';
+  skuBtn.style.color = 'var(--accent)';
+  var skuSpinner = document.createElement('span');
+  skuSpinner.className = 'spinner';
+  skuBtn.appendChild(skuSpinner);
+  skuBtn.appendChild(document.createTextNode(' Assign DW SKUs'));
+  skuBtn.addEventListener('click', (function(code) {
+    return function(e) {
+      e.stopPropagation();
+      assignDwSkus(code, e.currentTarget);
+    };
+  })(v.vendor_code));
+  actionsDiv.appendChild(skuBtn);
+
+  // Inquiry Page button — show for vendors with 0 catalog products
+  if (!v.catalog_count || parseInt(v.catalog_count) === 0) {
+    var inqBtn = document.createElement('button');
+    inqBtn.className = 'action-btn';
+    inqBtn.style.background = 'rgba(79,195,247,0.15)';
+    inqBtn.style.borderColor = 'rgba(79,195,247,0.3)';
+    inqBtn.style.color = '#4fc3f7';
+    var inqSpinner = document.createElement('span');
+    inqSpinner.className = 'spinner';
+    inqSpinner.style.display = 'none';
+    inqBtn.appendChild(inqSpinner);
+    inqBtn.appendChild(document.createTextNode(' Create Inquiry Page'));
+    inqBtn.addEventListener('click', (function(code, name) {
+      return function(e) {
+        e.stopPropagation();
+        createInquiryPage(code, name, e.currentTarget);
+      };
+    })(v.vendor_code, v.vendor_name));
+    actionsDiv.appendChild(inqBtn);
+  }
+
+  actionsSection.appendChild(actionsDiv);
+
+  // Agent link
+  var linkDiv = document.createElement('div');
+  linkDiv.style.marginTop = '12px';
+  var agentLink = document.createElement('a');
+  agentLink.className = 'agent-link';
+  agentLink.href = 'http://45.61.58.125:' + (v.agent_port || '');
+  agentLink.target = '_blank';
+  agentLink.textContent = 'Open ' + (v.agent_name || v.vendor_code) + ' Agent Dashboard (port ' + (v.agent_port || '?') + ')';
+  linkDiv.appendChild(agentLink);
+  actionsSection.appendChild(linkDiv);
+
+  content.appendChild(actionsSection);
+
+  // Details section
+  var detailsSection = document.createElement('div');
+  detailsSection.className = 'drill-section';
+  var detailsTitle = document.createElement('h4');
+  detailsTitle.textContent = 'Details';
+  detailsSection.appendChild(detailsTitle);
+
+  var detailGrid = document.createElement('div');
+  detailGrid.className = 'detail-grid';
+  var details = [
+    { label: 'Vendor Code', value: v.vendor_code },
+    { label: 'Agent Name', value: v.agent_name || '-' },
+    { label: 'PM2 Name', value: v.pm2_name || '-' },
+    { label: 'SKU Range Start', value: v.sku_range_start || '-' },
+    { label: 'Catalog Table', value: v.catalog_table || '-' },
+    { label: 'Login URL', value: v.login_url || '-' },
+    { label: 'Website URL', value: v.website_url || '-' },
+    { label: 'Website Score', value: v.website_score ? v.website_score + '/10 — ' + (v.website_summary || '') : 'Not scored' },
+    { label: 'Tech Stack', value: v.website_tech_stack || '-' },
+    { label: 'Scored At', value: fmtDateFull(v.website_scored_at) },
+    { label: 'Crawl Cron', value: v.crawl_cron || 'Not set' },
+    { label: 'Next Crawl', value: fmtDateFull(v.next_scheduled_crawl) },
+    { label: 'Last Blog', value: fmtDateFull(v.last_blog_post) },
+    { label: 'Last Instagram', value: fmtDateFull(v.last_instagram_post) },
+    { label: 'Last Discontinued Check', value: fmtDateFull(v.last_discontinued_check) },
+    { label: 'Created', value: fmtDateFull(v.created_at) }
+  ];
+  for (var d = 0; d < details.length; d++) {
+    var item = document.createElement('div');
+    item.className = 'detail-item';
+    var lbl = document.createElement('span');
+    lbl.className = 'label';
+    lbl.textContent = details[d].label;
+    item.appendChild(lbl);
+    var val = document.createElement('span');
+    val.className = 'value';
+    val.textContent = details[d].value;
+    if (details[d].label === 'Login URL') {
+      val.style.fontSize = '11px';
+      val.style.wordBreak = 'break-all';
+    }
+    item.appendChild(val);
+    detailGrid.appendChild(item);
+  }
+  detailsSection.appendChild(detailGrid);
+  content.appendChild(detailsSection);
+
+  // Pricing & Discount section
+  var pricingSection = document.createElement('div');
+  pricingSection.className = 'drill-section';
+  pricingSection.style.gridColumn = '1 / -1';
+  var pricingTitle = document.createElement('h4');
+  pricingTitle.textContent = 'Pricing & Discount';
+  pricingSection.appendChild(pricingTitle);
+
+  var pricingGrid = document.createElement('div');
+  pricingGrid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:12px;';
+
+  // Discount %
+  var discBox = document.createElement('div');
+  discBox.style.cssText = 'background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;padding:12px;';
+  var discLabel = document.createElement('div');
+  discLabel.style.cssText = 'font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px;';
+  discLabel.textContent = 'Vendor Discount Off List';
+  discBox.appendChild(discLabel);
+  var discInput = document.createElement('input');
+  discInput.type = 'number';
+  discInput.id = 'discount-pct-' + v.vendor_code;
+  discInput.min = '0';
+  discInput.max = '100';
+  discInput.step = '0.5';
+  discInput.value = v.vendor_discount_pct != null ? v.vendor_discount_pct : '';
+  discInput.placeholder = 'e.g. 20';
+  discInput.style.cssText = 'width:80px;padding:6px 8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:16px;font-weight:600;';
+  discInput.addEventListener('click', function(e) { e.stopPropagation(); });
+  discBox.appendChild(discInput);
+  var discSuffix = document.createElement('span');
+  discSuffix.style.cssText = 'font-size:14px;color:var(--text-secondary);margin-left:4px;';
+  discSuffix.textContent = '% off list';
+  discBox.appendChild(discSuffix);
+  pricingGrid.appendChild(discBox);
+
+  // Pricing Unit
+  var unitBox = document.createElement('div');
+  unitBox.style.cssText = 'background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;padding:12px;';
+  var unitLabel = document.createElement('div');
+  unitLabel.style.cssText = 'font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px;';
+  unitLabel.textContent = 'Pricing Unit';
+  unitBox.appendChild(unitLabel);
+  var unitSelect = document.createElement('select');
+  unitSelect.id = 'pricing-unit-' + v.vendor_code;
+  unitSelect.style.cssText = 'padding:6px 8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:14px;';
+  var units = ['yard', 'roll', 'single roll', 'double roll', 'bolt', 'sqft', 'panel', 'box', 'meter', 'linear foot'];
+  for (var u = 0; u < units.length; u++) {
+    var opt = document.createElement('option');
+    opt.value = units[u];
+    opt.textContent = units[u].charAt(0).toUpperCase() + units[u].slice(1);
+    if ((v.pricing_unit || 'yard') === units[u]) opt.selected = true;
+    unitSelect.appendChild(opt);
+  }
+  unitSelect.addEventListener('click', function(e) { e.stopPropagation(); });
+  unitBox.appendChild(unitSelect);
+  pricingGrid.appendChild(unitBox);
+
+  // Formula preview (built with safe DOM methods)
+  var formulaBox = document.createElement('div');
+  formulaBox.style.cssText = 'background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;padding:12px;';
+  var formulaLabel = document.createElement('div');
+  formulaLabel.style.cssText = 'font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px;';
+  formulaLabel.textContent = 'Retail Price Formula';
+  formulaBox.appendChild(formulaLabel);
+  var formulaText = document.createElement('div');
+  formulaText.id = 'formula-preview-' + v.vendor_code;
+  formulaText.style.cssText = 'font-size:13px;color:var(--text);font-family:monospace;line-height:1.6;';
+
+  function buildFormulaDisplay(container, discPct) {
+    container.textContent = '';
+    if (discPct !== null && !isNaN(discPct) && discPct >= 0 && discPct <= 100) {
+      var costMult = (1 - discPct / 100).toFixed(2);
+      var line1 = document.createElement('div');
+      line1.textContent = 'List x ' + costMult + ' = ';
+      var costSpan = document.createElement('span');
+      costSpan.style.color = 'var(--cyan)';
+      costSpan.textContent = 'Cost';
+      line1.appendChild(costSpan);
+      container.appendChild(line1);
+
+      var line2 = document.createElement('div');
+      line2.textContent = 'Cost / 0.65 / 0.85 = ';
+      var retailSpan = document.createElement('span');
+      retailSpan.style.color = 'var(--green)';
+      retailSpan.textContent = 'Retail';
+      line2.appendChild(retailSpan);
+      container.appendChild(line2);
+
+      var example = document.createElement('div');
+      example.style.cssText = 'font-size:11px;color:var(--text-muted);margin-top:4px;';
+      var exRetail = (50 * (1 - discPct / 100) / 0.65 / 0.85).toFixed(2);
+      example.textContent = 'Example: $50 list -> $' + (50 * (1 - discPct / 100)).toFixed(2) + ' cost -> $' + exRetail + ' retail';
+      container.appendChild(example);
+    } else {
+      var warn = document.createElement('span');
+      warn.style.color = 'var(--yellow)';
+      warn.textContent = 'Set discount % to see formula';
+      container.appendChild(warn);
+    }
+  }
+
+  var disc = v.vendor_discount_pct != null ? parseFloat(v.vendor_discount_pct) : null;
+  buildFormulaDisplay(formulaText, disc);
+  formulaBox.appendChild(formulaText);
+  pricingGrid.appendChild(formulaBox);
+
+  // Sample price
+  var sampleBox = document.createElement('div');
+  sampleBox.style.cssText = 'background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;padding:12px;';
+  var sampleLabel = document.createElement('div');
+  sampleLabel.style.cssText = 'font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px;';
+  sampleLabel.textContent = 'Sample Price';
+  sampleBox.appendChild(sampleLabel);
+  var sampleInput = document.createElement('input');
+  sampleInput.type = 'number';
+  sampleInput.id = 'sample-price-' + v.vendor_code;
+  sampleInput.min = '0';
+  sampleInput.step = '0.25';
+  sampleInput.value = v.sample_price != null ? v.sample_price : '4.25';
+  sampleInput.style.cssText = 'width:80px;padding:6px 8px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:16px;font-weight:600;';
+  sampleInput.addEventListener('click', function(e) { e.stopPropagation(); });
+  sampleBox.appendChild(sampleInput);
+  pricingGrid.appendChild(sampleBox);
+
+  pricingSection.appendChild(pricingGrid);
+
+  // Live formula updater
+  discInput.addEventListener('input', (function(code) {
+    return function() {
+      var d = parseFloat(document.getElementById('discount-pct-' + code).value);
+      var preview = document.getElementById('formula-preview-' + code);
+      buildFormulaDisplay(preview, isNaN(d) ? null : d);
+    };
+  })(v.vendor_code));
+
+  // Pricing notes
+  var notesDiv = document.createElement('div');
+  notesDiv.style.cssText = 'margin-top:8px;';
+  var notesLabel2 = document.createElement('div');
+  notesLabel2.style.cssText = 'font-size:10px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px;';
+  notesLabel2.textContent = 'Pricing Notes (minimums, freight, terms)';
+  notesDiv.appendChild(notesLabel2);
+  var notesInput = document.createElement('input');
+  notesInput.type = 'text';
+  notesInput.id = 'pricing-notes-' + v.vendor_code;
+  notesInput.value = v.pricing_notes || '';
+  notesInput.placeholder = 'e.g. Net 30, $500 min order, free freight over $2000...';
+  notesInput.style.cssText = 'width:100%;padding:8px 10px;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-size:13px;box-sizing:border-box;';
+  notesInput.addEventListener('click', function(e) { e.stopPropagation(); });
+  notesDiv.appendChild(notesInput);
+  pricingSection.appendChild(notesDiv);
+
+  // Save button
+  var savePricingBtn = document.createElement('button');
+  savePricingBtn.className = 'action-btn';
+  savePricingBtn.textContent = 'Save Pricing Settings';
+  savePricingBtn.style.cssText = 'margin-top:10px;font-size:12px;padding:8px 20px;background:var(--green);color:#000;font-weight:600;';
+  savePricingBtn.addEventListener('click', (function(code) {
+    return function(e) {
+      e.stopPropagation();
+      var discVal = document.getElementById('discount-pct-' + code).value;
+      var unitVal = document.getElementById('pricing-unit-' + code).value;
+      var notesVal = document.getElementById('pricing-notes-' + code).value;
+      var sampleVal = document.getElementById('sample-price-' + code).value;
+      var body = {};
+      body.vendor_discount_pct = discVal !== '' ? parseFloat(discVal) : null;
+      body.pricing_unit = unitVal;
+      body.pricing_notes = notesVal;
+      body.sample_price = sampleVal !== '' ? parseFloat(sampleVal) : 4.25;
+      apiCall('/api/vendors/' + encodeURIComponent(code), {
+        method: 'PUT',
+        body: JSON.stringify(body)
+      }).then(function(res) {
+        if (res.success) {
+          savePricingBtn.textContent = 'Saved!';
+          savePricingBtn.style.background = 'var(--cyan)';
+          setTimeout(function() { savePricingBtn.textContent = 'Save Pricing Settings'; savePricingBtn.style.background = 'var(--green)'; }, 2000);
+        } else {
+          savePricingBtn.textContent = 'Error: ' + (res.error || 'Failed');
+          savePricingBtn.style.background = 'var(--red)';
+          setTimeout(function() { savePricingBtn.textContent = 'Save Pricing Settings'; savePricingBtn.style.background = 'var(--green)'; }, 3000);
+        }
+      });
+    };
+  })(v.vendor_code));
+  pricingSection.appendChild(savePricingBtn);
+
+  content.appendChild(pricingSection);
+
+  // Learnings section (full width)
+  var learningsSection = document.createElement('div');
+  learningsSection.className = 'drill-section';
+  learningsSection.style.gridColumn = '1 / -1';
+  var learningsTitle = document.createElement('h4');
+  learningsTitle.textContent = 'Learnings & Notes';
+  learningsSection.appendChild(learningsTitle);
+
+  var textarea = document.createElement('textarea');
+  textarea.className = 'learnings-textarea';
+  textarea.id = 'learnings-' + v.vendor_code;
+  textarea.placeholder = 'Add vendor notes, observations, issues...';
+  textarea.value = v.learnings || '';
+  textarea.addEventListener('blur', (function(code) {
+    return function(e) { saveLearnings(code, e.target.value); };
+  })(v.vendor_code));
+  textarea.addEventListener('click', function(e) { e.stopPropagation(); });
+  learningsSection.appendChild(textarea);
+
+  var hint = document.createElement('div');
+  hint.style.fontSize = '10px';
+  hint.style.color = 'var(--text-muted)';
+  hint.style.marginTop = '4px';
+  hint.textContent = 'Auto-saves on blur';
+  learningsSection.appendChild(hint);
+
+  content.appendChild(learningsSection);
+
+  // Agent Chat section (full width)
+  var chatSection = document.createElement('div');
+  chatSection.className = 'drill-section';
+  chatSection.style.gridColumn = '1 / -1';
+  var chatTitle = document.createElement('h4');
+  chatTitle.textContent = 'Chat with ' + (v.agent_name || v.vendor_code);
+  chatSection.appendChild(chatTitle);
+
+  var chatMessages = document.createElement('div');
+  chatMessages.id = 'chat-messages-' + v.vendor_code;
+  chatMessages.style.cssText = 'max-height:250px;overflow-y:auto;margin-bottom:8px;padding:8px;background:var(--bg-primary);border-radius:6px;border:1px solid var(--border);min-height:60px;';
+  var chatPlaceholder = document.createElement('div');
+  chatPlaceholder.style.cssText = 'color:var(--text-muted);font-size:12px;padding:8px;font-style:italic;';
+  chatPlaceholder.textContent = 'Ask anything about this vendor — e.g. "why only ' + (v.catalog_count || 0) + ' items?" or "what specs are missing?"';
+  chatMessages.appendChild(chatPlaceholder);
+  chatSection.appendChild(chatMessages);
+
+  var chatInputDiv = document.createElement('div');
+  chatInputDiv.style.cssText = 'display:flex;gap:8px;';
+  var chatInput = document.createElement('input');
+  chatInput.type = 'text';
+  chatInput.id = 'chat-input-' + v.vendor_code;
+  chatInput.placeholder = 'Ask ' + (v.agent_name || 'agent') + ' a question...';
+  chatInput.style.cssText = 'flex:1;padding:8px 12px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;';
+  chatInput.addEventListener('click', function(e) { e.stopPropagation(); });
+  chatInput.addEventListener('keydown', (function(code) {
+    return function(e) { if (e.key === 'Enter') sendChat(code); };
+  })(v.vendor_code));
+  chatInputDiv.appendChild(chatInput);
+  var chatSendBtn = document.createElement('button');
+  chatSendBtn.className = 'action-btn';
+  chatSendBtn.textContent = 'Send';
+  chatSendBtn.style.cssText = 'font-size:12px;padding:8px 16px;';
+  chatSendBtn.addEventListener('click', (function(code) {
+    return function(e) { e.stopPropagation(); sendChat(code); };
+  })(v.vendor_code));
+  chatInputDiv.appendChild(chatSendBtn);
+  chatSection.appendChild(chatInputDiv);
+  content.appendChild(chatSection);
+
+  // Sample Product section (full width)
+  var sampleSection = document.createElement('div');
+  sampleSection.className = 'drill-section';
+  sampleSection.style.gridColumn = '1 / -1';
+  var sampleTitle = document.createElement('h4');
+  sampleTitle.textContent = 'Sample Product (Best Populated)';
+  sampleSection.appendChild(sampleTitle);
+  var sampleContainer = document.createElement('div');
+  sampleContainer.id = 'sample-product-' + v.vendor_code;
+  sampleContainer.style.cssText = 'padding:8px;';
+  var sampleLoading = document.createElement('div');
+  sampleLoading.style.cssText = 'color:var(--text-muted);font-size:12px;';
+  sampleLoading.textContent = 'Loading sample product...';
+  sampleContainer.appendChild(sampleLoading);
+  sampleSection.appendChild(sampleContainer);
+
+  // Rescrape Schema button
+  var rescrapeDiv = document.createElement('div');
+  rescrapeDiv.style.cssText = 'margin-top:8px;display:flex;gap:8px;align-items:center;';
+  var rescrapeBtn = document.createElement('button');
+  rescrapeBtn.className = 'action-btn';
+  rescrapeBtn.textContent = 'Rescrape & Refresh Schema';
+  rescrapeBtn.style.fontSize = '11px';
+  rescrapeBtn.addEventListener('click', (function(code) {
+    return function(e) {
+      e.stopPropagation();
+      rescrapeBtn.classList.add('loading');
+      vendorAction(code, 'scan', rescrapeBtn);
+      setTimeout(function() { loadSampleProduct(code); }, 3000);
+    };
+  })(v.vendor_code));
+  rescrapeDiv.appendChild(rescrapeBtn);
+  var schemaHint = document.createElement('span');
+  schemaHint.style.cssText = 'font-size:10px;color:var(--text-muted);';
+  schemaHint.textContent = 'Triggers a full rescrape then reloads the sample product';
+  rescrapeDiv.appendChild(schemaHint);
+  sampleSection.appendChild(rescrapeDiv);
+  content.appendChild(sampleSection);
+
+  // NOTE: loadSampleProduct is called lazily from toggleExpand, not here
+
+  // Observations section (full width)
+  var obsSection = document.createElement('div');
+  obsSection.className = 'drill-section';
+  obsSection.style.gridColumn = '1 / -1';
+  var obsTitle = document.createElement('h4');
+  obsTitle.textContent = 'Recent Observations';
+  obsSection.appendChild(obsTitle);
+  var obsList = document.createElement('div');
+  obsList.id = 'obs-' + v.vendor_code;
+  obsList.style.maxHeight = '200px';
+  obsList.style.overflowY = 'auto';
+  var obsLoading = document.createElement('div');
+  obsLoading.style.color = 'var(--text-muted)';
+  obsLoading.style.fontSize = '12px';
+  obsLoading.style.padding = '8px';
+  obsLoading.textContent = 'Loading observations...';
+  obsList.appendChild(obsLoading);
+  obsSection.appendChild(obsList);
+  content.appendChild(obsSection);
+
+  // Products section (full width)
+  var productsSection = document.createElement('div');
+  productsSection.className = 'drill-section';
+  productsSection.style.gridColumn = '1 / -1';
+  var productsTitle = document.createElement('h4');
+  productsTitle.textContent = 'Products (' + (v.total_products || 0) + ')';
+  productsSection.appendChild(productsTitle);
+
+  var prodSearchDiv = document.createElement('div');
+  prodSearchDiv.style.display = 'flex';
+  prodSearchDiv.style.gap = '8px';
+  prodSearchDiv.style.marginBottom = '8px';
+  var prodSearchInput = document.createElement('input');
+  prodSearchInput.type = 'text';
+  prodSearchInput.placeholder = 'Search products by title or SKU...';
+  prodSearchInput.style.cssText = 'flex:1;padding:6px 10px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:12px;';
+  prodSearchInput.id = 'prod-search-' + v.vendor_code;
+  prodSearchInput.addEventListener('click', function(e) { e.stopPropagation(); });
+  prodSearchDiv.appendChild(prodSearchInput);
+  var prodSearchBtn = document.createElement('button');
+  prodSearchBtn.className = 'action-btn';
+  prodSearchBtn.textContent = 'Load Products';
+  prodSearchBtn.style.fontSize = '11px';
+  prodSearchBtn.addEventListener('click', (function(code) {
+    return function(e) {
+      e.stopPropagation();
+      loadProducts(code, 0);
+    };
+  })(v.vendor_code));
+  prodSearchDiv.appendChild(prodSearchBtn);
+  productsSection.appendChild(prodSearchDiv);
+
+  var prodContainer = document.createElement('div');
+  prodContainer.id = 'products-' + v.vendor_code;
+  prodContainer.style.maxHeight = '400px';
+  prodContainer.style.overflowY = 'auto';
+  var prodHint = document.createElement('div');
+  prodHint.style.color = 'var(--text-muted)';
+  prodHint.style.fontSize = '12px';
+  prodHint.style.padding = '8px';
+  prodHint.textContent = 'Click "Load Products" to view Shopify product data';
+  prodContainer.appendChild(prodHint);
+  productsSection.appendChild(prodContainer);
+  content.appendChild(productsSection);
+
+  container.appendChild(content);
+}
+
+// Load products for a vendor
+function loadProducts(code, offset) {
+  var el = document.getElementById('products-' + code);
+  if (!el) return;
+  var searchVal = '';
+  var searchInput = document.getElementById('prod-search-' + code);
+  if (searchInput) searchVal = searchInput.value;
+  var url = '/api/vendors/' + encodeURIComponent(code) + '/products?limit=50&offset=' + offset;
+  if (searchVal) url += '&search=' + encodeURIComponent(searchVal);
+
+  if (offset === 0) {
+    while (el.firstChild) el.removeChild(el.firstChild);
+    var loading = document.createElement('div');
+    loading.style.cssText = 'color:var(--text-muted);font-size:12px;padding:8px;';
+    loading.textContent = 'Loading products...';
+    el.appendChild(loading);
+  }
+
+  apiCall(url).then(function(res) {
+    if (offset === 0) while (el.firstChild) el.removeChild(el.firstChild);
+
+    if (!res.data || res.data.length === 0) {
+      if (offset === 0) {
+        var msg = document.createElement('div');
+        msg.style.cssText = 'color:var(--text-muted);font-size:12px;padding:8px;';
+        msg.textContent = res.message || 'No products found';
+        el.appendChild(msg);
+      }
+      return;
+    }
+
+    // Summary bar
+    if (offset === 0) {
+      var summary = document.createElement('div');
+      summary.style.cssText = 'padding:6px 10px;background:var(--surface);border-radius:6px;margin-bottom:8px;font-size:12px;color:var(--text-secondary);';
+      summary.textContent = res.count + ' total products for ' + res.vendor + ' | Showing ' + Math.min(res.data.length, 50) + ' of ' + res.count;
+      el.appendChild(summary);
+    }
+
+    // Product table
+    var table = document.createElement('table');
+    table.style.cssText = 'width:100%;border-collapse:collapse;font-size:11px;';
+    if (offset === 0) {
+      var thead = document.createElement('thead');
+      var headerRow = document.createElement('tr');
+      ['Image','Title','Vendor','SKU','DW SKU','Cost','Retail','Price','Status'].forEach(function(h) {
+        var th = document.createElement('th');
+        th.style.cssText = 'padding:6px 8px;text-align:left;border-bottom:1px solid var(--border);color:var(--text-muted);font-weight:600;white-space:nowrap;';
+        th.textContent = h;
+        headerRow.appendChild(th);
+      });
+      thead.appendChild(headerRow);
+      table.appendChild(thead);
+    }
+    var tbod = document.createElement('tbody');
+    for (var i = 0; i < res.data.length; i++) {
+      var p = res.data[i];
+      var row = document.createElement('tr');
+      row.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
+
+      // Image cell
+      var imgTd = document.createElement('td');
+      imgTd.style.padding = '4px';
+      if (p.image_url) {
+        var img = document.createElement('img');
+        img.src = p.image_url;
+        img.style.cssText = 'width:40px;height:40px;object-fit:cover;border-radius:4px;';
+        img.loading = 'lazy';
+        imgTd.appendChild(img);
+      } else {
+        var noImg = document.createElement('span');
+        noImg.style.cssText = 'color:var(--text-muted);font-size:10px;';
+        noImg.textContent = 'No img';
+        imgTd.appendChild(noImg);
+      }
+      row.appendChild(imgTd);
+
+      // Title
+      var titleTd = document.createElement('td');
+      titleTd.style.cssText = 'padding:4px 8px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
+      titleTd.textContent = p.title || '-';
+      titleTd.title = p.title || '';
+      row.appendChild(titleTd);
+
+      // Vendor
+      var vendTd = document.createElement('td');
+      vendTd.style.cssText = 'padding:4px 8px;font-size:10px;color:var(--text-muted);max-width:120px;overflow:hidden;text-overflow:ellipsis;';
+      vendTd.textContent = p.vendor || '-';
+      row.appendChild(vendTd);
+
+      // SKU
+      var skuTd = document.createElement('td');
+      skuTd.style.cssText = 'padding:4px 8px;font-family:monospace;font-size:10px;';
+      skuTd.textContent = p.sku || p.mfr_sku || '-';
+      row.appendChild(skuTd);
+
+      // DW SKU
+      var dwSkuTd = document.createElement('td');
+      dwSkuTd.style.cssText = 'padding:4px 8px;font-family:monospace;font-size:10px;color:var(--accent);';
+      dwSkuTd.textContent = p.dw_sku || '-';
+      row.appendChild(dwSkuTd);
+
+      // Cost
+      var costTd = document.createElement('td');
+      costTd.style.cssText = 'padding:4px 8px;';
+      if (p.cost_price && parseFloat(p.cost_price) > 0) {
+        costTd.style.color = 'var(--green)';
+        costTd.textContent = '$' + parseFloat(p.cost_price).toFixed(2);
+      } else {
+        costTd.style.color = 'var(--text-muted)';
+        costTd.textContent = '-';
+      }
+      row.appendChild(costTd);
+
+      // Retail
+      var retailTd = document.createElement('td');
+      retailTd.style.cssText = 'padding:4px 8px;';
+      retailTd.textContent = p.retail_price ? '$' + parseFloat(p.retail_price).toFixed(2) : '-';
+      row.appendChild(retailTd);
+
+      // Price
+      var priceTd = document.createElement('td');
+      priceTd.style.cssText = 'padding:4px 8px;';
+      priceTd.textContent = p.price ? '$' + parseFloat(p.price).toFixed(2) : '-';
+      row.appendChild(priceTd);
+
+      // Status
+      var statusTd = document.createElement('td');
+      statusTd.style.cssText = 'padding:4px 8px;font-size:10px;';
+      statusTd.textContent = p.status || '-';
+      if (p.status === 'active') statusTd.style.color = 'var(--green)';
+      else if (p.status === 'draft') statusTd.style.color = 'var(--orange)';
+      else statusTd.style.color = 'var(--text-muted)';
+      row.appendChild(statusTd);
+
+      tbod.appendChild(row);
+    }
+    table.appendChild(tbod);
+    el.appendChild(table);
+
+    // Load more button
+    if (res.data.length >= 50 && (offset + 50) < res.count) {
+      var moreBtn = document.createElement('button');
+      moreBtn.className = 'action-btn';
+      moreBtn.style.cssText = 'margin:8px 0;font-size:11px;width:100%;';
+      moreBtn.textContent = 'Load more (' + (res.count - offset - 50) + ' remaining)';
+      moreBtn.addEventListener('click', (function(c, o) {
+        return function(e) {
+          e.stopPropagation();
+          e.target.remove();
+          loadProducts(c, o);
+        };
+      })(code, offset + 50));
+      el.appendChild(moreBtn);
+    }
+  });
+}
+
+// Toggle expand
+function toggleExpand(code) {
+  if (expandedRow === code) {
+    expandedRow = null;
+  } else {
+    expandedRow = code;
+  }
+  applyFilters();
+  if (expandedRow === code) {
+    loadObservations(code);
+    loadSampleProduct(code);
+    loadProducts(code, 0);
+  }
+}
+
+// Send chat message to vendor agent
+function sendChat(code) {
+  var input = document.getElementById('chat-input-' + code);
+  var container = document.getElementById('chat-messages-' + code);
+  if (!input || !container) return;
+  var msg = input.value.trim();
+  if (!msg) return;
+  input.value = '';
+
+  // Clear placeholder
+  var placeholder = container.querySelector('[style*="font-style:italic"]');
+  if (placeholder) placeholder.remove();
+
+  // Add user message
+  var userDiv = document.createElement('div');
+  userDiv.style.cssText = 'margin-bottom:8px;text-align:right;';
+  var userBubble = document.createElement('span');
+  userBubble.style.cssText = 'display:inline-block;padding:6px 12px;background:var(--accent);color:#fff;border-radius:12px 12px 2px 12px;font-size:12px;max-width:80%;text-align:left;';
+  userBubble.textContent = msg;
+  userDiv.appendChild(userBubble);
+  container.appendChild(userDiv);
+
+  // Add typing indicator
+  var typingDiv = document.createElement('div');
+  typingDiv.id = 'typing-' + code;
+  typingDiv.style.cssText = 'margin-bottom:8px;';
+  var typingBubble = document.createElement('span');
+  typingBubble.style.cssText = 'display:inline-block;padding:6px 12px;background:var(--bg-input);border-radius:12px 12px 12px 2px;font-size:12px;color:var(--text-muted);';
+  typingBubble.textContent = 'Thinking...';
+  typingDiv.appendChild(typingBubble);
+  container.appendChild(typingDiv);
+  container.scrollTop = container.scrollHeight;
+
+  apiCall('/api/vendors/' + encodeURIComponent(code) + '/chat', {
+    method: 'POST',
+    body: JSON.stringify({ message: msg })
+  }).then(function(res) {
+    var typing = document.getElementById('typing-' + code);
+    if (typing) typing.remove();
+    var respDiv = document.createElement('div');
+    respDiv.style.cssText = 'margin-bottom:8px;';
+    var respBubble = document.createElement('span');
+    respBubble.style.cssText = 'display:inline-block;padding:8px 12px;background:var(--bg-input);border-radius:12px 12px 12px 2px;font-size:12px;max-width:90%;white-space:pre-wrap;line-height:1.5;color:var(--text-primary);';
+    if (res.success) {
+      respBubble.textContent = res.response;
+      var sourceTag = document.createElement('div');
+      sourceTag.style.cssText = 'font-size:9px;color:var(--text-muted);margin-top:4px;';
+      sourceTag.textContent = 'Source: ' + (res.source === 'agent' ? 'Agent direct' : 'Catalog data');
+      respDiv.appendChild(respBubble);
+      respDiv.appendChild(sourceTag);
+    } else {
+      respBubble.style.color = 'var(--red)';
+      respBubble.textContent = 'Error: ' + (res.error || 'Failed to get response');
+      respDiv.appendChild(respBubble);
+    }
+    container.appendChild(respDiv);
+    container.scrollTop = container.scrollHeight;
+  }).catch(function(err) {
+    var typing = document.getElementById('typing-' + code);
+    if (typing) typing.remove();
+    var errDiv = document.createElement('div');
+    errDiv.style.cssText = 'margin-bottom:8px;';
+    var errBubble = document.createElement('span');
+    errBubble.style.cssText = 'display:inline-block;padding:6px 12px;background:rgba(239,68,68,0.1);border-radius:12px;font-size:12px;color:var(--red);';
+    errBubble.textContent = 'Error: ' + err.message;
+    errDiv.appendChild(errBubble);
+    container.appendChild(errDiv);
+  });
+}
+
+// Load sample product card for a vendor
+function loadSampleProduct(code) {
+  var container = document.getElementById('sample-product-' + code);
+  if (!container) return;
+  apiCall('/api/vendors/' + encodeURIComponent(code) + '/sample-product')
+    .then(function(res) {
+      container.textContent = '';
+      if (!res.success || !res.data) {
+        container.style.cssText = 'color:var(--text-muted);font-size:12px;padding:8px;';
+        container.textContent = res.message || 'No catalog data found';
+        return;
+      }
+      var p = res.data;
+      var schema = res.schema || [];
+
+      // Card layout: image left, fields right
+      var card = document.createElement('div');
+      card.style.cssText = 'display:flex;gap:16px;flex-wrap:wrap;';
+
+      // Image
+      if (p.image_url) {
+        var imgWrap = document.createElement('div');
+        imgWrap.style.cssText = 'flex-shrink:0;width:180px;';
+        var img = document.createElement('img');
+        img.src = p.image_url;
+        img.alt = p.pattern_name || p.mfr_sku || 'Product';
+        img.style.cssText = 'width:180px;height:auto;border-radius:8px;border:1px solid var(--border);object-fit:cover;max-height:240px;';
+        img.onerror = function() { this.style.display = 'none'; };
+        imgWrap.appendChild(img);
+
+        // Show additional images count if available
+        if (p.all_images) {
+          try {
+            var allImgs = JSON.parse(p.all_images);
+            if (allImgs && allImgs.length > 1) {
+              var imgCount = document.createElement('div');
+              imgCount.style.cssText = 'font-size:10px;color:var(--text-muted);margin-top:4px;text-align:center;';
+              imgCount.textContent = '+' + (allImgs.length - 1) + ' more images';
+              imgWrap.appendChild(imgCount);
+            }
+          } catch (e) {}
+        }
+        card.appendChild(imgWrap);
+      }
+
+      // Fields grid
+      var fieldsWrap = document.createElement('div');
+      fieldsWrap.style.cssText = 'flex:1;min-width:300px;';
+
+      // Product title
+      var titleDiv = document.createElement('div');
+      titleDiv.style.cssText = 'font-size:14px;font-weight:700;margin-bottom:8px;color:var(--text-primary);';
+      titleDiv.textContent = (p.pattern_name || '') + (p.color_name ? ' — ' + p.color_name : '') + (p.mfr_sku ? ' (' + p.mfr_sku + ')' : '');
+      fieldsWrap.appendChild(titleDiv);
+
+      // Schema coverage grid
+      var fieldGrid = document.createElement('div');
+      fieldGrid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:4px 12px;';
+
+      var skipFields = ['id', 'created_at', 'updated_at', 'last_scraped'];
+      for (var i = 0; i < schema.length; i++) {
+        var col = schema[i].column_name;
+        if (skipFields.indexOf(col) >= 0) continue;
+        var val = p[col];
+        var hasValue = val !== null && val !== undefined && val !== '';
+        var fieldDiv = document.createElement('div');
+        fieldDiv.style.cssText = 'display:flex;align-items:baseline;gap:4px;font-size:11px;padding:2px 0;';
+        var dot = document.createElement('span');
+        dot.style.cssText = 'width:6px;height:6px;border-radius:50%;flex-shrink:0;display:inline-block;margin-top:3px;' + (hasValue ? 'background:var(--green);' : 'background:var(--red);opacity:0.5;');
+        fieldDiv.appendChild(dot);
+        var fieldLabel = document.createElement('span');
+        fieldLabel.style.cssText = 'color:var(--text-muted);min-width:80px;';
+        fieldLabel.textContent = col;
+        fieldDiv.appendChild(fieldLabel);
+        var fieldVal = document.createElement('span');
+        fieldVal.style.cssText = 'color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px;';
+        fieldVal.textContent = hasValue ? String(val).substring(0, 60) : '—';
+        fieldVal.title = hasValue ? String(val) : 'Empty';
+        fieldDiv.appendChild(fieldVal);
+        fieldGrid.appendChild(fieldDiv);
+      }
+      fieldsWrap.appendChild(fieldGrid);
+
+      // Schema summary
+      var filled = schema.filter(function(s) { var v2 = p[s.column_name]; return v2 !== null && v2 !== undefined && v2 !== ''; }).length;
+      var summaryDiv = document.createElement('div');
+      summaryDiv.style.cssText = 'margin-top:8px;font-size:11px;color:var(--text-muted);border-top:1px solid var(--border);padding-top:6px;';
+      summaryDiv.textContent = 'Schema: ' + filled + '/' + schema.length + ' fields populated (' + Math.round(filled/schema.length*100) + '%) · Table: ' + (res.vendor || '');
+      fieldsWrap.appendChild(summaryDiv);
+
+      card.appendChild(fieldsWrap);
+      container.appendChild(card);
+    })
+    .catch(function(err) {
+      container.textContent = '';
+      container.style.cssText = 'color:var(--red);font-size:12px;padding:8px;';
+      container.textContent = 'Error loading sample: ' + err.message;
+    });
+}
+
+// Load observations for a vendor
+function loadObservations(code) {
+  var el = document.getElementById('obs-' + code);
+  if (!el) return;
+  apiCall('/api/vendors/' + encodeURIComponent(code) + '/observations')
+    .then(function(res) {
+      while (el.firstChild) el.removeChild(el.firstChild);
+      var obs = res.data || [];
+      if (Array.isArray(obs) && obs.length > 0) {
+        for (var i = 0; i < Math.min(obs.length, 20); i++) {
+          var item = document.createElement('div');
+          item.style.padding = '6px 8px';
+          item.style.borderBottom = '1px solid var(--border)';
+          item.style.fontSize = '12px';
+          item.style.color = 'var(--text-secondary)';
+          var text = typeof obs[i] === 'string' ? obs[i] : (obs[i].message || obs[i].text || JSON.stringify(obs[i]));
+          item.textContent = text;
+          el.appendChild(item);
+        }
+      } else {
+        var msg = document.createElement('div');
+        msg.style.color = 'var(--text-muted)';
+        msg.style.fontSize = '12px';
+        msg.style.padding = '8px';
+        msg.textContent = res.message || 'No observations available';
+        el.appendChild(msg);
+      }
+    })
+    .catch(function(err) {
+      while (el.firstChild) el.removeChild(el.firstChild);
+      var errMsg = document.createElement('div');
+      errMsg.style.color = 'var(--text-muted)';
+      errMsg.style.fontSize = '12px';
+      errMsg.style.padding = '8px';
+      errMsg.textContent = 'Failed to load: ' + err.message;
+      el.appendChild(errMsg);
+    });
+}
+
+// Select
+function toggleSelect(code, checked) {
+  if (checked) selectedVendors[code] = true;
+  else delete selectedVendors[code];
+  updateBulkUI();
+}
+
+function toggleSelectAll(checked) {
+  var rows = document.querySelectorAll('#vendorTableBody > tr:not(.drill-down)');
+  for (var i = 0; i < rows.length; i++) {
+    var cb = rows[i].querySelector('.cb-cell input[type="checkbox"]');
+    if (cb) {
+      var code = rows[i].getAttribute('data-code');
+      if (code) {
+        if (checked) selectedVendors[code] = true;
+        else delete selectedVendors[code];
+        cb.checked = checked;
+      }
+    }
+  }
+  updateBulkUI();
+}
+
+function updateBulkUI() {
+  var cnt = Object.keys(selectedVendors).length;
+  document.getElementById('selectedCount').textContent = cnt > 0 ? cnt + ' selected' : '';
+  document.getElementById('bulkCrawlBtn').disabled = cnt === 0;
+  var ltBtn = document.getElementById('launchTestBtn');
+  if (ltBtn) {
+    ltBtn.disabled = cnt === 0;
+    ltBtn.textContent = cnt > 0 ? 'Launch 1 Test SKU (' + cnt + ')' : 'Launch 1 Test SKU';
+  }
+}
+
+function launchTestSelected() {
+  var codes = Object.keys(selectedVendors);
+  if (!codes.length) return showToast('Select vendors first', 'error');
+  if (!confirm('Launch 1 newest SKU per vendor to Shopify for ' + codes.length + ' vendor(s)?\\n\\nEach goes through Silas validation. Products created as ACTIVE with metafields.')) return;
+
+  var btn = document.getElementById('launchTestBtn');
+  btn.disabled = true;
+  btn.textContent = 'Launching...';
+
+  apiCall('/api/launch-test', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ vendor_codes: codes })
+  }).then(function(res) {
+    if (res.success) {
+      var msg = 'Launched ' + res.launched + '/' + res.total + ' vendors';
+      showToast(msg, res.launched > 0 ? 'success' : 'warning');
+      // Show details
+      var details = res.results.map(function(r) {
+        return r.vendor + ': ' + r.status + (r.sku ? ' (' + r.sku + ')' : '') + (r.reason ? ' - ' + r.reason : '');
+      }).join('\\n');
+      alert('Launch Test Results:\\n\\n' + details);
+    } else {
+      showToast('Launch failed: ' + (res.error || 'Unknown'), 'error');
+    }
+    btn.textContent = 'Launch 1 Test SKU';
+    btn.disabled = false;
+    updateBulkUI();
+  }).catch(function(err) {
+    showToast('Error: ' + err.message, 'error');
+    btn.textContent = 'Launch 1 Test SKU';
+    btn.disabled = false;
+  });
+}
+
+// Vendor action
+function vendorAction(code, action, btn) {
+  btn.classList.add('loading');
+  var path;
+  if (action === 'score-website') {
+    path = '/api/vendors/' + encodeURIComponent(code) + '/score';
+  } else if (action === 'update-prices') {
+    path = '/api/vendors/' + encodeURIComponent(code) + '/update-prices';
+  } else {
+    path = '/api/vendors/' + encodeURIComponent(code) + '/scan';
+  }
+  apiCall(path, { method: 'POST', body: JSON.stringify({ action: action }) })
+    .then(function(res) {
+      if (res.success) {
+        if (action === 'score-website' && res.data) {
+          showToast(code + ': Score ' + res.data.score + '/10 — ' + (res.data.summary || ''), 'success');
+          loadVendors(); // Refresh to show new score
+        } else {
+          showToast(code + ': ' + action + ' sent to ' + (res.agent || 'agent'), 'success');
+        }
+      } else {
+        showToast(code + ': ' + (res.error || 'Failed'), 'error');
+      }
+      btn.classList.remove('loading');
+    })
+    .catch(function(err) {
+      showToast(code + ': ' + err.message, 'error');
+      btn.classList.remove('loading');
+    });
+}
+
+// Bulk action
+function bulkAction(action) {
+  var codes = Object.keys(selectedVendors);
+  if (codes.length === 0) return;
+  showToast('Sending ' + action + ' to ' + codes.length + ' vendors...', 'info');
+
+  if (action === 'scan') {
+    // Use the dedicated bulk-scan endpoint
+    apiCall('/api/bulk-scan', {
+      method: 'POST',
+      body: JSON.stringify({ codes: codes })
+    }).then(function(res) {
+      if (res.success) {
+        var triggered = 0, failed = 0;
+        for (var i = 0; i < (res.results || []).length; i++) {
+          if (res.results[i].status === 'triggered') triggered++;
+          else failed++;
+        }
+        showToast('Bulk scan: ' + triggered + ' triggered, ' + failed + ' failed/unreachable', triggered > 0 ? 'success' : 'error');
+      }
+    }).catch(function(err) {
+      showToast('Bulk scan failed: ' + err.message, 'error');
+    });
+    return;
+  }
+
+  // Fallback to bulk-action for other action types
+  apiCall('/api/bulk-action', {
+    method: 'POST',
+    body: JSON.stringify({ action: action, vendorCodes: codes })
+  }).then(function(res) {
+    if (res.success) {
+      var ok = 0, fail = 0;
+      for (var i = 0; i < res.results.length; i++) {
+        if (res.results[i].success) ok++;
+        else fail++;
+      }
+      showToast('Bulk ' + action + ': ' + ok + ' success, ' + fail + ' failed', ok > 0 ? 'success' : 'error');
+    }
+  }).catch(function(err) {
+    showToast('Bulk action failed: ' + err.message, 'error');
+  });
+}
+
+// Private Label updates
+function updateBrandsPage(code, checked) {
+  apiCall('/api/vendors/' + encodeURIComponent(code), {
+    method: 'PUT',
+    body: JSON.stringify({ show_on_brands_page: checked })
+  }).then(function(res) {
+    if (res.success) {
+      for (var i = 0; i < vendors.length; i++) {
+        if (vendors[i].vendor_code === code) vendors[i].show_on_brands_page = checked;
+      }
+      showToast(code + ': Brands page ' + (checked ? 'added' : 'removed'), 'success');
+    }
+  }).catch(function(err) {
+    showToast('Failed to update: ' + err.message, 'error');
+  });
+}
+
+function updatePL(code, checked) {
+  apiCall('/api/vendors/' + encodeURIComponent(code), {
+    method: 'PUT',
+    body: JSON.stringify({ is_private_label: checked })
+  }).then(function(res) {
+    if (res.success) {
+      for (var i = 0; i < vendors.length; i++) {
+        if (vendors[i].vendor_code === code) vendors[i].is_private_label = checked;
+      }
+      applyFilters();
+      showToast(code + ': Private Label ' + (checked ? 'enabled' : 'disabled'), 'success');
+    }
+  }).catch(function(err) {
+    showToast('Failed to update: ' + err.message, 'error');
+  });
+}
+
+function updatePLName(code, name) {
+  apiCall('/api/vendors/' + encodeURIComponent(code), {
+    method: 'PUT',
+    body: JSON.stringify({ private_label_name: name })
+  }).then(function(res) {
+    if (res.success) {
+      for (var i = 0; i < vendors.length; i++) {
+        if (vendors[i].vendor_code === code) vendors[i].private_label_name = name;
+      }
+      showToast(code + ': Label name saved', 'success');
+    }
+  }).catch(function(err) {
+    showToast('Failed to save label name: ' + err.message, 'error');
+  });
+}
+
+// Generic field updater (used for display_prices, sample_price, etc.)
+function updateField(code, field, value) {
+  var body = {};
+  body[field] = value;
+  apiCall('/api/vendors/' + encodeURIComponent(code), {
+    method: 'PUT',
+    body: JSON.stringify(body)
+  }).then(function(res) {
+    if (res.success) {
+      for (var i = 0; i < vendors.length; i++) {
+        if (vendors[i].vendor_code === code) vendors[i][field] = value;
+      }
+      showToast(code + ': ' + field + ' updated', 'success');
+    }
+  }).catch(function(err) {
+    showToast('Failed to update ' + field + ': ' + err.message, 'error');
+  });
+}
+
+// Learnings
+function saveLearnings(code, text) {
+  apiCall('/api/vendors/' + encodeURIComponent(code), {
+    method: 'PUT',
+    body: JSON.stringify({ learnings: text })
+  }).then(function(res) {
+    if (res.success) {
+      for (var i = 0; i < vendors.length; i++) {
+        if (vendors[i].vendor_code === code) vendors[i].learnings = text;
+      }
+      showToast(code + ': Learnings saved', 'success');
+    }
+  }).catch(function(err) {
+    showToast('Failed to save learnings: ' + err.message, 'error');
+  });
+}
+
+// Agent health is now included in /api/vendors response (server-side cached 30s)
+
+// Tab switching
+function switchTab(tab) {
+  var btns = document.querySelectorAll('.tab-btn');
+  for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
+  document.querySelector('.tab-btn[data-tab="' + tab + '"]').classList.add('active');
+  document.getElementById('vendorTab').style.display = tab === 'vendors' ? 'block' : 'none';
+  document.getElementById('vendorControls').style.display = tab === 'vendors' ? 'flex' : 'none';
+  document.getElementById('calendarTab').className = 'calendar-container' + (tab === 'calendar' ? ' show' : '');
+  document.getElementById('qualityTab').style.display = tab === 'quality' ? 'block' : 'none';
+  document.getElementById('discontinuedTab').style.display = tab === 'discontinued' ? 'block' : 'none';
+  document.getElementById('lineManagerTab').style.display = tab === 'linemanager' ? 'block' : 'none';
+  document.getElementById('tagManagerTab').style.display = tab === 'tagmanager' ? 'block' : 'none';
+  document.getElementById('standardizeTab').style.display = tab === 'standardize' ? 'block' : 'none';
+  document.getElementById('newLaunchesTab').style.display = tab === 'newlaunches' ? 'block' : 'none';
+  document.getElementById('productActionsTab').style.display = tab === 'productactions' ? 'block' : 'none';
+  if (tab === 'calendar') renderCalendar();
+  if (tab === 'quality') loadQualityData();
+  if (tab === 'discontinued') loadDiscontinuedData();
+  if (tab === 'linemanager') loadLineManager();
+  if (tab === 'tagmanager') loadTagManager();
+  if (tab === 'standardize') loadStandardize();
+  if (tab === 'newlaunches') loadNewLaunches();
+}
+
+// Calendar rendering using safe DOM methods
+function renderCalendar() {
+  var container = document.getElementById('calendarContent');
+  while (container.firstChild) container.removeChild(container.firstChild);
+
+  var now = new Date();
+  var year = now.getFullYear();
+  var month = now.getMonth();
+  var firstDay = new Date(year, month, 1).getDay();
+  var daysInMonth = new Date(year, month + 1, 0).getDate();
+  var monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December'];
+  var dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
+
+  var scheduleMap = {};
+  for (var i = 0; i < vendors.length; i++) {
+    var v = vendors[i];
+    if (v.next_scheduled_crawl) {
+      var dd = new Date(v.next_scheduled_crawl);
+      if (dd.getMonth() === month && dd.getFullYear() === year) {
+        var day = dd.getDate();
+        if (!scheduleMap[day]) scheduleMap[day] = [];
+        scheduleMap[day].push(v);
+      }
+    }
+  }
+
+  var title = document.createElement('h3');
+  title.style.marginBottom = '12px';
+  title.style.color = 'var(--text-primary)';
+  title.textContent = monthNames[month] + ' ' + year;
+  container.appendChild(title);
+
+  var grid = document.createElement('div');
+  grid.className = 'calendar-grid';
+
+  for (var h = 0; h < dayNames.length; h++) {
+    var hdr = document.createElement('div');
+    hdr.className = 'cal-header';
+    hdr.textContent = dayNames[h];
+    grid.appendChild(hdr);
+  }
+
+  for (var e = 0; e < firstDay; e++) {
+    var emptyDay = document.createElement('div');
+    emptyDay.className = 'cal-day empty';
+    grid.appendChild(emptyDay);
+  }
+
+  for (var d = 1; d <= daysInMonth; d++) {
+    var dayDiv = document.createElement('div');
+    dayDiv.className = 'cal-day';
+    if (d === now.getDate()) dayDiv.style.borderColor = 'var(--accent)';
+
+    var dayNum = document.createElement('div');
+    dayNum.className = 'day-num';
+    dayNum.textContent = d;
+    dayDiv.appendChild(dayNum);
+
+    var vendorsToday = scheduleMap[d] || [];
+    for (var vi = 0; vi < vendorsToday.length; vi++) {
+      var vv = vendorsToday[vi];
+      var vendorChip = document.createElement('div');
+      vendorChip.className = 'cal-vendor';
+      if (vv.crawl_priority === 'HIGH') {
+        vendorChip.style.background = 'var(--red-bg)';
+        vendorChip.style.color = 'var(--red)';
+      } else if (vv.crawl_priority === 'MEDIUM') {
+        vendorChip.style.background = 'var(--orange-bg)';
+        vendorChip.style.color = 'var(--orange)';
+      } else {
+        vendorChip.style.background = 'var(--green-bg)';
+        vendorChip.style.color = 'var(--green)';
+      }
+      vendorChip.title = vv.vendor_name;
+      vendorChip.textContent = vv.vendor_name;
+      dayDiv.appendChild(vendorChip);
+    }
+
+    grid.appendChild(dayDiv);
+  }
+
+  container.appendChild(grid);
+}
+
+// Date formatting
+function fmtDateFull(d) {
+  if (!d) return '-';
+  var dt = new Date(d);
+  return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'America/Los_Angeles' })
+    + ' ' + dt.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'America/Los_Angeles' });
+}
+
+function formatTimePT(d) {
+  return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'America/Los_Angeles' }) + ' PT';
+}
+
+// Init
+Promise.all([loadVendors(), loadStats()]).then(function() {
+  document.getElementById('lastRefresh').textContent = 'Last refresh: ' + formatTimePT(new Date());
+  // Update online count after both vendors and stats loaded
+  var onlineCount = vendors.filter(function(v) { return v.agent_status === 'online'; }).length;
+  var el = document.getElementById('statAgentsOnline');
+  if (el) el.textContent = onlineCount + ' / ' + vendors.length;
+});
+
+// ?vendor=<name> deep-link focus (from the CNCP vendors popout). Drops the name into the
+// search box + filters so the linked vendor is front-and-center, and scrolls to it if unique.
+(function(){
+  try {
+    var vName = new URLSearchParams(location.search).get('vendor');
+    if (!vName) return;
+    var apply = function(){
+      var sb = document.getElementById('searchInput');
+      if (sb) { sb.value = vName; if (typeof applyFilters === 'function') applyFilters(); }
+      var m = (vendors || []).filter(function(v){ return (v.vendor_name || '').toLowerCase() === vName.toLowerCase(); });
+      if (m.length === 1 && m[0].vendor_code && typeof scrollToVendor === 'function') { try { scrollToVendor(m[0].vendor_code); } catch(e){} }
+    };
+    var tries = 0;
+    (function wait(){ if ((typeof vendors !== 'undefined' && vendors && vendors.length) || tries > 30) return apply(); tries++; setTimeout(wait, 200); })();
+  } catch(e){}
+})();
+
+// Auto-refresh every 30 seconds
+setInterval(function() {
+  Promise.all([loadVendors(), loadStats()]).then(function() {
+    document.getElementById('lastRefresh').textContent = 'Last refresh: ' + formatTimePT(new Date());
+    var onlineCount = vendors.filter(function(v) { return v.agent_status === 'online'; }).length;
+    var el = document.getElementById('statAgentsOnline');
+    if (el) el.textContent = onlineCount + ' / ' + vendors.length;
+  });
+}, 30000);
+
+// --- Live Totals Poller (real-time catalog counts) ---
+var prevLiveTotals = {};
+function animateStat(elId, newVal) {
+  var el = document.getElementById(elId);
+  if (!el) return;
+  var oldText = el.textContent.replace(/,/g, '');
+  var oldVal = parseInt(oldText) || 0;
+  var formatted = Number(newVal).toLocaleString();
+  if (newVal !== oldVal && oldVal > 0) {
+    el.textContent = formatted;
+    el.classList.remove('stat-flash-up', 'stat-flash-down');
+    void el.offsetWidth; // force reflow
+    el.classList.add(newVal > oldVal ? 'stat-flash-up' : 'stat-flash-down');
+  } else {
+    el.textContent = formatted;
+  }
+}
+function pollLiveTotals() {
+  fetch('/api/live-totals', { headers: { 'Authorization': ''  /* auth via httpOnly vcc_session cookie — no embedded credential */ }})
+    .then(function(r) { return r.json(); })
+    .then(function(data) {
+      if (!data.success) return;
+      animateStat('statTotalProducts', data.catalog_total);
+      animateStat('statShopifyCount', data.shopify_total);
+      animateStat('statWithCost', data.with_cost);
+      animateStat('statWithImages', data.with_images);
+      animateStat('statWithWidth', data.with_width);
+      animateStat('statWithRepeat', data.with_repeat);
+      animateStat('statMissingImages', data.not_on_shopify);
+      animateStat('statToDisc', data.to_discontinue);
+      // Update coverage %
+      var covEl = document.getElementById('statCoverage');
+      if (covEl && data.catalog_total > 0) {
+        covEl.textContent = ((data.with_cost / data.catalog_total) * 100).toFixed(1) + '%';
+      }
+    })
+    .catch(function() {});
+}
+pollLiveTotals();
+setInterval(pollLiveTotals, 10000);
+
+// --- Live Activity Ticker ---
+var lastActivityTs = null;
+function loadActivity() {
+  var url = '/api/activity?limit=30';
+  if (lastActivityTs) url += '&since=' + encodeURIComponent(lastActivityTs);
+  fetch(url, { headers: { 'Authorization': ''  /* auth via httpOnly vcc_session cookie — no embedded credential */ }})
+    .then(function(r) { return r.json(); })
+    .then(function(data) {
+      var entries = data.entries || [];
+      if (entries.length === 0 && lastActivityTs) return;
+      if (entries.length > 0) lastActivityTs = entries[0].ts;
+      var container = document.getElementById('tickerEntries');
+      if (!container) return;
+      // Build entries HTML (doubled for seamless scroll)
+      var html = '';
+      var allEntries = entries.length > 0 ? entries : [{ts: new Date().toISOString(), type: 'info', message: 'System idle — waiting for activity'}];
+      for (var i = 0; i < allEntries.length; i++) {
+        var e = allEntries[i];
+        var t = new Date(e.ts);
+        var timeStr = t.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: 'America/Los_Angeles' });
+        html += '<span class="ticker-entry"><span class="ticker-time">' + timeStr + '</span>';
+        html += '<span class="ticker-type ' + e.type + '">[' + e.type.toUpperCase() + ']</span> ';
+        html += e.message + '</span>';
+      }
+      // Duplicate for seamless scroll
+      container.innerHTML = html + html;
+      // Update count
+      var countEl = document.getElementById('tickerCount');
+      if (countEl) countEl.textContent = data.total + ' events';
+      // Adjust animation speed based on content width
+      var scrollW = container.scrollWidth / 2;
+      var speed = Math.max(20, scrollW / 50);
+      container.style.animationDuration = speed + 's';
+    })
+    .catch(function() {});
+}
+loadActivity();
+setInterval(loadActivity, 5000);
+
+// ============================================================================
+// DATA QUALITY TAB
+// ============================================================================
+var qualityLoaded = false;
+var qualityVendors = [];
+var qualitySortCol = 'catalog_count';
+var qualitySortDir = 'desc';
+
+function loadQualityData() {
+  if (qualityLoaded) return;
+  apiCall('/api/data-quality/catalog').then(function(res) {
+    if (!res.success) return;
+    qualityLoaded = true;
+    qualityVendors = res.vendors;
+    document.getElementById('qualityLoading').style.display = 'none';
+    renderQualitySummary(res.vendors);
+    renderQualityHeatmap(res.vendors);
+  }).catch(function(err) {
+    document.getElementById('qualityLoading').textContent = 'Failed to load: ' + err.message;
+  });
+}
+
+function renderQualitySummary(vendors) {
+  var totalProducts = 0, totalImages = 0, totalWidth = 0, totalRepeat = 0;
+  for (var i = 0; i < vendors.length; i++) {
+    var v = vendors[i];
+    totalProducts += parseInt(v.catalog_count) || 0;
+    totalImages += parseInt(v.products_with_images) || 0;
+    totalWidth += parseInt(v.catalog_with_width) || 0;
+    totalRepeat += parseInt(v.catalog_with_repeat) || 0;
+  }
+  var cards = [
+    { label: 'Total Products', value: totalProducts.toLocaleString(), color: '#5dade2' },
+    { label: 'Vendors', value: String(vendors.length), color: '#8e44ad' },
+    { label: 'Images', value: (totalProducts > 0 ? (100*totalImages/totalProducts).toFixed(1) : '0') + '%', color: totalImages/totalProducts > 0.8 ? '#27ae60' : '#e67e22' },
+    { label: 'Width', value: (totalProducts > 0 ? (100*totalWidth/totalProducts).toFixed(1) : '0') + '%', color: totalWidth/totalProducts > 0.5 ? '#e67e22' : '#e74c3c' },
+    { label: 'Repeat', value: (totalProducts > 0 ? (100*totalRepeat/totalProducts).toFixed(1) : '0') + '%', color: totalRepeat/totalProducts > 0.5 ? '#e67e22' : '#e74c3c' }
+  ];
+  var container = document.getElementById('qualitySummary');
+  while (container.firstChild) container.removeChild(container.firstChild);
+  for (var c = 0; c < cards.length; c++) {
+    var d = document.createElement('div');
+    d.style.cssText = 'background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center;';
+    var valEl = document.createElement('div');
+    valEl.style.cssText = 'font-size:22px;font-weight:700;color:' + cards[c].color;
+    valEl.textContent = cards[c].value;
+    var lblEl = document.createElement('div');
+    lblEl.style.cssText = 'font-size:11px;color:var(--text-muted);margin-top:4px';
+    lblEl.textContent = cards[c].label;
+    d.appendChild(valEl);
+    d.appendChild(lblEl);
+    container.appendChild(d);
+  }
+}
+
+function filterQualityHeatmap() {
+  var filter = document.getElementById('qualityFilter').value;
+  var filtered = qualityVendors.filter(function(v) {
+    var img = parseFloat(v.image_pct) || 0;
+    var wid = parseFloat(v.width_pct) || 0;
+    var spec = parseFloat(v.spec_completeness) || 0;
+    var minVal = Math.min(img, wid, spec);
+    if (filter === 'critical') return minVal < 50;
+    if (filter === 'warning') return minVal < 80;
+    if (filter === 'good') return img >= 80 && wid >= 80 && spec >= 80;
+    return true;
+  });
+  document.getElementById('qualityHeatmap').textContent = '';
+  renderQualityHeatmap(filtered);
+}
+
+function sortQuality(col) {
+  if (qualitySortCol === col) { qualitySortDir = qualitySortDir === 'asc' ? 'desc' : 'asc'; }
+  else { qualitySortCol = col; qualitySortDir = 'desc'; }
+  filterQualityHeatmap();
+}
+
+function renderQualityHeatmap(vendors) {
+  var container = document.getElementById('qualityHeatmap');
+  container.textContent = '';
+
+  var sorted = vendors.slice().sort(function(a, b) {
+    var av = qualitySortCol === 'vendor_name' ? (a[qualitySortCol] || '').toLowerCase() : (parseFloat(a[qualitySortCol]) || 0);
+    var bv = qualitySortCol === 'vendor_name' ? (b[qualitySortCol] || '').toLowerCase() : (parseFloat(b[qualitySortCol]) || 0);
+    if (av < bv) return qualitySortDir === 'asc' ? -1 : 1;
+    if (av > bv) return qualitySortDir === 'asc' ? 1 : -1;
+    return 0;
+  });
+
+  var tbl = document.createElement('table');
+  tbl.style.cssText = 'width:100%;border-collapse:collapse;font-size:12px;';
+
+  var thead = document.createElement('thead');
+  var hr = document.createElement('tr');
+  var cols = [
+    { label: 'Vendor', key: 'vendor_name' },
+    { label: 'Products', key: 'catalog_count' },
+    { label: 'Images %', key: 'image_pct' },
+    { label: 'Width %', key: 'width_pct' },
+    { label: 'Repeat %', key: 'repeat_pct' },
+    { label: 'Cost %', key: 'cost_pct' },
+    { label: 'Spec Fill %', key: 'spec_completeness' }
+  ];
+  for (var i = 0; i < cols.length; i++) {
+    var th = document.createElement('th');
+    th.textContent = cols[i].label + (qualitySortCol === cols[i].key ? (qualitySortDir === 'asc' ? ' \\u25B2' : ' \\u25BC') : '');
+    th.style.cssText = 'padding:8px 10px;text-align:' + (i === 0 ? 'left' : 'right') + ';color:var(--text-secondary);border-bottom:1px solid var(--border);font-weight:500;white-space:nowrap;cursor:pointer;user-select:none;';
+    th.onclick = (function(key) { return function() { sortQuality(key); }; })(cols[i].key);
+    hr.appendChild(th);
+  }
+  thead.appendChild(hr);
+  tbl.appendChild(thead);
+
+  var tbody = document.createElement('tbody');
+  for (var j = 0; j < sorted.length; j++) {
+    var v = sorted[j];
+    var tr = document.createElement('tr');
+    tr.style.cssText = 'border-bottom:1px solid rgba(42,45,69,0.5);';
+
+    var tdName = document.createElement('td');
+    tdName.style.cssText = 'padding:6px 10px;color:var(--text-primary);font-weight:500;white-space:nowrap;';
+    tdName.textContent = v.vendor_name;
+    tr.appendChild(tdName);
+
+    var tdCount = document.createElement('td');
+    tdCount.style.cssText = 'padding:6px 10px;text-align:right;color:var(--text-secondary);';
+    tdCount.textContent = Number(v.catalog_count).toLocaleString();
+    tr.appendChild(tdCount);
+
+    var pcts = [
+      parseFloat(v.image_pct) || 0,
+      parseFloat(v.width_pct) || 0,
+      parseFloat(v.repeat_pct) || 0,
+      parseFloat(v.cost_pct) || 0,
+      parseFloat(v.spec_completeness) || 0
+    ];
+
+    for (var k = 0; k < pcts.length; k++) {
+      var td = document.createElement('td');
+      td.style.cssText = 'padding:6px 10px;text-align:right;font-weight:600;';
+      var pct = pcts[k];
+      var color, bg;
+      if (pct >= 80) { color = 'var(--green)'; bg = 'var(--green-bg)'; }
+      else if (pct >= 50) { color = 'var(--orange)'; bg = 'var(--orange-bg)'; }
+      else { color = 'var(--red)'; bg = 'var(--red-bg)'; }
+      td.style.color = color;
+      td.style.background = bg;
+      td.style.borderRadius = '4px';
+      td.textContent = pct.toFixed(1) + '%';
+      tr.appendChild(td);
+    }
+
+    tbody.appendChild(tr);
+  }
+  tbl.appendChild(tbody);
+  container.appendChild(tbl);
+
+  document.getElementById('qualityIssueCount').textContent = sorted.length + ' vendors shown';
+}
+
+function sendQualitySlackAlert() {
+  var btn = event.target;
+  btn.disabled = true;
+  btn.textContent = 'Sending...';
+  apiCall('/api/data-quality/slack-alert', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threshold: 80 }) }).then(function(res) {
+    btn.disabled = false;
+    btn.textContent = 'Slack Alert';
+    if (res.success) {
+      showToast('Slack alert sent: ' + res.flagged + ' vendors flagged below ' + (res.threshold || 80) + '%', 'success');
+    } else {
+      showToast('Alert failed: ' + (res.error || 'Unknown'), 'error');
+    }
+  }).catch(function(err) {
+    btn.disabled = false;
+    btn.textContent = 'Slack Alert';
+    showToast('Alert error: ' + err.message, 'error');
+  });
+}
+
+function runQualityAudit() {
+  var btn = event.target;
+  btn.disabled = true;
+  btn.textContent = 'Running...';
+  apiCall('/api/data-quality/audit', { method: 'POST' }).then(function(res) {
+    btn.disabled = false;
+    btn.textContent = 'Run Audit';
+    if (res.success) {
+      showToast('Audit complete: ' + res.issues_created + ' issues found across ' + res.products_audited + ' products', 'success');
+      document.getElementById('qualityIssueCount').textContent = res.issues_created + ' open issues';
+    } else {
+      showToast('Audit failed: ' + (res.error || 'Unknown'), 'error');
+    }
+  }).catch(function(err) {
+    btn.disabled = false;
+    btn.textContent = 'Run Audit';
+    showToast('Audit error: ' + err.message, 'error');
+  });
+}
+
+// --- Discontinued Tab ---
+var discPage = 1;
+var discData = null;
+
+function loadDiscontinuedData() {
+  var vendorFilter = document.getElementById('discVendorFilter').value;
+  var statusFilter = document.getElementById('discStatusFilter').value;
+  document.getElementById('discLoading').style.display = 'block';
+  var url = '/api/discontinued?page=' + discPage + '&limit=50&vendor=' + encodeURIComponent(vendorFilter) + '&status=' + encodeURIComponent(statusFilter);
+  apiCall(url).then(function(data) {
+    discData = data;
+    document.getElementById('discLoading').style.display = 'none';
+    renderDiscSummary(data.summary);
+    renderDiscVendors(data.vendors);
+    renderDiscProducts(data.products, data.pagination);
+    var sel = document.getElementById('discVendorFilter');
+    if (sel.options.length <= 1) {
+      data.vendors.forEach(function(v) {
+        var opt = document.createElement('option');
+        opt.value = v.vendor_name;
+        opt.textContent = v.vendor_name + ' (' + v.total + ')';
+        sel.appendChild(opt);
+      });
+    }
+  }).catch(function(err) {
+    document.getElementById('discLoading').style.display = 'none';
+    document.getElementById('discProducts').textContent = 'Error: ' + err.message;
+  });
+}
+
+function makeCard(label, value, color) {
+  var d = document.createElement('div');
+  d.style.cssText = 'background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:12px;text-align:center';
+  var val = document.createElement('div');
+  val.style.cssText = 'font-size:22px;font-weight:700;color:' + color;
+  val.textContent = value;
+  var lbl = document.createElement('div');
+  lbl.style.cssText = 'font-size:11px;color:var(--text-muted);margin-top:4px';
+  lbl.textContent = label;
+  d.appendChild(val);
+  d.appendChild(lbl);
+  return d;
+}
+
+function renderDiscSummary(s) {
+  var c = document.getElementById('discSummary');
+  while (c.firstChild) c.removeChild(c.firstChild);
+  c.appendChild(makeCard('Total Discontinued', Number(s.total_discontinued).toLocaleString(), '#e74c3c'));
+  c.appendChild(makeCard('Vendors', s.vendor_count, 'var(--accent)'));
+  c.appendChild(makeCard('On Shopify', Number(s.matched_shopify).toLocaleString(), '#3498db'));
+  c.appendChild(makeCard('Still ACTIVE', Number(s.still_active).toLocaleString(), '#e67e22'));
+  c.appendChild(makeCard('Already Archived', Number(s.already_archived).toLocaleString(), '#2ecc71'));
+  c.appendChild(makeCard('In Draft', Number(s.in_draft).toLocaleString(), '#9b59b6'));
+  c.appendChild(makeCard('Not on Shopify', Number(s.not_on_shopify).toLocaleString(), 'var(--text-muted)'));
+}
+
+function makeTh(text, align) {
+  var th = document.createElement('th');
+  th.style.cssText = 'text-align:' + (align||'left') + ';padding:6px;color:var(--text-muted)';
+  th.textContent = text;
+  return th;
+}
+
+function makeTd(text, style) {
+  var td = document.createElement('td');
+  td.style.cssText = style || 'padding:6px';
+  td.textContent = text || '';
+  return td;
+}
+
+function renderDiscVendors(vendors) {
+  var c = document.getElementById('discVendorBreakdown');
+  while (c.firstChild) c.removeChild(c.firstChild);
+  if (!vendors || vendors.length === 0) return;
+  var t = document.createElement('table');
+  t.style.cssText = 'width:100%;border-collapse:collapse;font-size:12px';
+  var thead = document.createElement('thead');
+  var hRow = document.createElement('tr');
+  hRow.style.borderBottom = '1px solid var(--border)';
+  hRow.appendChild(makeTh('Vendor'));
+  hRow.appendChild(makeTh('Total','right'));
+  hRow.appendChild(makeTh('On Shopify','right'));
+  hRow.appendChild(makeTh('Still Active','right'));
+  hRow.appendChild(makeTh('Archived','right'));
+  hRow.appendChild(makeTh('Draft','right'));
+  thead.appendChild(hRow);
+  t.appendChild(thead);
+  var tbody = document.createElement('tbody');
+  vendors.slice(0, 30).forEach(function(v) {
+    var tr = document.createElement('tr');
+    tr.style.cssText = 'border-bottom:1px solid var(--border);cursor:pointer';
+    tr.onclick = function() { document.getElementById('discVendorFilter').value = v.vendor_name; discPage = 1; loadDiscontinuedData(); };
+    tr.appendChild(makeTd(v.vendor_name || 'Unknown', 'padding:6px;color:var(--text-primary)'));
+    tr.appendChild(makeTd(v.total, 'text-align:right;padding:6px'));
+    tr.appendChild(makeTd(v.on_shopify, 'text-align:right;padding:6px'));
+    tr.appendChild(makeTd(v.active, 'text-align:right;padding:6px;' + (Number(v.active) > 0 ? 'color:#e67e22;font-weight:700' : '')));
+    tr.appendChild(makeTd(v.archived, 'text-align:right;padding:6px;color:#2ecc71'));
+    tr.appendChild(makeTd(v.draft, 'text-align:right;padding:6px'));
+    tbody.appendChild(tr);
+  });
+  t.appendChild(tbody);
+  c.appendChild(t);
+}
+
+function makeStatusBadge(status) {
+  var span = document.createElement('span');
+  if (status === 'active') { span.style.cssText = 'background:#e67e22;color:#fff;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600'; span.textContent = 'ACTIVE'; }
+  else if (status === 'archived') { span.style.cssText = 'background:#2ecc71;color:#fff;padding:2px 6px;border-radius:4px;font-size:10px'; span.textContent = 'Archived'; }
+  else if (status === 'draft') { span.style.cssText = 'background:#9b59b6;color:#fff;padding:2px 6px;border-radius:4px;font-size:10px'; span.textContent = 'Draft'; }
+  else { span.style.cssText = 'color:var(--text-muted);font-size:10px'; span.textContent = 'Not on Shopify'; }
+  return span;
+}
+
+function renderDiscProducts(products, pag) {
+  var info = document.getElementById('discPageInfo');
+  info.textContent = 'Page ' + pag.page + ' of ' + pag.pages + ' (' + pag.total.toLocaleString() + ' products)';
+  var c = document.getElementById('discProducts');
+  while (c.firstChild) c.removeChild(c.firstChild);
+  if (!products || products.length === 0) {
+    var empty = document.createElement('div');
+    empty.style.cssText = 'color:var(--text-muted);padding:20px;text-align:center';
+    empty.textContent = 'No products found for this filter.';
+    c.appendChild(empty);
+    return;
+  }
+  var t = document.createElement('table');
+  t.style.cssText = 'width:100%;border-collapse:collapse;font-size:11px';
+  var thead = document.createElement('thead');
+  var hRow = document.createElement('tr');
+  hRow.style.borderBottom = '1px solid var(--border)';
+  hRow.appendChild(makeTh('Img'));
+  hRow.appendChild(makeTh('DW SKU'));
+  hRow.appendChild(makeTh('MFR SKU'));
+  hRow.appendChild(makeTh('Title'));
+  hRow.appendChild(makeTh('Vendor'));
+  hRow.appendChild(makeTh('Shopify Status', 'center'));
+  hRow.appendChild(makeTh('Archive Tag'));
+  thead.appendChild(hRow);
+  t.appendChild(thead);
+  var tbody = document.createElement('tbody');
+  products.forEach(function(p) {
+    var tr = document.createElement('tr');
+    tr.style.cssText = 'border-bottom:1px solid var(--border)';
+    var imgTd = document.createElement('td');
+    imgTd.style.cssText = 'padding:4px 6px';
+    if (p.image_url) {
+      var img = document.createElement('img');
+      img.src = p.image_url;
+      img.style.cssText = 'width:36px;height:36px;object-fit:cover;border-radius:4px';
+      img.onerror = function() { this.style.display = 'none'; };
+      imgTd.appendChild(img);
+    } else {
+      var placeholder = document.createElement('div');
+      placeholder.style.cssText = 'width:36px;height:36px;background:var(--border);border-radius:4px';
+      imgTd.appendChild(placeholder);
+    }
+    tr.appendChild(imgTd);
+    tr.appendChild(makeTd(p.dw_sku, 'padding:6px;font-family:monospace;font-size:10px'));
+    tr.appendChild(makeTd(p.mfr_sku, 'padding:6px;font-family:monospace;font-size:10px'));
+    tr.appendChild(makeTd(p.title || p.dw_handle || '', 'padding:6px;color:var(--text-primary);max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'));
+    tr.appendChild(makeTd(p.vendor_name || p.shopify_vendor || '', 'padding:6px'));
+    var statusTd = document.createElement('td');
+    statusTd.style.cssText = 'text-align:center;padding:6px';
+    statusTd.appendChild(makeStatusBadge(p.shopify_status));
+    tr.appendChild(statusTd);
+    tr.appendChild(makeTd(p.archive_tag, 'padding:6px;font-size:10px;color:var(--text-muted)'));
+    tbody.appendChild(tr);
+  });
+  t.appendChild(tbody);
+  c.appendChild(t);
+}
+
+function discPrevPage() { if (discPage > 1) { discPage--; loadDiscontinuedData(); } }
+function discNextPage() { if (discData && discPage < discData.pagination.pages) { discPage++; loadDiscontinuedData(); } }
+
+// ==================== Assign DW SKUs ====================
+function assignDwSkus(code, btn) {
+  btn.disabled = true;
+  var origText = btn.textContent;
+  btn.textContent = 'Assigning...';
+  apiCall('/api/vendors/' + encodeURIComponent(code) + '/assign-skus', { method: 'POST' })
+    .then(function(res) {
+      if (res.success) {
+        btn.textContent = res.assigned + ' assigned';
+        btn.style.color = 'var(--green)';
+        showToast(res.message + ' (' + res.range + ')', 'success');
+        // Refresh vendor data after a brief delay
+        setTimeout(function() {
+          btn.textContent = origText;
+          btn.disabled = false;
+          btn.style.color = 'var(--accent)';
+          loadVendors();
+        }, 3000);
+      } else {
+        btn.textContent = 'Error';
+        btn.style.color = 'var(--red)';
+        showToast('SKU assignment failed: ' + (res.error || 'Unknown'), 'error');
+        setTimeout(function() { btn.textContent = origText; btn.disabled = false; btn.style.color = 'var(--accent)'; }, 3000);
+      }
+    })
+    .catch(function(err) {
+      btn.textContent = 'Error';
+      btn.style.color = 'var(--red)';
+      showToast('SKU assignment error: ' + err.message, 'error');
+      setTimeout(function() { btn.textContent = origText; btn.disabled = false; btn.style.color = 'var(--accent)'; }, 3000);
+    });
+}
+
+// ==================== Create Inquiry Page ====================
+function createInquiryPage(code, name, btn) {
+  btn.disabled = true;
+  var origText = btn.textContent;
+  btn.textContent = 'Creating...';
+  apiCall('/api/vendors/' + encodeURIComponent(code) + '/inquiry-page', { method: 'POST' })
+    .then(function(res) {
+      if (res.success) {
+        if (res.existing) {
+          btn.textContent = 'Page exists';
+          btn.style.color = '#4fc3f7';
+          showToast(name + ' inquiry page already exists', 'info');
+        } else {
+          btn.textContent = 'Page created';
+          btn.style.color = 'var(--green)';
+          showToast(name + ' inquiry page created on Shopify', 'success');
+        }
+        setTimeout(function() { btn.textContent = origText; btn.disabled = false; btn.style.color = '#4fc3f7'; }, 4000);
+      } else {
+        btn.textContent = 'Error';
+        btn.style.color = 'var(--red)';
+        showToast('Page creation failed: ' + (res.error || 'Unknown'), 'error');
+        setTimeout(function() { btn.textContent = origText; btn.disabled = false; btn.style.color = '#4fc3f7'; }, 3000);
+      }
+    })
+    .catch(function(err) {
+      btn.textContent = 'Error';
+      btn.style.color = 'var(--red)';
+      showToast('Inquiry page error: ' + err.message, 'error');
+      setTimeout(function() { btn.textContent = origText; btn.disabled = false; btn.style.color = '#4fc3f7'; }, 3000);
+    });
+}
+
+// ==================== Line Manager ====================
+var lmLines = [];
+var lmSelected = new Set();
+var lmLoaded = false;
+var lmQueuedConfig = null;
+var lmPollInterval = null;
+
+function lmCommaNum(n) {
+  return (n || 0).toLocaleString();
+}
+
+function lmTimeAgo(ts) {
+  if (!ts) return 'Never';
+  var diff = Date.now() - new Date(ts).getTime();
+  var mins = Math.floor(diff / 60000);
+  if (mins < 1) return 'Just now';
+  if (mins < 60) return mins + 'm ago';
+  var hrs = Math.floor(mins / 60);
+  if (hrs < 24) return hrs + 'h ago';
+  var days = Math.floor(hrs / 24);
+  if (days < 30) return days + 'd ago';
+  return Math.floor(days / 30) + 'mo ago';
+}
+
+function loadLineManager() {
+  if (lmLoaded) return;
+  apiCall('/api/lm/lines').then(function(resp) {
+    lmLines = resp.data || resp;
+    if (Array.isArray(resp)) lmLines = resp;
+    lmLoaded = true;
+    renderLmTable();
+    lmCheckPushStatus();
+    lmLoadCronStatus();
+  }).catch(function(err) {
+    var tbody = document.getElementById('lmTableBody');
+    tbody.textContent = '';
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.colSpan = 7;
+    td.style.cssText = 'color:var(--red);text-align:center;padding:20px;';
+    td.textContent = 'Error loading lines: ' + err.message;
+    tr.appendChild(td);
+    tbody.appendChild(tr);
+  });
+}
+
+function renderLmTable() {
+  var tbody = document.getElementById('lmTableBody');
+  tbody.textContent = '';
+  if (!lmLines || !lmLines.length) {
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.colSpan = 7;
+    td.style.cssText = 'text-align:center;color:var(--text-muted);padding:20px;';
+    td.textContent = 'No brand lines found';
+    tr.appendChild(td);
+    tbody.appendChild(tr);
+    return;
+  }
+  lmLines.forEach(function(line) {
+    var tr = document.createElement('tr');
+    tr.style.borderBottom = '1px solid var(--border)';
+    tr.style.transition = 'background .15s';
+    tr.addEventListener('mouseenter', function() { tr.style.background = 'rgba(124,58,237,0.06)'; });
+    tr.addEventListener('mouseleave', function() { tr.style.background = ''; });
+    // Checkbox
+    var tdCb = document.createElement('td');
+    tdCb.style.padding = '10px 12px';
+    var cb = document.createElement('input');
+    cb.type = 'checkbox';
+    cb.checked = lmSelected.has(line.brand);
+    cb.style.accentColor = 'var(--accent)';
+    cb.addEventListener('change', function() { lmToggle(line.brand, this.checked); });
+    tdCb.appendChild(cb);
+    tr.appendChild(tdCb);
+    // Brand
+    var tdBrand = document.createElement('td');
+    tdBrand.style.cssText = 'padding:10px 12px;font-weight:600;color:var(--text-primary)';
+    tdBrand.textContent = line.brand;
+    tr.appendChild(tdBrand);
+    // Total
+    var tdTotal = document.createElement('td');
+    tdTotal.style.cssText = 'padding:10px 12px;color:var(--text-secondary)';
+    tdTotal.textContent = lmCommaNum(line.total);
+    tr.appendChild(tdTotal);
+    // Ready
+    var readyPct = line.total > 0 ? Math.round(line.ready / line.total * 100) : 0;
+    var tdReady = document.createElement('td');
+    tdReady.style.cssText = 'padding:10px 12px;color:var(--green);font-weight:600';
+    tdReady.textContent = lmCommaNum(line.ready) + ' (' + readyPct + '%)';
+    tr.appendChild(tdReady);
+    // Pushed
+    var tdPushed = document.createElement('td');
+    tdPushed.style.cssText = 'padding:10px 12px;color:var(--accent);font-weight:600';
+    tdPushed.textContent = lmCommaNum(line.pushed);
+    tr.appendChild(tdPushed);
+    // Last Crawled
+    var tdCrawled = document.createElement('td');
+    tdCrawled.style.cssText = 'padding:10px 12px;color:var(--text-muted);font-size:12px';
+    tdCrawled.textContent = lmTimeAgo(line.last_crawled);
+    tr.appendChild(tdCrawled);
+    // Actions
+    var tdAct = document.createElement('td');
+    tdAct.style.padding = '10px 12px';
+    var crawlBtn = document.createElement('button');
+    crawlBtn.style.cssText = 'padding:4px 12px;font-size:11px;background:transparent;border:1px solid var(--border);border-radius:4px;color:var(--text-secondary);cursor:pointer;';
+    crawlBtn.textContent = 'Crawl';
+    crawlBtn.addEventListener('click', function() { lmTriggerCrawl(line.brand, crawlBtn); });
+    tdAct.appendChild(crawlBtn);
+    tr.appendChild(tdAct);
+    tbody.appendChild(tr);
+  });
+  lmUpdateSelectedInfo();
+}
+
+function lmToggle(brand, checked) {
+  if (checked) lmSelected.add(brand);
+  else lmSelected.delete(brand);
+  lmUpdateSelectedInfo();
+}
+
+function lmSelectAll() {
+  lmLines.forEach(function(l) { lmSelected.add(l.brand); });
+  renderLmTable();
+}
+
+function lmDeselectAll() {
+  lmSelected.clear();
+  renderLmTable();
+}
+
+function lmUpdateSelectedInfo() {
+  var count = lmSelected.size;
+  var totalReady = 0;
+  lmLines.forEach(function(l) { if (lmSelected.has(l.brand)) totalReady += l.ready; });
+  document.getElementById('lmSelectedInfo').textContent =
+    count + ' brand' + (count !== 1 ? 's' : '') + ' selected (' + lmCommaNum(totalReady) + ' ready)';
+  document.getElementById('lmPushInfo').textContent =
+    count > 0 ? lmCommaNum(totalReady) + ' products ready to push' : '';
+}
+
+function lmTriggerCrawl(brand, btn) {
+  btn.disabled = true;
+  btn.textContent = 'Running...';
+  apiCall('/api/lm/crawl/' + encodeURIComponent(brand), { method: 'POST' }).then(function() {
+    btn.textContent = 'Done';
+    btn.style.color = 'var(--green)';
+    btn.style.borderColor = 'var(--green)';
+    setTimeout(function() { lmLoaded = false; loadLineManager(); }, 1500);
+  }).catch(function() {
+    btn.textContent = 'Error';
+    btn.style.color = 'var(--red)';
+    setTimeout(function() { btn.textContent = 'Crawl'; btn.disabled = false; btn.style.color = ''; btn.style.borderColor = ''; }, 3000);
+  });
+}
+
+function lmGetPushConfig() {
+  var modeEls = document.querySelectorAll('input[name="lmPushMode"]');
+  var mode = 'all';
+  for (var i = 0; i < modeEls.length; i++) { if (modeEls[i].checked) mode = modeEls[i].value; }
+  var batchSize = parseInt(document.getElementById('lmBatchSize').value, 10);
+  var delayHours = parseInt(document.getElementById('lmDelayHours').value, 10);
+  return {
+    brands: Array.from(lmSelected),
+    mode: mode === 'all' ? 'all' : 'batched',
+    batchSize: mode === 'batched' ? batchSize : 9999,
+    delayHours: mode === 'batched' ? delayHours : 0
+  };
+}
+
+function lmQueuePush() {
+  if (lmSelected.size === 0) {
+    showToast('Select at least one brand line to queue.', 'error');
+    return;
+  }
+  lmQueuedConfig = lmGetPushConfig();
+  var qp = document.getElementById('lmQueuePanel');
+  qp.style.display = 'block';
+  var totalReady = 0;
+  var brandList = [];
+  lmLines.forEach(function(l) {
+    if (lmSelected.has(l.brand)) {
+      totalReady += l.ready;
+      brandList.push(l.brand + ' (' + lmCommaNum(l.ready) + ')');
+    }
+  });
+  var detailsEl = document.getElementById('lmQueueDetails');
+  detailsEl.textContent = '';
+  var modeDiv = document.createElement('div');
+  modeDiv.style.marginBottom = '6px';
+  modeDiv.textContent = lmCommaNum(totalReady) + ' products from ' + lmSelected.size + ' brands — ' +
+    (lmQueuedConfig.mode === 'all' ? 'Push all at once' : 'Batches of ' + lmQueuedConfig.batchSize + ' every ' + lmQueuedConfig.delayHours + 'h');
+  detailsEl.appendChild(modeDiv);
+  var brandsDiv = document.createElement('div');
+  brandsDiv.style.cssText = 'line-height:1.5;font-size:11px;color:var(--text-muted);';
+  brandsDiv.textContent = 'Brands: ' + brandList.join(', ');
+  detailsEl.appendChild(brandsDiv);
+  document.getElementById('lmBtnQueue').style.display = 'none';
+  document.getElementById('lmBtnStart').style.display = '';
+  document.getElementById('lmBtnStart').disabled = false;
+  showToast('Push queued — review and click Start Push to begin.', 'info');
+}
+
+function lmStartPush() {
+  if (!lmQueuedConfig || !lmQueuedConfig.brands.length) {
+    showToast('No products queued. Queue first.', 'error');
+    return;
+  }
+  document.getElementById('lmBtnStart').disabled = true;
+  apiCall('/api/lm/push/start', {
+    method: 'POST',
+    body: JSON.stringify(lmQueuedConfig)
+  }).then(function(data) {
+    if (data.success || data.jobId) {
+      document.getElementById('lmBtnStart').style.display = 'none';
+      document.getElementById('lmBtnStop').style.display = '';
+      document.getElementById('lmProgressPanel').style.display = 'block';
+      document.getElementById('lmQueuePanel').style.display = 'none';
+      lmStartPolling();
+      showToast('Push started! ' + lmCommaNum(data.totalProducts || 0) + ' products queued.', 'success');
+    } else {
+      showToast('Failed to start push: ' + (data.error || 'Unknown error'), 'error');
+      document.getElementById('lmBtnStart').disabled = false;
+    }
+  }).catch(function(err) {
+    showToast('Error: ' + err.message, 'error');
+    document.getElementById('lmBtnStart').disabled = false;
+  });
+}
+
+function lmStopPush() {
+  apiCall('/api/lm/push/stop', { method: 'POST' }).then(function() {
+    showToast('Push stop requested', 'info');
+  }).catch(function() {});
+}
+
+function lmStartPolling() {
+  if (lmPollInterval) clearInterval(lmPollInterval);
+  lmPollInterval = setInterval(lmPollStatus, 1500);
+}
+
+function lmStopPolling() {
+  if (lmPollInterval) { clearInterval(lmPollInterval); lmPollInterval = null; }
+}
+
+function lmCheckPushStatus() {
+  apiCall('/api/lm/push/status').then(function(d) {
+    if (d && d.active) {
+      document.getElementById('lmBtnQueue').style.display = 'none';
+      document.getElementById('lmBtnStart').style.display = 'none';
+      document.getElementById('lmBtnStop').style.display = '';
+      document.getElementById('lmProgressPanel').style.display = 'block';
+      lmUpdateProgressUI(d);
+      lmStartPolling();
+    }
+  }).catch(function() {});
+}
+
+function lmPollStatus() {
+  apiCall('/api/lm/push/status').then(function(d) {
+    lmUpdateProgressUI(d);
+    if (!d.active) {
+      lmStopPolling();
+      document.getElementById('lmBtnStop').style.display = 'none';
+      document.getElementById('lmBtnQueue').style.display = '';
+      document.getElementById('lmBtnStart').style.display = 'none';
+      document.getElementById('lmProgressTitle').textContent =
+        d.cancelled ? 'Push Cancelled' : 'Push Complete';
+      if (!d.cancelled && d.pushed > 0) {
+        lmShowCronScheduler();
+      }
+      lmLoaded = false;
+      loadLineManager();
+    }
+  }).catch(function() {
+    lmStopPolling();
+  });
+}
+
+function lmUpdateProgressUI(d) {
+  var total = d.totalProducts || 1;
+  var pct = Math.round((d.pushed + d.errors) / total * 100);
+  document.getElementById('lmProgressBar').style.width = pct + '%';
+  document.getElementById('lmPsPushed').textContent = lmCommaNum(d.pushed);
+  document.getElementById('lmPsErrors').textContent = d.errors || 0;
+  document.getElementById('lmPsSkipped').textContent = d.skipped || 0;
+  if (d.active) {
+    document.getElementById('lmProgressTitle').textContent =
+      'Push in Progress... (' + pct + '%) — Batch ' + (d.currentBatch || 0) + '/' + (d.totalBatches || 0);
+  }
+  var logEl = document.getElementById('lmProgressLog');
+  if (d.log && d.log.length) {
+    logEl.textContent = '';
+    d.log.forEach(function(line) {
+      var div = document.createElement('div');
+      div.style.lineHeight = '1.6';
+      if (line.indexOf('Error:') !== -1 || line.indexOf('Fatal') !== -1) div.style.color = 'var(--red)';
+      else if (line.indexOf('Pushed:') !== -1) div.style.color = 'var(--green)';
+      div.textContent = line;
+      logEl.appendChild(div);
+    });
+    logEl.scrollTop = logEl.scrollHeight;
+  }
+}
+
+// ==================== Line Manager Cron ====================
+function lmShowCronScheduler() {
+  var panel = document.getElementById('lmCronPanel');
+  panel.style.display = 'block';
+  lmRenderCronForm();
+}
+
+function lmLoadCronStatus() {
+  apiCall('/api/lm/push/cron').then(function(data) {
+    if (data && data.enabled) {
+      document.getElementById('lmCronPanel').style.display = 'block';
+      lmRenderCronActive(data);
+    } else if (data && !data.enabled) {
+      document.getElementById('lmCronPanel').style.display = 'block';
+      lmRenderCronForm();
+    }
+  }).catch(function() {});
+}
+
+function lmOrdinal(n) {
+  var s = ['th','st','nd','rd'];
+  var v = n % 100;
+  return n + (s[(v-20)%10] || s[v] || s[0]);
+}
+
+function lmRenderCronForm() {
+  var badge = document.getElementById('lmCronBadge');
+  badge.style.cssText = 'background:rgba(255,255,255,0.05);color:var(--text-muted);';
+  badge.textContent = 'Not Scheduled';
+
+  var content = document.getElementById('lmCronContent');
+  content.textContent = '';
+
+  var desc = document.createElement('div');
+  desc.style.cssText = 'font-size:12px;color:var(--text-muted);margin-bottom:10px;';
+  desc.textContent = 'Schedule this push to run automatically every month with the same brands and settings.';
+  content.appendChild(desc);
+
+  var form = document.createElement('div');
+  form.style.cssText = 'display:flex;align-items:center;gap:10px;flex-wrap:wrap;';
+
+  var dayLabel = document.createElement('span');
+  dayLabel.style.cssText = 'font-size:12px;color:var(--text-secondary);';
+  dayLabel.textContent = 'Day of month:';
+  form.appendChild(dayLabel);
+
+  var daySelect = document.createElement('select');
+  daySelect.id = 'lmCronDay';
+  daySelect.style.cssText = 'padding:4px 8px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px;';
+  for (var i = 1; i <= 28; i++) {
+    var opt = document.createElement('option');
+    opt.value = i;
+    opt.textContent = i;
+    if (i === 1) opt.selected = true;
+    daySelect.appendChild(opt);
+  }
+  form.appendChild(daySelect);
+
+  var atLabel = document.createElement('span');
+  atLabel.style.cssText = 'font-size:12px;color:var(--text-secondary);';
+  atLabel.textContent = 'at';
+  form.appendChild(atLabel);
+
+  var hourSelect = document.createElement('select');
+  hourSelect.id = 'lmCronHour';
+  hourSelect.style.cssText = 'padding:4px 8px;background:var(--bg-input);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);font-size:12px;';
+  for (var h = 0; h < 24; h++) {
+    var opt = document.createElement('option');
+    opt.value = h;
+    var ampm = h < 12 ? 'AM' : 'PM';
+    var disp = h === 0 ? 12 : (h > 12 ? h - 12 : h);
+    opt.textContent = disp + ':00 ' + ampm + ' PT';
+    if (h === 2) opt.selected = true;
+    hourSelect.appendChild(opt);
+  }
+  form.appendChild(hourSelect);
+
+  var schedBtn = document.createElement('button');
+  schedBtn.style.cssText = 'padding:6px 16px;background:var(--pink);color:#fff;border:none;border-radius:4px;font-size:12px;font-weight:600;cursor:pointer;';
+  schedBtn.textContent = 'Schedule Monthly';
+  schedBtn.addEventListener('click', lmScheduleCron);
+  form.appendChild(schedBtn);
+
+  content.appendChild(form);
+}
+
+function lmRenderCronActive(config) {
+  var badge = document.getElementById('lmCronBadge');
+  badge.style.cssText = 'background:rgba(46,125,50,0.2);color:var(--green);';
+  badge.textContent = 'Active';
+
+  var content = document.getElementById('lmCronContent');
+  content.textContent = '';
+
+  var ampm = config.hour < 12 ? 'AM' : 'PM';
+  var disp = config.hour === 0 ? 12 : (config.hour > 12 ? config.hour - 12 : config.hour);
+  var timeStr = disp + ':00 ' + ampm + ' PT';
+
+  var info = document.createElement('div');
+  info.style.cssText = 'font-size:13px;color:var(--text-primary);line-height:1.8;';
+
+  var line1 = document.createElement('div');
+  line1.textContent = 'Runs on the ' + lmOrdinal(config.dayOfMonth) + ' of every month at ' + timeStr;
+  info.appendChild(line1);
+
+  var line2 = document.createElement('div');
+  line2.style.cssText = 'font-size:12px;color:var(--text-muted);';
+  line2.textContent = (config.brands ? config.brands.length : '?') + ' brands, ' +
+    (config.mode === 'all' ? 'all at once' : 'batches of ' + config.batchSize + ' every ' + config.delayHours + 'h');
+  info.appendChild(line2);
+
+  if (config.createdAt) {
+    var line3 = document.createElement('div');
+    line3.style.cssText = 'font-size:11px;color:var(--text-muted);margin-top:4px;';
+    line3.textContent = 'Created: ' + new Date(config.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
+    info.appendChild(line3);
+  }
+  content.appendChild(info);
+
+  var btnWrap = document.createElement('div');
+  btnWrap.style.marginTop = '10px';
+  var removeBtn = document.createElement('button');
+  removeBtn.style.cssText = 'padding:6px 16px;background:rgba(239,83,80,0.15);color:var(--red);border:1px solid rgba(239,83,80,0.3);border-radius:4px;font-size:12px;cursor:pointer;';
+  removeBtn.textContent = 'Remove Schedule';
+  removeBtn.addEventListener('click', lmRemoveCron);
+  btnWrap.appendChild(removeBtn);
+  content.appendChild(btnWrap);
+}
+
+function lmScheduleCron() {
+  var config = lmQueuedConfig || lmGetPushConfig();
+  if (!config.brands || config.brands.length === 0) {
+    if (lmSelected.size === 0) {
+      showToast('No brands selected for cron schedule.', 'error');
+      return;
+    }
+    config = lmGetPushConfig();
+  }
+  var dayOfMonth = parseInt(document.getElementById('lmCronDay').value, 10);
+  var hour = parseInt(document.getElementById('lmCronHour').value, 10);
+  apiCall('/api/lm/push/cron', {
+    method: 'POST',
+    body: JSON.stringify({
+      brands: config.brands,
+      mode: config.mode,
+      batchSize: config.batchSize,
+      delayHours: config.delayHours,
+      dayOfMonth: dayOfMonth,
+      hour: hour
+    })
+  }).then(function(data) {
+    if (data.success) {
+      lmRenderCronActive(data.config);
+      showToast('Monthly cron scheduled!', 'success');
+    } else {
+      showToast('Failed to schedule: ' + (data.error || 'Unknown error'), 'error');
+    }
+  }).catch(function(err) {
+    showToast('Error: ' + err.message, 'error');
+  });
+}
+
+function lmRemoveCron() {
+  if (!confirm('Remove the monthly push schedule?')) return;
+  apiCall('/api/lm/push/cron', { method: 'DELETE' }).then(function() {
+    lmRenderCronForm();
+    showToast('Monthly schedule removed.', 'info');
+  }).catch(function(err) {
+    showToast('Error: ' + err.message, 'error');
+  });
+}
+
+// ===== TAG MANAGER =====
+var tmLoaded = false;
+var tmVendors = [];
+var tmSelectedVendors = {};
+var tmSelectedVendor = null;
+var tmPollTimer = null;
+
+function loadTagManager() {
+  if (tmLoaded) return;
+  updateTmStats();
+  tmLoaded = true;
+}
+
+function updateTmStats() {
+  apiCall('/api/tags/stats').then(function(resp) {
+    tmVendors = resp.vendors || [];
+    var totals = { total: 0, pending: 0, retry: 0, analyzed: 0, applied: 0 };
+    for (var i = 0; i < tmVendors.length; i++) {
+      var v = tmVendors[i];
+      totals.total += parseInt(v.total) || 0;
+      totals.pending += parseInt(v.pending) || 0;
+      totals.retry += parseInt(v.retry) || 0;
+      totals.analyzed += parseInt(v.analyzed) || 0;
+      totals.applied += parseInt(v.applied) || 0;
+    }
+    document.getElementById('tmFlagged').textContent = totals.total.toLocaleString();
+    document.getElementById('tmPending').textContent = totals.pending.toLocaleString();
+    document.getElementById('tmRetry').textContent = totals.retry.toLocaleString();
+    document.getElementById('tmAnalyzed').textContent = totals.analyzed.toLocaleString();
+    document.getElementById('tmApplied').textContent = totals.applied.toLocaleString();
+    renderTmVendors();
+  }).catch(function(err) {
+    showToast('Error loading tag stats: ' + err.message, 'error');
+  });
+}
+
+function renderTmVendors() {
+  var tbody = document.getElementById('tmVendorBody');
+  while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
+  if (tmVendors.length === 0) {
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.colSpan = 4;
+    td.style.cssText = 'padding:20px;text-align:center;color:var(--text-muted);font-size:13px;';
+    td.textContent = 'No scanned vendors yet. Click "Scan Store" to begin.';
+    tr.appendChild(td);
+    tbody.appendChild(tr);
+    return;
+  }
+  for (var i = 0; i < tmVendors.length; i++) {
+    var v = tmVendors[i];
+    var tr = document.createElement('tr');
+    tr.style.cssText = 'border-bottom:1px solid var(--border);cursor:pointer;';
+    tr.setAttribute('data-vendor', v.vendor);
+    // Checkbox cell
+    var tdCb = document.createElement('td');
+    tdCb.style.padding = '8px';
+    var cb = document.createElement('input');
+    cb.type = 'checkbox';
+    cb.checked = !!tmSelectedVendors[v.vendor];
+    cb.style.accentColor = 'var(--accent)';
+    cb.setAttribute('data-vendor', v.vendor);
+    cb.addEventListener('change', (function(vendor) {
+      return function(e) {
+        tmSelectedVendors[vendor] = e.target.checked;
+        e.stopPropagation();
+      };
+    })(v.vendor));
+    tdCb.appendChild(cb);
+    tr.appendChild(tdCb);
+    // Vendor name cell
+    var tdName = document.createElement('td');
+    tdName.style.cssText = 'padding:8px;font-size:13px;color:var(--text-primary);';
+    tdName.textContent = v.vendor;
+    tr.appendChild(tdName);
+    // Flagged count
+    var tdFlagged = document.createElement('td');
+    tdFlagged.style.cssText = 'padding:8px;text-align:right;font-size:13px;color:var(--orange);';
+    var pendingRetry = (parseInt(v.pending) || 0) + (parseInt(v.retry) || 0);
+    tdFlagged.textContent = pendingRetry.toLocaleString();
+    tr.appendChild(tdFlagged);
+    // Done count
+    var tdDone = document.createElement('td');
+    tdDone.style.cssText = 'padding:8px;text-align:right;font-size:13px;color:var(--green);';
+    tdDone.textContent = (parseInt(v.applied) || 0).toLocaleString();
+    tr.appendChild(tdDone);
+    // Click row to preview
+    tr.addEventListener('click', (function(vendor) {
+      return function() { tmSelectVendor(vendor); };
+    })(v.vendor));
+    tbody.appendChild(tr);
+  }
+}
+
+function tmSelectVendor(vendor) {
+  tmSelectedVendor = vendor;
+  document.getElementById('tmPreviewHeader').textContent = 'Loading ' + vendor + '...';
+  var body = document.getElementById('tmPreviewBody');
+  while (body.firstChild) body.removeChild(body.firstChild);
+  // Highlight selected row
+  var rows = document.querySelectorAll('#tmVendorBody tr');
+  for (var i = 0; i < rows.length; i++) {
+    rows[i].style.background = rows[i].getAttribute('data-vendor') === vendor ? 'var(--bg-hover)' : '';
+  }
+  apiCall('/api/tags/preview?vendor=' + encodeURIComponent(vendor) + '&limit=50').then(function(resp) {
+    document.getElementById('tmPreviewHeader').textContent = vendor + ' — ' + (resp.products || []).length + ' products';
+    renderTmPreview(resp.products || []);
+  }).catch(function(err) {
+    document.getElementById('tmPreviewHeader').textContent = 'Error loading preview';
+    showToast('Preview error: ' + err.message, 'error');
+  });
+}
+
+function renderTmPreview(products) {
+  var body = document.getElementById('tmPreviewBody');
+  while (body.firstChild) body.removeChild(body.firstChild);
+  if (products.length === 0) {
+    var p = document.createElement('p');
+    p.style.cssText = 'color:var(--text-muted);font-size:13px;';
+    p.textContent = 'No products found for this vendor.';
+    body.appendChild(p);
+    return;
+  }
+  for (var i = 0; i < products.length; i++) {
+    var pr = products[i];
+    var card = document.createElement('div');
+    card.style.cssText = 'background:var(--bg-card);border-radius:8px;padding:12px;margin-bottom:8px;border:1px solid var(--border);';
+    // Title + status
+    var header = document.createElement('div');
+    header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;';
+    var titleEl = document.createElement('span');
+    titleEl.style.cssText = 'font-size:13px;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%;';
+    titleEl.textContent = pr.title || 'Untitled';
+    titleEl.title = pr.title || '';
+    header.appendChild(titleEl);
+    var badge = document.createElement('span');
+    badge.style.cssText = 'font-size:11px;padding:2px 8px;border-radius:10px;font-weight:600;';
+    var statusColors = { pending: 'var(--orange)', analyzed: 'var(--blue)', applied: 'var(--green)', retry: 'var(--red)' };
+    badge.style.background = (statusColors[pr.status] || 'var(--text-muted)');
+    badge.style.color = '#fff';
+    badge.textContent = (pr.status || 'unknown').toUpperCase();
+    header.appendChild(badge);
+    card.appendChild(header);
+    // Current tags
+    var curLabel = document.createElement('div');
+    curLabel.style.cssText = 'font-size:11px;color:var(--text-muted);margin-bottom:2px;';
+    curLabel.textContent = 'CURRENT TAGS';
+    card.appendChild(curLabel);
+    var curTags = document.createElement('div');
+    curTags.style.cssText = 'font-size:12px;color:var(--text-secondary);margin-bottom:8px;word-wrap:break-word;max-height:60px;overflow:hidden;';
+    curTags.textContent = pr.currentTags || '(none)';
+    card.appendChild(curTags);
+    // Suggested tags (if analyzed)
+    if (pr.suggestedTags) {
+      var sugLabel = document.createElement('div');
+      sugLabel.style.cssText = 'font-size:11px;color:var(--accent);margin-bottom:2px;';
+      sugLabel.textContent = 'SUGGESTED TAGS';
+      card.appendChild(sugLabel);
+      var sugTags = document.createElement('div');
+      sugTags.style.cssText = 'font-size:12px;color:var(--text-primary);word-wrap:break-word;max-height:60px;overflow:hidden;';
+      sugTags.textContent = pr.suggestedTags;
+      card.appendChild(sugTags);
+    }
+    // AI analysis info
+    if (pr.aiColors || pr.aiStyles) {
+      var aiRow = document.createElement('div');
+      aiRow.style.cssText = 'display:flex;gap:12px;margin-top:6px;font-size:11px;color:var(--text-muted);';
+      if (pr.aiColors) {
+        var colSpan = document.createElement('span');
+        var colors = typeof pr.aiColors === 'string' ? JSON.parse(pr.aiColors) : pr.aiColors;
+        colSpan.textContent = 'Colors: ' + (Array.isArray(colors) ? colors.join(', ') : JSON.stringify(colors));
+        aiRow.appendChild(colSpan);
+      }
+      if (pr.aiStyles) {
+        var stySpan = document.createElement('span');
+        var styles = typeof pr.aiStyles === 'string' ? JSON.parse(pr.aiStyles) : pr.aiStyles;
+        stySpan.textContent = 'Styles: ' + (Array.isArray(styles) ? styles.join(', ') : JSON.stringify(styles));
+        aiRow.appendChild(stySpan);
+      }
+      card.appendChild(aiRow);
+    }
+    body.appendChild(card);
+  }
+}
+
+function tmScan() {
+  showToast('Starting store scan...', 'info');
+  apiCall('/api/tags/scan').then(function(resp) {
+    showToast(resp.message || 'Scan started', 'info');
+    tmStartPolling('scan');
+  }).catch(function(err) {
+    showToast('Scan error: ' + err.message, 'error');
+  });
+}
+
+function tmStartPolling(type) {
+  tmStopPolling();
+  var progressEl = document.getElementById('tmProgress');
+  progressEl.style.display = 'block';
+  tmPollTimer = setInterval(function() {
+    var url = type === 'scan' ? '/api/tags/scan-status' :
+              type === 'batch' ? '/api/tags/batch-status' :
+              '/api/tags/apply-status';
+    apiCall(url).then(function(state) {
+      if (type === 'scan') {
+        var pct = state.total > 0 ? Math.round((state.scanned / Math.max(state.total, state.scanned)) * 100) : 0;
+        document.getElementById('tmProgressLabel').textContent = 'Scanning... ' + (state.scanned || 0).toLocaleString() + ' products (' + (state.flagged || 0) + ' flagged)';
+        document.getElementById('tmProgressPct').textContent = pct + '%';
+        document.getElementById('tmProgressBar').style.width = pct + '%';
+        if (!state.running) {
+          tmStopPolling();
+          showToast('Scan complete! ' + (state.flagged || 0) + ' products flagged.', 'info');
+          tmLoaded = false;
+          loadTagManager();
+        }
+      } else if (type === 'batch') {
+        var pct = state.total > 0 ? Math.round((state.analyzed / state.total) * 100) : 0;
+        document.getElementById('tmProgressLabel').textContent = 'Analyzing... ' + (state.analyzed || 0) + '/' + (state.total || 0) + (state.retrying > 0 ? ' (' + state.retrying + ' retrying)' : '');
+        document.getElementById('tmProgressPct').textContent = pct + '%';
+        document.getElementById('tmProgressBar').style.width = pct + '%';
+        if (state.status !== 'processing') {
+          tmStopPolling();
+          showToast('Batch analysis complete! ' + (state.analyzed || 0) + ' analyzed.', 'info');
+          tmLoaded = false;
+          loadTagManager();
+        }
+      } else {
+        var pct = state.total > 0 ? Math.round((state.applied / state.total) * 100) : 0;
+        document.getElementById('tmProgressLabel').textContent = 'Applying... ' + (state.applied || 0) + '/' + (state.total || 0) + (state.errors > 0 ? ' (' + state.errors + ' errors)' : '');
+        document.getElementById('tmProgressPct').textContent = pct + '%';
+        document.getElementById('tmProgressBar').style.width = pct + '%';
+        if (!state.running) {
+          tmStopPolling();
+          showToast('Apply complete! ' + (state.applied || 0) + ' updated.', 'info');
+          tmLoaded = false;
+          loadTagManager();
+        }
+      }
+    });
+  }, 2000);
+}
+
+function tmStopPolling() {
+  if (tmPollTimer) { clearInterval(tmPollTimer); tmPollTimer = null; }
+  document.getElementById('tmProgress').style.display = 'none';
+}
+
+function tmBatchSubmit() {
+  var selected = [];
+  for (var v in tmSelectedVendors) {
+    if (tmSelectedVendors[v]) selected.push(v);
+  }
+  if (selected.length === 0) {
+    showToast('Select at least one vendor first.', 'error');
+    return;
+  }
+  showToast('Submitting batch for ' + selected.length + ' vendor(s)...', 'info');
+  apiCall('/api/tags/batch-submit', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ vendors: selected, limit: 5000 })
+  }).then(function(resp) {
+    showToast(resp.message || 'Batch submitted', 'info');
+    if (resp.productCount > 0) tmStartPolling('batch');
+  }).catch(function(err) {
+    showToast('Batch error: ' + err.message, 'error');
+  });
+}
+
+function tmApplyAll() {
+  var selected = [];
+  for (var v in tmSelectedVendors) {
+    if (tmSelectedVendors[v]) selected.push(v);
+  }
+  if (selected.length === 0 && tmSelectedVendor) {
+    selected = [tmSelectedVendor];
+  }
+  if (selected.length === 0) {
+    showToast('Select at least one vendor first.', 'error');
+    return;
+  }
+  // Apply one vendor at a time
+  var vendor = selected[0];
+  showToast('Applying tags for ' + vendor + '...', 'info');
+  apiCall('/api/tags/apply', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ vendor: vendor })
+  }).then(function(resp) {
+    showToast(resp.message || 'Applying...', 'info');
+    if (resp.total > 0) tmStartPolling('apply');
+  }).catch(function(err) {
+    showToast('Apply error: ' + err.message, 'error');
+  });
+}
+
+function tmToggleAll(checked) {
+  tmSelectedVendors = {};
+  for (var i = 0; i < tmVendors.length; i++) {
+    tmSelectedVendors[tmVendors[i].vendor] = checked;
+  }
+  var cbs = document.querySelectorAll('#tmVendorBody input[type="checkbox"]');
+  for (var j = 0; j < cbs.length; j++) cbs[j].checked = checked;
+}
+
+// =============================================================
+// STANDARDIZATION ENGINE
+// =============================================================
+var stdPolling = null;
+var stdVendorData = [];
+var stdSortKey = 'vendor';
+var stdSortDir = 'asc'; // 'asc' | 'desc'
+
+function stdGetSortVal(v, key) {
+  var total    = parseInt(v.std_total)    || 0;
+  var analyzed = parseInt(v.std_analyzed) || 0;
+  var applied  = parseInt(v.std_applied)  || 0;
+  var shopify  = parseInt(v.shopify_count) || 0;
+  switch (key) {
+    case 'vendor':   return (v.vendor_name || '').toLowerCase();
+    case 'shopify':  return shopify;
+    case 'scanned':  return total;
+    case 'analyzed': return analyzed;
+    case 'applied':  return applied;
+    case 'pct':      return total > 0 ? applied / total : -1;
+    case 'lastrun':  return v.last_analyzed ? new Date(v.last_analyzed).getTime() : 0;
+    case 'status': {
+      if (applied > 0 && applied >= total && total > 0) return 3;
+      if (analyzed > 0) return 2;
+      if (total > 0) return 1;
+      return 0;
+    }
+    case 'discount': {
+      // Confirmed > Unconfirmed-with-value > Unset, then by pct desc
+      const conf = v.discount_confirmed ? 1 : 0;
+      const pct  = v.vendor_discount_pct != null ? Number(v.vendor_discount_pct) : -1;
+      return conf * 1000 + pct;
+    }
+    default: return 0;
+  }
+}
+
+function stdSortBy(key) {
+  if (stdSortKey === key) {
+    stdSortDir = (stdSortDir === 'asc') ? 'desc' : 'asc';
+  } else {
+    stdSortKey = key;
+    // Numeric columns default to desc (biggest first); text cols asc
+    stdSortDir = (key === 'vendor') ? 'asc' : 'desc';
+  }
+  stdRenderVendorTable();
+}
+
+function stdApplySortIndicators() {
+  var keys = ['vendor','shopify','scanned','analyzed','applied','pct','lastrun','status','discount'];
+  for (var i = 0; i < keys.length; i++) {
+    var th = document.getElementById('stdTh-' + keys[i]);
+    if (!th) continue;
+    var ind = th.querySelector('.std-sort-ind');
+    if (!ind) continue;
+    if (keys[i] === stdSortKey) {
+      ind.textContent = (stdSortDir === 'asc') ? ' \u25B2' : ' \u25BC';
+      th.style.color = 'var(--accent)';
+    } else {
+      ind.textContent = '';
+      th.style.color = 'var(--text-muted)';
+    }
+  }
+}
+
+function loadStandardize() {
+  // Load vendor list
+  apiCall('/api/standardize/vendors').then(function(data) {
+    stdVendorData = data.vendors || [];
+    var sel = document.getElementById('stdVendorSelect');
+    sel.innerHTML = '<option value="">Select vendor...</option>';
+    for (var i = 0; i < stdVendorData.length; i++) {
+      var v = stdVendorData[i];
+      var opt = document.createElement('option');
+      opt.value = v.vendor_name;
+      opt.textContent = v.vendor_name + ' (' + (v.shopify_count || 0) + ' products)';
+      sel.appendChild(opt);
+    }
+    stdRenderVendorTable();
+  }).catch(function(err) {
+    console.error('Failed to load standardize vendors:', err);
+  });
+  // Check engine status
+  stdPollStatus();
+}
+
+function stdRenderVendorTable() {
+  var body = document.getElementById('stdVendorBody');
+  body.innerHTML = '';
+  var totalShopify = 0, totalScanned = 0, totalAnalyzed = 0, totalApplied = 0;
+
+  // Sort a copy so we never mutate the source array (status poller re-uses it)
+  var stdSorted = stdVendorData.slice().sort(function(a, b) {
+    var av = stdGetSortVal(a, stdSortKey);
+    var bv = stdGetSortVal(b, stdSortKey);
+    var cmp = (av < bv) ? -1 : (av > bv) ? 1 : 0;
+    return stdSortDir === 'asc' ? cmp : -cmp;
+  });
+  stdApplySortIndicators();
+
+  for (var i = 0; i < stdSorted.length; i++) {
+    var v = stdSorted[i];
+    var total = parseInt(v.std_total) || 0;
+    var analyzed = parseInt(v.std_analyzed) || 0;
+    var applied = parseInt(v.std_applied) || 0;
+    var shopify = parseInt(v.shopify_count) || 0;
+    var errors = parseInt(v.std_errors) || 0;
+    var pct = total > 0 ? Math.round((applied / total) * 100) : 0;
+    var pctAnalyzed = total > 0 ? Math.round((analyzed / total) * 100) : 0;
+
+    totalShopify += shopify;
+    totalScanned += total;
+    totalAnalyzed += analyzed;
+    totalApplied += applied;
+
+    var statusIcon = '';
+    if (applied > 0 && applied >= total && total > 0) statusIcon = '<span style="color:var(--green);font-size:16px;">&#10004;</span>';
+    else if (analyzed > 0) statusIcon = '<span style="color:var(--blue);font-size:13px;">&#9679;</span>';
+    else if (total > 0) statusIcon = '<span style="color:var(--orange);font-size:13px;">&#9679;</span>';
+    else statusIcon = '<span style="color:var(--text-muted);font-size:13px;">&#9711;</span>';
+
+    var lastRun = v.last_analyzed ? new Date(v.last_analyzed).toLocaleDateString('en-US', { month:'short', day:'numeric', timeZone:'America/Los_Angeles' }) : '--';
+
+    var pctBar = total > 0
+      ? '<div style="background:var(--bg-input);border-radius:3px;height:4px;width:60px;display:inline-block;vertical-align:middle;margin-right:4px;"><div style="background:' + (pct >= 100 ? 'var(--green)' : pct > 0 ? 'var(--blue)' : 'var(--text-muted)') + ';height:100%;width:' + pct + '%;border-radius:3px;"></div></div>' + pct + '%'
+      : '<span style="color:var(--text-muted);">--</span>';
+
+    var tr = document.createElement('tr');
+    tr.style.cssText = 'border-bottom:1px solid var(--border);cursor:pointer;';
+    tr.setAttribute('onclick', 'stdPreviewVendor("' + esc(v.vendor_name).replace(/"/g, '&quot;') + '")');
+    tr.innerHTML = '<td style="padding:8px 12px;font-size:13px;font-weight:500;">' + esc(v.vendor_name) + (v.is_private_label ? ' <span style="color:var(--pink);font-size:9px;">PL</span>' : '') + '</td>'
+      + '<td style="text-align:right;padding:8px;font-size:12px;color:var(--text-secondary);">' + shopify.toLocaleString() + '</td>'
+      + '<td style="text-align:right;padding:8px;font-size:12px;color:var(--text-secondary);">' + (total > 0 ? total.toLocaleString() : '--') + '</td>'
+      + '<td style="text-align:center;padding:8px;font-size:12px;color:var(--blue);">' + (analyzed > 0 ? analyzed.toLocaleString() : '--') + '</td>'
+      + '<td style="text-align:center;padding:8px;font-size:12px;color:var(--green);">' + (applied > 0 ? applied.toLocaleString() : '--') + '</td>'
+      + '<td style="text-align:center;padding:8px;font-size:12px;">' + pctBar + '</td>'
+      + '<td style="text-align:right;padding:8px;font-size:11px;color:var(--text-muted);">' + lastRun + '</td>'
+      + '<td style="text-align:center;padding:8px;">' + statusIcon + '</td>'
+      + '<td class="std-disc-cell" data-vendor="' + esc(v.vendor_name).replace(/"/g, '&quot;') + '" data-vendor-code="' + esc(v.vendor_code) + '" style="text-align:center;padding:8px;"></td>';
+    body.appendChild(tr);
+
+    // Paint the discount cell programmatically (input + confirm button) so
+    // click events don't bubble up to the row's onclick="stdPreviewVendor(...)"
+    (function(v, row){
+      var cell = row.querySelector('.std-disc-cell');
+      var wrap = document.createElement('div');
+      wrap.style.cssText = 'display:inline-flex;gap:4px;align-items:center;';
+      var inp = document.createElement('input');
+      inp.type = 'number'; inp.min = '0'; inp.max = '80'; inp.step = '0.5';
+      inp.placeholder = '—';
+      inp.style.cssText = 'width:46px;background:var(--bg-input);border:1px solid var(--border);color:var(--text-primary);padding:2px 4px;border-radius:3px;font:11px inherit;text-align:right;';
+      if (v.vendor_discount_pct != null) inp.value = v.vendor_discount_pct;
+      inp.addEventListener('click', function(e){ e.stopPropagation(); });
+      inp.addEventListener('keydown', function(e){ e.stopPropagation(); });
+      var btn = document.createElement('button');
+      btn.type = 'button';
+      var confirmed = !!v.discount_confirmed;
+      btn.textContent = confirmed ? '✓' : 'OK';
+      btn.title = confirmed ? 'Confirmed — click to re-save with new %' : 'Confirm this % for ' + v.vendor_code;
+      btn.style.cssText = 'background:' + (confirmed ? 'rgba(34,197,94,.15)' : 'rgba(251,191,36,.15)') +
+        ';color:' + (confirmed ? 'var(--green)' : 'var(--orange)') +
+        ';border:1px solid ' + (confirmed ? 'var(--green)' : 'var(--orange)') +
+        ';padding:2px 6px;border-radius:3px;font:10px inherit;font-weight:700;cursor:pointer;';
+      btn.addEventListener('click', function(e){
+        e.stopPropagation();
+        var pct = inp.value.trim() === '' ? null : Number(inp.value);
+        btn.textContent = '...'; btn.disabled = true;
+        apiCall('/api/vendor/discount', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ vendor_code: v.vendor_code, discount_pct: pct, confirmed: true })
+        }).then(function(d){
+          if (d && d.ok) {
+            btn.textContent = '✓';
+            btn.style.background = 'rgba(34,197,94,.15)';
+            btn.style.color = 'var(--green)';
+            btn.style.borderColor = 'var(--green)';
+            v.vendor_discount_pct = pct;
+            v.discount_confirmed = true;
+          } else {
+            btn.textContent = 'err';
+          }
+          btn.disabled = false;
+        }).catch(function(){ btn.textContent = 'err'; btn.disabled = false; });
+      });
+      wrap.appendChild(inp); wrap.appendChild(btn);
+      cell.appendChild(wrap);
+    })(v, tr);
+  }
+}
+
+function stdStart() {
+  var vendor = document.getElementById('stdVendorSelect').value;
+  if (!vendor) { showToast('Select a vendor first', 'error'); return; }
+  var analyzeOnly = document.getElementById('stdAnalyzeOnly').checked;
+  var limit = parseInt(document.getElementById('stdLimit').value) || 2;
+
+  apiCall('/api/standardize/start', {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ vendor: vendor, analyzeOnly: analyzeOnly, limit: limit })
+  }).then(function(data) {
+    if (data.error) { showToast(data.error, 'error'); return; }
+    showToast('Engine started for ' + vendor, 'success');
+    document.getElementById('stdStartBtn').style.display = 'none';
+    document.getElementById('stdStopBtn').style.display = '';
+    document.getElementById('stdProgressWrap').style.display = 'block';
+    stdStartPolling();
+  }).catch(function(err) {
+    showToast('Start error: ' + err.message, 'error');
+  });
+}
+
+function stdStop() {
+  apiCall('/api/standardize/stop', { method: 'POST' }).then(function(data) {
+    showToast(data.message || 'Stopping...', 'info');
+  });
+}
+
+function stdStartPolling() {
+  if (stdPolling) clearInterval(stdPolling);
+  stdPolling = setInterval(stdPollStatus, 2000);
+}
+
+function stdPollStatus() {
+  apiCall('/api/standardize/status').then(function(s) {
+    // Update status badge
+    var badge = document.getElementById('stdEngineStatus');
+    if (s.running) {
+      badge.textContent = 'RUNNING';
+      badge.style.cssText = 'padding:4px 12px;border-radius:12px;font-size:11px;font-weight:700;background:rgba(34,197,94,0.15);color:var(--green);';
+      document.getElementById('stdStartBtn').style.display = 'none';
+      document.getElementById('stdStopBtn').style.display = '';
+      document.getElementById('stdProgressWrap').style.display = 'block';
+    } else {
+      badge.textContent = s.paused ? 'PAUSED' : 'IDLE';
+      badge.style.cssText = 'padding:4px 12px;border-radius:12px;font-size:11px;font-weight:700;background:var(--bg-input);color:var(--text-muted);';
+      document.getElementById('stdStartBtn').style.display = '';
+      document.getElementById('stdStopBtn').style.display = 'none';
+      if (s.total > 0 && !s.running) {
+        document.getElementById('stdProgressWrap').style.display = 'block';
+      }
+      if (stdPolling && !s.running) {
+        clearInterval(stdPolling);
+        stdPolling = null;
+        // Refresh vendor table
+        loadStandardize();
+      }
+    }
+
+    // Update progress
+    if (s.total > 0) {
+      var pct = Math.round(((s.analyzed + s.applied + s.skipped + s.errors) / s.total) * 100);
+      document.getElementById('stdProgressBar').style.width = pct + '%';
+      document.getElementById('stdProgressPct').textContent = pct + '%';
+      document.getElementById('stdProgressLabel').textContent = s.running ? ('Processing ' + (s.currentVendor || '')) : (s.paused ? 'Paused' : 'Complete');
+      document.getElementById('stdAnalyzedCount').textContent = s.analyzed;
+      document.getElementById('stdAppliedCount').textContent = s.applied;
+      document.getElementById('stdErrorCount').textContent = s.errors;
+      document.getElementById('stdSkippedCount').textContent = s.skipped;
+      document.getElementById('stdCurrentProduct').textContent = s.lastProductTitle || '--';
+    }
+
+    // Update log
+    if (s.log && s.log.length > 0) {
+      var logPanel = document.getElementById('stdLogPanel');
+      logPanel.innerHTML = '';
+      for (var i = 0; i < s.log.length; i++) {
+        var entry = s.log[i];
+        var div = document.createElement('div');
+        div.style.cssText = 'padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.03);';
+        var isError = entry.msg.toLowerCase().includes('error') || entry.msg.toLowerCase().includes('fatal');
+        div.innerHTML = '<span style="color:var(--text-muted);">' + esc(entry.ts) + '</span> <span style="color:' + (isError ? 'var(--red)' : 'var(--text-secondary)') + ';">' + esc(entry.msg) + '</span>';
+        logPanel.appendChild(div);
+      }
+    }
+  }).catch(function() {});
+}
+
+function stdPreviewVendor(vendor) {
+  apiCall('/api/standardize/preview/' + encodeURIComponent(vendor)).then(function(data) {
+    var panel = document.getElementById('stdPreviewPanel');
+    panel.style.display = 'block';
+    document.getElementById('stdPreviewTitle').textContent = vendor + ' — Analyzed Products';
+    var body = document.getElementById('stdPreviewBody');
+    body.innerHTML = '';
+
+    var products = data.products || [];
+    if (products.length === 0) {
+      body.innerHTML = '<div style="color:var(--text-muted);font-size:13px;">No products analyzed yet. Select this vendor and click START.</div>';
+      return;
+    }
+
+    for (var i = 0; i < products.length; i++) {
+      var p = products[i];
+      var colors = [];
+      try { colors = JSON.parse(p.ai_colors || '[]'); } catch(e) {}
+      var hexCodes = [];
+      try { hexCodes = JSON.parse(p.ai_hex_codes || '[]'); } catch(e) {}
+      var styles = [];
+      try { styles = JSON.parse(p.ai_styles || '[]'); } catch(e) {}
+      var patterns = [];
+      try { patterns = JSON.parse(p.ai_patterns || '[]'); } catch(e) {}
+
+      var swatches = '';
+      for (var h = 0; h < hexCodes.length; h++) {
+        swatches += '<span style="display:inline-block;width:16px;height:16px;border-radius:50%;background:' + esc(hexCodes[h]) + ';border:1px solid rgba(255,255,255,0.2);margin-right:3px;vertical-align:middle;" title="' + esc(colors[h] || hexCodes[h]) + '"></span>';
+      }
+
+      var statusColor = p.status === 'applied' ? 'var(--green)' : p.status === 'analyzed' ? 'var(--blue)' : p.status === 'error' ? 'var(--red)' : 'var(--text-muted)';
+
+      var card = document.createElement('div');
+      card.style.cssText = 'background:var(--bg-input);border-radius:8px;padding:12px;margin-bottom:8px;';
+      card.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">'
+        + '<div><strong style="color:var(--text-primary);font-size:13px;">' + esc(p.title_standardized || p.title) + '</strong>'
+        + ' <span style="color:' + statusColor + ';font-size:10px;font-weight:700;text-transform:uppercase;">' + esc(p.status) + '</span></div>'
+        + '<a href="https://admin.shopify.com/store/designer-laboratory-sandbox/products/' + esc(p.id) + '" target="_blank" style="color:var(--accent);font-size:11px;">View &#8599;</a>'
+        + '</div>'
+        + '<div style="display:flex;gap:16px;font-size:12px;flex-wrap:wrap;">'
+        + '<div><span style="color:var(--text-muted);">Colors:</span> ' + swatches + '</div>'
+        + '<div><span style="color:var(--text-muted);">Styles:</span> <span style="color:var(--text-secondary);">' + esc(styles.join(', ') || '--') + '</span></div>'
+        + '<div><span style="color:var(--text-muted);">Patterns:</span> <span style="color:var(--text-secondary);">' + esc(patterns.join(', ') || '--') + '</span></div>'
+        + '<div><span style="color:var(--text-muted);">Material:</span> <span style="color:var(--text-secondary);">' + esc(p.ai_material || '--') + '</span></div>'
+        + '<div><span style="color:var(--text-muted);">Audience:</span> <span style="color:var(--text-secondary);">' + esc(p.ai_audience || '--') + '</span></div>'
+        + (p.ai_is_mural ? '<div><span style="color:var(--orange);font-weight:700;">MURAL</span></div>' : '')
+        + '</div>'
+        + (p.ai_description ? '<div style="margin-top:6px;font-size:11px;color:var(--text-muted);font-style:italic;">' + esc(p.ai_description) + '</div>' : '');
+      body.appendChild(card);
+    }
+  }).catch(function(err) {
+    showToast('Preview error: ' + err.message, 'error');
+  });
+}
+
+// ─── New Launches ──────────────────────────────────────────
+var nlLoaded = false;
+document.getElementById('nlDateRange').addEventListener('change', function() { loadNewLaunches(true); });
+
+function loadNewLaunches(force) {
+  if (nlLoaded && !force) return;
+  var days = document.getElementById('nlDateRange').value;
+  var countEl = document.getElementById('nlCount');
+  var loadingEl = document.getElementById('nlLoading');
+  var content = document.getElementById('nlContent');
+
+  loadingEl.style.display = 'inline';
+  countEl.textContent = '';
+
+  fetch('/api/new-launches?days=' + days)
+    .then(function(r) { return r.json(); })
+    .then(function(data) {
+      loadingEl.style.display = 'none';
+      nlLoaded = true;
+      if (!data.success) { countEl.textContent = 'Error'; return; }
+      countEl.textContent = data.total + ' products';
+
+      // Build DOM with safe methods
+      while (content.firstChild) content.removeChild(content.firstChild);
+
+      if (data.total === 0) {
+        var empty = document.createElement('div');
+        empty.style.cssText = 'text-align:center;padding:60px;color:var(--text-muted);font-size:14px;';
+        empty.textContent = 'No new products in the last ' + days + ' days';
+        content.appendChild(empty);
+        return;
+      }
+
+      var dates = Object.keys(data.grouped);
+      for (var d = 0; d < dates.length; d++) {
+        var dateKey = dates[d];
+        var items = data.grouped[dateKey];
+
+        // Date header
+        var dateHeader = document.createElement('div');
+        dateHeader.style.cssText = 'display:flex;align-items:center;gap:12px;margin:20px 0 12px;';
+        var dateLabel = document.createElement('div');
+        dateLabel.style.cssText = 'font-size:14px;font-weight:700;color:var(--accent);white-space:nowrap;';
+        dateLabel.textContent = dateKey;
+        var dateBadge = document.createElement('span');
+        dateBadge.style.cssText = 'font-size:11px;background:var(--accent-light);color:var(--accent);padding:2px 8px;border-radius:10px;font-weight:600;';
+        dateBadge.textContent = items.length + ' items';
+        var dateLine = document.createElement('div');
+        dateLine.style.cssText = 'flex:1;height:1px;background:var(--border);';
+        dateHeader.appendChild(dateLabel);
+        dateHeader.appendChild(dateBadge);
+        dateHeader.appendChild(dateLine);
+        content.appendChild(dateHeader);
+
+        // Thumbnail grid
+        var grid = document.createElement('div');
+        grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;';
+
+        for (var i = 0; i < items.length; i++) {
+          (function(p) {
+            var card = document.createElement('div');
+            card.style.cssText = 'background:var(--bg-card);border:1px solid var(--border);border-radius:10px;overflow:hidden;transition:transform 0.15s,box-shadow 0.15s;cursor:pointer;';
+            card.addEventListener('mouseenter', function() { this.style.transform='translateY(-3px)'; this.style.boxShadow='0 6px 20px rgba(0,0,0,0.4)'; });
+            card.addEventListener('mouseleave', function() { this.style.transform=''; this.style.boxShadow=''; });
+            card.addEventListener('click', function() {
+              window.open('https://www.designerwallcoverings.com/products/' + encodeURIComponent(p.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/,'')), '_blank');
+            });
+
+            // Image
+            var imgWrap = document.createElement('div');
+            imgWrap.style.cssText = 'width:100%;aspect-ratio:1;overflow:hidden;background:var(--bg-input);position:relative;';
+            if (p.image) {
+              var img = document.createElement('img');
+              img.src = p.image;
+              img.alt = p.title;
+              img.loading = 'lazy';
+              img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
+              img.onerror = function() { this.parentNode.style.display = 'flex'; this.parentNode.style.alignItems = 'center'; this.parentNode.style.justifyContent = 'center'; this.style.display = 'none'; var t = document.createElement('span'); t.textContent = 'No Image'; t.style.cssText = 'color:var(--text-muted);font-size:11px;'; this.parentNode.appendChild(t); };
+              imgWrap.appendChild(img);
+            } else {
+              var noImg = document.createElement('div');
+              noImg.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:11px;';
+              noImg.textContent = 'No Image';
+              imgWrap.appendChild(noImg);
+            }
+            card.appendChild(imgWrap);
+
+            // Info
+            var info = document.createElement('div');
+            info.style.cssText = 'padding:8px 10px;';
+
+            var sku = document.createElement('div');
+            sku.style.cssText = 'font-size:10px;font-family:monospace;color:var(--accent);margin-bottom:2px;';
+            sku.textContent = p.sku;
+            info.appendChild(sku);
+
+            var title = document.createElement('div');
+            title.style.cssText = 'font-size:12px;font-weight:600;color:var(--text-primary);line-height:1.3;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;';
+            title.textContent = p.title;
+            info.appendChild(title);
+
+            var meta = document.createElement('div');
+            meta.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-top:4px;';
+            var vendor = document.createElement('span');
+            vendor.style.cssText = 'font-size:10px;color:var(--text-muted);max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
+            vendor.textContent = p.vendor;
+            var typeBadge = document.createElement('span');
+            typeBadge.style.cssText = 'font-size:9px;padding:1px 6px;border-radius:3px;background:var(--blue-bg);color:var(--blue);font-weight:600;';
+            typeBadge.textContent = p.type || 'Product';
+            meta.appendChild(vendor);
+            meta.appendChild(typeBadge);
+            info.appendChild(meta);
+
+            card.appendChild(info);
+            grid.appendChild(card);
+          })(items[i]);
+        }
+
+        content.appendChild(grid);
+      }
+    })
+    .catch(function(err) {
+      loadingEl.style.display = 'none';
+      countEl.textContent = 'Error: ' + err.message;
+    });
+}
+
+// ============================================================
+// GLOBAL CATALOG SEARCH & VIEW ALL
+// ============================================================
+var srCurrentPage = 1;
+var srTotalPages = 1;
+var srMode = ''; // 'search' or 'viewall'
+var srQuery = '';
+var srVendor = '';
+var srDebounceTimer = null;
+
+// Enter key triggers search
+document.getElementById('globalSearchInput').addEventListener('keydown', function(e) {
+  if (e.key === 'Enter') {
+    e.preventDefault();
+    performGlobalSearch();
+  }
+});
+
+// Debounced live search on typing (400ms)
+document.getElementById('globalSearchInput').addEventListener('input', function() {
+  var val = this.value.trim();
+  if (val.length >= 3) {
+    clearTimeout(srDebounceTimer);
+    srDebounceTimer = setTimeout(function() { performGlobalSearch(); }, 400);
+  } else if (val.length === 0 && srMode === 'search') {
+    closeSearchResults();
+  }
+});
+
+function performGlobalSearch() {
+  var q = document.getElementById('globalSearchInput').value.trim();
+  if (!q) { showToast('Enter a search term', 'info'); return; }
+  srMode = 'search';
+  srQuery = q;
+  srCurrentPage = 1;
+  srVendor = '';
+  document.getElementById('srVendorFilter').style.display = 'none';
+  fetchSearchResults();
+}
+
+function viewAllProducts() {
+  srMode = 'viewall';
+  srQuery = '';
+  srCurrentPage = 1;
+  srVendor = '';
+  document.getElementById('globalSearchInput').value = '';
+  // Build vendor filter dropdown
+  var sel = document.getElementById('srVendorFilter');
+  sel.style.display = 'inline-block';
+  while (sel.options.length > 1) sel.remove(1);
+  if (vendors && vendors.length) {
+    var sorted = vendors.slice().sort(function(a,b) { return a.vendor_name.localeCompare(b.vendor_name); });
+    for (var i = 0; i < sorted.length; i++) {
+      if (sorted[i].catalog_table && sorted[i].catalog_count > 0) {
+        var opt = document.createElement('option');
+        opt.value = sorted[i].vendor_code;
+        opt.textContent = sorted[i].vendor_name + ' (' + (sorted[i].catalog_count || 0) + ')';
+        sel.appendChild(opt);
+      }
+    }
+  }
+  fetchSearchResults();
+}
+
+function filterByVendor() {
+  srVendor = document.getElementById('srVendorFilter').value;
+  srCurrentPage = 1;
+  fetchSearchResults();
+}
+
+function srBuildLoadingRow() {
+  var tr = document.createElement('tr');
+  var td = document.createElement('td');
+  td.colSpan = 11;
+  var div = document.createElement('div');
+  div.className = 'sr-loading';
+  var spinner = document.createElement('div');
+  spinner.className = 'spinner-ring';
+  div.appendChild(spinner);
+  var txt = document.createTextNode('Searching across all vendor catalogs...');
+  div.appendChild(txt);
+  td.appendChild(div);
+  tr.appendChild(td);
+  return tr;
+}
+
+function srBuildEmptyRow(message) {
+  var tr = document.createElement('tr');
+  var td = document.createElement('td');
+  td.colSpan = 11;
+  var div = document.createElement('div');
+  div.className = 'sr-empty';
+  div.textContent = message;
+  td.appendChild(div);
+  tr.appendChild(td);
+  return tr;
+}
+
+function fetchSearchResults() {
+  var panel = document.getElementById('searchResultsPanel');
+  var tbody = document.getElementById('srTableBody');
+  var title = document.getElementById('srTitle');
+  var count = document.getElementById('srCount');
+  var pagination = document.getElementById('srPagination');
+
+  panel.classList.add('active');
+
+  // Show loading
+  while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
+  tbody.appendChild(srBuildLoadingRow());
+  title.textContent = srMode === 'search' ? 'Search Results' : 'All Catalog Items';
+  count.textContent = '';
+  pagination.style.display = 'none';
+
+  var url;
+  if (srMode === 'search') {
+    url = '/api/catalog-search?q=' + encodeURIComponent(srQuery) + '&page=' + srCurrentPage + '&limit=50';
+  } else {
+    url = '/api/catalog-all?page=' + srCurrentPage + '&limit=50';
+    if (srVendor) url += '&vendor=' + encodeURIComponent(srVendor);
+  }
+
+  apiCall(url).then(function(res) {
+    if (!res.success) {
+      while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
+      tbody.appendChild(srBuildEmptyRow('Error: ' + (res.error || 'Unknown')));
+      return;
+    }
+
+    var data = res.data || [];
+    srTotalPages = res.pages || 1;
+    var total = res.total || 0;
+
+    if (srMode === 'search') {
+      title.textContent = 'Search Results for "' + srQuery + '"';
+    } else {
+      title.textContent = srVendor ? 'All Items — ' + (data.length > 0 ? data[0].vendor_name : srVendor) : 'All Catalog Items';
+    }
+    count.textContent = total.toLocaleString() + ' products found';
+
+    // Clear tbody
+    while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
+
+    if (data.length === 0) {
+      tbody.appendChild(srBuildEmptyRow('No products found' + (srMode === 'search' ? ' matching "' + srQuery + '"' : '')));
+      pagination.style.display = 'none';
+      return;
+    }
+
+    // Render results using safe DOM methods
+    for (var i = 0; i < data.length; i++) {
+      var item = data[i];
+      var tr = document.createElement('tr');
+
+      // Image
+      var tdImg = document.createElement('td');
+      if (item.image_url) {
+        var img = document.createElement('img');
+        img.className = 'sr-thumb';
+        img.src = item.image_url;
+        img.alt = (item.pattern_name || '') + ' ' + (item.color_name || '');
+        img.loading = 'lazy';
+        img.onerror = function() {
+          this.style.display = 'none';
+          var ph = document.createElement('div');
+          ph.className = 'sr-thumb-placeholder';
+          ph.textContent = '?';
+          this.parentNode.appendChild(ph);
+        };
+        tdImg.appendChild(img);
+      } else {
+        var placeholder = document.createElement('div');
+        placeholder.className = 'sr-thumb-placeholder';
+        placeholder.textContent = '?';
+        tdImg.appendChild(placeholder);
+      }
+      tr.appendChild(tdImg);
+
+      // Vendor
+      var tdVendor = document.createElement('td');
+      var vendorSpan = document.createElement('span');
+      vendorSpan.className = 'sr-vendor-badge';
+      vendorSpan.textContent = item.vendor_name || item.vendor_code || '';
+      tdVendor.appendChild(vendorSpan);
+      tr.appendChild(tdVendor);
+
+      // Pattern
+      var tdPattern = document.createElement('td');
+      tdPattern.style.fontWeight = '600';
+      tdPattern.style.maxWidth = '200px';
+      tdPattern.style.overflow = 'hidden';
+      tdPattern.style.textOverflow = 'ellipsis';
+      tdPattern.title = item.pattern_name || '';
+      tdPattern.textContent = item.pattern_name || '';
+      tr.appendChild(tdPattern);
+
+      // Color
+      var tdColor = document.createElement('td');
+      tdColor.textContent = item.color_name || '';
+      tdColor.style.maxWidth = '150px';
+      tdColor.style.overflow = 'hidden';
+      tdColor.style.textOverflow = 'ellipsis';
+      tdColor.title = item.color_name || '';
+      tr.appendChild(tdColor);
+
+      // MFR SKU
+      var tdSku = document.createElement('td');
+      tdSku.className = 'sr-sku';
+      tdSku.textContent = item.mfr_sku || '';
+      tr.appendChild(tdSku);
+
+      // DW SKU
+      var tdDwSku = document.createElement('td');
+      tdDwSku.className = 'sr-sku';
+      tdDwSku.style.color = item.dw_sku ? 'var(--accent)' : 'var(--text-muted)';
+      tdDwSku.textContent = item.dw_sku || '';
+      tr.appendChild(tdDwSku);
+
+      // Collection
+      var tdColl = document.createElement('td');
+      tdColl.textContent = item.collection || '';
+      tdColl.style.fontSize = '11px';
+      tdColl.style.color = 'var(--text-secondary)';
+      tdColl.style.maxWidth = '150px';
+      tdColl.style.overflow = 'hidden';
+      tdColl.style.textOverflow = 'ellipsis';
+      tdColl.title = item.collection || '';
+      tr.appendChild(tdColl);
+
+      // Product Type
+      var tdType = document.createElement('td');
+      tdType.textContent = item.product_type || '';
+      tdType.style.fontSize = '11px';
+      tdType.style.color = 'var(--text-secondary)';
+      tr.appendChild(tdType);
+
+      // Width
+      var tdWidth = document.createElement('td');
+      tdWidth.textContent = item.width || '';
+      tdWidth.style.fontFamily = 'monospace';
+      tdWidth.style.fontSize = '11px';
+      tr.appendChild(tdWidth);
+
+      // FULLPRODUCT Gate Status
+      var tdGate = document.createElement('td');
+      tdGate.style.textAlign = 'center';
+      var gateBar = document.createElement('div');
+      gateBar.className = 'gate-bar';
+
+      // Define gate checks: [label, value, isHard]
+      var gateChecks = [
+        ['Image', item.image_url, true],
+        ['Width', item.width, true],
+        ['MFR SKU', item.mfr_sku, true],
+        ['Color', item.color_name, true],
+        ['Repeat', item.repeat_v || item.repeat_h, false],
+        ['Material', item.material, false],
+        ['All Images', item.all_images, false],
+        ['Spec Sheet', item.spec_sheet, false],
+        ['About Vendor', item.about_vendor, false]
+      ];
+
+      var hardPass = 0, hardTotal = 0, softPass = 0, softTotal = 0;
+      for (var g = 0; g < gateChecks.length; g++) {
+        var gc = gateChecks[g];
+        var hasVal = gc[1] && String(gc[1]).trim() !== '' && gc[1] !== 'null';
+        var gdot = document.createElement('span');
+        gdot.className = 'gate-dot ' + (hasVal ? 'pass' : (gc[2] ? 'fail-hard' : 'fail-soft'));
+        gdot.title = gc[0] + ': ' + (hasVal ? 'OK' : 'MISSING') + (gc[2] ? ' (REQUIRED)' : '');
+        gateBar.appendChild(gdot);
+        if (gc[2]) { hardTotal++; if (hasVal) hardPass++; }
+        else { softTotal++; if (hasVal) softPass++; }
+      }
+
+      var gateScore = document.createElement('span');
+      var totalPass = hardPass + softPass;
+      var totalChecks = hardTotal + softTotal;
+      gateScore.className = 'gate-score ' + (hardPass < hardTotal ? 'blocked' : (totalPass === totalChecks ? 'ready' : 'partial'));
+      gateScore.textContent = totalPass + '/' + totalChecks;
+      gateScore.title = hardPass < hardTotal
+        ? 'BLOCKED: Missing ' + (hardTotal - hardPass) + ' required field(s)'
+        : (totalPass === totalChecks ? 'Shopify Ready' : totalPass + ' of ' + totalChecks + ' fields populated');
+      gateBar.appendChild(gateScore);
+
+      tdGate.appendChild(gateBar);
+      tr.appendChild(tdGate);
+
+      // Shopify Status
+      var tdShopify = document.createElement('td');
+      tdShopify.style.textAlign = 'center';
+      var dot = document.createElement('span');
+      dot.className = 'sr-shopify-dot ' + (item.shopify_product_id ? 'on-shopify' : 'not-on-shopify');
+      dot.title = item.shopify_product_id ? 'On Shopify (ID: ' + item.shopify_product_id + ')' : 'Not on Shopify';
+      tdShopify.appendChild(dot);
+      tr.appendChild(tdShopify);
+
+      // Click row to open product URL if available
+      if (item.product_url) {
+        tr.style.cursor = 'pointer';
+        tr.title = 'Click to open vendor page';
+        tr.addEventListener('click', (function(purl) {
+          return function() { window.open(purl, '_blank'); };
+        })(item.product_url));
+      }
+
+      tbody.appendChild(tr);
+    }
+
+    // Pagination
+    if (srTotalPages > 1) {
+      pagination.style.display = 'flex';
+      document.getElementById('srPrevBtn').disabled = srCurrentPage <= 1;
+      document.getElementById('srNextBtn').disabled = srCurrentPage >= srTotalPages;
+      document.getElementById('srPageInfo').textContent = 'Page ' + srCurrentPage + ' of ' + srTotalPages.toLocaleString();
+    } else {
+      pagination.style.display = 'none';
+    }
+  }).catch(function(err) {
+    while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
+    tbody.appendChild(srBuildEmptyRow('Error: ' + err.message));
+  });
+}
+
+function srPrevPage() {
+  if (srCurrentPage > 1) {
+    srCurrentPage--;
+    fetchSearchResults();
+    document.getElementById('searchResultsPanel').scrollTop = 0;
+  }
+}
+
+function srNextPage() {
+  if (srCurrentPage < srTotalPages) {
+    srCurrentPage++;
+    fetchSearchResults();
+    document.getElementById('searchResultsPanel').scrollTop = 0;
+  }
+}
+
+function closeSearchResults() {
+  document.getElementById('searchResultsPanel').classList.remove('active');
+  srMode = '';
+}
+
+// ═══════════════════════════════════════════════════════════════
+// Product Actions Tab
+// ═══════════════════════════════════════════════════════════════
+var paProducts = [];
+var paSelected = {};
+var paCursor = null;
+var paHasMore = false;
+
+function paSearch() {
+  var q = document.getElementById('paSearchInput').value.trim();
+  if (!q) return;
+  var status = document.getElementById('paStatusFilter').value;
+  document.getElementById('paSearching').style.display = 'inline';
+  document.getElementById('paTableBody').textContent = '';
+  paProducts = [];
+  paSelected = {};
+  paCursor = null;
+  paUpdateBulkBar();
+
+  var query = q;
+  if (status) query += ' status:' + status.toLowerCase();
+
+  fetch('/api/product-actions/search?q=' + encodeURIComponent(query), {headers:{'Authorization':''  /* auth via httpOnly vcc_session cookie — no embedded credential */}})
+    .then(function(r){return r.json()})
+    .then(function(data){
+      document.getElementById('paSearching').style.display = 'none';
+      paProducts = data.products || [];
+      paCursor = data.cursor || null;
+      paHasMore = data.hasMore || false;
+      document.getElementById('paResultCount').textContent = paProducts.length + ' products' + (paHasMore ? ' (more available)' : '');
+      paRenderTable();
+    })
+    .catch(function(e){
+      document.getElementById('paSearching').style.display = 'none';
+      document.getElementById('paResultCount').textContent = 'Error: ' + e.message;
+    });
+}
+
+function paLoadMore() {
+  if (!paCursor) return;
+  var q = document.getElementById('paSearchInput').value.trim();
+  var status = document.getElementById('paStatusFilter').value;
+  var query = q;
+  if (status) query += ' status:' + status.toLowerCase();
+
+  fetch('/api/product-actions/search?q=' + encodeURIComponent(query) + '&cursor=' + encodeURIComponent(paCursor), {headers:{'Authorization':''  /* auth via httpOnly vcc_session cookie — no embedded credential */}})
+    .then(function(r){return r.json()})
+    .then(function(data){
+      paProducts = paProducts.concat(data.products || []);
+      paCursor = data.cursor || null;
+      paHasMore = data.hasMore || false;
+      document.getElementById('paResultCount').textContent = paProducts.length + ' products' + (paHasMore ? ' (more available)' : '');
+      paRenderTable();
+    });
+}
+
+function paCreateCell(text, styles) {
+  var td = document.createElement('td');
+  td.textContent = text || '';
+  td.style.cssText = styles || 'padding:6px 8px;';
+  return td;
+}
+
+function paRenderTable() {
+  var tbody = document.getElementById('paTableBody');
+  tbody.textContent = '';
+  for (var i = 0; i < paProducts.length; i++) {
+    var p = paProducts[i];
+    var tr = document.createElement('tr');
+    tr.style.borderBottom = '1px solid var(--border)';
+    if (paSelected[p.id]) tr.style.background = 'rgba(88,166,255,0.06)';
+
+    var statusColor = p.status === 'ACTIVE' ? '#3fb950' : p.status === 'DRAFT' ? '#d29922' : '#8b949e';
+    var statusBg = p.status === 'ACTIVE' ? 'rgba(63,185,80,0.12)' : p.status === 'DRAFT' ? 'rgba(210,153,34,0.12)' : 'rgba(139,148,158,0.12)';
+
+    // Checkbox cell
+    var cbTd = document.createElement('td');
+    cbTd.style.cssText = 'padding:6px 8px;';
+    var cb = document.createElement('input');
+    cb.type = 'checkbox';
+    cb.dataset.pid = p.id;
+    cb.checked = !!paSelected[p.id];
+    cb.setAttribute('onchange', "paToggleOne('"+p.id+"',this.checked)");
+    cbTd.appendChild(cb);
+    tr.appendChild(cbTd);
+
+    // Image cell
+    var imgTd = document.createElement('td');
+    imgTd.style.cssText = 'padding:6px 8px;';
+    if (p.image) {
+      var img = document.createElement('img');
+      img.src = p.image;
+      img.style.cssText = 'width:40px;height:40px;object-fit:cover;border-radius:4px;';
+      imgTd.appendChild(img);
+    } else {
+      var placeholder = document.createElement('div');
+      placeholder.style.cssText = 'width:40px;height:40px;background:var(--border);border-radius:4px;';
+      imgTd.appendChild(placeholder);
+    }
+    tr.appendChild(imgTd);
+
+    // Title cell with link
+    var titleTd = document.createElement('td');
+    titleTd.style.cssText = 'padding:6px 8px;max-width:350px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
+    titleTd.title = p.title;
+    var link = document.createElement('a');
+    link.href = 'https://admin.shopify.com/store/designer-laboratory-sandbox/products/' + p.id;
+    link.target = '_blank';
+    link.style.cssText = 'color:var(--accent);text-decoration:none;';
+    link.textContent = p.title;
+    titleTd.appendChild(link);
+    tr.appendChild(titleTd);
+
+    // Vendor, Status, SKU, Type, Updated cells
+    tr.appendChild(paCreateCell(p.vendor, 'padding:6px 8px;color:var(--text-muted);font-size:12px;'));
+
+    var statusTd = document.createElement('td');
+    statusTd.style.cssText = 'padding:6px 8px;';
+    var badge = document.createElement('span');
+    badge.style.cssText = 'padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;background:'+statusBg+';color:'+statusColor+';';
+    badge.textContent = p.status;
+    statusTd.appendChild(badge);
+    tr.appendChild(statusTd);
+
+    tr.appendChild(paCreateCell(p.sku, 'padding:6px 8px;font-family:monospace;font-size:11px;color:var(--text-muted);'));
+    tr.appendChild(paCreateCell(p.type, 'padding:6px 8px;font-size:12px;color:var(--text-muted);'));
+    tr.appendChild(paCreateCell(p.updated, 'padding:6px 8px;font-size:11px;color:var(--text-muted);'));
+
+    tbody.appendChild(tr);
+  }
+  var pag = document.getElementById('paPagination');
+  pag.textContent = '';
+  if (paHasMore) {
+    var btn = document.createElement('button');
+    btn.textContent = 'Load More';
+    btn.style.cssText = 'padding:8px 24px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;';
+    btn.onclick = paLoadMore;
+    pag.appendChild(btn);
+  }
+}
+
+function paToggleOne(pid, checked) {
+  if (checked) paSelected[pid] = true;
+  else delete paSelected[pid];
+  paUpdateBulkBar();
+}
+
+function paToggleAll(checked) {
+  for (var i = 0; i < paProducts.length; i++) {
+    if (checked) paSelected[paProducts[i].id] = true;
+    else delete paSelected[paProducts[i].id];
+  }
+  paRenderTable();
+  paUpdateBulkBar();
+}
+
+function paUpdateBulkBar() {
+  var count = Object.keys(paSelected).length;
+  var bar = document.getElementById('paBulkBar');
+  bar.style.display = count > 0 ? 'flex' : 'none';
+  document.getElementById('paSelectedCount').textContent = count + ' selected';
+}
+
+function paBulkStatus(newStatus) {
+  var ids = Object.keys(paSelected);
+  if (!ids.length) return;
+  if (!confirm('Set ' + ids.length + ' products to ' + newStatus.toUpperCase() + '?')) return;
+
+  document.getElementById('paBulkProgress').textContent = 'Processing...';
+  fetch('/api/product-actions/bulk-status', {
+    method: 'POST',
+    headers: {'Content-Type':'application/json','Authorization':''  /* auth via httpOnly vcc_session cookie — no embedded credential */},
+    body: JSON.stringify({ids: ids, status: newStatus})
+  })
+  .then(function(r){return r.json()})
+  .then(function(data){
+    document.getElementById('paBulkProgress').textContent = 'Done: ' + data.success + ' updated, ' + data.errors + ' errors';
+    for (var i = 0; i < paProducts.length; i++) {
+      if (paSelected[paProducts[i].id]) paProducts[i].status = newStatus.toUpperCase();
+    }
+    paSelected = {};
+    paUpdateBulkBar();
+    paRenderTable();
+    document.getElementById('paSelectAll').checked = false;
+  })
+  .catch(function(e){
+    document.getElementById('paBulkProgress').textContent = 'Error: ' + e.message;
+  });
+}
+
+function paBulkRedirect() {
+  var ids = Object.keys(paSelected);
+  var target = document.getElementById('paRedirectTarget').value.trim() || '/';
+  if (!ids.length) return;
+  if (!confirm('Create redirects for ' + ids.length + ' products to "' + target + '"?')) return;
+
+  var handles = [];
+  for (var i = 0; i < paProducts.length; i++) {
+    if (paSelected[paProducts[i].id]) handles.push(paProducts[i].handle);
+  }
+
+  document.getElementById('paBulkProgress').textContent = 'Creating redirects...';
+  fetch('/api/product-actions/bulk-redirect', {
+    method: 'POST',
+    headers: {'Content-Type':'application/json','Authorization':''  /* auth via httpOnly vcc_session cookie — no embedded credential */},
+    body: JSON.stringify({handles: handles, target: target})
+  })
+  .then(function(r){return r.json()})
+  .then(function(data){
+    document.getElementById('paBulkProgress').textContent = 'Redirects: ' + data.created + ' created, ' + data.skipped + ' skipped, ' + data.errors + ' errors';
+  })
+  .catch(function(e){
+    document.getElementById('paBulkProgress').textContent = 'Error: ' + e.message;
+  });
+}
+
+</script>
+
+<!-- Vendor Trade Portal Credentials Panel -->
+<div id="credentialsPanel" style="margin:32px 24px 0 24px;">
+  <div id="credentialsPanelHeader" onclick="toggleCredentialsPanel()" style="
+    background:#13152a;border:1px solid #2a2d45;border-radius:10px;
+    padding:16px 20px;display:flex;align-items:center;gap:12px;
+    cursor:pointer;user-select:none;transition:background 0.2s;
+  " onmouseover="this.style.background='#1a1d35'" onmouseout="this.style.background='#13152a'">
+    <span style="font-size:18px;">&#128272;</span>
+    <span style="color:#e8e8f0;font-size:15px;font-weight:600;flex:1;">Trade Portal Credentials <span id="credCountBadge" style="color:#7c3aed;font-size:13px;font-weight:400;">(loading...)</span></span>
+    <span id="credChevron" style="color:#8888a8;font-size:18px;transition:transform 0.2s;">&#9660;</span>
+  </div>
+  <div id="credentialsBody" style="display:none;background:#0f0f1a;border:1px solid #2a2d45;border-top:none;border-radius:0 0 10px 10px;overflow:hidden;">
+    <div style="padding:12px 20px;border-bottom:1px solid #1e2140;">
+      <span style="color:#8888a8;font-size:13px;">Trade portal logins for vendor websites. Changes save instantly to the database.</span>
+    </div>
+    <div style="overflow-x:auto;">
+      <table id="credentialsTable" style="width:100%;border-collapse:collapse;">
+        <thead>
+          <tr style="background:#13152a;">
+            <th style="padding:10px 16px;text-align:left;color:#8888a8;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;min-width:180px;">Vendor</th>
+            <th style="padding:10px 16px;text-align:left;color:#8888a8;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;min-width:200px;">Portal URL</th>
+            <th style="padding:10px 16px;text-align:left;color:#8888a8;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;min-width:180px;">Username</th>
+            <th style="padding:10px 16px;text-align:left;color:#8888a8;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;min-width:180px;">Password</th>
+            <th style="padding:10px 16px;text-align:center;color:#8888a8;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;width:80px;">Save</th>
+          </tr>
+        </thead>
+        <tbody id="credentialsTableBody">
+          <tr id="cred-loading-row"><td colspan="5" style="padding:24px;text-align:center;color:#8888a8;font-size:13px;">Loading credentials...</td></tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
+
+<script>
+var credPanelOpen = false;
+var credVendors = [];
+
+function toggleCredentialsPanel() {
+  credPanelOpen = !credPanelOpen;
+  document.getElementById('credentialsBody').style.display = credPanelOpen ? 'block' : 'none';
+  document.getElementById('credChevron').style.transform = credPanelOpen ? 'rotate(180deg)' : '';
+  if (credPanelOpen && credVendors.length === 0) {
+    loadCredentials();
+  }
+}
+
+function loadCredentials() {
+  apiCall('/api/vendor-credentials').then(function(data) {
+    if (!data.ok) { return; }
+    credVendors = data.vendors;
+    document.getElementById('credCountBadge').textContent = '(' + data.vendors.length + ' vendors)';
+    renderCredentialsTable(data.vendors);
+  }).catch(function(e) {
+    var tbody = document.getElementById('credentialsTableBody');
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.colSpan = 5;
+    td.style.cssText = 'padding:24px;text-align:center;color:#ef4444;font-size:13px;';
+    td.textContent = 'Error loading credentials: ' + e.message;
+    tr.appendChild(td);
+    tbody.replaceChildren(tr);
+  });
+}
+
+function renderCredentialsTable(vendors) {
+  var tbody = document.getElementById('credentialsTableBody');
+  tbody.replaceChildren();
+
+  if (!vendors || vendors.length === 0) {
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.colSpan = 5;
+    td.style.cssText = 'padding:24px;text-align:center;color:#8888a8;font-size:13px;';
+    td.textContent = 'No vendors with credentials found.';
+    tr.appendChild(td);
+    tbody.appendChild(tr);
+    return;
+  }
+
+  vendors.forEach(function(v, i) {
+    var tr = document.createElement('tr');
+    tr.style.cssText = 'background:' + (i % 2 === 0 ? '#0f0f1a' : '#111120') + ';border-bottom:1px solid #1e2140;';
+    tr.id = 'cred-row-' + i;
+
+    // Vendor name cell
+    var tdName = document.createElement('td');
+    tdName.style.cssText = 'padding:10px 16px;color:#e8e8f0;font-size:13px;font-weight:500;';
+    tdName.textContent = v.vendor_name;
+    tr.appendChild(tdName);
+
+    // Portal URL cell
+    var tdUrl = document.createElement('td');
+    tdUrl.style.cssText = 'padding:10px 16px;';
+    var portalUrl = v.login_url || v.website_url || '';
+    if (portalUrl) {
+      var link = document.createElement('a');
+      link.href = portalUrl;
+      link.target = '_blank';
+      link.rel = 'noopener noreferrer';
+      link.style.cssText = 'color:#7c3aed;text-decoration:none;font-size:13px;';
+      link.title = portalUrl;
+      link.textContent = truncateUrl(portalUrl);
+      tdUrl.appendChild(link);
+    } else {
+      tdUrl.style.color = '#55557a';
+      tdUrl.textContent = '\u2014';
+    }
+    tr.appendChild(tdUrl);
+
+    // Username cell
+    var tdUser = document.createElement('td');
+    tdUser.style.cssText = 'padding:10px 16px;';
+    var userInput = document.createElement('input');
+    userInput.type = 'text';
+    userInput.id = 'cred-user-' + i;
+    userInput.value = v.trade_username || '';
+    userInput.placeholder = 'username';
+    userInput.autocomplete = 'off';
+    userInput.style.cssText = 'background:#1e2140;border:1px solid #2a2d45;border-radius:6px;color:#e8e8f0;font-size:13px;padding:6px 10px;width:100%;outline:none;';
+    userInput.addEventListener('focus', function(){ this.style.borderColor='#7c3aed'; });
+    userInput.addEventListener('blur', function(){ this.style.borderColor='#2a2d45'; });
+    tdUser.appendChild(userInput);
+    tr.appendChild(tdUser);
+
+    // Password cell
+    var tdPass = document.createElement('td');
+    tdPass.style.cssText = 'padding:10px 16px;';
+    var passInput = document.createElement('input');
+    passInput.type = 'password';
+    passInput.id = 'cred-pass-' + i;
+    passInput.value = v.trade_password || '';
+    passInput.placeholder = 'password';
+    passInput.autocomplete = 'new-password';
+    passInput.style.cssText = 'background:#1e2140;border:1px solid #2a2d45;border-radius:6px;color:#e8e8f0;font-size:13px;padding:6px 10px;width:100%;outline:none;';
+    passInput.addEventListener('focus', function(){ this.style.borderColor='#7c3aed'; });
+    passInput.addEventListener('blur', function(){ this.style.borderColor='#2a2d45'; });
+    tdPass.appendChild(passInput);
+    tr.appendChild(tdPass);
+
+    // Save cell
+    var tdSave = document.createElement('td');
+    tdSave.style.cssText = 'padding:10px 16px;text-align:center;';
+    var btn = document.createElement('button');
+    btn.id = 'cred-btn-' + i;
+    btn.textContent = 'Save';
+    btn.style.cssText = 'background:#7c3aed;color:#fff;border:none;border-radius:6px;padding:6px 14px;font-size:12px;font-weight:600;cursor:pointer;';
+    btn.addEventListener('mouseover', function(){ this.style.background='#6d28d9'; });
+    btn.addEventListener('mouseout', function(){ this.style.background='#7c3aed'; });
+    (function(vendorName, idx){ btn.addEventListener('click', function(){ saveCredential(vendorName, idx); }); })(v.vendor_name, i);
+    tdSave.appendChild(btn);
+
+    var okMark = document.createElement('span');
+    okMark.id = 'cred-ok-' + i;
+    okMark.style.cssText = 'display:none;color:#22c55e;font-size:16px;margin-left:6px;';
+    okMark.textContent = '\u2713';
+    tdSave.appendChild(okMark);
+    tr.appendChild(tdSave);
+
+    tbody.appendChild(tr);
+  });
+}
+
+function saveCredential(vendorName, idx) {
+  var userEl = document.getElementById('cred-user-' + idx);
+  var passEl = document.getElementById('cred-pass-' + idx);
+  var btn = document.getElementById('cred-btn-' + idx);
+  var ok = document.getElementById('cred-ok-' + idx);
+  if (!userEl || !passEl) return;
+  btn.disabled = true;
+  btn.textContent = '...';
+  apiCall('/api/vendor-credentials', {
+    method: 'POST',
+    body: JSON.stringify({ vendor_name: vendorName, trade_username: userEl.value, trade_password: passEl.value })
+  }).then(function(data) {
+    btn.disabled = false;
+    btn.textContent = 'Save';
+    if (data.ok) {
+      ok.style.display = 'inline';
+      setTimeout(function() { ok.style.display = 'none'; }, 2500);
+    } else {
+      alert('Error saving: ' + (data.error || 'unknown error'));
+    }
+  }).catch(function(e) {
+    btn.disabled = false;
+    btn.textContent = 'Save';
+    alert('Network error: ' + e.message);
+  });
+}
+
+function truncateUrl(url) {
+  try {
+    var u = new URL(url);
+    var path = u.pathname !== '/' ? u.pathname.slice(0, 20) + (u.pathname.length > 20 ? '\u2026' : '') : '';
+    return u.hostname + path;
+  } catch(e) {
+    return url.slice(0, 30) + (url.length > 30 ? '\u2026' : '');
+  }
+}
+
+// Load badge count on page load (even when panel is collapsed)
+apiCall('/api/vendor-credentials').then(function(data) {
+  if (data.ok) {
+    document.getElementById('credCountBadge').textContent = '(' + data.vendors.length + ' vendors)';
+    credVendors = data.vendors;
+  }
+}).catch(function(){});
+</script>
+
+<!-- Color Enrichment Progress Panel -->
+<div id="colorEnrichPanel" style="margin:20px 24px 0 24px;">
+  <div id="colorEnrichHeader" onclick="toggleColorEnrichPanel()" style="
+    background:#13152a;border:1px solid #2a2d45;border-radius:10px;
+    padding:16px 20px;display:flex;align-items:center;gap:12px;
+    cursor:pointer;user-select:none;transition:background 0.2s;
+  " onmouseover="this.style.background='#1a1d35'" onmouseout="this.style.background='#13152a'">
+    <span style="font-size:18px;">&#127912;</span>
+    <span style="color:#e8e8f0;font-size:15px;font-weight:600;flex:1;">Color Enrichment Progress <span id="colorEnrichBadge" style="color:#7c3aed;font-size:13px;font-weight:400;">(loading...)</span></span>
+    <span id="colorEnrichChevron" style="color:#8888a8;font-size:18px;transition:transform 0.2s;">&#9660;</span>
+  </div>
+  <div id="colorEnrichBody" style="display:none;background:#0f0f1a;border:1px solid #2a2d45;border-top:none;border-radius:0 0 10px 10px;overflow:hidden;padding:20px;">
+    <div id="colorEnrichLoading" style="color:#8888a8;text-align:center;padding:20px;">Loading color enrichment data...</div>
+    <div id="colorEnrichContent" style="display:none;">
+      <div id="colorEnrichSummary" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-bottom:20px;"></div>
+      <div id="colorEnrichBarWrap" style="margin-bottom:20px;">
+        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
+          <span style="color:#e8e8f0;font-size:13px;font-weight:600;">Overall Progress</span>
+          <span id="colorEnrichPctLabel" style="color:#7c3aed;font-size:14px;font-weight:700;">0%</span>
+        </div>
+        <div style="background:#1e2140;border-radius:8px;height:20px;overflow:hidden;position:relative;">
+          <div id="colorEnrichBar" style="height:100%;border-radius:8px;background:linear-gradient(90deg,#7c3aed,#22c55e);transition:width 0.6s ease;width:0%;"></div>
+        </div>
+        <div id="colorEnrichEta" style="color:#8888a8;font-size:11px;margin-top:4px;"></div>
+      </div>
+      <div style="overflow-x:auto;max-height:400px;overflow-y:auto;">
+        <table style="width:100%;border-collapse:collapse;">
+          <thead style="position:sticky;top:0;z-index:1;">
+            <tr style="background:#13152a;">
+              <th style="padding:8px 12px;text-align:left;color:#8888a8;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;">Vendor</th>
+              <th style="padding:8px 12px;text-align:right;color:#8888a8;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;">Total</th>
+              <th style="padding:8px 12px;text-align:right;color:#8888a8;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;">Enriched</th>
+              <th style="padding:8px 12px;text-align:right;color:#8888a8;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;">Remaining</th>
+              <th style="padding:8px 12px;text-align:left;color:#8888a8;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;border-bottom:1px solid #2a2d45;min-width:180px;">Progress</th>
+            </tr>
+          </thead>
+          <tbody id="colorEnrichTableBody"></tbody>
+        </table>
+      </div>
+    </div>
+  </div>
+</div>
+
+<script>
+var colorEnrichOpen = false;
+var colorEnrichLoaded = false;
+
+function toggleColorEnrichPanel() {
+  colorEnrichOpen = !colorEnrichOpen;
+  document.getElementById('colorEnrichBody').style.display = colorEnrichOpen ? 'block' : 'none';
+  document.getElementById('colorEnrichChevron').style.transform = colorEnrichOpen ? 'rotate(180deg)' : '';
+  if (colorEnrichOpen && !colorEnrichLoaded) {
+    loadColorEnrichment();
+  }
+}
+
+function loadColorEnrichment() {
+  apiCall('/api/enrichment/color-progress').then(function(data) {
+    if (!data.success) {
+      document.getElementById('colorEnrichLoading').textContent = 'Error: ' + (data.error || 'unknown');
+      return;
+    }
+    colorEnrichLoaded = true;
+    document.getElementById('colorEnrichLoading').style.display = 'none';
+    document.getElementById('colorEnrichContent').style.display = 'block';
+
+    var s = data.summary;
+
+    // Update badge
+    document.getElementById('colorEnrichBadge').textContent = '(' + s.pct + '% complete)';
+
+    // Summary cards
+    var summaryEl = document.getElementById('colorEnrichSummary');
+    summaryEl.replaceChildren();
+    var cards = [
+      { label: 'Vendors', value: s.total_vendors, color: '#7c3aed' },
+      { label: 'Active SKUs', value: s.total_active.toLocaleString(), color: '#3b82f6' },
+      { label: 'Enriched', value: s.enriched.toLocaleString(), color: '#22c55e' },
+      { label: 'Remaining', value: s.remaining.toLocaleString(), color: s.remaining > 0 ? '#f59e0b' : '#22c55e' },
+      { label: 'ETA (@ 900/hr)', value: s.eta_hours > 0 ? s.eta_hours + 'h' : 'Done', color: s.eta_hours > 0 ? '#06b6d4' : '#22c55e' }
+    ];
+    for (var ci = 0; ci < cards.length; ci++) {
+      var card = document.createElement('div');
+      card.style.cssText = 'background:#1e2140;border:1px solid #2a2d45;border-radius:8px;padding:10px 14px;text-align:center;';
+      var valSpan = document.createElement('div');
+      valSpan.style.cssText = 'font-size:22px;font-weight:700;color:' + cards[ci].color + ';';
+      valSpan.textContent = cards[ci].value;
+      var lblSpan = document.createElement('div');
+      lblSpan.style.cssText = 'font-size:11px;color:#8888a8;text-transform:uppercase;letter-spacing:0.5px;margin-top:2px;';
+      lblSpan.textContent = cards[ci].label;
+      card.appendChild(valSpan);
+      card.appendChild(lblSpan);
+      summaryEl.appendChild(card);
+    }
+
+    // Overall progress bar
+    document.getElementById('colorEnrichPctLabel').textContent = s.pct + '%';
+    document.getElementById('colorEnrichBar').style.width = s.pct + '%';
+    if (s.eta_hours > 0) {
+      document.getElementById('colorEnrichEta').textContent = s.remaining.toLocaleString() + ' products remaining. ETA: ~' + s.eta_hours + ' hours at 900 products/hour.';
+    } else {
+      document.getElementById('colorEnrichEta').textContent = 'All active products with images have been enriched.';
+    }
+
+    // Vendor table
+    var tbody = document.getElementById('colorEnrichTableBody');
+    tbody.replaceChildren();
+    var vendors = data.vendors;
+    for (var vi = 0; vi < vendors.length; vi++) {
+      var v = vendors[vi];
+      var tr = document.createElement('tr');
+      tr.style.cssText = 'border-bottom:1px solid #1e2140;' + (vi % 2 === 0 ? 'background:#0f0f1a;' : 'background:#111120;');
+
+      // Vendor name
+      var tdName = document.createElement('td');
+      tdName.style.cssText = 'padding:6px 12px;color:#e8e8f0;font-size:12px;font-weight:500;white-space:nowrap;';
+      tdName.textContent = v.vendor;
+      tr.appendChild(tdName);
+
+      // Total
+      var tdTotal = document.createElement('td');
+      tdTotal.style.cssText = 'padding:6px 12px;text-align:right;color:#e8e8f0;font-size:12px;font-family:monospace;';
+      tdTotal.textContent = v.total.toLocaleString();
+      tr.appendChild(tdTotal);
+
+      // Enriched
+      var tdEnr = document.createElement('td');
+      tdEnr.style.cssText = 'padding:6px 12px;text-align:right;font-size:12px;font-family:monospace;color:' + (v.enriched === v.total ? '#22c55e' : '#e8e8f0') + ';';
+      tdEnr.textContent = v.enriched.toLocaleString();
+      tr.appendChild(tdEnr);
+
+      // Remaining
+      var tdRem = document.createElement('td');
+      tdRem.style.cssText = 'padding:6px 12px;text-align:right;font-size:12px;font-family:monospace;color:' + (v.remaining > 0 ? '#f59e0b' : '#22c55e') + ';';
+      tdRem.textContent = v.remaining.toLocaleString();
+      tr.appendChild(tdRem);
+
+      // Progress bar
+      var tdProg = document.createElement('td');
+      tdProg.style.cssText = 'padding:6px 12px;';
+      var barWrap = document.createElement('div');
+      barWrap.style.cssText = 'display:flex;align-items:center;gap:8px;';
+      var barOuter = document.createElement('div');
+      barOuter.style.cssText = 'flex:1;background:#1e2140;border-radius:4px;height:12px;overflow:hidden;';
+      var barInner = document.createElement('div');
+      var barColor = v.pct >= 100 ? '#22c55e' : v.pct >= 50 ? '#7c3aed' : '#f59e0b';
+      barInner.style.cssText = 'height:100%;border-radius:4px;background:' + barColor + ';width:' + v.pct + '%;transition:width 0.3s;';
+      barOuter.appendChild(barInner);
+      var pctLabel = document.createElement('span');
+      pctLabel.style.cssText = 'font-size:11px;font-weight:600;color:' + barColor + ';min-width:40px;text-align:right;';
+      pctLabel.textContent = v.pct + '%';
+      barWrap.appendChild(barOuter);
+      barWrap.appendChild(pctLabel);
+      tdProg.appendChild(barWrap);
+      tr.appendChild(tdProg);
+
+      tbody.appendChild(tr);
+    }
+  }).catch(function(e) {
+    document.getElementById('colorEnrichLoading').textContent = 'Error loading: ' + e.message;
+    document.getElementById('colorEnrichLoading').style.color = '#ef4444';
+  });
+}
+
+// Load badge count on page load (even when panel is collapsed)
+apiCall('/api/enrichment/color-progress').then(function(data) {
+  if (data.success) {
+    document.getElementById('colorEnrichBadge').textContent = '(' + data.summary.pct + '% complete)';
+  }
+}).catch(function(){});
+</script>
+
+<!-- Live Activity Ticker Bar -->
+<div class="live-ticker">
+  <div class="ticker-pulse"></div>
+  <span class="ticker-label">LIVE</span>
+  <div class="ticker-scroll">
+    <div class="ticker-entries" id="tickerEntries">
+      <span class="ticker-entry"><span class="ticker-time">--:--:--</span> <span class="ticker-type info">[INFO]</span> Loading activity feed...</span>
+    </div>
+  </div>
+  <span class="ticker-count" id="tickerCount">--</span>
+</div>
+</body>
+</html>`;
+}
+
+// --- Enrichment Progress API Endpoints ---
+
+// GET /api/enrichment/progress — Per-vendor phase completion percentages
+app.get('/api/enrichment/progress', async (req, res) => {
+  try {
+    const result = await pool.query(`
+      SELECT et.vendor_code as vendor, COUNT(*) as total,
+        COUNT(et.phase2_specs_at) as specs, COUNT(et.phase3_ai_at) as ai,
+        COUNT(et.phase6_rooms_at) as rooms, COUNT(et.phase8_shopify_at) as shopify,
+        COUNT(*) - COUNT(et.phase3_ai_at) as gap
+      FROM enrichment_tracking et
+      JOIN vendor_registry vr ON vr.vendor_code = et.vendor_code
+      WHERE COALESCE(vr.skip_shopify, false) = false
+      GROUP BY et.vendor_code ORDER BY total DESC
+    `);
+    res.json(result.rows.map(r => ({
+      vendor: r.vendor,
+      total: parseInt(r.total),
+      specs: parseInt(r.specs),
+      ai: parseInt(r.ai),
+      rooms: parseInt(r.rooms),
+      shopify: parseInt(r.shopify),
+      gap: parseInt(r.gap)
+    })));
+  } catch (err) {
+    console.error('[Victor] enrichment/progress error:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// GET /api/enrichment/progress/:vendor — Detailed breakdown for one vendor
+app.get('/api/enrichment/progress/:vendor', async (req, res) => {
+  try {
+    const { vendor } = req.params;
+    const result = await pool.query(`
+      SELECT COUNT(*) as total,
+        COUNT(phase1_scraped_at) as phase1,
+        COUNT(phase2_specs_at) as phase2,
+        COUNT(phase3_ai_at) as phase3,
+        COUNT(phase4_silas_at) as phase4,
+        COUNT(phase5_vcc_at) as phase5,
+        COUNT(phase6_rooms_at) as phase6,
+        COUNT(phase7_spin_at) as phase7,
+        COUNT(phase8_shopify_at) as phase8
+      FROM enrichment_tracking
+      WHERE vendor_code = $1
+    `, [vendor]);
+    const r = result.rows[0];
+    res.json({
+      vendor: vendor,
+      total: parseInt(r.total),
+      phase1: parseInt(r.phase1),
+      phase2: parseInt(r.phase2),
+      phase3: parseInt(r.phase3),
+      phase4: parseInt(r.phase4),
+      phase5: parseInt(r.phase5),
+      phase6: parseInt(r.phase6),
+      phase7: parseInt(r.phase7),
+      phase8: parseInt(r.phase8)
+    });
+  } catch (err) {
+    console.error('[Victor] enrichment/progress/:vendor error:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// GET /api/enrichment/cost — Gemini cost estimate for pending phases
+app.get('/api/enrichment/cost', async (req, res) => {
+  try {
+    const result = await pool.query(`
+      SELECT
+        COUNT(*) FILTER (WHERE phase3_ai_at IS NULL) as phase3_pending,
+        COUNT(*) FILTER (WHERE phase6_rooms_at IS NULL) as phase6_pending
+      FROM enrichment_tracking
+    `);
+    const r = result.rows[0];
+    const p3 = parseInt(r.phase3_pending);
+    const p6 = parseInt(r.phase6_pending);
+    // Phase 3 (AI tags): ~$0.0006/product (Gemini Flash, ~600 tokens avg)
+    const p3Cost = Math.round(p3 * 0.0006);
+    // Phase 6 (Room renders): ~$0.024/product (Gemini image generation)
+    const p6Cost = Math.round(p6 * 0.024);
+    const totalCost = p3Cost + p6Cost;
+    res.json({
+      phase3_pending: p3,
+      phase3_cost: '$' + p3Cost,
+      phase6_pending: p6,
+      phase6_cost: '$' + p6Cost,
+      total_estimate: '$' + totalCost
+    });
+  } catch (err) {
+    console.error('[Victor] enrichment/cost error:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// GET /api/enrichment/global — System-wide enrichment totals
+app.get('/api/enrichment/global', async (req, res) => {
+  try {
+    const result = await pool.query(`
+      SELECT
+        COUNT(*) as total,
+        COUNT(phase1_scraped_at) as phase1,
+        COUNT(phase2_specs_at) as phase2,
+        COUNT(phase3_ai_at) as phase3,
+        COUNT(phase4_silas_at) as phase4,
+        COUNT(phase5_vcc_at) as phase5,
+        COUNT(phase6_rooms_at) as phase6,
+        COUNT(phase7_spin_at) as phase7,
+        COUNT(phase8_shopify_at) as phase8,
+        COUNT(DISTINCT vendor_code) as vendors,
+        COUNT(*) - COUNT(phase3_ai_at) as ai_gap,
+        COUNT(*) - COUNT(phase8_shopify_at) as shopify_gap
+      FROM enrichment_tracking
+    `);
+    const r = result.rows[0];
+    res.json({
+      total: parseInt(r.total),
+      vendors: parseInt(r.vendors),
+      phase1: parseInt(r.phase1),
+      phase2: parseInt(r.phase2),
+      phase3: parseInt(r.phase3),
+      phase4: parseInt(r.phase4),
+      phase5: parseInt(r.phase5),
+      phase6: parseInt(r.phase6),
+      phase7: parseInt(r.phase7),
+      phase8: parseInt(r.phase8),
+      ai_gap: parseInt(r.ai_gap),
+      shopify_gap: parseInt(r.shopify_gap)
+    });
+  } catch (err) {
+    console.error('[Victor] enrichment/global error:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// ─── NEW: Vendor Registry v2 Info ─────────────────────────────────────────
+// GET /api/registry/overview — System-wide agent registry stats
+app.get('/api/registry/overview', async (req, res) => {
+  try {
+    const vendorCount = await pool.query('SELECT COUNT(*) FROM vendor_registry');
+    const v2Count = await pool.query("SELECT COUNT(*) FROM vendor_registry WHERE agent_port IS NOT NULL");
+    const restrictions = {
+      never_touch: ['cowtan_tout', 'colefax_fowler'],
+      no_ai_tags: ['phillip_jeffries'],
+      no_new_products: ['york_wallcoverings'],
+      fabrics_only_skip: ['scalamandre'],
+      private_label: ['bespoke', 'phillipe_romano', 'architectural_wallcoverings'],
+    };
+    const killed = [
+      'CEO Dashboard (7120)', 'CFO Dashboard (7121)', 'COO Dashboard (7122)',
+      'CTO Dashboard (7124)', 'VP Ops Dashboard (7125)', 'Boardroom (9815)',
+      'Boardroom 3D (7681)', 'Web Uptime (9813)', 'Server Agent (9814)',
+      'Pierce Guardian (9630)', 'Legal Compliance (9878)', 'Accounting (9882)',
+      'Purchasing (9880)', 'Sample Vault (9879)', 'Trend Research (9883)',
+      'Competitive Intel (9886)', '38 vendor v1 agents',
+    ];
+    res.json({
+      total_vendors: parseInt(vendorCount.rows[0].count),
+      v2_agents_deployed: parseInt(v2Count.rows[0].count),
+      vendor_restrictions: restrictions,
+      killed_agents: killed,
+      architecture: 'YOLO + Full Monty core. Central Shopify API queue. All v1 agents killed session #111b.',
+    });
+  } catch (err) {
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// ─── NEW: Live Enrichment Dashboard ──────────────────────────────────────
+// GET /api/enrichment/live — Active enrichment processes + recent completions
+app.get('/api/enrichment/live', async (req, res) => {
+  try {
+    // Recent Phase 3 completions (last hour)
+    const recent = await pool.query(`
+      SELECT vendor_code, COUNT(*) as completed,
+        MAX(phase3_ai_at) as last_enriched,
+        AVG(phase3_tokens) as avg_tokens
+      FROM enrichment_tracking
+      WHERE phase3_ai_at > NOW() - INTERVAL '1 hour'
+      GROUP BY vendor_code ORDER BY completed DESC
+    `);
+
+    // Top 20 vendors by gap (most work remaining)
+    const gaps = await pool.query(`
+      SELECT vendor_code, COUNT(*) as total,
+        COUNT(phase3_ai_at) as done,
+        COUNT(*) - COUNT(phase3_ai_at) as remaining,
+        ROUND(COUNT(phase3_ai_at)::numeric / NULLIF(COUNT(*), 0) * 100, 1) as pct
+      FROM enrichment_tracking
+      GROUP BY vendor_code
+      HAVING COUNT(*) - COUNT(phase3_ai_at) > 0
+      ORDER BY COUNT(*) - COUNT(phase3_ai_at) DESC
+      LIMIT 20
+    `);
+
+    // Global totals
+    const global = await pool.query(`
+      SELECT COUNT(*) as total,
+        COUNT(phase3_ai_at) as enriched,
+        COUNT(*) - COUNT(phase3_ai_at) as remaining,
+        ROUND(COUNT(phase3_ai_at)::numeric / NULLIF(COUNT(*), 0) * 100, 1) as pct
+      FROM enrichment_tracking
+    `);
+
+    // Phase breakdown (all 8 phases)
+    const phases = await pool.query(`
+      SELECT
+        COUNT(phase1_scraped_at) as p1_scraped,
+        COUNT(phase2_specs_at) as p2_specs,
+        COUNT(phase3_ai_at) as p3_ai,
+        COUNT(phase4_silas_at) as p4_silas,
+        COUNT(phase5_vcc_at) as p5_vcc,
+        COUNT(phase6_rooms_at) as p6_rooms,
+        COUNT(phase7_spin_at) as p7_spin,
+        COUNT(phase8_shopify_at) as p8_shopify
+      FROM enrichment_tracking
+    `);
+
+    res.json({
+      global: {
+        total: parseInt(global.rows[0].total),
+        enriched: parseInt(global.rows[0].enriched),
+        remaining: parseInt(global.rows[0].remaining),
+        pct: parseFloat(global.rows[0].pct),
+      },
+      phases: {
+        p1_scraped: parseInt(phases.rows[0].p1_scraped),
+        p2_specs: parseInt(phases.rows[0].p2_specs),
+        p3_ai: parseInt(phases.rows[0].p3_ai),
+        p4_silas: parseInt(phases.rows[0].p4_silas),
+        p5_vcc: parseInt(phases.rows[0].p5_vcc),
+        p6_rooms: parseInt(phases.rows[0].p6_rooms),
+        p7_spin: parseInt(phases.rows[0].p7_spin),
+        p8_shopify: parseInt(phases.rows[0].p8_shopify),
+      },
+      recent_activity: recent.rows.map(r => ({
+        vendor: r.vendor_code,
+        completed_last_hour: parseInt(r.completed),
+        last_enriched: r.last_enriched,
+        avg_tokens: Math.round(parseFloat(r.avg_tokens || 0)),
+      })),
+      top_gaps: gaps.rows.map(r => ({
+        vendor: r.vendor_code,
+        total: parseInt(r.total),
+        done: parseInt(r.done),
+        remaining: parseInt(r.remaining),
+        pct: parseFloat(r.pct),
+      })),
+    });
+  } catch (err) {
+    console.error('[Victor] enrichment/live error:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// ─── NEW: Cost Tracking Panel ────────────────────────────────────────────
+// GET /api/costs — Gemini cost tracker state + Claude session cost log
+app.get('/api/costs', async (req, res) => {
+  try {
+    const fs = require('fs');
+    // Gemini costs
+    let gemini = { totalCost: 0, totalCalls: 0, alertsSent: 0, startDate: null };
+    try {
+      gemini = JSON.parse(fs.readFileSync('/root/DW-Agents/logs/gemini-cost-state.json', 'utf8'));
+    } catch (e) { /* no state yet */ }
+
+    // Recent Gemini log lines
+    let recentGemini = [];
+    try {
+      const lines = fs.readFileSync('/root/DW-Agents/logs/gemini-costs.log', 'utf8').trim().split('\n');
+      recentGemini = lines.slice(-20).reverse().map(line => {
+        const parts = line.split(' | ');
+        return { time: parts[0], model: parts[1], tokens: parts[2], cost: parts[3], total: parts[4], task: parts[5] };
+      });
+    } catch (e) { /* no log yet */ }
+
+    // Claude session costs (if log exists)
+    let recentClaude = [];
+    try {
+      const lines = fs.readFileSync('/root/DW-Agents/logs/ai-costs.log', 'utf8').trim().split('\n');
+      recentClaude = lines.slice(-10).reverse();
+    } catch (e) { /* no log yet */ }
+
+    // Cost projections
+    const pendingResult = await pool.query(`
+      SELECT
+        COUNT(*) FILTER (WHERE phase3_ai_at IS NULL) as phase3_pending,
+        COUNT(*) FILTER (WHERE phase6_rooms_at IS NULL) as phase6_pending
+      FROM enrichment_tracking
+    `);
+    const p3Pending = parseInt(pendingResult.rows[0].phase3_pending);
+    const p6Pending = parseInt(pendingResult.rows[0].phase6_pending);
+
+    res.json({
+      gemini: {
+        total_cost: gemini.totalCost,
+        total_calls: gemini.totalCalls,
+        alerts_sent: gemini.alertsSent,
+        alert_threshold: 50,
+        next_alert_at: (Math.floor(gemini.totalCost / 50) + 1) * 50,
+        tracking_since: gemini.startDate,
+      },
+      projections: {
+        phase3_pending: p3Pending,
+        phase3_estimated_cost: Math.round(p3Pending * 0.0004 * 100) / 100,
+        phase6_pending: p6Pending,
+        phase6_estimated_cost: Math.round(p6Pending * 0.85 * 0.012 * 100) / 100, // 3 rooms, ~85% need rooms (15% already have MFR images)
+        total_estimated: Math.round((p3Pending * 0.0004 + p6Pending * 0.85 * 0.012) * 100) / 100, // 3 rooms, 85% eligible
+      },
+      recent_gemini_calls: recentGemini,
+      recent_claude_sessions: recentClaude,
+    });
+  } catch (err) {
+    console.error('[Victor] costs error:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// ─── NEW: Vendor Restrictions ────────────────────────────────────────────
+// GET /api/restrictions — Vendor-specific rules and exclusions
+app.get('/api/restrictions', (req, res) => {
+  res.json({
+    never_touch: [
+      { vendor: 'cowtan_tout', rule: 'NEVER. No scraping, no push, no enrichment, no AI tags, no images.' },
+      { vendor: 'colefax_fowler', rule: 'NEVER. Same as Cowtan (Cowtan company).' },
+    ],
+    no_ai_tags: [
+      { vendor: 'phillip_jeffries', rule: 'NO AI TAGS. Browse-hidden, search-only visibility.' },
+    ],
+    private_label: [
+      { vendor: 'bespoke', rule: 'DW own brand. Uses DIG- SKUs, not DWWC-. Special handling.' },
+      { vendor: 'phillipe_romano', rule: 'Private label. Never expose real MFR in public content.' },
+      { vendor: 'architectural_wallcoverings', rule: 'Private label. DW brand.' },
+    ],
+    special_handling: [
+      { vendor: 'scalamandre', rule: 'Fabrics are NOT wallcoverings. Only wallcovering SKUs valid.' },
+      { vendor: 'york_wallcoverings', rule: '4,540 images in DB but no products on Shopify. No-new-products directive.' },
+    ],
+  });
+});
+
+// --- Vendor Trade Portal Credentials ---
+
+// GET /api/vendor-credentials — Return all vendors with has_credentials=true
+app.get('/api/vendor-credentials', async (req, res) => {
+  try {
+    const { rows } = await pool.query(`
+      SELECT vendor_name, has_credentials, login_url, website_url, trade_username, trade_password
+      FROM vendor_registry
+      WHERE has_credentials = true
+      ORDER BY vendor_name
+    `);
+    res.json({ ok: true, vendors: rows });
+  } catch (err) {
+    console.error('[Victor] vendor-credentials GET error:', err.message);
+    res.status(500).json({ ok: false, error: err.message });
+  }
+});
+
+// POST /api/vendor-credentials — Save username/password for a vendor
+app.post('/api/vendor-credentials', async (req, res) => {
+  try {
+    const { vendor_name, trade_username, trade_password } = req.body;
+    if (!vendor_name) return res.json({ ok: false, error: 'vendor_name required' });
+    await pool.query(`
+      UPDATE vendor_registry SET trade_username = $1, trade_password = $2
+      WHERE vendor_name = $3
+    `, [trade_username || null, trade_password || null, vendor_name]);
+    res.json({ ok: true, vendor: vendor_name });
+  } catch (err) {
+    console.error('[Victor] vendor-credentials POST error:', err.message);
+    res.status(500).json({ ok: false, error: err.message });
+  }
+});
+
+// GET /api/enrichment/color-progress — Color enrichment progress (shopify_color_enrichment vs shopify_products)
+app.get('/api/enrichment/color-progress', async (req, res) => {
+  try {
+    // Overall summary
+    const summaryResult = await pool.query(`
+      SELECT
+        COUNT(DISTINCT sp.vendor) as total_vendors,
+        COUNT(*) as total_active,
+        COUNT(*) FILTER (WHERE sce.status = 'enriched') as enriched,
+        COUNT(*) - COUNT(*) FILTER (WHERE sce.status = 'enriched') as remaining,
+        ROUND(100.0 * COUNT(*) FILTER (WHERE sce.status = 'enriched') / NULLIF(COUNT(*), 0), 1) as pct
+      FROM shopify_products sp
+      LEFT JOIN shopify_color_enrichment sce ON sce.shopify_id = sp.shopify_id
+      WHERE UPPER(sp.status) = 'ACTIVE' AND sp.image_url IS NOT NULL
+    `);
+    const summary = summaryResult.rows[0];
+
+    // Per-vendor breakdown
+    const vendorResult = await pool.query(`
+      SELECT sp.vendor, COUNT(*) as total,
+        COUNT(*) FILTER (WHERE sce.status = 'enriched') as enriched
+      FROM shopify_products sp
+      LEFT JOIN shopify_color_enrichment sce ON sce.shopify_id = sp.shopify_id
+      WHERE UPPER(sp.status) = 'ACTIVE' AND sp.image_url IS NOT NULL
+      GROUP BY sp.vendor ORDER BY total DESC
+    `);
+
+    const vendors = vendorResult.rows.map(r => ({
+      vendor: r.vendor,
+      total: parseInt(r.total),
+      enriched: parseInt(r.enriched),
+      remaining: parseInt(r.total) - parseInt(r.enriched),
+      pct: r.total > 0 ? Math.round(1000 * parseInt(r.enriched) / parseInt(r.total)) / 10 : 0
+    }));
+
+    const totalActive = parseInt(summary.total_active) || 0;
+    const enriched = parseInt(summary.enriched) || 0;
+    const remaining = parseInt(summary.remaining) || 0;
+
+    res.json({
+      success: true,
+      summary: {
+        total_vendors: parseInt(summary.total_vendors) || 0,
+        total_active: totalActive,
+        enriched: enriched,
+        remaining: remaining,
+        pct: parseFloat(summary.pct) || 0,
+        eta_hours: remaining > 0 ? Math.round(10 * remaining / 900) / 10 : 0
+      },
+      vendors: vendors,
+      ts: new Date().toISOString()
+    });
+  } catch (err) {
+    console.error('[Victor] enrichment/color-progress error:', err.message);
+    res.status(500).json({ success: false, error: err.message });
+  }
+});
+
+// --- Start server ---
+app.listen(PORT, '0.0.0.0', function() {
+  console.log('[Victor] Vendor Command Center running on port ' + PORT);
+  console.log('Dashboard: http://45.61.58.125:' + PORT + '/');
+  console.log('Health:    http://45.61.58.125:' + PORT + '/health');
+});

← 08e84a2c Fix vendor_registry total_products rollup: filter shared cat  ·  back to Designer Wallcoverings  ·  VCC: fix DB fallback connection string (dw_admin@127.0.0.1, 9ca4e85a →