← 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
A jobs/last5.jsonA probes/shopify-recent.mjs
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 →