← back to Dw Yolo Loop
showroom-lines-scan.js
103 lines
'use strict';
/**
* Showroom Lines scan (READ-ONLY, $0).
* Steve's directive 2026-06-16: products whose ONLY variant is the $4.25 memo
* sample are a deliberate category — "SHOWROOM LINES" — kept live, $4.25 is the
* accurate price. This identifies that exact set and characterizes it so we can
* tag + metafield them and surface a Showroom section.
*
* Showroom-line predicate: status:active AND exactly 1 variant AND that variant
* price <= $4.255 (the $4.25 sample floor, with epsilon).
*
* Writes a worklist + a characterization report. Zero writes to Shopify.
*/
const fs = require('fs');
const SHOP = 'designer-laboratory-sandbox.myshopify.com';
const VERSION = '2024-10';
const ENV = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const ENDPOINT = `https://${SHOP}/admin/api/${VERSION}/graphql.json`;
const SAMPLE = 4.255;
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function gql(query, variables) {
for (let attempt = 0; attempt < 7; attempt++) {
let res;
try {
res = await fetch(ENDPOINT, { method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }) });
} catch (e) { await sleep(2000 * (attempt + 1)); continue; }
if (res.status === 429) { await sleep(2000 * (attempt + 1)); continue; }
const json = await res.json();
if (json.errors) {
if (JSON.stringify(json.errors).includes('THROTTLED')) { await sleep(2000 * (attempt + 1)); continue; }
throw new Error(JSON.stringify(json.errors));
}
const avail = json.extensions?.cost?.throttleStatus?.currentlyAvailable ?? 4000;
if (avail < 400) await sleep(1500);
return json.data;
}
throw new Error('exhausted retries');
}
const QUERY = `
query($cursor: String) {
products(first: 100, after: $cursor, query: "status:active") {
pageInfo { hasNextPage endCursor }
nodes {
id title vendor handle productType
tags
variants(first: 10) { nodes { title price } }
}
}
}`;
// junk heuristics — things we'd want to eyeball before auto-classing as showroom
const JUNK_RE = /\b(test|placeholder|do not use|sample card|deleted)\b/i;
(async () => {
let cursor = null, pages = 0, total = 0;
const showroom = [];
const vendors = {};
let junk = [], priceOther = 0;
const t0 = Date.now();
do {
const d = await gql(QUERY, { cursor });
const conn = d.products;
for (const p of conn.nodes) {
total++;
const vs = p.variants.nodes;
if (vs.length !== 1) continue; // must be single-variant
const price = parseFloat(vs[0].price);
if (!(price <= SAMPLE)) { priceOther++; continue; } // single variant but not $4.25
const rec = { id: p.id, title: p.title, vendor: p.vendor || '(none)', handle: p.handle,
price, alreadyTagged: (p.tags || []).includes('Showroom Line') };
showroom.push(rec);
vendors[rec.vendor] = (vendors[rec.vendor] || 0) + 1;
if (JUNK_RE.test(p.title)) junk.push(rec);
}
cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
if (++pages % 25 === 0) process.stderr.write(` ...${pages} pages, ${total} active, showroom=${showroom.length}\n`);
} while (cursor);
const topVendors = Object.entries(vendors).sort((a, b) => b[1] - a[1]).slice(0, 20);
const alreadyTagged = showroom.filter(r => r.alreadyTagged).length;
const report = {
scannedActive: total,
showroomCount: showroom.length,
singleVariantNon425: priceOther,
alreadyTagged,
needTag: showroom.length - alreadyTagged,
junkSuspects: junk.length,
topVendors,
elapsedSec: Math.round((Date.now() - t0) / 1000),
};
fs.writeFileSync(__dirname + '/showroom-lines-worklist.json',
JSON.stringify({ generatedAt: new Date().toISOString(), predicate: 'active & 1 variant & price<=4.255', report, ids: showroom.map(r => r.id) }, null, 2));
fs.writeFileSync(__dirname + '/showroom-lines-report.json', JSON.stringify({ report, junkSuspects: junk.slice(0, 50), sample: showroom.slice(0, 25) }, null, 2));
console.log(JSON.stringify(report, null, 2));
console.log('\nWorklist → showroom-lines-worklist.json Report → showroom-lines-report.json');
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });