Files
jbergner 74d6b2f7d2
release-tag / release-image (push) Failing after 2m11s
RC-10
2026-08-11 22:08:00 +02:00

24 lines
12 KiB
JavaScript
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.
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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();