5e43b6b246
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).
234 lines
7.2 KiB
JavaScript
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(); }
|