@@ -137,7 +137,7 @@ class NeuralMap {
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 . 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 ) ) ;
}
@@ -175,7 +175,7 @@ class NeuralMap {
}
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 ) ) ;
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 ( ) ;
}
@@ -189,6 +189,13 @@ class NeuralMap {
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 ) }
@@ -261,6 +268,13 @@ class NeuralMap {
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' ;
@@ -278,7 +292,7 @@ class NeuralMap {
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 ( d < bestD ) { best = x ; bestD = d } } this . hover = best ; if ( ! best ) { this . tooltip . classList . add ( 'hidden' ) ; return } const p = best . p , group = p . _group || p . _count > 1 , 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 ? ` <strong>LOD-Gruppe · ${ p . _count } Clients</strong><small>Ø Score ${ Number ( p . score || 0 ) . toFixed ( 2 ) } · Best ${ Number ( p . _bestScore || 0 ) . toFixed ( 2 ) } <br> ${ Number ( p . guess _count || 0 ) . toLocaleString ( 'de-DE' ) } Tipps</small> ` : ` <strong> ${ p . client _id === this . selfId ? 'DU' : esc ( String ( p . client _id ) . slice ( 0 , 16 ) ) } </strong><small>Score ${ score . toFixed ( 2 ) } · Rank # ${ p . rank || '—' } <br> ${ zone } <br> ${ Number ( p . guess _count || 0 ) . toLocaleString ( 'de-DE' ) } Tipps</small> ` ; 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 . lastPaint < minFrame ) { this . frame = requestAnimationFrame ( t => this . 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 ( ) ;
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 ) ) ;
}
@@ -440,20 +454,24 @@ function adminShell(){
<nav class="admin-mobile-tabs glass" id="adminMobileTabs"><button data-admin-panel="map" class="active">MAP</button><button data-admin-panel="tasks">TASKS</button><button data-admin-panel="control">CONTROL</button></nav>
<main class="adminGrid mobile-show-map" id="adminGrid">
<section class="glass tasklist"><div class="panel-title"><span>TASKS</span><span class="chip" id="taskCount">0</span></div><div class="toolbar"><select id="statusfilter"><option value="">alle Status</option><option>active</option><option>completed</option><option>closed</option></select><input id="taskquery" placeholder="Task / Winner"><button id="filter">FILTER</button></div><div class="tasks" id="tasks"></div></section>
<section class="adminMap glass"><div id="adminmap" class="neural-map"></div><div class="map-overlay top"><span><b id="selectedtask">Task wählen</b></span><span><b id="adminbits">—</b> Bit</span><span><b id="adminpoints">0</b> Clients</span><span><b id="adminrender">0</b> Render</span><span><b id="adminfps">0</b> FPS</span><span id="winner"></span><label class="inline-filter">Score ≥ <input id="adminMinScore" type="number" min="0" max="100" step="1" value="0"></label><input id="adminClientFilter" class="client-filter" placeholder="Client-ID filtern"></div><div class="map-overlay bottom"><label>MAX NODES <input id="adminmaxnodes" type="range" min="100" max="50000" step="100" value="5000"><b id="adminmaxvalue">5.000</b></label><button id="adminProximity" class="active">TARGET FIELD</button><button id="adminRotate" class="active">ORBIT</button><button id="adminEdges" class="active">SIGNALWEGE</button><button id="adminShells" class="active">SCORE-RINGE</button><button id="adminLOD" class="active">LOD</button><button id="adminEco">ECO</button><button id="adminReset">RESET</button></div></section>
<section class="adminMap glass"><div id="adminmap" class="neural-map"></div><div class="map-overlay top"><span><b id="selectedtask">Task wählen</b></span><span><b id="adminbits">—</b> Bit</span><span><b id="adminpoints">0</b> Clients</span><span><b id="adminrender">0</b> Render</span><span><b id="adminfps">0</b> FPS</span><span id="winner"></span><label class="inline-filter">Score ≥ <input id="adminMinScore" type="number" min="0" max="100" step="1" value="0"></label><input id="adminClientFilter" class="client-filter" placeholder="Client-ID filtern"></div><div class="map-overlay bottom"><label>MAX NODES <input id="adminmaxnodes" type="range" min="100" max="50000" step="100" value="5000"><b id="adminmaxvalue">5.000</b></label><button id="adminProximity" class="active">TARGET FIELD</button><button id="adminRotate" class="active">ORBIT</button><button id="adminEdges" class="active">SIGNALWEGE</button><button id="adminShells" class="active">SCORE-RINGE</button><button id="adminLOD" class="active">LOD</button><button id="adminGuessFlash" title="Jeden ausgewerteten Tipp kurz als Prozentwert anzeigen, auch unterhalb des Highscores">TIPPS %</button><button id="adminEco">ECO</button><button id="adminReset">RESET</button></div></section>
<section class="glass settings"><div class="panel-title"><span>CONTROL PLANE</span><span class="chip">SQLite</span></div><div class="settings-tabs"><button id="tabRuntime" class="active">RUNTIME</button><button id="tabTask">TASK ACTIONS</button><button id="tabArtifact">ARTIFACT</button></div><div id="settingfields"></div><div class="actions"><button id="savesettings">SPEICHERN</button><button id="ensuretasks">ACTIVE TASKS SICHERN</button></div><p class="small statusline" id="adminstatus"></p></section>
</main> ` ;
}
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 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 ;
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 , 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' , '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' ] ;
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' } ;
const msg = s => $ ( 'adminstatus' ) . textContent = s ;
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 || '' ;
@@ -547,12 +565,12 @@ async function runAdmin(){
async function load ( keepMessage = false , refreshControls = false ) { if ( loading ) return ; loading = true ; try { const status = $ ( 'statusfilter' ) . value , q = $ ( 'taskquery' ) . value , dayStart = new Date ( ) ; dayStart . setHours ( 0 , 0 , 0 , 0 ) ; const [ ts , st , ov , pv , pf , au ] = await Promise . all ( [ api ( ` /api/admin/tasks?status= ${ encodeURIComponent ( status ) } &q= ${ encodeURIComponent ( q ) } &limit=300 ` , { } , true ) , api ( '/api/admin/settings' , { } , true ) , api ( '/api/admin/overview' , { } , true ) , api ( '/api/admin/artifact/providers' , { } , true ) , api ( '/api/admin/performance' , { } , true ) , api ( ` /api/admin/artifact/usage?day_start_ms= ${ dayStart . getTime ( ) } ` , { } , true ) ] ) ; tasks = Array . isArray ( ts ) ? ts : [ ] ; settings = st || { } ; providers = pv || { } ; artifactUsage = au || { } ; refreshArtifactUsageTelemetry ( ) ; renderOverview ( ov || { } ) ; renderPerformance ( pf || { } ) ; if ( selected ) { selected = tasks . find ( t => t . id === selected . id ) || selected } renderTasks ( ) ; if ( selected ) await refreshSelected ( refreshControls ) ; else if ( refreshControls ) renderSettings ( ) } catch ( e ) { if ( e . status === 401 ) { location . reload ( ) ; return } msg ( e . message || 'Laden fehlgeschlagen' ) } finally { loading = false } }
async function openTask ( t ) { captureDraft ( ) ; selected = t ; draft . selectedTaskId = t ? . id || '' ; saveDraft ( ) ; await refreshSelected ( true ) ; renderTasks ( ) ; if ( document . documentElement . classList . contains ( 'mobile-mode' ) ) setAdminPanel ( 'map' ) }
$ ( 'filter' ) . onclick = ( ) => { saveDraft ( ) ; load ( ) } ; $ ( 'statusfilter' ) . onchange = ( ) => { saveDraft ( ) ; load ( ) } ; $ ( 'taskquery' ) . addEventListener ( 'input' , saveDraft ) ; $ ( 'taskquery' ) . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' ) { saveDraft ( ) ; load ( ) } } ) ; $ ( 'adminmaxnodes' ) . oninput = e => { $ ( 'adminmaxvalue' ) . textContent = Number ( e . target . value ) . toLocaleString ( 'de-DE' ) ; renderMap ( ) } ; $ ( 'adminMinScore' ) . oninput = renderMap ; $ ( 'adminClientFilter' ) . oninput = renderMap ;
$ ( 'adminProximity' ) . onclick = ( ) => { map . proximityFocus = ! map . proximityFocus ; $ ( 'adminProximity' ) . textContent = map . proximityFocus ? 'TARGET FIELD' : 'RAW 3D' ; setActive ( 'adminProximity' , map . proximityFocus ) } ; $ ( 'adminRotate' ) . onclick = ( ) => { map . autoRotate = ! map . autoRotate ; setActive ( 'adminRotate' , map . autoRotate ) } ; $ ( 'adminEdges' ) . onclick = ( ) => { map . edges = ! map . edges ; setActive ( 'adminEdges' , map . edges ) } ; $ ( 'adminShells' ) . onclick = ( ) => { map . shells = ! map . shells ; setActive ( 'adminShells' , map . shells ) } ; $ ( 'adminLOD' ) . onclick = ( ) => { map . lodEnabled = ! map . lodEnabled ; map . rebuild ( ) ; setActive ( 'adminLOD' , map . lodEnabled ) } ; $ ( 'adminEco' ) . onclick = ( ) => { map . eco = ! map . eco ; map . resize ( ) ; setActive ( 'adminEco' , map . eco ) } ; $ ( 'adminReset' ) . onclick = ( ) => map . resetView ( ) ;
$ ( 'adminProximity' ) . onclick = ( ) => { map . proximityFocus = ! map . proximityFocus ; $ ( 'adminProximity' ) . textContent = map . proximityFocus ? 'TARGET FIELD' : 'RAW 3D' ; setActive ( 'adminProximity' , map . proximityFocus ) } ; $ ( 'adminRotate' ) . onclick = ( ) => { map . autoRotate = ! map . autoRotate ; setActive ( 'adminRotate' , map . autoRotate ) } ; $ ( 'adminEdges' ) . onclick = ( ) => { map . edges = ! map . edges ; setActive ( 'adminEdges' , map . edges ) } ; $ ( 'adminShells' ) . onclick = ( ) => { map . shells = ! map . shells ; setActive ( 'adminShells' , map . shells ) } ; $ ( 'adminLOD' ) . onclick = ( ) => { map . lodEnabled = ! map . lodEnabled ; map . rebuild ( ) ; setActive ( 'adminLOD' , map . lodEnabled ) } ; $ ( 'adminGuessFlash' ) . onclick = ( ) => { adminGuessFlash = ! adminGuessFlash ; map . guessFlashEnabled = adminGuessFlash ; if ( ! adminGuessFlash ) map . guessFlashes = [ ] ; setActive ( 'adminGuessFlash' , adminGuessFlash ) ; try { localStorage . setItem ( adminGuessFlashKey , adminGuessFlash ? '1' : '0' ) } catch { } if ( adminGuessFlash ) openAdminSignalWS ( ) ; else closeAdminSignalWS ( ) } ; $ ( ' adminEco') . onclick = ( ) => { map . eco = ! map . eco ; map . resize ( ) ; setActive ( 'adminEco' , map . eco ) } ; $ ( 'adminReset' ) . onclick = ( ) => map . resetView ( ) ;
$ ( 'tabRuntime' ) . onclick = ( ) => { captureDraft ( ) ; tab = 'runtime' ; saveDraft ( ) ; renderSettings ( ) } ; $ ( 'tabTask' ) . onclick = ( ) => { captureDraft ( ) ; tab = 'task' ; saveDraft ( ) ; renderSettings ( ) } ; $ ( 'tabArtifact' ) . onclick = ( ) => { captureDraft ( ) ; tab = 'artifact' ; saveDraft ( ) ; renderSettings ( ) } ;
$ ( 'savesettings' ) . onclick = async ( ) => { try { captureDraft ( ) ; const out = { ... settings } ; document . querySelectorAll ( '[data-setting]' ) . forEach ( i => out [ i . dataset . setting ] = Number ( i . value ) ) ; document . querySelectorAll ( '[data-setting-string]' ) . forEach ( i => out [ i . dataset . settingString ] = i . value ) ; settings = await api ( '/api/admin/settings' , { method : 'PUT' , body : JSON . stringify ( out ) } , true ) ; if ( draft . fields ) delete draft . fields [ draftScope ( ) ] ; saveDraft ( ) ; msg ( 'gespeichert' ) ; renderSettings ( ) } catch ( e ) { msg ( e . message ) } } ;
$ ( 'ensuretasks' ) . onclick = async ( ) => { try { await api ( '/api/admin/tasks/ensure' , { method : 'POST' } , true ) ; msg ( 'aktive Tasks sichergestellt' ) ; await load ( true , true ) } catch ( e ) { msg ( e . message ) } } ;
$ ( 'adminlogout' ) . onclick = async ( ) => { try { await fetch ( '/api/admin/logout' , { method : 'POST' , credentials : 'same-origin' } ) } finally { location . reload ( ) } } ;
$ ( 'settingfields' ) . addEventListener ( 'input' , captureDraft ) ; $ ( 'settingfields' ) . addEventListener ( 'change' , captureDraft ) ; if ( draft . filters ) { $ ( 'statusfilter' ) . value = draft . filters . status || '' ; $ ( 'taskquery' ) . value = draft . filters . q || '' } await load ( ) ; const first = tasks . find ( t => t . id === draft . selectedTaskId ) || ( tasks . find ( t => t . status === 'active' ) || tasks [ 0 ] ) ; if ( first ) await openTask ( first ) ; else renderSettings ( ) ; poll = setInterval ( ( ) => load ( true , false ) , 3000 ) ; addEventListener ( 'beforeunload' , ( ) => { captureDraft ( ) ; clearInterval ( poll ) ; window . removeEventListener ( 'neuralhunt-mobile-mode' , syncAdminMobile ) ; map . destroy ( ) } , { once : true } ) ;
$ ( 'settingfields' ) . addEventListener ( 'input' , captureDraft ) ; $ ( 'settingfields' ) . addEventListener ( 'change' , captureDraft ) ; if ( draft . filters ) { $ ( 'statusfilter' ) . value = draft . filters . status || '' ; $ ( 'taskquery' ) . value = draft . filters . q || '' } await load ( ) ; const first = tasks . find ( t => t . id === draft . selectedTaskId ) || ( tasks . find ( t => t . status === 'active' ) || tasks [ 0 ] ) ; if ( first ) await openTask ( first ) ; else renderSettings ( ) ; poll = setInterval ( ( ) => load ( true , false ) , 3000 ) ; addEventListener ( 'beforeunload' , ( ) => { adminSignalClosed = true ; closeAdminSignalWS ( ) ; captureDraft ( ) ; clearInterval ( poll ) ; window . removeEventListener ( 'neuralhunt-mobile-mode' , syncAdminMobile ) ; map . destroy ( ) } , { once : true } ) ;
}
applyMobileMode ( mobileModeEnabled ( ) ) ;