'use strict'; const app = document.getElementById('app'); const $ = id => document.getElementById(id); const esc = (s='') => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const clamp = (v,a,b) => Math.max(a, Math.min(b, v)); const lerp = (a,b,t) => a + (b-a)*t; const smooth = t => { t=clamp(t,0,1); return t*t*(3-2*t); }; const tokenKey = 'neuralhunt.token'; const mobileModeKey = 'neuralhunt.mobileMode'; const getToken = () => localStorage.getItem(tokenKey) || ''; function autoMobileMode(){return matchMedia('(max-width: 850px)').matches || matchMedia('(pointer: coarse)').matches} function mobileModeEnabled(){const v=localStorage.getItem(mobileModeKey);return v===null?autoMobileMode():v==='1'} function applyMobileMode(on,persist=false){document.documentElement.classList.toggle('mobile-mode',!!on);if(persist)localStorage.setItem(mobileModeKey,on?'1':'0');window.dispatchEvent(new CustomEvent('neuralhunt-mobile-mode',{detail:{enabled:!!on}}))} function toggleMobileMode(){applyMobileMode(!document.documentElement.classList.contains('mobile-mode'),true)} function mobileButtonLabel(){return document.documentElement.classList.contains('mobile-mode')?'MOBILE ON':'MOBILE OFF'} const setToken = t => localStorage.setItem(tokenKey, t); const clearToken = () => localStorage.removeItem(tokenKey); async function api(path, init={}, admin=false) { const token = admin ? '' : getToken(); const headers = new Headers(init.headers || {}); if (!(init.body instanceof FormData)) headers.set('Content-Type','application/json'); if (token) headers.set('Authorization','Bearer '+token); const r = await fetch(path,{...init,headers,credentials:'same-origin'}); if (!r.ok) { let msg = `HTTP ${r.status}`, payload = null; try { payload=await r.json(); msg=payload?.error || msg; } catch {} const err = new Error(msg); err.status = r.status; err.code = payload?.code || ''; err.data = payload || null; throw err; } return r.json(); } async function loadProtectedImage(path,img,admin=false){ if(!img)return;const token=admin?'':getToken();const headers={};if(token)headers.Authorization='Bearer '+token; const r=await fetch(path,{headers,credentials:'same-origin'});if(!r.ok)throw new Error(`Bild HTTP ${r.status}`);const blob=await r.blob();const old=img.dataset.objectUrl;if(old)URL.revokeObjectURL(old);const u=URL.createObjectURL(blob);img.dataset.objectUrl=u;img.src=u; } // Browser-persistent cryptographic identity. The private key never leaves the // browser unencrypted. Export/import is password-protected AES-GCM. const identityKey='neuralhunt.identity.v1'; const b64u=b=>{const a=b instanceof Uint8Array?b:new Uint8Array(b);let s='';a.forEach(x=>s+=String.fromCharCode(x));return btoa(s).replaceAll('+','-').replaceAll('/','_').replaceAll('=','')}; const ub64=s=>{s=s.replaceAll('-','+').replaceAll('_','/');while(s.length%4)s+='=';const x=atob(s);return Uint8Array.from(x,c=>c.charCodeAt(0))}; function requireWebCrypto(){ const c=globalThis.crypto; if(c?.subtle)return c.subtle; const host=location.hostname; const local=host==='localhost'||host==='127.0.0.1'||host==='::1'||host==='[::1]'; if(location.protocol!=='https:'&&!local){ throw new Error(`Sichere Verbindung erforderlich: Neural Hunt verwendet Browser-WebCrypto für deine lokale Identität. Öffne ${location.host} über HTTPS statt HTTP.`); } throw new Error('WebCrypto ist in diesem Browser nicht verfügbar. Bitte verwende einen aktuellen Browser mit aktivierter WebCrypto-Unterstützung.'); } async function clientId(pub){const subtle=requireWebCrypto();const s=`${pub.kty}|${pub.crv}|${pub.x}|${pub.y}`;return b64u(await subtle.digest('SHA-256',new TextEncoder().encode(s)))} async function validateIdentityBundle(b){ const subtle=requireWebCrypto(); if(!b||Number(b.version)!==1||b.publicJwk?.kty!=='EC'||b.publicJwk?.crv!=='P-256'||b.privateJwk?.kty!=='EC'||b.privateJwk?.crv!=='P-256'||!b.privateJwk?.d)throw new Error('Ungültige Neural-Hunt-Identität'); const pub=await subtle.importKey('jwk',b.publicJwk,{name:'ECDSA',namedCurve:'P-256'},false,['verify']); const priv=await subtle.importKey('jwk',b.privateJwk,{name:'ECDSA',namedCurve:'P-256'},false,['sign']); const probe=crypto.getRandomValues(new Uint8Array(32)),sig=await subtle.sign({name:'ECDSA',hash:'SHA-256'},priv,probe); if(!await subtle.verify({name:'ECDSA',hash:'SHA-256'},pub,sig,probe))throw new Error('Public/Private Key der Identität passen nicht zusammen'); return clientId(b.publicJwk); } async function ensureIdentity(){const raw=localStorage.getItem(identityKey);if(raw){requireWebCrypto();const b=JSON.parse(raw);await validateIdentityBundle(b);return b}const subtle=requireWebCrypto();const kp=await subtle.generateKey({name:'ECDSA',namedCurve:'P-256'},true,['sign','verify']);const b={version:1,publicJwk:await subtle.exportKey('jwk',kp.publicKey),privateJwk:await subtle.exportKey('jwk',kp.privateKey)};await validateIdentityBundle(b);localStorage.setItem(identityKey,JSON.stringify(b));return b} async function sign(message){const subtle=requireWebCrypto();const b=await ensureIdentity();const k=await subtle.importKey('jwk',b.privateJwk,{name:'ECDSA',namedCurve:'P-256'},false,['sign']);return b64u(await subtle.sign({name:'ECDSA',hash:'SHA-256'},k,new TextEncoder().encode(message)))} async function responseError(r,fallback){try{const b=await r.json();return b?.error?`${fallback}: ${b.error}`:`${fallback} (HTTP ${r.status})`}catch{return `${fallback} (HTTP ${r.status})`}} function zeroBits(bytes){let n=0;for(const x of bytes){if(x===0){n+=8;continue}for(let m=0x80;m&&!(x&m);m>>=1)n++;break}return n} async function solveIdentityProof(challenge,cid,bits){bits=Number(bits||0);if(bits<=0)return '';const subtle=requireWebCrypto(),enc=new TextEncoder(),prefix=`nh-pow-v1|${challenge}|${cid}|`;let counter=0;const batch=96;while(true){const nums=Array.from({length:batch},(_,i)=>counter+i),hashes=await Promise.all(nums.map(n=>subtle.digest('SHA-256',enc.encode(prefix+n))));for(let i=0;i=bits)return String(nums[i]);counter+=batch;if(counter%3072===0)await new Promise(r=>setTimeout(r,0))}} async function loginIdentity(){const b=await ensureIdentity();const cr=await fetch('/api/auth/challenge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({public_jwk:b.publicJwk})});if(!cr.ok)throw new Error(await responseError(cr,'Challenge fehlgeschlagen'));const c=await cr.json(),bits=Number(c.proof_of_work_bits||0);if(bits>0&&$('status'))$('status').textContent=`Neue Identität wird geprüft · ${bits}-Bit Proof-of-Work …`;const proof_of_work_counter=await solveIdentityProof(c.challenge,c.client_id,bits),signature=await sign(`login|${c.challenge}|${c.client_id}`);const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({public_jwk:b.publicJwk,challenge:c.challenge,signature,proof_of_work_counter})});if(!r.ok)throw new Error(await responseError(r,'Login fehlgeschlagen'));return r.json()} async function deterministicGuess(taskID,seed,cid,seq,bits){const subtle=requireWebCrypto();const h=new Uint8Array(await subtle.digest('SHA-256',new TextEncoder().encode(`${taskID}|${seed}|${cid}|${seq}`)));let n=0n;for(const x of h)n=(n<<8n)|BigInt(x);return (n%(1n<2000000)throw new Error('Nicht unterstützte KDF-Konfiguration.');const base=await subtle.importKey('raw',new TextEncoder().encode(passphrase),'PBKDF2',false,['deriveKey']);const aes=await subtle.deriveKey({name:'PBKDF2',salt:ub64(x.salt),iterations,hash:'SHA-256'},base,{name:'AES-GCM',length:256},false,['decrypt']);let pt;try{pt=await subtle.decrypt({name:'AES-GCM',iv:ub64(x.iv)},aes,ub64(x.ciphertext))}catch{throw new Error('Identität konnte nicht entschlüsselt werden: falsche Passphrase oder beschädigter Export.')}const b=JSON.parse(new TextDecoder().decode(pt)),cid=await validateIdentityBundle(b);if(x.clientId&&x.clientId!==cid)throw new Error('Client-ID im Export stimmt nicht mit dem Schlüssel überein.');return {bundle:b,clientId:cid}} function hashInt(s){let h=2166136261>>>0;s=String(s||'');for(let i=0;i>>0} function pseudo(s,o=0){return ((Math.sin((hashInt(`${s}:${o}`)+1)*0.00000137+o*12.345)*43758.5453123)%1+1)%1} function scoreRGB(score){ const t=clamp(Number(score||0)/100,0,1); // low = deep blue, middle = cyan, high = mint/amber, near-perfect = magenta-white const stops=[[0,[75,123,255]],[.45,[82,231,255]],[.78,[93,255,189]],[.94,[255,180,82]],[1,[255,111,188]]]; for(let i=1;iMath.round(lerp(v,cb[j],q)))} return stops.at(-1)[1]; } function rgba(rgb,a){return `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${a})`} function makeLOD(points,maxNodes,selfId,enabled=true){ const source=(points||[]).map(p=>({...p,_count:1,_group:false,_bestScore:Number(p.score||0)})); if(source.length<=maxNodes) return source; const self=source.find(p=>p.client_id===selfId); if(!enabled){ const rest=self?source.filter(p=>p!==self):source; rest.sort((a,b)=>(Number(b.score||0)-Number(a.score||0))||(Number(a.rank||1e12)-Number(b.rank||1e12))||a.client_id.localeCompare(b.client_id)); const keep=rest.slice(0,Math.max(1,maxNodes-(self?1:0))); return self?[...keep,self]:keep; } const others=self?source.filter(p=>p!==self):source; const target=Math.max(8,maxNodes-(self?1:0)); if(others.length<=target) return self?[...others,self]:others; let minX=Infinity,maxX=-Infinity,minY=Infinity,maxY=-Infinity,minZ=Infinity,maxZ=-Infinity; for(const p of others){minX=Math.min(minX,p.x);maxX=Math.max(maxX,p.x);minY=Math.min(minY,p.y);maxY=Math.max(maxY,p.y);minZ=Math.min(minZ,p.z);maxZ=Math.max(maxZ,p.z)} const span=Math.max(1,maxX-minX,maxY-minY,maxZ-minZ); let cell=span/Math.max(2,Math.cbrt(target)*.82), grouped=[]; for(let attempt=0;attempt<12;attempt++){ const buckets=new Map(); for(const p of others){ const ix=Math.floor((p.x-minX)/cell),iy=Math.floor((p.y-minY)/cell),iz=Math.floor((p.z-minZ)/cell),key=`${ix}:${iy}:${iz}`; let b=buckets.get(key); if(!b){b={client_id:`cluster:${key}`,x:0,y:0,z:0,score:0,rank:Number.MAX_SAFE_INTEGER,guess_count:0,_count:0,_group:true,_bestScore:0,_scoreSum:0};buckets.set(key,b)} const weight=1+Math.sqrt(Math.max(0,Number(p.score||0)))/12; b.x+=Number(p.x)*weight;b.y+=Number(p.y)*weight;b.z+=Number(p.z)*weight;b._mass=(b._mass||0)+weight; b._count++;b._scoreSum+=Number(p.score||0);b._bestScore=Math.max(b._bestScore,Number(p.score||0));b.rank=Math.min(b.rank,Number(p.rank||Number.MAX_SAFE_INTEGER));b.guess_count+=Number(p.guess_count||0); } grouped=[...buckets.values()].map(b=>({...b,x:b.x/b._mass,y:b.y/b._mass,z:b.z/b._mass,score:b._scoreSum/b._count})); if(grouped.length<=target) break; cell*=1.24; } if(grouped.length>target){ grouped.sort((a,b)=>(b._bestScore-a._bestScore)||(b._count-a._count)||a.client_id.localeCompare(b.client_id)); grouped=grouped.slice(0,target); } return self?[...grouped,self]:grouped; } class NeuralMap { constructor(host, opts={}) { this.host=host; this.opts={panelOffset:0,admin:false,...opts}; this.canvas=document.createElement('canvas'); this.canvas.className='brain-canvas'; host.appendChild(this.canvas); this.ctx=this.canvas.getContext('2d',{alpha:false}); this.tooltip=document.createElement('div'); this.tooltip.className='tooltip hidden'; host.appendChild(this.tooltip); this.points=[]; this.rawPoints=[]; this.selfId=''; this.maxNodes=2000; this.lodEnabled=true; // TARGET FIELD is the default: score controls orbital radius exactly. RAW 3D // remains available for inspecting the server-provided x/y/z coordinates. this.yaw=.18; this.pitch=-.58; this.zoom=1.02; this.autoRotate=true; this.labels=true; this.edges=true; this.shells=true; this.eco=false; this.proximityFocus=true; this.drag=false; this.moved=false; this.lastX=0; this.lastY=0; this.mouseX=0; this.mouseY=0; this.hover=null; this.width=1; this.height=1; this.dpr=1; this.last=performance.now(); this.lastPaint=0; this.fps=0; this.fpsFrames=0; this.fpsStart=performance.now(); this.renderCount=0; this.background=document.createElement('canvas'); this.backgroundKey=''; this.motion=new Map(); this.scoreMotion=new Map(); this.particles=[]; this.guessFlashes=[]; this.guessFlashEnabled=false; this.rawBy=new Map(); this.projected=[]; this.statsAt=0; this.ro=new ResizeObserver(()=>this.resize()); this.ro.observe(host); this.resize(); this.bind(); this.frame=requestAnimationFrame(t=>this.draw(t)); } bind(){ this.canvas.addEventListener('pointerdown',e=>{this.drag=true;this.moved=false;this.lastX=e.clientX;this.lastY=e.clientY;this.canvas.setPointerCapture(e.pointerId)}); this.canvas.addEventListener('pointermove',e=>{const r=this.canvas.getBoundingClientRect();this.mouseX=e.clientX-r.left;this.mouseY=e.clientY-r.top;if(!this.drag){this.pick();return}const dx=e.clientX-this.lastX,dy=e.clientY-this.lastY;if(Math.abs(dx)+Math.abs(dy)>2)this.moved=true;this.yaw+=dx*.006;this.pitch=clamp(this.pitch+dy*.004,-1.02,-.12);this.lastX=e.clientX;this.lastY=e.clientY}); this.canvas.addEventListener('pointerup',()=>{this.drag=false;this.pick()}); this.canvas.addEventListener('pointercancel',()=>this.drag=false); this.canvas.addEventListener('pointerleave',()=>{if(!this.drag){this.hover=null;this.tooltip.classList.add('hidden')}}); this.canvas.addEventListener('wheel',e=>{e.preventDefault();this.zoom=clamp(this.zoom*Math.exp(-e.deltaY*.001),.55,2.4)},{passive:false}); this.canvas.addEventListener('dblclick',()=>this.resetView()); } resize(){const r=this.host.getBoundingClientRect();this.width=Math.max(1,r.width);this.height=Math.max(1,r.height);this.dpr=Math.min(devicePixelRatio||1,this.eco?1.15:2);this.canvas.width=Math.max(1,Math.floor(this.width*this.dpr));this.canvas.height=Math.max(1,Math.floor(this.height*this.dpr));this.canvas.style.width=this.width+'px';this.canvas.style.height=this.height+'px';this.backgroundKey=''} setOptions(opts={}){Object.assign(this,opts);if('eco' in opts)this.resize();if('lodEnabled' in opts||'maxNodes' in opts)this.rebuild()} resetView(){this.yaw=.18;this.pitch=-.58;this.zoom=1.02} fieldRadius(score){ // Score itself is logarithmic. This perceptual expansion deliberately gives // the 90..100 region much more room so 95, 98 and 99+ are visibly distinct. const miss=clamp(1-Number(score||0)/100,0,1); return .045+.955*Math.pow(miss,.38); } fieldGeometry(){ const panel=this.width>1050?this.opts.panelOffset:0; const scale=Math.min(this.width*(this.opts.admin?.36:.34),this.height*.42)*this.zoom; // TARGET FIELD stays face-on so screen distance is a mathematically exact // proximity cue. Pitch changes only depth/perspective intensity. const depth=clamp(.07+(-this.pitch-.12)*.10,.07,.16); return {cx:this.width/2+panel,cy:this.height/2+8,scale,depth}; } currentMotion(id,now=performance.now()){const m=this.motion.get(id);if(!m)return null;const q=smooth((now-m.start)/m.duration);return {x:lerp(m.fx,m.tx,q),y:lerp(m.fy,m.ty,q),z:lerp(m.fz,m.tz,q)}} currentScore(p,now=performance.now()){ const m=this.scoreMotion.get(p.client_id); if(!m)return Number(p._group?(p._bestScore||p.score):p.score||0); const q=clamp((now-m.start)/m.duration,0,1); if(q>=1){this.scoreMotion.delete(p.client_id);return Number(m.to)} return lerp(m.from,m.to,smooth(q)); } update(points,selfId,maxNodes){ const oldRaw=new Map(this.rawPoints.map(p=>[p.client_id,p])),now=performance.now(); this.rawPoints=Array.isArray(points)?points:[]; this.rawBy=new Map(this.rawPoints.map(p=>[p.client_id,p])); this.selfId=selfId||''; this.maxNodes=Math.max(10,Number(maxNodes||this.maxNodes)); for(const p of this.rawPoints){const prev=oldRaw.get(p.client_id);if(prev&&Number(p.score||0)>Number(prev.score||0)+.0001){this.scoreMotion.set(p.client_id,{from:Number(prev.score||0),to:Number(p.score||0),start:now,duration:900});this.emitSignal(p)}} this.rebuild(); } rebuild(){ const now=performance.now(),next=makeLOD(this.rawPoints,this.maxNodes,this.selfId,this.lodEnabled),oldBy=new Map(this.points.map(p=>[p.client_id,p])); for(const p of next){const prior=this.currentMotion(p.client_id,now)||oldBy.get(p.client_id)||p;this.motion.set(p.client_id,{fx:Number(prior.x||0),fy:Number(prior.y||0),fz:Number(prior.z||0),tx:Number(p.x||0),ty:Number(p.y||0),tz:Number(p.z||0),start:now,duration:650})} this.points=next; } emitSignal(p){ if(this.particles.length>180)this.particles.splice(0,this.particles.length-120); const now=performance.now(),col=scoreRGB(p.score); for(let i=0;i<3;i++)this.particles.push({id:p.client_id,start:now+i*95,duration:720+i*90,color:col,size:1+clamp(Number(p.score||0)/100,0,1)*.55}); } flashGuess(data){ if(!this.guessFlashEnabled||!data)return; const id=String(data.client_id||''),score=Number(data.score); if(!id||!Number.isFinite(score))return; if(this.guessFlashes.length>140)this.guessFlashes.splice(0,this.guessFlashes.length-100); this.guessFlashes.push({id,score,best:Number(data.best_score||0),improved:!!data.improved,correct:!!data.correct,start:performance.now(),duration:data.correct?1800:1250}); } rawCamera(){const panel=this.width>1050?this.opts.panelOffset:0;return {cy:Math.cos(this.yaw),sy:Math.sin(this.yaw),cp:Math.cos(this.pitch),sp:Math.sin(this.pitch),cx:this.width/2+panel,cyy:this.height/2,scale:Math.min(this.width*(this.opts.admin?.44:.40),this.height*.48)*this.zoom}} projectRawXYZ(x,y,z){const cam=this.rawCamera();const x1=x*cam.cy-z*cam.sy,z1=x*cam.sy+z*cam.cy,y1=y*cam.cp-z1*cam.sp,z2=y*cam.sp+z1*cam.cp;const perspective=2.9/(3.3-z2*.042);return {x:cam.cx+x1*cam.scale*perspective/13.5,y:cam.cyy-y1*cam.scale*perspective/13.5,z:z2,p:perspective}} projectXYZ(x,y,z){return this.projectRawXYZ(x,y,z)} projectFieldPoint(p,now){ const g=this.fieldGeometry(),score=this.currentScore(p,now),r=this.fieldRadius(score); const id=p.client_id||'node',a=pseudo(id,41)*Math.PI*2+this.yaw; const side=Math.cos(a)*r,vertical=Math.sin(a)*r; // Screen radius is exactly r*scale. The third dimension is encoded only as // perspective/brightness, never as a positional offset that could invert // who appears closer to the task. const z=(Math.sin(a)*.78+(pseudo(id,42)-.5)*.22)*r; const x=g.cx+side*g.scale; const y=g.cy+vertical*g.scale; const perspective=clamp(1+z*g.depth,.90,1.10); return {x,y,z,p:perspective,score,fieldR:r,angle:a}; } projectPoint(p,now){ if(this.proximityFocus)return this.projectFieldPoint(p,now); const m=this.currentMotion(p.client_id,now)||p;return this.projectRawXYZ(Number(m.x||0),Number(m.y||0),Number(m.z||0)); } buildBackground(){const key=`${Math.floor(this.width)}:${Math.floor(this.height)}:${this.eco?1:0}`;if(key===this.backgroundKey)return;this.backgroundKey=key;this.background.width=Math.ceil(this.width);this.background.height=Math.ceil(this.height);const c=this.background.getContext('2d',{alpha:false}),g=c.createRadialGradient(this.width*.5,this.height*.48,30,this.width*.5,this.height*.48,Math.max(this.width,this.height)*.78);g.addColorStop(0,'#071827');g.addColorStop(.5,'#020711');g.addColorStop(1,'#010207');c.fillStyle=g;c.fillRect(0,0,this.width,this.height);c.globalAlpha=this.eco?.08:.14;const stars=this.eco?28:90;for(let i=0;i=90; // Back half is dim/dashed; front half is brighter. This depth cue makes // the target plane read as 3D while ring radius stays a precise score cue. c.setLineDash(major?[3,6]:[2,9]);c.strokeStyle=rgba(col,major?.14:.06);c.lineWidth=major?.9:.55;c.beginPath();c.arc(g.cx,g.cy,rx,Math.PI,Math.PI*2);c.stroke(); c.setLineDash([]);c.strokeStyle=rgba(col,major?.38:.14);c.lineWidth=major?1.05:.65;c.beginPath();c.arc(g.cx,g.cy,rx,0,Math.PI);c.stroke(); if(this.labels&&this.width>650){const lx=g.cx+rx+5,ly=g.cy;c.font=major?'800 9px Inter,system-ui':'700 8px Inter,system-ui';c.fillStyle=rgba(col,major?.82:.47);c.fillText(score===99?'99+':String(score),lx,ly+3)} } if(this.labels&&this.width>720){c.font='700 8px Inter,system-ui';c.fillStyle='rgba(134,171,190,.58)';c.fillText('AUSSEN = WEIT',g.cx-g.scale-2,g.cy+g.scale+19);c.fillText('INNEN = NAH AM TASK',g.cx+14,g.cy-18)} c.restore(); } drawRawShells(){if(!this.shells||this.proximityFocus)return;const bands=[0,25,50,75,90,95,99];const c=this.ctx;c.save();c.globalCompositeOperation='screen';for(const s of bands){const r=.34+13*Math.sqrt(clamp(1-s/100,0,1)),col=scoreRGB(s);this.drawRawRing3D(r,'xy',col,s>=95?.19:.06,s>=95?1:.6,[3,7])}c.restore()} drawAura(now){const c=this.ctx,g=this.proximityFocus?this.fieldGeometry():null,core=g?{x:g.cx,y:g.cy}:this.projectRawXYZ(0,0,0),pulse=.5+.5*Math.sin(now*.0032),radius=this.eco?52:74+pulse*9;c.save();c.globalCompositeOperation='screen';const grad=c.createRadialGradient(core.x,core.y,0,core.x,core.y,radius);grad.addColorStop(0,'rgba(82,231,255,.17)');grad.addColorStop(.25,'rgba(82,231,255,.045)');grad.addColorStop(1,'rgba(82,231,255,0)');c.fillStyle=grad;c.beginPath();c.arc(core.x,core.y,radius,0,Math.PI*2);c.fill();c.restore()} drawClouds(now){ if(this.eco||this.points.length<6||!this.proximityFocus)return; const c=this.ctx,g=this.fieldGeometry(),groups=[{lo:0,hi:50},{lo:50,hi:75},{lo:75,hi:90},{lo:90,hi:95},{lo:95,hi:101}]; c.save();c.globalCompositeOperation='screen'; for(const band of groups){const members=this.points.filter(p=>{const s=Number(p._group?(p._bestScore||p.score):p.score||0);return s>=band.lo&&s=90?.018:.008;c.strokeStyle=rgba(col,alpha*Math.min(4,1+Math.log10(members.length+1)));c.lineWidth=Math.max(8,g.scale*(band.hi-band.lo)/900);c.beginPath();c.arc(g.cx,g.cy,g.scale*r,0,Math.PI*2);c.stroke()}c.restore() } selectedPaths(projected){ const singles=projected.filter(x=>!x.p._group).slice().sort((a,b)=>Number((b.q.score??b.p.score)||0)-Number((a.q.score??a.p.score)||0)); const chosen=singles.filter(x=>Number((x.q.score??x.p.score)||0)>=75).slice(0,this.eco?4:9); const self=singles.find(x=>x.p.client_id===this.selfId);if(self&&!chosen.includes(self))chosen.push(self); return chosen; } pathControl(q,core,id){const dx=core.x-q.x,dy=core.y-q.y,len=Math.max(1,Math.hypot(dx,dy)),nx=-dy/len,ny=dx/len,bend=(pseudo(id,73)-.5)*Math.min(46,len*.18);return {x:(q.x+core.x)/2+nx*bend,y:(q.y+core.y)/2+ny*bend}} drawEdges(projected){ // "Signalwege" are intentionally NOT client-to-client graph edges. There // is no semantic client relation in Neural Hunt. Only contenders -> task // paths are drawn, so every visible line has a real meaning. if(!this.edges||!projected.length)return; const c=this.ctx,core=this.proximityFocus?(()=>{const g=this.fieldGeometry();return{x:g.cx,y:g.cy}})():this.projectRawXYZ(0,0,0),chosen=this.selectedPaths(projected); c.save();c.globalCompositeOperation='screen'; chosen.forEach((x,i)=>{const score=Number((x.q.score??x.p.score)||0),self=x.p.client_id===this.selfId,col=self?[255,255,255]:scoreRGB(score),ctrl=this.pathControl(x.q,core,x.p.client_id),alpha=self?.36:i<3?.20:.07+.08*clamp((score-75)/25,0,1);c.strokeStyle=rgba(col,alpha);c.lineWidth=self?1.35:i<3?.9:.55;c.setLineDash(score<90?[2,7]:[]);c.beginPath();c.moveTo(x.q.x,x.q.y);c.quadraticCurveTo(ctrl.x,ctrl.y,core.x,core.y);c.stroke()}); c.setLineDash([]);c.restore(); } drawParticles(now){ if(!this.particles.length)return;const c=this.ctx,by=new Map(this.projected.map(x=>[x.p.client_id,x])),core=this.proximityFocus?(()=>{const g=this.fieldGeometry();return{x:g.cx,y:g.cy}})():this.projectRawXYZ(0,0,0); c.save();c.globalCompositeOperation='lighter'; for(let i=this.particles.length-1;i>=0;i--){const x=this.particles[i],t=(now-x.start)/x.duration;if(t<0)continue;if(t>=1){this.particles.splice(i,1);continue}const px=by.get(x.id);if(!px)continue;const q=px.q,ctrl=this.pathControl(q,core,x.id),e=smooth(t),u=1-e,bx=u*u*q.x+2*u*e*ctrl.x+e*e*core.x,byy=u*u*q.y+2*u*e*ctrl.y+e*e*core.y,r=(this.eco?2.0:5.0)*x.size*(.8+Math.sin(t*Math.PI)*.35);if(this.eco){c.fillStyle=rgba(x.color,.68);c.beginPath();c.arc(bx,byy,Math.max(1,r),0,Math.PI*2);c.fill()}else{const grad=c.createRadialGradient(bx,byy,0,bx,byy,r);grad.addColorStop(0,'rgba(255,255,255,.98)');grad.addColorStop(.2,rgba(x.color,.85));grad.addColorStop(1,rgba(x.color,0));c.fillStyle=grad;c.beginPath();c.arc(bx,byy,r,0,Math.PI*2);c.fill()}} c.restore(); } drawGuessFlashes(now){ if(!this.guessFlashEnabled||!this.guessFlashes.length)return; const c=this.ctx,projectedBy=new Map(this.projected.filter(x=>!x.p._group).map(x=>[x.p.client_id,x.q])); c.save();c.globalCompositeOperation='source-over';c.textBaseline='middle';c.textAlign='left';c.font=this.eco?'800 9px Inter,system-ui':'800 11px Inter,system-ui'; for(let i=this.guessFlashes.length-1;i>=0;i--){const f=this.guessFlashes[i],t=(now-f.start)/f.duration;if(t>=1){this.guessFlashes.splice(i,1);continue}if(t<0)continue;let q=projectedBy.get(f.id);if(!q){const raw=this.rawBy.get(f.id);if(raw)q=this.projectPoint(raw,now)}if(!q)continue;const rise=10+24*smooth(t),alpha=Math.pow(1-t,.72),col=f.correct?[255,255,255]:scoreRGB(f.score),text=`${f.score.toFixed(2)}%`,w=c.measureText(text).width+14,h=this.eco?17:20,x=clamp(q.x+9,4,this.width-w-4),y=clamp(q.y-rise,4,this.height-h-4);c.fillStyle=`rgba(2,7,14,${(.84*alpha).toFixed(3)})`;c.fillRect(x,y,w,h);c.strokeStyle=rgba(col,(f.improved?.72:.38)*alpha);c.lineWidth=f.correct?1.2:.7;c.strokeRect(x,y,w,h);c.fillStyle=rgba(col,.98*alpha);c.fillText(text,x+7,y+h/2+.2)} c.restore(); } drawNodes(projected,now){ const c=this.ctx,sorted=[...projected].filter(x=>!x.p._group).sort((a,b)=>Number((b.q.score??b.p.score)||0)-Number((a.q.score??a.p.score)||0)),topIDs=new Set(sorted.slice(0,10).map(x=>x.p.client_id)); c.save();c.globalCompositeOperation='lighter'; for(const {p,q} of projected){const self=p.client_id===this.selfId,group=p._group||p._count>1,score=Number((q.score??(group?(p._bestScore||p.score):p.score))||0),col=self?[255,255,255]:scoreRGB(score),depth=this.proximityFocus?clamp(q.p,.88,1.12):clamp(.74+q.p*.18,.72,1.22),mass=group?Math.min(7,1+Math.log2((p._count||1)+1)*.82):0,near=clamp((score-75)/25,0,1),base=self?5.6:group?3.4+mass:1.15+clamp(score/75,0,1)*.55+near*2.25,breathe=.5+.5*Math.sin(now*.0016+hashInt(p.client_id)*.00001),r=Math.max(.9,base*depth*(.94+breathe*.08)),important=self||score>=90||group||topIDs.has(p.client_id); if(important&&!this.eco){const hr=r*(self?5.0:group?3.0:3.1+near*1.2),grad=c.createRadialGradient(q.x,q.y,0,q.x,q.y,hr);grad.addColorStop(0,rgba(col,self?.62:.20+near*.12));grad.addColorStop(.3,rgba(col,self?.15:.05+near*.06));grad.addColorStop(1,rgba(col,0));c.fillStyle=grad;c.beginPath();c.arc(q.x,q.y,hr,0,Math.PI*2);c.fill()} c.fillStyle=rgba(col,self?.99:group?.70:.10+.17*clamp(score/75,0,1)+near*.62);c.beginPath();c.arc(q.x,q.y,r,0,Math.PI*2);c.fill(); if(score>=95&&!group){c.strokeStyle=rgba(col,.35+near*.35);c.lineWidth=.7;c.beginPath();c.arc(q.x,q.y,r*(1.75+near*.55)+breathe,0,Math.PI*2);c.stroke()} if(group){c.strokeStyle=rgba(col,.40);c.lineWidth=.75;c.beginPath();c.arc(q.x,q.y,r*1.34+breathe,0,Math.PI*2);c.stroke();if(!this.eco&&p._count>=5&&r>4.2){c.save();c.globalCompositeOperation='source-over';c.font='700 8px Inter,system-ui';c.textAlign='center';c.textBaseline='middle';c.fillStyle='rgba(236,249,255,.88)';c.fillText(p._count>999?`${Math.round(p._count/100)/10}k`:String(p._count),q.x,q.y+.4);c.restore()}} if(self){c.strokeStyle='rgba(255,255,255,.96)';c.lineWidth=1.15;c.beginPath();c.arc(q.x,q.y,r*2.2+breathe*1.7,0,Math.PI*2);c.stroke()} } c.restore(); if(this.labels){c.save();c.globalCompositeOperation='source-over';c.font='10px Inter,system-ui';c.textBaseline='middle';let n=0,max=this.eco?8:24,occupied=[];for(const {p,q} of [...projected].sort((a,b)=>Number((b.q.score??b.p.score)||0)-Number((a.q.score??a.p.score)||0))){if(n>=max)break;const self=p.client_id===this.selfId,group=p._group||p._count>1,score=Number((q.score??p.score)||0),important=self||topIDs.has(p.client_id)||score>=95||this.hover?.p?.client_id===p.client_id;if(!important)continue;const text=self?`DU · ${score.toFixed(2)}`:group?`${p._count} Clients · best ${Number(p._bestScore||0).toFixed(1)}`:`#${p.rank||'—'} · ${score.toFixed(2)}`,w=c.measureText(text).width+14,x=clamp(q.x+10,4,this.width-w-4),y=clamp(q.y-9,4,this.height-22);if(!self&&occupied.some(b=>Math.abs(b.x-x)<(b.w+w)*.48&&Math.abs(b.y-y)<17))continue;occupied.push({x,y,w});c.fillStyle='rgba(2,7,14,.88)';c.fillRect(x,y,w,18);c.strokeStyle=rgba(self?[255,255,255]:scoreRGB(score),self?.55:.16);c.lineWidth=.5;c.strokeRect(x,y,w,18);c.fillStyle=rgba(self?[255,255,255]:scoreRGB(score),.98);c.fillText(text,x+7,y+9);n++}c.restore()} } drawCore(now){const c=this.ctx,g=this.proximityFocus?this.fieldGeometry():null,q=g?{x:g.cx,y:g.cy}:this.projectRawXYZ(0,0,0),pulse=.5+.5*Math.sin(now*.004);c.save();c.globalCompositeOperation='lighter';const radius=this.eco?30:48+pulse*7;if(!this.eco){const grad=c.createRadialGradient(q.x,q.y,0,q.x,q.y,radius);grad.addColorStop(0,'rgba(255,255,255,.99)');grad.addColorStop(.10,'rgba(99,243,255,.92)');grad.addColorStop(.44,'rgba(82,231,255,.13)');grad.addColorStop(1,'rgba(82,231,255,0)');c.fillStyle=grad;c.beginPath();c.arc(q.x,q.y,radius,0,Math.PI*2);c.fill()}c.fillStyle='#fff';c.beginPath();c.arc(q.x,q.y,5.7+pulse*.8,0,Math.PI*2);c.fill();c.strokeStyle=`rgba(82,231,255,${.54+pulse*.28})`;c.lineWidth=1;c.beginPath();c.arc(q.x,q.y,13+pulse*3,0,Math.PI*2);c.stroke();c.restore();if(this.labels&&this.width>610){c.save();c.font='800 10px Inter,system-ui';c.fillStyle='rgba(224,251,255,.92)';c.fillText('TASK · 100',q.x+20,q.y+3);c.restore()}} pick(){if(!this.projected.length)return;let best=null,bestD=20;for(const x of this.projected){const d=Math.hypot(x.q.x-this.mouseX,x.q.y-this.mouseY);if(d1,score=Number((best.q.score??p.score)||0),zone=score>=99?'99+ · unmittelbar am Task':score>=95?'95–99 · sehr nah':score>=90?'90–95 · nah':score>=75?'75–90 · gutes Feld':score>=50?'50–75 · mittlere Distanz':'<50 · weit';this.tooltip.innerHTML=group?`LOD-Gruppe · ${p._count} ClientsØ Score ${Number(p.score||0).toFixed(2)} · Best ${Number(p._bestScore||0).toFixed(2)}
${Number(p.guess_count||0).toLocaleString('de-DE')} Tipps
`:`${p.client_id===this.selfId?'DU':esc(String(p.client_id).slice(0,16))}Score ${score.toFixed(2)} · Rank #${p.rank||'—'}
${zone}
${Number(p.guess_count||0).toLocaleString('de-DE')} Tipps
`;this.tooltip.classList.remove('hidden');this.tooltip.style.left=clamp(this.mouseX+14,8,this.width-270)+'px';this.tooltip.style.top=clamp(this.mouseY+14,8,this.height-100)+'px'} draw(now){const minFrame=this.eco?33:0;if(minFrame&&this.lastPaint&&now-this.lastPaintthis.draw(t));return}this.lastPaint=now;const dt=Math.min(.075,(now-this.last)/1000);this.last=now;if(this.autoRotate&&!this.drag)this.yaw+=dt*.035;this.fpsFrames++;if(now-this.fpsStart>800){this.fps=Math.round(this.fpsFrames*1000/(now-this.fpsStart));this.fpsFrames=0;this.fpsStart=now} const c=this.ctx;c.setTransform(this.dpr,0,0,this.dpr,0,0);this.drawBackground(now);this.drawAura(now);this.drawTargetField();this.drawRawShells();this.drawClouds(now); const projected=[];for(const p of this.points)projected.push({p,q:this.projectPoint(p,now)});projected.sort((a,b)=>a.q.z-b.q.z);this.projected=projected;this.renderCount=projected.length;this.drawEdges(projected);this.drawParticles(now);this.drawNodes(projected,now);this.drawGuessFlashes(now);this.drawCore(now);if(this.hover)this.pick(); if(this.opts.onStats&&now>this.statsAt){this.statsAt=now+500;this.opts.onStats({fps:this.fps,render:this.renderCount,raw:this.rawPoints.length,groups:this.points.filter(p=>p._group).length})} this.frame=requestAnimationFrame(t=>this.draw(t)); } destroy(){cancelAnimationFrame(this.frame);this.ro.disconnect();this.canvas.remove();this.tooltip.remove()} } function markHTML(){return ''} function setActive(id,on){const el=$(id);if(el)el.classList.toggle('active',!!on)} function shortID(s,n=10){return String(s||'').slice(0,n)} function fmtDate(v){try{return new Date(v).toLocaleString('de-DE')}catch{return '—'}} function fmtScore(v){return Number(v||0).toFixed(2)} function actionLabel(a){return ({set_range_bits:'Zahlenraum ändern',set_intervals:'Intervalle setzen',clear_intervals:'Intervalle erben',pause:'Task pausieren',resume:'Task fortsetzen',reroll:'Ziel neu würfeln',close:'Task beenden',regenerate_artifact:'NFT-Neugenerierung anstoßen'})[a]||a} function proximityRows(points,selfId,limit=7){ const rows=(Array.isArray(points)?points:[]).filter(p=>!p._group).slice().sort((a,b)=>Number(b.score||0)-Number(a.score||0)).slice(0,limit); const self=(points||[]).find(p=>p.client_id===selfId);if(self&&!rows.some(x=>x.client_id===selfId))rows.push(self); return rows; } function userShell(){ app.className='hunt'; app.innerHTML=`
${markHTML()}
NEURAL HUNTSocial Probability Experiment · signed clients
LIVINGTask auswählen
RANK #—SCORE 0.00—
0 Clients0 Render0 FPSTask —initialisiert
TARGET FIELD0 · WEIT75909599+100 · TASK
MAX NODES2000
CHOOSE YOUR FIELD

