[object Object]

← back to Dw Marketing Reels

Embed all 35 DW-owned IG accounts in the Reels console (list + publish dropdown)

bc8402942c5d952c61315ebe3970bee3beb23736 · 2026-08-25 10:03:57 -0700 · Steve

The console sourced accounts from its own data/ig-accounts.json (7 entries) — missing
34 of the canonical 35 owned accounts. Add a canonical file mirrored from Norma
(scripts/build-dw-accounts.js → data/dw-canonical-accounts.json) and union it into the
account list server-side (readAccountsUnion), so #sec-accts AND the #pubAcct publish
dropdown AND every per-reel .pick-acct now offer all owned accounts (41 total = 7
managed + 34 canonical). The reel-account validator uses the union too, so canonical
accounts are valid reel targets. Managed entries win on handle-dedupe (keep ids/creds);
canonical-only accounts show '(needs creds)'. ig-accounts.json untouched.

Verified: /api/ig-accounts returns 41, ids valid, dedupe correct; browser shows 41 in
the list and 41 in both dropdown paths, no page errors.

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

Files touched

Diff

commit bc8402942c5d952c61315ebe3970bee3beb23736
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Aug 25 10:03:57 2026 -0700

    Embed all 35 DW-owned IG accounts in the Reels console (list + publish dropdown)
    
    The console sourced accounts from its own data/ig-accounts.json (7 entries) — missing
    34 of the canonical 35 owned accounts. Add a canonical file mirrored from Norma
    (scripts/build-dw-accounts.js → data/dw-canonical-accounts.json) and union it into the
    account list server-side (readAccountsUnion), so #sec-accts AND the #pubAcct publish
    dropdown AND every per-reel .pick-acct now offer all owned accounts (41 total = 7
    managed + 34 canonical). The reel-account validator uses the union too, so canonical
    accounts are valid reel targets. Managed entries win on handle-dedupe (keep ids/creds);
    canonical-only accounts show '(needs creds)'. ig-accounts.json untouched.
    
    Verified: /api/ig-accounts returns 41, ids valid, dedupe correct; browser shows 41 in
    the list and 41 in both dropdown paths, no page errors.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 data/dw-canonical-accounts.json | 182 ++++++++++++++++++++++++++++++++++++++++
 scripts/build-dw-accounts.js    |  46 ++++++++++
 server.js                       |  26 +++++-
 3 files changed, 252 insertions(+), 2 deletions(-)

