24 lines
12 KiB
JavaScript
24 lines
12 KiB
JavaScript
const $=id=>document.getElementById(id);let state={me:null,tasks:[],workers:[],registration:null};
|
||
function msg(t,err=false){const e=$('msg');e.textContent=t;e.className='msg'+(err?' err':'');e.classList.remove('hidden');setTimeout(()=>e.classList.add('hidden'),6500)}
|
||
async function api(path,opt={}){const o={credentials:'same-origin',...opt};if(o.body&&typeof o.body!=='string'){o.headers={...(o.headers||{}),'Content-Type':'application/json'};o.body=JSON.stringify(o.body)}const r=await fetch(path,o);const ct=r.headers.get('content-type')||'';const b=ct.includes('json')?await r.json():await r.text();if(!r.ok){const e=new Error(b?.error||b||`HTTP ${r.status}`);e.status=r.status;e.path=path;throw e}return b}
|
||
const credits=m=>(Number(m||0)/1e6).toLocaleString('de-DE',{maximumFractionDigits:3});
|
||
function showAuth(){state.me=null;$('auth').classList.remove('hidden');$('portal').classList.add('hidden');$('logout').classList.add('hidden');loadRegistrationConfig().catch(()=>{})}
|
||
function showPortal(me){$('auth').classList.add('hidden');$('portal').classList.remove('hidden');$('logout').classList.remove('hidden');$('balance').textContent=credits(me.balance_micros);$('rate').textContent=credits(me.worker_rate_micros_per_minute);$('rewardId').textContent=me.customer.reward_client_id||'noch nicht gekoppelt';$('paypalSection').classList.toggle('hidden',!me.paypal_enabled);$('topGrid').classList.toggle('single',!me.paypal_enabled)}
|
||
async function loadRegistrationConfig(){const c=await api('/api/registration/config');state.registration=c;$('login').disabled=!c.login_enabled;$('loginNote').textContent=c.login_enabled?'':'Anmeldungen sind derzeit administrativ deaktiviert.';$('register').disabled=!c.registration_enabled;$('regInviteRow').classList.toggle('hidden',!c.invite_required);const bonus=credits(c.signup_bonus_micros);let note=c.registration_enabled?(c.signup_bonus_micros>0?`Neukonten erhalten ${bonus} Start-Credits.`:'Registrierung ist geöffnet.'):'Registrierungen sind derzeit deaktiviert.';if(c.registration_enabled&&c.pow_bits>0)note+=` Anti-Bot Proof-of-Work: ${c.pow_bits} Bit.`;if(c.invite_required)note+=' Ein einmaliger Invite-Code ist erforderlich.';$('registrationNote').textContent=note}
|
||
async function boot(){try{await load()}catch(e){if(e.status===401)showAuth();else{showAuth();msg(`Portal konnte nicht geladen werden: ${e.message}`,true)}}const q=new URLSearchParams(location.search);if(q.get('paypal')==='return'&&q.get('token')){try{await api('/api/billing/paypal/capture',{method:'POST',body:{order_id:q.get('token')}});history.replaceState({},'',location.pathname);msg('PayPal-Zahlung verbucht');await load()}catch(e){msg(e.message,true)}}}
|
||
async function load(){const me=await api('/api/me');state.me=me;showPortal(me);const reqs=[['tasks','/api/tasks'],['workers','/api/workers'],['ledger','/api/ledger']];if(me.paypal_enabled)reqs.push(['packages','/api/billing/packages']);const rs=await Promise.allSettled(reqs.map(([,path])=>api(path)));const errors=[];let tasks=[],workers=[],ledger=[],packages={paypal_enabled:false,packages:[]};rs.forEach((r,i)=>{const [name,path]=reqs[i];if(r.status==='fulfilled'){if(name==='tasks')tasks=r.value;if(name==='workers')workers=r.value;if(name==='ledger')ledger=r.value;if(name==='packages')packages=r.value}else{errors.push(`${path}: ${r.reason?.message||'Fehler'}`)}});state.tasks=Array.isArray(tasks)?tasks:[];state.workers=Array.isArray(workers)?workers:[];$('workerCount').textContent=state.workers.length;$('runningCount').textContent=`${state.workers.filter(x=>x.status==='running').length} aktiv`;renderTasks();renderWorkers();renderLedger(Array.isArray(ledger)?ledger:[]);renderPackages(packages||{paypal_enabled:false,packages:[]});if(errors.length)msg(`Session ist aktiv, aber Teile des Portals sind nicht erreichbar: ${errors.join(' · ')}`,true)}
|
||
function taskName(t){return t.display_name||`Task ${String(t.id).slice(-8)}`}
|
||
function renderTasks(){const html=state.tasks.map(t=>`<option value="${esc(t.id)}">${esc(taskName(t))} · ${t.range_bits} bit${t.paused?' · PAUSED':''}</option>`).join('');$('newTask').innerHTML=html}
|
||
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
||
function renderWorkers(){const host=$('workers');if(!state.workers.length){host.innerHTML='<article class="muted">Noch keine Worker angelegt.</article>';return}host.innerHTML=state.workers.map(w=>{const opts=state.tasks.map(t=>`<option value="${esc(t.id)}" ${t.id===w.task_id?'selected':''}>${esc(taskName(t))}</option>`).join('');return `<article class="worker" data-id="${esc(w.id)}"><span class="status ${w.status==='error'?'error':''}">${esc(w.status)}</span><h2>${esc(w.id)}</h2><div class="meta">Worker-ID: ${esc(w.worker_client_id||'wird beim ersten Start erzeugt')}</div><label>Task<select class="task">${opts}</select></label><label>Beacon<select class="path"><option value="auto" ${w.beacon_path==='auto'?'selected':''}>AUTO</option><option value="pulse" ${w.beacon_path==='pulse'?'selected':''}>PULSE</option><option value="flux" ${w.beacon_path==='flux'?'selected':''}>FLUX</option><option value="orbit" ${w.beacon_path==='orbit'?'selected':''}>ORBIT</option></select></label><div class="meta">${w.last_error?`Fehler: ${esc(w.last_error)}`:`Rate: ${credits(w.rate_micros_per_minute)} Credits/min`}</div><div class="buttons"><button data-act="save" class="ghost">ZUORDNUNG</button>${w.status==='running'?'<button data-act="stop" class="ghost">STOP</button>':'<button data-act="start">START</button>'}<button data-act="getid" class="ghost">IDENTITY ↓</button><button data-act="putid" class="ghost">IDENTITY ↑</button><button data-act="delete" class="danger">LÖSCHEN</button><input class="idfile hidden" type="file" accept="application/json,.json"></div></article>`}).join('');host.querySelectorAll('button[data-act]').forEach(b=>b.onclick=()=>workerAction(b.closest('.worker'),b.dataset.act));host.querySelectorAll('.idfile').forEach(i=>i.onchange=()=>uploadIdentity(i.closest('.worker'),i.files?.[0]))}
|
||
async function workerAction(card,act){const id=card.dataset.id;try{if(act==='save')await api(`/api/workers/${encodeURIComponent(id)}`,{method:'PUT',body:{TaskID:card.querySelector('.task').value,BeaconPath:card.querySelector('.path').value}});if(act==='start')await api(`/api/workers/${encodeURIComponent(id)}/start`,{method:'POST'});if(act==='stop')await api(`/api/workers/${encodeURIComponent(id)}/stop`,{method:'POST'});if(act==='delete'){if(!confirm('Worker UND seine private Identity dauerhaft löschen? Vorher Identity herunterladen, falls sie erhalten bleiben soll.'))return;await api(`/api/workers/${encodeURIComponent(id)}`,{method:'DELETE'})}if(act==='getid'){const r=await fetch(`/api/workers/${encodeURIComponent(id)}/identity`,{credentials:'same-origin'});if(!r.ok){const x=await r.json().catch(()=>({error:'Download fehlgeschlagen'}));throw new Error(x.error)}const blob=await r.blob();const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`${id}-identity.json`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000);return}if(act==='putid'){card.querySelector('.idfile').click();return}msg('Worker aktualisiert');await load()}catch(e){msg(e.message,true)}}
|
||
async function uploadIdentity(card,file){if(!file)return;if(!confirm('Die aktuelle Worker-Identity wird ersetzt. Der Worker wird dabei gestoppt. Fortfahren?'))return;try{const r=await fetch(`/api/workers/${encodeURIComponent(card.dataset.id)}/identity`,{method:'PUT',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:await file.text()});const x=await r.json().catch(()=>({}));if(!r.ok)throw new Error(x.error||'Upload fehlgeschlagen');msg('Identity ersetzt');await load()}catch(e){msg(e.message,true)}}
|
||
function ledgerLabel(reason){if(reason==='worker_minute')return'Worker-Laufzeit';if(reason==='positive_tip')return'Positive Tipps';if(reason==='signup_bonus')return'Startguthaben';if(reason==='paypal_topup')return'PayPal-Aufladung';if(reason.startsWith('manual_test_grant'))return'Manuelle Gutschrift';if(reason.startsWith('worker_start_refund'))return'Worker-Erstattung';return reason.replaceAll('_',' ')}
|
||
function renderLedger(xs){$('ledger').innerHTML=xs.length?xs.map(x=>`<div class="ledger-row"><span><strong>${esc(ledgerLabel(x.reason))}</strong>${x.count>1?` <small>× ${x.count}</small>`:''}</span><span>${new Date(x.created_at).toLocaleDateString()} </span><strong class="${x.delta_micros>=0?'credit':'debit'}">${x.delta_micros>=0?'+':''}${credits(x.delta_micros)}</strong></div>`).join(''):'<div class="muted">Noch keine Buchungen.</div>'}
|
||
function renderPackages(v){const sec=$('paypalSection');if(!v.paypal_enabled){sec.classList.add('hidden');$('packages').innerHTML='';return}sec.classList.remove('hidden');$('paypalNote').textContent=`PayPal ${v.environment||''} · Credits werden erst nach serverseitig bestätigtem Capture verbucht.`;$('packages').innerHTML=(v.packages||[]).map(p=>`<div class="package"><span><strong>${credits(p.credits_micros)} Credits</strong><br><small>${(p.amount_cents/100).toFixed(2)} ${esc(p.currency)}</small></span><button data-pkg="${esc(p.id)}">PAYPAL</button></div>`).join('');$('packages').querySelectorAll('[data-pkg]').forEach(b=>b.onclick=async()=>{try{const x=await api('/api/billing/paypal/order',{method:'POST',body:{package_id:b.dataset.pkg}});location.href=x.approval_url}catch(e){msg(e.message,true)}})}
|
||
function leadingZeroBits(buf,bits){const a=new Uint8Array(buf);let full=Math.floor(bits/8),rem=bits%8;for(let i=0;i<full;i++)if(a[i]!==0)return false;if(rem)return (a[full]&(0xff<<(8-rem)))===0;return true}
|
||
async function solveRegistrationPow(username,challenge,bits){if(!bits)return 0;if(!globalThis.crypto?.subtle)throw new Error('WebCrypto wird für den Anti-Bot-Proof benötigt. Bitte HTTPS/aktuellen Browser verwenden.');const enc=new TextEncoder(),batch=192;let base=0;while(true){const jobs=[];for(let i=0;i<batch;i++){const n=base+i;const msg=`nh-register|${username.trim().toLowerCase()}|${challenge}|${n}`;jobs.push(crypto.subtle.digest('SHA-256',enc.encode(msg)).then(h=>[n,h]))}const out=await Promise.all(jobs);for(const [n,h] of out)if(leadingZeroBits(h,bits))return n;base+=batch;if(base%(batch*40)===0){$('register').textContent=`ANTI-BOT · ${base.toLocaleString('de-DE')}`;await new Promise(r=>setTimeout(r,0))}}}
|
||
$('login').onclick=async()=>{try{await api('/api/login',{method:'POST',body:{Username:$('loginUser').value,Password:$('loginPass').value}});await load()}catch(e){msg(e.message,true)}};
|
||
$('register').onclick=async()=>{const b=$('register');try{const username=$('regUser').value,password=$('regPass').value;const cfg=state.registration||await api('/api/registration/config');if(!cfg.registration_enabled)throw new Error('Registrierungen sind derzeit deaktiviert.');b.disabled=true;b.textContent='PRÜFE…';const ch=await api('/api/register/challenge',{method:'POST',body:{username}});const counter=await solveRegistrationPow(username,ch.challenge,ch.pow_bits||0);b.textContent='REGISTRIERE…';const x=await api('/api/register',{method:'POST',body:{Username:username,Password:password,Challenge:ch.challenge||'',ProofCounter:counter,InviteCode:$('regInvite').value.trim()}});msg(x.signup_bonus_micros>0?`Konto erstellt · ${credits(x.signup_bonus_micros)} Start-Credits`:'Konto erstellt');await load()}catch(e){msg(e.message,true)}finally{b.textContent='REGISTRIEREN';b.disabled=!(state.registration?.registration_enabled??true)}};
|
||
$('logout').onclick=async()=>{await api('/api/logout',{method:'POST'}).catch(()=>{});location.reload()};$('saveReward').onclick=async()=>{try{const code=$('rewardLinkCode').value.trim();if(!code)throw new Error('Hosted-Code einfügen');const x=await api('/api/reward-identity',{method:'PUT',body:{link_code:code}});$('rewardLinkCode').value='';msg(x.warning||'Haupt-Identität sicher gekoppelt',!!x.warning);await load()}catch(e){msg(e.message,true)}};$('clearReward').onclick=async()=>{if(!confirm('Reward-Kopplung wirklich lösen? Laufende Worker sollten vorher gestoppt werden.'))return;try{await api('/api/reward-identity',{method:'PUT',body:{clear:true}});msg('Reward-Kopplung gelöst');await load()}catch(e){msg(e.message,true)}};$('newWorker').onclick=()=> $('newWorkerBox').classList.toggle('hidden');$('createWorker').onclick=async()=>{try{await api('/api/workers',{method:'POST',body:{TaskID:$('newTask').value,BeaconPath:$('newPath').value}});$('newWorkerBox').classList.add('hidden');await load()}catch(e){msg(e.message,true)}};boot();
|