Wähle deinen Task

Jeder Task ist ein eigener Wahrscheinlichkeitsraum. Du kannst jederzeit wechseln; deine Identität und bereits erreichte Bestwerte bleiben erhalten.

DEINE IDENTITÄTinitialisiere …
PLACE
Tasks werden geladen …
Ein Client kann immer nur mit einem Task aktiv verbunden sein.Neural Place → · Echtzeit-Leaderboard →
`; } async function runUser(){ userShell(); let task=null,points=[],cid='',scheduler=null,countdownTimer=null,ws=null,submitting=false,nextGuessAt=0,refreshing=false,landingBusy=false,landingTimer=null,wsReconnectTimer=null,wsBackoff=500,beaconPath=localStorage.getItem('neuralhunt.beaconPath')||'PULSE',lastKnownScore=-1,lastPlaceRefresh=0; const map=new NeuralMap($('map'),{panelOffset:-115,onStats:s=>{if($('rendercount'))$('rendercount').textContent=s.render.toLocaleString('de-DE');if($('fpscount'))$('fpscount').textContent=s.fps}}); let detailsOpen=false; const syncMobile=()=>{const on=document.documentElement.classList.contains('mobile-mode');$('toggleMobile').textContent=mobileButtonLabel();setActive('toggleMobile',on);$('signalPanel').classList.toggle('expanded',on&&detailsOpen);if(on){map.eco=true;map.labels=false;map.edges=false;map.zoom=Math.min(map.zoom,.96);setActive('toggleEco',true);setActive('toggleLabels',false);setActive('toggleEdges',false);if(+$('maxnodes').value>1000){$('maxnodes').value=1000;$('maxnodesvalue').textContent='1.000';map.update(points,cid,1000)}}else{map.eco=false;setActive('toggleEco',false)}map.resize()}; const status=(s,mode='living')=>{if($('status'))$('status').textContent=s;const m=$('visualMode');if(m){m.className=`mode-status ${mode}`;$('modeTitle').textContent=mode==='thinking'?'GUESS':mode==='researching'?'WIN':task?.paused?'PAUSED':'LIVING';$('modeDetail').textContent=s}}; const syncBeacon=()=>{const on=Number(task?.beacon_hunt_enabled||0)===1&&Number(task?.guess_lottery_max_accepted||0)>0,box=$('beaconChoice');if(!box)return;box.classList.toggle('hidden',!on);box.querySelectorAll('[data-beacon-path]').forEach(b=>setActive(b,b.dataset.beaconPath===beaconPath));if(on)$('beaconMeta').textContent=`Treffer = Gewicht ×${Number(task.beacon_bonus_weight||2)}`}; const renderRadar=()=>{const rows=proximityRows(points,cid);$('proximityRows').innerHTML=rows.map((p,i)=>`
${p.client_id===cid?'DU':`#${p.rank||i+1}`}
${fmtScore(p.score)}
`).join('')||'
Noch keine Signale
'}; const render=()=>{points=Array.isArray(points)?points:[];const budget=Math.min(10000,Math.max(300,(+$('maxnodes').value||task?.default_max_nodes||2000)*3));if(points.length>budget){const own=points.find(p=>p.client_id===cid),top=points.filter(p=>p.client_id!==cid).sort((a,b)=>Number(b.score||0)-Number(a.score||0)).slice(0,budget-(own?1:0));points=own?[...top,own]:top}if($('nodecount'))$('nodecount').textContent=points.length.toLocaleString('de-DE');if($('clientmetric'))$('clientmetric').textContent=points.length.toLocaleString('de-DE');map.update(points,cid,+$('maxnodes').value);renderRadar()}; const countdown=()=>{let text='—';if(task?.paused)text='PAUSE';else if(task&&nextGuessAt){const sec=Math.max(0,Math.ceil((nextGuessAt-Date.now())/1000));text=sec?`${sec}s`:'jetzt'}$('guessCountdown').textContent=text;if($('guessMobile'))$('guessMobile').textContent=text}; async function refreshMe(){try{const me=await api('/api/me'),rawScore=Number(me.score||0),rank='#'+(me.rank||'—'),score=fmtScore(rawScore);$('rank').textContent=rank;$('score').textContent=score;if($('rankMobile'))$('rankMobile').textContent=rank;if($('scoreMobile'))$('scoreMobile').textContent=score;$('wins').textContent=me.wins||0;$('unlocks').innerHTML=(me.unlocks||[]).map(x=>`${esc(x)}`).join('');const shouldRefreshPlace=lastKnownScore<0||rawScore>lastKnownScore+1e-9||Date.now()-lastPlaceRefresh>60000;if(shouldRefreshPlace){try{const pl=await api('/api/place/me');if($('placeBadge'))$('placeBadge').textContent=Number(pl?.wallet?.available_pixels||0).toLocaleString('de-DE');lastPlaceRefresh=Date.now()}catch{}}lastKnownScore=rawScore}catch{}} async function refreshLeaders(){try{const raw=await api('/api/leaderboard'),ls=Array.isArray(raw)?raw:[];$('leaders').innerHTML=ls.slice(0,12).map((l,i)=>`
${i+1}${esc(shortID(l.client_id,11))}${l.wins} Wins · live ${fmtScore(l.live_score)}${Number(l.best_score||0).toFixed(1)}
`).join('')||'
Noch keine Teilnehmer
'}catch{}} async function ensureSession(){if(!getToken()){const x=await loginIdentity();setToken(x.token);return}try{const me=await api('/api/me');if(me?.client_id!==cid)throw Object.assign(new Error('identity mismatch'),{status:401})}catch(e){if(e.status!==401)throw e;clearToken();const x=await loginIdentity();setToken(x.token)}} function stopTimers(){if(scheduler){clearInterval(scheduler);scheduler=null}if(countdownTimer){clearInterval(countdownTimer);countdownTimer=null}nextGuessAt=0;countdown()} function clearWSReconnect(){if(wsReconnectTimer){clearTimeout(wsReconnectTimer);wsReconnectTimer=null}} function wsReady(){return !!ws&&ws.readyState===WebSocket.OPEN} async function closeWS(){clearWSReconnect();if(!ws)return;const socket=ws;ws=null;socket._plannedClose=true;await new Promise(resolve=>{let done=false;const finish=()=>{if(done)return;done=true;resolve()};socket.addEventListener('close',finish,{once:true});try{socket.close(1000,'task switch')}catch{}setTimeout(finish,650)})} async function stopTaskSession(){stopTimers();clearWSReconnect();await closeWS();submitting=false} function taskCardName(t){return String(t.display_name||'').trim()||`Task ${String(t.id||'').slice(-8)}`} function renderTaskCards(items){const host=$('taskCards');items=Array.isArray(items)?items:[];host.innerHTML=items.length?items.map((t,i)=>`
${String(i+1).padStart(2,'0')}
${t.selected?'DEIN AKTUELLER TASK':'ACTIVE FIELD'}

