[object Object]

← back to Dw Marketing Reels

record completed image backfill: upload-imageless-11 (11/11 EUR SKUs imaged, Steve-approved live Shopify write, verified in mirror)

8d53d7f1a463543e38a1132fa8257001adf290c8 · 2026-07-24 08:36:42 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 8d53d7f1a463543e38a1132fa8257001adf290c8
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 24 08:36:42 2026 -0700

    record completed image backfill: upload-imageless-11 (11/11 EUR SKUs imaged, Steve-approved live Shopify write, verified in mirror)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/upload-imageless-11.mjs | 106 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/scripts/upload-imageless-11.mjs b/scripts/upload-imageless-11.mjs
new file mode 100644
index 0000000..e14e818
--- /dev/null
+++ b/scripts/upload-imageless-11.mjs
@@ -0,0 +1,106 @@
+#!/usr/bin/env node
+// upload-imageless-11.mjs — attach sourced featured images to the still-imageless DW products.
+// Gated action APPROVED by Steve 2026-07-24. LIVE store write (customer-facing).
+// Flow per product: stagedUploadsCreate → PUT bytes → productCreateMedia → poll featured image
+//                   → write CDN url back to the dw_unified mirror (Shopify is authoritative).
+// EUR-80361 (7513505431603) is EXCLUDED — auto-recovery already gave it an image; don't clobber.
+import { readFile } from 'node:fs/promises';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { execFile } from 'node:child_process';
+import { promisify } from 'node:util';
+const pexec = promisify(execFile);
+
+const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
+const IMGDIR = join(ROOT, 'sourced-images', 'imageless-14');
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+
+const TOKEN = (await readFile(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8'))
+  .split('\n').find(l => l.startsWith('SHOPIFY_ADMIN_TOKEN='))?.split('=').slice(1).join('=').trim();
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+const JOBS = [
+  ['EUR-71130', '7515289845811', 'W7814-01__w7814-01_n6lger3vo8gdn9ly.webp'],
+  ['EUR-71059', '7515288109107', 'W7682-01__w7682-01_ff8svfx2jzq6e1ez.webp'],
+  ['EUR-70743', '7515278704691', 'W7024-03__w7024-03.webp'],
+  ['EUR-70486', '7514199457843', 'W6541-02__4591.webp'],
+  ['EUR-70279', '7514192871475', 'W5223-03__w5223-03.webp'],
+  ['EUR-70883', '7515283554355', 'W7334-1__w7334-01.webp'],
+  ['EUR-70328', '7514194444339', 'EUR-70328__5314.webp'],
+  ['EUR-70402', '7514196901939', 'EUR-70402__5053.webp'],
+  ['EUR-70351', '7514195066931', 'EUR-70351__5223.webp'],
+  ['EUR-80261', '7513501499443', 'NCW4270-03__coromandel-offwhite.webp'],
+  ['EUR-80263', '7513501564979', 'NCW4270-05__coromandel-offwhite.webp'],
+];
+
+const gql = async (query, variables) => {
+  const r = await fetch(`https://${SHOP}/admin/api/${API}/graphql.json`, {
+    method: 'POST',
+    headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+    body: JSON.stringify({ query, variables }),
+  });
+  const j = await r.json();
+  if (j.errors) throw new Error('GQL ' + JSON.stringify(j.errors));
+  return j.data;
+};
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+async function stage(filename, bytes) {
+  const d = await gql(`mutation($input:[StagedUploadInput!]!){stagedUploadsCreate(input:$input){
+    stagedTargets{url resourceUrl parameters{name value}} userErrors{field message}}}`,
+    { input: [{ filename, mimeType: 'image/webp', resource: 'IMAGE', httpMethod: 'POST', fileSize: String(bytes.length) }] });
+  const e = d.stagedUploadsCreate.userErrors;
+  if (e.length) throw new Error('stage ' + JSON.stringify(e));
+  return d.stagedUploadsCreate.stagedTargets[0];
+}
+
+async function putBytes(target, filename, bytes) {
+  const form = new FormData();
+  for (const p of target.parameters) form.append(p.name, p.value);
+  form.append('file', new Blob([bytes], { type: 'image/webp' }), filename);
+  const r = await fetch(target.url, { method: 'POST', body: form });
+  if (!(r.status === 201 || r.status === 204 || r.status === 200))
+    throw new Error('PUT bytes HTTP ' + r.status + ' ' + (await r.text()).slice(0, 200));
+}
+
+async function attach(productId, resourceUrl) {
+  const d = await gql(`mutation($productId:ID!,$media:[CreateMediaInput!]!){
+    productCreateMedia(productId:$productId,media:$media){
+      media{...on MediaImage{id status image{url}}} mediaUserErrors{field message}}}`,
+    { productId, media: [{ originalSource: resourceUrl, mediaContentType: 'IMAGE' }] });
+  const e = d.productCreateMedia.mediaUserErrors;
+  if (e.length) throw new Error('attach ' + JSON.stringify(e));
+  return d.productCreateMedia.media[0];
+}
+
+async function featured(productId) {
+  const d = await gql(`query($id:ID!){product(id:$id){featuredImage{url}}}`, { id: productId });
+  return d.product?.featuredImage?.url || null;
+}
+
+let ok = 0, fail = 0;
+for (const [sku, numid, file] of JOBS) {
+  const gid = `gid://shopify/Product/${numid}`;
+  try {
+    const bytes = await readFile(join(IMGDIR, file));
+    const target = await stage(file, bytes);
+    await putBytes(target, file, bytes);
+    const m = await attach(gid, target.resourceUrl);
+    // media processing is async — poll featured image up to ~20s
+    let url = m?.image?.url || null;
+    for (let i = 0; i < 10 && !url; i++) { await sleep(2000); url = await featured(gid); }
+    if (url) {
+      await pexec('psql', ['-h', '/tmp', '-d', 'dw_unified', '-c',
+        `UPDATE shopify_products SET image_url=$$${url}$$ WHERE shopify_id=$$${gid}$$`]);
+    }
+    console.log(`✓ ${sku}  ${file}  → ${url ? url.slice(0, 70) : '(processing; mirror will resync)'}`);
+    ok++;
+  } catch (e) {
+    console.error(`✗ ${sku}  ${file}  — ${e.message}`);
+    fail++;
+  }
+}
+console.log(`\nDONE: ${ok} uploaded, ${fail} failed (of ${JOBS.length}).`);
+process.exit(fail ? 1 : 0);

← 5ebec05 auto-save: 2026-07-24T07:26:03 (3 files) — data/new-arrivals  ·  back to Dw Marketing Reels  ·  chore: vN patch (session close — image backfill) 689fe27 →