← back to Pattern Vault

wpb-uploaders/public/control-panel.html

94 lines

<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>WPB Uploader Control Panel</title>
<style>
  :root{--bg:#0d0e12;--card:#15171e;--line:#262a35;--ink:#f2f2f5;--dim:#9aa0ad;--accent:#f4a261;--good:#2ec4b6;--warn:#e9c46a;--bad:#e76f51;--pending:#8a8f9c}
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
  .wrap{max-width:900px;margin:0 auto;padding:26px 20px 80px}
  h1{font:700 22px/1 Georgia,serif;margin:0 0 4px}
  h1 b{color:var(--accent)}
  .sub{color:var(--dim);font-size:13px;margin-bottom:22px}
  .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px 18px;margin-bottom:12px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
  .card .nm{font-weight:700;font-size:16px}
  .card .meta{color:var(--dim);font-size:12.5px}
  .pill{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 9px;border-radius:999px;white-space:nowrap}
  .pill.good{background:rgba(46,196,182,.16);color:var(--good)}
  .pill.warn{background:rgba(233,196,106,.16);color:var(--warn)}
  .pill.bad{background:rgba(231,111,81,.16);color:var(--bad)}
  .pill.pending{background:rgba(138,143,156,.16);color:var(--pending)}
  .grow{flex:1;min-width:180px}
  .actions{display:flex;gap:8px;flex-wrap:wrap}
  button{background:var(--accent);color:#141414;border:0;border-radius:9px;padding:9px 15px;font-weight:700;font-size:13px;cursor:pointer}
  button.ghost{background:transparent;color:var(--ink);border:1px solid var(--line)}
  button:disabled{opacity:.4;cursor:not-allowed}
  button:hover:not(:disabled){filter:brightness(1.07)}
  .msg{font-size:12.5px;color:var(--dim);margin-top:8px;width:100%}
  input.tok{background:#1b1e27;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:8px 10px;font-size:12.5px;min-width:220px;flex:1}
  .foot{color:var(--dim);font-size:11.5px;margin-top:24px;text-align:center}
  a{color:var(--accent)}
</style></head><body>
<div class="wrap">
  <h1><b>WPB</b> Uploader Control Panel</h1>
  <div class="sub">Launch each platform yourself. Spoonflower is live; the rest need one thing from you.</div>
  <div id="cards"></div>
  <div class="foot">Local · admin:•••• · :9813 · Spoonflower uploads daily on its own. $0 (local).</div>
</div>
<script>
const $ = s => document.querySelector(s);
const api = (u,m) => fetch(u,{method:m||'GET'}).then(r=>r.json());
function pill(row){
  if(row.key==='spoonflower') return '<span class="pill good">● LIVE</span>';
  if(row.running) return '<span class="pill warn">◐ login window open…</span>';
  if(row.apiBased) return row.blocker==='token present'?'<span class="pill warn">token — dry-run ready</span>':'<span class="pill bad">needs token</span>';
  if(row.hasSession) return '<span class="pill warn">session ✓ — calibrating</span>';
  if(row.blocker.includes('pending')) return '<span class="pill pending">account pending</span>';
  return '<span class="pill bad">needs login</span>';
}
function actions(row){
  if(row.key==='spoonflower') return '<span class="meta">Uploads daily via the armed cron.</span>';
  if(row.apiBased) return `
    <input class="tok" id="etsytok" placeholder="paste Etsy OAuth token (listings_w)…">
    <button class="ghost" onclick="etsyDry()">Preview 38 drafts</button>
    <button onclick="etsyPush()" title="gated — pushes drafts once token is set">Push drafts</button>`;
  if(row.blocker.includes('pending')) return '<span class="meta">Confirm the account (check email), then it moves to “needs login”.</span>';
  // web-login platforms
  return `
    <button onclick="cap('${row.key}')" ${row.running?'disabled':''}>${row.hasSession?'Re-login':'Log in'}</button>
    <button class="ghost" onclick="verify('${row.key}')">Verify session</button>`;
}
async function render(){
  const {rows} = await api('/api/status');
  $('#cards').innerHTML = rows.map(r=>`
    <div class="card" data-k="${r.key}">
      <div class="grow">
        <div class="nm">${r.name} ${pill(r)}</div>
        <div class="meta">${r.brand} · ${r.blocker}</div>
      </div>
      <div class="actions">${actions(r)}</div>
      <div class="msg" id="msg-${r.key}"></div>
    </div>`).join('');
}
function msg(k,t){ const el=$('#msg-'+k); if(el) el.textContent=t; }
async function cap(k){
  msg(k,'launching a login window… log in there, then click “Verify session”.');
  const r = await api('/api/capture/'+k,'POST');
  msg(k, r.ok ? '✅ browser opening — log in, then click “Verify session”.' : ('✗ '+(r.error||'failed')));
  setTimeout(render, 1200);
}
async function verify(k){
  msg(k,'checking your session (loading the account page)…');
  const r = await api('/api/verify/'+k,'POST');
  msg(k, r.captured ? '✅ session is GENUINELY logged in — captured & verified.' : '✗ not logged in yet (finish the login, then Verify again).');
  render();
}
async function etsyDry(){
  const r = await api('/api/etsy/dryrun');
  msg('etsy', r.error?('✗ '+r.error):`✅ ${r.count} drafts mapped @ $${r.price}. Blockers: ${(r.blockers||[]).join('; ')||'none'}. (Sample title: ${(r.sample&&r.sample[0]&&r.sample[0].title||'').slice(0,50)})`);
}
function etsyPush(){ msg('etsy','⛔ Pushing live drafts is gated — paste the token + say “push etsy” to Claude, who’ll fire the 38 drafts (Steve-approved).'); }
render(); setInterval(render, 5000);
</script>
</body></html>