diff --git a/data/dw-canonical-accounts.json b/data/dw-canonical-accounts.json
new file mode 100644
index 0000000..74114f5
--- /dev/null
+++ b/data/dw-canonical-accounts.json
@@ -0,0 +1,182 @@
+{
+  "source": "Norma/agents/instagram-agent/accounts.json",
+  "generated_at": "2026-08-25T17:01:10.609Z",
+  "count": 35,
+  "accounts": [
+    {
+      "handle": "asseeninhotels",
+      "name": "As Seen in Hotels",
+      "ig_user_id": "17841409119139761"
+    },
+    {
+      "handle": "beverlyhillsvideos",
+      "name": "Designer Wallcoverings",
+      "ig_user_id": "17841408681887144"
+    },
+    {
+      "handle": "borninbeverlyhills",
+      "name": "Born in Beverly Hills",
+      "ig_user_id": "17841408772995866"
+    },
+    {
+      "handle": "brazilliancewallpaper",
+      "name": "Brazilliance Wallpaper and Fabric",
+      "ig_user_id": "17841407025318686"
+    },
+    {
+      "handle": "chinoiseriewallpaper",
+      "name": "Et Cie Chinoiserie Wall Murals",
+      "ig_user_id": "17841407176278551"
+    },
+    {
+      "handle": "commercialwallcoverings",
+      "name": "Commercial Wallcoverings",
+      "ig_user_id": "17841408775091102"
+    },
+    {
+      "handle": "customwallcoverings",
+      "name": "Custom Wall Papers and Wallcoverings",
+      "ig_user_id": "17841408685240867"
+    },
+    {
+      "handle": "designerlaboratory",
+      "name": "Designer Laboratory",
+      "ig_user_id": "17841405610219019"
+    },
+    {
+      "handle": "designerwallcoverings",
+      "name": "DesignerWallcoverings.com",
+      "ig_user_id": "17841400781263605"
+    },
+    {
+      "handle": "fabric_fridays",
+      "name": "Fabric Fridays",
+      "ig_user_id": "17841434885415391"
+    },
+    {
+      "handle": "ffepurchasing",
+      "name": "FFE Purchasing",
+      "ig_user_id": "17841407058348607"
+    },
+    {
+      "handle": "goldleafwallpaper",
+      "name": "Gold Leaf Wallpaper",
+      "ig_user_id": "17841408856660866"
+    },
+    {
+      "handle": "grassclothwallpaper",
+      "name": "Wallpaper Weekly",
+      "ig_user_id": "17841408868631110"
+    },
+    {
+      "handle": "hollywoodwallcoverings",
+      "name": "Hollywood Wallcoverings",
+      "ig_user_id": "17841406772943912"
+    },
+    {
+      "handle": "hospitalitywallcoverings",
+      "name": "Hospitality Wallpaper",
+      "ig_user_id": "17841407188578748"
+    },
+    {
+      "handle": "interiordesignervideos",
+      "name": "Interior Designer Videos",
+      "ig_user_id": "17841408790905574"
+    },
+    {
+      "handle": "linenwallpaper",
+      "name": "Linen Wallpaper",
+      "ig_user_id": "17841408700186822"
+    },
+    {
+      "handle": "metallicwallpaper",
+      "name": "Metallic Wallpaper",
+      "ig_user_id": "17841408654197157"
+    },
+    {
+      "handle": "papelestapiz",
+      "name": "Papel Tapiz",
+      "ig_user_id": "17841407051208925"
+    },
+    {
+      "handle": "patterndesignlab",
+      "name": "Pattern Design Lab",
+      "ig_user_id": "17841405752089037"
+    },
+    {
+      "handle": "philliperomanodesigns",
+      "name": "Phillipe Romano",
+      "ig_user_id": "17841406249858383"
+    },
+    {
+      "handle": "restorationwallpaper",
+      "name": "Restoration Wallpaper",
+      "ig_user_id": "17841408840359842"
+    },
+    {
+      "handle": "retrowalls",
+      "name": "Retro Walls",
+      "ig_user_id": "17841406464478261"
+    },
+    {
+      "handle": "roomsettings",
+      "name": "Room Settings",
+      "ig_user_id": "17841408698848037"
+    },
+    {
+      "handle": "screenprintedwallpaper",
+      "name": "Screen Printed Wallpaper",
+      "ig_user_id": "17841408660869742"
+    },
+    {
+      "handle": "sheltermagazines",
+      "name": "Shelter Magazines",
+      "ig_user_id": "17841406881694118"
+    },
+    {
+      "handle": "suedewallpaper",
+      "name": "Suede Wallpaper",
+      "ig_user_id": "17841408648437136"
+    },
+    {
+      "handle": "textilewallpaper",
+      "name": "Textile Wallpaper",
+      "ig_user_id": "17841408813467105"
+    },
+    {
+      "handle": "thedesignerlibrary",
+      "name": "Designer Library",
+      "ig_user_id": "17841408572952750"
+    },
+    {
+      "handle": "thesetdecorator",
+      "name": "The Set Decorator",
+      "ig_user_id": "17841406774743877"
+    },
+    {
+      "handle": "traditionalwhimsy",
+      "name": "Traditional Whimsy",
+      "ig_user_id": "17841408645959795"
+    },
+    {
+      "handle": "velvetwallpaper",
+      "name": "Velvet Wallpaper",
+      "ig_user_id": "17841408844140138"
+    },
+    {
+      "handle": "wallpaperhistory",
+      "name": "Wallpaper History",
+      "ig_user_id": "17841408790217103"
+    },
+    {
+      "handle": "wallpaperinstallers",
+      "name": "Wallpaper Installers",
+      "ig_user_id": "17841414900195265"
+    },
+    {
+      "handle": "wallpaperwednesdays",
+      "name": "Wallpaper Wednesdays",
+      "ig_user_id": "17841409211532596"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/scripts/build-dw-accounts.js b/scripts/build-dw-accounts.js
new file mode 100644
index 0000000..1d46729
--- /dev/null
+++ b/scripts/build-dw-accounts.js
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+/*
+ * build-dw-accounts.js — regenerate data/dw-canonical-accounts.json, the canonical
+ * list of every DW-owned Instagram account, from Norma's postable-accounts registry.
+ *
+ * The Reels console keeps its own data/ig-accounts.json as the "managed / has-creds"
+ * layer (ids + publish tokens for the accounts we actually post from). This canonical
+ * file is unioned on top at read time (server.js → readAccountsUnion) so EVERY owned
+ * account is pickable in the console list AND the publish dropdown — the ones without
+ * creds simply show "(needs creds)". One source of truth, mirrored from the same Norma
+ * origin the main Marketing Command Center uses.
+ *
+ * Source (overridable via env):
+ *   NORMA_ACCOUNTS  (default ~/Projects/Norma/agents/instagram-agent/accounts.json)
+ *
+ * Runs on Mac2 (where Norma lives). The OUT file is committed so it ships to Kamatera,
+ * where Norma is unreachable. If the source is missing, the existing file is preserved.
+ * Idempotent.  →  node scripts/build-dw-accounts.js
+ */
+import { readFileSync, writeFileSync, existsSync, renameSync } from 'node:fs';
+import { homedir } from 'node:os';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const SRC = process.env.NORMA_ACCOUNTS ||
+  join(homedir(), 'Projects', 'Norma', 'agents', 'instagram-agent', 'accounts.json');
+const OUT = join(ROOT, 'data', 'dw-canonical-accounts.json');
+
+if (!existsSync(SRC)) {
+  const n = existsSync(OUT) ? (JSON.parse(readFileSync(OUT, 'utf8')).accounts || []).length : 0;
+  console.log(`• Norma source missing (${SRC}) — preserving existing ${n} accounts`);
+  process.exit(0);
+}
+
+const reg = JSON.parse(readFileSync(SRC, 'utf8'));
+const accounts = Object.values(reg.accounts || {})
+  .filter(a => a && a.handle)
+  .map(a => ({ handle: String(a.handle), name: String(a.page_name || a.handle), ig_user_id: a.ig_user_id || null }))
+  .sort((x, y) => x.handle.localeCompare(y.handle));
+
+const out = { source: 'Norma/agents/instagram-agent/accounts.json', generated_at: new Date().toISOString(), count: accounts.length, accounts };
+const tmp = OUT + '.tmp';
+writeFileSync(tmp, JSON.stringify(out, null, 2));
+renameSync(tmp, OUT);
+console.log(`wrote data/dw-canonical-accounts.json — ${accounts.length} DW-owned accounts`);
diff --git a/server.js b/server.js
index c51deb0..97dc2f6 100644
--- a/server.js
+++ b/server.js
@@ -55,6 +55,28 @@ async function readJson(fp, fallback) {
   return existsSync(fp) ? JSON.parse(await readFile(fp, 'utf8')) : fallback;
 }
 
+// The console's account list = the managed layer (data/ig-accounts.json — ids +
+// creds/status for the accounts we actually publish from) UNION the canonical
+// DW-owned accounts (data/dw-canonical-accounts.json, mirrored from Norma). This
+// makes every owned Instagram account pickable in the console list AND the publish
+// dropdown; canonical-only accounts show "(needs creds)" until a token is wired.
+// Deduped by handle — a managed entry always wins (keeps its id/creds/status).
+async function readAccountsUnion() {
+  const stored = await readJson(join(ROOT, 'data', 'ig-accounts.json'), []);
+  const canon = await readJson(join(ROOT, 'data', 'dw-canonical-accounts.json'), { accounts: [] });
+  const have = new Set(stored.map(a => String(a.handle || '').toLowerCase()));
+  const extras = (canon.accounts || [])
+    .filter(c => c.handle && !have.has(String(c.handle).toLowerCase()))
+    .map(c => ({
+      id: 'ig-' + String(c.handle).toLowerCase().replace(/[^a-z0-9-]+/g, '-'),
+      label: c.name || c.handle,
+      handle: c.handle,
+      status: 'pending-creds',
+      source: 'canonical',
+    }));
+  return stored.concat(extras);
+}
+
 // Build + validate a Video Studio job from a request body. Shared by the manual
 // /api/video/queue door AND the "Use it" action on a daily suggestion, so both paths
 // validate identically. Returns { job } on success or { error, status } on rejection.
@@ -158,7 +180,7 @@ const server = http.createServer(async (req, res) => {
         await writeFile(fp, JSON.stringify(list, null, 2));
         return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(list));
       }
-      return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await readJson(fp, [])));
+      return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await readAccountsUnion()));
     }
     if (p === '/api/reel-account' && req.method === 'POST') {
       // set which IG account a reel will publish to: {file, accountId}
@@ -167,7 +189,7 @@ const server = http.createServer(async (req, res) => {
       const reels = await readJson(rp, []);
       const r = reels.find(x => x.file === file);
       if (!r) return res.writeHead(404).end(JSON.stringify({ error: 'reel not found' }));
-      const accounts = await readJson(join(ROOT, 'data', 'ig-accounts.json'), []);
+      const accounts = await readAccountsUnion();
       if (!accounts.some(a => a.id === accountId)) return res.writeHead(400).end(JSON.stringify({ error: 'unknown account' }));
       r.igAccount = accountId;
       await writeFile(rp, JSON.stringify(reels, null, 2));

← 973621a reels: credential-safe fetch guard so Suggested videos loads  ·  back to Dw Marketing Reels  ·  reels: prune 3 dead 2026-07-13 entries whose mp4s never rend 2165410 →