[object Object]

← back to Designer Wallcoverings

wire activation gate into activate-publish-v2 + make sync --quick actually filter by updated_at

63ffa88ab211751aabe7458fb5ed7d89d90dbdd3 · 2026-07-21 17:30:01 -0700 · Steve

Files touched

Diff

commit 63ffa88ab211751aabe7458fb5ed7d89d90dbdd3
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 21 17:30:01 2026 -0700

    wire activation gate into activate-publish-v2 + make sync --quick actually filter by updated_at
---
 scripts/wallquest-refresh/activate-publish-v2.cjs | 22 ++++++++++++++++++++++
 shopify/scripts/sync-shopify-products.js          | 15 ++++++++++++---
 2 files changed, 34 insertions(+), 3 deletions(-)

diff --git a/scripts/wallquest-refresh/activate-publish-v2.cjs b/scripts/wallquest-refresh/activate-publish-v2.cjs
index fea27f7a..be79e1d5 100644
--- a/scripts/wallquest-refresh/activate-publish-v2.cjs
+++ b/scripts/wallquest-refresh/activate-publish-v2.cjs
@@ -4,6 +4,11 @@
 // Settlement: Blossom Citrus Floral (Limonaia) BLOCK was overridden by Steve — published too.
 const https = require('https');
 const { Client } = require('pg');
+// DW activation gate — the SINGLE enforced validator. This script previously flipped
+// ACTIVE on trust ("Pre-req: pushed as draft with … images") with no check, which is
+// how 7 image-less products went live on 7/17. Now every product is fetched and
+// validated (image/width/description/sample/banned-title) BEFORE the status flip.
+const { validateBeforeActivate } = require(__dirname + '/../../shopify/scripts/lib/validate-before-activate.js');
 const STORE = 'designer-laboratory-sandbox.myshopify.com';
 const API = '2024-10';
 const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
@@ -51,6 +56,23 @@ function gql(query, variables) {
   for (const r of rows) {
     const gid = `gid://shopify/Product/${r.shopify_product_id}`;
     try {
+      const pre = await gql(`query($id:ID!){ product(id:$id){ title vendor tags descriptionHtml
+        gwidth: metafield(namespace:"global", key:"width"){ value }
+        cwidth: metafield(namespace:"custom", key:"width"){ value }
+        media(first:10){ edges{ node{ ... on MediaImage { image { url } } } } }
+        variants(first:25){ edges{ node{ sku title } } } } }`, { id: gid });
+      const prod = pre.data && pre.data.product;
+      if (!prod) { err++; console.log(`✗ ${r.dw_sku} gate: product fetch failed — skipped`); await sleep(600); continue; }
+      const verdict = validateBeforeActivate({
+        title: prod.title, vendor: prod.vendor, tags: prod.tags, dwSku: r.dw_sku,
+        descriptionHtml: prod.descriptionHtml,
+        specs: { width: (prod.gwidth && prod.gwidth.value) || (prod.cwidth && prod.cwidth.value) || '' },
+        vendorSpecs: {},   // catalog row not in hand at promote time — vendor-provided extras not enforced here
+        images: prod.media.edges.map(e => e.node && e.node.image && e.node.image.url).filter(Boolean),
+        vendorImages: [],
+        variants: prod.variants.edges.map(e => e.node),
+      });
+      if (!verdict.ok) { err++; console.log(`✋ ${r.dw_sku} GATE FAIL — stays DRAFT: ${verdict.reasons.join('; ')}`); await sleep(600); continue; }
       const a = await gql('mutation($id:ID!){ productUpdate(input:{id:$id, status:ACTIVE}){ product{id status} userErrors{message} } }', { id: gid });
       const aErr = a.errors || a.data?.productUpdate?.userErrors?.length;
       if (!aErr) act++;
diff --git a/shopify/scripts/sync-shopify-products.js b/shopify/scripts/sync-shopify-products.js
index 2ec37e7e..514b489f 100644
--- a/shopify/scripts/sync-shopify-products.js
+++ b/shopify/scripts/sync-shopify-products.js
@@ -87,6 +87,15 @@ async function syncProducts(quickMode = false) {
     }
   }
 
+  // Quick mode previously computed lastSync but NEVER passed it to Shopify — the
+  // hourly --quick job was silently doing a full ~700-page crawl every hour.
+  // Now it asks Shopify only for products updated since the last sync (15-min
+  // overlap so a run that lands mid-minute can't drop boundary updates).
+  const quickFilter = (quickMode && lastSync)
+    ? `updated_at:>'${new Date(new Date(lastSync).getTime() - 15 * 60 * 1000).toISOString()}'`
+    : null;
+  if (quickFilter) console.log(`   Filter: ${quickFilter}`);
+
   // 5-field rule (Steve 2026-06-21): the mirror must also SEE variant price +
   // descriptionHtml + variant shape so it can monitor the sample/product/price
   // completeness rule going forward (this sync was previously BLIND to all of
@@ -94,8 +103,8 @@ async function syncProducts(quickMode = false) {
   // added here; the per-product rollup (has_sample/has_product/variant_count/
   // min_variant_price/has_desc) is computed below and stored on the product row.
   const query = `
-    query getProducts($cursor: String) {
-      products(first: 200, after: $cursor) {
+    query getProducts($cursor: String, $q: String) {
+      products(first: 200, after: $cursor, query: $q) {
         pageInfo {
           hasNextPage
           endCursor
@@ -135,7 +144,7 @@ async function syncProducts(quickMode = false) {
     page++;
     console.log(`\n📦 Fetching page ${page}...`);
 
-    const result = await shopifyGraphQL(query, { cursor });
+    const result = await shopifyGraphQL(query, { cursor, q: quickFilter });
 
     // FAIL-LOUD: a GraphQL error (auth, throttle, schema) must NOT print a "Synced 0"
     // success and exit 0. Exit non-zero so launchd/the canary sees a real failure.

← f3d8f23f mirror sync now carries featuredImage into image_url (closes  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-21T18:09:04 (2 files) — scripts/wallquest d3fe56ce →