← back to Designer Wallcoverings
Recrawl: mark vendor-404 SKUs as discontinued (excluded) + Shopify archive step
2ec9fcaed8dc8fa32cec2b2b69ba5eb6c8a2ce82 · 2026-06-11 10:29:45 -0700 · SteveStudio2
schu-api-recrawl.js now marks HTTP_404 SKUs excluded=true, price='DISCONTINUED'
(skipped on resume, logged to /tmp/schu-disco-skus.txt). schu-archive-disco.js sets
status=ARCHIVED + tag 'discontinued-vendor' on any disco SKU live on Shopify (dry-run
default). Standing rule: log + archive disco SKUs; never show discontinued to customers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M shopify/scripts/schu-api-recrawl.jsA shopify/scripts/schu-archive-disco.js
Diff
commit 2ec9fcaed8dc8fa32cec2b2b69ba5eb6c8a2ce82
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu Jun 11 10:29:45 2026 -0700
Recrawl: mark vendor-404 SKUs as discontinued (excluded) + Shopify archive step
schu-api-recrawl.js now marks HTTP_404 SKUs excluded=true, price='DISCONTINUED'
(skipped on resume, logged to /tmp/schu-disco-skus.txt). schu-archive-disco.js sets
status=ARCHIVED + tag 'discontinued-vendor' on any disco SKU live on Shopify (dry-run
default). Standing rule: log + archive disco SKUs; never show discontinued to customers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
shopify/scripts/schu-api-recrawl.js | 22 +++++++++----
shopify/scripts/schu-archive-disco.js | 60 +++++++++++++++++++++++++++++++++++
2 files changed, 75 insertions(+), 7 deletions(-)
diff --git a/shopify/scripts/schu-api-recrawl.js b/shopify/scripts/schu-api-recrawl.js
index 44dd6182..91ab7f2e 100644
--- a/shopify/scripts/schu-api-recrawl.js
+++ b/shopify/scripts/schu-api-recrawl.js
@@ -96,12 +96,12 @@ function parse(j) {
if (TEST) {
skus = ['R11358658', ...psql(`SELECT mfr_sku FROM schumacher_catalog WHERE product_type IN ('Wallcovering','Commercial Wallcovering') AND retail_price IS NULL ORDER BY mfr_sku LIMIT 2;`).split('\n').filter(Boolean)];
} else {
- // resume-safe: only rows still missing price
- skus = psql(`SELECT mfr_sku FROM schumacher_catalog WHERE product_type IN ('Wallcovering','Commercial Wallcovering') AND retail_price IS NULL AND mfr_sku IS NOT NULL AND mfr_sku<>'' ORDER BY mfr_sku LIMIT ${LIMIT};`).split('\n').filter(Boolean);
+ // resume-safe: only rows still missing price AND not already marked discontinued/excluded
+ skus = psql(`SELECT mfr_sku FROM schumacher_catalog WHERE product_type IN ('Wallcovering','Commercial Wallcovering') AND retail_price IS NULL AND COALESCE(excluded,false)=false AND mfr_sku IS NOT NULL AND mfr_sku<>'' ORDER BY mfr_sku LIMIT ${LIMIT};`).split('\n').filter(Boolean);
}
console.log(`SKUs to fetch: ${skus.length}`);
- let ok = 0, priced = 0, miss = 0, err = 0;
+ let ok = 0, priced = 0, miss = 0, err = 0, disco = 0;
for (let i = 0; i < skus.length; i++) {
const sku = skus[i];
try {
@@ -135,12 +135,20 @@ function parse(j) {
}
ok++;
} catch (e) {
- err++;
if (e.auth) { console.error(`\nAUTH FAILURE on ${sku} (${e.message}) — token likely expired/invalid. Stopping.`); break; }
- console.log(` ${sku} ERR ${e.message}`);
+ if (e.message === 'HTTP_404') {
+ // discontinued at vendor — log + mark excluded so it's skipped on resume + queued for Shopify archive
+ disco++;
+ if (COMMIT && !TEST) psql(`UPDATE schumacher_catalog SET excluded=true, price='DISCONTINUED', agent_updated_at=now() WHERE mfr_sku=${q(sku)};`);
+ fs.appendFileSync('/tmp/schu-disco-skus.txt', sku + '\n');
+ console.log(` ${sku} DISCONTINUED (404) — marked excluded`);
+ } else {
+ err++;
+ console.log(` ${sku} ERR ${e.message}`);
+ }
}
- if (!TEST && ok % 50 === 0) process.stdout.write(`\r progress ${i + 1}/${skus.length} | priced ${priced} | miss ${miss} | err ${err}`);
+ if (!TEST && (i + 1) % 50 === 0) process.stdout.write(`\r progress ${i + 1}/${skus.length} | priced ${priced} | disco ${disco} | err ${err}`);
await sleep(350);
}
- console.log(`\nDONE: fetched ${ok}, priced ${priced}, no-price ${miss}, errors ${err}${COMMIT && !TEST ? ' (written to schumacher_catalog)' : ' (dry — no write)'}`);
+ console.log(`\nDONE: fetched ${ok}, priced ${priced}, no-price ${miss}, discontinued ${disco}, errors ${err}${COMMIT && !TEST ? ' (written to schumacher_catalog; disco marked excluded → /tmp/schu-disco-skus.txt)' : ' (dry — no write)'}`);
})();
diff --git a/shopify/scripts/schu-archive-disco.js b/shopify/scripts/schu-archive-disco.js
new file mode 100644
index 00000000..45127272
--- /dev/null
+++ b/shopify/scripts/schu-archive-disco.js
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+/**
+ * Archive DISCONTINUED Schumacher SKUs on Shopify.
+ *
+ * The API recrawl (schu-api-recrawl.js) marks vendor-404 SKUs excluded=true,
+ * price='DISCONTINUED'. Any of those that are live on Shopify (shopify_product_id set)
+ * must be ARCHIVED so they stop showing to customers — Steve's standing rule:
+ * "log and archive on shopify for disco skus."
+ *
+ * node schu-archive-disco.js # DRY-RUN: list what would be archived
+ * node schu-archive-disco.js --commit # set status=ARCHIVED + tag 'discontinued-vendor'
+ */
+const https = require('https');
+const fs = require('fs');
+const os = require('os');
+const { execFileSync } = require('child_process');
+const COMMIT = process.argv.includes('--commit');
+const TOKEN = (fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function psql(sql) { return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', sql], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim(); }
+const q = s => "'" + String(s).replace(/'/g, "''") + "'";
+
+function gql(query, variables) {
+ return new Promise(res => {
+ const data = JSON.stringify({ query, variables });
+ const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
+ r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { res({ status: r.statusCode, json: JSON.parse(d) }); } catch { res({ status: r.statusCode, raw: d.slice(0, 300) }); } }); });
+ req.on('error', e => res({ status: 0, err: e.message })); req.write(data); req.end();
+ });
+}
+async function gqlRetry(query, v) { for (let a = 0; a < 5; a++) { const r = await gql(query, v); const t = r.json && r.json.errors && JSON.stringify(r.json.errors).includes('THROTTLED'); if (r.status === 429 || t) { await sleep(2000 * (a + 1)); continue; } await sleep(400); return r; } return { status: 429 }; }
+
+(async () => {
+ if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+ const rows = psql(`SELECT mfr_sku, shopify_product_id FROM schumacher_catalog WHERE excluded=true AND price='DISCONTINUED' AND shopify_product_id IS NOT NULL AND shopify_product_id<>'' ORDER BY mfr_sku;`)
+ .split('\n').filter(Boolean).map(l => { const [sku, pid] = l.split('\t'); return { sku, pid }; });
+ console.log(`Discontinued Schumacher SKUs live on Shopify: ${rows.length}`);
+ if (!rows.length) { console.log('nothing to archive.'); return; }
+
+ if (!COMMIT) {
+ rows.slice(0, 30).forEach(r => console.log(` would archive sku=${r.sku} product=${r.pid}`));
+ console.log(`\nDRY-RUN — ${rows.length} would be set status=ARCHIVED + tag discontinued-vendor. Re-run with --commit.`);
+ return;
+ }
+
+ const mUpd = `mutation($input:ProductInput!){productUpdate(input:$input){product{id status} userErrors{field message}}}`;
+ const mTag = `mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}`;
+ let ok = 0, fail = 0;
+ for (const r of rows) {
+ const gid = `gid://shopify/Product/${r.pid}`;
+ const r1 = await gqlRetry(mUpd, { input: { id: gid, status: 'ARCHIVED' } });
+ const r2 = await gqlRetry(mTag, { id: gid, tags: ['discontinued-vendor'] });
+ const e1 = r1.json?.data?.productUpdate?.userErrors, e2 = r2.json?.data?.tagsAdd?.userErrors;
+ if ((e1 && e1.length) || (e2 && e2.length)) { fail++; console.log(` ✗ ${r.sku} ${JSON.stringify(e1 || e2).slice(0, 120)}`); }
+ else { ok++; psql(`UPDATE schumacher_catalog SET on_shopify=false, agent_updated_at=now() WHERE mfr_sku=${q(r.sku)};`); if (ok % 20 === 0) process.stdout.write(`\r archived ${ok}/${rows.length}…`); }
+ }
+ console.log(`\nDONE: archived ${ok}, failed ${fail}.`);
+})();
← a51f9ed8 Scrub plaintext Schumacher password from browser-login scrip
·
back to Designer Wallcoverings
·
Add compute-price-coverage.js — truthful vendor cost-coverag 46463911 →