'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 adminTokenKey = 'neuralhunt.adminToken'; 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 ? localStorage.getItem(adminTokenKey) : 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}); 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?localStorage.getItem(adminTokenKey):getToken();const headers={};if(token)headers.Authorization='Bearer '+token; const r=await fetch(path,{headers});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))}; async function ensureIdentity(){const raw=localStorage.getItem(identityKey);if(raw)return JSON.parse(raw);const kp=await crypto.subtle.generateKey({name:'ECDSA',namedCurve:'P-256'},true,['sign','verify']);const b={version:1,publicJwk:await crypto.subtle.exportKey('jwk',kp.publicKey),privateJwk:await crypto.subtle.exportKey('jwk',kp.privateKey)};localStorage.setItem(identityKey,JSON.stringify(b));return b} async function clientId(pub){const s=`${pub.kty}|${pub.crv}|${pub.x}|${pub.y}`;return b64u(await crypto.subtle.digest('SHA-256',new TextEncoder().encode(s)))} async function sign(message){const b=await ensureIdentity();const k=await crypto.subtle.importKey('jwk',b.privateJwk,{name:'ECDSA',namedCurve:'P-256'},false,['sign']);return b64u(await crypto.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})`}} 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();const 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})});if(!r.ok)throw new Error(await responseError(r,'Login fehlgeschlagen'));return r.json()} async function deterministicGuess(taskID,seed,cid,seq,bits){const h=new Uint8Array(await crypto.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<>>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.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.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}); } 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(); } 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.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-Bild neu erzeugen'})[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 …
Tasks werden geladen …
Ein Client kann immer nur mit einem Task aktiv verbunden sein.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; 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 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'),rank='#'+(me.rank||'—'),score=fmtScore(me.score);$('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('')}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;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),signature=await sign(`guess|${task.id}|${seq}|${guess}`),correct=await api(`/api/tasks/${task.id}/guess`,{method:'POST',body:JSON.stringify({seq,guess,signature})});nextGuessAt=Date.now()+Math.max(1,task.client_submit_interval_sec)*1000;status(correct?'Treffer — Task gelöst!':'Tipp akzeptiert',correct?'researching':'living');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;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?token=${encodeURIComponent(getToken())}&max_nodes=${encodeURIComponent(mx)}`);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');$('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(); $('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(); $('exportid').onclick=async()=>{const p=prompt('Passphrase für den verschlüsselten Identitäts-Export');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)}catch(e){status(e.message||'Export fehlgeschlagen')}}; $('importid').onchange=async e=>{const f=e.target.files?.[0];if(!f)return;const p=prompt('Passphrase für diesen Identitäts-Export');if(!p)return;try{await importIdentity(await f.text(),p);clearToken();location.reload()}catch(err){status(err.message||'Import fehlgeschlagen')}}; 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 · 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 adminLoginShell(){app.className='login';app.innerHTML=``} function adminShell(){ app.className='admin';app.innerHTML=`
${markHTML()}
NEURAL HUNT / ADMINTasks · Clients · Runtime · Scheduler
CLIENTLEADERBOARD
TASKS0
Task wählen Bit0 Clients0 Render0 FPS
CONTROL PLANESQLite

`; } async function runAdmin(){ if(!localStorage.getItem(adminTokenKey)){adminLoginShell();$('adminlogin').onclick=async()=>{try{const r=await fetch('/api/admin/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user:$('adminuser').value,password:$('adminpass').value})});if(!r.ok)throw new Error('Login fehlgeschlagen');const x=await r.json();localStorage.setItem(adminTokenKey,x.token);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 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,poll=null,tab=['runtime','task','artifact'].includes(draft.tab)?draft.tab:'runtime',loading=false; 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','task_range_bits','active_task_count','presence_ttl_sec','default_max_nodes','public_score_precision']; const labels={guess_min_interval_sec:'Server-Tippintervall (s)',client_submit_interval_sec:'Client-Tippintervall (s)',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'}; const msg=s=>$('adminstatus').textContent=s; 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||{},x=p?.process||{};$('performance').innerHTML=`${Number(r.guesses_per_sec||0).toFixed(1)} Guess/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(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 token=localStorage.getItem(adminTokenKey),r=await fetch(`/api/admin/tasks/${encodeURIComponent(taskID)}/${kind}`,{headers:{Authorization:'Bearer '+token}});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=>``).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('')}

Diese Defaults gelten für neue Tasks. Task-spezifische Intervalle und Zahlenräume steuerst du im Tab „Task Actions“.

`;$('savesettings').style.display='inline-block';$('ensuretasks').style.display='inline-block'; } function renderArtifact(){ const ps=providers?.providers||{},preset=settings?.artifact_preset||'legacy',u=artifactUsage||{},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. Das Modell erzeugt nur die Full-Art-Illustration; das finale Kartenlayout wird anschließend programmgesteuert aufgebaut.

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
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'})} 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'}}; }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(); $('settingfields').innerHTML=`
TASK ${esc(selected.display_name||selected.id.slice(-12))}
${selected.range_bits} Bit${selected.paused?'PAUSED':selected.status} Status${selected.revision} Revision
TASK-DARSTELLUNG & RIFT ART-DIRECTION
${selected.parent_task_id?`↳ geerbt von ${esc(String(selected.parent_task_id).slice(-12))}`:'ROOT TASK'}

Der Folge-Task erbt 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.

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 token=localStorage.getItem(adminTokenKey),resp=await fetch(r.url,{headers:{Authorization:'Bearer '+token}});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 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'?'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 queued.':'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})},true);clearDraftKeys(['taskDisplayName','taskDescription','taskNFTPrompt','taskNFTNegative']);msg('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