diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6119bb3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.env +web/node_modules +web/dist +data +*.db +*.db-shm +*.db-wal +*.zip diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a933c03 --- /dev/null +++ b/.env.example @@ -0,0 +1,59 @@ +# Core +HTTP_ADDR=:8080 +JWT_SECRET=change-me-to-a-long-random-secret +ADMIN_USER=admin +ADMIN_PASSWORD=change-me + +# Local data paths. Docker Compose overrides both to /data/... so the named volume remains persistent. +SQLITE_PATH=./data/neuralhunt.db +ARTIFACT_DIR=./data/artifacts +# Optional absolute base URL. Leave empty to store relative /artifacts/... links. +ARTIFACT_PUBLIC_BASE_URL= + +# Runtime defaults. These are copied into SQLite on first start and can then be +# changed in the Admin UI. +DEFAULT_GUESS_MIN_INTERVAL_SEC=10 +DEFAULT_CLIENT_SUBMIT_INTERVAL_SEC=11 +DEFAULT_TASK_RANGE_BITS=28 +DEFAULT_ACTIVE_TASK_COUNT=1 +DEFAULT_PRESENCE_TTL_SEC=35 +DEFAULT_MAX_NODES=2000 +DEFAULT_PUBLIC_SCORE_PRECISION=2 + +# RIFT full-art collection (default). For normal operation you only need these +# two values. Create the global RIFT character anchor once from Admin → ARTIFACT +# (or let the first winner create it as a fallback). Each task can then receive +# its own JPEG/PNG style reference in Admin → TASK ACTIONS; the bundled style +# reference is only the fallback for tasks without a custom style. +ARTIFACT_MODEL=gpt-image-2 +OPENAI_API_KEY= + +# Optional OpenAI endpoint override. Normally leave this unchanged/omitted. +OPENAI_BASE_URL=https://api.openai.com + +# Advanced overrides only; the built-in preset already uses OpenAI, 1024x1536 +# portrait output, medium quality and a deterministic programmatic SVG card frame. +# ARTIFACT_PRESET=raccoon_full_art_v1 +# ARTIFACT_PROVIDER=openai +# ARTIFACT_WIDTH=1024 +# ARTIFACT_HEIGHT=1536 +# ARTIFACT_QUALITY=medium +# ARTIFACT_HTTP_TIMEOUT=4m +# ARTIFACT_PROMPT=... # only used by legacy preset/providers +# ARTIFACT_NEGATIVE_PROMPT=... # only used by legacy/local providers + +# ComfyUI local API. COMFYUI_WORKFLOW_PATH must point to a workflow exported in +# API format. The workflow can use placeholders documented in README.md. +# When Neural Hunt runs in Docker and ComfyUI runs on the host, use +# http://host.docker.internal:8188 and mount/copy the workflow into /data. +COMFYUI_URL= +COMFYUI_WORKFLOW_PATH= +COMFYUI_POLL_TIMEOUT=4m + +# AUTOMATIC1111 Stable Diffusion WebUI API. Start A1111 with --api. +# Docker-to-host example: http://host.docker.internal:7860 +A1111_URL= +A1111_USER= +A1111_PASSWORD= +A1111_SAMPLER= +A1111_CFG_SCALE=7 diff --git a/.gitea/workflows/registry.yml b/.gitea/workflows/registry.yml new file mode 100644 index 0000000..cfe785d --- /dev/null +++ b/.gitea/workflows/registry.yml @@ -0,0 +1,51 @@ +name: release-tag +on: + push: + branches: + - 'main' +jobs: + release-image: + runs-on: ubuntu-latest + env: + DOCKER_ORG: sendnrw + DOCKER_LATEST: latest + RUNNER_TOOL_CACHE: /toolcache + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker BuildX + uses: docker/setup-buildx-action@v2 + with: # replace it with your local IP + config-inline: | + [registry."git.send.nrw"] + http = true + insecure = true + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + registry: git.send.nrw # replace it with your local IP + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Get Meta + id: meta + run: | + echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT + echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + platforms: | + linux/amd64 + push: true + tags: | # replace it with your local IP and tags + git.send.nrw/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }} + git.send.nrw/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b0ca15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.env +web/node_modules +web/dist +data/ +*.db +*.db-shm +*.db-wal +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81de087 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.26-alpine AS build +WORKDIR /src +COPY go.mod ./ +RUN go mod download +COPY cmd ./cmd +COPY internal ./internal +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/neuralhunt ./cmd/server \ + && CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/neuralhunt-client ./cmd/client + +FROM alpine:3.24 +RUN adduser -D -H app && mkdir -p /data /app && chown -R app:app /data /app +WORKDIR /app +COPY --from=build /out/neuralhunt /app/neuralhunt +COPY --from=build /out/neuralhunt-client /app/neuralhunt-client +USER app +ENV SQLITE_PATH=/data/neuralhunt.db \ + ARTIFACT_DIR=/data/artifacts +EXPOSE 8080 +ENTRYPOINT ["/app/neuralhunt"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b6131c0 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +.PHONY: dev local client build test + +dev: + docker compose up --build + +local: + go run ./cmd/server + +client: + go run ./cmd/client + +build: + go build -o neuralhunt ./cmd/server + go build -o neuralhunt-client ./cmd/client + +test: + go test ./... diff --git a/README.md b/README.md index 733e279..813bbb6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,631 @@ -# neural-hunt +# Neural Hunt — V2.8 RIFT Task-Style Collection + + +> **V2.8 RIFT Task-Styles:** RIFT-Identität und Rendering-Stil sind jetzt sauber getrennt. `/data/artifacts/_collection/character_anchor.png` ist ein globaler, neutraler Identity-Lock für den Waschbären RIFT und kann im Admin-Tab **ARTIFACT** einmalig manuell erzeugt und geprüft werden. Jeder Task kann im Admin-Tab **TASK ACTIONS** ein eigenes JPEG-/PNG-Style-Referenzbild erhalten; Nutzer sehen dieses Stylebild bereits in der Task-Auswahl und wählen damit indirekt die gewünschte NFT-Art. Bei jeder RIFT-Karte sendet Neural Hunt **Image 1 = Character Anchor** und **Image 2 = Task Style Reference** an die Images Edit API. Ohne eigenen Task-Style bleibt `internal/artifact/assets/style_reference.jpg` nur noch der Default-Fallback. Folge-Tasks erben ihren Style. `medium` bleibt Standard und die OpenAI-Usage-/Kosten-KPIs aus V2.7 bleiben erhalten. + + +> **V2.5.2 Hotfix:** Der 3-Sekunden-Telemetrie-Poll hat in V2.5.1 den kompletten Control-Plane-DOM neu aufgebaut. Dadurch wurden native `
+
LEADERBOARDECHTZEIT →
+ +
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
CLIENTADMIN
+
+
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=`
${markHTML()}
NEURAL HUNT / ADMINControl plane

← Client-Ansicht
`} +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