← back to Norma
agents/instagram-agent/public/spoonflower-viewer.html
90 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Spoonflower Purge — flagged posts</title>
<style>
:root { --bg:#0c0f1a; --surface:#151a2b; --el:#1d2440; --line:#2a3350; --text:#eef1f8; --mut:#9aa3c0;
--red:#f4436b; --amber:#f5a623; --green:#25c281; }
* { box-sizing:border-box; }
body { margin:0; background:var(--bg); color:var(--text); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
header { padding:22px 26px; border-bottom:1px solid var(--line); background:linear-gradient(180deg,#141a2e,#0c0f1a); }
h1 { margin:0 0 4px; font-size:20px; letter-spacing:.2px; }
.sub { color:var(--mut); font-size:13px; }
.stats { display:flex; gap:10px; margin-top:14px; flex-wrap:wrap; }
.pill { background:var(--el); border:1px solid var(--line); border-radius:999px; padding:5px 13px; font-size:13px; }
.pill b { color:#fff; }
.pill.red b { color:var(--red); } .pill.green b { color:var(--green); }
main { padding:22px 26px; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:18px; }
.card { background:var(--surface); border:1px solid var(--line); border-radius:14px; overflow:hidden; display:flex; flex-direction:column; }
.thumb { aspect-ratio:1/1; background:#0a0d17 center/cover no-repeat; position:relative; }
.badge { position:absolute; top:10px; left:10px; font-size:11px; font-weight:700; letter-spacing:.4px; text-transform:uppercase;
padding:4px 9px; border-radius:6px; }
.badge.pending { background:rgba(244,67,107,.16); color:var(--red); border:1px solid rgba(244,67,107,.4); }
.badge.deleted { background:rgba(37,194,129,.16); color:var(--green); border:1px solid rgba(37,194,129,.4); }
.kind { position:absolute; top:10px; right:10px; font-size:11px; padding:4px 8px; border-radius:6px; background:rgba(0,0,0,.5); color:var(--mut); }
.body { padding:13px 14px; display:flex; flex-direction:column; gap:8px; flex:1; }
.acct { font-weight:600; }
.acct span { color:var(--mut); font-weight:400; font-size:12px; }
.when { color:var(--mut); font-size:12px; }
.cap { color:#cfd6ee; font-size:13px; max-height:4.5em; overflow:hidden; }
.cap b { color:var(--amber); }
.row { display:flex; gap:8px; margin-top:auto; padding-top:6px; }
a.btn { text-decoration:none; text-align:center; flex:1; padding:8px; border-radius:8px; font-size:13px; font-weight:600;
background:var(--el); color:var(--text); border:1px solid var(--line); }
a.btn:hover { border-color:#3a4570; }
.note { color:var(--mut); font-size:12.5px; margin-top:18px; padding:12px 14px; background:var(--surface); border:1px solid var(--line); border-radius:10px; }
</style>
</head>
<body>
<header>
<h1>🧹 Spoonflower Purge — flagged posts</h1>
<div class="sub">Competitor (Spoonflower) references across the DW Instagram fleet. Deletion runs via logged-in Chrome; this board updates as posts clear.</div>
<div class="stats" id="stats"></div>
</header>
<main>
<div class="grid" id="grid"></div>
<div class="note" id="note"></div>
</main>
<script id="sf-data" type="application/json">/*__SPOONFLOWER_JSON__*/</script>
<script>
const esc = (s) => (s||'').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
const hi = (s) => esc(s).replace(/(spoonflower)/ig, '<b>$1</b>');
// Server injects the data into #sf-data so the board renders even if the authed
// XHR is blocked (Chrome strips URL creds → /api/spoonflower 401). XHR is refresh-only.
function injected() { try { return JSON.parse(document.getElementById('sf-data').textContent); } catch { return null; } }
function render(d) {
if (!d || !d.posts) return;
const stats = document.getElementById('stats');
stats.innerHTML =
`<span class="pill">Total flagged <b>${d.total}</b></span>` +
`<span class="pill red">Pending delete <b>${d.pending}</b></span>` +
`<span class="pill green">Deleted <b>${d.deleted}</b></span>`;
document.getElementById('grid').innerHTML = d.posts.map(p => `
<div class="card">
<div class="thumb" style="background-image:url('${esc(p.image)}')">
<span class="badge ${p.status}">${p.status === 'deleted' ? '✓ deleted' : '● pending delete'}</span>
<span class="kind">${p.kind === 'reshare' ? 'reshare' : 'hashtag'}</span>
</div>
<div class="body">
<div class="acct">@${esc(p.handle)} <span>· ${esc(p.page_name)}</span></div>
<div class="when">🕓 ${new Date(p.timestamp).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})}</div>
<div class="cap">${hi(p.caption)}</div>
<div class="row"><a class="btn" href="${esc(p.permalink)}" target="_blank" rel="noopener">View on Instagram ↗</a></div>
</div>
</div>`).join('');
document.getElementById('note').innerHTML =
'<b>reshare</b> = Norma auto-generated post crediting Spoonflower (clearly in scope). ' +
'<b>hashtag</b> = a genuine 2022 repost where #spoonflowerwallpaper is 1 of 16 tags (Steve chose: delete it too). ' +
'Deletion needs the openclaw Chrome logged in as each owning account, then runs serialized + verified.';
}
async function refresh() { try { const r = await fetch('/api/spoonflower'); if (r.ok) render(await r.json()); } catch {} }
render(injected()); // first paint from server-injected data (no auth needed)
refresh(); // then try a live refresh (silently ignored if XHR is 401)
setInterval(refresh, 15000);
</script>
</body>
</html>