Files
foodlinkk-command-center/cockpit/templates/hermes.html
T
Aissa 5e43b6b246 Ship Herman chat, Hermes KPI explorer, and intelligence analytics.
Adds floating Herman assistant with voice, Hermes technical KPI dashboard with file explorer, and a wins/kansen analytics pulse with Hermes findings hooks. Includes optional local HTTPS via Caddy for mic permissions (public certs only).
2026-08-01 17:50:45 +00:00

269 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Hermes · KPI's{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/hermes.css?v=11" />
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
{% endblock %}
{% block content %}
<div x-data="hermesKpiDashboard()" x-init="init()" class="hermes-shell hm-neo">
<header class="hm-neo-header page-header">
<div>
<h1>Hermes · Technische KPI's</h1>
<p class="hm-sub">Live processen van Hermes Agent + Herman — klik een blok voor detail ·
<a href="http://10.4.7.10:9119/" target="_blank" rel="noopener">Hermes UI</a>
<span x-show="kpis.synced_at" class="text-muted" x-text="' · sync ' + formatTime(kpis.synced_at)"></span>
</p>
</div>
<div class="retail-actions">
<span class="status-badge" :class="kpis.online ? 'on' : 'off'" x-text="kpis.online ? 'Online' : 'Offline'"></span>
<button type="button" class="btn btn-sm" @click="refresh()" :disabled="loading">↻ Vernieuwen</button>
</div>
</header>
<!-- Zone A -->
<div class="hm-neo-kpi-row">
<div class="hm-neo-kpi" :class="{ highlight: kpis.online }">
<div class="hm-neo-kpi-top">
<div class="hm-neo-ring" :style="'--ring-color:' + (kpis.online ? '#b8ff3c' : '#f87171') + ';--ring-pct:80%'"><div class="hm-neo-ring-inner" x-text="kpis.online ? '●' : '○'"></div></div>
<div><div class="hm-neo-kpi-label">Hermes</div><div class="hm-neo-kpi-sub">Agent status</div></div>
</div>
<div class="hm-neo-kpi-value" style="font-size:1.25rem" x-text="kpis.online ? 'Online' : 'Offline'"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-top">
<div class="hm-neo-ring" style="--ring-color:#00e5ff;--ring-pct:70%"><div class="hm-neo-ring-inner">✈️</div></div>
<div><div class="hm-neo-kpi-label">Telegram</div><div class="hm-neo-kpi-sub">actieve chats</div></div>
</div>
<div class="hm-neo-kpi-value" x-text="ov.telegram_sessions_active || 0"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-top">
<div class="hm-neo-ring" style="--ring-color:#ff9f43;--ring-pct:75%"><div class="hm-neo-ring-inner">🔧</div></div>
<div><div class="hm-neo-kpi-label">Tool-calls</div><div class="hm-neo-kpi-sub">totaal werk</div></div>
</div>
<div class="hm-neo-kpi-value" x-text="fmt(ov.tool_calls_total)"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-top">
<div class="hm-neo-ring" style="--ring-color:#a855f7;--ring-pct:65%"><div class="hm-neo-ring-inner">💬</div></div>
<div><div class="hm-neo-kpi-label">Tekst in / uit</div><div class="hm-neo-kpi-sub">user · agent</div></div>
</div>
<div class="hm-neo-kpi-value" style="font-size:1.15rem" x-text="(ov.text_in||0) + ' / ' + (ov.text_out||0)"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-top">
<div class="hm-neo-ring" style="--ring-color:#38bdf8;--ring-pct:55%"><div class="hm-neo-ring-inner">🧩</div></div>
<div><div class="hm-neo-kpi-label">Skills</div><div class="hm-neo-kpi-sub">geïnstalleerd</div></div>
</div>
<div class="hm-neo-kpi-value" x-text="ov.skills_installed || 0"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-top">
<div class="hm-neo-ring" style="--ring-color:#94a3b8;--ring-pct:50%"><div class="hm-neo-ring-inner">⏱</div></div>
<div><div class="hm-neo-kpi-label">Sync</div><div class="hm-neo-kpi-sub">laatste snapshot</div></div>
</div>
<div class="hm-neo-kpi-value" style="font-size:0.95rem" x-text="formatTime(kpis.synced_at) || '—'"></div>
</div>
</div>
<!-- Zone B -->
<h2 class="hm-section-title">Wat doet hij? <span class="hm-section-hint">klik een blok</span></h2>
<div class="hm-bucket-grid">
<template x-for="b in (kpis.buckets || [])" :key="b.key">
<button type="button" class="hm-bucket-card hm-bucket-click" @click="openBucket(b)">
<div class="hm-bucket-label" x-text="b.label"></div>
<div class="hm-bucket-count" x-text="fmt(b.count)"></div>
<div class="hm-bucket-parts" x-text="partsLine(b.parts)"></div>
<div class="hm-bucket-open">Open →</div>
</button>
</template>
</div>
<!-- Zone C -->
<div class="hm-neo-charts" style="margin-top:1.25rem">
<div class="hm-chart-panel">
<div class="hm-chart-head"><h3>Top tools</h3><small>meest gebruikt</small></div>
<div class="hm-chart-wrap"><canvas id="hm-chart-tools"></canvas></div>
<ul class="hm-tool-list">
<template x-for="t in (kpis.tools_top || []).slice(0,8)" :key="t.name">
<li><span x-text="t.name"></span><strong x-text="fmt(t.count)"></strong></li>
</template>
</ul>
</div>
<div class="hm-chart-panel">
<div class="hm-chart-head"><h3>Bronnen</h3><small>via welk kanaal</small></div>
<div class="hm-chart-wrap"><canvas id="hm-chart-sources"></canvas></div>
<ul class="hm-tool-list">
<template x-for="s in (kpis.sources || [])" :key="s.source">
<li>
<span x-text="sourceLabel(s.source)"></span>
<strong x-text="fmt(s.tools) + ' tools · ' + (s.sessions||0) + ' sessies'"></strong>
</li>
</template>
</ul>
</div>
<div class="hm-chart-panel">
<div class="hm-chart-head"><h3>Herman (cockpit)</h3><small>agent_events</small></div>
<ul class="hm-tool-list hm-tool-list-tall">
<template x-for="e in (kpis.herman_cockpit || [])" :key="e.event_type">
<li><span x-text="e.event_type"></span><strong x-text="fmt(e.count)"></strong></li>
</template>
<li x-show="!(kpis.herman_cockpit || []).length" class="text-muted">Geen events</li>
</ul>
<p class="page-tip" style="margin-top:0.75rem;font-size:0.72rem">
Delegaties: <strong x-text="ov.async_delegations || 0"></strong> ·
Delivery: <strong x-text="ov.delivery_obligations || 0"></strong>
</p>
</div>
</div>
<!-- Explorer / detail modal -->
<div class="hm-modal-backdrop" x-show="modal.open" x-cloak @keydown.escape.window="closeModal()" @click.self="closeModal()">
<div class="hm-modal" role="dialog" aria-modal="true" @click.stop>
<header class="hm-modal-head">
<div>
<h2 x-text="modal.title"></h2>
<p class="hm-modal-sub">
<span x-text="fmt(modal.detail.count || 0) + ' calls'"></span>
<template x-for="(n, name) in (modal.detail.parts || {})" :key="name">
<button type="button" class="hm-chip" :class="{ active: modal.toolFilter === name }"
@click="setToolFilter(name)" x-text="n + ' ' + name.replace(/_/g,' ')"></button>
</template>
<button type="button" class="hm-chip" :class="{ active: !modal.toolFilter }" @click="setToolFilter(null)">alles</button>
</p>
</div>
<button type="button" class="hm-modal-close" @click="closeModal()" aria-label="Sluiten">×</button>
</header>
<div class="hm-modal-tabs" x-show="modal.key === 'files'">
<button type="button" :class="{ active: modal.tab === 'activity' }" @click="modal.tab = 'activity'">Activiteit</button>
<button type="button" :class="{ active: modal.tab === 'touched' }" @click="modal.tab = 'touched'">Aangeraakte files</button>
<button type="button" :class="{ active: modal.tab === 'explorer' }" @click="openExplorerTab()">Verkenner</button>
</div>
<div class="hm-modal-body" :class="{ 'hm-modal-split': true }">
<!-- Left pane -->
<aside class="hm-modal-side">
<div class="hm-side-loading" x-show="modal.loading">Laden…</div>
<!-- Activity list -->
<template x-if="modal.tab === 'activity' || modal.key !== 'files'">
<ul class="hm-event-list">
<template x-for="ev in (modal.detail.events || [])" :key="ev.id">
<li>
<button type="button" class="hm-event-item" :class="{ active: modal.selectedEvent && modal.selectedEvent.id === ev.id }"
@click="selectEvent(ev)">
<div class="hm-event-top">
<strong x-text="ev.title"></strong>
<time x-text="formatTime(ev.created_at)"></time>
</div>
<div class="hm-event-tool" x-text="ev.tool"></div>
<div class="hm-event-preview" x-text="ev.preview"></div>
</button>
</li>
</template>
<li x-show="!(modal.detail.events || []).length && !modal.loading" class="text-muted hm-empty">Geen events</li>
</ul>
</template>
<!-- Touched files -->
<template x-if="modal.key === 'files' && modal.tab === 'touched'">
<ul class="hm-file-list">
<template x-for="f in (modal.detail.files || [])" :key="f.path">
<li>
<button type="button" class="hm-file-item" :class="{ active: modal.preview && modal.preview.path === f.path }"
@click="openFile(f.path)">
<div class="hm-file-name">
<span x-text="f.exists ? '📄' : '◌'"></span>
<strong x-text="f.name"></strong>
</div>
<div class="hm-file-path" x-text="f.path"></div>
<div class="hm-file-meta">
<span x-text="Object.keys(f.ops || {}).map(k => (f.ops[k] + ' ' + k.replace(/_/g,' '))).join(' · ')"></span>
<time x-text="formatTime(f.last_at)"></time>
</div>
</button>
</li>
</template>
<li x-show="!(modal.detail.files || []).length && !modal.loading" class="text-muted hm-empty">Geen bestanden gevonden</li>
</ul>
</template>
<!-- Explorer -->
<template x-if="modal.key === 'files' && modal.tab === 'explorer'">
<div class="hm-explorer">
<div class="hm-explorer-bar">
<button type="button" class="btn btn-sm" @click="explorerUp()" :disabled="!modal.explorer.parent && !modal.explorer.path">↑ Omhoog</button>
<button type="button" class="btn btn-sm" @click="loadExplorer('')">Roots</button>
<code class="hm-explorer-path" x-text="modal.explorer.path || '(roots)'"></code>
</div>
<ul class="hm-file-list">
<template x-for="e in (modal.explorer.entries || [])" :key="e.path">
<li>
<button type="button" class="hm-file-item" @click="e.type === 'dir' ? loadExplorer(e.path) : openFile(e.path)">
<div class="hm-file-name">
<span x-text="e.type === 'dir' ? '📁' : '📄'"></span>
<strong x-text="e.name"></strong>
</div>
<div class="hm-file-meta">
<span x-show="e.size != null" x-text="fmtBytes(e.size)"></span>
<time x-text="formatTime(e.mtime)"></time>
</div>
</button>
</li>
</template>
</ul>
</div>
</template>
</aside>
<!-- Right pane preview -->
<section class="hm-modal-preview">
<div x-show="modal.previewLoading" class="hm-side-loading">Bestand laden…</div>
<template x-if="modal.preview && !modal.previewLoading">
<div>
<div class="hm-preview-head">
<div>
<h3 x-text="modal.preview.name || 'Preview'"></h3>
<code x-text="modal.preview.path || ''"></code>
</div>
<div class="hm-preview-meta" x-show="modal.preview.size != null">
<span x-text="fmtBytes(modal.preview.size)"></span>
<span x-text="formatTime(modal.preview.mtime)"></span>
</div>
</div>
<pre class="hm-preview-code" x-text="modal.preview.content || modal.preview.preview || modal.preview.error || '—'"></pre>
<p class="hm-preview-note" x-show="modal.preview.truncated">Preview afgekapt (bestand te groot).</p>
</div>
</template>
<template x-if="!modal.preview && modal.selectedEvent && !modal.previewLoading">
<div>
<div class="hm-preview-head">
<div>
<h3 x-text="modal.selectedEvent.title"></h3>
<p class="hm-modal-sub" x-text="modal.selectedEvent.tool + ' · ' + formatTime(modal.selectedEvent.created_at)"></p>
</div>
</div>
<pre class="hm-preview-code" x-text="modal.selectedEvent.preview || '—'"></pre>
<div class="hm-preview-paths" x-show="(modal.selectedEvent.paths || []).length">
<template x-for="p in (modal.selectedEvent.paths || [])" :key="p">
<button type="button" class="hm-chip" @click="openFile(p); if(modal.key==='files') modal.tab='touched'" x-text="p"></button>
</template>
</div>
</div>
</template>
<div x-show="!modal.preview && !modal.selectedEvent && !modal.previewLoading" class="hm-empty-pane">
Selecteer een event of bestand links om details te zien.
</div>
</section>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/hermes-ui.js?v=11"></script>
{% endblock %}