(() => {
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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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=>``).join('');$('taskAgent').innerHTML=options;$('profileAgent').innerHTML=''+options;$('jobAgent').innerHTML=''+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 http://host.docker.internal:${esc(location.port||'8090')} erreichbar.`:'';bw.classList.remove('hidden');bw.innerHTML=`BRAIN_PUBLIC_URL ist nicht gesetzt.
Prüfe die vom Agent erreichbare Adresse.${hint}
`}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 `${esc(a.name)}${esc(a.id)}
● ${online?'online':'offline'}${caps.map(c=>`${esc(c)}`).join('')||'keine Capability gemeldet'}
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`:''}${a.last_error?`
${esc(a.last_error)}
`:''}
${own.map(t=>`
${esc(t.name)} · ${esc(t.type)} · ${esc(t.poll_interval)}${esc(t.url)} `).join('')}
`}).join(''):'Noch kein Agent registriert.
';
}
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 `${esc(a.name)}${esc(a.id)} · Docker ${esc(c.engine_version||'—')} · API ${esc(c.api_version||'—')}
${c.reachable?'● erreichbar':'● nicht erreichbar'}${containers.length} Container${c.running||0} running${c.unhealthy||0} unhealthy${networks.length} Netzwerke${volumes.length} Volumes${c.compose_available?'ja':'nein'} Compose
${c.last_error?`${esc(c.last_error)}
`:''}Container
${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 `
${esc(name)}${esc(x.image||'')} · ${esc(x.status||x.state||'')}${running?``:``}
`}).join('')}
Netzwerke (${networks.length})
${networks.slice(0,30).map(n=>`
${esc(n.name)}${esc(n.driver||'')} · ${esc(n.scope||'')}
`).join('')}
Volumes (${volumes.length})
${volumes.slice(0,30).map(v=>`
${esc(v.name)}${esc(v.driver||'')} · ${esc(v.scope||'')}
`).join('')}
`}).join(''):'Kein online Agent meldet docker_controller. Am Agent muss Docker.sock gemountet und BRAIN_AGENT_DOCKER_CONTROLLER_ENABLED=true gesetzt sein.
';
}
function renderProfiles(profiles){$('controllerProfiles').innerHTML=profiles.length?profiles.map(p=>`${esc(p.name)}${esc(p.id)} · ${esc(p.purpose)} · ${esc(p.kind)}
${p.enabled?(p.autonomous?'AUTONOM':'MANUELL'):'DEAKTIVIERT'}${esc(pretty(p.config,500))}Agent ${esc(p.agent_id||'beliebig')} · letzter Lauf ${esc(when(p.last_run_at))} `).join(''):'Keine Controller-Profile angelegt.
'}
function renderControllerJobs(){
$('controllerJobRows').innerHTML=controllerJobs.length?controllerJobs.map(j=>`${esc(j.status)}${j.autonomous?' autonom':''}${j.dry_run?' dry-run':''} | ${esc(j.kind)} ${esc(j.purpose||'operations')} | ${esc(j.claimed_by||j.agent_id||'beliebig')} ${esc(j.profile_id||'—')} | Parameter${esc(pretty(j.parameters))} ${j.result&&Object.keys(j.result).length?`Ergebnis${esc(pretty(j.result))} `:''}${j.error?`${esc(j.error)}`:''} | ${esc(when(j.created_at))} ${esc(when(j.completed_at))} | ${['queued','claimed'].includes(j.status)?``:''} |
`).join(''):'| Noch keine Controller-Jobs. |
'
}
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 `| ${esc(x.status)} | ${esc(x.document.title)} ${esc(x.document.source_name||x.document.source_base_url||'')} | ${(priority*100).toFixed(0)}% KB ${(semantic*100).toFixed(0)}% · frisch ${(fresh*100).toFixed(0)}% · Signal ${(signal*100).toFixed(0)}% | ${esc(x.agent_id)} ${esc(x.task_id)} | ${esc(when(x.received_at))} |
`}).join('')}catch(e){toast(e.message)}}
$('inboxFilter').onchange=loadInbox;$('reload').onclick=loadAll;loadAll();setInterval(loadAll,15000);
})();