${esc(taskCardName(t))}

${esc(shortID(t.id.slice(-16),16))}
${t.range_bits}BIT
Style-Referenz für ${esc(taskCardName(t))}${t.has_custom_style_reference?'TASK STYLE':'DEFAULT STYLE'}

${esc(t.description||'Ein aktiver Neural-Hunt-Zahlenraum. Bewege dein Signal mit jedem besseren Tipp näher an den Task-Kern.')}

CLIENTS${Number(t.point_count||0).toLocaleString('de-DE')}DEIN SCORE${fmtScore(t.own_score)}DEIN RANK${t.own_rank?`#${t.own_rank}`:'—'}STATUS${t.paused?'PAUSE':'LIVE'}
`).join(''):'
Keine aktiven TasksDer Server erzeugt gerade einen neuen Wahrscheinlichkeitsraum.
'; document.querySelectorAll('[data-task-choice]').forEach(b=>b.onclick=()=>enterTask(b.dataset.taskChoice)); } async function showLanding(){if(landingBusy)return;landingBusy=true;try{await stopTaskSession();task=null;points=[];render();$('taskid').textContent='—';$('taskLanding').classList.add('visible');status('Task auswählen');const items=await api('/api/tasks');renderTaskCards(items)}catch(e){status(e.message||'Tasks konnten nicht geladen werden');$('taskCards').innerHTML=`
Fehler beim Laden${esc(e.message||'Unbekannter Fehler')}
`}finally{landingBusy=false}} async function refreshTaskConfig(forcePoints=false){if(refreshing||!task)return;refreshing=true;try{const current=await api('/api/tasks/current');if(task&¤t.id!==task.id){setTimeout(()=>showLanding(),0);return}const changed=current.revision!==task.revision||current.public_seed!==task.public_seed||current.range_bits!==task.range_bits;task=current;$('taskid').textContent=task.display_name?shortID(task.display_name,16):shortID(task.id.slice(-12),12);if(changed||forcePoints){points=await api(`/api/tasks/${task.id}/points?limit=${Math.min(10000,Math.max(300,(+$('maxnodes').value||task.default_max_nodes||2000)*3))}`);points=Array.isArray(points)?points:[];render()}if(task.paused){status(`Task pausiert · ${task.range_bits} Bit`);nextGuessAt=0}else if(changed){status(`Task aktualisiert · ${task.range_bits} Bit`);nextGuessAt=Date.now()+Math.max(1,task.client_submit_interval_sec)*1000}}finally{refreshing=false}} function scheduleWSReconnect(){if(wsReconnectTimer||!task||$('taskLanding').classList.contains('visible'))return;const delay=Math.min(8000,wsBackoff)+Math.floor(Math.random()*250);wsReconnectTimer=setTimeout(()=>{wsReconnectTimer=null;openWS()},delay);wsBackoff=Math.min(8000,Math.max(750,wsBackoff*1.7))} async function recover409(e){ if(e.code==='task_inactive'||e.code==='selection_conflict'){await showLanding();return} try{const current=await api('/api/tasks/current');if(!task||current.id!==task.id){await showLanding();return}task=current;$('taskid').textContent=task.display_name?shortID(task.display_name,16):shortID(task.id.slice(-12),12)}catch{} if(e.code==='presence_required')openWS(true);else if(!wsReady())scheduleWSReconnect(); if(e.code==='task_config_changed')await refreshTaskConfig(true); nextGuessAt=Date.now()+1500;status(e.code==='presence_required'?'Live-Verbindung wird automatisch wiederhergestellt …':'Client wird automatisch synchronisiert …') } async function submit(){if(!task||submitting)return;if(!wsReady()){scheduleWSReconnect();nextGuessAt=Date.now()+1000;status('Live-Verbindung wird wiederhergestellt …');return}submitting=true;status('signiert Tipp …','thinking');try{const current=await api('/api/tasks/current');if(current.id!==task.id){await showLanding();return}task=current;syncBeacon();if(task.paused){status('Task pausiert');nextGuessAt=0;return}const seq=task.next_seq,guess=await deterministicGuess(task.id,task.public_seed,cid,seq,task.range_bits),beaconOn=Number(task.beacon_hunt_enabled||0)===1&&Number(task.guess_lottery_max_accepted||0)>0,msg=beaconOn?`guess|${task.id}|${seq}|${guess}|${beaconPath}`:`guess|${task.id}|${seq}|${guess}`,signature=await sign(msg);if(Number(task.guess_lottery_max_accepted||0)>0)status(beaconOn?`Beacon ${beaconPath} committed · wartet auf externen Draw`:`wartet auf Losziehung · max. ${Number(task.guess_lottery_max_accepted).toLocaleString('de-DE')} Tipps / ${Number(task.guess_lottery_window_sec||60)}s`,'thinking');const correct=await api(`/api/tasks/${task.id}/guess`,{method:'POST',body:JSON.stringify({seq,guess,signature,beacon_path:beaconOn?beaconPath:''})});nextGuessAt=Date.now()+Math.max(1,task.client_submit_interval_sec)*1000;if(beaconOn){try{const d=await api(`/api/public/beacon/${encodeURIComponent(task.id)}/latest`);$('beaconLast').textContent=`Dein Pfad ${beaconPath} · Boost ${d.boosted_path} · drand #${d.beacon_round}`;status(correct?'Treffer — Task gelöst!':`Draw ${d.boosted_path} · Tipp gezogen & geprüft`,correct?'researching':'living')}catch{status(correct?'Treffer — Task gelöst!':'Beacon-Tipp gezogen & geprüft',correct?'researching':'living')}}else status(correct?'Treffer — Task gelöst!':Number(task.guess_lottery_max_accepted||0)>0?'Tipp gezogen & geprüft':'Tipp akzeptiert',correct?'researching':'living');lastPlaceRefresh=0;await Promise.all([refreshMe(),refreshLeaders()]);if(correct)setTimeout(()=>showLanding(),1300)}catch(e){if(e.status===409){await recover409(e);return}nextGuessAt=Date.now()+Math.max(2,task?.client_submit_interval_sec||11)*1000;if(e.code==='identity_warmup'){const wait=Math.max(1,Number(e.data?.retry_after_sec||task?.client_submit_interval_sec||15));nextGuessAt=Date.now()+wait*1000;status(`Anti-Sybil-Wartezeit · ${wait}s`,'living');return}if(e.code==='lottery_not_selected'){if(e.data?.boosted_path&&$('beaconLast'))$('beaconLast').textContent=`Dein Pfad ${e.data.chosen_path} · Boost ${e.data.boosted_path} · Gewicht ×${e.data.weight||1} · drand #${e.data.beacon_round}`;status('Tipp diesmal nicht gezogen · nächstes Los folgt','living');lastPlaceRefresh=0;await refreshMe();return}if(e.code==='beacon_unavailable'){status('Randomness Beacon nicht erreichbar · kein Tipp ausgewertet','living');return}if(e.code==='lottery_full'){status('Losfenster voll · nächster Versuch folgt','living');return}status(e.message||'Tipp fehlgeschlagen')}finally{submitting=false}} function openWS(force=false){if(!task||$('taskLanding').classList.contains('visible'))return;clearWSReconnect();if(ws&&(ws.readyState===WebSocket.OPEN||ws.readyState===WebSocket.CONNECTING)){if(!force)return;const old=ws;old._plannedClose=true;try{old.close(1000,'reconnect')}catch{}}const proto=location.protocol==='https:'?'wss':'ws',mx=+$('maxnodes').value||task.default_max_nodes||2000,socket=new WebSocket(`${proto}://${location.host}/api/ws?max_nodes=${encodeURIComponent(mx)}`,['neuralhunt.v1',`nh-auth.${getToken()}`]);ws=socket;socket.onopen=()=>{if(ws!==socket)return;wsBackoff=500;status(task?.paused?'Task pausiert':'verbunden')};socket.onmessage=async ev=>{if(ws!==socket)return;const e=JSON.parse(ev.data);if(e.type==='snapshot'){points=Array.isArray(e.data)?e.data:[];render()}else if(e.type==='point'){const p=e.data,i=points.findIndex(x=>x.client_id===p.client_id);if(i<0)points.push(p);else points[i]=p;render()}else if(e.type==='points'){for(const p of (Array.isArray(e.data)?e.data:[])){const i=points.findIndex(x=>x.client_id===p.client_id);if(i<0)points.push(p);else points[i]=p}render()}else if(e.type==='task_changed'){await refreshTaskConfig(true);await Promise.all([refreshMe(),refreshLeaders()])}else if(e.type==='task_completed'){status('Task abgeschlossen — Folge-Task ist bereit','researching');setTimeout(()=>showLanding(),1300)}};socket.onclose=e=>{if(ws===socket)ws=null;if(!socket._plannedClose&&task&&!$('taskLanding').classList.contains('visible')){status('Live-Verbindung unterbrochen · verbinde automatisch neu …');scheduleWSReconnect()}};socket.onerror=()=>{if(!socket._plannedClose&&ws===socket)status('WebSocket-Fehler · Reconnect folgt automatisch')}} async function enterTask(taskID){if(landingBusy)return;landingBusy=true;try{await stopTaskSession();task=await api('/api/tasks/select',{method:'POST',body:JSON.stringify({task_id:taskID})});$('taskLanding').classList.remove('visible');syncBeacon();$('taskid').textContent=task.display_name?shortID(task.display_name,16):shortID(task.id.slice(-12),12);let max=+$('maxnodes').value||clamp(Number(task.default_max_nodes||2000),100,25000);if(document.documentElement.classList.contains('mobile-mode'))max=Math.min(max,1000);$('maxnodes').value=max;$('maxnodesvalue').textContent=max.toLocaleString('de-DE');points=await api(`/api/tasks/${task.id}/points?limit=${Math.min(10000,Math.max(300,max*3))}`);points=Array.isArray(points)?points:[];render();await Promise.all([refreshMe(),refreshLeaders()]);syncMobile();openWS();status(task.paused?'Task pausiert':`${task.range_bits} Bit · verbunden`);nextGuessAt=task.paused?0:Date.now()+Math.max(1,task.client_submit_interval_sec)*1000;scheduler=setInterval(()=>{if(task&&!task.paused&&nextGuessAt&&Date.now()>=nextGuessAt)submit()},300);countdownTimer=setInterval(countdown,250);countdown()}catch(e){status(e.message||'Task konnte nicht gestartet werden');$('taskLanding').classList.add('visible')}finally{landingBusy=false}} try{const id=await ensureIdentity();cid=await clientId(id.publicJwk);$('cid').textContent=cid;$('landingCid').textContent=cid;await ensureSession();syncMobile();await Promise.all([refreshLeaders()]);await showLanding()}catch(e){status(e.message||'Startfehler');$('taskCards').innerHTML=`
Startfehler${esc(e.message||'')}
`} $('landingRefresh').onclick=()=>showLanding();$('chooseTask').onclick=()=>showLanding(); document.querySelectorAll('[data-beacon-path]').forEach(b=>b.onclick=()=>{beaconPath=b.dataset.beaconPath;localStorage.setItem('neuralhunt.beaconPath',beaconPath);syncBeacon();status(`Beacon-Pfad ${beaconPath} gewählt`) }); $('maxnodes').addEventListener('input',e=>{$('maxnodesvalue').textContent=Number(e.target.value).toLocaleString('de-DE');render()}); $('toggleMobile').onclick=()=>toggleMobileMode();$('toggleDetails').onclick=()=>{detailsOpen=!detailsOpen;$('signalPanel').classList.toggle('expanded',detailsOpen);setActive('toggleDetails',detailsOpen)};window.addEventListener('neuralhunt-mobile-mode',syncMobile); $('toggleProximity').onclick=()=>{map.proximityFocus=!map.proximityFocus;$('toggleProximity').textContent=map.proximityFocus?'TARGET FIELD':'RAW 3D';setActive('toggleProximity',map.proximityFocus)};$('toggleRotate').onclick=()=>{map.autoRotate=!map.autoRotate;setActive('toggleRotate',map.autoRotate)};$('toggleLabels').onclick=()=>{map.labels=!map.labels;setActive('toggleLabels',map.labels)};$('toggleEdges').onclick=()=>{map.edges=!map.edges;setActive('toggleEdges',map.edges)};$('toggleShells').onclick=()=>{map.shells=!map.shells;setActive('toggleShells',map.shells)};$('toggleLOD').onclick=()=>{map.lodEnabled=!map.lodEnabled;map.rebuild();setActive('toggleLOD',map.lodEnabled)};$('toggleEco').onclick=()=>{map.eco=!map.eco;map.resize();setActive('toggleEco',map.eco)};$('resetView').onclick=()=>map.resetView(); async function downloadMyNFT(n){const r=await fetch(n.download_uri,{headers:{Authorization:'Bearer '+getToken()},credentials:'same-origin'});if(!r.ok)throw new Error(await responseError(r,'Original konnte nicht geladen werden'));const blob=await r.blob(),a=document.createElement('a'),ext=(blob.type==='image/svg+xml'?'.svg':blob.type==='image/png'?'.png':blob.type==='image/webp'?'.webp':blob.type==='image/jpeg'?'.jpg':'');a.href=URL.createObjectURL(blob);a.download=`neuralhunt-${n.task_id}${ext}`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),5000)} async function renderOwnedNFTs(host){host.innerHTML='lade …';try{const items=await api('/api/me/artifacts?limit=24');host.innerHTML=items?.length?items.map(n=>`
${esc(n.display_name||(n.artifact_origin==='admin_drop'?'ADMIN DROP':'WINNER NFT'))}${n.artifact_origin==='admin_drop'?'ADMIN DROP · ':''}${n.rarity?esc(n.rarity)+' · ':''}${esc(shortID(n.task_id,12))} · ${Number(n.range_bits||0)} Bit · ${esc(fmtDate(n.completed_at))}
`).join(''):'Noch keine fertigen Gewinner-Artefakte für diese Identität.';host.querySelectorAll('[data-my-nft]').forEach(b=>b.onclick=async()=>{const n=items.find(x=>x.task_id===b.dataset.myNft);if(!n)return;b.disabled=true;try{await downloadMyNFT(n)}catch(e){status(e.message||'Download fehlgeschlagen')}finally{b.disabled=false}})}catch(e){host.innerHTML=`${esc(e.message||'NFTs konnten nicht geladen werden')}`}} async function toggleOwnedNFTs(host){if(!host)return;if(!host.classList.contains('hidden')){host.classList.add('hidden');return}host.classList.remove('hidden');await renderOwnedNFTs(host)} async function performIdentityExport(){const p=prompt('Passphrase für den verschlüsselten Identitäts-Export (mindestens 12 Zeichen). Bewahre Export und Passphrase getrennt auf.');if(!p)return;try{const text=await exportIdentity(p),a=document.createElement('a');a.href=URL.createObjectURL(new Blob([text],{type:'application/json'}));a.download=`neuralhunt-identity-${cid.slice(0,10)}.json`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),5000);status('Identität verschlüsselt gesichert')}catch(e){status(e.message||'Export fehlgeschlagen')}} async function performHostedLinkCode(){try{const x=await api('/api/me/customer-link',{method:'POST',body:JSON.stringify({})});const code=x.code||'';if(!code)throw new Error('Kein Hosted-Code erhalten');try{await navigator.clipboard?.writeText(code)}catch{}prompt('Einmaliger Hosted-Code (10 Minuten gültig). Im Customer-Service-Portal unter Haupt-Identität einfügen:',code);status('Hosted-Code erzeugt · nur einmal verwendbar')}catch(e){status(e.message||'Hosted-Code konnte nicht erzeugt werden')}} async function performIdentityImport(input){const f=input?.files?.[0];if(!f)return;const p=prompt('Passphrase für diesen Identitäts-Export');if(!p){input.value='';return}try{const imported=await importIdentity(await f.text(),p),current=cid;if(imported.clientId===current){status('Diese Identität ist bereits aktiv');input.value='';return}if(!confirm(`Identität wechseln?\n\nAktuell: ${current}\nImport: ${imported.clientId}\n\nDie lokale Browser-Identität wird ersetzt. Sichere die aktuelle Identität vorher, wenn du sie später noch brauchst.`)){input.value='';return}localStorage.setItem(identityKey,JSON.stringify(imported.bundle));clearToken();location.reload()}catch(err){status(err.message||'Import fehlgeschlagen');input.value=''}} $('mynfts').onclick=()=>toggleOwnedNFTs($('myNftsPanel')); $('landingMyNfts').onclick=()=>toggleOwnedNFTs($('landingNftsPanel')); $('hostedCode').onclick=performHostedLinkCode;$('landingHostedCode').onclick=performHostedLinkCode; $('exportid').onclick=performIdentityExport;$('landingExportId').onclick=performIdentityExport; $('importid').onchange=e=>performIdentityImport(e.target);$('landingImportId').onchange=e=>performIdentityImport(e.target); addEventListener('beforeunload',()=>{stopTimers();clearWSReconnect();if(landingTimer)clearTimeout(landingTimer);if(ws){ws._plannedClose=true;ws.close()}window.removeEventListener('neuralhunt-mobile-mode',syncMobile);map.destroy()},{once:true}); } function leaderboardShell(){ app.className='leaderboard-page';app.innerHTML=`
${markHTML()}
NEURAL HUNT / REALTIME LEADERBOARDLive score · wins · watermarked winner NFTs
PUBLIC SIGNAL

