[object Object]

← back to Reid Witlin Onboarding

Reid Witlin/Architectural Fabrics: activate all 1,005 DRAFT products live

a5b4a184c282e9852ed382e72b198771b828ff95 · 2026-09-03 09:39:31 -0700 · Steve Abrams

Steve approved live in conversation 2026-09-03: "make them active with
our pl - architectural fabrics line". These are quote-only, single-
Sample-variant products (same shape as the original 190 already-ACTIVE
products), structurally excluded from the automatic rotation-activator
cadence (SKU matches SAMPLE_SKU_REGEX -(sample|s)$), so this was a
direct one-time scripted activation, not a cadence fix.

Fetched the live DRAFT set fresh via GraphQL (1,005, matching post
codex overnight race-twin cleanup) rather than any local file.
DRY_RUN verified clean, live run: 1005/1005 activated, 0 errors.
Independently re-verified via GraphQL nodes() against the exact set:
all 1005 confirmed ACTIVE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Files touched

Diff

commit a5b4a184c282e9852ed382e72b198771b828ff95
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 3 09:39:31 2026 -0700

    Reid Witlin/Architectural Fabrics: activate all 1,005 DRAFT products live
    
    Steve approved live in conversation 2026-09-03: "make them active with
    our pl - architectural fabrics line". These are quote-only, single-
    Sample-variant products (same shape as the original 190 already-ACTIVE
    products), structurally excluded from the automatic rotation-activator
    cadence (SKU matches SAMPLE_SKU_REGEX -(sample|s)$), so this was a
    direct one-time scripted activation, not a cadence fix.
    
    Fetched the live DRAFT set fresh via GraphQL (1,005, matching post
    codex overnight race-twin cleanup) rather than any local file.
    DRY_RUN verified clean, live run: 1005/1005 activated, 0 errors.
    Independently re-verified via GraphQL nodes() against the exact set:
    all 1005 confirmed ACTIVE.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
 activate-results.json |   5 +++
 activate_batch.py     | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 110 insertions(+)

diff --git a/activate-results.json b/activate-results.json
new file mode 100644
index 0000000..14dba4e
--- /dev/null
+++ b/activate-results.json
@@ -0,0 +1,5 @@
+{
+  "activated": 1005,
+  "errors": [],
+  "total": 1005
+}
\ No newline at end of file
diff --git a/activate_batch.py b/activate_batch.py
new file mode 100644
index 0000000..01001db
--- /dev/null
+++ b/activate_batch.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+"""
+Reid Witlin (Architectural Fabrics PL) — activate the 1,005 DRAFT products
+Steve approved live in conversation 2026-09-03 ("make them active with our
+pl - architectural fabrics line"). These are quote-only, single-Sample-variant
+products (same shape as the original 190 already-ACTIVE Reid Witlin products) —
+structurally excluded from the automatic rotation-activator cadence (their SKU
+matches SAMPLE_SKU_REGEX), so this is a direct, one-time scripted activation,
+not a cadence bug fix.
+
+SAFETY:
+  * DRY_RUN=1 by default.
+  * Reads the live product set fresh via GraphQL (vendor:'Architectural Fabrics'
+    status:draft) rather than any local file, so it reflects codex's overnight
+    race-twin cleanup.
+  * status -> ACTIVE only; no other field touched.
+"""
+import os, json, time, urllib.request
+
+DRY_RUN = os.environ.get("DRY_RUN", "1") != "0"
+LIMIT = int(os.environ.get("LIMIT", "0")) or None
+
+def _tok():
+    p = os.path.expanduser("~/Projects/secrets-manager/.env")
+    for line in open(p):
+        if line.startswith("SHOPIFY_ADMIN_TOKEN="):
+            return line.split("=", 1)[1].strip().strip('"')
+    raise SystemExit("SHOPIFY_ADMIN_TOKEN not found")
+
+TOKEN = os.environ.get("AT") or _tok()
+URL = "https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json"
+
+def gql(q, v=None):
+    body = json.dumps({"query": q, "variables": v or {}}).encode()
+    req = urllib.request.Request(URL, body,
+        {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"})
+    for a in range(8):
+        try:
+            d = json.load(urllib.request.urlopen(req, timeout=90))
+            if "errors" in d and any("THROTTLED" in str(e) for e in d["errors"]):
+                time.sleep(2 * (a + 1)); continue
+            return d
+        except Exception:
+            time.sleep(2 * (a + 1))
+    raise RuntimeError("gql failed")
+
+LIST_Q = """
+query($cursor: String) {
+  products(first: 250, after: $cursor, query: "vendor:'Architectural Fabrics' status:draft") {
+    pageInfo { hasNextPage endCursor }
+    nodes { id handle title }
+  }
+}"""
+
+ACTIVATE_M = """
+mutation($input: ProductInput!) {
+  productUpdate(input: $input) {
+    product { id status }
+    userErrors { field message }
+  }
+}"""
+
+def fetch_all():
+    out, cursor = [], None
+    while True:
+        d = gql(LIST_Q, {"cursor": cursor})
+        data = d.get("data", {}).get("products", {})
+        out.extend(data.get("nodes", []))
+        pi = data.get("pageInfo", {})
+        if not pi.get("hasNextPage"):
+            return out
+        cursor = pi.get("endCursor")
+
+def main():
+    products = fetch_all()
+    if LIMIT:
+        products = products[:LIMIT]
+    print(f"{'DRY-RUN' if DRY_RUN else 'LIVE'}: {len(products)} products "
+          f"({'no writes' if DRY_RUN else 'activating on LIVE store'})")
+    activated, errs = 0, []
+    for i, p in enumerate(products):
+        if DRY_RUN:
+            if i < 3:
+                print(f"  would activate {p['handle']} ({p['title']})")
+            activated += 1
+            continue
+        d = gql(ACTIVATE_M, {"input": {"id": p["id"], "status": "ACTIVE"}})
+        r = (d.get("data") or {}).get("productUpdate") or {}
+        ue = r.get("userErrors") or []
+        if ue:
+            errs.append({"id": p["id"], "handle": p["handle"], "errors": ue[:2]})
+        else:
+            activated += 1
+        if i % 50 == 0:
+            print(f"  ...{i}/{len(products)} activated={activated} errs={len(errs)}")
+        time.sleep(0.25)
+    out = {"activated": activated, "errors": errs[:20], "total": len(products)}
+    json.dump(out, open("activate-results.json", "w"), indent=2)
+    print(f"\nDONE. activated={activated} userErrors={len(errs)} "
+          f"({'DRY-RUN — nothing written' if DRY_RUN else 'ACTIVE on live store'})")
+    if errs:
+        print("sample errors:", json.dumps(errs[:3]))
+
+if __name__ == "__main__":
+    main()

← b205e69 Verify Reid Witlin recovery end to end  ·  back to Reid Witlin Onboarding  ·  auto-data-snapshot: 2026-09-03T09:53:05 (1 data files) — pub a401b87 →