Files
foodlinkk-command-center/cockpit/hermes.html
T
Aissa 5d60d33db1 Platform bundle: marketing publish, IT ops, packaging, agents mesh.
Volledige Foodlinkk Command Center uitbreiding met social automatisering,
reclamefolder filters, Proxmox monitoring en documentatie.
2026-06-09 00:41:27 +00:00

245 lines
12 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 · Telegram Command Center{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/hermes.css" />
<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">
<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>
</header>
<!-- Neo KPI row -->
<div class="hm-neo-kpi-row">
<div class="hm-neo-kpi" :class="{ highlight: tab === 'feed' }">
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</div>
<!-- Neo charts -->
<div class="hm-neo-charts">
<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>
</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>
<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>
{% endblock %}