Echtzeit-Ranking

Live-Score zeigt die beste Position in einem aktuell aktiven Task. Gewinner-Artefakte werden öffentlich ausschließlich über eine serverseitig erzeugte Vorschau mit Wasserzeichen angezeigt.

verbinde …
WINNER ARTIFACTS

NFT-Galerie

0 Artefakte · nur Wasserzeichen-Vorschau
0 Clientsautomatische Aktualisierung über WebSocket
`; } async function runLeaderboard(){ leaderboardShell();let mode='live',rows=[],artifacts=[],ws=null,refreshTimer=null; const openNFT=a=>{if(!a?.preview_uri)return;$('nftLarge').src=a.preview_uri;$('nftLargeTitle').textContent=`Task ${shortID(a.task_id?.slice(-14),14)}`;$('nftLargeMeta').textContent=`Winner ${shortID(a.winner_client_id,18)} · ${a.range_bits} Bit${a.rarity?' · '+a.rarity:''} · WATERMARKED PREVIEW`;$('nftLightbox').classList.remove('hidden')}; const closeNFT=()=>{$('nftLightbox').classList.add('hidden');$('nftLarge').removeAttribute('src')}; const render=()=>{ const q=$('lbSearch').value.trim().toLowerCase(),filtered=rows.filter(x=>!q||String(x.client_id).toLowerCase().includes(q)||String(x.nft_task_id||'').toLowerCase().includes(q)); $('lbCount').textContent=`${filtered.length.toLocaleString('de-DE')} Clients`; const top=filtered.slice(0,3); $('lbPodium').innerHTML=top.map((x,i)=>`
${x.nft_preview_uri?`Wasserzeichen NFT Vorschau`:`#${i+1}`}
#${i+1} · ${esc(shortID(x.client_id,16))}${x.connected?'● online':'○ offline'} · Best ${fmtScore(x.best_score)} · ${x.nft_count||0} NFTs
${mode==='live'?fmtScore(x.live_score):`${x.wins} Wins`}
`).join(''); $('lbTable').innerHTML=filtered.map((x,i)=>`
#${i+1}${esc(shortID(x.client_id,22))}${(x.unlocks||[]).slice(0,3).map(esc).join(' · ')||'keine Unlocks'}
LIVE${fmtScore(x.live_score)}BEST${fmtScore(x.best_score)}WINS${x.wins||0}TIPPS${Number(x.guess_count||0).toLocaleString('de-DE')}
${x.nft_preview_uri?``:'—'}
`).join('')||'
Noch keine Teilnehmer
'; const shownArtifacts=artifacts.filter(a=>!q||String(a.winner_client_id).toLowerCase().includes(q)||String(a.task_id).toLowerCase().includes(q)); $('nftCount').textContent=shownArtifacts.length.toLocaleString('de-DE'); $('nftGallery').innerHTML=shownArtifacts.length?shownArtifacts.map(a=>``).join(''):'
Noch keine fertigen Gewinner-Artefakte
'; document.querySelectorAll('[data-gallery-task]').forEach(b=>b.onclick=()=>openNFT(artifacts.find(a=>a.task_id===b.dataset.galleryTask))); document.querySelectorAll('[data-nft-task]').forEach(b=>b.onclick=()=>openNFT(artifacts.find(a=>a.task_id===b.dataset.nftTask)||{task_id:b.dataset.nftTask,winner_client_id:filtered.find(x=>x.nft_task_id===b.dataset.nftTask)?.client_id,range_bits:'—',preview_uri:filtered.find(x=>x.nft_task_id===b.dataset.nftTask)?.nft_preview_uri})); }; async function refresh(){try{const [x,a]=await Promise.all([api(`/api/public/leaderboard?mode=${mode}&limit=500`),api('/api/public/artifacts?limit=96')]);rows=Array.isArray(x)?x:[];artifacts=Array.isArray(a)?a:[];$('lbState').textContent='LIVE · '+new Date().toLocaleTimeString('de-DE');render()}catch(e){$('lbState').textContent=e.message}} const debounce=()=>{clearTimeout(refreshTimer);refreshTimer=setTimeout(refresh,120)}; $('lbLive').onclick=()=>{mode='live';setActive('lbLive',true);setActive('lbAll',false);refresh()};$('lbAll').onclick=()=>{mode='alltime';setActive('lbLive',false);setActive('lbAll',true);refresh()};$('lbSearch').oninput=render; $('lbMobile').onclick=()=>{toggleMobileMode();$('lbMobile').textContent=mobileButtonLabel();setActive('lbMobile',document.documentElement.classList.contains('mobile-mode'))};$('lbMobile').textContent=mobileButtonLabel();setActive('lbMobile',document.documentElement.classList.contains('mobile-mode')); $('nftClose').onclick=closeNFT;$('nftLightbox').onclick=e=>{if(e.target===$('nftLightbox'))closeNFT()};addEventListener('keydown',e=>{if(e.key==='Escape')closeNFT()}); await refresh();const proto=location.protocol==='https:'?'wss':'ws';ws=new WebSocket(`${proto}://${location.host}/api/leaderboard/ws`);ws.onopen=()=>{$('lbState').textContent='LIVE · verbunden'};ws.onmessage=debounce;ws.onclose=()=>{$('lbState').textContent='WebSocket getrennt'};addEventListener('beforeunload',()=>{clearTimeout(refreshTimer);if(ws)ws.close()},{once:true}); } function placeShell(){ app.className='place-page';app.innerHTML=`
${markHTML()}
NEURAL HUNT / PLACEFortschritt wird zu Farbe · globale Live-Leinwand
verbinde …RANKINGHUNT
NEURAL PLACEPixel wählen
0 belegt0 Placements0 Artists
Scroll = Zoom · Ziehen = Pan · Klick = Pixel wählen
FARBE
AUSWAHL—Wähle einen Pixel auf der Leinwand.
`; } async function runPlace(){ placeShell(); let cid='',snapshot=null,config=null,wallet=null,width=0,height=0,palette=[],revision=0,selectedColor=Number(localStorage.getItem('neuralhunt.place.color')||15),selected=null; let pixels=null,meta=new Map(),filledCount=0,seenFeed=new Set(),off=document.createElement('canvas'),offctx=off.getContext('2d'),canvas=$('placeCanvas'),ctx=canvas.getContext('2d'),scale=1,panX=0,panY=0,dpr=1,drag=null,ws=null,pollTimer=null,walletTimer=null,toastTimer=null; const pxKey=(x,y)=>y*width+x; const fmtPoints=v=>Number(v||0).toLocaleString('de-DE',{maximumFractionDigits:3}); const toast=(text,bad=false)=>{const el=$('placeToast');el.textContent=text;el.classList.toggle('bad',!!bad);el.classList.remove('hidden');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.add('hidden'),2600)}; async function ensurePlaceSession(){const id=await ensureIdentity();cid=await clientId(id.publicJwk);if(!getToken()){const x=await loginIdentity();setToken(x.token);return}try{const x=await api('/api/place/me');if(x?.wallet?.client_id!==cid)throw Object.assign(new Error('identity mismatch'),{status:401})}catch(e){if(e.status!==401)throw e;clearToken();const x=await loginIdentity();setToken(x.token)}} function renderWallet(){if(!wallet||!config)return;$('placePoints').textContent=fmtPoints(wallet.balance_points);$('placePixelsAvailable').textContent=Number(wallet.available_pixels||0).toLocaleString('de-DE');$('placeEarned').textContent=fmtPoints(wallet.earned_points);$('placeSpent').textContent=fmtPoints(wallet.spent_points);$('placePlacements').textContent=Number(wallet.placements||0).toLocaleString('de-DE');$('placeOwned').textContent=Number(wallet.owned_pixels||0).toLocaleString('de-DE');$('placeRate').textContent=fmtPoints(config.points_per_score);$('placeDrawRate').textContent=fmtPoints(config.draw_points);$('placeTimeRate').textContent=`${fmtPoints(config.time_points)} / ${Number(config.time_interval_sec||0)}s`;$('placeCost').textContent=fmtPoints(config.pixel_cost);$('placeSubmitCost').textContent=`· ${fmtPoints(config.pixel_cost)} P`;const beaconText=config.draw_beacon_multiplier?` · Beacon-Boost multipliziert Draw-Punkte mit dem Pfadgewicht`:'',timeText=Number(config.time_points||0)>0?` · ${fmtPoints(config.time_points)} P je ${Number(config.time_interval_sec||0)}s aktive Hunt-Zeit`:'';$('placeEconomy').textContent=`+1,00 % Fortschritt = ${fmtPoints(config.points_per_score)} P · gezogen = ${fmtPoints(config.draw_points)} P Basis${beaconText}${timeText} · Pixel = ${fmtPoints(config.pixel_cost)} P.`;const workers=wallet.linked_workers||[];$('placeWorkerCount').textContent=workers.length;$('placeWorkerList').innerHTML=workers.length?workers.map(x=>`${esc(shortID(x,18))}`).join(''):'Keine Worker verknüpft';$('placeEarnings').innerHTML=(wallet.recent_earnings||[]).map(e=>{const worker=e.source_client_id!==cid,kind=String(e.kind||'progress'),tag=kind==='progress'?'FORTSCHRITT':kind==='draw'?'GEZOGEN':kind==='time'?'AKTIVZEIT':kind==='admin'?'ADMIN':kind.toUpperCase();let detail='';if(kind==='progress')detail=`${Number(e.old_score||0).toFixed(2)} → ${Number(e.new_score||0).toFixed(2)} %`;else if(kind==='draw')detail=`Lotterie gezogen${Number(e.multiplier||1)>1?` · Beacon ×${Number(e.multiplier).toLocaleString('de-DE')}`:''}`;else if(kind==='time')detail=`${Number(e.units||1)} Aktivzeit-Intervall${Number(e.units||1)===1?'':'e'}`;else detail=String(e.detail||'Manuelle Gutschrift');if(e.task_id)detail+=` · ${shortID(String(e.task_id).slice(-10),10)}`;return `
${esc(tag)}
+${fmtPoints(e.points)} P${esc(detail)}${worker?` · Worker ${esc(shortID(e.source_client_id,12))}`:''}
`}).join('')||'Noch keine Place-Rewards.';updateSubmit()} async function refreshWallet(){try{const x=await api('/api/place/me');config=x.config||config;wallet=x.wallet||wallet;renderWallet()}catch(e){if(e.status===401)return;}} function renderPalette(){$('placePalette').innerHTML=palette.map((c,i)=>``).join('');document.querySelectorAll('[data-place-color]').forEach(b=>b.onclick=()=>{selectedColor=Number(b.dataset.placeColor);localStorage.setItem('neuralhunt.place.color',String(selectedColor));renderPalette();draw();updateSubmit()})} function initBoard(list){pixels=new Uint8Array(width*height);pixels.fill(255);meta=new Map();filledCount=0;off.width=width;off.height=height;offctx.imageSmoothingEnabled=false;offctx.fillStyle='#f5f5f5';offctx.fillRect(0,0,width,height);for(const p of (list||[]))applyPixel(p,false);fit();draw()} function applyPixel(p,redraw=true){if(!p||p.x<0||p.y<0||p.x>=width||p.y>=height)return;const k=pxKey(p.x,p.y),ci=Number(p.color_index);if(pixels[k]===255)filledCount++;pixels[k]=ci;meta.set(k,p);offctx.fillStyle=palette[ci]||'#fff';offctx.fillRect(p.x,p.y,1,1);revision=Math.max(revision,Number(p.revision||0));if($('placeRevision'))$('placeRevision').textContent=`REV ${revision.toLocaleString('de-DE')}`;if($('placeFilled'))$('placeFilled').textContent=filledCount.toLocaleString('de-DE');if($('placeTotal'))$('placeTotal').textContent=revision.toLocaleString('de-DE');if(selected&&selected.x===p.x&&selected.y===p.y)updateSelection();if(redraw)draw()} function viewportRect(){return $('placeViewport').getBoundingClientRect()} function resize(){const r=viewportRect();dpr=Math.min(2,window.devicePixelRatio||1);const w=Math.max(1,Math.floor(r.width*dpr)),h=Math.max(1,Math.floor(r.height*dpr));if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;canvas.style.width=r.width+'px';canvas.style.height=r.height+'px'}draw()} function fit(){const r=viewportRect();if(!width||!height||!r.width)return;scale=Math.max(.15,Math.min(r.width/width,r.height/height)*.88);panX=(r.width-width*scale)/2;panY=(r.height-height*scale)/2;draw()} function boardPos(clientX,clientY){const r=viewportRect(),sx=clientX-r.left,sy=clientY-r.top;return {x:Math.floor((sx-panX)/scale),y:Math.floor((sy-panY)/scale),sx,sy}} function draw(){if(!pixels)return;const r=viewportRect();ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,r.width,r.height);ctx.save();ctx.translate(panX,panY);ctx.scale(scale,scale);ctx.imageSmoothingEnabled=false;ctx.drawImage(off,0,0);if(scale>=9){ctx.strokeStyle='rgba(255,255,255,.13)';ctx.lineWidth=1/scale;ctx.beginPath();for(let x=0;x<=width;x++){ctx.moveTo(x,0);ctx.lineTo(x,height)}for(let y=0;y<=height;y++){ctx.moveTo(0,y);ctx.lineTo(width,y)}ctx.stroke()}if(selected){ctx.strokeStyle=palette[selectedColor]||'#fff';ctx.lineWidth=Math.max(2/scale,.12);ctx.strokeRect(selected.x+.06,selected.y+.06,.88,.88);ctx.strokeStyle='rgba(255,255,255,.95)';ctx.lineWidth=Math.max(1/scale,.07);ctx.strokeRect(selected.x+.18,selected.y+.18,.64,.64)}ctx.restore()} function updateSelection(){if(!selected){$('placeSelection').textContent='—';$('placeSelectionOwner').textContent='Wähle einen Pixel auf der Leinwand.';$('placeCoords').textContent='Pixel wählen';updateSubmit();return}const p=meta.get(pxKey(selected.x,selected.y));$('placeSelection').textContent=`X ${selected.x} · Y ${selected.y}`;$('placeCoords').textContent=`${selected.x}, ${selected.y}`;$('placeSelectionOwner').textContent=p?`Aktuell von ${shortID(p.client_id,18)} · Rev ${Number(p.revision||0).toLocaleString('de-DE')}`:'Noch unbelegt';updateSubmit()} function updateSubmit(){const can=!!selected&&config?.enabled&&Number(wallet?.balance_points||0)>=Number(config.pixel_cost||Infinity);$('placeSubmit').disabled=!can;if(config&&!config.enabled)$('placeSubmit').textContent='PLACE DEAKTIVIERT';else $('placeSubmit').innerHTML=`PIXEL SETZEN · ${fmtPoints(config?.pixel_cost||0)} P`} function addFeed(p,prepend=true){const rev=Number(p?.revision||0);if(rev&&seenFeed.has(rev))return;if(rev)seenFeed.add(rev);const host=$('placeFeed'),row=document.createElement('div');row.className='place-feed-row';row.innerHTML=`
${esc(shortID(p.client_id,16))}${Number(p.x)}, ${Number(p.y)} · Rev ${Number(p.revision||0).toLocaleString('de-DE')}
`;if(prepend)host.prepend(row);else host.append(row);while(host.children.length>40)host.lastElementChild.remove()} async function syncChanges(){try{const x=await api(`/api/public/place/changes?after=${revision}`);for(const p of (x.pixels||[])){applyPixel(p,false);addFeed(p)}revision=Math.max(revision,Number(x.revision||0));$('placeRevision').textContent=`REV ${revision.toLocaleString('de-DE')}`;draw()}catch{}} function openPlaceWS(){const proto=location.protocol==='https:'?'wss':'ws';ws=new WebSocket(`${proto}://${location.host}/api/place/ws`,['neuralhunt.v1']);ws.onopen=()=>{$('placeLive').innerHTML=' LIVE';$('placeLive').classList.add('on')};ws.onmessage=ev=>{try{const e=JSON.parse(ev.data);if(e.type==='place_pixel'){applyPixel(e.data);addFeed(e.data);if(e.data.client_id===cid)refreshWallet()}else if(e.type==='place_ready'&&Number(e.data?.revision||0)>revision)syncChanges()}catch{}};ws.onclose=()=>{$('placeLive').innerHTML=' RECONNECT';$('placeLive').classList.remove('on');setTimeout(()=>{if(document.body.contains(canvas))openPlaceWS()},1200)}} try{ await ensurePlaceSession(); const [pub,me]=await Promise.all([api('/api/public/place'),api('/api/place/me')]);snapshot=pub;config=me.config||pub.config;wallet=me.wallet;width=Number(config.width);height=Number(config.height);palette=config.palette||[];revision=Number(pub.stats?.revision||0);selectedColor=clamp(selectedColor,0,Math.max(0,palette.length-1));renderWallet();renderPalette();initBoard(pub.pixels||[]);$('placeParticipants').textContent=Number(pub.stats?.participants||0).toLocaleString('de-DE');$('placeRevision').textContent=`REV ${revision.toLocaleString('de-DE')}`;for(const p of (pub.recent||[]).slice().reverse())addFeed(p);if(!config.enabled)toast('Neural Place ist aktuell nur lesbar.',true); }catch(e){$('placeLive').textContent='STARTFEHLER';toast(e.message||'Place konnte nicht geladen werden',true);return} const viewport=$('placeViewport'); viewport.onpointerdown=e=>{if(e.button!==0)return;viewport.setPointerCapture(e.pointerId);drag={id:e.pointerId,x:e.clientX,y:e.clientY,px:panX,py:panY,moved:false};canvas.style.cursor='grabbing'}; viewport.onpointermove=e=>{const b=boardPos(e.clientX,e.clientY);if(drag&&drag.id===e.pointerId){const dx=e.clientX-drag.x,dy=e.clientY-drag.y;if(Math.hypot(dx,dy)>4)drag.moved=true;panX=drag.px+dx;panY=drag.py+dy;draw()}if(b.x>=0&&b.y>=0&&b.x${b.x}, ${b.y}${p?esc(shortID(p.client_id,16)):'frei'}`;h.style.left=clamp(b.sx+14,8,viewport.clientWidth-150)+'px';h.style.top=clamp(b.sy+14,8,viewport.clientHeight-55)+'px';h.classList.remove('hidden')}else $('placeHover').classList.add('hidden')}; viewport.onpointerup=e=>{if(!drag||drag.id!==e.pointerId)return;const wasMoved=drag.moved;drag=null;canvas.style.cursor='crosshair';if(!wasMoved){const b=boardPos(e.clientX,e.clientY);if(b.x>=0&&b.y>=0&&b.x{drag=null;canvas.style.cursor='crosshair'}; viewport.onwheel=e=>{e.preventDefault();const r=viewportRect(),mx=e.clientX-r.left,my=e.clientY-r.top,bx=(mx-panX)/scale,by=(my-panY)/scale,f=e.deltaY<0?1.18:.84,newScale=clamp(scale*f,.08,48);panX=mx-bx*newScale;panY=my-by*newScale;scale=newScale;draw()}; $('placeZoomIn').onclick=()=>{const r=viewportRect(),mx=r.width/2,my=r.height/2,bx=(mx-panX)/scale,by=(my-panY)/scale;scale=clamp(scale*1.35,.08,48);panX=mx-bx*scale;panY=my-by*scale;draw()};$('placeZoomOut').onclick=()=>{const r=viewportRect(),mx=r.width/2,my=r.height/2,bx=(mx-panX)/scale,by=(my-panY)/scale;scale=clamp(scale/1.35,.08,48);panX=mx-bx*scale;panY=my-by*scale;draw()};$('placeFit').onclick=fit; $('placeSubmit').onclick=async()=>{if(!selected)return;const b=$('placeSubmit');b.disabled=true;try{const out=await api('/api/place/pixel',{method:'POST',body:JSON.stringify({x:selected.x,y:selected.y,color_index:selectedColor})});wallet=out.wallet||wallet;applyPixel(out.pixel);addFeed(out.pixel);renderWallet();if(out.wallet_refresh_required)refreshWallet();toast(`Pixel ${selected.x}, ${selected.y} gesetzt`)}catch(e){toast(e.code==='insufficient_place_points'?'Nicht genug Place-Punkte. Verdiene Fortschritt im Hunt.':e.message||'Placement fehlgeschlagen',true)}finally{updateSubmit()}}; const onResize=()=>resize();addEventListener('resize',onResize);resize();openPlaceWS();pollTimer=setInterval(syncChanges,5000);walletTimer=setInterval(refreshWallet,5000);addEventListener('beforeunload',()=>{removeEventListener('resize',onResize);clearInterval(pollTimer);clearInterval(walletTimer);if(ws){ws.onclose=null;ws.close()}},{once:true}); } function adminLoginShell(){app.className='login';app.innerHTML=``} function adminShell(){ app.className='admin';app.innerHTML=`
${markHTML()}
NEURAL HUNT / ADMINTasks · Clients · Runtime · Scheduler
PRIVATE CONTROL PLANE
TASKS0
Task wählen— Bit0 Clients0 Render0 FPS
CONTROL PLANESQLite

