Загрузка данных


<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MM2 Auto-Seller</title>
<style>
  :root{
    --bg:#0d1220; --panel:#151b2e; --panel2:#1b2338; --line:#27324d;
    --txt:#dfe6f5; --mut:#8b96ad; --blue:#4da3ff; --green:#28c76f;
    --warn:#ffb400; --red:#ff5c5c;
  }
  *{box-sizing:border-box}
  body{margin:0;background:radial-gradient(1200px 600px at 20% -10%,#1a2440 0%,var(--bg) 55%);
    color:var(--txt);font-family:'Segoe UI',system-ui,-apple-system,sans-serif;min-height:100vh}
  .wrap{max-width:1180px;margin:0 auto;padding:22px}
  header{display:flex;align-items:center;gap:14px;margin-bottom:20px}
  .logo{width:46px;height:46px;border-radius:12px;background:linear-gradient(135deg,#4da3ff,#8b5cff);
    display:grid;place-items:center;font-weight:800;font-size:20px;color:#fff}
  header h1{font-size:20px;margin:0;font-weight:700}
  header p{margin:2px 0 0;color:var(--mut);font-size:13px}
  .badge{margin-left:auto;font-size:12px;padding:6px 12px;border-radius:999px;
    background:#1b2338;border:1px solid var(--line);color:var(--mut)}
  .badge.on{color:#0c1320;background:var(--green);border-color:var(--green);font-weight:700}
  .grid{display:grid;grid-template-columns:1fr 1fr;gap:18px}
  @media(max-width:900px){.grid{grid-template-columns:1fr}}
  .card{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:18px}
  .card h2{font-size:14px;margin:0 0 14px;color:var(--mut);text-transform:uppercase;letter-spacing:.06em}
  textarea{width:100%;height:210px;background:var(--panel2);border:1px solid var(--line);border-radius:12px;
    color:var(--txt);padding:12px;font-family:'Consolas','Menlo',monospace;font-size:12px;resize:vertical}
  textarea:focus{outline:none;border-color:var(--blue)}
  .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:12px}
  button{border:none;cursor:pointer;border-radius:10px;padding:11px 18px;font-size:14px;font-weight:600;
    color:#fff;transition:.15s;font-family:inherit}
  button:disabled{opacity:.45;cursor:not-allowed}
  .btn-blue{background:linear-gradient(135deg,#3d8bff,#7a5cff)}
  .btn-blue:hover:not(:disabled){filter:brightness(1.12)}
  .btn-gray{background:#232c44}
  .btn-gray:hover:not(:disabled){background:#2c3755}
  .meta{display:flex;gap:14px;flex-wrap:wrap;margin-top:14px}
  .chip{font-size:12px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:6px 10px;color:var(--mut)}
  .chip b{color:var(--txt)}
  /* log */
  .logbox{background:#0a0f1d;border:1px solid var(--line);border-radius:12px;height:260px;overflow:auto;
    padding:12px;font-family:'Consolas','Menlo',monospace;font-size:12px;white-space:pre-wrap}
  .logbox::-webkit-scrollbar,.tbl::-webkit-scrollbar{width:9px;height:9px}
  .logbox::-webkit-scrollbar-thumb,.tbl::-webkit-scrollbar-thumb{background:#2c3755;border-radius:6px}
  .l-info{color:var(--mut)} .l-ok{color:var(--green)} .l-warn{color:var(--warn)}
  .l-err{color:var(--red)} .l-sys{color:#6b7690} .l-title{color:var(--blue);font-weight:700}
  .colhead{display:flex;align-items:center;margin:16px 0 10px}
  .colhead h3{font-size:14px;margin:0;color:var(--txt)}
  .count{margin-left:auto;font-size:12px;color:var(--mut)}
  /* stat cards */
  .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:18px 0 4px}
  @media(max-width:600px){.stats{grid-template-columns:1fr}}
  .stat{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:16px 18px}
  .stat .lab{font-size:12px;color:var(--mut);text-transform:uppercase;letter-spacing:.06em}
  .stat .val{font-size:26px;font-weight:800;margin-top:6px;color:var(--txt)}
  .stat .sub{font-size:12px;color:var(--mut);margin-top:2px}
  .stat.green .val{color:var(--green)}
  .stat.blue .val{color:var(--blue)}
  .tbl{background:var(--panel2);border:1px solid var(--line);border-radius:12px;height:300px;overflow:auto}
  table{width:100%;border-collapse:collapse;font-size:12.5px;min-width:680px}
  th,td{padding:9px 12px;text-align:left;border-bottom:1px solid #222b44;white-space:nowrap}
  th{position:sticky;top:0;background:#1b2338;color:var(--mut);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.05em;z-index:2}
  td img{width:30px;height:30px;border-radius:6px;object-fit:cover;background:#0a0f1d}
  .status{display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;font-size:11px;font-weight:600}
  .st-pending{background:#2a3550;color:#c3cbe0}
  .st-success{background:rgba(40,199,111,.16);color:var(--green)}
  .st-error{background:rgba(255,92,92,.16);color:var(--red)}
  .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
  .empty{padding:40px;text-align:center;color:var(--mut)}
  .foot{color:var(--mut);font-size:12px;margin-top:16px;text-align:center}
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="logo">MM</div>
    <div>
      <h1>MM2 Market — Автовыставление</h1>
      <p>Вставь cURL инвентаря — софт сам найдёт onSale=false и выставит на продажу по цене −20%</p>
    </div>
    <div class="badge" id="badge">Ожидание</div>
  </header>

  <div class="grid">
    <div class="card">
      <h2>1. Вставь cURL запрос инвентаря</h2>
      <textarea id="curl" placeholder="curl --url 'https://mm2-market.apineural.com/api/inventory/items/fs/139373/c/rub' \ ..."></textarea>
      <div class="row">
        <button class="btn-blue" id="btnStart">▶ Запустить</button>
        <button class="btn-gray" id="btnExample">Пример</button>
        <button class="btn-gray" id="btnClear">Очистить</button>
      </div>
      <div class="meta" id="meta" style="display:none">
        <span class="chip">Roblox ID: <b id="mAcc">—</b></span>
        <span class="chip">Валюта: <b id="mCur">—</b></span>
        <span class="chip">Заголовков: <b id="mHdr">—</b></span>
        <span class="chip">Токен: <b id="mTok">—</b></span>
      </div>
    </div>

    <div class="card">
      <h2>2. Лог запуска</h2>
      <div class="logbox" id="log"></div>
    </div>
  </div>

  <div class="stats">
    <div class="stat">
      <div class="lab">Найдено (onSale=false)</div>
      <div class="val" id="stFound">0</div>
      <div class="sub">заблокировано для продажи</div>
    </div>
    <div class="stat blue">
      <div class="lab">Выставлено предметов</div>
      <div class="val" id="stListed">0</div>
      <div class="sub">успешно добавлено на продажу</div>
    </div>
    <div class="stat green">
      <div class="lab">Общая сумма выставления</div>
      <div class="val" id="stTotal">0 ₽</div>
      <div class="sub">по цене −20%</div>
    </div>
  </div>

  <div class="colhead">
    <h3>Найденные для продажи</h3>
    <div class="count" id="cnt">0 предметов</div>
  </div>
  <div class="tbl">
    <table>
      <thead>
        <tr>
          <th>Иконка</th><th>Название</th><th>Product ID</th><th>ID предмета</th>
          <th>База (₽)</th><th>Цена выст. (₽)</th><th>Статус</th>
        </tr>
      </thead>
      <tbody id="tbody"></tbody>
    </table>
    <div class="empty" id="empty" style="display:none">Тут появятся предметы, у которых onSale = false</div>
  </div>

  <div class="foot">Пауза между запросами 1 сек · выставление пачками по 10 предметов · лог дублируется в консоль</div>
</div>

<script>
const $ = s => document.querySelector(s);
let state = {logs:[], jobs:[], running:false};

const lvlClass = {info:'l-info',ok:'l-ok',warn:'l-warn',err:'l-err',sys:'l-sys',title:'l-title'};

function renderLog(){
  const el = $('#log');
  const auto = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
  el.innerHTML = state.logs.map(l=>`<div class="${lvlClass[l.lvl]||'l-info'}">${l.t} ${l.msg}</div>`).join('');
  if(auto) el.scrollTop = el.scrollHeight;
}

function stBadge(st){
  if(st==='success') return '<span class="status st-success"><span class="dot"></span>Выставлен</span>';
  if(st==='error')   return '<span class="status st-error"><span class="dot"></span>Ошибка</span>';
  return '<span class="status st-pending"><span class="dot"></span>В очереди</span>';
}

function renderJobs(){
  const tb = $('#tbody');
  const empty = $('#empty');
  const sum = state.summary || {found:0, listed:0, total:0};
  $('#stFound').textContent = sum.found;
  $('#stListed').textContent = sum.listed;
  const tot = (sum.total||0);
  $('#stTotal').textContent = (Number.isInteger(tot) ? tot : tot.toFixed(2)) + ' ₽';
  $('#cnt').textContent = state.jobs.length + ' предметов';
  if(!state.jobs.length){ tb.innerHTML=''; empty.style.display='block'; return; }
  empty.style.display='none';
  tb.innerHTML = state.jobs.map(j=>`
    <tr>
      <td><img src="${j.img||''}" onerror="this.style.visibility='hidden'"></td>
      <td><b>${j.name||''}</b></td>
      <td>${j.productId}</td>
      <td>${j.id}</td>
      <td>${j.base!=null ? j.base : '—'}</td>
      <td>${j.price!=null ? j.price : '—'}</td>
      <td>${stBadge(j.status)}</td>
    </tr>`).join('');
}

async function poll(){
  try{
    const r = await fetch('/api/state');
    const s = await r.json();
    state = s;
    renderLog(); renderJobs();
    const b = $('#badge');
    if(s.running){ b.className='badge on'; b.textContent='Работает…'; }
    else { b.className='badge'; b.textContent='Ожидание'; }
  }catch(e){}
}
setInterval(poll, 500); poll();

$('#btnExample').onclick = ()=>{
  $('#curl').value = `curl --url 'https://mm2-market.apineural.com/api/inventory/items/fs/139373/c/rub' \\
  -H 'accept: */*' \\
  -H 'accept-language: ru,en;q=0.9,en-GB;q=0.8,en-US;q=0.7' \\
  -H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2YTgxNzI0ZDNhOGJhYTc1Y2RiYjhiZjUifSwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTc4ODg4NTMwNCwiZXhwIjoxNzg4ODg2MjA0fQ.0QQ9mk6_nYL7NC11XLfg_4b6u36Uk6yutd0z828UD2Y' \\
  -H 'content-type: application/json' \\
  -H 'origin: https://starpets.gg' \\
  -H 'referer: https://starpets.gg/' \\
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/152.0.0.0'`;
};

$('#btnClear').onclick = ()=>{ $('#curl').value=''; $('#meta').style.display='none'; };

$('#btnStart').onclick = async ()=>{
  const curl = $('#curl').value.trim();
  if(!curl){ alert('Вставь cURL запрос'); return; }
  // парсинг для предпросмотра
  const p = await (await fetch('/api/parse',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({curl})})).json();
  if(p.url){
    $('#mAcc').textContent = p.account_id || '—';
    $('#mCur').textContent = p.currency || '—';
    $('#mHdr').textContent = p.headers || '—';
    $('#mTok').textContent = p.token;
    $('#meta').style.display='flex';
  }
  const res = await (await fetch('/api/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({curl})})).json();
  if(!res.ok) alert(res.error||'Ошибка запуска');
};
</script>
<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a37fbbacda478fe2',t:'MTc4ODg4OTE3MQ=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>