[object Object]

← back to Designer Wallcoverings

vendor-command-center: TK-11919 introspect columns once to stop PG error-log flood

f4ccb4ae3c64ad702aa28d40a865359efe0f163b · 2026-09-20 12:07:25 -0700 · Steve Abrams

Reviewed + isolated: VCC uses its own vcc_session auth (0 refs to DW_SESSION_SECRET/dw_central_session). pm2-reloaded + health 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xoYjhg4XpLjXwhLbhBndb

Files touched

Diff

commit f4ccb4ae3c64ad702aa28d40a865359efe0f163b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Sep 20 12:07:25 2026 -0700

    vendor-command-center: TK-11919 introspect columns once to stop PG error-log flood
    
    Reviewed + isolated: VCC uses its own vcc_session auth (0 refs to DW_SESSION_SECRET/dw_central_session). pm2-reloaded + health 200.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011xoYjhg4XpLjXwhLbhBndb
---
 DW-Agents/vendor-command-center/server.js | 52 +++++++++++++++++++++++++------
 1 file changed, 42 insertions(+), 10 deletions(-)

diff --git a/DW-Agents/vendor-command-center/server.js b/DW-Agents/vendor-command-center/server.js
index f6927cd7..a7c1ad73 100644
--- a/DW-Agents/vendor-command-center/server.js
+++ b/DW-Agents/vendor-command-center/server.js
@@ -256,7 +256,12 @@ app.get('/api/programs', (req, res) => {
   });
 });
 app.get('/programs', (req, res) => {
-  res.sendFile(__dirname + '/public/programs.html');
+  res.sendFile(__dirname + '/public/programs.html', (err) => {
+    if (err && !res.headersSent) {
+      console.error('programs.html send failed:', err.code || err.message);
+      res.status(err.status || 404).send('Not found');
+    }
+  });
 });
 // -----------------------------------------------------------------------------
 
@@ -2378,6 +2383,22 @@ async function syncVendorCounts() {
     for (const _v of allVendors) {
       if (_v.catalog_table) tableVendorCounts[_v.catalog_table] = (tableVendorCounts[_v.catalog_table] || 0) + 1;
     }
+    // TK-11919: per-table column set, introspected ONCE per sweep. Every per-column COUNT below
+    // used to be fired blind and rely on try/catch for missing columns — the app swallowed the
+    // error but Postgres logged ERROR+STATEMENT for each one: 483 errors per 5-min sweep,
+    // ~5,800/hour, ~37k per 200k log lines. Now a column that does not exist is never queried.
+    const tableColumnsCache = {};
+    async function tableColumns(t) {
+      if (t in tableColumnsCache) return tableColumnsCache[t];
+      let cols = new Set();
+      try {
+        const { rows } = await pool.query(
+          "SELECT column_name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name=$1", [t]);
+        cols = new Set(rows.map(r => r.column_name));
+      } catch (_) { /* table may not exist */ }
+      tableColumnsCache[t] = cols;
+      return cols;
+    }
     const sharedDiscriminatorCache = {};
     async function resolveSharedDiscriminator(t) {
       if (t in sharedDiscriminatorCache) return sharedDiscriminatorCache[t];
@@ -2409,7 +2430,7 @@ async function syncVendorCounts() {
       // --- PRIMARY: Catalog table (source of truth) ---
       if (catTable) {
         try {
-          const cols = CATALOG_COLUMNS[catTable] || { price: null, image: 'image_url' };
+          const cols = { ...(CATALOG_COLUMNS[catTable] || { price: null, image: 'image_url' }) };   // copy: resolved below, never mutate the shared map
 
           // Shared catalog tables (>1 vendor) must be filtered to THIS vendor's rows,
           // else every sharer is counted the whole table. Discriminator resolved per
@@ -2427,12 +2448,20 @@ async function syncVendorCounts() {
             }
           }
 
+          const have = await tableColumns(catTable);   // TK-11919
           // Total catalog products
           const totalRes = await pool.query(`SELECT COUNT(*) as cnt FROM ${catTable}${vendorFilter}`);
           catalogCount = parseInt(totalRes.rows[0].cnt) || 0;
 
+          // TK-11919: the default image column is a guess — resolve it against the live
+          // schema (sisterparish_catalog has primary_image, not image_url) instead of
+          // letting a missing column throw and abort every count below it.
+          if (cols.image && !cols.image.includes('[') && !have.has(cols.image)) {
+            cols.image = ['image_url', 'primary_image', 'image', 'thumbnail_url'].find(c => have.has(c)) || null;
+          }
+
           // Catalog products with cost/price
-          if (cols.price) {
+          if (cols.price && have.has(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}`
             );
@@ -2445,18 +2474,20 @@ async function syncVendorCounts() {
             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}`;
+              imgQuery = have.has(arrCol) ? `SELECT COUNT(*) as cnt FROM ${catTable} WHERE ${arrCol} IS NOT NULL AND array_length(${arrCol},1) > 0${vendorAnd}` : null;
             } 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;
+            if (imgQuery) {
+              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) {
+            if (specCols.width && have.has(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}`
@@ -2464,7 +2495,7 @@ async function syncVendorCounts() {
                 catWithWidth = parseInt(widthRes.rows[0].cnt) || 0;
               } catch (_) {}
             }
-            if (specCols.repeat) {
+            if (specCols.repeat && have.has(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}`
@@ -2482,6 +2513,7 @@ async function syncVendorCounts() {
             { col: 'body_html', setter: (v) => catWithBodyHtml = v }
           ];
           for (const af of assetFields) {
+            if (!have.has(af.col)) continue;            // TK-11919: never query a column the table lacks
             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}`
@@ -2491,9 +2523,9 @@ async function syncVendorCounts() {
           }
 
           // Catalog products already on Shopify
-          try {
+          if (have.has('shopify_product_id')) try {
             const shopRes = await pool.query(
-              `SELECT COUNT(*) as cnt FROM ${catTable} WHERE shopify_product_id IS NOT NULL AND shopify_product_id != ''${vendorAnd}`
+              `SELECT COUNT(*) as cnt FROM ${catTable} WHERE shopify_product_id IS NOT NULL AND shopify_product_id::text != ''${vendorAnd}`   // TK-11919: column is bigint on vendor_catalog
             );
             catOnShopify = parseInt(shopRes.rows[0].cnt) || 0;
           } catch (_) { /* no shopify_product_id column */ }

← 569c03a7 auto-data-snapshot: 2026-09-20T11:40:52 (3 data files) — sho  ·  back to Designer Wallcoverings  ·  auto-data-snapshot: 2026-09-20T12:49:08 (3 data files) — sho d696601f →