2026-06-09 00:41:27 +00:00
{% extends "base.html" %}
{% block extra_head %}
< script src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" ></ script >
< link rel = "stylesheet" href = "/static/css/hermes.css" />
2026-06-09 10:41:13 +00:00
< link rel = "stylesheet" href = "/static/css/herman-dashboard.css?v=13" />
2026-06-09 00:41:27 +00:00
{% endblock %}
{% block content %}
< div class = "herman-shell hm-neo" >
< div class = "herman-hero" >
< h1 > Herman · Command Center < span class = "live-badge" > Live</ span ></ h1 >
2026-06-09 10:41:13 +00:00
< p class = "hero-sub" > CEO overzicht · trending retail · briefing · platform links</ p >
< div class = "hm-hero-links beurs-mini-row" style = "margin-top:0.85rem" >
< a href = "http://10.4.7.18:3001/aissa/foodlinkk-command-center" target = "_blank" rel = "noopener" class = "beurs-mini-chip" > 📦 Gitea</ a >
< a href = "/ops" class = "beurs-mini-chip" > 🖥️ IT Ops</ a >
< a href = "/packaging" class = "beurs-mini-chip" > 📐 Packaging</ a >
< a href = "/marketing?tab=publish" class = "beurs-mini-chip" > 🚀 Automatisering</ a >
< a href = "/marketing?tab=reclame" class = "beurs-mini-chip" > 📁 Reclame folders</ a >
< a href = "/agents?tab=mesh" class = "beurs-mini-chip" > 🕸️ Agent mesh</ a >
< a href = "/settings?tab=social" class = "beurs-mini-chip" > ⚙️ Social API's</ a >
< a href = "/projects" class = "beurs-mini-chip" > 📁 Projecten</ a >
< a href = "http://10.4.7.18:8700/docs" target = "_blank" rel = "noopener" class = "beurs-mini-chip" > 📡 Tools API</ a >
</ div >
2026-06-09 00:41:27 +00:00
</ div >
< section class = "panel herman-briefing" x-data = "dashboardBriefing()" x-init = "init()" >
< div class = "briefing-loading-overlay" x-show = "loading" x-cloak >
< div class = "loading-dots" >< span ></ span >< span ></ span >< span ></ span ></ div >
< p x-text = "loadingMsg" ></ p >
</ div >
< header class = "herman-briefing-header" >
< div >
< h2 > Dagelijkse briefing</ h2 >
< div class = "briefing-meta" id = "briefing-meta" ></ div >
</ div >
< div class = "herman-briefing-actions" >
2026-06-09 10:41:13 +00:00
< button type = "button" class = "btn btn-sm" @ click = "editLayout = !editLayout" x-text = "editLayout ? 'Klaar ordenen' : '⋮⋮ Ordenen'" ></ button >
2026-06-09 00:41:27 +00:00
< button type = "button" class = "btn btn-pulse" @ click = "refreshLive()" :disabled = "loading" > ↻ Live data</ button >
< button type = "button" class = "btn btn-pulse-green" @ click = "generate()" :disabled = "loading" > ✨ Genereer dagrapport</ button >
< a href = "/clients" class = "btn btn-pulse-green" > CRM Clients</ a >
< a href = "/marketing?tab=reclame" class = "btn btn-pulse-purple" > Reclame folders</ a >
< a href = "/analytics" class = "btn" > Analytics</ a >
</ div >
</ header >
2026-06-09 10:41:13 +00:00
< div id = "viz-toolbar" class = "viz-toolbar" ></ div >
2026-06-09 00:41:27 +00:00
< div id = "briefing-dashboard" class = "hm-dash-layout" >
2026-06-09 10:41:13 +00:00
< section class = "hm-dash-section hm-dash-full hm-widget-draggable" data-widget = "kpis" >
< div class = "hm-widget-handle" x-show = "editLayout" > Sleep widget · CEO KPI's</ div >
2026-06-09 00:41:27 +00:00
< h3 class = "hm-section-title" >< span class = "hm-live-dot" ></ span > CEO KPI's</ h3 >
< div id = "briefing-kpis" class = "hm-neo-kpi-row" ></ div >
</ section >
2026-06-09 10:41:13 +00:00
< section class = "hm-dash-section hm-dash-full hm-widget-draggable" data-widget = "executive" >
< div class = "hm-widget-handle" x-show = "editLayout" > Sleep widget · Executive</ div >
2026-06-09 00:41:27 +00:00
< h3 class = "hm-section-title" >< span class = "hm-live-dot" ></ span > Alles op een rij</ h3 >
< div class = "hm-chart-panel" >
2026-06-09 10:41:13 +00:00
< div class = "hm-chart-head" >< h4 > Executive samenvatting</ h4 >< small > live database</ small ></ div >
2026-06-09 00:41:27 +00:00
< div id = "briefing-executive-summary" ></ div >
</ div >
< div id = "briefing-clients-mini" class = "beurs-mini-row" style = "margin-top:0.75rem" ></ div >
</ section >
2026-06-09 10:41:13 +00:00
< section class = "hm-dash-section hm-dash-full hm-widget-draggable" data-widget = "briefing" >
< div class = "hm-widget-handle" x-show = "editLayout" > Sleep widget · Briefing</ div >
2026-06-09 00:41:27 +00:00
< h3 class = "hm-section-title" > Herman briefing</ h3 >
< div class = "briefing-summary-grid" >
< div class = "briefing-card summary" >
< h4 > Samenvatting</ h4 >
< p id = "briefing-summary-text" class = "briefing-typewriter" ></ p >
</ div >
2026-06-09 10:41:13 +00:00
< div class = "briefing-card" style = "max-height:220px;overflow-y:auto" >
< h4 > 📋 Herman activiteitenlog</ h4 >
< ul id = "briefing-activity-log" class = "hm-exec-list" style = "margin:0;padding-left:1.1rem;font-size:0.82rem" ></ ul >
</ div >
2026-06-09 00:41:27 +00:00
< div class = "briefing-horizon-col" >
< div class = "horizon-card short" >
< h4 > ⚡ Korte termijn</ h4 >
< ul id = "briefing-actions-list" ></ ul >
</ div >
< div class = "horizon-card long" >
< h4 > 🎯 Lange termijn</ h4 >
< ul id = "briefing-longterm-list" ></ ul >
</ div >
</ div >
</ div >
</ section >
2026-06-09 10:41:13 +00:00
< section class = "hm-dash-section hm-widget-draggable" data-widget = "retail" >
< div class = "hm-widget-handle" x-show = "editLayout" > Sleep widget · Retail</ div >
2026-06-09 00:41:27 +00:00
< h3 class = "hm-section-title" > Retail operatie</ h3 >
< div class = "hm-dash-two-col" >
< div class = "hm-chart-panel" >
2026-06-09 10:41:13 +00:00
< div class = "hm-chart-head" >< h4 > Trending food retail</ h4 >< small >< a href = "/marketing" > Marketing Hub →</ a ></ small ></ div >
2026-06-09 00:41:27 +00:00
< div id = "briefing-retail" ></ div >
2026-06-09 10:41:13 +00:00
< div class = "hm-eq" >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span ></ div >
2026-06-09 00:41:27 +00:00
</ div >
< div class = "hm-chart-panel" >
< div class = "hm-chart-head" >< h4 > Sales milestones</ h4 >< small > live DB</ small ></ div >
< div id = "briefing-milestones" ></ div >
</ div >
</ div >
< div class = "hm-chart-panel hm-dash-trends-only" style = "margin-top:1rem" >
< div class = "hm-chart-head" >< h4 > Markt highlights</ h4 >< small > M& A · retail</ small ></ div >
< div id = "briefing-food-highlights" class = "hm-highlight-scroll" ></ div >
</ div >
</ section >
2026-06-09 10:41:13 +00:00
< section class = "hm-dash-section hm-dash-full hm-dash-analytics hm-widget-draggable" data-widget = "analytics" >
< div class = "hm-widget-handle" x-show = "editLayout" > Sleep widget · Analytics</ div >
< h3 class = "hm-section-title" >< span class = "hm-live-dot" ></ span > Data & analytics</ h3 >
2026-06-09 00:41:27 +00:00
< div class = "hm-neo-charts" >
2026-06-09 10:41:13 +00:00
< div class = "hm-chart-panel hm-analytics-panel" >
2026-06-09 00:41:27 +00:00
< div class = "hm-chart-head" >< h4 > Pipeline</ h4 >< small > live deals</ small ></ div >
< div class = "hm-chart-wrap" >< canvas id = "chart-pipeline" ></ canvas ></ div >
2026-06-09 10:41:13 +00:00
< div class = "hm-eq" >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span ></ div >
2026-06-09 00:41:27 +00:00
</ div >
2026-06-09 10:41:13 +00:00
< div class = "hm-chart-panel hm-analytics-panel page-viz-panel viz-mode-chart" >
< div class = "hm-chart-head" >< h4 > NAS sentiment</ h4 >< small id = "viz-label-sentiment" > neo-bars</ small ></ div >
< div class = "page-viz-body" >
< div class = "hm-chart-wrap page-viz-canvas" >< canvas id = "chart-sentiment" ></ canvas ></ div >
< div id = "viz-sentiment-alt" class = "viz-alt" ></ div >
</ div >
2026-06-09 00:41:27 +00:00
</ div >
2026-06-09 10:41:13 +00:00
< div class = "hm-chart-panel hm-analytics-panel" >
2026-06-09 00:41:27 +00:00
< div class = "hm-chart-head" >< h4 > NAS top woorden</ h4 >< small > corpus</ small ></ div >
< div class = "hm-chart-wrap" >< canvas id = "chart-words" ></ canvas ></ div >
2026-06-09 10:41:13 +00:00
< div class = "hm-eq hm-eq-cyan" >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span >< span ></ span ></ div >
2026-06-09 00:41:27 +00:00
</ div >
2026-06-09 10:41:13 +00:00
< div class = "hm-chart-panel hm-analytics-panel page-viz-panel viz-mode-chart" >
< div class = "hm-chart-head" >< h4 > Agent activiteit</ h4 >< small id = "viz-label-agents" > neo-bars</ small ></ div >
< div class = "page-viz-body" >
< div class = "hm-chart-wrap page-viz-canvas" >< canvas id = "chart-agents" ></ canvas ></ div >
< div id = "viz-agents-alt" class = "viz-alt" ></ div >
</ div >
2026-06-09 00:41:27 +00:00
</ div >
</ div >
</ section >
2026-06-09 10:41:13 +00:00
< section class = "hm-dash-section hm-dash-full hm-widget-draggable" data-widget = "approvals" id = "widget-approvals" >
< div class = "hm-widget-handle" x-show = "editLayout" > Sleep widget · Goedkeuringen</ div >
< h3 class = "hm-section-title" >< span class = "hm-live-dot" ></ span > Agent goedkeuringen < span class = "live-badge" id = "approvals-pending-badge" style = "display:none" > 0 open</ span ></ h3 >
< div class = "panel" style = "background:transparent;border:none;padding:0" >
< p class = "page-tip" > SysOps vraagt dagelijks toestemming voor < strong > update-scan</ strong > en < strong > Gitea backup</ strong > . Keur goed → actie wordt automatisch uitgevoerd.</ p >
< div id = "dash-approvals-list" ></ div >
</ div >
</ section >
2026-06-09 00:41:27 +00:00
2026-06-09 10:41:13 +00:00
< div class = "hub-grid hm-dash-feed hm-widget-draggable" data-widget = "feed" >
< div class = "hm-widget-handle" x-show = "editLayout" > Sleep widget · Feed</ div >
< section class = "panel" >
< h2 > Agent feed < span class = "live-badge" id = "agent-feed-badge" > Live</ span ></ h2 >
< div id = "agent-feed-live" >
{% for ev in agent_feed[:12] %}
< div class = "hub-feed-item" >
< span > {{ ev.created_at[11:16] if ev.created_at else '' }}</ span >
< span class = "badge badge-agent-{{ ev.agent_name|lower }}" > {{ ev.agent_name }}</ span >
{{ ev.title or ev.event_type }}
</ div >
{% else %}< p class = "empty-state" > Nog geen events.</ p > {% endfor %}
</ div >
</ section >
< section class = "panel" >
< h2 > Projecten < a href = "/projects" class = "btn btn-sm" style = "float:right" > Alle projecten →</ a ></ h2 >
< div id = "dash-projects-mini" >< p class = "muted" > Laden…</ p ></ div >
</ section >
2026-06-09 00:41:27 +00:00
</ div >
2026-06-09 10:41:13 +00:00
</ div >
</ section >
2026-06-09 00:41:27 +00:00
</ div >
{% endblock %}
{% block scripts %}
2026-06-09 10:41:13 +00:00
< script src = "/static/js/viz-engine.js?v=3" ></ script >
< script src = "/static/js/dashboard-layout.js?v=1" ></ script >
< script src = "/static/js/briefing-charts.js?v=11" ></ script >
2026-06-09 00:41:27 +00:00
< script >
const INITIAL_BRIEFING = {{ briefing_payload | tojson }};
function dashboardBriefing () {
return {
2026-06-09 10:41:13 +00:00
loading : false , loadingMsg : 'Herman werkt…' , editLayout : false ,
2026-06-09 00:41:27 +00:00
content : INITIAL_BRIEFING . content || '' , createdAt : INITIAL_BRIEFING . created_at || '' ,
2026-06-09 10:41:13 +00:00
_pollStop : null , _wsStop : null , _vizMode : 'neo-bars' , _stats : null ,
2026-06-09 00:41:27 +00:00
async init () {
2026-06-09 10:41:13 +00:00
window . _dashboardVizMode = this . _vizMode ;
const prefs = await DashboardLayout . loadPrefs ();
this . _vizMode = prefs . global_viz_mode || 'neo-bars' ;
window . _dashboardVizMode = this . _vizMode ;
DashboardLayout . applyOrder ( document . getElementById ( 'briefing-dashboard' ), prefs . dashboard_layout || []);
DashboardLayout . initDrag ( document . getElementById ( 'briefing-dashboard' ), ( order ) => DashboardLayout . saveLayout ( order ));
DashboardLayout . buildVizSelector ( document . getElementById ( 'viz-toolbar' ), prefs , async ( mode ) => {
this . _vizMode = mode ;
window . _dashboardVizMode = mode ;
await DashboardLayout . saveViz ( mode );
document . getElementById ( 'viz-label-sentiment' ). textContent = mode ;
document . getElementById ( 'viz-label-agents' ). textContent = mode ;
if ( this . _stats ) BriefingCharts . renderLive ( document . getElementById ( 'briefing-dashboard' ), this . _stats , mode );
});
2026-06-09 00:41:27 +00:00
await this . refreshLive ( false );
2026-06-09 10:41:13 +00:00
await this . loadApprovals ();
await this . loadProjectsMini ();
2026-06-09 00:41:27 +00:00
if ( this . content ) BriefingCharts . renderText ( document . getElementById ( 'briefing-dashboard' ), this . content , this . createdAt );
window . _refreshBriefingLive = ( t ) => this . refreshLive ( t );
2026-06-09 10:41:13 +00:00
this . _pollStop = CockpitLive . startPolling (() => { this . refreshLive ( false ); this . loadApprovals (); }, 30000 );
2026-06-09 00:41:27 +00:00
this . _wsStop = CockpitLive . connectFeed (( data ) => {
if ( data . events ) CockpitLive . renderAgentFeed ( document . getElementById ( 'agent-feed-live' ), data . events );
CockpitLive . updateLiveBadge ( document . getElementById ( 'agent-feed-badge' ), new Date (). toLocaleTimeString ( 'nl-NL' , { hour : '2-digit' , minute : '2-digit' , timeZone : 'Europe/Amsterdam' }));
});
},
2026-06-09 10:41:13 +00:00
async loadApprovals () {
try {
const r = await fetch ( '/api/agents/approvals?status=pending&limit=10' ). then ( x => x . json ());
const el = document . getElementById ( 'dash-approvals-list' );
const badge = document . getElementById ( 'approvals-pending-badge' );
if ( ! el ) return ;
const items = r . items || [];
if ( badge ) {
badge . style . display = items . length ? 'inline' : 'none' ;
badge . textContent = items . length + ' open' ;
}
const actionLabel = ( t ) => ({ maintenance_scan : '🔍 Update-scan' , config_backup : '💾 Gitea backup' }[ t ] || t );
if ( ! items . length ) { el . innerHTML = '<p class="empty-state">Geen open goedkeuringen 🎉</p>' ; return ; }
el . innerHTML = '<table class="data-table"><thead><tr><th>Agent</th><th>Type</th><th>Verzoek</th><th></th></tr></thead><tbody>' +
items . map ( i => '<tr class="' + ( i . action_type === 'maintenance_scan' ? 'approval-row-scan' : '' ) + '"><td>@' + i . agent_key + '</td><td>' + actionLabel ( i . action_type ) + '</td><td>' + i . title + '</td><td class="approval-dash-row">' +
'<button class="btn btn-sm btn-approve" data-aid="' + i . id + '">✓ Goedkeuren</button>' +
'<button class="btn btn-sm btn-reject" data-rid="' + i . id + '">✗</button></td></tr>' ). join ( '' ) +
'</tbody></table><p class="page-tip" style="margin-top:0.5rem"><a href="/agents?tab=approvals">Volledige queue →</a></p>' ;
el . querySelectorAll ( '[data-aid]' ). forEach ( btn => btn . addEventListener ( 'click' , async () => {
await Cockpit . api ( '/api/agents/approvals/' + btn . dataset . aid + '/approve' , { method : 'POST' , body : '{}' });
Cockpit . toast ( 'Goedgekeurd' , 'success' ); await this . loadApprovals ();
}));
el . querySelectorAll ( '[data-rid]' ). forEach ( btn => btn . addEventListener ( 'click' , async () => {
await Cockpit . api ( '/api/agents/approvals/' + btn . dataset . rid + '/reject' , { method : 'POST' , body : JSON . stringify ({ reason : '' }) });
Cockpit . toast ( 'Afgewezen' , 'info' ); await this . loadApprovals ();
}));
} catch ( e ) {}
},
async loadProjectsMini () {
try {
const r = await fetch ( '/api/projects?limit=5' ). then ( x => x . json ());
const el = document . getElementById ( 'dash-projects-mini' );
if ( ! el ) return ;
const items = r . items || [];
el . innerHTML = items . length ? items . map ( p =>
'<a href="/projects?id=' + p . id + '" class="beurs-mini-chip"><strong>' + p . name + '</strong> · ' + ( p . asset_count || 0 ) + ' assets</a>'
). join ( '' ) : '<p class="muted">Nog geen projecten — <a href="/projects">maak aan</a></p>' ;
} catch ( e ) {}
},
renderActivityLog ( entries ) {
const el = document . getElementById ( 'briefing-activity-log' );
if ( ! el ) return ;
el . innerHTML = ( entries || []). length
? entries . slice ( 0 , 12 ). map ( e => '<li>' + e + '</li>' ). join ( '' )
: '<li class="muted">Nog geen agent-acties vandaag — Herman documenteert automatisch.</li>' ;
},
2026-06-09 00:41:27 +00:00
renderClientsMini ( stats ) {
const el = document . getElementById ( 'briefing-clients-mini' );
if ( ! el ) return ;
const active = stats . clients_active || 0 , total = stats . clients_total || stats . clients || 0 ;
el . innerHTML = '<a href="/clients" class="beurs-mini-chip up"><strong>' + active + '</strong> actief</a>' +
'<a href="/clients" class="beurs-mini-chip"><strong>' + total + '</strong> totaal klanten</a>' +
'<a href="/retail" class="beurs-mini-chip"><strong>' + ( stats . crm_partnerships || 0 ) + '</strong> partnerships</a>' +
'<a href="/marketing?tab=reclame" class="beurs-mini-chip"><strong>' + ( stats . promo_campaigns || 0 ) + '</strong> promo\'s</a>' ;
},
async refreshLive ( toast ) {
if ( toast !== false ) Cockpit . toast ( 'Live data ophalen…' , 'info' );
try {
const r = await fetch ( '/api/herman/briefing/stats' ). then ( x => x . json ());
if ( r . stats ) {
2026-06-09 10:41:13 +00:00
this . _stats = r . stats ;
BriefingCharts . renderLive ( document . getElementById ( 'briefing-dashboard' ), r . stats , this . _vizMode );
2026-06-09 00:41:27 +00:00
this . renderClientsMini ( r . stats );
2026-06-09 10:41:13 +00:00
this . renderActivityLog ( r . stats . activity_log || []);
2026-06-09 00:41:27 +00:00
document . getElementById ( 'briefing-meta' ). textContent = 'Live sync: ' + ( r . at || '' ). substring ( 0 , 19 ). replace ( 'T' , ' ' ) + ' UTC' ;
}
if ( toast !== false ) Cockpit . toast ( 'Live data bijgewerkt' , 'success' );
} catch ( e ) { if ( toast !== false ) Cockpit . toast ( e . message , 'error' ); }
},
async generate () {
this . loading = true ;
this . loadingMsg = 'Live data laden…' ;
await this . refreshLive ( false );
this . loadingMsg = 'Herman schrijft dagrapport…' ;
try {
const r = await Cockpit . api ( '/api/herman/briefing' , { method : 'POST' });
this . content = r . content || '' ;
this . createdAt = r . generated_at || new Date (). toISOString ();
2026-06-09 10:41:13 +00:00
BriefingCharts . render ( document . getElementById ( 'briefing-dashboard' ), r . stats , r . content , this . createdAt , this . _vizMode );
2026-06-09 00:41:27 +00:00
this . renderClientsMini ( r . stats || {});
Cockpit . toast ( 'Dagrapport klaar!' , 'success' );
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
this . loading = false ;
}
};
}
</ script >
{% endblock %}