17 lines
12 KiB
JavaScript
17 lines
12 KiB
JavaScript
const $=id=>document.getElementById(id);let manual=false,last=null,taskCatalog=[];
|
|
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))}
|
|
function msg(t,e=false){$('msg').textContent=t;$('msg').className='msg'+(e?' err':'');$('msg').classList.remove('hidden');setTimeout(()=>$('msg').classList.add('hidden'),7000)}
|
|
async function api(p,o={}){if(o.body&&typeof o.body!=='string'){o.headers={'Content-Type':'application/json',...(o.headers||{})};o.body=JSON.stringify(o.body)}const r=await fetch(p,{credentials:'same-origin',...o});const x=await r.json().catch(()=>({}));if(!r.ok){const e=new Error(x.error||`HTTP ${r.status}`);e.status=r.status;throw e}return x}
|
|
const cr=m=>(Number(m||0)/1e6).toLocaleString('de-DE',{maximumFractionDigits:3});
|
|
function settingsToUI(s){$('loginEnabled').checked=!!s.login_enabled;$('registrationEnabled').checked=!!s.registration_enabled;$('inviteRequired').checked=!!s.invite_required;$('powBits').value=s.registration_pow_bits??0;$('signupBonus').value=Number(s.signup_bonus_micros||0)/1e6;$('positiveBonus').value=Number(s.positive_tip_bonus_micros||0)/1e6;$('inviteCount').textContent=s.active_registration_invites??0}
|
|
function settingsPayload(){return{login_enabled:$('loginEnabled').checked,registration_enabled:$('registrationEnabled').checked,invite_required:$('inviteRequired').checked,registration_pow_bits:Number($('powBits').value||0),signup_bonus_credits:Number($('signupBonus').value||0),positive_tip_bonus_credits:Number($('positiveBonus').value||0)}}
|
|
async function saveSettings(){try{const x=await api('/api/admin/settings',{method:'PUT',body:settingsPayload()});settingsToUI(x);msg('Portal-Einstellungen gespeichert')}catch(e){msg(e.message,true)}}
|
|
function workerRows(c){if(!c.worker_items?.length)return'<div class="small">Keine Worker.</div>';return `<div class="worker-list">${c.worker_items.map(w=>`<div class="worker-row"><div class="worker-main"><strong>${esc(w.id)}</strong><div class="small">${esc(w.worker_client_id||'noch keine Identity')} · Task ${esc(w.task_id)} · Host ${esc(w.controller_id||'local')}</div>${w.last_error?`<div class="small danger-text">${esc(w.last_error)}</div>`:''}</div><span class="pill ${w.status==='running'?'on':''}">${esc(w.status)}</span><button class="ghost updateWorkerImage" data-wid="${esc(w.id)}">IMAGE UPDATE</button><button class="ghost stopWorker" data-wid="${esc(w.id)}" ${w.status==='stopped'?'disabled':''}>STOP</button></div>`).join('')}</div>`}
|
|
function controllerHTML(c){const state=c.online?'<span class="pill on">ONLINE</span>':'<span class="pill danger">OFFLINE</span>',drain=c.draining?'<span class="pill warn">DRAIN</span>':'',disabled=!c.enabled?'<span class="pill danger">DISABLED</span>':'';return `<div class="controller-card ${c.online?'':'offline'} ${c.draining?'drain':''}" data-cid="${esc(c.id)}"><div class="controller-head"><div><strong>${esc(c.name||c.id)}</strong><div class="small">${esc(c.id)} · protocol v${Number(c.protocol_version||0)} ${state} ${drain} ${disabled}</div></div><div class="small">${Number(c.assigned_running||0)}/${Number(c.max_running||0)} running<br>${Number(c.assigned_workers||0)}/${Number(c.max_workers||0)} assigned</div></div><div class="controller-host">${esc(c.base_url||'—')}</div><div class="small">Heartbeat: ${c.last_seen?new Date(c.last_seen).toLocaleString('de-DE'):'—'} · reported ${Number(c.reported_running||0)}/${Number(c.reported_workers||0)}</div><div class="controller-actions"><button class="ghost controllerDrain">${c.draining?'DRAIN AUFHEBEN':'DRAIN'}</button><button class="ghost controllerEnable">${c.enabled?'DEAKTIVIEREN':'AKTIVIEREN'}</button></div></div>`}
|
|
function bindControllers(){document.querySelectorAll('.controllerDrain').forEach(b=>b.onclick=async()=>{const row=b.closest('.controller-card'),c=last?.controllers?.find(x=>x.id===row.dataset.cid);try{await api(`/api/admin/controllers/${encodeURIComponent(row.dataset.cid)}`,{method:'PUT',body:{enabled:c?.enabled!==false,draining:!c?.draining}});msg(c?.draining?'Drain aufgehoben':'Controller auf Drain gesetzt');await load()}catch(e){msg(e.message,true)}});document.querySelectorAll('.controllerEnable').forEach(b=>b.onclick=async()=>{const row=b.closest('.controller-card'),c=last?.controllers?.find(x=>x.id===row.dataset.cid);const enabled=!(c?.enabled!==false);if(!enabled&&!confirm('Controller deaktivieren? Bestehende Worker werden nicht automatisch verschoben; Steueraktionen sind bis zur Reaktivierung blockiert.'))return;try{await api(`/api/admin/controllers/${encodeURIComponent(row.dataset.cid)}`,{method:'PUT',body:{enabled,draining:c?.draining||false}});msg(enabled?'Controller aktiviert':'Controller deaktiviert');await load()}catch(e){msg(e.message,true)}})}
|
|
function taskOptions(){return taskCatalog.map(t=>`<option value="${esc(t.id)}">${esc(t.display_name||t.id.slice(-12))} · ${Number(t.range_bits||0)} Bit</option>`).join('')}
|
|
function customerHTML(c){const status=c.blocked?`<span class="pill danger">GESPERRT</span>`:'<span class="pill on">AKTIV</span>',bypass=c.worker_limit_bypass?'<span class="pill warn">LIMIT BYPASS</span>':'';return `<article class="customer ${c.blocked?'blocked':''}" data-id="${esc(c.id)}"><div class="customer-head"><div><h3>${esc(c.username)} ${status} ${bypass}</h3><div class="small">${esc(c.id)} · Reward ${esc(c.reward_client_id||'—')}</div>${c.blocked_reason?`<div class="small danger-text">Grund: ${esc(c.blocked_reason)}</div>`:''}</div><div class="customer-stats"><strong>${cr(c.balance_micros)} Credits</strong><span>${c.running}/${c.workers} Worker aktiv</span></div></div><div class="actions">${c.blocked?'<button class="unblock">FREIGEBEN</button>':'<button class="danger block">BENUTZER SPERREN</button>'}<button class="ghost stopAll" ${c.running?'' :'disabled'}>ALLE WORKER STOPPEN</button><button class="ghost limitBypass">${c.worker_limit_bypass?'LIMIT NORMAL':'LIMIT AUFHEBEN'}</button>${manual?'<input class="amount" type="number" min="0.001" step="1" placeholder="Credits"><button class="grantBtn">+ CREDIT</button>':''}</div><div class="admin-worker-create"><select class="newWorkerTask"><option value="">Task wählen …</option>${taskOptions()}</select><select class="newWorkerBeacon"><option value="auto">AUTO</option><option value="pulse">PULSE</option><option value="flux">FLUX</option><option value="orbit">ORBIT</option></select><button class="addWorker" ${taskCatalog.length?'':'disabled'}>WORKER +</button></div>${workerRows(c)}</article>`}
|
|
function bindCustomerActions(){document.querySelectorAll('.block').forEach(b=>b.onclick=async()=>{const row=b.closest('.customer');const reason=prompt('Optionaler Sperrgrund:','');if(reason===null)return;if(!confirm('Benutzer sperren, Sessions invalidieren und alle Worker stoppen?'))return;try{const x=await api(`/api/admin/customers/${encodeURIComponent(row.dataset.id)}/block`,{method:'POST',body:{reason}});msg(x.warnings?.length?`Benutzer gesperrt · Warnungen: ${x.warnings.join(' · ')}`:'Benutzer gesperrt',!!x.warnings?.length);await load()}catch(e){msg(e.message,true)}});document.querySelectorAll('.unblock').forEach(b=>b.onclick=async()=>{const row=b.closest('.customer');try{await api(`/api/admin/customers/${encodeURIComponent(row.dataset.id)}/unblock`,{method:'POST'});msg('Benutzer freigegeben');await load()}catch(e){msg(e.message,true)}});document.querySelectorAll('.stopAll').forEach(b=>b.onclick=async()=>{const row=b.closest('.customer');if(!confirm('Alle Worker dieses Benutzers stoppen?'))return;try{const x=await api(`/api/admin/customers/${encodeURIComponent(row.dataset.id)}/workers/stop`,{method:'POST'});msg(x.warnings?.length?x.warnings.join(' · '):'Worker gestoppt',!!x.warnings?.length);await load()}catch(e){msg(e.message,true)}});document.querySelectorAll('.stopWorker').forEach(b=>b.onclick=async()=>{try{await api(`/api/admin/workers/${encodeURIComponent(b.dataset.wid)}/stop`,{method:'POST'});msg('Worker gestoppt');await load()}catch(e){msg(e.message,true)}});document.querySelectorAll('.updateWorkerImage').forEach(b=>b.onclick=async()=>{if(!confirm(`Worker ${b.dataset.wid} mit dem aktuell konfigurierten Image neu erzeugen? Die Identity bleibt erhalten.`))return;const old=b.textContent;b.disabled=true;b.textContent='UPDATE …';try{const x=await api(`/api/admin/workers/${encodeURIComponent(b.dataset.wid)}/update-image`,{method:'POST'});msg(`Worker aktualisiert · ${x.image}${x.resumed?' · wieder gestartet':''}`);await load()}catch(e){msg(e.message,true)}finally{b.disabled=false;b.textContent=old}});document.querySelectorAll('.limitBypass').forEach(b=>b.onclick=async()=>{const row=b.closest('.customer'),c=last?.customers?.find(x=>x.id===row.dataset.id);try{await api(`/api/admin/customers/${encodeURIComponent(row.dataset.id)}/worker-limit`,{method:'PUT',body:{bypass:!c?.worker_limit_bypass}});msg(c?.worker_limit_bypass?'Worker-Limit wieder aktiv':'Worker-Limit für Benutzer aufgehoben');await load()}catch(e){msg(e.message,true)}});document.querySelectorAll('.addWorker').forEach(b=>b.onclick=async()=>{const row=b.closest('.customer'),task=row.querySelector('.newWorkerTask').value,beacon=row.querySelector('.newWorkerBeacon').value;if(!task){msg('Task auswählen',true);return}try{await api(`/api/admin/customers/${encodeURIComponent(row.dataset.id)}/workers`,{method:'POST',body:{task_id:task,beacon_path:beacon}});msg('Worker dem Benutzer zugeordnet');await load()}catch(e){msg(e.message,true)}});document.querySelectorAll('.grantBtn').forEach(b=>b.onclick=async()=>{const row=b.closest('.customer');const n=Number(row.querySelector('.amount').value);if(!n)return;try{await api('/api/admin/credits/grant',{method:'POST',body:{customer_id:row.dataset.id,credits:n,reason:'admin-ui'}});msg(`${n} Credits gebucht`);await load()}catch(e){msg(e.message,true)}})}
|
|
async function load(){const [x,tasks]=await Promise.all([api('/api/admin/overview'),api('/api/admin/tasks').catch(()=>[])]);last=x;taskCatalog=Array.isArray(tasks)?tasks:[];manual=x.manual_credits_enabled;$('loginBox').classList.add('hidden');$('panel').classList.remove('hidden');$('manual').textContent=manual?'MANUELLER CREDIT-BYPASS IST AKTIV. Jede Buchung bleibt im Ledger nachvollziehbar.':'Manuelle Credits sind deaktiviert (CS_ALLOW_MANUAL_CREDITS=0).';settingsToUI(x.settings||{});$('workerImage').textContent=x.worker_image||'—';$('orchestrationMode').textContent=(x.orchestration_mode||'direct').toUpperCase();$('controllers').innerHTML=(x.controllers||[]).map(controllerHTML).join('')||'<div class="small">Keine Service Controller registriert.</div>';$('customers').innerHTML=(x.customers||[]).map(customerHTML).join('')||'<div class="notice">Noch keine Benutzer.</div>';bindControllers();bindCustomerActions()}
|
|
$('saveSettings').onclick=saveSettings;$('saveRewards').onclick=saveSettings;$('createInvite').onclick=async()=>{try{const x=await api('/api/admin/invites',{method:'POST',body:{label:$('inviteLabel').value,hours:Number($('inviteHours').value||24)}});$('inviteOut').textContent=x.invite_code;$('inviteOut').classList.remove('hidden');msg('Invite erzeugt · jetzt sicher an genau einen Benutzer weitergeben');await load()}catch(e){msg(e.message,true)}};$('updateAllImages').onclick=async()=>{const total=(last?.customers||[]).reduce((n,c)=>n+Number(c.workers||0),0);if(!confirm(`CS_WORKER_IMAGE neu pullen und ${total} bestehende Worker aktualisieren? Laufende Worker werden kurz neu gestartet.`))return;const b=$('updateAllImages'),old=b.textContent;b.disabled=true;b.textContent='IMAGE UPDATE LÄUFT …';try{const x=await api('/api/admin/workers/update-image',{method:'POST'});const warn=x.warnings?.length?` · ${x.warnings.length} Fehler`:'';msg(`${x.updated}/${x.total} Worker aktualisiert · ${x.image}${warn}`,!!x.warnings?.length);await load()}catch(e){msg(e.message,true)}finally{b.disabled=false;b.textContent=old}};$('refresh').onclick=()=>load().catch(e=>msg(e.message,true));$('login').onclick=async()=>{try{await api('/api/admin/login',{method:'POST',body:{Username:$('user').value,Password:$('pass').value}});await load()}catch(e){msg(e.message,true)}};$('logout').onclick=async()=>{await api('/api/admin/logout',{method:'POST'}).catch(()=>{});location.reload()};load().catch(()=>{});
|