Add live CEO briefing, Wereldexport CRM, halal engine, and realtime cockpit.

Ship export intel with contact filters and CRM push, Herman live digest with Telegram briefing, halal recommendation engine, revenue cockpit, and dashboard polling/WebSocket fixes.
This commit is contained in:
Aissa
2026-07-19 18:25:01 +00:00
parent 544d9e611d
commit 4dd1b8265f
25 changed files with 2757 additions and 108 deletions
+41 -10
View File
@@ -2,7 +2,7 @@
{% 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" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=17" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=21" />
{% endblock %}
{% block content %}
<div class="herman-shell hm-neo">
@@ -93,8 +93,8 @@
<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>
<h4>Samenvatting · live</h4>
<div id="briefing-summary-text" class="live-digest-body"></div>
</div>
<div class="briefing-card" style="max-height:220px;overflow-y:auto">
<h4>📋 Herman activiteitenlog</h4>
@@ -252,7 +252,7 @@
{% block scripts %}
<script src="/static/js/viz-engine.js?v=3"></script>
<script src="/static/js/dashboard-layout.js?v=4"></script>
<script src="/static/js/briefing-charts.js?v=13"></script>
<script src="/static/js/briefing-charts.js?v=19"></script>
<script>
localStorage.setItem('foodlinkk-persona', 'ceo');
const INITIAL_BRIEFING = {{ briefing_payload | tojson }};
@@ -264,7 +264,9 @@ function dashboardBriefing() {
rssItems: [], rssTotal: 0, rssCategory: '', rssSearch: '', rssLoading: false,
rssRegulations: {}, rssBookmarks: [],
content: INITIAL_BRIEFING.content || '', createdAt: INITIAL_BRIEFING.created_at || '',
liveDigest: INITIAL_BRIEFING.live_digest || null,
_pollStop: null, _wsStop: null, _vizMode: 'neo-bars', _stats: null,
_liveRefreshing: false, _pollTick: 0,
_prefs: null, _setDragMode: null, _saveTimer: null,
applyDashboardPrefs(prefs) {
this._prefs = prefs;
@@ -274,7 +276,7 @@ function dashboardBriefing() {
DashboardLayout.applyOrder(dash, order);
DashboardLayout.applyVisibility(dash, prefs.dashboard_widgets);
if (this._setDragMode) this._setDragMode(this.editLayout);
if (this._stats) BriefingCharts.renderLive(dash, this._stats, this._vizMode, window._dashboardKpis);
if (this._stats) BriefingCharts.renderLive(dash, this._stats, this._vizMode, window._dashboardKpis, this.liveDigest);
},
scheduleSave(patch) {
if (!this._prefs) return;
@@ -375,9 +377,14 @@ function dashboardBriefing() {
if (cnt < 3) { sessionStorage.setItem('rss_auto_refreshed', '1'); await this.refreshRss(false); }
}
await this.loadProjectsMini();
if (this.content) BriefingCharts.renderText(dash, this.content, this.createdAt);
if (this.liveDigest) {
BriefingCharts.renderLiveSummary(this.liveDigest, false);
} else if (this.content) {
BriefingCharts.renderText(dash, this.content, this.createdAt);
}
window._refreshBriefingLive = (t) => this.refreshLive(t);
this._pollStop = CockpitLive.startPolling(() => { this.refreshLive(false); }, 30000);
this._pollStop = CockpitLive.startPolling(() => { this.refreshLive(false); }, 20000);
this._wsStop = CockpitLive.connectFeed(() => { this.refreshLive(false); });
if (this.hermanTab === 'rss') await this.loadRssTab();
},
async refreshRss(toast) {
@@ -448,12 +455,34 @@ function dashboardBriefing() {
'<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>';
},
updateLiveMeta(at, digest) {
const tsRaw = at || (digest && digest.updated_at) || '';
const ts = String(tsRaw).substring(0, 19).replace('T', ' ');
const meta = document.getElementById('briefing-meta');
if (meta && ts) {
const n = digest && digest.activity_count != null ? ' · ' + digest.activity_count + ' activiteiten' : '';
meta.textContent = 'Live · ververst ' + ts + ' UTC' + n;
}
const badge = document.querySelector('.herman-hero .live-badge');
if (badge && tsRaw) {
try {
badge.textContent = 'Live · ' + new Date(tsRaw).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });
} catch (e) {}
}
},
async refreshLive(toast) {
if (this._liveRefreshing) return;
this._liveRefreshing = true;
const isPoll = toast === false;
if (isPoll) this._pollTick = (this._pollTick || 0) + 1;
if (toast !== false) Cockpit.toast('Live data ophalen…', 'info');
try {
const r = await fetch('/api/herman/briefing/stats').then(x => x.json());
const light = isPoll && (this._pollTick % 6 !== 0);
const url = light ? '/api/herman/briefing/stats?light=1' : '/api/herman/briefing/stats';
const r = await fetch(url).then(x => x.json());
if (r.stats) {
this._stats = r.stats;
if (r.live_digest) this.liveDigest = r.live_digest;
try {
const live = await fetch('/api/retail/rss/live?limit=25').then(x => x.json());
if (live.items && live.items.length) {
@@ -462,13 +491,14 @@ function dashboardBriefing() {
this.rssTotal = this._stats.rss_items;
}
} catch (e) {}
BriefingCharts.renderLive(document.getElementById('briefing-dashboard'), this._stats, this._vizMode, window._dashboardKpis);
BriefingCharts.renderLive(document.getElementById('briefing-dashboard'), this._stats, this._vizMode, window._dashboardKpis, this.liveDigest, { pollRefresh: isPoll });
this.renderClientsMini(this._stats);
this.renderActivityLog(this._stats.activity_log || []);
document.getElementById('briefing-meta').textContent = 'Live sync: ' + (r.at || '').substring(0, 19).replace('T', ' ') + ' UTC';
this.updateLiveMeta(r.at, this.liveDigest);
}
if (toast !== false) Cockpit.toast('Live data bijgewerkt', 'success');
} catch (e) { if (toast !== false) Cockpit.toast(e.message, 'error'); }
finally { this._liveRefreshing = false; }
},
pushLiveEntry(entry) {
this.liveLog.push(entry);
@@ -523,6 +553,7 @@ function dashboardBriefing() {
this.content = result.content || '';
this.createdAt = result.generated_at || new Date().toISOString();
this._stats = result.stats;
if (result.stats && result.stats.live_digest) this.liveDigest = result.stats.live_digest;
BriefingCharts.render(document.getElementById('briefing-dashboard'), result.stats, result.content, this.createdAt, this._vizMode);
this.renderClientsMini(result.stats || {});
this.renderActivityLog((result.stats || {}).activity_log || []);