2026-06-09 00:41:27 +00:00
{% extends "base.html" %}
{% block extra_head %}
2026-06-09 10:41:13 +00:00
< script src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" ></ script >
2026-06-09 00:41:27 +00:00
< link rel = "stylesheet" href = "/static/css/hermes.css" />
2026-06-09 10:41:13 +00:00
< link rel = "stylesheet" href = "/static/css/herman-dashboard.css?v=13" />
2026-06-23 10:04:23 +00:00
< link rel = "stylesheet" href = "/static/css/agents-mesh.css?v=8" />
2026-06-09 10:41:13 +00:00
< link rel = "stylesheet" href = "/static/css/agent-characters.css?v=3" />
< script src = "/static/js/agent-avatars.js?v=3" ></ script >
2026-06-09 00:41:27 +00:00
{% endblock %}
{% block content %}
< div class = "hm-neo" x-data = "agentsSoulHub()" x-init = "init()" >
< header class = "hm-neo-header page-header" >
< h1 > Agents · Soul Registry</ h1 >
< p class = "hm-sub" > Animated team · duidelijke rollen · soul.md · live activiteit</ p >
</ header >
2026-06-09 10:41:13 +00:00
< div id = "viz-toolbar" class = "viz-toolbar" ></ div >
< div class = "hm-chart-panel page-viz-panel viz-mode-chart agents-viz-compact" style = "margin-bottom:1rem" >
< div class = "hm-chart-head" >< h4 > Agent activiteit (events)</ h4 >< small > 5 visualisatie-modi</ small ></ div >
< div class = "page-viz-body" >
< div class = "hm-chart-wrap page-viz-canvas" >< canvas id = "chart-agents-viz" ></ canvas ></ div >
< div id = "viz-agents-alt" class = "viz-alt" ></ div >
</ div >
</ div >
2026-06-09 00:41:27 +00:00
< div class = "vtabs-layout" style = "--vt-color:#ffd700" x-ref = "vtabsWrap" >
< nav class = "vtabs-nav" x-ref = "vtabsNav" :style = "'--vt-indicator-top:'+tabIndicatorTop+'px;--vt-indicator-h:'+tabIndicatorH+'px'" >
2026-06-23 10:04:23 +00:00
< div class = "vtabs-nav-header" >< strong > Agents</ strong ></ div >
2026-06-09 00:41:27 +00:00
< button class = "vtab-btn" :class = "{active: tab==='souls'}" @ click = "setTab('souls', $el)" >
< span class = "vtab-icon" > 🧬</ span >< span > Souls</ span >
</ button >
< button class = "vtab-btn" :class = "{active: tab==='mesh'}" @ click = "setTab('mesh', $el)" >
< span class = "vtab-icon" > 🕸️</ span >< span > Netwerk</ span >
</ button >
2026-06-23 10:04:23 +00:00
< button class = "vtab-btn" :class = "{active: tab==='terminals'}" @ click = "setTab('terminals', $el)" data-tab = "terminals" >
< span class = "vtab-icon" > 🖥️</ span >< span > Terminals</ span >
</ button >
2026-06-09 10:41:13 +00:00
< button class = "vtab-btn" :class = "{active: tab==='approvals'}" @ click = "setTab('approvals', $el)" >
< span class = "vtab-icon" > ✓</ span >< span > Goedkeuringen</ span >
< span class = "vtab-badge" x-show = "pendingCount" x-text = "pendingCount" ></ span >
</ button >
2026-06-09 00:41:27 +00:00
</ nav >
< div class = "vtabs-content" >
< section x-show = "tab==='souls'" x-cloak >
< div class = "hm-neo-kpi-row" style = "grid-template-columns:repeat(4,1fr)" >
< div class = "hm-neo-kpi" >< div class = "hm-neo-kpi-top" >< div class = "hm-neo-ring" style = "--ring-color:#00e5ff;--ring-pct:80%" >< div class = "hm-neo-ring-inner" > 🤖</ div ></ div >< div >< div class = "hm-neo-kpi-label" > Agents</ div >< div class = "hm-neo-kpi-sub" > geregistreerd</ div ></ div ></ div >< div class = "hm-neo-kpi-value" x-text = "souls.length" > 0</ div >< div class = "hm-eq" >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span ></ div ></ div >
< div class = "hm-neo-kpi" >< div class = "hm-neo-kpi-top" >< div class = "hm-neo-ring" style = "--ring-color:#b8ff3c;--ring-pct:70%" >< div class = "hm-neo-ring-inner" > ⚡</ div ></ div >< div >< div class = "hm-neo-kpi-label" > Events</ div >< div class = "hm-neo-kpi-sub" > totaal gelogd</ div ></ div ></ div >< div class = "hm-neo-kpi-value" x-text = "totalEvents" > 0</ div >< div class = "hm-eq" >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span ></ div ></ div >
< div class = "hm-neo-kpi" >< div class = "hm-neo-kpi-top" >< div class = "hm-neo-ring" style = "--ring-color:#ff9f43;--ring-pct:50%" >< div class = "hm-neo-ring-inner" > ✓</ div ></ div >< div >< div class = "hm-neo-kpi-label" > Actief</ div >< div class = "hm-neo-kpi-sub" > agents online</ div ></ div ></ div >< div class = "hm-neo-kpi-value" x-text = "activeCount" > 0</ div >< div class = "hm-eq" >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span ></ div ></ div >
2026-06-09 10:41:13 +00:00
< div class = "hm-neo-kpi" >< div class = "hm-neo-kpi-top" >< div class = "hm-neo-ring" style = "--ring-color:#ffd700;--ring-pct:90%" >< div class = "hm-neo-ring-inner" > 👑</ div ></ div >< div >< div class = "hm-neo-kpi-label" > Herman</ div >< div class = "hm-neo-kpi-sub" > Co-CEO · takenverdeler</ div ></ div ></ div >< div class = "hm-neo-kpi-value" style = "font-size:1.1rem" > Co-CEO</ div >< div class = "hm-eq" >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span ></ div ></ div >
2026-06-09 00:41:27 +00:00
</ div >
< div class = "agents-neo-grid" >
< template x-for = "s in souls" :key = "s.agent_key" >
< article class = "agent-soul-card" :class = "{ selected: selected?.agent_key === s.agent_key }" @ click = "select(s.agent_key)" >
2026-06-09 10:41:13 +00:00
< div class = "agent-char-wrap" :class = "s.agent_key"
:data-agent-avatar = "s.agent_key"
:data-agent-color = "s.avatar_color || '#1e3a5f'"
x-init = "$nextTick(() => window.AgentAvatars && AgentAvatars.paint($el, s.agent_key, s.avatar_color))" ></ div >
< strong x-text = "s.display_name" ></ strong >
< div class = "hm-neo-kpi-sub" x-text = "s.role_title" ></ div >
< span class = "agent-status-pill" :class = "statusClass(s)" >
< span class = "dot" ></ span >
< span x-text = "statusLabel(s)" ></ span >
</ span >
< p class = "agent-current-task" x-text = "s.current_task || 'Wacht op taak…'" ></ p >
2026-06-09 12:56:59 +00:00
< p class = "agent-role-desc" x-text = "s.responsibilities || '—'" ></ p >
2026-06-09 00:41:27 +00:00
< div class = "agent-meta-row" >
2026-06-23 10:04:23 +00:00
< button type = "button" class = "agent-events-link"
@ click . stop = "goTerminal(s.agent_key)"
title = "Open live terminal" > Terminal →</ button >
2026-06-09 10:41:13 +00:00
< button type = "button" class = "agent-events-link"
@ click . stop = "openEvents(s.agent_key, s.display_name)"
x-text = "(s.event_count||0) + ' events'"
:title = "'Bekijk wat ' + s.display_name + ' heeft gedaan'" ></ button >
< span class = "agent-tag" x-text = "s.agent_key" ></ span >
2026-06-09 00:41:27 +00:00
</ div >
</ article >
</ template >
</ div >
< div class = "agent-soul-editor" x-show = "selected" x-cloak >
< div class = "agent-editor-header" >
2026-06-09 10:41:13 +00:00
< div class = "agent-char-wrap large" :class = "selected?.agent_key"
:data-agent-avatar = "selected?.agent_key"
:data-agent-color = "selected?.avatar_color || '#1e3a5f'"
x-effect = "selected && $nextTick(() => window.AgentAvatars && AgentAvatars.paint($el, selected.agent_key, selected.avatar_color))" ></ div >
2026-06-09 00:41:27 +00:00
< div style = "flex:1" >
< h2 >< span x-text = "selected?.display_name" ></ span > — < span x-text = "selected?.role_title" ></ span ></ h2 >
< p style = "margin:0.25rem 0 0;color:#94a3b8;font-size:0.85rem" x-text = "selected?.responsibilities" ></ p >
</ div >
< div style = "display:flex;gap:0.5rem;flex-shrink:0" >
< button class = "btn btn-pulse-green" @ click = "save()" :disabled = "saving" > Opslaan</ button >
< a href = "/settings?tab=permissions" class = "btn btn-pulse-purple" > Herman rechten →</ a >
</ div >
</ div >
< label > Rol titel< input class = "form-input" x-model = "form.role_title" ></ label >
< label > Functie & werkzaamheden< textarea class = "form-input" rows = "3" x-model = "form.responsibilities" ></ textarea ></ label >
< label > Soul.md (karakter, tone of voice, regels)< textarea x-model = "form.soul_md" ></ textarea ></ label >
2026-06-09 10:41:13 +00:00
< div style = "display:flex;justify-content:space-between;align-items:center;margin-top:1rem" >
< h3 style = "font-size:0.75rem;text-transform:uppercase;color:#64748b;margin:0" > Recente activiteit</ h3 >
< button type = "button" class = "agent-events-link" x-show = "selected" @ click = "openEvents(selected.agent_key, selected.display_name)" > Alle events →</ button >
</ div >
2026-06-09 00:41:27 +00:00
< template x-for = "ev in (selected?.recent_events||[])" :key = "ev.id" >
2026-06-09 10:41:13 +00:00
< button type = "button" class = "hub-feed-item agent-event-clickable" @ click = "openEvents(selected.agent_key, selected.display_name, ev.id)" >
< span class = "agent-event-time" x-text = "formatEventTime(ev.created_at)" ></ span >
< span class = "agent-event-title" x-text = "ev.title||ev.event_type" ></ span >
< span class = "badge badge-sm" x-text = "ev.status||'—'" ></ span >
</ button >
2026-06-09 00:41:27 +00:00
</ template >
< p x-show = "!(selected?.recent_events||[]).length" class = "muted" > Nog geen events voor deze agent.</ p >
</ div >
</ section >
2026-06-23 10:04:23 +00:00
< section x-show = "tab==='mesh'" x-cloak
x-effect = "if (tab === 'mesh') { $nextTick(() => mountMesh()) }" >
2026-06-09 00:41:27 +00:00
< div class = "panel" >
2026-06-23 10:04:23 +00:00
< div class = "mesh-framework-head" >
< h2 style = "margin-top:0;" > Agent Communicatie Framework</ h2 >
< p class = "page-tip" style = "margin-top:.25rem;" > Kies hoe je het netwerk wilt zien. Klik een agent → live terminal. Alle terminals onder tab < strong > Terminals</ strong > .</ p >
</ div >
< div class = "mesh-mode-section" id = "mesh-mode-picker" >
< label > Communicatiestijl</ label >
< div class = "mesh-mode-grid" >
< button type = "button" class = "mesh-mode-card" data-mesh-mode = "pyramid" aria-pressed = "false"
onclick = "window.AgentsMesh&&AgentsMesh.pickMode('pyramid')" >
< span class = "mesh-mode-check" aria-hidden = "true" ></ span >
< span class = "mesh-mode-icon" > ◆</ span >
< span class = "mesh-mode-title" > Piramide</ span >
< span class = "mesh-mode-sub" > Herman als hub</ span >
< span class = "mesh-mode-desc" > Rapport ↑ en delegatie ↓. Alleen live handoffs.</ span >
</ button >
< button type = "button" class = "mesh-mode-card" data-mesh-mode = "mesh" aria-pressed = "false"
onclick = "window.AgentsMesh&&AgentsMesh.pickMode('mesh')" >
< span class = "mesh-mode-check" aria-hidden = "true" ></ span >
< span class = "mesh-mode-icon" > ⬡</ span >
< span class = "mesh-mode-title" > Vol netwerk</ span >
< span class = "mesh-mode-sub" > Alle partners</ span >
< span class = "mesh-mode-desc" > Volledige samenwerkingsmatrix zichtbaar.</ span >
</ button >
< button type = "button" class = "mesh-mode-card" data-mesh-mode = "org" aria-pressed = "false"
onclick = "window.AgentsMesh&&AgentsMesh.pickMode('org')" >
< span class = "mesh-mode-check" aria-hidden = "true" ></ span >
< span class = "mesh-mode-icon" > ▤</ span >
< span class = "mesh-mode-title" > Organigram</ span >
< span class = "mesh-mode-sub" > Pure hiërarchie</ span >
< span class = "mesh-mode-desc" > CEO → Herman → agents, geen peer-lijnen.</ span >
</ button >
< button type = "button" class = "mesh-mode-card" data-mesh-mode = "pulse" aria-pressed = "false"
onclick = "window.AgentsMesh&&AgentsMesh.pickMode('pulse')" >
< span class = "mesh-mode-check" aria-hidden = "true" ></ span >
< span class = "mesh-mode-icon" > ◎</ span >
< span class = "mesh-mode-title" > Live stream</ span >
< span class = "mesh-mode-sub" > Alleen actief</ span >
< span class = "mesh-mode-desc" > Geen stille lijnen — alleen wat nu beweegt.</ span >
</ button >
< button type = "button" class = "mesh-mode-card" data-mesh-mode = "clusters" aria-pressed = "false"
onclick = "window.AgentsMesh&&AgentsMesh.pickMode('clusters')" >
< span class = "mesh-mode-check" aria-hidden = "true" ></ span >
< span class = "mesh-mode-icon" > ◉</ span >
< span class = "mesh-mode-title" > Domeinen</ span >
< span class = "mesh-mode-sub" > Per afdeling</ span >
< span class = "mesh-mode-desc" > Groei · Product · Operatie gegroepeerd.</ span >
</ button >
</ div >
</ div >
< div class = "mesh-toolbar" >
< span id = "mesh-mode-badge" class = "mesh-mode-badge" > Piramide</ span >
< button type = "button" class = "btn btn-sm" @ click = "remountMesh()" > ↻ Verversen</ button >
< button type = "button" class = "btn btn-sm btn-pulse" @ click = "setTab('terminals', $refs.vtabsNav?.querySelector('[data-tab=terminals]'))" > Alle terminals</ button >
</ div >
2026-06-09 00:41:27 +00:00
< div class = "mesh-wrap" >
2026-06-23 10:04:23 +00:00
< svg id = "agents-mesh-canvas" class = "mesh-svg" viewBox = "0 0 1200 860" ></ svg >
2026-06-09 00:41:27 +00:00
</ div >
< div class = "mesh-legend" >
2026-06-23 10:04:23 +00:00
< span class = "leg-report" > ↑ Rapport</ span >
< span class = "leg-delegate" > ↓ Delegatie</ span >
< span class = "leg-peer" x-show = "meshLegendPeer" > ↔ Handoff</ span >
< span class = "leg-exec" x-show = "meshLegendExec" > ⇅ Executive</ span >
</ div >
< div class = "mesh-terminal-focus" x-show = "focusTerminalKey" x-cloak >
< div class = "agent-term-head" >
< span class = "term-dot" ></ span >
< strong x-text = "'Terminal · ' + (focusTerminalName || '')" ></ strong >
< button type = "button" class = "btn btn-sm" @ click = "clearFocusTerminal()" > Sluiten</ button >
</ div >
< div class = "agent-term-body" :data-term-key = "focusTerminalKey" data-term-focus = "1" ></ div >
</ div >
</ div >
</ section >
< section x-show = "tab==='terminals'" x-cloak >
< div class = "panel" >
< h2 style = "margin-top:0;" > Agent terminals</ h2 >
< p class = "agents-terminals-intro" > Elke agent heeft een eigen terminal. Typ commando's en druk < kbd > Enter</ kbd > — output verschijnt live. Start met < code > help</ code > .</ p >
< div class = "agents-terminals-grid" >
< template x-for = "s in souls" :key = "'term-' + s.agent_key" >
< article class = "agent-term-window"
:class = "{ 'is-live': (s.events_6h||0) > 0 || s.current_task, 'is-expanded': expandedTerminal === s.agent_key }"
@ click = "expandTerminal(s.agent_key)" >
< header class = "agent-term-head" >
< span class = "term-dot" ></ span >
< strong x-text = "s.display_name" ></ strong >
< small x-text = "s.current_task || s.last_event_title || 'Standby'" ></ small >
</ header >
< div class = "agent-term-body" :data-term-key = "s.agent_key" ></ div >
< footer class = "agent-term-foot" @ click . stop >
< label class = "agent-term-cmd" :for = "'term-in-' + s.agent_key" >
< span class = "term-prompt" x-text = "s.agent_key + '>'" ></ span >
< input type = "text"
:id = "'term-in-' + s.agent_key"
class = "agent-term-input"
:data-term-input = "s.agent_key"
:placeholder = "s.agent_key === 'herman' ? 'briefing · vraag aan Herman…' : (s.agent_key === 'webbuilder' ? 'build <opdracht> · projects · status …' : 'help · status · handoff …')"
autocomplete = "off"
spellcheck = "false" />
</ label >
</ footer >
</ article >
</ template >
2026-06-09 00:41:27 +00:00
</ div >
</ div >
</ section >
2026-06-09 10:41:13 +00:00
< section x-show = "tab==='approvals'" x-cloak x-data = "approvalsPanel()" x-init = "load()" >
< div class = "panel" >
< div style = "display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.75rem;margin-bottom:1rem" >
< div >
< h2 style = "margin:0;" > Approval queue</ h2 >
< p class = "page-tip" > Agents dienen queries in → jij keurt goed → pas daarna mag de actie uitgevoerd worden.</ p >
</ div >
< button class = "btn btn-sm btn-pulse" @ click = "load()" > ↻ Verversen</ button >
</ div >
< table class = "data-table" >
< thead >< tr >< th > #</ th >< th > Agent</ th >< th > Actie</ th >< th > Titel</ th >< th > Status</ th >< th > Tijd</ th >< th ></ th ></ tr ></ thead >
< tbody >
< template x-for = "r in items" :key = "r.id" >
< tr >
< td x-text = "r.id" ></ td >
< td x-text = "r.agent_key" ></ td >
< td x-text = "r.action_type" ></ td >
< td x-text = "r.title" ></ td >
< td >< span class = "badge" :class = "r.status==='pending'?'badge-warn':'badge-positive'" x-text = "r.status" ></ span ></ td >
< td x-text = "(r.created_at||'').substring(0,16).replace('T',' ')" ></ td >
< td >
< template x-if = "r.status==='pending'" >
< span >
< button class = "btn btn-sm btn-approve" @ click = "approve(r.id)" > ✓</ button >
< button class = "btn btn-sm btn-reject" @ click = "reject(r.id)" > ✗</ button >
</ span >
</ template >
</ td >
</ tr >
</ template >
</ tbody >
</ table >
< p x-show = "!items.length" class = "muted" > Geen approval requests.</ p >
</ div >
</ section >
2026-06-09 00:41:27 +00:00
</ div >
</ div >
2026-06-09 10:41:13 +00:00
< div class = "agent-events-backdrop" x-show = "eventsOpen" x-cloak @ click = "closeEvents()" ></ div >
< aside class = "agent-events-drawer" :class = "{ open: eventsOpen }" x-cloak >
< header class = "agent-events-head" >
< div >
< h3 x-text = "eventsAgentName || 'Agent'" ></ h3 >
< p class = "muted" x-text = "eventsList.length + ' activiteiten gelogd'" ></ p >
</ div >
< button type = "button" class = "btn btn-sm" @ click = "closeEvents()" > ✕</ button >
</ header >
< div class = "agent-events-body" x-show = "!eventsLoading" >
< template x-for = "ev in eventsList" :key = "ev.id" >
< article class = "agent-event-card" :class = "{ expanded: selectedEvent?.id === ev.id }" @ click = "toggleEventDetail(ev)" >
< div class = "agent-event-card-top" >
< time x-text = "formatEventTime(ev.created_at)" ></ time >
< span class = "badge badge-sm" :class = "eventStatusClass(ev.status)" x-text = "ev.status || 'completed'" ></ span >
</ div >
< strong x-text = "ev.title || ev.event_type" ></ strong >
< div class = "agent-event-meta" >
< span x-text = "ev.event_type" ></ span >
< span x-show = "ev.channel" x-text = "'· ' + ev.channel" ></ span >
</ div >
< div class = "agent-event-detail" x-show = "selectedEvent?.id === ev.id" x-cloak >
< p x-show = "ev.body" x-text = "ev.body" ></ p >
< pre x-show = "!ev.body && ev.metadata && Object.keys(ev.metadata||{}).length" x-text = "JSON.stringify(ev.metadata, null, 2)" ></ pre >
< p x-show = "!ev.body && !(ev.metadata && Object.keys(ev.metadata||{}).length)" class = "muted" > Geen extra details voor dit event.</ p >
</ div >
</ article >
</ template >
< p x-show = "!eventsList.length && !eventsLoading" class = "muted" style = "padding:1rem" > Nog geen events voor deze agent.</ p >
</ div >
< p x-show = "eventsLoading" class = "muted" style = "padding:1.5rem" > Events laden…</ p >
</ aside >
2026-06-09 00:41:27 +00:00
</ div >
{% endblock %}
{% block scripts %}
2026-06-09 10:41:13 +00:00
< script src = "/static/js/dashboard-layout.js?v=2" ></ script >
< script src = "/static/js/page-viz.js?v=1" ></ script >
2026-06-23 10:04:23 +00:00
< script src = "/static/js/agents-mesh.js?v=11" ></ script >
< script src = "/static/js/agents-terminals.js?v=6" ></ script >
2026-06-09 00:41:27 +00:00
< script >
function agentsSoulHub () {
return {
tab : '{{ active_tab }}' ,
tabIndicatorTop : 0 ,
tabIndicatorH : 40 ,
meshLoaded : false ,
2026-06-09 10:41:13 +00:00
pendingCount : 0 ,
2026-06-09 00:41:27 +00:00
souls : [], selected : null , form : {}, saving : false ,
2026-06-09 10:41:13 +00:00
eventsOpen : false , eventsAgentKey : null , eventsAgentName : '' , eventsList : [], eventsLoading : false , selectedEvent : null ,
2026-06-23 10:04:23 +00:00
terminalOpen : false , terminalAgentKey : null , terminalAgentName : '' , terminalLines : [], _terminalEs : null ,
meshCommMode : 'pyramid' ,
meshLegendPeer : true ,
meshLegendExec : true ,
focusTerminalKey : null , focusTerminalName : '' , expandedTerminal : null ,
2026-06-09 00:41:27 +00:00
get totalEvents () { return this . souls . reduce (( a , s ) => a + ( s . event_count || 0 ), 0 ); },
get activeCount () { return this . souls . filter ( s => s . is_active !== false ). length ; },
2026-06-09 10:41:13 +00:00
statusClass ( s ) {
const st = ( s . current_status || '' ). toLowerCase ();
if ( st === 'needs_approval' || st === 'pending' ) return 'busy' ;
if ( st === 'error' || st === 'rejected' ) return 'error' ;
if ( st === 'completed' || ( s . events_6h > 0 )) return 'active' ;
return 'idle' ;
},
formatEventTime ( ts ) {
return ( ts || '' ). substring ( 0 , 16 ). replace ( 'T' , ' ' );
},
eventStatusClass ( status ) {
const s = ( status || '' ). toLowerCase ();
if ( s === 'pending' || s === 'needs_approval' ) return 'badge-warn' ;
if ( s === 'error' || s === 'rejected' ) return 'badge-negative' ;
if ( s === 'approved' || s === 'completed' ) return 'badge-positive' ;
return '' ;
},
async openEvents ( key , name , focusId ) {
if ( ! key ) return ;
this . eventsAgentKey = key ;
this . eventsAgentName = name || key ;
this . eventsOpen = true ;
this . eventsLoading = true ;
this . selectedEvent = null ;
try {
const r = await fetch ( '/api/agents/souls/' + encodeURIComponent ( key ) + '/events?limit=50' ). then ( x => x . json ());
this . eventsList = r . items || [];
if ( focusId ) {
const hit = this . eventsList . find ( e => e . id === focusId );
if ( hit ) this . selectedEvent = hit ;
}
} catch ( e ) { this . eventsList = []; }
this . eventsLoading = false ;
},
closeEvents () {
this . eventsOpen = false ;
this . selectedEvent = null ;
},
toggleEventDetail ( ev ) {
this . selectedEvent = this . selectedEvent ? . id === ev . id ? null : ev ;
},
statusLabel ( s ) {
const st = ( s . current_status || '' ). toLowerCase ();
if ( st === 'needs_approval' ) return 'Wacht op goedkeuring' ;
if ( st === 'pending' ) return 'In wachtrij' ;
if ( st === 'completed' ) return 'Klaar' ;
if ( st === 'error' ) return 'Fout' ;
if ( s . current_task ) return 'Bezig' ;
return 'Standby' ;
},
2026-06-09 00:41:27 +00:00
async init () {
2026-06-23 10:04:23 +00:00
this . syncMeshMode ();
2026-06-09 00:41:27 +00:00
await this . load ();
2026-06-09 10:41:13 +00:00
await this . loadPendingCount ();
if ( window . PageViz ) {
window . PageViz . init ( 'agents' , ( mode ) => this . renderViz ( mode ));
}
2026-06-09 00:41:27 +00:00
this . $nextTick (() => {
const activeBtn = this . $refs . vtabsNav ? . querySelector ( '.vtab-btn.active' );
if ( activeBtn ) this . updateTabIndicator ( activeBtn );
2026-06-23 10:04:23 +00:00
const self = this ;
if ( window . AgentsMesh ) {
AgentsMesh . bindModePicker ({
onNodeClick : function ( node ) {
self . focusTerminal ( node . agent_key , node . display_name );
},
});
}
if ( window . AgentsTerminals ) AgentsTerminals . startLiveStream ();
2026-06-09 00:41:27 +00:00
if ( this . tab === 'mesh' ) this . mountMesh ();
2026-06-23 10:04:23 +00:00
if ( this . tab === 'terminals' ) this . startTerminals ();
2026-06-09 00:41:27 +00:00
});
},
updateTabIndicator ( el ) {
if ( ! el || ! this . $refs . vtabsNav ) return ;
const navRect = this . $refs . vtabsNav . getBoundingClientRect ();
const btnRect = el . getBoundingClientRect ();
this . tabIndicatorTop = Math . max ( 0 , btnRect . top - navRect . top );
this . tabIndicatorH = btnRect . height || 40 ;
},
setTab ( next , el ) {
this . tab = next ;
this . updateTabIndicator ( el );
const url = new URL ( window . location . href );
url . searchParams . set ( 'tab' , next );
window . history . replaceState ({}, '' , url . toString ());
if ( next === 'mesh' ) this . mountMesh ();
2026-06-23 10:04:23 +00:00
if ( next === 'terminals' ) this . startTerminals ();
2026-06-09 10:41:13 +00:00
if ( next === 'approvals' ) this . loadPendingCount ();
},
async loadPendingCount () {
try {
const r = await fetch ( '/api/agents/approvals?status=pending' ). then ( x => x . json ());
this . pendingCount = ( r . items || []). length ;
} catch ( e ) { this . pendingCount = 0 ; }
2026-06-09 00:41:27 +00:00
},
2026-06-23 10:04:23 +00:00
syncMeshMode () {
if ( ! window . AgentsMesh ) return ;
const self = this ;
this . meshCommMode = AgentsMesh . getSavedMode ();
this . updateMeshLegend ();
AgentsMesh . setModeChangeCallback ( function ( id ) {
self . meshCommMode = id ;
self . updateMeshLegend ();
});
},
updateMeshLegend () {
const m = ( window . AgentsMesh && AgentsMesh . COMM_MODES ) ? AgentsMesh . COMM_MODES [ this . meshCommMode ] : null ;
this . meshLegendPeer = m ? ( m . showLivePeers || m . showStaticPeers ) : true ;
this . meshLegendExec = m ? m . showExecutive !== false : true ;
},
setMeshMode ( id ) {
if ( window . AgentsMesh ) AgentsMesh . pickMode ( id );
this . meshCommMode = id ;
this . updateMeshLegend ();
},
remountMesh () {
if ( window . AgentsMesh && AgentsMesh . refresh ) {
const self = this ;
AgentsMesh . refresh ( 'agents-mesh-canvas' , {
commMode : this . meshCommMode ,
onNodeClick : function ( node ) {
self . focusTerminal ( node . agent_key , node . display_name );
},
});
} else {
this . mountMesh ();
}
},
2026-06-09 00:41:27 +00:00
mountMesh () {
2026-06-23 10:04:23 +00:00
if ( ! window . AgentsMesh ) return ;
const self = this ;
const opts = {
commMode : this . meshCommMode ,
onNodeClick : function ( node ) {
self . focusTerminal ( node . agent_key , node . display_name );
},
};
window . AgentsMesh . mount ( 'agents-mesh-canvas' , opts ). catch ( function ( e ) {
if ( window . Cockpit ) Cockpit . toast ( 'Mesh: ' + ( e . message || 'laden mislukt' ), 'error' );
});
if ( this . focusTerminalKey && window . AgentsTerminals ) {
this . $nextTick ( function () { AgentsTerminals . mountAll (); });
}
},
focusTerminal ( key , name ) {
this . focusTerminalKey = key ;
this . focusTerminalName = name || key ;
this . expandedTerminal = key ;
this . $nextTick ( function () {
if ( window . AgentsTerminals ) AgentsTerminals . mountAll ();
});
},
clearFocusTerminal () {
this . focusTerminalKey = null ;
this . focusTerminalName = '' ;
},
goTerminal ( key ) {
const soul = this . souls . find ( function ( s ) { return s . agent_key === key ; });
const btn = this . $refs . vtabsNav ? . querySelector ( '[data-tab=terminals]' );
this . setTab ( 'terminals' , btn );
this . expandedTerminal = key ;
this . $nextTick ( function () {
const el = document . querySelector ( '[data-term-key="' + key + '"]' );
if ( el ) el . closest ( '.agent-term-window' ) ? . scrollIntoView ({ behavior : 'smooth' , block : 'center' });
});
},
expandTerminal ( key ) {
this . expandedTerminal = this . expandedTerminal === key ? null : key ;
},
startTerminals () {
this . $nextTick ( function () {
if ( window . AgentsTerminals ) {
AgentsTerminals . mountAll ();
AgentsTerminals . bindInputs ();
}
});
2026-06-09 00:41:27 +00:00
},
async load () {
const r = await fetch ( '/api/agents/souls' ). then ( x => x . json ());
this . souls = r . items || [];
if ( this . souls . length && ! this . selected ) this . select ( this . souls [ 0 ]. agent_key );
2026-06-09 10:41:13 +00:00
this . renderViz ( window . __vizMode || 'neo-bars' );
this . $nextTick (() => window . AgentAvatars && AgentAvatars . paintAll ( this . $root ));
},
renderViz ( mode ) {
if ( ! window . VizEngine ) return ;
const rows = ( this . souls || []). map ( s => ({
label : s . agent_key ,
value : s . event_count || 0 ,
raw : s . event_count || 0 ,
})). sort (( a , b ) => b . raw - a . raw ). slice ( 0 , 12 );
if ( ! rows . length ) rows . push ({ label : 'geen' , value : 0 , raw : 0 });
VizEngine . renderFromRows ( mode ,
document . getElementById ( 'chart-agents-viz' ),
document . getElementById ( 'viz-agents-alt' ),
'agents-page' , 'Events per agent' , rows , 'hm-eq-lime'
);
2026-06-09 00:41:27 +00:00
},
async select ( key ) {
const r = await fetch ( '/api/agents/souls/' + key ). then ( x => x . json ());
this . selected = r . soul ;
this . form = { role_title : r . soul . role_title , responsibilities : r . soul . responsibilities , soul_md : r . soul . soul_md };
},
async save () {
if ( ! this . selected ) return ;
this . saving = true ;
try {
await Cockpit . api ( '/api/agents/souls/' + this . selected . agent_key , { method : 'PUT' , body : JSON . stringify ( this . form ) });
Cockpit . toast ( 'Soul opgeslagen' , 'success' );
await this . load ();
await this . select ( this . selected . agent_key );
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
this . saving = false ;
}
};
}
2026-06-09 10:41:13 +00:00
function approvalsPanel () {
return {
items : [],
async load () {
try {
const r = await fetch ( '/api/agents/approvals?limit=50' ). then ( x => x . json ());
this . items = r . items || [];
} catch ( e ) { this . items = []; }
},
async approve ( id ) {
await Cockpit . api ( '/api/agents/approvals/' + id + '/approve' , { method : 'POST' , body : '{}' });
Cockpit . toast ( 'Goedgekeurd — agent mag query uitvoeren' , 'success' );
await this . load ();
},
async reject ( id ) {
const reason = prompt ( 'Reden afwijzing (optioneel):' ) || '' ;
await Cockpit . api ( '/api/agents/approvals/' + id + '/reject' , { method : 'POST' , body : JSON . stringify ({ reason }) });
Cockpit . toast ( 'Afgewezen' , 'info' );
await this . load ();
},
};
}
2026-06-09 00:41:27 +00:00
</ script >
{% endblock %}