← 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 };