@@ -0,0 +1,17 @@
|
||||
# Neural Hunt V4.2.8.1 – Admin Drop Queue Fix
|
||||
|
||||
This patch improves the admin NFT gift/drop path introduced in V4.2.8.
|
||||
|
||||
## Changes
|
||||
|
||||
- Admin NFT drops now wake the in-process artifact worker immediately after the database transaction commits. The 3-second polling ticker remains as a fallback.
|
||||
- The Server Admin UI tracks the exact synthetic drop task IDs independently from the task-list status filter and shows `PENDING`, `GENERATING`, `READY`, or `ERROR` live.
|
||||
- A completed drop gets an `ÖFFNEN` button directly in the drop status area.
|
||||
- Circuit-breaker deferrals and provider/style errors are visible in the live drop status instead of looking like a button that did nothing.
|
||||
- Random admin drops whose inherited custom style file no longer exists fall back to the bundled default style. Normal winner cards keep the stricter behavior.
|
||||
- The server logs every queued admin drop with owner, count and task IDs.
|
||||
- New admin-only endpoint: `GET /api/admin/artifacts/status?id=<task>&id=<task>` (or `?ids=a,b`) for up to 20 queue items.
|
||||
|
||||
No database migration is required.
|
||||
|
||||
Only the server image needs to be rebuilt.
|
||||
Regular → Executable
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -113,6 +114,10 @@ func (w *Worker) openAI(ctx context.Context, cfg settings.Runtime, x win, prompt
|
||||
return imageResult{}, err
|
||||
}
|
||||
styleRef, err := w.loadStyleReference(x.StyleReference)
|
||||
if err != nil && strings.EqualFold(strings.TrimSpace(x.Origin), "admin_drop") {
|
||||
log.Printf("artifact worker admin drop %s: template style %q unavailable (%v); falling back to bundled default style", x.ID, x.StyleReference, err)
|
||||
styleRef, err = w.loadStyleReference("")
|
||||
}
|
||||
if err != nil {
|
||||
return imageResult{}, err
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type Worker struct {
|
||||
publicBase string
|
||||
settings *settings.Manager
|
||||
http *http.Client
|
||||
wake chan struct{}
|
||||
budgetHoldMu sync.Mutex
|
||||
budgetHoldUntil time.Time
|
||||
}
|
||||
@@ -51,6 +52,7 @@ func New(db *sql.DB, dir string, sm *settings.Manager) (*Worker, error) {
|
||||
publicBase: strings.TrimRight(os.Getenv("ARTIFACT_PUBLIC_BASE_URL"), "/"),
|
||||
settings: sm,
|
||||
http: &http.Client{Timeout: envDuration("ARTIFACT_HTTP_TIMEOUT", 4*time.Minute)},
|
||||
wake: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -86,10 +88,24 @@ func (w *Worker) Run(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
if err := w.one(ctx); err != nil {
|
||||
log.Printf("artifact worker: %v", err)
|
||||
}
|
||||
case <-w.wake:
|
||||
}
|
||||
if err := w.one(ctx); err != nil {
|
||||
log.Printf("artifact worker: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify wakes the artifact worker after an admin action has queued work. The
|
||||
// channel is deliberately coalescing: one wake-up is enough even if an admin
|
||||
// creates several drops at once. The normal ticker remains as a safety net.
|
||||
func (w *Worker) Notify() {
|
||||
if w == nil || w.wake == nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case w.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1168,6 +1168,72 @@ func (s *Store) TransferArtifact(ctx context.Context, taskID, targetClientID, re
|
||||
return from, nil
|
||||
}
|
||||
|
||||
// AdminArtifactQueueItem is a compact admin-only view of queued/generated
|
||||
// collectibles. It is intentionally independent from the task list filters so
|
||||
// an admin drop stays observable even when the UI is currently showing only
|
||||
// active hunt tasks.
|
||||
type AdminArtifactQueueItem struct {
|
||||
TaskID string `json:"task_id"`
|
||||
ArtifactOrigin string `json:"artifact_origin"`
|
||||
ArtifactStatus string `json:"artifact_status"`
|
||||
ArtifactError *string `json:"artifact_error,omitempty"`
|
||||
ArtifactURI *string `json:"artifact_uri,omitempty"`
|
||||
OwnerClientID string `json:"owner_client_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
}
|
||||
|
||||
func (s *Store) AdminArtifactQueueItems(ctx context.Context, ids []string) ([]AdminArtifactQueueItem, error) {
|
||||
clean := make([]string, 0, len(ids))
|
||||
seen := map[string]struct{}{}
|
||||
for _, id := range ids {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
clean = append(clean, id)
|
||||
if len(clean) >= 20 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return []AdminArtifactQueueItem{}, nil
|
||||
}
|
||||
marks := make([]string, len(clean))
|
||||
args := make([]any, len(clean))
|
||||
for i, id := range clean {
|
||||
marks[i] = "?"
|
||||
args[i] = id
|
||||
}
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT id,artifact_origin,artifact_status,artifact_error,artifact_uri,COALESCE(artifact_owner_client_id,winner_client_id,''),created_at
|
||||
FROM tasks WHERE id IN (`+strings.Join(marks, ",")+`) ORDER BY created_at`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]AdminArtifactQueueItem, 0, len(clean))
|
||||
for rows.Next() {
|
||||
var x AdminArtifactQueueItem
|
||||
var aerr, uri sql.NullString
|
||||
if err := rows.Scan(&x.TaskID, &x.ArtifactOrigin, &x.ArtifactStatus, &aerr, &uri, &x.OwnerClientID, &x.CreatedAtMS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if aerr.Valid {
|
||||
v := aerr.String
|
||||
x.ArtifactError = &v
|
||||
}
|
||||
if uri.Valid {
|
||||
v := uri.String
|
||||
x.ArtifactURI = &v
|
||||
}
|
||||
out = append(out, x)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// CreateAdminArtifactDrop queues a collectible for an existing client without
|
||||
// recording a game win. A random task is used as the art-direction template
|
||||
// when templateTaskID is empty; the new seed/id still randomize collection traits.
|
||||
|
||||
@@ -345,6 +345,7 @@ func (s *Server) Routes() http.Handler {
|
||||
r.Put("/api/admin/tasks/{id}/config", s.adminTaskConfigPut)
|
||||
r.Post("/api/admin/tasks/{id}/artifact/transfer", s.adminArtifactTransfer)
|
||||
r.Post("/api/admin/artifacts/drop", s.adminArtifactDrop)
|
||||
r.Get("/api/admin/artifacts/status", s.adminArtifactStatus)
|
||||
r.Post("/api/admin/tasks/{id}/actions", s.adminScheduleAction)
|
||||
r.Post("/api/admin/actions/{id}/cancel", s.adminCancelAction)
|
||||
r.Get("/api/admin/artifact/providers", s.adminArtifactProviders)
|
||||
@@ -1570,9 +1571,30 @@ func (s *Server) adminArtifactDrop(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
created = append(created, t.ID)
|
||||
}
|
||||
if s.artifactWorker != nil {
|
||||
s.artifactWorker.Notify()
|
||||
}
|
||||
log.Printf("admin artifact drop queued: owner=%s count=%d tasks=%s", strings.TrimSpace(in.TargetClientID), len(created), strings.Join(created, ","))
|
||||
jsonOut(w, 201, map[string]any{"ok": true, "created_task_ids": created, "artifact_status": "pending"})
|
||||
}
|
||||
|
||||
func (s *Server) adminArtifactStatus(w http.ResponseWriter, r *http.Request) {
|
||||
ids := append([]string(nil), r.URL.Query()["id"]...)
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("ids")); raw != "" {
|
||||
ids = append(ids, strings.Split(raw, ",")...)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
jsonOut(w, 400, map[string]string{"error": "at least one id is required"})
|
||||
return
|
||||
}
|
||||
items, err := s.store.AdminArtifactQueueItems(r.Context(), ids)
|
||||
if err != nil {
|
||||
jsonOut(w, 500, map[string]string{"error": "artifact status lookup failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
jsonOut(w, 200, items)
|
||||
}
|
||||
|
||||
func (s *Server) adminTaskConfigPut(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
var in struct {
|
||||
|
||||
Vendored
+7
-3
@@ -461,13 +461,16 @@ 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;
|
||||
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;
|
||||
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;
|
||||
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><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)}};
|
||||
const watchAdminDrops=ids=>{adminDropWatchIDs=(Array.isArray(ids)?ids:[]).map(String).filter(Boolean).slice(0,20);clearTimeout(adminDropWatchTimer);adminDropWatchTimer=null;if(adminDropWatchIDs.length)refreshAdminDropWatch()};
|
||||
let adminSignalWS=null,adminSignalTimer=null,adminSignalBackoff=500,adminSignalClosed=false;
|
||||
const closeAdminSignalWS=()=>{clearTimeout(adminSignalTimer);adminSignalTimer=null;if(adminSignalWS){adminSignalWS._plannedClose=true;try{adminSignalWS.close(1000,'disabled')}catch{}adminSignalWS=null}};
|
||||
const openAdminSignalWS=()=>{if(!adminGuessFlash||adminSignalClosed)return;if(adminSignalWS&&(adminSignalWS.readyState===WebSocket.OPEN||adminSignalWS.readyState===WebSocket.CONNECTING))return;const proto=location.protocol==='https:'?'wss':'ws',socket=new WebSocket(`${proto}://${location.host}/api/admin/ws`);adminSignalWS=socket;socket.onopen=()=>{if(adminSignalWS!==socket)return;adminSignalBackoff=500};socket.onmessage=ev=>{if(adminSignalWS!==socket||!adminGuessFlash)return;try{const e=JSON.parse(ev.data);if(e.type==='guess_signal'&&selected?.id===e.task_id)map.flashGuess(e.data)}catch{}};socket.onclose=()=>{if(adminSignalWS===socket)adminSignalWS=null;if(!socket._plannedClose&&adminGuessFlash&&!adminSignalClosed){clearTimeout(adminSignalTimer);adminSignalTimer=setTimeout(openAdminSignalWS,adminSignalBackoff);adminSignalBackoff=Math.min(10000,adminSignalBackoff*1.8)}}};
|
||||
@@ -511,8 +514,9 @@ async function runAdmin(){
|
||||
<div class="usage-table-wrap"><table class="usage-table"><thead><tr><th>Zeit</th><th>Typ</th><th>Modell</th><th>Tokens (Text + Bild → Output)</th><th>Kosten</th><th>Request</th></tr></thead><tbody id="artifactUsageRows">${usageRows}</tbody></table></div>
|
||||
<p class="small">Task-spezifische Style-Bilder und optionale kreative Vorgaben pflegst du im Tab <b>TASK ACTIONS</b>.</p></div>`;
|
||||
if(anchorReady){const img=$('anchorPreview');loadProtectedImage('/api/admin/artifact/character-anchor?ts='+Date.now(),img,true).catch(()=>{if(img)img.alt='Anchor konnte nicht geladen werden'})}
|
||||
if(adminDropWatchIDs.length)setTimeout(refreshAdminDropWatch,0);
|
||||
const create=$('createCharacterAnchor');if(create)create.onclick=async()=>{if(!confirm('RIFT Character Anchor jetzt einmalig erzeugen? Danach wird er absichtlich nicht automatisch überschrieben.'))return;create.disabled=true;create.textContent='ANCHOR WIRD ERZEUGT …';try{await api('/api/admin/artifact/character-anchor',{method:'POST'},true);msg('RIFT Character Anchor erzeugt und gesperrt');await load(true,true)}catch(e){msg(e.message);create.disabled=false;create.textContent='RIFT-ANCHOR JETZT ERZEUGEN'}};
|
||||
const drop=$('createAdminDrop');if(drop)drop.onclick=async()=>{const target=String($('dropTargetClient')?.value||'').trim(),count=Number($('dropCount')?.value||1),template=String($('dropTemplateTask')?.value||'').trim();if(!target){msg('Ziel Client-ID fehlt');return}if(!Number.isInteger(count)||count<1||count>20){msg('Anzahl muss 1–20 sein');return}if(!confirm(`${count} Admin-NFT${count===1?'':'s'} für ${target.slice(0,18)}… erzeugen?\n\nDies kann API-Kosten auslösen; der Circuit-Breaker bleibt aktiv.`))return;drop.disabled=true;drop.textContent='WIRD EINGEREIHT …';try{const r=await api('/api/admin/artifacts/drop',{method:'POST',body:JSON.stringify({target_client_id:target,template_task_id:template,count})},true);$('adminDropResult').textContent=`${(r.created_task_ids||[]).length} Collectible(s) pending · ${(r.created_task_ids||[]).map(x=>x.slice(-10)).join(', ')}`;msg('Admin NFT-Drop eingereiht');await load(true,false)}catch(e){msg(e.message)}finally{drop.disabled=false;drop.textContent='NFT-DROP IN QUEUE STELLEN'}};
|
||||
const drop=$('createAdminDrop');if(drop)drop.onclick=async()=>{const target=String($('dropTargetClient')?.value||'').trim(),count=Number($('dropCount')?.value||1),template=String($('dropTemplateTask')?.value||'').trim();if(!target){msg('Ziel Client-ID fehlt');return}if(!Number.isInteger(count)||count<1||count>20){msg('Anzahl muss 1–20 sein');return}if(!confirm(`${count} Admin-NFT${count===1?'':'s'} für ${target.slice(0,18)}… erzeugen?\n\nDies kann API-Kosten auslösen; der Circuit-Breaker bleibt aktiv.`))return;drop.disabled=true;drop.textContent='WIRD EINGEREIHT …';try{const r=await api('/api/admin/artifacts/drop',{method:'POST',body:JSON.stringify({target_client_id:target,template_task_id:template,count})},true);const ids=r.created_task_ids||[];$('adminDropResult').textContent=`${ids.length} Collectible(s) eingereiht · Status wird verfolgt …`;watchAdminDrops(ids);msg('Admin NFT-Drop eingereiht · Artifact-Worker wurde geweckt');await load(true,false)}catch(e){msg(e.message)}finally{drop.disabled=false;drop.textContent='NFT-DROP IN QUEUE STELLEN'}};
|
||||
}else{
|
||||
$('settingfields').innerHTML=`<div class="control-section"><div class="section-title">LEGACY ARTIFACT GENERATION</div><div class="provider-status">${['local','openai','comfyui','a1111'].map(k=>`<span class="${ps[k]?'ready':'off'}">${k.toUpperCase()} · ${ps[k]?'READY':'ENV FEHLT'}</span>`).join('')}</div>
|
||||
<label><span>Provider</span><select data-setting-string="artifact_provider"><option value="local">local</option><option value="openai">openai</option><option value="comfyui">comfyui</option><option value="a1111">a1111</option><option value="auto">auto fallback</option></select></label>
|
||||
@@ -577,7 +581,7 @@ async function runAdmin(){
|
||||
$('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',()=>{adminSignalClosed=true;closeAdminSignalWS();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();clearTimeout(adminDropWatchTimer);captureDraft();clearInterval(poll);window.removeEventListener('neuralhunt-mobile-mode',syncAdminMobile);map.destroy()},{once:true});
|
||||
}
|
||||
|
||||
applyMobileMode(mobileModeEnabled());
|
||||
|
||||
Vendored
+1
@@ -115,3 +115,4 @@ html.mobile-mode .reference-admin-box,html.mobile-mode .task-style-admin{grid-te
|
||||
.beacon-buttons button.active{border-color:#fff;box-shadow:0 0 18px rgba(255,255,255,.14);background:rgba(255,255,255,.13)}
|
||||
.beacon-choice>small{display:block;opacity:.68;line-height:1.35}
|
||||
.admin-drop-box,.ownership-box{margin-top:12px}.admin-drop-box #adminDropResult{margin-top:8px;color:#8edbb8}.task-facts{grid-template-columns:repeat(4,minmax(0,1fr))}@media(max-width:720px){.task-facts{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
.admin-drop-watch{display:grid;gap:5px;margin-top:8px}.admin-drop-watch-row{display:grid;grid-template-columns:78px 112px minmax(0,1fr) auto;gap:7px;align-items:center;padding:7px 8px;border:1px solid rgba(133,200,255,.10);border-radius:9px;background:rgba(255,255,255,.018);font-size:8px}.admin-drop-watch-row b{font-size:7px;letter-spacing:.09em}.admin-drop-watch-row code{font-size:7px;color:#8fbdcf}.admin-drop-watch-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#819dac}.admin-drop-watch-row.generating b{color:#8feeff}.admin-drop-watch-row.ready b{color:#7dffc3}.admin-drop-watch-row.error{border-color:rgba(255,95,136,.28)}.admin-drop-watch-row.error b,.admin-drop-watch-row.error span{color:#ff9bad}.admin-drop-watch-row button{padding:5px 7px}@media(max-width:720px){.admin-drop-watch-row{grid-template-columns:70px 1fr}.admin-drop-watch-row span{grid-column:1/-1;white-space:normal}.admin-drop-watch-row button{grid-column:1/-1}}
|
||||
|
||||
Reference in New Issue
Block a user