Files

63 lines
8.4 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin · PolHunter</title>
<meta name="robots" content="noindex,nofollow">
<link rel="stylesheet" href="/style.css?v=12">
<style>
label{display:block;font-size:12px;color:var(--dim);letter-spacing:.08em;text-transform:uppercase;margin:10px 0 4px}
input,textarea,select{width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--edge);background:rgba(0,0,0,.35);color:var(--ink);font:14px var(--font)}
textarea{min-height:70px} .two{display:grid;grid-template-columns:1fr 1fr;gap:12px}
table{width:100%;border-collapse:collapse;font-size:13px} td,th{padding:8px 10px;border-bottom:1px solid var(--edge);text-align:left;vertical-align:top} th{color:var(--dim);font-weight:600;font-size:11px;letter-spacing:.1em;text-transform:uppercase}
.gate{display:inline-block;padding:3px 9px;border-radius:999px;font-size:11px;letter-spacing:.1em;text-transform:uppercase;border:1px solid var(--edge)} .gate.on{color:var(--ok);border-color:rgba(75,227,165,.4)} .gate.off{color:var(--bad);border-color:rgba(255,122,122,.4)}
</style>
</head>
<body>
<div class="wrap">
<header class="top"><a class="mark" href="/"><span class="coin"></span>PolHunter <span style="font-weight:400;color:var(--dim)">admin</span></a><nav class="nav"><input id="key" placeholder="admin key" style="width:220px"><button class="btn sm" id="go">Open</button></nav></header>
<div id="ui" hidden>
<section class="section" style="padding-top:6px"><h2>Posture</h2><div id="gates"></div></section>
<section class="section"><h2>Pool</h2><div class="stats" id="stats"></div>
<div class="card"><div class="two"><div><label>Min POL per drip</label><input id="minPol"></div><div><label>Max POL per drip</label><input id="maxPol"></div><div><label>Daily cap (POL)</label><input id="dailyCapPol"></div><div><label>Low-balance alert (POL)</label><input id="lowBalancePol"></div></div><div style="margin-top:12px"><button class="btn sm" id="saveSettings">Save pool settings</button> <button class="btn ghost sm" id="tickFaucet">Run the faucet now</button> <span id="faucetLine" style="font-size:13px;color:var(--muted)"></span></div></div>
</section>
<section class="section"><h2>Missions</h2>
<div class="card"><div class="two"><div><label>id (slug)</label><input id="m_id" placeholder="iap-ledger"></div><div><label>Site label</label><input id="m_site" placeholder="InstantAdPay"></div></div>
<label>Name</label><input id="m_name" placeholder="Find your code on the live ledger">
<label>Brief (what the hunter is told)</label><textarea id="m_brief" placeholder="Open the public ledger and find your code next to the newest payout."></textarea>
<label>URL the mission link opens (the token is appended)</label><input id="m_url" placeholder="https://instantadpay.com/ledger">
<div class="two"><div><label>Dwell (seconds)</label><input id="m_dwell" value="45"></div><div><label>Slots on that page (hiding places)</label><input id="m_slots" value="1"></div><div><label>Budget (completions, 0 = unlimited)</label><input id="m_budget" value="0"></div><div><label>Active</label><select id="m_active"><option value="true">yes</option><option value="false">no</option></select></div></div>
<div style="margin-top:12px"><button class="btn sm" id="saveMission">Save mission</button> <span id="missionMsg" style="font-size:13px;color:var(--muted)"></span></div></div>
<table id="missions" style="margin-top:14px"></table>
</section>
<section class="section"><h2>Recent drips</h2><table id="payouts"></table></section>
</div>
</div>
<script>
(() => {
const $ = id => document.getElementById(id); const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
let KEY = ''; const api = async (p, opt) => (await fetch(p, Object.assign({ headers: { 'X-Admin-Key': KEY, 'Content-Type': 'application/json' } }, opt || {}))).json();
let st = null;
async function load() {
st = await api('/api/admin/state'); if (st.error) { alert(st.error); return; }
$('ui').hidden = false;
const g = st.gates; $('gates').innerHTML = ['outbound', 'signups', 'curtain', 'sso'].map(k => '<span class="gate ' + (g[k] ? 'on' : 'off') + '">' + k + ': ' + (g[k] ? 'on' : 'off') + '</span> ').join('') + ' <span class="gate ' + (st.faucet.on ? 'on' : 'off') + '">faucet: ' + (st.faucet.on ? esc(st.faucet.address) : 'off') + '</span>';
const t = st.totals; $('stats').innerHTML = [['paid today', (t.today || 0).toFixed(2) + ' POL'], ['queued', t.queued], ['drips paid', t.paid], ['POL paid', t.pol], ['hunters', t.hunters], ['faucet balance', st.faucet.state && st.faucet.state.balance != null ? Number(st.faucet.state.balance).toFixed(2) + ' POL' : '—']].map(([l, v]) => '<div class="stat"><div class="v num">' + v + '</div><div class="l">' + l + '</div></div>').join('');
for (const k of ['minPol', 'maxPol', 'dailyCapPol', 'lowBalancePol']) $(k).value = st.settings[k];
$('missions').innerHTML = '<tr><th>id</th><th>site</th><th>name</th><th>url</th><th>dwell</th><th>slots</th><th>budget</th><th>active</th><th></th></tr>' + st.missions.map(m => '<tr><td class="num">' + esc(m.id) + '</td><td>' + esc(m.site) + '</td><td>' + esc(m.name) + '</td><td style="max-width:220px;word-break:break-all">' + esc(m.url) + '</td><td>' + m.dwell + '</td><td>' + (m.slots || 1) + '</td><td>' + (m.budget || '∞') + '</td><td>' + (m.active ? 'yes' : 'no') + '</td><td><button class="btn ghost sm" data-edit="' + esc(m.id) + '">edit</button> <button class="btn ghost sm" data-test="' + esc(m.id) + '">test link</button> <button class="btn ghost sm" data-del="' + esc(m.id) + '">×</button></td></tr>').join('');
$('payouts').innerHTML = '<tr><th>when</th><th>who</th><th>mission</th><th>POL</th><th>status</th><th>tx / error</th></tr>' + st.payouts.map(p => '<tr><td>' + new Date(p.at).toLocaleString() + '</td><td>' + esc(p.username ? '@' + p.username : '#' + p.memberId) + '</td><td>' + esc(p.missionId) + '</td><td class="num">' + p.pol + '</td><td>' + esc(p.status) + '</td><td style="word-break:break-all">' + esc(p.tx || p.error || '') + '</td></tr>').join('');
document.querySelectorAll('[data-edit]').forEach(b => b.addEventListener('click', () => { const m = st.missions.find(x => x.id === b.dataset.edit); for (const k of ['id', 'site', 'name', 'brief', 'url', 'dwell', 'slots', 'budget']) $('m_' + k).value = m[k] == null ? '' : m[k]; $('m_active').value = String(m.active !== false); window.scrollTo({ top: $('m_id').getBoundingClientRect().top + window.scrollY - 80, behavior: 'smooth' }); }));
document.querySelectorAll('[data-del]').forEach(b => b.addEventListener('click', async () => { if (!confirm('Delete mission ' + b.dataset.del + '?')) return; await api('/api/admin/mission', { method: 'DELETE', body: JSON.stringify({ id: b.dataset.del }) }); load(); }));
document.querySelectorAll('[data-test]').forEach(b => b.addEventListener('click', async () => { const r = await api('/api/admin/embed-test?id=' + encodeURIComponent(b.dataset.test)); if (r.url) { window.open(r.url, '_blank', 'noopener'); alert('Opened with a test token. The code renders after ' + r.dwell + 's.'); } else alert(r.error || 'failed'); }));
}
$('go').addEventListener('click', () => { KEY = $('key').value.trim(); load(); });
$('key').addEventListener('keydown', e => { if (e.key === 'Enter') $('go').click(); });
$('saveSettings').addEventListener('click', async () => { const b = {}; for (const k of ['minPol', 'maxPol', 'dailyCapPol', 'lowBalancePol']) b[k] = Number($(k).value); const r = await api('/api/admin/settings', { method: 'POST', body: JSON.stringify(b) }); if (r.error) alert(r.error); load(); });
$('tickFaucet').addEventListener('click', async () => { $('faucetLine').textContent = 'running…'; const r = await api('/api/admin/faucet/tick', { method: 'POST' }); $('faucetLine').textContent = r.error ? r.error : 'paid ' + r.paid + ' · balance ' + (r.balance == null ? '—' : Number(r.balance).toFixed(4) + ' POL'); load(); });
$('saveMission').addEventListener('click', async () => { const b = {}; for (const k of ['id', 'site', 'name', 'brief', 'url', 'dwell', 'slots', 'budget']) b[k] = $('m_' + k).value; b.active = $('m_active').value === 'true'; const r = await api('/api/admin/mission', { method: 'POST', body: JSON.stringify(b) }); $('missionMsg').textContent = r.error || 'saved'; load(); });
})();
</script>
</body>
</html>