SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC

This commit is contained in:
sysops
2026-06-23 10:04:23 +00:00
parent 3bf15c4850
commit 26fe76afdd
165 changed files with 47427 additions and 1264 deletions
+211 -13
View File
@@ -3,7 +3,7 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<link rel="stylesheet" href="/static/css/hermes.css" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
<link rel="stylesheet" href="/static/css/agents-mesh.css" />
<link rel="stylesheet" href="/static/css/agents-mesh.css?v=8" />
<link rel="stylesheet" href="/static/css/agent-characters.css?v=3" />
<script src="/static/js/agent-avatars.js?v=3"></script>
{% endblock %}
@@ -25,12 +25,16 @@
<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'">
<div class="vtabs-nav-header"><strong>Agents</strong></div>
<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>
<button class="vtab-btn" :class="{active: tab==='terminals'}" @click="setTab('terminals', $el)" data-tab="terminals">
<span class="vtab-icon">🖥️</span><span>Terminals</span>
</button>
<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>
@@ -62,6 +66,9 @@
<p class="agent-current-task" x-text="s.current_task || 'Wacht op taak…'"></p>
<p class="agent-role-desc" x-text="s.responsibilities || '—'"></p>
<div class="agent-meta-row">
<button type="button" class="agent-events-link"
@click.stop="goTerminal(s.agent_key)"
title="Open live terminal">Terminal →</button>
<button type="button" class="agent-events-link"
@click.stop="openEvents(s.agent_key, s.display_name)"
x-text="(s.event_count||0) + ' events'"
@@ -105,18 +112,114 @@
</div>
</section>
<section x-show="tab==='mesh'" x-cloak>
<section x-show="tab==='mesh'" x-cloak
x-effect="if (tab === 'mesh') { $nextTick(() => mountMesh()) }">
<div class="panel">
<h2 style="margin-top:0;">Netwerk</h2>
<p class="page-tip" style="margin-top:.25rem;">Agents onderaan → Herman (Co-CEO) → CEO &amp; CTO. Pulse = actieve communicatie.</p>
<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>
<div class="mesh-wrap">
<svg id="agents-mesh-canvas" class="mesh-svg" viewBox="0 0 1100 640"></svg>
<svg id="agents-mesh-canvas" class="mesh-svg" viewBox="0 0 1200 860"></svg>
</div>
<div class="mesh-legend">
<span>Groen = healthy</span>
<span>Oranje = waarschuwing</span>
<span>Blauw = idle</span>
<span>Grijs = offline</span>
<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>
</div>
</div>
</section>
@@ -196,7 +299,8 @@
{% block scripts %}
<script src="/static/js/dashboard-layout.js?v=2"></script>
<script src="/static/js/page-viz.js?v=1"></script>
<script src="/static/js/agents-mesh.js"></script>
<script src="/static/js/agents-mesh.js?v=11"></script>
<script src="/static/js/agents-terminals.js?v=6"></script>
<script>
function agentsSoulHub() {
return {
@@ -207,6 +311,11 @@ function agentsSoulHub() {
pendingCount: 0,
souls: [], selected: null, form: {}, saving: false,
eventsOpen: false, eventsAgentKey: null, eventsAgentName: '', eventsList: [], eventsLoading: false, selectedEvent: null,
terminalOpen: false, terminalAgentKey: null, terminalAgentName: '', terminalLines: [], _terminalEs: null,
meshCommMode: 'pyramid',
meshLegendPeer: true,
meshLegendExec: true,
focusTerminalKey: null, focusTerminalName: '', expandedTerminal: null,
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; },
statusClass(s) {
@@ -260,6 +369,7 @@ function agentsSoulHub() {
return 'Standby';
},
async init() {
this.syncMeshMode();
await this.load();
await this.loadPendingCount();
if (window.PageViz) {
@@ -268,7 +378,17 @@ function agentsSoulHub() {
this.$nextTick(() => {
const activeBtn = this.$refs.vtabsNav?.querySelector('.vtab-btn.active');
if (activeBtn) this.updateTabIndicator(activeBtn);
const self = this;
if (window.AgentsMesh) {
AgentsMesh.bindModePicker({
onNodeClick: function (node) {
self.focusTerminal(node.agent_key, node.display_name);
},
});
}
if (window.AgentsTerminals) AgentsTerminals.startLiveStream();
if (this.tab === 'mesh') this.mountMesh();
if (this.tab === 'terminals') this.startTerminals();
});
},
updateTabIndicator(el) {
@@ -285,6 +405,7 @@ function agentsSoulHub() {
url.searchParams.set('tab', next);
window.history.replaceState({}, '', url.toString());
if (next === 'mesh') this.mountMesh();
if (next === 'terminals') this.startTerminals();
if (next === 'approvals') this.loadPendingCount();
},
async loadPendingCount() {
@@ -293,10 +414,87 @@ function agentsSoulHub() {
this.pendingCount = (r.items || []).length;
} catch (e) { this.pendingCount = 0; }
},
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();
}
},
mountMesh() {
if (this.meshLoaded || !window.AgentsMesh) return;
this.meshLoaded = true;
window.AgentsMesh.mount('agents-mesh-canvas');
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();
}
});
},
async load() {
const r = await fetch('/api/agents/souls').then(x => x.json());