5d60d33db1
Volledige Foodlinkk Command Center uitbreiding met social automatisering, reclamefolder filters, Proxmox monitoring en documentatie.
245 lines
12 KiB
HTML
245 lines
12 KiB
HTML
{% 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 %}
|