[object Object]

← back to Marketing Command Center

Add /posts/refresh-one endpoint: fresh IG image on 'Make it ours' click (kills stale-URL 403); reuses bulk-refresh path, handle-allowlisted, graceful degrade

796eba1a37ff55a26536a5c72d818664ad82ff6b · 2026-08-25 11:15:08 -0700 · Steve Abrams

Files touched

Diff

commit 796eba1a37ff55a26536a5c72d818664ad82ff6b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 25 11:15:08 2026 -0700

    Add /posts/refresh-one endpoint: fresh IG image on 'Make it ours' click (kills stale-URL 403); reuses bulk-refresh path, handle-allowlisted, graceful degrade
---
 modules/vendors/index.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/modules/vendors/index.js b/modules/vendors/index.js
index d70eedc..89848c7 100644
--- a/modules/vendors/index.js
+++ b/modules/vendors/index.js
@@ -135,5 +135,51 @@ module.exports = {
       }
       res.json({ ok: true, discoveringIg: '@' + ig.username, refreshed: ok, failed, total: targets.length });
     });
+
+    // Refresh ONE account's recent media and RETURN its fresh posts. This is the
+    // "Make it ours" refresh-on-click path (TK-10843): the branded-card compositor
+    // sources p.image, which is a SIGNED IG CDN url that expires (~a day) — a cached
+    // post that's days old returns 403 → /api/img-proxy 502s → the card can't
+    // compose. So before compositing, the client fetches a FRESH image url for that
+    // specific post via a targeted, single-handle refresh.
+    //
+    // Reuses the EXACT bulk-refresh code path (metaToken → discoveringIg →
+    // fetchPosts), scoped to one handle, and writes the SAME cache the bulk refresh
+    // writes (data/vendor-posts-cache.json) so the two stay consistent. It returns
+    // the account's fresh posts (id / permalink / image / timestamp / caption) so the
+    // client can match the clicked post to its refreshed image url.
+    //
+    // COST: Business Discovery is free within Meta rate limits (no metered call).
+    // GRACEFUL DEGRADE: a dead/expired Meta token (see the meta-token-canary
+    // dependency), a rate-limit, or an unknown handle returns { ok:false, error } —
+    // the client shows a clear inline message and does NOT hard-error.
+    router.post('/posts/refresh-one', async (req, res) => {
+      const handle = String((req.body && req.body.handle) || (req.query && req.query.handle) || '')
+        .replace(/^@/, '').trim().slice(0, 80);
+      if (!handle) return res.json({ ok: false, error: 'no handle' });
+      // Only refresh a handle that's actually in the vendor roster (don't let an
+      // arbitrary username be used to probe Business Discovery through our token).
+      const known = load().some(r => handleOf(r).toLowerCase() === handle.toLowerCase() && r.handle !== 'none found');
+      if (!known) return res.json({ ok: false, error: 'unknown handle' });
+
+      const token = metaToken();
+      if (!token) return res.json({ ok: false, error: 'No usable Meta token (META_ACCESS_TOKEN / IG_ACCESS_TOKEN) — check the meta-token canary' });
+      let ig;
+      try { ig = await discoveringIg(token); }
+      catch (e) { return res.json({ ok: false, error: 'token/IG check failed: ' + e.message + (/expired|session/i.test(e.message) ? ' — paste a fresh long-lived META_ACCESS_TOKEN' : '') }); }
+
+      const cache = loadCache();
+      try {
+        const data = await fetchPosts(ig.id, token, handle);
+        cache[handle] = { ...data, fetchedAt: new Date().toISOString(), error: null };
+        saveCache(cache);
+        // Business Discovery is not a metered call — no cost to report.
+        res.json({ ok: true, handle, discoveringIg: '@' + ig.username, fetchedAt: cache[handle].fetchedAt, posts: data.posts || [] });
+      } catch (e) {
+        cache[handle] = { ...(cache[handle] || {}), error: e.message, fetchedAt: new Date().toISOString() };
+        saveCache(cache);
+        res.json({ ok: false, handle, error: e.message + (/rate|limit|#4|#17/i.test(e.message) ? ' — rate-limited, try again shortly' : '') });
+      }
+    });
   },
 };

← 3c85e99 Composer: one create flow — photo → details → song → decide  ·  back to Marketing Command Center  ·  chore: v1.11.0 (session close) — vendor-amplify + Make-it-ou 33e8840 →