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'
Keine Worker.
';return `${c.worker_items.map(w=>`
${esc(w.id)} ${esc(w.worker_client_id||'noch keine Identity')} · Task ${esc(w.task_id)} · Host ${esc(w.controller_id||'local')}
${w.last_error?`
${esc(w.last_error)}
`:''}
${esc(w.status)} IMAGE UPDATE STOP `).join('')}
`}
function controllerHTML(c){const state=c.online?'ONLINE ':'OFFLINE ',drain=c.draining?'DRAIN ':'',disabled=!c.enabled?'DISABLED ':'';return `${esc(c.name||c.id)} ${esc(c.id)} · protocol v${Number(c.protocol_version||0)} ${state} ${drain} ${disabled}
${Number(c.assigned_running||0)}/${Number(c.max_running||0)} running ${Number(c.assigned_workers||0)}/${Number(c.max_workers||0)} assigned
${esc(c.base_url||'—')}
Heartbeat: ${c.last_seen?new Date(c.last_seen).toLocaleString('de-DE'):'—'} · reported ${Number(c.reported_running||0)}/${Number(c.reported_workers||0)}
${c.draining?'DRAIN AUFHEBEN':'DRAIN'} ${c.enabled?'DEAKTIVIEREN':'AKTIVIEREN'}
`}
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=>`${esc(t.display_name||t.id.slice(-12))} · ${Number(t.range_bits||0)} Bit `).join('')}
function customerHTML(c){const status=c.blocked?`GESPERRT `:'AKTIV ',bypass=c.worker_limit_bypass?'LIMIT BYPASS ':'';return `${esc(c.username)} ${status} ${bypass} ${esc(c.id)} · Reward ${esc(c.reward_client_id||'—')}
${c.blocked_reason?`
Grund: ${esc(c.blocked_reason)}
`:''}
${cr(c.balance_micros)} Credits ${c.running}/${c.workers} Worker aktiv
${c.blocked?'FREIGEBEN ':'BENUTZER SPERREN '}ALLE WORKER STOPPEN ${c.worker_limit_bypass?'LIMIT NORMAL':'LIMIT AUFHEBEN'} ${manual?'+ CREDIT ':''}
Task wählen … ${taskOptions()}AUTO PULSE FLUX ORBIT WORKER +
${workerRows(c)} `}
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('')||'Keine Service Controller registriert.
';$('customers').innerHTML=(x.customers||[]).map(customerHTML).join('')||'Noch keine Benutzer.
';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(()=>{});