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).
This commit is contained in:
+230
-206
@@ -1,244 +1,268 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Hermes · Telegram Command Center{% endblock %}
|
||||
|
||||
{% block title %}Hermes · KPI's{% endblock %}
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="/static/css/hermes.css" />
|
||||
<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>
|
||||
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div x-data="hermesControl()" x-init="init()" class="hermes-shell hm-neo">
|
||||
|
||||
<div x-data="hermesKpiDashboard()" x-init="init()" class="hermes-shell hm-neo">
|
||||
<header class="hm-neo-header page-header">
|
||||
<h1>Hermes — Telegram Command Center</h1>
|
||||
<p class="hm-sub">Live feed · 4 PA browsers · second brain · volledige controle</p>
|
||||
<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>
|
||||
|
||||
<!-- Neo KPI row -->
|
||||
<!-- Zone A -->
|
||||
<div class="hm-neo-kpi-row">
|
||||
<div class="hm-neo-kpi" :class="{ highlight: tab === 'feed' }">
|
||||
<div class="hm-neo-kpi" :class="{ highlight: kpis.online }">
|
||||
<div class="hm-neo-kpi-top">
|
||||
<div class="hm-neo-ring" style="--ring-color:#00e5ff;--ring-pct:72%"><div class="hm-neo-ring-inner">💬</div></div>
|
||||
<div><div class="hm-neo-kpi-label">Conversaties</div><div class="hm-neo-kpi-sub">Telegram chats</div></div>
|
||||
<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" x-text="stats.conversations">0</div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></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:#b8ff3c;--ring-pct:85%"><div class="hm-neo-ring-inner">📨</div></div>
|
||||
<div><div class="hm-neo-kpi-label">Berichten</div><div class="hm-neo-kpi-sub">in + out</div></div>
|
||||
<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="stats.messages">0</div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></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:60%"><div class="hm-neo-ring-inner">🕸</div></div>
|
||||
<div><div class="hm-neo-kpi-label">Relaties</div><div class="hm-neo-kpi-sub">graph edges</div></div>
|
||||
<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="stats.edges">0</div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></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:55%"><div class="hm-neo-ring-inner">🧠</div></div>
|
||||
<div><div class="hm-neo-kpi-label">Vectors</div><div class="hm-neo-kpi-sub">pgvector</div></div>
|
||||
<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" x-text="stats.embeddings">0</div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></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>
|
||||
|
||||
<!-- Neo charts -->
|
||||
<div class="hm-neo-charts">
|
||||
<!-- 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><span class="hm-live-dot"></span> Berichten per richting</h3>
|
||||
<small>klik voor detail</small>
|
||||
</div>
|
||||
<div class="hm-chart-wrap"><canvas id="hm-chart-direction"></canvas></div>
|
||||
</div>
|
||||
<div class="hm-chart-panel">
|
||||
<div class="hm-chart-head">
|
||||
<h3><span class="hm-live-dot"></span> Agent activiteit</h3>
|
||||
<small>laatste events</small>
|
||||
</div>
|
||||
<div class="hm-chart-wrap"><canvas id="hm-chart-agents"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hermes-tabs">
|
||||
<button type="button" class="hermes-tab" :class="{ active: tab === 'feed' }" @click="setTab('feed')">📨 Live Feed</button>
|
||||
<button type="button" class="hermes-tab" :class="{ active: tab === 'pa' }" @click="setTab('pa')">🌐 PA Live (4×)</button>
|
||||
<button type="button" class="hermes-tab" :class="{ active: tab === 'graph' }" @click="setTab('graph')">🕸 Graph</button>
|
||||
<button type="button" class="hermes-tab" :class="{ active: tab === 'search' }" @click="setTab('search')">🔍 Memory</button>
|
||||
<button type="button" class="hermes-tab" :class="{ active: tab === 'control' }" @click="setTab('control')">⚙️ Control</button>
|
||||
</div>
|
||||
|
||||
<!-- LIVE FEED -->
|
||||
<div x-show="tab === 'feed'" class="hermes-grid">
|
||||
<aside class="panel hermes-sidebar">
|
||||
<div class="hm-team">
|
||||
<h3>Team — Foodlinkk leadership</h3>
|
||||
<template x-for="m in teamRoster" :key="m.chat_id">
|
||||
<div class="hm-team-card" :class="{ online: m.online || m.pa_mode }">
|
||||
<span class="hm-status-dot" :class="m.online ? 'on' : 'off'"></span>
|
||||
<span class="hm-role-badge" :class="m.role === 'CEO' ? 'hm-role-ceo' : 'hm-role-cto'" x-text="m.role"></span>
|
||||
<div class="hm-person">
|
||||
<strong x-text="m.name"></strong>
|
||||
<small>
|
||||
<span x-text="m.chat_id"></span>
|
||||
<span x-show="m.pa_mode" class="tag tag-pa">PA</span>
|
||||
<span x-show="m.message_count" x-text="' · ' + m.message_count + ' msgs'"></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:0.7rem;letter-spacing:0.12em;text-transform:uppercase;color:#64748b;margin:1rem 0 0.5rem">Filter chat</h3>
|
||||
<button class="btn btn-sm" @click="selectChat(null)" :class="{ 'btn-primary': !selectedChatId }">Alle chats</button>
|
||||
<template x-for="m in teamRoster" :key="'c'+m.chat_id">
|
||||
<button class="hermes-conv-btn" @click="selectChat(m.chat_id)" :class="{ active: selectedChatId == m.chat_id }">
|
||||
<span class="conv-role hm-role-badge" :class="m.role === 'CEO' ? 'hm-role-ceo' : 'hm-role-cto'" x-text="m.role"></span>
|
||||
<span x-text="m.name"></span>
|
||||
</button>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<section class="panel hermes-feed">
|
||||
<div class="panel-header">
|
||||
<h3>Telegram live feed</h3>
|
||||
<span class="text-muted" x-text="live ? '🟢 Live (5s)' : '⏸'"></span>
|
||||
<button class="btn btn-sm" @click="refreshFeed()">↻ Refresh</button>
|
||||
</div>
|
||||
<div class="hermes-messages">
|
||||
<template x-for="msg in messages" :key="msg.id">
|
||||
<div class="hermes-msg" :class="msg.direction === 'in' ? 'msg-in' : 'msg-out'">
|
||||
<div class="hermes-msg-meta">
|
||||
<span x-text="msg.direction === 'in' ? '← IN' : '→ OUT'"></span>
|
||||
<span x-text="msgLabel(msg)"></span>
|
||||
<span x-text="formatTime(msg.created_at)"></span>
|
||||
<span class="badge" x-show="msg.has_embedding">vector</span>
|
||||
<span class="badge" x-show="msg.agent_name" x-text="msg.agent_name"></span>
|
||||
</div>
|
||||
<div class="hermes-msg-body" x-text="msg.content_text || '(' + (msg.content_type || 'empty') + ')'"></div>
|
||||
</div>
|
||||
</template>
|
||||
<p x-show="!messages.length" class="text-muted">Nog geen berichten — stuur iets via @klaploper_bot</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- PA LIVE -->
|
||||
<div x-show="tab === 'pa'" class="panel">
|
||||
<div class="hermes-pa-header">
|
||||
<div>
|
||||
<strong>Personal PA — 4 site browsers</strong>
|
||||
<p class="text-muted" style="margin:0.25rem 0 0" x-text="paLive.query ? 'Zoekt: ' + paLive.query : 'Wacht op @PA via Telegram'"></p>
|
||||
</div>
|
||||
<span class="status-badge" :class="paLive.status || 'idle'" x-text="paStatusLabel()"></span>
|
||||
<span class="text-muted" x-show="paLiveUser" x-text="paLiveUser"></span>
|
||||
<button class="btn btn-sm btn-primary" @click="refreshPaLive()">↻ Refresh</button>
|
||||
</div>
|
||||
<div class="hermes-pa-grid">
|
||||
<template x-for="slot in paSlots" :key="slot.label">
|
||||
<div class="hermes-pa-slot" :class="'status-' + (slot.status || 'idle')">
|
||||
<div class="hermes-pa-slot-head">
|
||||
<strong x-text="slot.label"></strong>
|
||||
<span class="status-badge" :class="slot.status || 'idle'" x-text="slotStatusLabel(slot.status)"></span>
|
||||
</div>
|
||||
<div class="hermes-pa-slot-body">
|
||||
<img x-show="slot.has_screenshot" :src="slotScreenshotUrl(slot.label)" :alt="slot.label" />
|
||||
<div class="hermes-pa-placeholder" x-show="!slot.has_screenshot && slot.status === 'waiting'">⏳ Wacht…</div>
|
||||
<div class="hermes-pa-placeholder" x-show="!slot.has_screenshot && slot.status === 'loading'">🔄 Bezig…</div>
|
||||
<div class="hermes-pa-placeholder" x-show="!slot.has_screenshot && (!slot.status || slot.status === 'idle')">— idle —</div>
|
||||
<div class="hermes-pa-placeholder" x-show="slot.status === 'failed'" x-text="slot.error || 'Mislukt'"></div>
|
||||
</div>
|
||||
<div class="hermes-pa-slot-foot" x-text="slot.title || slot.url || slot.label"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GRAPH -->
|
||||
<div x-show="tab === 'graph'" class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>Second brain — message graph</h3>
|
||||
<button class="btn btn-sm btn-primary" @click="loadGraph()">↻ Herlaad</button>
|
||||
<select class="form-input hermes-select" x-model="graphChatFilter" @change="loadGraph()">
|
||||
<option value="">Alle chats</option>
|
||||
<template x-for="m in teamRoster" :key="'g'+m.chat_id">
|
||||
<option :value="m.chat_id" x-text="m.role + ' — ' + m.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div id="hermes-graph" class="hermes-graph-canvas"></div>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH -->
|
||||
<div x-show="tab === 'search'" class="panel">
|
||||
<h3>Vector + full-text memory search</h3>
|
||||
<div class="hermes-search-bar">
|
||||
<input class="form-input" type="search" placeholder="Zoek in Telegram-geheugen…" x-model="searchQuery" @keydown.enter="runSearch()" />
|
||||
<button class="btn btn-primary" @click="runSearch()">Zoeken</button>
|
||||
</div>
|
||||
<div class="hermes-search-results">
|
||||
<template x-for="r in searchResults" :key="r.id">
|
||||
<div class="hermes-search-hit">
|
||||
<div class="hermes-msg-meta">
|
||||
<span x-text="msgLabel(r)"></span>
|
||||
<span x-text="formatTime(r.created_at)"></span>
|
||||
<span class="badge" x-text="'score ' + (r.score || 0).toFixed(2)"></span>
|
||||
</div>
|
||||
<p x-text="r.content_text"></p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTROL -->
|
||||
<div x-show="tab === 'control'" class="hermes-control-grid">
|
||||
<div class="panel">
|
||||
<h3>Hermes status</h3>
|
||||
<ul class="hermes-info-list">
|
||||
<li>Bot: <strong>@klaploper_bot</strong></li>
|
||||
<li>CEO: <strong>Aïssa</strong> · CTO: <strong>Mo</strong></li>
|
||||
<li>Control API: <span x-text="hermesOnline ? '🟢 Online' : '🔴 Offline'"></span></li>
|
||||
<li>Inbound: <span x-text="stats.inbound || 0"></span> · Outbound: <span x-text="stats.outbound || 0"></span></li>
|
||||
</ul>
|
||||
<div class="hermes-actions">
|
||||
<button class="btn btn-primary" @click="runAction('evening-briefing')" :disabled="actionBusy">🌙 Avondbriefing → CEO</button>
|
||||
<button class="btn" @click="runAction('morning-briefing')" :disabled="actionBusy">☀️ Ochtendbriefing</button>
|
||||
<button class="btn" @click="runAction('resume-jobs')" :disabled="actionBusy">🔧 Herstel jobs</button>
|
||||
</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>
|
||||
<p x-show="actionMsg" class="text-muted" style="margin-top:0.5rem" x-text="actionMsg"></p>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Team</h3>
|
||||
<template x-for="m in teamRoster" :key="'t'+m.chat_id">
|
||||
<div class="hm-team-card" :class="{ online: m.online }">
|
||||
<span class="hm-role-badge" :class="m.role === 'CEO' ? 'hm-role-ceo' : 'hm-role-cto'" x-text="m.role"></span>
|
||||
<div class="hm-person"><strong x-text="m.name"></strong><small x-text="m.chat_id"></small></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="panel hermes-events">
|
||||
<h3>Agent events</h3>
|
||||
<template x-for="ev in agentEvents" :key="ev.id">
|
||||
<div class="hermes-event">
|
||||
<strong x-text="ev.title"></strong>
|
||||
<span class="badge" x-text="ev.agent_name"></span>
|
||||
<small x-text="formatTime(ev.created_at)"></small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/hermes-ui.js"></script>
|
||||
<script src="/static/js/hermes-ui.js?v=11"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user