Files
glpi-neural-brain/internal/web/static/source-agents.js
jbergner 440423c5b6
All checks were successful
release-tag / release-image (push) Successful in 2m43s
RC-3
2026-08-09 11:29:13 +02:00

89 lines
19 KiB
JavaScript

(() => {
const $ = id => document.getElementById(id);
let snapshot = {agents: [], tasks: [], inbox: {}, compute: {}, controller: {}, brain_url: ''};
let controllerJobs = [];
$('apiKey').value = sessionStorage.getItem('brain_api_key') || '';
$('saveKey').onclick = () => { sessionStorage.setItem('brain_api_key', $('apiKey').value.trim()); toast('API-Key für diese Session gespeichert.'); loadAll(); };
function headers() { const h = {'Content-Type':'application/json'}; const key=sessionStorage.getItem('brain_api_key')||''; if(key) h.Authorization=`Bearer ${key}`; return h; }
async function api(path,opt={}) { const res=await fetch(path,{...opt,headers:{...headers(),...(opt.headers||{})}}); const data=await res.json().catch(()=>({})); if(!res.ok) throw new Error(data.error||`HTTP ${res.status}`); return data; }
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),4200)}
function esc(v){return String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
function when(v){if(!v)return'nie';const d=new Date(v);if(Number.isNaN(d.getTime())||d.getUTCFullYear()<=1970)return'nie';return d.toLocaleString()}
function browserUsesLoopback(){return['localhost','127.0.0.1','::1'].includes(location.hostname)}
function lines(v){return Array.isArray(v)?v.join('\n'):''}
function parseLines(v){return String(v||'').split(/\r?\n/).map(x=>x.trim()).filter(Boolean)}
function pretty(v,max=900){let s='';try{s=JSON.stringify(v??{},null,2)}catch{s=String(v??'')}return s.length>max?s.slice(0,max)+'…':s}
function parseJSONField(id){const raw=$(id).value.trim();if(!raw)return{};try{return JSON.parse(raw)}catch{throw new Error(`${id}: ungültiges JSON`)}}
async function loadAll(){
try{
const [raw,jobsRaw]=await Promise.all([api('/api/source-agents'),api('/api/controller/jobs?limit=150')]);
snapshot={...raw,agents:Array.isArray(raw.agents)?raw.agents.filter(Boolean):[],tasks:Array.isArray(raw.tasks)?raw.tasks.filter(Boolean):[],inbox:raw.inbox&&typeof raw.inbox==='object'?raw.inbox:{},compute:raw.compute&&typeof raw.compute==='object'?raw.compute:{},controller:raw.controller&&typeof raw.controller==='object'?raw.controller:{}};
controllerJobs=Array.isArray(jobsRaw.jobs)?jobsRaw.jobs:[];
render(); await loadInbox();
}catch(e){toast(e.message)}
}
function render(){
const agents=snapshot.agents||[],tasks=snapshot.tasks||[],inbox=snapshot.inbox||{},controller=snapshot.controller||{},policy=controller.policy||{};
const controllerAgents=agents.filter(a=>(a.capabilities||[]).includes('docker_controller'));
$('agentCount').textContent=agents.length;$('controllerCount').textContent=controllerAgents.length;$('controllerJobs').textContent=controllerJobs.filter(j=>['queued','claimed'].includes(j.status)).length;$('candidateCount').textContent=inbox.candidate||0;$('receivedCount').textContent=(inbox.received||0)+(inbox.processing||0)+(inbox.security_queued||0)+(inbox.security_processing||0);
renderPolicy(policy); renderAgentSelects(agents); renderAgents(agents,tasks); renderControllerInventory(controllerAgents); renderProfiles(Array.isArray(controller.profiles)?controller.profiles:[]); renderControllerJobs(); renderBrainURLWarning(); wireActions();
}
function renderPolicy(p){
$('controllerEnabled').checked=!!p.enabled;$('controllerAutonomous').checked=!!p.autonomous_enabled;$('controllerDryRun').checked=p.dry_run!==false;$('controllerDestructive').checked=!!p.allow_destructive;
$('controllerMaxJobs').value=p.max_concurrent_jobs||1;$('controllerMaxDuration').value=p.max_job_duration||'10m';$('controllerImages').value=lines(p.allowed_images||['curlimages/curl:']);$('controllerComposeRoots').value=lines(p.allowed_compose_roots||[]);$('controllerProtectedContainers').value=lines(p.protected_containers||['brain','*brain*','source-agent','*source-agent*']);$('controllerProtectedNetworks').value=lines(p.protected_networks||[]);$('controllerProtectedVolumes').value=lines(p.protected_volumes||[]);
const st=$('controllerMasterState');st.textContent=p.enabled?'AKTIV':'GESTOPPT';st.className='master-state '+(p.enabled?'on':'off');
}
function renderAgentSelects(agents){
const options=agents.map(a=>`<option value="${esc(a.id)}">${esc(a.name)} · ${esc(a.id)}</option>`).join('');$('taskAgent').innerHTML=options;$('profileAgent').innerHTML='<option value="">beliebiger Controller</option>'+options;$('jobAgent').innerHTML='<option value="">beliebiger Controller</option>'+options;
}
function renderBrainURLWarning(){const bw=$('brainURLWarning');if(!snapshot.brain_url){const hint=browserUsesLoopback()&&location.protocol==='http:'?` Für einen separaten Agent wäre häufig <code>http://host.docker.internal:${esc(location.port||'8090')}</code> erreichbar.`:'';bw.classList.remove('hidden');bw.innerHTML=`<b>BRAIN_PUBLIC_URL ist nicht gesetzt.</b><p>Prüfe die vom Agent erreichbare Adresse.${hint}</p>`}else{bw.classList.add('hidden');bw.innerHTML=''}}
function renderAgents(agents,tasks){
$('agents').innerHTML=agents.length?agents.map(a=>{const own=tasks.filter(t=>t.agent_id===a.id),last=a.last_seen?new Date(a.last_seen).getTime():0,online=last>0&&Date.now()-last<15*60*1000,caps=Array.isArray(a.capabilities)?a.capabilities:[],ctrl=a.controller||{};return `<div class="card"><div class="card-head"><div><b>${esc(a.name)}</b><small>${esc(a.id)}</small></div><span class="${online?'status-online':'status-offline'}">● ${online?'online':'offline'}</span></div><div class="cap-row">${caps.map(c=>`<span class="cap">${esc(c)}</span>`).join('')||'<span class="muted">keine Capability gemeldet</span>'}</div><small>Version ${esc(a.version||'—')} · Kontakt ${esc(when(a.last_seen))} · Tasks ${own.length}${caps.includes('docker_controller')?` · Docker ${ctrl.reachable?'erreichbar':'Fehler'} · ${ctrl.running||0}/${ctrl.containers||0} running`:''}</small>${a.last_error?`<p>${esc(a.last_error)}</p>`:''}<div class="actions"><button data-rotate="${esc(a.id)}">TOKEN ROTIEREN</button><button data-toggle="${esc(a.id)}" data-enabled="${a.enabled}">${a.enabled?'DEAKTIVIEREN':'AKTIVIEREN'}</button><button data-delete-agent="${esc(a.id)}">LÖSCHEN</button></div>${own.map(t=>`<div class="task"><b>${esc(t.name)} · ${esc(t.type)} · ${esc(t.poll_interval)}</b><span>${esc(t.url)}</span><div class="actions"><button data-delete-task="${esc(t.id)}">TASK LÖSCHEN</button></div></div>`).join('')}</div>`}).join(''):'<p class="muted">Noch kein Agent registriert.</p>';
}
function renderControllerInventory(agents){
$('controllerInventory').innerHTML=agents.length?agents.map(a=>{const c=a.controller||{},inv=c.inventory||{},containers=Array.isArray(inv.containers)?inv.containers:[],networks=Array.isArray(inv.networks)?inv.networks:[],volumes=Array.isArray(inv.volumes)?inv.volumes:[];return `<article class="inventory-card"><div class="card-head"><div><b>${esc(a.name)}</b><small>${esc(a.id)} · Docker ${esc(c.engine_version||'—')} · API ${esc(c.api_version||'—')}</small></div><span class="${c.reachable?'status-online':'status-offline'}">${c.reachable?'● erreichbar':'● nicht erreichbar'}</span></div><div class="mini-stats"><span><b>${containers.length}</b> Container</span><span><b>${c.running||0}</b> running</span><span><b>${c.unhealthy||0}</b> unhealthy</span><span><b>${networks.length}</b> Netzwerke</span><span><b>${volumes.length}</b> Volumes</span><span><b>${c.compose_available?'ja':'nein'}</b> Compose</span></div>${c.last_error?`<p class="error-text">${esc(c.last_error)}</p>`:''}<h3>Container</h3><div class="resource-list">${containers.slice(0,40).map(x=>{const name=(x.names||[]).map(n=>String(n).replace(/^\//,'')).join(', ')||x.id?.slice(0,12),running=x.state==='running';return `<div class="resource-row"><span class="dot ${x.health==='unhealthy'?'bad':running?'good':''}"></span><span class="resource-main"><b>${esc(name)}</b><small>${esc(x.image||'')} · ${esc(x.status||x.state||'')}</small></span><span class="resource-actions">${running?`<button data-controller-action="container_restart" data-controller-agent="${esc(a.id)}" data-controller-target="${esc(name)}">RESTART</button><button data-controller-action="container_stop" data-controller-agent="${esc(a.id)}" data-controller-target="${esc(name)}">STOP</button>`:`<button data-controller-action="container_start" data-controller-agent="${esc(a.id)}" data-controller-target="${esc(name)}">START</button>`}</span></div>`}).join('')}</div><details><summary>Netzwerke (${networks.length})</summary><div class="resource-list compact">${networks.slice(0,30).map(n=>`<div class="resource-row"><span class="resource-main"><b>${esc(n.name)}</b><small>${esc(n.driver||'')} · ${esc(n.scope||'')}</small></span><button data-controller-action="network_remove" data-controller-agent="${esc(a.id)}" data-controller-target="${esc(n.name)}">REMOVE</button></div>`).join('')}</div></details><details><summary>Volumes (${volumes.length})</summary><div class="resource-list compact">${volumes.slice(0,30).map(v=>`<div class="resource-row"><span class="resource-main"><b>${esc(v.name)}</b><small>${esc(v.driver||'')} · ${esc(v.scope||'')}</small></span><button data-controller-action="volume_remove" data-controller-agent="${esc(a.id)}" data-controller-target="${esc(v.name)}">REMOVE</button></div>`).join('')}</div></details></article>`}).join(''):'<p class="muted">Kein online Agent meldet <code>docker_controller</code>. Am Agent muss Docker.sock gemountet und <code>BRAIN_AGENT_DOCKER_CONTROLLER_ENABLED=true</code> gesetzt sein.</p>';
}
function renderProfiles(profiles){$('controllerProfiles').innerHTML=profiles.length?profiles.map(p=>`<div class="card"><div class="card-head"><div><b>${esc(p.name)}</b><small>${esc(p.id)} · ${esc(p.purpose)} · ${esc(p.kind)}</small></div><span class="${p.enabled?'status-online':'status-offline'}">${p.enabled?(p.autonomous?'AUTONOM':'MANUELL'):'DEAKTIVIERT'}</span></div><pre class="compact-pre">${esc(pretty(p.config,500))}</pre><small>Agent ${esc(p.agent_id||'beliebig')} · letzter Lauf ${esc(when(p.last_run_at))}</small><div class="actions"><button data-run-profile="${esc(p.id)}" ${p.enabled?'':'disabled'}>JETZT AUSFÜHREN</button><button data-toggle-profile="${esc(p.id)}" data-profile-enabled="${p.enabled}">${p.enabled?'DEAKTIVIEREN':'AKTIVIEREN'}</button><button data-delete-profile="${esc(p.id)}">LÖSCHEN</button></div></div>`).join(''):'<p class="muted">Keine Controller-Profile angelegt.</p>'}
function renderControllerJobs(){
$('controllerJobRows').innerHTML=controllerJobs.length?controllerJobs.map(j=>`<tr><td><span class="pill ${esc(j.status)}">${esc(j.status)}</span>${j.autonomous?'<br><span class="muted">autonom</span>':''}${j.dry_run?'<br><span class="muted">dry-run</span>':''}</td><td><b>${esc(j.kind)}</b><br><span class="muted">${esc(j.purpose||'operations')}</span></td><td>${esc(j.claimed_by||j.agent_id||'beliebig')}<br><span class="muted">${esc(j.profile_id||'—')}</span></td><td><details><summary>Parameter</summary><pre class="compact-pre">${esc(pretty(j.parameters))}</pre></details>${j.result&&Object.keys(j.result).length?`<details><summary>Ergebnis</summary><pre class="compact-pre">${esc(pretty(j.result))}</pre></details>`:''}${j.error?`<span class="error-text">${esc(j.error)}</span>`:''}</td><td>${esc(when(j.created_at))}<br><span class="muted">${esc(when(j.completed_at))}</span></td><td>${['queued','claimed'].includes(j.status)?`<button data-cancel-job="${esc(j.id)}">ABBRECHEN</button>`:''}</td></tr>`).join(''):'<tr><td colspan="6" class="muted">Noch keine Controller-Jobs.</td></tr>'
}
function wireActions(){
document.querySelectorAll('[data-rotate]').forEach(b=>b.onclick=async()=>{try{const d=await api(`/api/source-agents/${b.dataset.rotate}/rotate-token`,{method:'POST'});showToken(b.dataset.rotate,d.token,d.brain_url);await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-toggle]').forEach(b=>b.onclick=async()=>{try{await api(`/api/source-agents/${b.dataset.toggle}`,{method:'PATCH',body:JSON.stringify({enabled:b.dataset.enabled!=='true'})});await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-delete-agent]').forEach(b=>b.onclick=async()=>{if(!confirm('Agent inklusive Tasks löschen?'))return;try{await api(`/api/source-agents/${b.dataset.deleteAgent}`,{method:'DELETE'});await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-delete-task]').forEach(b=>b.onclick=async()=>{try{await api(`/api/source-tasks/${b.dataset.deleteTask}`,{method:'DELETE'});await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-run-profile]').forEach(b=>b.onclick=async()=>{try{await api(`/api/controller/profiles/${b.dataset.runProfile}/run`,{method:'POST',body:'{}'});toast('Controller-Profil eingeplant.');await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-delete-profile]').forEach(b=>b.onclick=async()=>{if(!confirm('Controller-Profil löschen?'))return;try{await api(`/api/controller/profiles/${b.dataset.deleteProfile}`,{method:'DELETE'});await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-toggle-profile]').forEach(b=>b.onclick=async()=>{const p=(snapshot.controller?.profiles||[]).find(x=>x.id===b.dataset.toggleProfile);if(!p)return;try{await api(`/api/controller/profiles/${p.id}`,{method:'PUT',body:JSON.stringify({...p,enabled:b.dataset.profileEnabled!=='true'})});await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-cancel-job]').forEach(b=>b.onclick=async()=>{try{await api(`/api/controller/jobs/${b.dataset.cancelJob}/cancel`,{method:'POST'});await loadAll()}catch(e){toast(e.message)}});
document.querySelectorAll('[data-controller-action]').forEach(b=>b.onclick=async()=>{const kind=b.dataset.controllerAction,target=b.dataset.controllerTarget,agent=b.dataset.controllerAgent;if(['container_stop','container_restart','network_remove','volume_remove'].includes(kind)&&!confirm(`${kind} für ${target} einplanen?`))return;const key=kind.startsWith('container_')?'container':kind.startsWith('network_')?'network':'volume';try{await api('/api/controller/jobs',{method:'POST',body:JSON.stringify({agent_id:agent,kind,purpose:'operations',parameters:{[key]:target}})});toast(`${kind} eingeplant.`);await loadAll()}catch(e){toast(e.message)}});
}
$('saveControllerPolicy').onclick=async()=>{try{const body={enabled:$('controllerEnabled').checked,autonomous_enabled:$('controllerAutonomous').checked,dry_run:$('controllerDryRun').checked,allow_destructive:$('controllerDestructive').checked,max_concurrent_jobs:Number($('controllerMaxJobs').value)||1,max_job_duration:$('controllerMaxDuration').value.trim()||'10m',allowed_images:parseLines($('controllerImages').value),allowed_compose_roots:parseLines($('controllerComposeRoots').value),protected_containers:parseLines($('controllerProtectedContainers').value),protected_networks:parseLines($('controllerProtectedNetworks').value),protected_volumes:parseLines($('controllerProtectedVolumes').value)};await api('/api/controller/policy',{method:'PUT',body:JSON.stringify(body)});toast('Controller-Policy gespeichert.');await loadAll()}catch(e){toast(e.message)}};
$('controllerEmergencyStop').onclick=async()=>{if(!confirm('Docker-Steuerung zentral stoppen und alle wartenden Controller-Jobs abbrechen?'))return;try{const p=snapshot.controller?.policy||{};await api('/api/controller/policy',{method:'PUT',body:JSON.stringify({...p,enabled:false,autonomous_enabled:false})});toast('Docker-Controller gestoppt.');await loadAll()}catch(e){toast(e.message)}};
$('createProfile').onclick=async()=>{try{const body={name:$('profileName').value.trim(),agent_id:$('profileAgent').value,purpose:$('profilePurpose').value,kind:$('profileKind').value,enabled:true,autonomous:$('profileAutonomous').checked,config:parseJSONField('profileConfig')};await api('/api/controller/profiles',{method:'POST',body:JSON.stringify(body)});$('profileName').value='';$('profileConfig').value='';$('profileAutonomous').checked=false;toast('Controller-Profil gespeichert.');await loadAll()}catch(e){toast(e.message)}};
$('createControllerJob').onclick=async()=>{try{const body={agent_id:$('jobAgent').value,kind:$('jobKind').value,purpose:'operations',dry_run:$('jobDryRun').checked,parameters:parseJSONField('jobParams')};await api('/api/controller/jobs',{method:'POST',body:JSON.stringify(body)});toast('Controller-Job eingeplant.');await loadAll()}catch(e){toast(e.message)}};
function showToken(id,token,brainURL=''){let brain=(brainURL||snapshot.brain_url||'').trim(),note='';if(!brain){if(browserUsesLoopback()&&location.protocol==='http:'){brain=`http://host.docker.internal:${location.port||'8090'}`;note='\n\nBRAIN_PUBLIC_URL prüfen.'}else{brain=location.origin}}$('tokenOutput').textContent=`Token nur jetzt kopieren:\n${token}\n\nAgent-ENV:\nBRAIN_MODE=agent\nBRAIN_AGENT_BRAIN_URL=${brain}\nBRAIN_AGENT_ID=${id}\nBRAIN_AGENT_TOKEN=${token}\nBRAIN_AGENT_COMPUTE_ENABLED=true\n\nController optional:\nBRAIN_AGENT_DOCKER_CONTROLLER_ENABLED=true\nBRAIN_AGENT_DOCKER_SOCKET=/var/run/docker.sock${note}`;$('tokenOutput').classList.remove('hidden')}
$('createAgent').onclick=async()=>{try{const d=await api('/api/source-agents',{method:'POST',body:JSON.stringify({id:$('agentID').value.trim(),name:$('agentName').value.trim()})});showToken(d.agent.id,d.token,d.brain_url);$('agentName').value='';$('agentID').value='';await loadAll()}catch(e){toast(e.message)}};
$('createTask').onclick=async()=>{const agent=$('taskAgent').value;if(!agent)return toast('Zuerst einen Agent anlegen.');const task={name:$('taskName').value.trim(),type:$('taskType').value,url:$('taskURL').value.trim(),enabled:true,poll_interval:$('taskInterval').value.trim(),categories:$('taskCategories').value.split(',').map(x=>x.trim()).filter(Boolean),max_items:Number($('taskMaxItems').value)||20,config:{refetch_seen:$('taskRefetchSeen').checked?'true':'false',security_proactive:$('taskSecurityProactive').value||'auto'}};try{await api(`/api/source-agents/${encodeURIComponent(agent)}/tasks`,{method:'POST',body:JSON.stringify(task)});$('taskName').value='';$('taskURL').value='';await loadAll();toast('Polling-Aufgabe gespeichert.')}catch(e){toast(e.message)}};
async function loadInbox(){try{const status=$('inboxFilter').value,d=await api(`/api/source-inbox?limit=100${status?`&status=${encodeURIComponent(status)}`:''}`);$('inbox').innerHTML=(d.documents||[]).map(x=>{const m=x.metadata&&typeof x.metadata==='object'?x.metadata:{},priority=Number(m.priority_score??x.relevance??0),semantic=Number(m.semantic_similarity??x.relevance??0),fresh=Number(m.freshness_score??0),signal=Number(m.event_signal_score??0);return `<tr><td><span class="pill ${esc(x.status)}">${esc(x.status)}</span></td><td><a href="${esc(x.document.canonical_url||x.document.url)}" target="_blank" rel="noreferrer">${esc(x.document.title)}</a><br><span class="muted">${esc(x.document.source_name||x.document.source_base_url||'')}</span></td><td><b>${(priority*100).toFixed(0)}%</b><br><span class="muted">KB ${(semantic*100).toFixed(0)}% · frisch ${(fresh*100).toFixed(0)}% · Signal ${(signal*100).toFixed(0)}%</span></td><td>${esc(x.agent_id)}<br><span class="muted">${esc(x.task_id)}</span></td><td>${esc(when(x.received_at))}</td></tr>`}).join('')}catch(e){toast(e.message)}}
$('inboxFilter').onchange=loadInbox;$('reload').onclick=loadAll;loadAll();setInterval(loadAll,15000);
})();