Vendored
+17
-10
@@ -387,7 +387,7 @@ async function runUser(){
|
||||
if(e.code==='task_config_changed')await refreshTaskConfig(true);
|
||||
nextGuessAt=Date.now()+1500;status(e.code==='presence_required'?'Live-Verbindung wird automatisch wiederhergestellt …':'Client wird automatisch synchronisiert …')
|
||||
}
|
||||
async function submit(){if(!task||submitting)return;if(!wsReady()){scheduleWSReconnect();nextGuessAt=Date.now()+1000;status('Live-Verbindung wird wiederhergestellt …');return}submitting=true;status('signiert Tipp …','thinking');try{const current=await api('/api/tasks/current');if(current.id!==task.id){await showLanding();return}task=current;syncBeacon();if(task.paused){status('Task pausiert');nextGuessAt=0;return}const seq=task.next_seq,guess=await deterministicGuess(task.id,task.public_seed,cid,seq,task.range_bits),beaconOn=Number(task.beacon_hunt_enabled||0)===1&&Number(task.guess_lottery_max_accepted||0)>0,msg=beaconOn?`guess|${task.id}|${seq}|${guess}|${beaconPath}`:`guess|${task.id}|${seq}|${guess}`,signature=await sign(msg);if(Number(task.guess_lottery_max_accepted||0)>0)status(beaconOn?`Beacon ${beaconPath} committed · wartet auf externen Draw`:`wartet auf Losziehung · max. ${Number(task.guess_lottery_max_accepted).toLocaleString('de-DE')} Tipps / ${Number(task.guess_lottery_window_sec||60)}s`,'thinking');const correct=await api(`/api/tasks/${task.id}/guess`,{method:'POST',body:JSON.stringify({seq,guess,signature,beacon_path:beaconOn?beaconPath:''})});nextGuessAt=Date.now()+Math.max(1,task.client_submit_interval_sec)*1000;if(beaconOn){try{const d=await api(`/api/public/beacon/${encodeURIComponent(task.id)}/latest`);$('beaconLast').textContent=`Dein Pfad ${beaconPath} · Boost ${d.boosted_path} · drand #${d.beacon_round}`;status(correct?'Treffer — Task gelöst!':`Draw ${d.boosted_path} · Tipp gezogen & geprüft`,correct?'researching':'living')}catch{status(correct?'Treffer — Task gelöst!':'Beacon-Tipp gezogen & geprüft',correct?'researching':'living')}}else status(correct?'Treffer — Task gelöst!':Number(task.guess_lottery_max_accepted||0)>0?'Tipp gezogen & geprüft':'Tipp akzeptiert',correct?'researching':'living');await Promise.all([refreshMe(),refreshLeaders()]);if(correct)setTimeout(()=>showLanding(),1300)}catch(e){if(e.status===409){await recover409(e);return}nextGuessAt=Date.now()+Math.max(2,task?.client_submit_interval_sec||11)*1000;if(e.code==='identity_warmup'){const wait=Math.max(1,Number(e.data?.retry_after_sec||task?.client_submit_interval_sec||15));nextGuessAt=Date.now()+wait*1000;status(`Anti-Sybil-Wartezeit · ${wait}s`,'living');return}if(e.code==='lottery_not_selected'){if(e.data?.boosted_path&&$('beaconLast'))$('beaconLast').textContent=`Dein Pfad ${e.data.chosen_path} · Boost ${e.data.boosted_path} · Gewicht ×${e.data.weight||1} · drand #${e.data.beacon_round}`;status('Tipp diesmal nicht gezogen · nächstes Los folgt','living');return}if(e.code==='beacon_unavailable'){status('Randomness Beacon nicht erreichbar · kein Tipp ausgewertet','living');return}if(e.code==='lottery_full'){status('Losfenster voll · nächster Versuch folgt','living');return}status(e.message||'Tipp fehlgeschlagen')}finally{submitting=false}}
|
||||
async function submit(){if(!task||submitting)return;if(!wsReady()){scheduleWSReconnect();nextGuessAt=Date.now()+1000;status('Live-Verbindung wird wiederhergestellt …');return}submitting=true;status('signiert Tipp …','thinking');try{const current=await api('/api/tasks/current');if(current.id!==task.id){await showLanding();return}task=current;syncBeacon();if(task.paused){status('Task pausiert');nextGuessAt=0;return}const seq=task.next_seq,guess=await deterministicGuess(task.id,task.public_seed,cid,seq,task.range_bits),beaconOn=Number(task.beacon_hunt_enabled||0)===1&&Number(task.guess_lottery_max_accepted||0)>0,msg=beaconOn?`guess|${task.id}|${seq}|${guess}|${beaconPath}`:`guess|${task.id}|${seq}|${guess}`,signature=await sign(msg);if(Number(task.guess_lottery_max_accepted||0)>0)status(beaconOn?`Beacon ${beaconPath} committed · wartet auf externen Draw`:`wartet auf Losziehung · max. ${Number(task.guess_lottery_max_accepted).toLocaleString('de-DE')} Tipps / ${Number(task.guess_lottery_window_sec||60)}s`,'thinking');const correct=await api(`/api/tasks/${task.id}/guess`,{method:'POST',body:JSON.stringify({seq,guess,signature,beacon_path:beaconOn?beaconPath:''})});nextGuessAt=Date.now()+Math.max(1,task.client_submit_interval_sec)*1000;if(beaconOn){try{const d=await api(`/api/public/beacon/${encodeURIComponent(task.id)}/latest`);$('beaconLast').textContent=`Dein Pfad ${beaconPath} · Boost ${d.boosted_path} · drand #${d.beacon_round}`;status(correct?'Treffer — Task gelöst!':`Draw ${d.boosted_path} · Tipp gezogen & geprüft`,correct?'researching':'living')}catch{status(correct?'Treffer — Task gelöst!':'Beacon-Tipp gezogen & geprüft',correct?'researching':'living')}}else status(correct?'Treffer — Task gelöst!':Number(task.guess_lottery_max_accepted||0)>0?'Tipp gezogen & geprüft':'Tipp akzeptiert',correct?'researching':'living');lastPlaceRefresh=0;await Promise.all([refreshMe(),refreshLeaders()]);if(correct)setTimeout(()=>showLanding(),1300)}catch(e){if(e.status===409){await recover409(e);return}nextGuessAt=Date.now()+Math.max(2,task?.client_submit_interval_sec||11)*1000;if(e.code==='identity_warmup'){const wait=Math.max(1,Number(e.data?.retry_after_sec||task?.client_submit_interval_sec||15));nextGuessAt=Date.now()+wait*1000;status(`Anti-Sybil-Wartezeit · ${wait}s`,'living');return}if(e.code==='lottery_not_selected'){if(e.data?.boosted_path&&$('beaconLast'))$('beaconLast').textContent=`Dein Pfad ${e.data.chosen_path} · Boost ${e.data.boosted_path} · Gewicht ×${e.data.weight||1} · drand #${e.data.beacon_round}`;status('Tipp diesmal nicht gezogen · nächstes Los folgt','living');lastPlaceRefresh=0;await refreshMe();return}if(e.code==='beacon_unavailable'){status('Randomness Beacon nicht erreichbar · kein Tipp ausgewertet','living');return}if(e.code==='lottery_full'){status('Losfenster voll · nächster Versuch folgt','living');return}status(e.message||'Tipp fehlgeschlagen')}finally{submitting=false}}
|
||||
function openWS(force=false){if(!task||$('taskLanding').classList.contains('visible'))return;clearWSReconnect();if(ws&&(ws.readyState===WebSocket.OPEN||ws.readyState===WebSocket.CONNECTING)){if(!force)return;const old=ws;old._plannedClose=true;try{old.close(1000,'reconnect')}catch{}}const proto=location.protocol==='https:'?'wss':'ws',mx=+$('maxnodes').value||task.default_max_nodes||2000,socket=new WebSocket(`${proto}://${location.host}/api/ws?max_nodes=${encodeURIComponent(mx)}`,['neuralhunt.v1',`nh-auth.${getToken()}`]);ws=socket;socket.onopen=()=>{if(ws!==socket)return;wsBackoff=500;status(task?.paused?'Task pausiert':'verbunden')};socket.onmessage=async ev=>{if(ws!==socket)return;const e=JSON.parse(ev.data);if(e.type==='snapshot'){points=Array.isArray(e.data)?e.data:[];render()}else if(e.type==='point'){const p=e.data,i=points.findIndex(x=>x.client_id===p.client_id);if(i<0)points.push(p);else points[i]=p;render()}else if(e.type==='points'){for(const p of (Array.isArray(e.data)?e.data:[])){const i=points.findIndex(x=>x.client_id===p.client_id);if(i<0)points.push(p);else points[i]=p}render()}else if(e.type==='task_changed'){await refreshTaskConfig(true);await Promise.all([refreshMe(),refreshLeaders()])}else if(e.type==='task_completed'){status('Task abgeschlossen — Folge-Task ist bereit','researching');setTimeout(()=>showLanding(),1300)}};socket.onclose=e=>{if(ws===socket)ws=null;if(!socket._plannedClose&&task&&!$('taskLanding').classList.contains('visible')){status('Live-Verbindung unterbrochen · verbinde automatisch neu …');scheduleWSReconnect()}};socket.onerror=()=>{if(!socket._plannedClose&&ws===socket)status('WebSocket-Fehler · Reconnect folgt automatisch')}}
|
||||
async function enterTask(taskID){if(landingBusy)return;landingBusy=true;try{await stopTaskSession();task=await api('/api/tasks/select',{method:'POST',body:JSON.stringify({task_id:taskID})});$('taskLanding').classList.remove('visible');syncBeacon();$('taskid').textContent=task.display_name?shortID(task.display_name,16):shortID(task.id.slice(-12),12);let max=+$('maxnodes').value||clamp(Number(task.default_max_nodes||2000),100,25000);if(document.documentElement.classList.contains('mobile-mode'))max=Math.min(max,1000);$('maxnodes').value=max;$('maxnodesvalue').textContent=max.toLocaleString('de-DE');points=await api(`/api/tasks/${task.id}/points?limit=${Math.min(10000,Math.max(300,max*3))}`);points=Array.isArray(points)?points:[];render();await Promise.all([refreshMe(),refreshLeaders()]);syncMobile();openWS();status(task.paused?'Task pausiert':`${task.range_bits} Bit · verbunden`);nextGuessAt=task.paused?0:Date.now()+Math.max(1,task.client_submit_interval_sec)*1000;scheduler=setInterval(()=>{if(task&&!task.paused&&nextGuessAt&&Date.now()>=nextGuessAt)submit()},300);countdownTimer=setInterval(countdown,250);countdown()}catch(e){status(e.message||'Task konnte nicht gestartet werden');$('taskLanding').classList.add('visible')}finally{landingBusy=false}}
|
||||
try{const id=await ensureIdentity();cid=await clientId(id.publicJwk);$('cid').textContent=cid;$('landingCid').textContent=cid;await ensureSession();syncMobile();await Promise.all([refreshLeaders()]);await showLanding()}catch(e){status(e.message||'Startfehler');$('taskCards').innerHTML=`<div class="task-card-empty glass"><b>Startfehler</b><span>${esc(e.message||'')}</span></div>`}
|
||||
@@ -455,16 +455,16 @@ function placeShell(){
|
||||
<div class="place-kicker">DEIN PLACE WALLET</div>
|
||||
<div class="place-balance"><small>PUNKTE</small><strong id="placePoints">0</strong><span><b id="placePixelsAvailable">0</b> Pixel verfügbar</span></div>
|
||||
<div class="place-wallet-grid"><span><small>ERHALTEN</small><b id="placeEarned">0</b></span><span><small>AUSGEGEBEN</small><b id="placeSpent">0</b></span><span><small>PLACEMENTS</small><b id="placePlacements">0</b></span><span><small>AKTUELL DEINE</small><b id="placeOwned">0</b></span></div>
|
||||
<div class="place-economy"><span class="eyebrow">ÖKONOMIE</span><p id="placeEconomy">Hunt-Fortschritt erzeugt Place-Punkte.</p><div><b id="placeRate">—</b><small>Punkte pro +1,00 %</small></div><div><b id="placeCost">—</b><small>Punkte pro Pixel</small></div></div>
|
||||
<div class="place-economy"><span class="eyebrow">ÖKONOMIE</span><p id="placeEconomy">Hunt-Aktivität erzeugt Place-Punkte.</p><div><b id="placeRate">—</b><small>Punkte pro +1,00 %</small></div><div><b id="placeDrawRate">—</b><small>Basis je gezogenem Tipp</small></div><div><b id="placeTimeRate">—</b><small>Punkte je Aktivzeit-Intervall</small></div><div><b id="placeCost">—</b><small>Punkte pro Pixel</small></div></div>
|
||||
<div class="place-workers"><div class="place-section-head"><span>VERKNÜPFTE WORKER</span><small id="placeWorkerCount">0</small></div><div id="placeWorkerList" class="place-worker-list"><span class="empty small">keine</span></div></div>
|
||||
<div class="place-earnings"><div class="place-section-head"><span>LETZTE FORTSCHRITTE</span><small>HUNT → PLACE</small></div><div id="placeEarnings" class="place-earning-list"></div></div>
|
||||
<div class="place-earnings"><div class="place-section-head"><span>LETZTE REWARDS</span><small>HUNT → PLACE</small></div><div id="placeEarnings" class="place-earning-list"></div></div>
|
||||
</aside>
|
||||
<section class="place-stage glass">
|
||||
<div class="place-canvas-toolbar"><div><span class="eyebrow">NEURAL PLACE</span><b id="placeCoords">Pixel wählen</b></div><div class="place-canvas-stats"><span><b id="placeFilled">0</b> belegt</span><span><b id="placeTotal">0</b> Placements</span><span><b id="placeParticipants">0</b> Artists</span></div><div class="place-tools"><button id="placeZoomOut">−</button><button id="placeFit">FIT</button><button id="placeZoomIn">+</button></div></div>
|
||||
<div id="placeViewport" class="place-viewport"><canvas id="placeCanvas"></canvas><div id="placeHover" class="place-hover hidden"></div><div class="place-hint">Scroll = Zoom · Ziehen = Pan · Klick = Pixel wählen</div></div>
|
||||
<div class="place-compose"><div><span class="eyebrow">FARBE</span><div id="placePalette" class="place-palette"></div></div><div class="place-selection"><small>AUSWAHL</small><b id="placeSelection">—</b><span id="placeSelectionOwner">Wähle einen Pixel auf der Leinwand.</span></div><button id="placeSubmit" disabled>PIXEL SETZEN <span id="placeSubmitCost"></span></button></div>
|
||||
</section>
|
||||
<aside class="place-side place-right glass"><div class="place-section-head"><span>LIVE FEED</span><small id="placeRevision">REV 0</small></div><div id="placeFeed" class="place-feed"></div><div class="place-rules"><span class="eyebrow">REGEL</span><p>Kein Zeit-Cooldown. Die knappe Ressource ist echter Hunt-Fortschritt. Eigene Fortschritte und Fortschritte deiner verknüpften Worker landen im selben Wallet.</p></div></aside>
|
||||
<aside class="place-side place-right glass"><div class="place-section-head"><span>LIVE FEED</span><small id="placeRevision">REV 0</small></div><div id="placeFeed" class="place-feed"></div><div class="place-rules"><span class="eyebrow">REGEL</span><p>Kein Pixel-Cooldown. Punkte kommen aus Fortschritt, tatsächlich gezogenen Tipps und aktiver Hunt-Zeit. Beacon-Boosts können den Draw-Bonus erhöhen. Verknüpfte Worker zahlen in dasselbe Owner-Wallet ein.</p></div></aside>
|
||||
</main>
|
||||
<div id="placeToast" class="place-toast hidden"></div>`;
|
||||
}
|
||||
@@ -477,7 +477,7 @@ async function runPlace(){
|
||||
const fmtPoints=v=>Number(v||0).toLocaleString('de-DE',{maximumFractionDigits:3});
|
||||
const toast=(text,bad=false)=>{const el=$('placeToast');el.textContent=text;el.classList.toggle('bad',!!bad);el.classList.remove('hidden');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.classList.add('hidden'),2600)};
|
||||
async function ensurePlaceSession(){const id=await ensureIdentity();cid=await clientId(id.publicJwk);if(!getToken()){const x=await loginIdentity();setToken(x.token);return}try{const x=await api('/api/place/me');if(x?.wallet?.client_id!==cid)throw Object.assign(new Error('identity mismatch'),{status:401})}catch(e){if(e.status!==401)throw e;clearToken();const x=await loginIdentity();setToken(x.token)}}
|
||||
function renderWallet(){if(!wallet||!config)return;$('placePoints').textContent=fmtPoints(wallet.balance_points);$('placePixelsAvailable').textContent=Number(wallet.available_pixels||0).toLocaleString('de-DE');$('placeEarned').textContent=fmtPoints(wallet.earned_points);$('placeSpent').textContent=fmtPoints(wallet.spent_points);$('placePlacements').textContent=Number(wallet.placements||0).toLocaleString('de-DE');$('placeOwned').textContent=Number(wallet.owned_pixels||0).toLocaleString('de-DE');$('placeRate').textContent=fmtPoints(config.points_per_score);$('placeCost').textContent=fmtPoints(config.pixel_cost);$('placeSubmitCost').textContent=`· ${fmtPoints(config.pixel_cost)} P`;$('placeEconomy').textContent=`+1,00 % Hunt-Fortschritt = ${fmtPoints(config.points_per_score)} Punkte. Ein Pixel kostet ${fmtPoints(config.pixel_cost)} Punkte.`;const workers=wallet.linked_workers||[];$('placeWorkerCount').textContent=workers.length;$('placeWorkerList').innerHTML=workers.length?workers.map(x=>`<code title="${esc(x)}">${esc(shortID(x,18))}</code>`).join(''):'<span class="empty small">Keine Worker verknüpft</span>';$('placeEarnings').innerHTML=(wallet.recent_earnings||[]).map(e=>{const worker=e.source_client_id!==cid;return `<div class="place-earning"><span class="place-source ${worker?'worker':''}">${worker?'WORKER':'DU'}</span><div><b>+${fmtPoints(e.points)} P</b><small>${Number(e.old_score||0).toFixed(2)} → ${Number(e.new_score||0).toFixed(2)} % · ${esc(shortID(e.task_id?.slice(-10),10))}</small></div></div>`}).join('')||'<span class="empty small">Noch kein vergüteter Fortschritt.</span>';updateSubmit()}
|
||||
function renderWallet(){if(!wallet||!config)return;$('placePoints').textContent=fmtPoints(wallet.balance_points);$('placePixelsAvailable').textContent=Number(wallet.available_pixels||0).toLocaleString('de-DE');$('placeEarned').textContent=fmtPoints(wallet.earned_points);$('placeSpent').textContent=fmtPoints(wallet.spent_points);$('placePlacements').textContent=Number(wallet.placements||0).toLocaleString('de-DE');$('placeOwned').textContent=Number(wallet.owned_pixels||0).toLocaleString('de-DE');$('placeRate').textContent=fmtPoints(config.points_per_score);$('placeDrawRate').textContent=fmtPoints(config.draw_points);$('placeTimeRate').textContent=`${fmtPoints(config.time_points)} / ${Number(config.time_interval_sec||0)}s`;$('placeCost').textContent=fmtPoints(config.pixel_cost);$('placeSubmitCost').textContent=`· ${fmtPoints(config.pixel_cost)} P`;const beaconText=config.draw_beacon_multiplier?` · Beacon-Boost multipliziert Draw-Punkte mit dem Pfadgewicht`:'',timeText=Number(config.time_points||0)>0?` · ${fmtPoints(config.time_points)} P je ${Number(config.time_interval_sec||0)}s aktive Hunt-Zeit`:'';$('placeEconomy').textContent=`+1,00 % Fortschritt = ${fmtPoints(config.points_per_score)} P · gezogen = ${fmtPoints(config.draw_points)} P Basis${beaconText}${timeText} · Pixel = ${fmtPoints(config.pixel_cost)} P.`;const workers=wallet.linked_workers||[];$('placeWorkerCount').textContent=workers.length;$('placeWorkerList').innerHTML=workers.length?workers.map(x=>`<code title="${esc(x)}">${esc(shortID(x,18))}</code>`).join(''):'<span class="empty small">Keine Worker verknüpft</span>';$('placeEarnings').innerHTML=(wallet.recent_earnings||[]).map(e=>{const worker=e.source_client_id!==cid,kind=String(e.kind||'progress'),tag=kind==='progress'?'FORTSCHRITT':kind==='draw'?'GEZOGEN':kind==='time'?'AKTIVZEIT':kind==='admin'?'ADMIN':kind.toUpperCase();let detail='';if(kind==='progress')detail=`${Number(e.old_score||0).toFixed(2)} → ${Number(e.new_score||0).toFixed(2)} %`;else if(kind==='draw')detail=`Lotterie gezogen${Number(e.multiplier||1)>1?` · Beacon ×${Number(e.multiplier).toLocaleString('de-DE')}`:''}`;else if(kind==='time')detail=`${Number(e.units||1)} Aktivzeit-Intervall${Number(e.units||1)===1?'':'e'}`;else detail=String(e.detail||'Manuelle Gutschrift');if(e.task_id)detail+=` · ${shortID(String(e.task_id).slice(-10),10)}`;return `<div class="place-earning"><span class="place-source ${worker?'worker':''}">${esc(tag)}</span><div><b>+${fmtPoints(e.points)} P</b><small>${esc(detail)}${worker?` · Worker ${esc(shortID(e.source_client_id,12))}`:''}</small></div></div>`}).join('')||'<span class="empty small">Noch keine Place-Rewards.</span>';updateSubmit()}
|
||||
async function refreshWallet(){try{const x=await api('/api/place/me');config=x.config||config;wallet=x.wallet||wallet;renderWallet()}catch(e){if(e.status===401)return;}}
|
||||
function renderPalette(){$('placePalette').innerHTML=palette.map((c,i)=>`<button class="place-swatch ${i===selectedColor?'active':''}" data-place-color="${i}" style="--swatch:${c}" aria-label="Farbe ${i+1}"><i></i></button>`).join('');document.querySelectorAll('[data-place-color]').forEach(b=>b.onclick=()=>{selectedColor=Number(b.dataset.placeColor);localStorage.setItem('neuralhunt.place.color',String(selectedColor));renderPalette();draw();updateSubmit()})}
|
||||
function initBoard(list){pixels=new Uint8Array(width*height);pixels.fill(255);meta=new Map();filledCount=0;off.width=width;off.height=height;offctx.imageSmoothingEnabled=false;offctx.fillStyle='#f5f5f5';offctx.fillRect(0,0,width,height);for(const p of (list||[]))applyPixel(p,false);fit();draw()}
|
||||
@@ -522,12 +522,13 @@ function adminShell(){
|
||||
|
||||
async function runAdmin(){
|
||||
let adminOK=false;try{await api('/api/admin/session',{},true);adminOK=true}catch{}if(!adminOK){adminLoginShell();$('adminlogin').onclick=async()=>{try{const r=await fetch('/api/admin/login',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',body:JSON.stringify({user:$('adminuser').value,password:$('adminpass').value})});if(!r.ok)throw new Error('Login fehlgeschlagen');location.reload()}catch(e){$('adminerr').textContent=e.message}};return}
|
||||
adminShell();const map=new NeuralMap($('adminmap'),{admin:true,onStats:s=>{if($('adminrender'))$('adminrender').textContent=s.render.toLocaleString('de-DE');if($('adminfps'))$('adminfps').textContent=s.fps}});const adminGuessFlashKey='neuralhunt.adminGuessFlash.v1';let adminGuessFlash=false;try{adminGuessFlash=localStorage.getItem(adminGuessFlashKey)==='1'}catch{}map.guessFlashEnabled=adminGuessFlash;setActive('adminGuessFlash',adminGuessFlash);const adminDraftKey='neuralhunt.adminDraft.v2';let draft={};try{draft=JSON.parse(localStorage.getItem(adminDraftKey)||'{}')||{}}catch{draft={}};let tasks=[],selected=null,points=[],settings=null,actions=[],providers=null,artifactUsage=null,clients=[],poll=null,tab=['runtime','task','artifact'].includes(draft.tab)?draft.tab:'runtime',loading=false,adminDropWatchIDs=[],adminDropWatchTimer=null;
|
||||
adminShell();const map=new NeuralMap($('adminmap'),{admin:true,onStats:s=>{if($('adminrender'))$('adminrender').textContent=s.render.toLocaleString('de-DE');if($('adminfps'))$('adminfps').textContent=s.fps}});const adminGuessFlashKey='neuralhunt.adminGuessFlash.v1';let adminGuessFlash=false;try{adminGuessFlash=localStorage.getItem(adminGuessFlashKey)==='1'}catch{}map.guessFlashEnabled=adminGuessFlash;setActive('adminGuessFlash',adminGuessFlash);const adminDraftKey='neuralhunt.adminDraft.v2';let draft={};try{draft=JSON.parse(localStorage.getItem(adminDraftKey)||'{}')||{}}catch{draft={}};let tasks=[],selected=null,points=[],settings=null,actions=[],providers=null,artifactUsage=null,clients=[],placeBonusEvents=[],poll=null,tab=['runtime','task','artifact'].includes(draft.tab)?draft.tab:'runtime',loading=false,adminDropWatchIDs=[],adminDropWatchTimer=null;
|
||||
const setAdminPanel=name=>{const grid=$('adminGrid');grid.classList.remove('mobile-show-map','mobile-show-tasks','mobile-show-control');grid.classList.add(`mobile-show-${name}`);document.querySelectorAll('[data-admin-panel]').forEach(b=>b.classList.toggle('active',b.dataset.adminPanel===name));if(name==='map')setTimeout(()=>map.resize(),30)};
|
||||
const syncAdminMobile=()=>{const on=document.documentElement.classList.contains('mobile-mode');$('adminMobileToggle').textContent=mobileButtonLabel();setActive('adminMobileToggle',on);if(on){map.eco=true;map.labels=false;map.edges=false;map.zoom=Math.min(map.zoom,.94);setActive('adminEco',true);setActive('adminEdges',false);if(+$('adminmaxnodes').value>1500){$('adminmaxnodes').value=1500;$('adminmaxvalue').textContent='1.500'}}else{map.eco=false;setActive('adminEco',false)}map.resize()};
|
||||
$('adminMobileToggle').onclick=()=>toggleMobileMode();document.querySelectorAll('[data-admin-panel]').forEach(b=>b.onclick=()=>setAdminPanel(b.dataset.adminPanel));window.addEventListener('neuralhunt-mobile-mode',syncAdminMobile);syncAdminMobile();
|
||||
const runtimeKeys=['guess_min_interval_sec','client_submit_interval_sec','guess_lottery_window_sec','guess_lottery_max_accepted','beacon_hunt_enabled','beacon_bonus_weight','sybil_pow_bits','sybil_warmup_sec','openai_max_calls_1h','openai_max_calls_24h','openai_max_cost_24h_usd','openai_budget_reserve_usd','task_range_bits','active_task_count','presence_ttl_sec','default_max_nodes','public_score_precision','place_enabled','place_width','place_height','place_points_per_score','place_pixel_cost'];
|
||||
const labels={guess_min_interval_sec:'Server-Tippintervall (s)',client_submit_interval_sec:'Client-Tippintervall (s)',guess_lottery_window_sec:'Lotterie-Zeitfenster (s)',guess_lottery_max_accepted:'Max. gezogene Tipps je Task/Fenster (0 = aus)',beacon_hunt_enabled:'Beacon Hunt (0 = aus, 1 = an)',beacon_bonus_weight:'Beacon Treffer-Gewicht (1–10)',sybil_pow_bits:'Anti-Sybil Proof-of-Work (Bit, 0 = aus)',sybil_warmup_sec:'Neue Identität Wartezeit (s)',openai_max_calls_1h:'OpenAI max. Bild-Calls / 1h (0 = aus)',openai_max_calls_24h:'OpenAI max. Bild-Calls / 24h (0 = aus)',openai_max_cost_24h_usd:'OpenAI max. geschätzte Kosten / 24h USD (0 = aus)',openai_budget_reserve_usd:'OpenAI Sicherheitsreserve pro nächstem Call USD',task_range_bits:'Default Zahlenraum (Bit)',active_task_count:'Parallele aktive Tasks',presence_ttl_sec:'Presence TTL (s)',default_max_nodes:'Default Max Nodes',public_score_precision:'Öffentliche Score-Präzision',place_enabled:'Neural Place (0 = aus, 1 = an)',place_width:'Place Breite (Pixel)',place_height:'Place Höhe (Pixel)',place_points_per_score:'Place-Punkte je +1,00 Score',place_pixel_cost:'Place-Kosten je Pixel'};
|
||||
const runtimeKeys=['guess_min_interval_sec','client_submit_interval_sec','guess_lottery_window_sec','guess_lottery_max_accepted','beacon_hunt_enabled','beacon_bonus_weight','sybil_pow_bits','sybil_warmup_sec','openai_max_calls_1h','openai_max_calls_24h','openai_max_cost_24h_usd','openai_budget_reserve_usd','task_range_bits','active_task_count','presence_ttl_sec','default_max_nodes','public_score_precision','place_enabled','place_width','place_height','place_points_per_score','place_draw_points','place_draw_beacon_multiplier','place_time_points','place_time_interval_sec','place_time_max_gap_sec','place_pixel_cost'];
|
||||
const labels={guess_min_interval_sec:'Server-Tippintervall (s)',client_submit_interval_sec:'Client-Tippintervall (s)',guess_lottery_window_sec:'Lotterie-Zeitfenster (s)',guess_lottery_max_accepted:'Max. gezogene Tipps je Task/Fenster (0 = aus)',beacon_hunt_enabled:'Beacon Hunt (0 = aus, 1 = an)',beacon_bonus_weight:'Beacon Treffer-Gewicht (1–10)',sybil_pow_bits:'Anti-Sybil Proof-of-Work (Bit, 0 = aus)',sybil_warmup_sec:'Neue Identität Wartezeit (s)',openai_max_calls_1h:'OpenAI max. Bild-Calls / 1h (0 = aus)',openai_max_calls_24h:'OpenAI max. Bild-Calls / 24h (0 = aus)',openai_max_cost_24h_usd:'OpenAI max. geschätzte Kosten / 24h USD (0 = aus)',openai_budget_reserve_usd:'OpenAI Sicherheitsreserve pro nächstem Call USD',task_range_bits:'Default Zahlenraum (Bit)',active_task_count:'Parallele aktive Tasks',presence_ttl_sec:'Presence TTL (s)',default_max_nodes:'Default Max Nodes',public_score_precision:'Öffentliche Score-Präzision',place_enabled:'Neural Place (0 = aus, 1 = an)',place_width:'Place Breite (Pixel)',place_height:'Place Höhe (Pixel)',place_points_per_score:'Place-Punkte je +1,00 Score',place_draw_points:'Place-Punkte je gezogenem Tipp (0 = aus)',place_draw_beacon_multiplier:'Draw-Bonus × Beacon-Pfadgewicht (0/1)',place_time_points:'Place-Punkte je Aktivzeit-Intervall (0 = aus)',place_time_interval_sec:'Aktivzeit-Intervall (s)',place_time_max_gap_sec:'Max. Aktivitätslücke, die zählt (s)',place_pixel_cost:'Place-Kosten je Pixel'};
|
||||
const fmtPlacePoints=v=>Number(v||0).toLocaleString('de-DE',{maximumFractionDigits:3});
|
||||
const msg=s=>$('adminstatus').textContent=s;
|
||||
const renderAdminDropWatch=items=>{const host=$('adminDropResult');if(!host)return;const rows=Array.isArray(items)?items:[];if(!rows.length){host.textContent=adminDropWatchIDs.length?'Queue-Einträge werden gesucht …':'';return}const order={error:0,generating:1,pending:2,ready:3};rows.sort((a,b)=>(order[a.artifact_status]??9)-(order[b.artifact_status]??9));host.innerHTML=`<div class="admin-drop-watch">${rows.map(x=>{const st=String(x.artifact_status||'unknown'),err=String(x.artifact_error||'').trim(),id=String(x.task_id||'');let info=st==='pending'?'wartet auf Artifact-Worker':st==='generating'?'Bild wird gerade erzeugt':st==='ready'?'Collectible fertig':st==='error'?(err||'Generierung fehlgeschlagen'):st;return `<div class="admin-drop-watch-row ${esc(st)}"><b>${esc(st.toUpperCase())}</b><code>${esc(id.slice(-14))}</code>${(x.artifact_rarity||x.artifact_rarity_override)?`<small>${esc(x.artifact_rarity||x.artifact_rarity_override)}</small>`:''}<span title="${esc(err)}">${esc(info.length>180?info.slice(0,179)+'…':info)}</span>${st==='ready'?`<button type="button" data-drop-open="${esc(id)}">ÖFFNEN</button>`:''}</div>`}).join('')}</div>`;host.querySelectorAll('[data-drop-open]').forEach(b=>b.onclick=()=>openAdminFile(b.dataset.dropOpen,'artifact'))};
|
||||
const refreshAdminDropWatch=async()=>{clearTimeout(adminDropWatchTimer);adminDropWatchTimer=null;if(!adminDropWatchIDs.length)return;try{const rows=await api(`/api/admin/artifacts/status?ids=${encodeURIComponent(adminDropWatchIDs.join(','))}`,{},true);renderAdminDropWatch(rows);const active=(rows||[]).some(x=>x.artifact_status==='pending'||x.artifact_status==='generating');if(active){adminDropWatchTimer=setTimeout(refreshAdminDropWatch,2000)}else{adminDropWatchIDs=[];setTimeout(()=>load(true,false),0)}}catch(e){const host=$('adminDropResult');if(host)host.textContent='Queue-Status konnte nicht geladen werden: '+(e.message||e);adminDropWatchTimer=setTimeout(refreshAdminDropWatch,4000)}};
|
||||
@@ -547,11 +548,17 @@ async function runAdmin(){
|
||||
async function openAdminFile(taskID,kind){const popup=window.open('','_blank');try{const r=await fetch(`/api/admin/tasks/${encodeURIComponent(taskID)}/${kind}`,{credentials:'same-origin'});if(!r.ok)throw new Error(await responseError(r,'Datei konnte nicht geöffnet werden'));const blob=await r.blob(),u=URL.createObjectURL(blob);if(popup)popup.location=u;else{const a=document.createElement('a');a.href=u;a.target='_blank';a.click()}setTimeout(()=>URL.revokeObjectURL(u),60000)}catch(e){if(popup)popup.close();msg(e.message)}}
|
||||
function renderTasks(){tasks=Array.isArray(tasks)?tasks:[];$('taskCount').textContent=tasks.length;$('tasks').innerHTML=tasks.length?tasks.map(t=>{const aerr=String(t.artifact_error||'').trim();return `<button data-id="${esc(t.id)}" class="${selected?.id===t.id?'selected':''}"><span><b>${esc(t.display_name||t.id.slice(-12))}</b><small>${esc(t.id.slice(-12))} · ${fmtDate(t.created_at)}</small></span><span><em class="task-state ${esc(t.status)}">${t.paused?'PAUSED':esc(t.status)}</em><small>${t.range_bits} Bit · rev ${t.revision} · ${Number(t.point_count||0).toLocaleString('de-DE')} Clients · ${Number(t.guess_count||0).toLocaleString('de-DE')} Tipps</small></span><span class="artifact-state-wrap"><b class="artifact-state ${esc(t.artifact_status||'')}">${esc(t.artifact_status||'—')}</b>${aerr?`<small class="danger artifact-error-summary" title="${esc(aerr)}">${esc(aerr.length>76?aerr.slice(0,75)+'…':aerr)}</small>`:`<small>${t.artifact_origin==='admin_drop'?`ADMIN DROP${(t.artifact_rarity||t.artifact_rarity_override)?' · '+esc(t.artifact_rarity||t.artifact_rarity_override):''}`:t.retire_after_completion?'AUSLAUFEND':t.parent_task_id?`↳ ${esc(String(t.parent_task_id).slice(-7))}`:'ROOT'}</small>`}<small class="artifactLinks">${t.artifact_uri?`<span data-artifact-task="${esc(t.id)}">Bild</span> · <span data-manifest-task="${esc(t.id)}">Manifest</span>`:''}</small></span></button>`}).join(''):'<div class="empty">Keine Tasks</div>';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=`<div class="control-section"><div class="section-title">GLOBAL RUNTIME</div>${runtimeKeys.map(k=>`<label><span>${esc(labels[k])}</span><input type="number" step="any" data-setting="${k}" value="${settings?.[k]??''}"></label>`).join('')}<p class="small">Die Tipp-Lotterie gilt <b>getrennt pro aktivem Task</b>. Bei einem Wert > 0 werden alle gültigen Tipps eines Zeitfensters gesammelt und am Fensterende exakt bis zur eingestellten Menge zufällig gezogen. Nicht gezogene Tipps werden nicht gegen das Ziel geprüft und verändern den Score nicht; ihre Sequenz wird trotzdem verbraucht. <b>0 = Lotterie aus</b>. Änderungen greifen für neu beginnende Fenster.</p><p class="small"><b>Beacon Hunt:</b> Optional wählen Spieler PULSE, FLUX oder ORBIT. Der erste öffentliche drand-Round nach Fensterschluss bestimmt reproduzierbar den Boost-Pfad und die gewichtete Ziehung. Alle Reveal-Daten werden gespeichert und über die Public API nachvollziehbar gemacht.</p><p class="small"><b>Anti-Sybil:</b> Neue Browser-Identitäten lösen einmalig einen Proof-of-Work und warten anschließend die konfigurierte Warmup-Zeit, bevor Tipps gewertet werden. Das erhöht die Kosten massenhafter Identitätserstellung, ersetzt aber keine externe echte Identitätsprüfung.</p><p class="small"><b>OpenAI Circuit Breaker:</b> Vor jedem Bild-Call werden rollierende 1h-/24h-Call-Limits und das geschätzte 24h-Kostenbudget geprüft. Bei Überschreitung bleibt die Gewinnerkarte in der Queue und wird später erneut versucht.</p><p class="small"><b>Neural Place:</b> Bei aktiviertem Place-Modus ist der Score ein Prozentwert von 0–100. Jede echte Highscore-Steigerung erzeugt <code>Place-Punkte je +1,00 Score</code>; verknüpfte Hosted Worker schreiben dieselben Punkte dem Owner-Wallet gut. Ein Placement – auch das Übermalen eines belegten Feldes – kostet <code>Place-Kosten je Pixel</code>. Breite/Höhe können erweitert oder nur so weit verkleinert werden, dass kein vorhandener Pixel abgeschnitten wird; offene Place-Seiten sollten nach einer Größenänderung neu geladen werden.</p><p class="small">Die übrigen Defaults gelten global. Task-spezifische Intervalle und Zahlenräume steuerst du im Tab „Task Actions“.</p></div>
|
||||
$('settingfields').innerHTML=`<div class="control-section"><div class="section-title">GLOBAL RUNTIME</div>${runtimeKeys.map(k=>`<label><span>${esc(labels[k])}</span><input type="number" step="any" data-setting="${k}" value="${settings?.[k]??''}"></label>`).join('')}<p class="small">Die Tipp-Lotterie gilt <b>getrennt pro aktivem Task</b>. Bei einem Wert > 0 werden alle gültigen Tipps eines Zeitfensters gesammelt und am Fensterende exakt bis zur eingestellten Menge zufällig gezogen. Nicht gezogene Tipps werden nicht gegen das Ziel geprüft und verändern den Score nicht; ihre Sequenz wird trotzdem verbraucht. <b>0 = Lotterie aus</b>. Änderungen greifen für neu beginnende Fenster.</p><p class="small"><b>Beacon Hunt:</b> Optional wählen Spieler PULSE, FLUX oder ORBIT. Der erste öffentliche drand-Round nach Fensterschluss bestimmt reproduzierbar den Boost-Pfad und die gewichtete Ziehung. Alle Reveal-Daten werden gespeichert und über die Public API nachvollziehbar gemacht.</p><p class="small"><b>Anti-Sybil:</b> Neue Browser-Identitäten lösen einmalig einen Proof-of-Work und warten anschließend die konfigurierte Warmup-Zeit, bevor Tipps gewertet werden. Das erhöht die Kosten massenhafter Identitätserstellung, ersetzt aber keine externe echte Identitätsprüfung.</p><p class="small"><b>OpenAI Circuit Breaker:</b> Vor jedem Bild-Call werden rollierende 1h-/24h-Call-Limits und das geschätzte 24h-Kostenbudget geprüft. Bei Überschreitung bleibt die Gewinnerkarte in der Queue und wird später erneut versucht.</p><p class="small"><b>Neural Place:</b> Punkte können aus drei automatischen Quellen kommen: echter Highscore-Fortschritt, ein <b>tatsächlich gezogener</b> Lotterie-Tipp und aktive Hunt-Zeit. Der Draw-Bonus kann mit demselben Beacon-Pfadgewicht multipliziert werden, das auch die Ziehung beeinflusst. Aktivzeit zählt nur zwischen gültigen signierten Requests; Pausen über <code>Max. Aktivitätslücke</code> werden nicht nachvergütet. Verknüpfte Hosted Worker schreiben alle diese Rewards dem Owner-Wallet gut. Ein Placement – auch das Übermalen – kostet <code>Place-Kosten je Pixel</code>.</p><p class="small">Die übrigen Defaults gelten global. Task-spezifische Intervalle und Zahlenräume steuerst du im Tab „Task Actions“.</p></div>
|
||||
<div class="control-section place-admin-grant"><div class="section-title">PLACE-PUNKTE MANUELL VERGEBEN</div><p class="small">Gutschriften werden sofort dem Place-Wallet gutgeschrieben und dauerhaft auditiert. Wird eine verknüpfte Worker-ID gewählt, landet die Gutschrift konsistent im Wallet des Owners.</p>
|
||||
<label class="wide"><span>Ziel Client-/Worker-ID</span><input id="placeGrantTarget" data-draft="placeGrantTarget" list="placeGrantClientIDs" placeholder="Client-ID"></label><datalist id="placeGrantClientIDs">${clients.map(c=>`<option value="${esc(c.client_id)}">${fmtPlacePoints(c.place_balance_points||0)} P${c.place_owner_client_id&&c.place_owner_client_id!==c.client_id?` · → Owner ${esc(shortID(c.place_owner_client_id,12))}`:''} · ${Number(c.wins||0)} Wins · ${Number(c.nft_count||0)} NFTs</option>`).join('')}</datalist>
|
||||
<label><span>Punkte</span><input id="placeGrantPoints" data-draft="placeGrantPoints" type="number" min="0.001" max="1000000000" step="0.001" value="100"></label><label class="wide"><span>Grund / Notiz</span><input id="placeGrantReason" data-draft="placeGrantReason" maxlength="500" placeholder="z.B. Event-Bonus, Support-Korrektur"></label>
|
||||
<div class="task-config-actions"><button id="grantPlacePoints">PUNKTE GUTSCHREIBEN</button></div><div class="section-title action-history-title">LETZTE PLACE-BONUS-EVENTS</div><div class="action-list">${placeBonusEvents.length?placeBonusEvents.slice(0,16).map(e=>`<div class="action-item done"><span><b>${esc(String(e.kind||'bonus').toUpperCase())} · +${fmtPlacePoints(e.points)} P</b><small>${fmtDate(e.created_at)} · ${esc(shortID(e.owner_client_id||'',16))}${e.source_client_id&&e.source_client_id!==e.owner_client_id?` ← Worker ${esc(shortID(e.source_client_id,14))}`:''}${Number(e.multiplier||1)>1?` · ×${Number(e.multiplier).toLocaleString('de-DE')}`:''}</small>${e.detail?`<small>${esc(e.detail)}</small>`:''}</span><em>DONE</em></div>`).join(''):'<div class="empty small">Noch keine Draw-, Aktivzeit- oder Admin-Boni.</div>'}</div>
|
||||
</div>
|
||||
<div class="control-section profile-cleanup"><div class="section-title">ALTE PROFILE BEREINIGEN</div><p class="small">Löscht ausschließlich Accounts, die <b>seit mindestens X Zeit inaktiv</b>, aktuell <b>nicht verbunden</b>, <b>niemals Gewinner</b> eines Tasks und <b>keine Neural-Place-Teilnehmer</b> sind. Gewinner, verknüpfte Worker/Owner und Identitäten mit verdientem Place-Wert oder Placements werden unabhängig vom Alter geschützt. Zugehörige Hunt-Punkte, Unlocks und Task-Auswahl werden bei löschbaren Profilen mit entfernt.</p>
|
||||
<div class="cleanup-controls"><label><span>Inaktiv seit mindestens</span><input id="profileCleanupValue" data-draft="profileCleanupValue" type="number" min="1" step="1" value="30"></label><label><span>Einheit</span><select id="profileCleanupUnit" data-draft="profileCleanupUnit"><option value="hours">Stunden</option><option value="days" selected>Tage</option><option value="weeks">Wochen</option></select></label></div>
|
||||
<div class="cleanup-actions"><button id="previewProfileCleanup">PRÜFEN</button><button id="runProfileCleanup" class="danger-button">PROFILE LÖSCHEN</button></div><div id="profileCleanupResult" class="cleanup-result">Noch nicht geprüft.</div>
|
||||
</div>`;$('savesettings').style.display='inline-block';$('ensuretasks').style.display='inline-block';
|
||||
$('grantPlacePoints').onclick=async()=>{const target=String($('placeGrantTarget')?.value||'').trim(),points=Number($('placeGrantPoints')?.value||0),reason=String($('placeGrantReason')?.value||'').trim();if(!target){msg('Ziel Client-/Worker-ID fehlt');return}if(!Number.isFinite(points)||points<=0){msg('Punkte müssen größer als 0 sein');return}const b=$('grantPlacePoints');b.disabled=true;try{const out=await api('/api/admin/place/points',{method:'POST',body:JSON.stringify({target_client_id:target,points,reason})},true);const credited=out?.event?.owner_client_id||target;msg(`+${fmtPlacePoints(out?.event?.points||points)} Place-Punkte an ${shortID(credited,18)} gutgeschrieben`);clearDraftKeys(['placeGrantTarget','placeGrantPoints','placeGrantReason']);await load(true,true)}catch(e){msg(e.message)}finally{if(document.body.contains(b))b.disabled=false}};
|
||||
const cleanupSeconds=()=>{const v=Math.max(1,Number($('profileCleanupValue')?.value||0)),unit=$('profileCleanupUnit')?.value||'days',factor=unit==='hours'?3600:unit==='weeks'?7*86400:86400;return Math.round(v*factor)};
|
||||
const showCleanup=p=>{const box=$('profileCleanupResult');if(!box)return;const eligible=Number(p?.eligible||0),wins=Number(p?.protected_winners||0),place=Number(p?.protected_place||0),online=Number(p?.protected_connected||0),cutoff=p?.cutoff_ms?fmtDate(p.cutoff_ms):'—';box.innerHTML=`<b>${eligible.toLocaleString('de-DE')} löschbar</b><span>· ${wins.toLocaleString('de-DE')} alte Gewinner · ${place.toLocaleString('de-DE')} Place-Teilnehmer · ${online.toLocaleString('de-DE')} aktuell verbundene Accounts geschützt</span><small>Grenze: letzte Aktivität vor ${esc(cutoff)}</small>`;};
|
||||
const previewCleanup=async()=>{try{captureDraft();const seconds=cleanupSeconds(),p=await api(`/api/admin/profiles/cleanup-preview?inactive_for_seconds=${seconds}`,{},true);showCleanup(p);return p}catch(e){msg(e.message);throw e}};
|
||||
@@ -636,7 +643,7 @@ async function runAdmin(){
|
||||
function renderSettings(){setActive('tabRuntime',tab==='runtime');setActive('tabTask',tab==='task');setActive('tabArtifact',tab==='artifact');if(tab==='runtime')renderRuntime();else if(tab==='artifact')renderArtifact();else renderTaskControl();restoreDraft()}
|
||||
function renderMap(){points=Array.isArray(points)?points:[];const min=Number($('adminMinScore')?.value||0),needle=String($('adminClientFilter')?.value||'').trim().toLowerCase(),filtered=points.filter(p=>Number(p.score||0)>=min&&(!needle||String(p.client_id||'').toLowerCase().includes(needle)));$('adminpoints').textContent=`${filtered.length.toLocaleString('de-DE')} / ${points.length.toLocaleString('de-DE')}`;$('selectedtask').textContent=selected?.id||'Task wählen';$('adminbits').textContent=selected?.range_bits??'—';$('winner').textContent=selected?.winner_client_id?`Winner ${shortID(selected.winner_client_id,12)}`:selected?.paused?'PAUSED':'';map.update(filtered,'',+$('adminmaxnodes').value)}
|
||||
async function refreshSelected(refreshControls=false){if(!selected){points=[];actions=[];renderMap();if(refreshControls)renderSettings();return}try{const [ps,as]=await Promise.all([api(`/api/admin/tasks/${selected.id}/points?limit=100000`,{},true),api(`/api/admin/tasks/${selected.id}/actions?limit=100`,{},true)]);points=Array.isArray(ps)?ps:[];actions=Array.isArray(as)?as:[];renderMap();if(refreshControls)renderSettings()}catch(e){msg(e.message)}}
|
||||
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,cl]=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),api('/api/admin/clients?limit=300',{},true)]);tasks=Array.isArray(ts)?ts:[];clients=Array.isArray(cl)?cl:[];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 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,cl,pbe]=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),api('/api/admin/clients?limit=300',{},true),api('/api/admin/place/earnings?limit=50',{},true)]);tasks=Array.isArray(ts)?ts:[];clients=Array.isArray(cl)?cl:[];placeBonusEvents=Array.isArray(pbe)?pbe:[];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)};$('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();
|
||||
|
||||
Reference in New Issue
Block a user