Files
foodlinkk-command-center/cockpit/static/js/hermes-ui.js
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

234 lines
7.2 KiB
JavaScript

/**
* Hermes technical KPI dashboard + bucket explorer popup
*/
function hermesKpiDashboard() {
const SOURCE_LABELS = {
telegram: 'Telegram',
api_server: 'Cockpit API',
tui: 'TUI',
cron: 'Cron',
subagent: 'Subagent',
};
return {
loading: false,
kpis: { online: false, overview: {}, buckets: [], tools_top: [], sources: [], herman_cockpit: [] },
chartTools: null,
chartSources: null,
_timer: null,
modal: {
open: false,
loading: false,
previewLoading: false,
key: null,
title: '',
tab: 'activity',
toolFilter: null,
detail: { events: [], files: [], parts: {}, count: 0 },
explorer: { path: '', parent: null, entries: [] },
selectedEvent: null,
preview: null,
},
get ov() { return this.kpis.overview || {}; },
fmt(n) {
const v = Number(n || 0);
return v.toLocaleString('nl-NL');
},
fmtBytes(n) {
if (n == null || n === '') return '';
const v = Number(n);
if (v < 1024) return v + ' B';
if (v < 1024 * 1024) return (v / 1024).toFixed(1) + ' KB';
return (v / (1024 * 1024)).toFixed(1) + ' MB';
},
formatTime(iso) {
if (!iso) return '';
try {
return new Date(iso).toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' });
} catch (e) { return String(iso).slice(0, 19); }
},
partsLine(parts) {
if (!parts) return '';
return Object.keys(parts)
.filter((k) => parts[k] > 0)
.map((k) => parts[k] + ' ' + k.replace(/_/g, ' '))
.join(' · ');
},
sourceLabel(s) { return SOURCE_LABELS[s] || s || '—'; },
async init() {
await this.refresh();
this._timer = setInterval(() => this.refresh(true), 15000);
},
async refresh(quiet) {
if (!quiet) this.loading = true;
try {
const data = await Cockpit.api('/hermes/kpis');
this.kpis = data || this.kpis;
this.$nextTick(() => this.renderCharts());
} catch (e) {
if (!quiet && window.Cockpit) Cockpit.toast(e.message || 'KPI laden mislukt', 'error');
this.kpis.online = false;
}
this.loading = false;
},
async openBucket(b) {
this.modal.open = true;
this.modal.key = b.key;
this.modal.title = b.label;
this.modal.tab = b.key === 'files' ? 'touched' : 'activity';
this.modal.toolFilter = null;
this.modal.selectedEvent = null;
this.modal.preview = null;
this.modal.detail = { events: [], files: [], parts: b.parts || {}, count: b.count || 0 };
await this.loadBucketDetail();
if (b.key === 'files') {
// preload roots quietly
this.loadExplorer('').catch(() => {});
}
},
closeModal() {
this.modal.open = false;
this.modal.preview = null;
this.modal.selectedEvent = null;
},
async setToolFilter(tool) {
this.modal.toolFilter = tool;
await this.loadBucketDetail();
},
async loadBucketDetail() {
if (!this.modal.key) return;
this.modal.loading = true;
try {
const q = new URLSearchParams({ limit: '80' });
if (this.modal.toolFilter) q.set('tool', this.modal.toolFilter);
const data = await Cockpit.api('/hermes/bucket/' + encodeURIComponent(this.modal.key) + '?' + q.toString());
this.modal.detail = data || this.modal.detail;
if (!(this.modal.detail.events || []).length) {
this.modal.selectedEvent = null;
}
} catch (e) {
if (window.Cockpit) Cockpit.toast(e.message || 'Detail laden mislukt', 'error');
}
this.modal.loading = false;
},
selectEvent(ev) {
this.modal.selectedEvent = ev;
this.modal.preview = null;
if (ev.paths && ev.paths.length === 1 && this.modal.key === 'files') {
this.openFile(ev.paths[0]);
}
},
openExplorerTab() {
this.modal.tab = 'explorer';
if (!(this.modal.explorer.entries || []).length) {
this.loadExplorer(this.modal.explorer.path || '');
}
},
async loadExplorer(path) {
this.modal.loading = true;
try {
const q = path ? ('?path=' + encodeURIComponent(path)) : '';
const data = await Cockpit.api('/hermes/explorer' + q);
this.modal.explorer = data || { path: '', parent: null, entries: [] };
} catch (e) {
if (window.Cockpit) Cockpit.toast(e.message || 'Verkenner laden mislukt', 'error');
}
this.modal.loading = false;
},
explorerUp() {
if (this.modal.explorer.parent) this.loadExplorer(this.modal.explorer.parent);
else this.loadExplorer('');
},
async openFile(path) {
if (!path) return;
this.modal.previewLoading = true;
this.modal.selectedEvent = null;
try {
const data = await Cockpit.api('/hermes/explorer/file?path=' + encodeURIComponent(path));
this.modal.preview = data || { path, error: 'Geen data', name: path.split('/').pop() };
if (this.modal.key === 'files' && this.modal.tab === 'activity') {
// keep tab; preview still shows
}
} catch (e) {
this.modal.preview = { path, name: path.split('/').pop(), error: e.message || 'Laden mislukt', content: null };
}
this.modal.previewLoading = false;
},
renderCharts() {
if (typeof Chart === 'undefined') return;
const tools = (this.kpis.tools_top || []).slice(0, 8);
const sources = this.kpis.sources || [];
const toolsCtx = document.getElementById('hm-chart-tools');
if (toolsCtx) {
if (this.chartTools) this.chartTools.destroy();
this.chartTools = new Chart(toolsCtx, {
type: 'bar',
data: {
labels: tools.map((t) => t.name),
datasets: [{
label: 'Calls',
data: tools.map((t) => t.count),
backgroundColor: 'rgba(14, 165, 233, 0.55)',
borderColor: 'rgba(56, 189, 248, 0.9)',
borderWidth: 1,
}],
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(148,163,184,0.12)' } },
y: { ticks: { color: '#cbd5e1', font: { size: 10 } }, grid: { display: false } },
},
},
});
}
const srcCtx = document.getElementById('hm-chart-sources');
if (srcCtx) {
if (this.chartSources) this.chartSources.destroy();
this.chartSources = new Chart(srcCtx, {
type: 'doughnut',
data: {
labels: sources.map((s) => this.sourceLabel(s.source)),
datasets: [{
data: sources.map((s) => s.tools || 0),
backgroundColor: ['#0ea5e9', '#a855f7', '#f59e0b', '#22c55e', '#f43f5e', '#64748b'],
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { color: '#94a3b8', boxWidth: 10, font: { size: 10 } } },
},
},
});
}
},
};
}
function hermesControl() { return hermesKpiDashboard(); }