`; } async function runAdmin(){ let adminOK=false;try{await api('/api/admin/session',{},true);adminOK=true}catch{}if(!adminOK){adminLoginShell();$('adminlogin').onclick=async()=>{try{const r=await fetch('/api/admin/login',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({user:$('adminuser').value,password:$('adminpass').value})});if(!r.ok)throw new Error('Login fehlgeschlagen');location.reload()}catch(e){$('adminerr').textContent=e.message}};return} adminShell();const map=new NeuralMap($('adminmap'),{admin:true,onStats:s=>{if($('adminrender'))$('adminrender').textContent=s.render.toLocaleString('de-DE');if($('adminfps'))$('adminfps').textContent=s.fps}});const adminGuessFlashKey='neuralhunt.adminGuessFlash.v1';let adminGuessFlash=false;try{adminGuessFlash=localStorage.getItem(adminGuessFlashKey)==='1'}catch{}map.guessFlashEnabled=adminGuessFlash;setActive('adminGuessFlash',adminGuessFlash);const adminDraftKey='neuralhunt.adminDraft.v2';let draft={};try{draft=JSON.parse(localStorage.getItem(adminDraftKey)||'{}')||{}}catch{draft={}};let tasks=[],selected=null,points=[],settings=null,actions=[],providers=null,artifactUsage=null,clients=[],placeBonusEvents=[],poll=null,tab=['runtime','task','artifact'].includes(draft.tab)?draft.tab:'runtime',loading=false,adminDropWatchIDs=[],adminDropWatchTimer=null; const setAdminPanel=name=>{const grid=$('adminGrid');grid.classList.remove('mobile-show-map','mobile-show-tasks','mobile-show-control');grid.classList.add(`mobile-show-${name}`);document.querySelectorAll('[data-admin-panel]').forEach(b=>b.classList.toggle('active',b.dataset.adminPanel===name));if(name==='map')setTimeout(()=>map.resize(),30)}; const syncAdminMobile=()=>{const on=document.documentElement.classList.contains('mobile-mode');$('adminMobileToggle').textContent=mobileButtonLabel();setActive('adminMobileToggle',on);if(on){map.eco=true;map.labels=false;map.edges=false;map.zoom=Math.min(map.zoom,.94);setActive('adminEco',true);setActive('adminEdges',false);if(+$('adminmaxnodes').value>1500){$('adminmaxnodes').value=1500;$('adminmaxvalue').textContent='1.500'}}else{map.eco=false;setActive('adminEco',false)}map.resize()}; $('adminMobileToggle').onclick=()=>toggleMobileMode();document.querySelectorAll('[data-admin-panel]').forEach(b=>b.onclick=()=>setAdminPanel(b.dataset.adminPanel));window.addEventListener('neuralhunt-mobile-mode',syncAdminMobile);syncAdminMobile(); const runtimeKeys=['guess_min_interval_sec','client_submit_interval_sec','guess_lottery_window_sec','guess_lottery_max_accepted','beacon_hunt_enabled','beacon_bonus_weight','sybil_pow_bits','sybil_warmup_sec','openai_max_calls_1h','openai_max_calls_24h','openai_max_cost_24h_usd','openai_budget_reserve_usd','task_range_bits','active_task_count','presence_ttl_sec','default_max_nodes','public_score_precision','place_enabled','place_width','place_height','place_points_per_score','place_draw_points','place_draw_beacon_multiplier','place_time_points','place_time_interval_sec','place_time_max_gap_sec','place_pixel_cost']; const labels={guess_min_interval_sec:'Server-Tippintervall (s)',client_submit_interval_sec:'Client-Tippintervall (s)',guess_lottery_window_sec:'Lotterie-Zeitfenster (s)',guess_lottery_max_accepted:'Max. gezogene Tipps je Task/Fenster (0 = aus)',beacon_hunt_enabled:'Beacon Hunt (0 = aus, 1 = an)',beacon_bonus_weight:'Beacon Treffer-Gewicht (1–10)',sybil_pow_bits:'Anti-Sybil Proof-of-Work (Bit, 0 = aus)',sybil_warmup_sec:'Neue Identität Wartezeit (s)',openai_max_calls_1h:'OpenAI max. Bild-Calls / 1h (0 = aus)',openai_max_calls_24h:'OpenAI max. Bild-Calls / 24h (0 = aus)',openai_max_cost_24h_usd:'OpenAI max. geschätzte Kosten / 24h USD (0 = aus)',openai_budget_reserve_usd:'OpenAI Sicherheitsreserve pro nächstem Call USD',task_range_bits:'Default Zahlenraum (Bit)',active_task_count:'Parallele aktive Tasks',presence_ttl_sec:'Presence TTL (s)',default_max_nodes:'Default Max Nodes',public_score_precision:'Öffentliche Score-Präzision',place_enabled:'Neural Place (0 = aus, 1 = an)',place_width:'Place Breite (Pixel)',place_height:'Place Höhe (Pixel)',place_points_per_score:'Place-Punkte je +1,00 Score',place_draw_points:'Place-Punkte je gezogenem Tipp (0 = aus)',place_draw_beacon_multiplier:'Draw-Bonus × Beacon-Pfadgewicht (0/1)',place_time_points:'Place-Punkte je Aktivzeit-Intervall (0 = aus)',place_time_interval_sec:'Aktivzeit-Intervall (s)',place_time_max_gap_sec:'Max. Aktivitätslücke, die zählt (s)',place_pixel_cost:'Place-Kosten je Pixel'}; const fmtPlacePoints=v=>Number(v||0).toLocaleString('de-DE',{maximumFractionDigits:3}); const msg=s=>$('adminstatus').textContent=s; const renderAdminDropWatch=items=>{const host=$('adminDropResult');if(!host)return;const rows=Array.isArray(items)?items:[];if(!rows.length){host.textContent=adminDropWatchIDs.length?'Queue-Einträge werden gesucht …':'';return}const order={error:0,generating:1,pending:2,ready:3};rows.sort((a,b)=>(order[a.artifact_status]??9)-(order[b.artifact_status]??9));host.innerHTML=`
${rows.map(x=>{const st=String(x.artifact_status||'unknown'),err=String(x.artifact_error||'').trim(),id=String(x.task_id||'');let info=st==='pending'?'wartet auf Artifact-Worker':st==='generating'?'Bild wird gerade erzeugt':st==='ready'?'Collectible fertig':st==='error'?(err||'Generierung fehlgeschlagen'):st;return `
${esc(st.toUpperCase())}${esc(id.slice(-14))}${(x.artifact_rarity||x.artifact_rarity_override)?`${esc(x.artifact_rarity||x.artifact_rarity_override)}`:''}${esc(info.length>180?info.slice(0,179)+'…':info)}${st==='ready'?``:''}
`}).join('')}
`;host.querySelectorAll('[data-drop-open]').forEach(b=>b.onclick=()=>openAdminFile(b.dataset.dropOpen,'artifact'))}; const refreshAdminDropWatch=async()=>{clearTimeout(adminDropWatchTimer);adminDropWatchTimer=null;if(!adminDropWatchIDs.length)return;try{const rows=await api(`/api/admin/artifacts/status?ids=${encodeURIComponent(adminDropWatchIDs.join(','))}`,{},true);renderAdminDropWatch(rows);const active=(rows||[]).some(x=>x.artifact_status==='pending'||x.artifact_status==='generating');if(active){adminDropWatchTimer=setTimeout(refreshAdminDropWatch,2000)}else{adminDropWatchIDs=[];setTimeout(()=>load(true,false),0)}}catch(e){const host=$('adminDropResult');if(host)host.textContent='Queue-Status konnte nicht geladen werden: '+(e.message||e);adminDropWatchTimer=setTimeout(refreshAdminDropWatch,4000)}}; const watchAdminDrops=ids=>{adminDropWatchIDs=(Array.isArray(ids)?ids:[]).map(String).filter(Boolean).slice(0,20);clearTimeout(adminDropWatchTimer);adminDropWatchTimer=null;if(adminDropWatchIDs.length)refreshAdminDropWatch()}; let adminSignalWS=null,adminSignalTimer=null,adminSignalBackoff=500,adminSignalClosed=false; const closeAdminSignalWS=()=>{clearTimeout(adminSignalTimer);adminSignalTimer=null;if(adminSignalWS){adminSignalWS._plannedClose=true;try{adminSignalWS.close(1000,'disabled')}catch{}adminSignalWS=null}}; const openAdminSignalWS=()=>{if(!adminGuessFlash||adminSignalClosed)return;if(adminSignalWS&&(adminSignalWS.readyState===WebSocket.OPEN||adminSignalWS.readyState===WebSocket.CONNECTING))return;const proto=location.protocol==='https:'?'wss':'ws',socket=new WebSocket(`${proto}://${location.host}/api/admin/ws`);adminSignalWS=socket;socket.onopen=()=>{if(adminSignalWS!==socket)return;adminSignalBackoff=500};socket.onmessage=ev=>{if(adminSignalWS!==socket||!adminGuessFlash)return;try{const e=JSON.parse(ev.data);if(e.type==='guess_signal'&&selected?.id===e.task_id)map.flashGuess(e.data)}catch{}};socket.onclose=()=>{if(adminSignalWS===socket)adminSignalWS=null;if(!socket._plannedClose&&adminGuessFlash&&!adminSignalClosed){clearTimeout(adminSignalTimer);adminSignalTimer=setTimeout(openAdminSignalWS,adminSignalBackoff);adminSignalBackoff=Math.min(10000,adminSignalBackoff*1.8)}}}; if(adminGuessFlash)openAdminSignalWS(); const saveDraft=()=>{try{draft.tab=tab;draft.selectedTaskId=selected?.id||draft.selectedTaskId||'';draft.filters={status:$('statusfilter')?.value||'',q:$('taskquery')?.value||''};localStorage.setItem(adminDraftKey,JSON.stringify(draft))}catch{}}; const draftScope=()=>tab==='task'&&selected?`task:${selected.id}`:`global:${tab}`; const draftFieldKey=el=>el.dataset.setting?`setting:${el.dataset.setting}`:el.dataset.settingString?`string:${el.dataset.settingString}`:el.dataset.draft||el.id||''; const captureDraft=()=>{const scope=draftScope();draft.fields=draft.fields||{};draft.fields[scope]=draft.fields[scope]||{};document.querySelectorAll('#settingfields input,#settingfields textarea,#settingfields select').forEach(el=>{if(el.type==='file')return;const k=draftFieldKey(el);if(k)draft.fields[scope][k]=el.value});saveDraft()}; const restoreDraft=()=>{const scope=draftScope(),values=draft.fields?.[scope]||{};document.querySelectorAll('#settingfields input,#settingfields textarea,#settingfields select').forEach(el=>{if(el.type==='file')return;const k=draftFieldKey(el);if(k&&Object.prototype.hasOwnProperty.call(values,k))el.value=values[k]})}; const clearDraftKeys=keys=>{const scope=draftScope(),values=draft.fields?.[scope];if(values){keys.forEach(k=>delete values[k]);saveDraft()}}; function renderOverview(o){$('overview').innerHTML=`${o.connected} verbunden${o.clients} Identitäten${o.active_tasks} aktive Tasks${o.completed_tasks} abgeschlossen${Number(o.guesses||0).toLocaleString('de-DE')} Tipps${o.artifacts_ready} Artefakte`} function renderPerformance(p){const r=p?.runtime||{},w=p?.websocket||{},pw=p?.place_websocket||{},x=p?.process||{};$('performance').innerHTML=`${Number(r.guesses_per_sec||0).toFixed(1)} Guess/s${Number(r.rejected_per_sec||0).toFixed(1)} Reject/s${Number(r.improvements_per_sec||0).toFixed(1)} Improve/s${Number(r.sqlite_writes_per_sec||0).toFixed(1)} SQLite W/s${Number(w.frames_per_sec||0).toFixed(0)} WS Frames/s${(Number(w.bytes_per_sec||0)/1048576).toFixed(2)} WS MB/s${Number(w.dropped_per_sec||0).toFixed(1)} Drops/s${Number(pw.connected||0).toLocaleString('de-DE')} Place WS${Number(pw.frames_per_sec||0).toFixed(0)} Place F/s${Number(x.goroutines||0).toLocaleString('de-DE')} Goroutines${(Number(x.heap_bytes||0)/1048576).toFixed(1)} Heap MB`} async function openAdminFile(taskID,kind){const popup=window.open('','_blank');try{const r=await fetch(`/api/admin/tasks/${encodeURIComponent(taskID)}/${kind}`,{credentials:'same-origin'});if(!r.ok)throw new Error(await responseError(r,'Datei konnte nicht geöffnet werden'));const blob=await r.blob(),u=URL.createObjectURL(blob);if(popup)popup.location=u;else{const a=document.createElement('a');a.href=u;a.target='_blank';a.click()}setTimeout(()=>URL.revokeObjectURL(u),60000)}catch(e){if(popup)popup.close();msg(e.message)}} function renderTasks(){tasks=Array.isArray(tasks)?tasks:[];$('taskCount').textContent=tasks.length;$('tasks').innerHTML=tasks.length?tasks.map(t=>{const aerr=String(t.artifact_error||'').trim();return ``}).join(''):'
Keine Tasks
';document.querySelectorAll('#tasks button[data-id]').forEach(b=>b.onclick=e=>{const art=e.target.closest('[data-artifact-task]'),man=e.target.closest('[data-manifest-task]');if(art){e.preventDefault();e.stopPropagation();openAdminFile(art.dataset.artifactTask,'artifact');return}if(man){e.preventDefault();e.stopPropagation();openAdminFile(man.dataset.manifestTask,'manifest');return}openTask(tasks.find(t=>t.id===b.dataset.id))})} function renderRuntime(){ $('settingfields').innerHTML=`
GLOBAL RUNTIME
${runtimeKeys.map(k=>``).join('')}

