[object Object]

← back to Fix Live Board

add last5 job: last-5-days new-products go-live board (live Shopify, 6-field rule); fix sample-as-sellable false-green in recent probe

5f15a196cdc2a53981b9d6fead7b71d9cf596fea · 2026-09-02 13:56:24 -0700 · Steve

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

Files touched

Diff

commit 5f15a196cdc2a53981b9d6fead7b71d9cf596fea
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 2 13:56:24 2026 -0700

    add last5 job: last-5-days new-products go-live board (live Shopify, 6-field rule); fix sample-as-sellable false-green in recent probe
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KkjHuJKiariXaTtFujZxWC
---
 jobs/last5.json           | 17 +++++++++++
 probes/shopify-recent.mjs | 74 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 91 insertions(+)

diff --git a/jobs/last5.json b/jobs/last5.json
new file mode 100644
index 0000000..366098e
--- /dev/null
+++ b/jobs/last5.json
@@ -0,0 +1,17 @@
+{
+  "id": "last5",
+  "name": "Last 5 Days · New Products",
+  "blurb": "Every product created in the last 5 days (all vendors) vs Steve's go-live rule: sample + sellable variant, real price, description, ≥2 tags, featured image.",
+  "watch": true,
+  "probe": "DAYS=5 node probes/shopify-recent.mjs",
+  "probeTimeoutMs": 60000,
+  "fields": [
+    { "key": "sample", "ok": "sample variant", "bad": "no sample" },
+    { "key": "sellable", "ok": "sellable variant", "bad": "no sellable variant" },
+    { "key": "price", "ok": "has price", "bad": "no price" },
+    { "key": "desc", "ok": "description", "bad": "no description" },
+    { "key": "tags", "ok": "≥2 tags", "bad": "<2 tags" },
+    { "key": "image", "ok": "image", "bad": "no image" }
+  ],
+  "fixers": []
+}
diff --git a/probes/shopify-recent.mjs b/probes/shopify-recent.mjs
new file mode 100644
index 0000000..d5645e7
--- /dev/null
+++ b/probes/shopify-recent.mjs
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+// "Last N days" probe — every product created in the last N days (all statuses), across all
+// vendors, checked against Steve's standing go-live rule: sample variant, sellable variant,
+// complete price, description, >=2 tags, featured image. READ-ONLY live DW Shopify.
+// Env: DAYS (default 5). Token: SHOPIFY_ADMIN_TOKEN -> secrets-manager/.env -> flock-fix-viewer/.token
+import https from 'https';
+import fs from 'fs';
+import path from 'path';
+import os from 'os';
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const DAYS = parseInt(process.env.DAYS || '5', 10);
+function token() {
+  if (process.env.SHOPIFY_ADMIN_TOKEN) return process.env.SHOPIFY_ADMIN_TOKEN.trim();
+  try { const env = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8'); const m = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m); if (m) return m[1].trim(); } catch (e) {}
+  return fs.readFileSync(path.join(os.homedir(), 'Projects/flock-fix-viewer/.token'), 'utf8').trim();
+}
+const TOKEN = token();
+const since = new Date(Date.now() - DAYS * 864e5).toISOString().slice(0, 10);
+const QUERY = 'created_at:>=' + since;
+const PRODUCT_Q = `query($c:String){
+  products(first:100, query:${JSON.stringify(QUERY)}, sortKey:CREATED_AT, reverse:true, after:$c){
+    pageInfo{hasNextPage endCursor}
+    edges{node{ handle title status createdAt vendor descriptionHtml featuredImage{url} tags
+      variants(first:20){edges{node{sku title price}}}
+    }}
+  }
+}`;
+function gql(query, variables) {
+  return new Promise((resolve, reject) => {
+    const body = JSON.stringify({ query, variables: variables || {} });
+    const req = https.request({ host: SHOP, path: '/admin/api/2024-10/graphql.json', method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
+      res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); });
+    req.on('error', reject); req.write(body); req.end();
+  });
+}
+(async () => {
+  let cur = null, all = [];
+  do {
+    const r = await gql(PRODUCT_Q, { c: cur });
+    const p = r && r.data && r.data.products; if (!p) { if (r && r.errors) process.stderr.write(JSON.stringify(r.errors)); break; }
+    all = all.concat(p.edges);
+    cur = p.pageInfo.hasNextPage ? p.pageInfo.endCursor : null;
+  } while (cur && all.length < 3000);
+  const rows = all.map(e => {
+    const n = e.node;
+    const vs = (n.variants.edges || []).map(v => v.node);
+    const isSample = v => /sample/i.test(v.title || '') || /-sample$/i.test(v.sku || '');
+    const sampleV = vs.find(isSample);
+    const sellV = vs.find(v => v.sku && !isSample(v)); // sellable = a real (non-sample) variant
+    const sellPrice = sellV ? (parseFloat(sellV.price) || 0) : 0;
+    const tags = n.tags || [];
+    const f = {
+      sample: !!sampleV,
+      sellable: !!sellV,
+      price: sellPrice > 0,
+      desc: !!(n.descriptionHtml && n.descriptionHtml.replace(/<[^>]*>/g, '').trim().length > 0),
+      tags: tags.length >= 2,
+      image: !!(n.featuredImage && n.featuredImage.url)
+    };
+    return {
+      id: (sellV && sellV.sku) || (sampleV && sampleV.sku) || n.handle,
+      sku: (sellV && sellV.sku) || (sampleV && sampleV.sku) || '—',
+      handle: n.handle,
+      title: '[' + (n.vendor || '?') + '] ' + n.title,
+      status: n.status, price: sellPrice,
+      img: (n.featuredImage && n.featuredImage.url) || null,
+      created: n.createdAt, fields: f,
+      fixed: f.sample && f.sellable && f.price && f.desc && f.tags && f.image
+    };
+  });
+  process.stdout.write(JSON.stringify(rows));
+})().catch(e => { process.stderr.write(String(e)); process.exit(1); });

← b6cba65 fix-live-board: generalized broken→fixed live board (source-  ·  back to Fix Live Board  ·  last5: treat showroom lines (Phillip Jeffries) as sample-onl d014212 →