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:
@@ -366,3 +366,8 @@
|
||||
.ei-contact-role { font-size: 0.72rem; color: #64748b; margin-left: 0.35rem; }
|
||||
.ei-drawer-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 1rem; }
|
||||
.ei-sync-msg { font-size: 0.75rem; color: #7dd3fc; margin-top: 0.5rem; }
|
||||
.ei-tab-panel-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
|
||||
.ei-link-btn { background: none; border: none; padding: 0; color: #38bdf8; cursor: pointer; font: inherit; text-align: left; }
|
||||
.ei-link-btn:hover { text-decoration: underline; }
|
||||
.ei-td-actions { white-space: nowrap; }
|
||||
.ei-load-more { display: flex; justify-content: center; padding: 1rem 0 0.5rem; }
|
||||
|
||||
@@ -34,11 +34,85 @@
|
||||
@media (max-width: 900px) { .hm-dash-two-col { grid-template-columns: 1fr; } }
|
||||
|
||||
.briefing-summary-grid {
|
||||
display: grid; grid-template-columns: 1.2fr 1fr; gap: 1rem;
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;
|
||||
}
|
||||
.briefing-card.summary { grid-column: 1 / -1; }
|
||||
.briefing-horizon-col { display: grid; gap: 1rem; }
|
||||
@media (max-width: 900px) { .briefing-summary-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.live-digest-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
@media (max-width: 1100px) { .live-digest-body { grid-template-columns: 1fr; } }
|
||||
|
||||
.live-digest-section {
|
||||
background: rgba(0, 0, 0, 0.22);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem 0.85rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.live-digest-section.brand-foodlinkk { border-left: 3px solid #fbbf24; }
|
||||
.live-digest-section.brand-cucina { border-left: 3px solid #fb923c; }
|
||||
.live-digest-section.brand-platform { border-left: 3px solid var(--hm-cyan); }
|
||||
.live-digest-section.brand-news { border-left: 3px solid #a855f7; }
|
||||
|
||||
.live-digest-section h5 {
|
||||
margin: 0 0 0.45rem;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
}
|
||||
.live-digest-section.brand-foodlinkk h5 { color: #fbbf24; }
|
||||
.live-digest-section.brand-cucina h5 { color: #fdba74; }
|
||||
.live-digest-section.brand-platform h5 { color: var(--hm-cyan); }
|
||||
.live-digest-section.brand-news h5 { color: #c4b5fd; }
|
||||
|
||||
.live-digest-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.live-digest-list li {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
color: #e2e8f0;
|
||||
padding-left: 0.65rem;
|
||||
border-left: 2px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.live-digest-list li.is-typing::after {
|
||||
content: '▋';
|
||||
display: inline-block;
|
||||
margin-left: 1px;
|
||||
color: var(--hm-cyan);
|
||||
animation: twCursorBlink 0.85s step-end infinite;
|
||||
font-weight: 400;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.live-digest-body.is-typing .live-digest-section {
|
||||
animation: twSectionIn 0.35s ease-out both;
|
||||
}
|
||||
@keyframes twCursorBlink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
@keyframes twSectionIn {
|
||||
from { opacity: 0.4; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.horizon-card ul li.is-typing::after {
|
||||
content: '▋';
|
||||
color: #ff9f43;
|
||||
animation: twCursorBlink 0.85s step-end infinite;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.briefing-card.summary,
|
||||
.horizon-card {
|
||||
background: var(--hm-panel); border-radius: 14px; padding: 1rem;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
window.BriefingCharts = (function () {
|
||||
var charts = {};
|
||||
var typewriterTimer = null;
|
||||
var typewriterAbort = null;
|
||||
var lastDigestSig = '';
|
||||
|
||||
function destroyAll() {
|
||||
Object.keys(charts).forEach(function (k) {
|
||||
@@ -393,17 +395,207 @@ window.BriefingCharts = (function () {
|
||||
var parsed = parseContent(content);
|
||||
var meta = document.getElementById('briefing-meta');
|
||||
if (meta && createdAt) meta.textContent = 'Laatst bijgewerkt: ' + String(createdAt).substring(0, 19).replace('T', ' ');
|
||||
typewriter(document.getElementById('briefing-summary-text'), parsed.summary, 8);
|
||||
var sumEl = document.getElementById('briefing-summary-text');
|
||||
if (sumEl && parsed.summary) {
|
||||
var lines = parsed.summary.split(/(?<=[.!?])\s+/).filter(Boolean);
|
||||
typewriterLiveDigest(sumEl, {
|
||||
sections: [{ brand: 'platform', title: 'Herman rapport', icon: '📝', lines: lines.length ? lines : [parsed.summary] }],
|
||||
}, { charMs: 10, linePause: 90 });
|
||||
}
|
||||
var actEl = document.getElementById('briefing-actions-list');
|
||||
if (actEl) actEl.innerHTML = parsed.actions.length ? parsed.actions.map(function (a) { return '<li>' + a + '</li>'; }).join('') : '<li>Genereer dagrapport voor actiepunten</li>';
|
||||
if (actEl) typewriterListItems(actEl, parsed.actions.length ? parsed.actions : ['Genereer dagrapport voor actiepunten'], { charMs: 8 });
|
||||
var longEl = document.getElementById('briefing-longterm-list');
|
||||
if (longEl) longEl.innerHTML = parsed.longTerm.length ? parsed.longTerm.map(function (a) { return '<li>' + a + '</li>'; }).join('') : '<li>Halal kant-en-klaar partnerships schalen</li>';
|
||||
if (longEl) setTimeout(function () {
|
||||
typewriterListItems(longEl, parsed.longTerm.length ? parsed.longTerm : ['Halal kant-en-klaar partnerships schalen'], { charMs: 8 });
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function renderLive(root, stats, vizMode, kpiSelection) {
|
||||
function escHtml(s) {
|
||||
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function digestSignature(digest) {
|
||||
if (!digest) return '';
|
||||
return JSON.stringify({
|
||||
sections: digest.sections || [],
|
||||
actions: digest.actions || [],
|
||||
long_term: digest.long_term || [],
|
||||
});
|
||||
}
|
||||
|
||||
function fillDigestInstant(container, digest) {
|
||||
var sections = (digest && digest.sections) || [];
|
||||
if (!sections.length) {
|
||||
container.innerHTML = '<p class="muted">Live samenvatting laden…</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = sections.map(function (sec) {
|
||||
var lines = (sec.lines || []).filter(Boolean);
|
||||
var lis = lines.length
|
||||
? lines.map(function (l) { return '<li>' + escHtml(l) + '</li>'; }).join('')
|
||||
: '<li class="muted">Geen data</li>';
|
||||
return '<div class="live-digest-section brand-' + escHtml(sec.brand || 'shared') + '">' +
|
||||
'<h5>' + escHtml((sec.icon || '') + ' ' + (sec.title || '')) + '</h5>' +
|
||||
'<ul class="live-digest-list">' + lis + '</ul></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function typewriterLiveDigest(container, digest, opts) {
|
||||
if (!container) return;
|
||||
opts = opts || {};
|
||||
var charMs = opts.charMs || 14;
|
||||
var linePause = opts.linePause || 120;
|
||||
var sectionPause = opts.sectionPause || 200;
|
||||
|
||||
if (typewriterAbort) { typewriterAbort(); typewriterAbort = null; }
|
||||
if (typewriterTimer) { clearInterval(typewriterTimer); typewriterTimer = null; }
|
||||
|
||||
var sections = (digest && digest.sections) || [];
|
||||
if (!sections.length) {
|
||||
var summary = (digest && digest.summary) || '';
|
||||
if (summary) {
|
||||
typewriter(container, summary, charMs);
|
||||
} else {
|
||||
container.innerHTML = '<p class="muted">Live samenvatting laden…</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sections.map(function (sec) {
|
||||
var lines = (sec.lines || []).filter(Boolean);
|
||||
var lis = lines.length
|
||||
? lines.map(function () { return '<li class="tw-line"></li>'; }).join('')
|
||||
: '<li class="muted">Geen data</li>';
|
||||
return '<div class="live-digest-section brand-' + escHtml(sec.brand || 'shared') + '">' +
|
||||
'<h5 class="tw-heading">' + escHtml((sec.icon || '') + ' ' + (sec.title || '')) + '</h5>' +
|
||||
'<ul class="live-digest-list">' + lis + '</ul></div>';
|
||||
}).join('');
|
||||
|
||||
var queue = [];
|
||||
container.querySelectorAll('.live-digest-section').forEach(function (secEl, si) {
|
||||
var lines = (sections[si] && sections[si].lines) || [];
|
||||
var lis = secEl.querySelectorAll('li.tw-line');
|
||||
for (var i = 0; i < lis.length; i++) {
|
||||
queue.push({ el: lis[i], text: lines[i] || '', section: si });
|
||||
}
|
||||
});
|
||||
|
||||
var cancelled = false;
|
||||
typewriterAbort = function () { cancelled = true; };
|
||||
container.classList.add('is-typing');
|
||||
|
||||
function tick(qi, ci, prevSection) {
|
||||
if (cancelled) return;
|
||||
if (qi >= queue.length) {
|
||||
container.classList.remove('is-typing');
|
||||
container.querySelectorAll('.is-typing').forEach(function (n) { n.classList.remove('is-typing'); });
|
||||
return;
|
||||
}
|
||||
var item = queue[qi];
|
||||
var el = item.el;
|
||||
var text = item.text;
|
||||
if (prevSection !== item.section && ci === 0) {
|
||||
setTimeout(function () { if (!cancelled) tick(qi, ci, item.section); }, sectionPause);
|
||||
return;
|
||||
}
|
||||
el.classList.add('is-typing');
|
||||
container.querySelectorAll('li.is-typing').forEach(function (n) { if (n !== el) n.classList.remove('is-typing'); });
|
||||
if (ci < text.length) {
|
||||
el.textContent = text.substring(0, ci + 1);
|
||||
setTimeout(function () { tick(qi, ci + 1, item.section); }, charMs);
|
||||
} else {
|
||||
el.classList.remove('is-typing');
|
||||
setTimeout(function () { tick(qi + 1, 0, item.section); }, linePause);
|
||||
}
|
||||
}
|
||||
tick(0, 0, -1);
|
||||
}
|
||||
|
||||
function typewriterListItems(listEl, items, opts) {
|
||||
if (!listEl || !items || !items.length) return;
|
||||
opts = opts || {};
|
||||
var charMs = opts.charMs || 10;
|
||||
listEl.innerHTML = items.map(function () { return '<li class="tw-line"></li>'; }).join('');
|
||||
var lis = listEl.querySelectorAll('li.tw-line');
|
||||
var qi = 0, ci = 0, cancelled = false;
|
||||
function next() {
|
||||
if (cancelled || qi >= items.length) return;
|
||||
var el = lis[qi];
|
||||
var text = items[qi];
|
||||
el.classList.add('is-typing');
|
||||
if (ci < text.length) {
|
||||
el.textContent = text.substring(0, ci + 1);
|
||||
ci++;
|
||||
setTimeout(next, charMs);
|
||||
} else {
|
||||
el.classList.remove('is-typing');
|
||||
qi++; ci = 0;
|
||||
setTimeout(next, 90);
|
||||
}
|
||||
}
|
||||
next();
|
||||
return function () { cancelled = true; };
|
||||
}
|
||||
|
||||
function fillListInstant(listEl, items) {
|
||||
if (!listEl) return;
|
||||
var rows = (items && items.length) ? items : ['Geen urgente acties — check agents & retail'];
|
||||
listEl.innerHTML = rows.map(function (t) { return '<li>' + escHtml(t) + '</li>'; }).join('');
|
||||
}
|
||||
|
||||
function renderLiveSummary(digest, pollRefresh) {
|
||||
if (!digest) return;
|
||||
var sumEl = document.getElementById('briefing-summary-text');
|
||||
var actEl = document.getElementById('briefing-actions-list');
|
||||
var longEl = document.getElementById('briefing-longterm-list');
|
||||
var meta = document.getElementById('briefing-meta');
|
||||
|
||||
var sig = digestSignature(digest);
|
||||
var unchanged = pollRefresh && sig === lastDigestSig;
|
||||
if (!unchanged) lastDigestSig = sig;
|
||||
|
||||
if (meta) {
|
||||
var ts = String(digest.updated_at || '').substring(0, 19).replace('T', ' ');
|
||||
var n = digest.activity_count != null ? ' · ' + digest.activity_count + ' activiteiten' : '';
|
||||
meta.textContent = 'Live · ververst ' + ts + ' UTC' + n;
|
||||
}
|
||||
|
||||
if (unchanged) {
|
||||
if (actEl) fillListInstant(actEl, digest.actions || []);
|
||||
if (longEl) fillListInstant(longEl, digest.long_term || []);
|
||||
return;
|
||||
}
|
||||
|
||||
var isUpdate = !!lastDigestSig && !pollRefresh;
|
||||
var twOpts = { charMs: isUpdate ? 8 : 12, linePause: isUpdate ? 70 : 100, sectionPause: isUpdate ? 120 : 180 };
|
||||
|
||||
if (sumEl) {
|
||||
if (pollRefresh) fillDigestInstant(sumEl, digest);
|
||||
else typewriterLiveDigest(sumEl, digest, twOpts);
|
||||
}
|
||||
|
||||
var actions = digest.actions || [];
|
||||
if (actEl) {
|
||||
if (pollRefresh) fillListInstant(actEl, actions.length ? actions : ['Geen urgente acties — check agents & retail']);
|
||||
else typewriterListItems(actEl, actions.length ? actions : ['Geen urgente acties — check agents & retail'], { charMs: isUpdate ? 6 : 8 });
|
||||
}
|
||||
|
||||
var lt = digest.long_term || [];
|
||||
if (longEl) {
|
||||
if (pollRefresh) fillListInstant(longEl, lt.length ? lt : ['Halal kant-en-klaar partnerships schalen']);
|
||||
else setTimeout(function () {
|
||||
typewriterListItems(longEl, lt.length ? lt : ['Halal kant-en-klaar partnerships schalen'], { charMs: isUpdate ? 6 : 8 });
|
||||
}, isUpdate ? 200 : 400);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLive(root, stats, vizMode, kpiSelection, liveDigest, opts) {
|
||||
if (!root || !stats) return;
|
||||
opts = opts || {};
|
||||
var mode = vizMode || window._dashboardVizMode || 'neo-bars';
|
||||
var kpis = kpiSelection || window._dashboardKpis;
|
||||
if (liveDigest) renderLiveSummary(liveDigest, !!opts.pollRefresh);
|
||||
else if (stats.live_digest) renderLiveSummary(stats.live_digest, !!opts.pollRefresh);
|
||||
renderKpis(document.getElementById('briefing-kpis'), stats, kpis);
|
||||
renderFoodHighlights(document.getElementById('briefing-food-highlights'), stats);
|
||||
renderExecutiveSummary(document.getElementById('briefing-executive-summary'), stats);
|
||||
@@ -432,7 +624,7 @@ window.BriefingCharts = (function () {
|
||||
}
|
||||
|
||||
return {
|
||||
render: render, renderLive: renderLive, renderText: renderText,
|
||||
render: render, renderLive: renderLive, renderText: renderText, renderLiveSummary: renderLiveSummary,
|
||||
renderExecutiveSummary: renderExecutiveSummary, renderRssFeed: renderRssFeed,
|
||||
renderKpis: renderKpis, destroyAll: destroyAll,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ function exportIntelApp() {
|
||||
region: '',
|
||||
entities: [],
|
||||
contacts: [],
|
||||
contactsHasMore: false,
|
||||
contactsLoadingMore: false,
|
||||
tenders: [],
|
||||
catererPresence: [],
|
||||
catererBrands: [],
|
||||
@@ -139,7 +141,7 @@ function exportIntelApp() {
|
||||
},
|
||||
|
||||
showEntityTypeFilter() {
|
||||
return ['distributors', 'customers', 'caterers'].indexOf(this.tab) >= 0;
|
||||
return ['distributors', 'customers', 'caterers', 'contacts'].indexOf(this.tab) >= 0;
|
||||
},
|
||||
|
||||
showMapTypeFilter() {
|
||||
@@ -183,6 +185,14 @@ function exportIntelApp() {
|
||||
return this.tab === 'contacts';
|
||||
},
|
||||
|
||||
showCrmFilter() {
|
||||
return this.tab === 'map' || this.tab === 'contacts';
|
||||
},
|
||||
|
||||
showSelectionBar() {
|
||||
return this.tab === 'map' || this.tab === 'contacts';
|
||||
},
|
||||
|
||||
entityTypeOptions() {
|
||||
if (this.tab === 'distributors') {
|
||||
return [
|
||||
@@ -205,6 +215,18 @@ function exportIntelApp() {
|
||||
if (this.tab === 'caterers') {
|
||||
return [{ v: '', l: 'Alle types' }, { v: 'contract_caterer', l: 'Contract cateraar' }];
|
||||
}
|
||||
if (this.tab === 'contacts') {
|
||||
return [
|
||||
{ v: '', l: 'Alle types' },
|
||||
{ v: 'distributor', l: 'Distributeur' },
|
||||
{ v: 'wholesaler', l: 'Groothandel' },
|
||||
{ v: 'importer', l: 'Importeur' },
|
||||
{ v: 'logistics', l: 'Logistiek' },
|
||||
{ v: 'contract_caterer', l: 'Cateraar' },
|
||||
{ v: 'restaurant', l: 'Restaurant' },
|
||||
{ v: 'foodservice', l: 'Foodservice' },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ v: '', l: 'Alle types' },
|
||||
{ v: 'distributor', l: 'Distributeur' },
|
||||
@@ -214,7 +236,8 @@ function exportIntelApp() {
|
||||
];
|
||||
},
|
||||
|
||||
async loadTabData() {
|
||||
async loadTabData(opts) {
|
||||
opts = opts || {};
|
||||
var base = '/api/export-intel';
|
||||
var fq = this.filterQs();
|
||||
if (this.tab === 'map') {
|
||||
@@ -245,9 +268,17 @@ function exportIntelApp() {
|
||||
this.catererPresence = await fetch(base + '/caterers/presence' + (this.country ? '?country=' + this.country : '')).then((x) => x.json());
|
||||
this.catererBrands = await fetch(base + '/caterers/brands').then((x) => x.json());
|
||||
} else if (this.tab === 'contacts') {
|
||||
var r4 = await fetch(base + '/contacts?limit=500' + fq).then((x) => x.json());
|
||||
this.contacts = r4.items || [];
|
||||
if (!opts.append) {
|
||||
this.contacts = [];
|
||||
this.contactsHasMore = false;
|
||||
}
|
||||
var offset = opts.append ? (this.contacts || []).length : 0;
|
||||
var r4 = await fetch(base + '/contacts?limit=200&offset=' + offset + fq).then((x) => x.json());
|
||||
var items = r4.items || [];
|
||||
if (opts.append) this.contacts = (this.contacts || []).concat(items);
|
||||
else this.contacts = items;
|
||||
this.resultCount = r4.total != null ? r4.total : this.contacts.length;
|
||||
this.contactsHasMore = this.contacts.length < this.resultCount;
|
||||
} else if (this.tab === 'tenders') {
|
||||
var tenders = await fetch(base + '/tenders' + (this.country ? '?country=' + this.country : '')).then((x) => x.json());
|
||||
var items = tenders.items || [];
|
||||
@@ -468,10 +499,26 @@ function exportIntelApp() {
|
||||
var u = '/api/export-intel/contacts/export.csv?';
|
||||
var parts = [];
|
||||
if (this.country) parts.push('country=' + encodeURIComponent(this.country));
|
||||
if (this.region) parts.push('region=' + encodeURIComponent(this.region));
|
||||
if (this.entityTypeFilter) parts.push('entity_type=' + encodeURIComponent(this.entityTypeFilter));
|
||||
if (this.filterHasEmail === 'yes') parts.push('has_email=true');
|
||||
if (this.filterHasEmail === 'no') parts.push('has_email=false');
|
||||
if (this.filterCrmStatus === 'linked') parts.push('crm_linked=true');
|
||||
if (this.filterCrmStatus === 'not_linked') parts.push('crm_linked=false');
|
||||
if (this.q && this.q.trim()) parts.push('q=' + encodeURIComponent(this.q.trim()));
|
||||
return u + parts.join('&');
|
||||
},
|
||||
|
||||
async loadMoreContacts() {
|
||||
if (!this.contactsHasMore || this.contactsLoadingMore) return;
|
||||
this.contactsLoadingMore = true;
|
||||
try {
|
||||
await this.loadTabData({ append: true });
|
||||
} finally {
|
||||
this.contactsLoadingMore = false;
|
||||
}
|
||||
},
|
||||
|
||||
typeLabel(t) {
|
||||
return (t || '').replace(/_/g, ' ');
|
||||
},
|
||||
@@ -494,6 +541,25 @@ function exportIntelApp() {
|
||||
},
|
||||
|
||||
toggleSelectAllVisible() {
|
||||
if (this.tab === 'contacts') {
|
||||
var entityIds = [];
|
||||
(this.contacts || []).forEach(function (c) {
|
||||
if (c.entity_id && entityIds.indexOf(c.entity_id) < 0) entityIds.push(c.entity_id);
|
||||
});
|
||||
if (!entityIds.length) return;
|
||||
var allSelected = entityIds.every((id) => this.isSelected(id));
|
||||
if (allSelected) {
|
||||
entityIds.forEach((id) => {
|
||||
var i = this.selectedIds.indexOf(id);
|
||||
if (i >= 0) this.selectedIds.splice(i, 1);
|
||||
});
|
||||
} else {
|
||||
entityIds.forEach((id) => {
|
||||
if (!this.isSelected(id)) this.selectedIds.push(id);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
var list = this.mapHalalMode && this.halalTopEntities.length ? this.halalTopEntities : this.entities;
|
||||
if (!list.length) return;
|
||||
var allSelected = list.every((e) => this.isSelected(e.id));
|
||||
@@ -590,6 +656,10 @@ function exportIntelApp() {
|
||||
},
|
||||
|
||||
onFilterCollectionChange() {
|
||||
if (this.tab === 'contacts') {
|
||||
this.loadTabData();
|
||||
return;
|
||||
}
|
||||
this.applyMapFilters();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,19 +9,41 @@ window.CockpitLive = (function () {
|
||||
return function () { clearInterval(id); };
|
||||
}
|
||||
|
||||
function eventFingerprint(data) {
|
||||
var ev = (data && data.events) || [];
|
||||
if (!ev.length) return '';
|
||||
return ev.slice(0, 5).map(function (e) {
|
||||
return String(e.created_at || '') + '#' + String(e.agent_name || '') + '#' + String(e.title || e.event_type || '');
|
||||
}).join('|');
|
||||
}
|
||||
|
||||
function connectFeed(onMessage) {
|
||||
if (typeof WebSocket === 'undefined') return function () {};
|
||||
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var lastFp = '';
|
||||
var booted = false;
|
||||
function connect() {
|
||||
try {
|
||||
ws = new WebSocket(proto + '//' + location.host + '/ws/feed');
|
||||
ws.onmessage = function (ev) {
|
||||
try {
|
||||
var data = JSON.parse(ev.data);
|
||||
if (onMessage) onMessage(data);
|
||||
var fp = eventFingerprint(data);
|
||||
if (!booted) {
|
||||
booted = true;
|
||||
lastFp = fp;
|
||||
if (onMessage) onMessage(data, { reason: 'connect' });
|
||||
return;
|
||||
}
|
||||
if (fp && fp !== lastFp) {
|
||||
lastFp = fp;
|
||||
if (onMessage) onMessage(data, { reason: 'events' });
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
ws.onclose = function () {
|
||||
booted = false;
|
||||
lastFp = '';
|
||||
reconnectTimer = setTimeout(connect, 5000);
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -3,6 +3,7 @@ function revenueCockpit() {
|
||||
loading: false,
|
||||
saving: false,
|
||||
savedAt: '',
|
||||
_pollStop: null,
|
||||
viewMode: 'cockpit',
|
||||
goals: { vision_text: '', horizon_text: '', mid_text: '', tagline: '' },
|
||||
projects: [],
|
||||
@@ -46,6 +47,12 @@ function revenueCockpit() {
|
||||
localStorage.setItem('foodlinkk-persona', 'ceo');
|
||||
await this.loadLive();
|
||||
await this.loadDbQuiet();
|
||||
if (window.CockpitLive) {
|
||||
this._pollStop = CockpitLive.startPolling(async () => {
|
||||
await this.loadLive();
|
||||
await this.loadDbQuiet();
|
||||
}, 45000);
|
||||
}
|
||||
},
|
||||
|
||||
projectKey(p) {
|
||||
|
||||
Reference in New Issue
Block a user