Die Tipp-Lotterie gilt getrennt pro aktivem Task. Bei einem Wert > 0 werden alle gültigen Tipps eines Zeitfensters gesammelt und am Fensterende exakt bis zur eingestellten Menge zufällig gezogen. Nicht gezogene Tipps werden nicht gegen das Ziel geprüft und verändern den Score nicht; ihre Sequenz wird trotzdem verbraucht. 0 = Lotterie aus. Änderungen greifen für neu beginnende Fenster.

Beacon Hunt: Optional wählen Spieler PULSE, FLUX oder ORBIT. Der erste öffentliche drand-Round nach Fensterschluss bestimmt reproduzierbar den Boost-Pfad und die gewichtete Ziehung. Alle Reveal-Daten werden gespeichert und über die Public API nachvollziehbar gemacht.

Anti-Sybil: Neue Browser-Identitäten lösen einmalig einen Proof-of-Work und warten anschließend die konfigurierte Warmup-Zeit, bevor Tipps gewertet werden. Das erhöht die Kosten massenhafter Identitätserstellung, ersetzt aber keine externe echte Identitätsprüfung.

OpenAI Circuit Breaker: Vor jedem Bild-Call werden rollierende 1h-/24h-Call-Limits und das geschätzte 24h-Kostenbudget geprüft. Bei Überschreitung bleibt die Gewinnerkarte in der Queue und wird später erneut versucht.

