← back to Norma

agents/instagram-agent/spoonflower-api.js

70 lines

/**
 * spoonflower-api.js — a focused viewer for the Spoonflower-competitor purge.
 * The 4 flagged posts are RESHARES (not in post-ledger.jsonl), so the normal /posts
 * viewer can't show them. This surfaces them with thumbnails + captions + live
 * delete-status (cross-referenced against deleted-posts.jsonl), so Steve can SEE
 * exactly what's staged for deletion and watch it clear as deletes run.
 *
 *   GET /spoonflower       → viewer HTML (public/spoonflower-viewer.html)
 *   GET /api/spoonflower   → { posts:[...], total, deleted, pending }
 */
const fs = require('fs');
const path = require('path');

const DIR = __dirname;
const DATA = path.join(DIR, 'data');
const ENRICHED = path.join(DATA, 'spoonflower-hitlist-enriched.json');
const HITLIST = path.join(DATA, 'spoonflower-hitlist.jsonl');
const TOMBS = path.join(DATA, 'deleted-posts.jsonl');
const VIEWER = path.join(DIR, 'public', 'spoonflower-viewer.html');

function readJsonl(file) {
  if (!fs.existsSync(file)) return [];
  return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean)
    .map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
}

function loadHits() {
  if (fs.existsSync(ENRICHED)) { try { return JSON.parse(fs.readFileSync(ENRICHED, 'utf8')); } catch { /* fall through */ } }
  return readJsonl(HITLIST);
}

function getSpoonflower() {
  const deleted = new Set(readJsonl(TOMBS).map((t) => t.permalink).filter(Boolean));
  const hits = loadHits().map((h) => {
    // reshares literally headline Spoonflower; the 2022 post is an incidental hashtag
    const reshare = /^(We saw|Okay)\s+Spoonflower/i.test(h.caption || '');
    return {
      handle: h.handle,
      page_name: h.page_name || h.handle,
      permalink: h.permalink,
      media_id: h.media_id,
      timestamp: h.timestamp,
      image: h.thumbnail_url || h.media_url || '',
      caption: h.caption || '',
      kind: reshare ? 'reshare' : 'incidental-hashtag',
      status: deleted.has(h.permalink) ? 'deleted' : 'pending',
    };
  }).sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
  return { posts: hits, total: hits.length,
    deleted: hits.filter((h) => h.status === 'deleted').length,
    pending: hits.filter((h) => h.status === 'pending').length };
}

function registerSpoonflowerRoutes(app) {
  app.get('/spoonflower', (req, res) => {
    if (!fs.existsSync(VIEWER)) return res.status(404).send('spoonflower-viewer.html missing');
    // Inject the data straight into the page so it renders without a separate authed XHR
    // (browsers strip URL creds → /api/spoonflower 401 → empty board).
    let html = fs.readFileSync(VIEWER, 'utf8');
    const json = JSON.stringify(getSpoonflower()).replace(/</g, '\\u003c');
    html = html.replace('/*__SPOONFLOWER_JSON__*/', json);
    res.type('html').send(html);
  });
  app.get('/api/spoonflower', (req, res) => {
    try { res.json(getSpoonflower()); } catch (e) { res.status(500).json({ error: e.message }); }
  });
}

module.exports = { registerSpoonflowerRoutes, getSpoonflower };