Neural Place: Punkte können aus drei automatischen Quellen kommen: echter Highscore-Fortschritt, ein tatsächlich gezogener Lotterie-Tipp und aktive Hunt-Zeit. Der Draw-Bonus kann mit demselben Beacon-Pfadgewicht multipliziert werden, das auch die Ziehung beeinflusst. Aktivzeit zählt nur zwischen gültigen signierten Requests; Pausen über Max. Aktivitätslücke werden nicht nachvergütet. Verknüpfte Hosted Worker schreiben alle diese Rewards dem Owner-Wallet gut. Ein Placement – auch das Übermalen – kostet Place-Kosten je Pixel.

Die übrigen Defaults gelten global. Task-spezifische Intervalle und Zahlenräume steuerst du im Tab „Task Actions“.

PLACE-PUNKTE MANUELL VERGEBEN

Gutschriften werden sofort dem Place-Wallet gutgeschrieben und dauerhaft auditiert. Wird eine verknüpfte Worker-ID gewählt, landet die Gutschrift konsistent im Wallet des Owners.

${clients.map(c=>``).join('')}
LETZTE PLACE-BONUS-EVENTS
${placeBonusEvents.length?placeBonusEvents.slice(0,16).map(e=>`
${esc(String(e.kind||'bonus').toUpperCase())} · +${fmtPlacePoints(e.points)} P${fmtDate(e.created_at)} · ${esc(shortID(e.owner_client_id||'',16))}${e.source_client_id&&e.source_client_id!==e.owner_client_id?` ← Worker ${esc(shortID(e.source_client_id,14))}`:''}${Number(e.multiplier||1)>1?` · ×${Number(e.multiplier).toLocaleString('de-DE')}`:''}${e.detail?`${esc(e.detail)}`:''}DONE
`).join(''):'
Noch keine Draw-, Aktivzeit- oder Admin-Boni.
'}
ALTE PROFILE BEREINIGEN

Löscht ausschließlich Accounts, die seit mindestens X Zeit inaktiv, aktuell nicht verbunden, niemals Gewinner eines Tasks und keine Neural-Place-Teilnehmer sind. Gewinner, verknüpfte Worker/Owner und Identitäten mit verdientem Place-Wert oder Placements werden unabhängig vom Alter geschützt. Zugehörige Hunt-Punkte, Unlocks und Task-Auswahl werden bei löschbaren Profilen mit entfernt.

Noch nicht geprüft.
`;$('savesettings').style.display='inline-block';$('ensuretasks').style.display='inline-block'; $('grantPlacePoints').onclick=async()=>{const target=String($('placeGrantTarget')?.value||'').trim(),points=Number($('placeGrantPoints')?.value||0),reason=String($('placeGrantReason')?.value||'').trim();if(!target){msg('Ziel Client-/Worker-ID fehlt');return}if(!Number.isFinite(points)||points<=0){msg('Punkte müssen größer als 0 sein');return}const b=$('grantPlacePoints');b.disabled=true;try{const out=await api('/api/admin/place/points',{method:'POST',body:JSON.stringify({target_client_id:target,points,reason})},true);const credited=out?.event?.owner_client_id||target;msg(`+${fmtPlacePoints(out?.event?.points||points)} Place-Punkte an ${shortID(credited,18)} gutgeschrieben`);clearDraftKeys(['placeGrantTarget','placeGrantPoints','placeGrantReason']);await load(true,true)}catch(e){msg(e.message)}finally{if(document.body.contains(b))b.disabled=false}}; const cleanupSeconds=()=>{const v=Math.max(1,Number($('profileCleanupValue')?.value||0)),unit=$('profileCleanupUnit')?.value||'days',factor=unit==='hours'?3600:unit==='weeks'?7*86400:86400;return Math.round(v*factor)}; const showCleanup=p=>{const box=$('profileCleanupResult');if(!box)return;const eligible=Number(p?.eligible||0),wins=Number(p?.protected_winners||0),place=Number(p?.protected_place||0),online=Number(p?.protected_connected||0),cutoff=p?.cutoff_ms?fmtDate(p.cutoff_ms):'—';box.innerHTML=`${eligible.toLocaleString('de-DE')} löschbar· ${wins.toLocaleString('de-DE')} alte Gewinner · ${place.toLocaleString('de-DE')} Place-Teilnehmer · ${online.toLocaleString('de-DE')} aktuell verbundene Accounts geschütztGrenze: letzte Aktivität vor ${esc(cutoff)}`;}; const previewCleanup=async()=>{try{captureDraft();const seconds=cleanupSeconds(),p=await api(`/api/admin/profiles/cleanup-preview?inactive_for_seconds=${seconds}`,{},true);showCleanup(p);return p}catch(e){msg(e.message);throw e}}; $('previewProfileCleanup').onclick=()=>previewCleanup().catch(()=>{}); $('runProfileCleanup').onclick=async()=>{try{const p=await previewCleanup();const n=Number(p?.eligible||0);if(!n){msg('Keine passenden inaktiven Nicht-Gewinner-Profile gefunden');return}const value=$('profileCleanupValue').value,unitLabel=$('profileCleanupUnit').selectedOptions[0]?.textContent||'';if(!confirm(`${n.toLocaleString('de-DE')} Profile endgültig löschen?\n\nKriterium: seit mindestens ${value} ${unitLabel} inaktiv, offline, niemals Gewinner und ohne Neural-Place-Wert.\nGewinner, Place-Teilnehmer, verknüpfte Worker/Owner und aktuell verbundene Accounts bleiben geschützt.`))return;const out=await api('/api/admin/profiles/cleanup',{method:'POST',body:JSON.stringify({inactive_for_seconds:cleanupSeconds()})},true);msg(`${Number(out.deleted||0).toLocaleString('de-DE')} alte Profile gelöscht`);showCleanup({...p,eligible:Math.max(0,n-Number(out.deleted||0))});await load(true,false)}catch(e){msg(e.message)}}; } function renderArtifact(){ const ps=providers?.providers||{},preset=settings?.artifact_preset||'legacy',u=artifactUsage||{},cb=u.circuit_breaker||{},recent=Array.isArray(u.recent)?u.recent:[]; const usd=(n,d=4)=>Number(n||0).toLocaleString('de-DE',{style:'currency',currency:'USD',minimumFractionDigits:d,maximumFractionDigits:d}); const usageRows=recent.length?recent.map(r=>`${fmtDate(r.created_at)}${r.kind==='character_anchor'?'ANCHOR':'KARTE'}${esc(r.model||'—')}${esc(r.quality||'—')} · ${esc(r.size||'—')}${Number(r.input_text_tokens||0).toLocaleString('de-DE')} T + ${Number(r.input_image_tokens||0).toLocaleString('de-DE')} I → ${Number(r.output_tokens||0).toLocaleString('de-DE')}${r.estimated_cost_usd==null?'—':usd(r.estimated_cost_usd,5)}${esc(r.request_id?String(r.request_id).slice(-16):'—')}`).join(''):'Noch keine OpenAI-Bildgenerierung protokolliert.'; if(preset==='raccoon_full_art_v1'){ const anchorReady=!!providers?.character_anchor; $('settingfields').innerHTML=`
RIFT FULL-ART COLLECTION
OPENAI · ${ps.openai?'API KEY READY':'API KEY FEHLT'}CHARACTER ANCHOR · ${anchorReady?'LOCKED':'NOCH NICHT ERZEUGT'}
RIFT CHARACTER ANCHOR

Der Anchor definiert ausschließlich, wer RIFT ist. Er wird einmalig als neutrales Character-Referenzbild erzeugt und anschließend für alle Tasks verwendet. Die visuelle Stilrichtung kommt getrennt aus der Style-Referenz des jeweiligen Tasks.

${anchorReady?'

Anchor vorhanden · Identität ist gesperrt. Es gibt absichtlich keinen Überschreiben-Button.

':'

Du kannst den Anchor jetzt kontrolliert erzeugen. Falls du das nicht tust, erzeugt der Worker ihn weiterhin automatisch beim ersten Gewinnerbild als Sicherheits-Fallback.

'}
${anchorReady?'RIFT Character Anchor':'NO ANCHOR'}
${anchorReady?'':`
`}
PIPELINE

Provider OpenAI · Ausgabe 1024 × 1536 · Quality ${esc(settings?.artifact_quality||'medium')} · Preset raccoon_full_art_v1.

Jede Karten-Generierung sendet zwei getrennte Referenzen: Image 1 = globaler RIFT-Character-Anchor, Image 2 = Style-Referenz des gewählten Tasks. Ohne eigenen Task-Style wird das eingebettete internal/artifact/assets/style_reference.jpg nur als Default-Style verwendet.

Theme, Kleidung, Accessoires, Szene, Pose, Stimmung, Farb-Akzente und Rarity werden deterministisch aus Task/Winner/Seed gewählt. Die Rarity-Stufen Common, Uncommon, Rare, Ultra Rare und Special Illustration Rare steuern zusätzlich Farbigkeit und Karten-Effekte des programmgesteuerten Layouts. Das Modell erzeugt nur die Full-Art-Illustration; das finale Kartenlayout wird anschließend programmgesteuert aufgebaut.

RARITY-CHANCEN

Globale Wahrscheinlichkeiten für Gewinnerkarten und Admin-Drops mit ZUFÄLLIG NACH CHANCEN. Die Summe muss exakt 100 % ergeben. Bereits erzeugte Karten bleiben unverändert.

ADMIN NFT DROP

Erzeugt 1–20 zufällige RIFT-Collectibles für eine vorhandene Client-Identität, ohne einen Spielgewinn zu buchen. Die Karten laufen normal durch Artifact-Queue und OpenAI-Circuit-Breaker. Bei leerem Template wird pro Karte zufällig eine bestehende Task-Serie als Art-Direction verwendet. Die Rarity kann nach den globalen Chancen gezogen oder für das Geschenk fest vorgegeben werden.

${clients.map(c=>``).join('')}
OPENAI NUTZUNG & KOSTEN
KOSTEN HEUTE${usd(u.today_cost_usd,4)}${Number(u.today_calls||0).toLocaleString('de-DE')} API-Calls · ${Number(u.today_cards||0).toLocaleString('de-DE')} Karten${Number(u.today_calls||0)>Number(u.today_priced_calls||0)?` · ${(Number(u.today_calls||0)-Number(u.today_priced_calls||0)).toLocaleString('de-DE')} ohne Kostendaten`:''}
Ø KOSTEN PRO KARTE${usd(u.avg_card_cost_usd,5)}${Number(u.priced_card_generations||0).toLocaleString('de-DE')} bepreiste Generierungen
KOSTEN PRO 1.000 KARTEN${usd(u.cost_per_1000_usd,2)}hochgerechnet aus dem bisherigen Kartenmittel
CIRCUIT BREAKER${cb.blocked?'BLOCKED':'READY'}${Number(cb.calls_1h||0).toLocaleString('de-DE')} / ${Number(cb.max_calls_1h||0).toLocaleString('de-DE')} Calls 1h · ${Number(cb.calls_24h||0).toLocaleString('de-DE')} / ${Number(cb.max_calls_24h||0).toLocaleString('de-DE')} Calls 24h · ${usd(cb.cost_24h_usd,3)} / ${usd(cb.max_cost_24h_usd,2)}
Die Token-Nutzung stammt direkt aus der OpenAI-Antwort. Die USD-Werte werden lokal daraus mit fest hinterlegten öffentlichen Standardpreisen berechnet; sie sind kein Rechnungsabgleich. Anchor-Kosten zählen in „heute“, aber nicht in den Karten-Durchschnitt.
${usageRows}
ZeitTypModellTokens (Text + Bild → Output)KostenRequest

Task-spezifische Style-Bilder und optionale kreative Vorgaben pflegst du im Tab TASK ACTIONS.

`; if(anchorReady){const img=$('anchorPreview');loadProtectedImage('/api/admin/artifact/character-anchor?ts='+Date.now(),img,true).catch(()=>{if(img)img.alt='Anchor konnte nicht geladen werden'})} if(adminDropWatchIDs.length)setTimeout(refreshAdminDropWatch,0); const updateRarityChanceTotal=()=>{const inputs=[...document.querySelectorAll('[data-rarity-pct]')],total=inputs.reduce((sum,i)=>sum+Number(i.value||0),0),host=$('rarityChanceTotal');if(host)host.innerHTML=`Summe: ${total.toFixed(2)} %${Math.abs(total-100)>.001?' · muss 100 % ergeben':' · READY'}`};document.querySelectorAll('[data-rarity-pct]').forEach(i=>i.addEventListener('input',updateRarityChanceTotal));updateRarityChanceTotal(); const create=$('createCharacterAnchor');if(create)create.onclick=async()=>{if(!confirm('RIFT Character Anchor jetzt einmalig erzeugen? Danach wird er absichtlich nicht automatisch überschrieben.'))return;create.disabled=true;create.textContent='ANCHOR WIRD ERZEUGT …';try{await api('/api/admin/artifact/character-anchor',{method:'POST'},true);msg('RIFT Character Anchor erzeugt und gesperrt');await load(true,true)}catch(e){msg(e.message);create.disabled=false;create.textContent='RIFT-ANCHOR JETZT ERZEUGEN'}}; const drop=$('createAdminDrop');if(drop)drop.onclick=async()=>{const target=String($('dropTargetClient')?.value||'').trim(),count=Number($('dropCount')?.value||1),template=String($('dropTemplateTask')?.value||'').trim(),rarity=String($('dropRarity')?.value||'').trim();if(!target){msg('Ziel Client-ID fehlt');return}if(!Number.isInteger(count)||count<1||count>20){msg('Anzahl muss 1–20 sein');return}const rarityLabel=rarity||'ZUFÄLLIG NACH CHANCEN';if(!confirm(`${count} Admin-NFT${count===1?'':'s'} für ${target.slice(0,18)}… erzeugen?\nRarity: ${rarityLabel}\n\nDies kann API-Kosten auslösen; der Circuit-Breaker bleibt aktiv.`))return;drop.disabled=true;drop.textContent='WIRD EINGEREIHT …';try{const r=await api('/api/admin/artifacts/drop',{method:'POST',body:JSON.stringify({target_client_id:target,template_task_id:template,rarity,count})},true);const ids=r.created_task_ids||[];$('adminDropResult').textContent=`${ids.length} Collectible(s) eingereiht · ${rarityLabel} · Status wird verfolgt …`;watchAdminDrops(ids);msg('Admin NFT-Drop eingereiht · Artifact-Worker wurde geweckt');await load(true,false)}catch(e){msg(e.message)}finally{drop.disabled=false;drop.textContent='NFT-DROP IN QUEUE STELLEN'}}; }else{ $('settingfields').innerHTML=`
LEGACY ARTIFACT GENERATION
${['local','openai','comfyui','a1111'].map(k=>`${k.toUpperCase()} · ${ps[k]?'READY':'ENV FEHLT'}`).join('')}
`; const pr=document.querySelector('[data-setting-string="artifact_provider"]'),qu=document.querySelector('[data-setting-string="artifact_quality"]');if(pr)pr.value=settings?.artifact_provider||'local';if(qu)qu.value=settings?.artifact_quality||'medium'; } $('savesettings').style.display='inline-block';$('ensuretasks').style.display='none'; } function payloadText(a){try{const p=typeof a.payload==='string'?JSON.parse(a.payload):a.payload;return Object.keys(p||{}).length?JSON.stringify(p):'—'}catch{return '—'}} function renderTaskControl(){ $('savesettings').style.display='none';$('ensuretasks').style.display='none';if(!selected){$('settingfields').innerHTML='
Links einen Task auswählen.
';return} const customStyle=!!String(selected.nft_style_reference||'').trim(); const artifactErr=String(selected.artifact_error||'').trim(); const artifactOwner=String(selected.artifact_owner_client_id||selected.winner_client_id||'').trim(); const artifactDiag=selected.artifact_status==='error'?`
NFT-GENERIERUNG FEHLGESCHLAGEN${esc(artifactErr||'Unbekannter Artifact-Fehler')}
Die Meldung stammt direkt aus tasks.artifact_error. „NFT-Neugenerierung anstoßen · DONE“ bedeutet nur, dass die Admin-Aktion die Karte erneut in die Queue gestellt hat.
`:''; $('settingfields').innerHTML=`
TASK ${esc(selected.display_name||selected.id.slice(-12))}
${selected.range_bits} Bit${selected.paused?'PAUSED':selected.status} Status${selected.revision} Revision${selected.retire_after_completion?'AUSLAUFEND':'FORTLAUFEND'} Serie
${artifactDiag}
TASK-DARSTELLUNG & RIFT ART-DIRECTION
${selected.parent_task_id?`↳ geerbt von ${esc(String(selected.parent_task_id).slice(-12))}`:'ROOT TASK'}

Solange der Task nicht als auslaufend markiert ist, erbt der Folge-Task Anzeigename, Beschreibung, kreative Vorgaben, Ausschlüsse und die Style-Referenz. Entwürfe in Textfeldern bleiben bei Auto-Refresh erhalten.

Style-Referenz des Tasks
NFT STYLE-REFERENZ · ${customStyle?'CUSTOM':'DEFAULT'}

Dieses Bild definiert wie RIFT für diese Task-Serie gerendert wird. Der globale Character Anchor definiert separat wer RIFT ist. Nutzer sehen diese Vorschau auf der Task-Auswahl.

${customStyle?`Aktuell: ${esc(String(selected.nft_style_reference).slice(0,18))}…`:'Kein eigener Style hochgeladen · es wird das eingebettete Default-Style-Bild verwendet.'}

${customStyle?'':''}
LOKALER PIPELINE-TEST

Erzeugt eine komplette Testkarte ohne OpenAI-Aufruf. Der vorhandene character_anchor.png dient als Mock-Artwork, die Style-Referenz dieses Tasks als Hintergrund. Kartenlayout, Traits, Dateischreiben und SVG-Ausgabe werden lokal durchgespielt. Der echte Task-/Artifact-Status bleibt unverändert.

${artifactOwner&&selected.status==='completed'?`
NFT-EIGENTUM

Historischer Gewinner bleibt unverändert. Nur der aktuelle Besitzer des Collectibles wird übertragen und der Transfer wird in SQLite protokolliert.

Aktueller Besitzer: ${esc(artifactOwner)} · Ursprung: ${esc(selected.artifact_origin||'win')}
`:''} ${clients.map(c=>``).join('')}
AKTIONEN / SCHEDULER
AKTIONSPLAN / AUDIT
${actions.length?actions.map(a=>`
${esc(actionLabel(a.action_type))}${fmtDate(a.execute_at)} · ${esc(payloadText(a))}${a.error?`${esc(a.error)}`:''}${esc(a.status)}${a.status==='pending'?``:''}
`).join(''):'
Noch keine geplanten Aktionen
'}
`; const styleImg=$('taskStylePreview');loadProtectedImage(`/api/admin/tasks/${selected.id}/style-reference?ts=${Date.now()}`,styleImg,true).catch(()=>{if(styleImg)styleImg.alt='Style-Referenz konnte nicht geladen werden'}); const uploadStyle=$('uploadTaskStyle');if(uploadStyle)uploadStyle.onclick=async()=>{const file=$('taskStyleFile')?.files?.[0];if(!file){msg('Bitte zuerst ein JPEG- oder PNG-Stylebild auswählen');return}const fd=new FormData();fd.append('file',file,file.name);uploadStyle.disabled=true;uploadStyle.textContent='STYLE WIRD HOCHGELADEN …';try{await api(`/api/admin/tasks/${selected.id}/style-reference`,{method:'PUT',body:fd},true);msg('Task-Style gespeichert · Folge-Task übernimmt ihn');await load(true,true)}catch(e){msg(e.message);uploadStyle.disabled=false;uploadStyle.textContent='STYLE HOCHLADEN / ERSETZEN'}}; const clearStyle=$('clearTaskStyle');if(clearStyle)clearStyle.onclick=async()=>{if(!confirm('Eigenen Task-Style entfernen und wieder den eingebetteten Default-Style verwenden?'))return;try{await api(`/api/admin/tasks/${selected.id}/style-reference`,{method:'DELETE'},true);msg('Task-Style auf Default zurückgesetzt');await load(true,true)}catch(e){msg(e.message)}}; const pipelineTest=$('runPipelineTest');if(pipelineTest)pipelineTest.onclick=async()=>{pipelineTest.disabled=true;pipelineTest.textContent='TEST-KARTE WIRD LOKAL ERZEUGT …';try{const r=await api(`/api/admin/tasks/${selected.id}/pipeline-test`,{method:'POST'},true);const popup=window.open('','_blank');const resp=await fetch(r.url,{credentials:'same-origin'});if(!resp.ok)throw new Error(await responseError(resp,'Testkarte konnte nicht geöffnet werden'));const blob=await resp.blob(),u=URL.createObjectURL(blob);if(popup)popup.location=u;else{const a=document.createElement('a');a.href=u;a.target='_blank';a.click()}setTimeout(()=>URL.revokeObjectURL(u),60000);msg('Lokale Testkarte erzeugt · 0 API-Calls · $0.00')}catch(e){msg(e.message)}finally{pipelineTest.disabled=false;pipelineTest.textContent='TEST-KARTE ERZEUGEN · 0 API-TOKENS'}}; const transferArtifact=$('transferArtifact');if(transferArtifact)transferArtifact.onclick=async()=>{const target=String($('transferTargetClient')?.value||'').trim(),reason=String($('transferReason')?.value||'').trim();if(!target){msg('Ziel Client-ID fehlt');return}if(!confirm(`NFT von ${artifactOwner.slice(0,18)}… auf ${target.slice(0,18)}… übertragen?\n\nDer historische Gewinner bleibt unverändert.`))return;transferArtifact.disabled=true;try{await api(`/api/admin/tasks/${selected.id}/artifact/transfer`,{method:'POST',body:JSON.stringify({target_client_id:target,reason})},true);msg('NFT-Eigentum übertragen');clearDraftKeys(['transferTargetClient','transferReason']);await load(true,true)}catch(e){msg(e.message)}finally{transferArtifact.disabled=false}}; const renderPayload=()=>{const type=$('actionType').value,host=$('actionPayload');if(type==='set_range_bits')host.innerHTML=`

Preserve erhält Seed/Sequenzen und re-skaliert Scores mathematisch. Reroll setzt Scores/Sequenzen zurück.

`;else if(type==='set_intervals')host.innerHTML=``;else host.innerHTML=`

${type==='reroll'?'Achtung: neues Ziel und neuer öffentlicher Seed; aktuelle Scores werden auf 0 gesetzt.':type==='close'?(selected.retire_after_completion?'Beendet den Task. AUSLAUFEND ist aktiv: es wird kein vererbter Folge-Task erzeugt.':'Beendet den Task. Der automatisch erzeugte Folge-Task erbt Zahlenraum, Intervalle, Darstellung, RIFT-Prompt und Style-Referenz.'):type==='regenerate_artifact'?'Nur für abgeschlossene Tasks: setzt das Artifact wieder auf pending. DONE im Audit bestätigt nur das Einreihen; der Artifact-Status zeigt danach generating, ready oder error.':'Keine weiteren Parameter.'}

`}; const defaultAt=new Date(Date.now()+5*60*1000);defaultAt.setMinutes(defaultAt.getMinutes()-defaultAt.getTimezoneOffset());$('actionAt').value=defaultAt.toISOString().slice(0,16);restoreDraft();renderPayload();restoreDraft();$('actionType').onchange=()=>{renderPayload();restoreDraft();captureDraft()}; $('saveTaskConfig').onclick=async()=>{try{captureDraft();await api(`/api/admin/tasks/${selected.id}/config`,{method:'PUT',body:JSON.stringify({display_name:$('taskDisplayName').value,description:$('taskDescription').value,nft_prompt_instructions:$('taskNFTPrompt').value,nft_negative_prompt:$('taskNFTNegative').value,retire_after_completion:$('taskRetiring').value==='1'})},true);clearDraftKeys(['taskDisplayName','taskDescription','taskNFTPrompt','taskNFTNegative']);msg($('taskRetiring').value==='1'?'Task-Konfiguration gespeichert · Task ist AUSLAUFEND':'Task-Konfiguration gespeichert · Folge-Task übernimmt sie');await load(true,true)}catch(e){msg(e.message)}}; const submitAction=async runNow=>{try{captureDraft();const type=$('actionType').value,payload={};if(type==='set_range_bits'){payload.bits=Number($('actionBits').value);payload.mode=$('actionMode').value}else if(type==='set_intervals'){payload.server_min_interval_sec=Number($('actionServer').value);payload.client_submit_interval_sec=Number($('actionClient').value)}const execute_at=runNow?null:new Date($('actionAt').value).toISOString();await api(`/api/admin/tasks/${selected.id}/actions`,{method:'POST',body:JSON.stringify({action_type:type,payload,execute_at})},true);msg(runNow?'Aktion ausgeführt':'Aktion geplant');await load(true,true)}catch(e){msg(e.message)}}; $('runAction').onclick=()=>submitAction(true);$('scheduleAction').onclick=()=>submitAction(false);document.querySelectorAll('[data-cancel-action]').forEach(b=>b.onclick=async()=>{try{await api(`/api/admin/actions/${b.dataset.cancelAction}/cancel`,{method:'POST'},true);await load(true,true)}catch(e){msg(e.message)}}); } // The control plane is intentionally NOT rebuilt by the 3-second telemetry poll. // Replacing