SysOps: voice-agy-webbuilder-backup — 2026-06-23 10:04 UTC
This commit is contained in:
@@ -2,28 +2,38 @@
|
||||
{% 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=15" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=17" />
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="herman-shell hm-neo">
|
||||
|
||||
<div class="herman-hero">
|
||||
<h1>Herman · Command Center <span class="live-badge">Live</span></h1>
|
||||
<p class="hero-sub">CEO overzicht · trending retail · briefing · platform links</p>
|
||||
<h1>CEO Dashboard · Cucina <span class="live-badge">Live</span></h1>
|
||||
<p class="hero-sub">Business overzicht · retail · deals · briefing · marketing</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="/clients" class="beurs-mini-chip">👥 Klanten</a>
|
||||
<a href="/deals" class="beurs-mini-chip">💼 Deals</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>
|
||||
<a href="/retail" class="beurs-mini-chip">🛒 Retail</a>
|
||||
<a href="/analytics" class="beurs-mini-chip">📊 Analytics</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel herman-briefing" x-data="dashboardBriefing()" x-init="init()">
|
||||
|
||||
<nav class="herman-dash-tabs" aria-label="Herman weergaven">
|
||||
<button type="button" class="herman-dash-tab" :class="{ active: hermanTab === 'overview' }" @click="setHermanTab('overview')">
|
||||
<span aria-hidden="true">📊</span> Overzicht
|
||||
</button>
|
||||
<button type="button" class="herman-dash-tab" :class="{ active: hermanTab === 'rss' }" @click="setHermanTab('rss')">
|
||||
<span aria-hidden="true">📰</span> RSS Feeds
|
||||
<span class="herman-dash-tab-badge" x-show="rssTotal > 0" x-text="rssTotal"></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<header class="herman-briefing-header">
|
||||
<div>
|
||||
<h2>Dagelijkse briefing</h2>
|
||||
@@ -60,7 +70,7 @@
|
||||
<div id="viz-toolbar" class="viz-toolbar"></div>
|
||||
<div id="dashboard-customize" class="hm-customize-wrap" x-show="editLayout" x-cloak></div>
|
||||
|
||||
<div id="briefing-dashboard" class="hm-dash-layout" :class="editLayout ? 'hm-dash-editing' : ''">
|
||||
<div id="briefing-dashboard" class="hm-dash-layout" :class="editLayout ? 'hm-dash-editing' : ''" x-show="hermanTab === 'overview'">
|
||||
|
||||
<section class="hm-dash-section hm-dash-full" data-widget="kpis">
|
||||
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · CEO KPI's</div>
|
||||
@@ -103,14 +113,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hm-dash-section hm-dash-full" data-widget="rss">
|
||||
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · RSS feed</div>
|
||||
<h3 class="hm-section-title"><span class="hm-live-dot"></span> RSS live feed</h3>
|
||||
<div class="hm-chart-panel">
|
||||
<div id="briefing-rss-feed" class="hm-rss-panel"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hm-dash-section" data-widget="retail">
|
||||
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · Retail</div>
|
||||
<h3 class="hm-section-title">Retail operatie</h3>
|
||||
@@ -152,59 +154,115 @@
|
||||
<div class="hm-chart-wrap"><canvas id="chart-words"></canvas></div>
|
||||
<div class="hm-eq hm-eq-cyan"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="hub-grid hm-dash-feed" data-widget="feed">
|
||||
<div class="hm-widget-handle" x-show="editLayout">⋮⋮ Sleep · 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">
|
||||
<section class="panel hm-dash-full">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ═══ RSS FEEDS TAB ═══ -->
|
||||
<div class="herman-rss-hub" x-show="hermanTab === 'rss'" x-cloak>
|
||||
<header class="herman-rss-head">
|
||||
<div>
|
||||
<h2>RSS Feeds</h2>
|
||||
<p class="page-tip">Alle live feeds op één plek — filter op categorie of zoek op titel</p>
|
||||
</div>
|
||||
<div class="herman-rss-actions">
|
||||
<button type="button" class="btn btn-sm btn-pulse" @click="refreshRss(true)" :disabled="rssLoading">↻ RSS ophalen</button>
|
||||
<a class="btn btn-sm" href="/marketing?tab=live">Marketing Hub</a>
|
||||
<a class="btn btn-sm" href="/marketing?tab=bookmarks">★ Bookmarks</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="herman-rss-filters">
|
||||
<input type="search" class="form-input" placeholder="Zoek in titels…" x-model="rssSearch" />
|
||||
<select class="form-input" x-model="rssCategory" @change="loadRssTab()">
|
||||
<option value="">Alle categorieën</option>
|
||||
<option value="kant-en-klaar">Kant-en-klaar</option>
|
||||
<option value="supermarkt">Supermarkt</option>
|
||||
<option value="food">Food</option>
|
||||
<option value="markt">Markt & M&A</option>
|
||||
<option value="regelgeving">Regelgeving</option>
|
||||
<option value="cbs">CBS</option>
|
||||
</select>
|
||||
<span class="herman-rss-count" x-text="filteredRss().length + ' van ' + rssTotal + ' items'"></span>
|
||||
</div>
|
||||
|
||||
<div class="herman-rss-grid">
|
||||
<section class="herman-rss-col herman-rss-col-main">
|
||||
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Live feed</h3>
|
||||
<div class="herman-rss-list">
|
||||
<template x-for="r in filteredRss()" :key="r.id || (r.link + r.title)">
|
||||
<article class="feed-card herman-rss-card">
|
||||
<div class="source" x-text="(r.feed_name || 'RSS') + ' · ' + (r.category || 'feed')"></div>
|
||||
<a :href="r.link" target="_blank" rel="noopener"><strong x-text="r.title"></strong></a>
|
||||
<p class="muted" x-show="r.description" x-text="(r.description || '').slice(0, 160) + ((r.description || '').length > 160 ? '…' : '')"></p>
|
||||
<div class="feed-actions">
|
||||
<small class="muted" x-text="(r.published_at || '').substring(0, 16).replace('T', ' ')"></small>
|
||||
<a class="btn-source" :href="r.feed_url || r.link" target="_blank" rel="noopener">Bron feed →</a>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
<p class="empty-state" x-show="!rssLoading && !filteredRss().length">Geen items — klik <strong>RSS ophalen</strong> of pas filters aan.</p>
|
||||
<p class="empty-state" x-show="rssLoading">Feeds laden…</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="herman-rss-col herman-rss-col-side">
|
||||
<section class="hm-chart-panel" x-show="(rssRegulations.regelgeving || []).length">
|
||||
<h4>Regelgeving</h4>
|
||||
<template x-for="r in (rssRegulations.regelgeving || []).slice(0, 12)" :key="'reg'+r.id">
|
||||
<div class="herman-rss-side-item">
|
||||
<a :href="r.link" target="_blank" rel="noopener" x-text="r.title"></a>
|
||||
<small x-text="r.feed_name"></small>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
<section class="hm-chart-panel" x-show="(rssRegulations.cbs || []).length">
|
||||
<h4>CBS & data</h4>
|
||||
<template x-for="r in (rssRegulations.cbs || []).slice(0, 8)" :key="'cbs'+r.id">
|
||||
<div class="herman-rss-side-item">
|
||||
<a :href="r.link" target="_blank" rel="noopener" x-text="r.title"></a>
|
||||
<small x-text="r.feed_name"></small>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
<section class="hm-chart-panel" x-show="rssBookmarks.length">
|
||||
<h4>★ Opgeslagen</h4>
|
||||
<template x-for="b in rssBookmarks.slice(0, 10)" :key="'bm'+b.id">
|
||||
<div class="herman-rss-side-item">
|
||||
<a :href="b.link" target="_blank" rel="noopener" x-text="b.title"></a>
|
||||
<small x-text="b.feed_name"></small>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/viz-engine.js?v=3"></script>
|
||||
<script src="/static/js/dashboard-layout.js?v=3"></script>
|
||||
<script src="/static/js/briefing-charts.js?v=12"></script>
|
||||
<script src="/static/js/dashboard-layout.js?v=4"></script>
|
||||
<script src="/static/js/briefing-charts.js?v=13"></script>
|
||||
<script>
|
||||
localStorage.setItem('foodlinkk-persona', 'ceo');
|
||||
const INITIAL_BRIEFING = {{ briefing_payload | tojson }};
|
||||
function dashboardBriefing() {
|
||||
return {
|
||||
loading: false, loadingMsg: 'Herman werkt…', liveLog: [], liveProgress: '',
|
||||
editLayout: false,
|
||||
hermanTab: 'overview',
|
||||
rssItems: [], rssTotal: 0, rssCategory: '', rssSearch: '', rssLoading: false,
|
||||
rssRegulations: {}, rssBookmarks: [],
|
||||
content: INITIAL_BRIEFING.content || '', createdAt: INITIAL_BRIEFING.created_at || '',
|
||||
_pollStop: null, _wsStop: null, _vizMode: 'neo-bars', _stats: null,
|
||||
_prefs: null, _setDragMode: null, _saveTimer: null,
|
||||
@@ -237,7 +295,51 @@ function dashboardBriefing() {
|
||||
this.editLayout = !this.editLayout;
|
||||
if (this._setDragMode) this._setDragMode(this.editLayout);
|
||||
},
|
||||
setHermanTab(tab) {
|
||||
this.hermanTab = tab;
|
||||
try {
|
||||
const url = new URL(location.href);
|
||||
if (tab === 'rss') url.searchParams.set('tab', 'rss');
|
||||
else url.searchParams.delete('tab');
|
||||
history.replaceState({}, '', url);
|
||||
} catch (e) {}
|
||||
if (tab === 'rss') this.loadRssTab();
|
||||
},
|
||||
filteredRss() {
|
||||
const q = (this.rssSearch || '').toLowerCase().trim();
|
||||
if (!q) return this.rssItems;
|
||||
return this.rssItems.filter(function (r) {
|
||||
return [r.title, r.feed_name, r.category, r.description].some(function (v) {
|
||||
return v && String(v).toLowerCase().includes(q);
|
||||
});
|
||||
});
|
||||
},
|
||||
async loadRssTab() {
|
||||
this.rssLoading = true;
|
||||
try {
|
||||
const params = new URLSearchParams({ limit: '100' });
|
||||
if (this.rssCategory) params.set('category', this.rssCategory);
|
||||
const [live, reg, bookmarks] = await Promise.all([
|
||||
fetch('/api/retail/rss/live?' + params).then(function (x) { return x.json(); }),
|
||||
fetch('/api/retail/regulations?limit=40').then(function (x) { return x.json(); }).catch(function () { return {}; }),
|
||||
fetch('/api/retail/rss/bookmarks?limit=50').then(function (x) { return x.json(); }).catch(function () { return { items: [] }; }),
|
||||
]);
|
||||
this.rssItems = live.items || [];
|
||||
this.rssTotal = live.count || this.rssItems.length;
|
||||
this.rssRegulations = reg || {};
|
||||
this.rssBookmarks = bookmarks.items || [];
|
||||
if (this._stats) {
|
||||
this._stats.rss_live = this.rssItems;
|
||||
this._stats.rss_items = this.rssTotal;
|
||||
}
|
||||
} catch (e) {
|
||||
Cockpit.toast(e.message || 'RSS laden mislukt', 'error');
|
||||
}
|
||||
this.rssLoading = false;
|
||||
},
|
||||
async init() {
|
||||
const qsTab = new URLSearchParams(location.search).get('tab');
|
||||
if (qsTab === 'rss') this.hermanTab = 'rss';
|
||||
window._dashboardVizMode = this._vizMode;
|
||||
const prefs = await DashboardLayout.loadPrefs();
|
||||
this._prefs = prefs;
|
||||
@@ -272,29 +374,23 @@ function dashboardBriefing() {
|
||||
const cnt = (this._stats && (this._stats.rss_items || (this._stats.rss_live || []).length)) || 0;
|
||||
if (cnt < 3) { sessionStorage.setItem('rss_auto_refreshed', '1'); await this.refreshRss(false); }
|
||||
}
|
||||
await this.loadApprovals();
|
||||
await this.loadProjectsMini();
|
||||
if (this.content) BriefingCharts.renderText(dash, this.content, this.createdAt);
|
||||
window._refreshBriefingLive = (t) => this.refreshLive(t);
|
||||
this._pollStop = CockpitLive.startPolling(() => { this.refreshLive(false); this.loadApprovals(); }, 30000);
|
||||
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'}));
|
||||
});
|
||||
this._pollStop = CockpitLive.startPolling(() => { this.refreshLive(false); }, 30000);
|
||||
if (this.hermanTab === 'rss') await this.loadRssTab();
|
||||
},
|
||||
async refreshRss(toast) {
|
||||
if (toast) Cockpit.toast('RSS feeds ophalen…', 'info');
|
||||
try {
|
||||
await fetch('/api/retail/rss/refresh', { method: 'POST' });
|
||||
const live = await fetch('/api/retail/rss/live?limit=25').then(x => x.json());
|
||||
if (this._stats && live.items) {
|
||||
this._stats.rss_live = live.items;
|
||||
this._stats.rss_items = live.count || live.items.length;
|
||||
BriefingCharts.renderRssFeed(document.getElementById('briefing-rss-feed'), this._stats);
|
||||
await this.loadRssTab();
|
||||
if (this._stats) {
|
||||
BriefingCharts.renderRetail(document.getElementById('briefing-retail'), this._stats);
|
||||
BriefingCharts.renderFoodHighlights(document.getElementById('briefing-food-highlights'), this._stats);
|
||||
BriefingCharts.renderKpis(document.getElementById('briefing-kpis'), this._stats, window._dashboardKpis);
|
||||
}
|
||||
if (toast) Cockpit.toast('RSS bijgewerkt (' + (live.count || 0) + ' items)', 'success');
|
||||
if (toast) Cockpit.toast('RSS bijgewerkt (' + (this.rssTotal || 0) + ' items)', 'success');
|
||||
} catch (e) { if (toast) Cockpit.toast(e.message || 'RSS fout', 'error'); }
|
||||
},
|
||||
async loadApprovals() {
|
||||
@@ -363,6 +459,7 @@ function dashboardBriefing() {
|
||||
if (live.items && live.items.length) {
|
||||
this._stats.rss_live = live.items;
|
||||
this._stats.rss_items = live.count || live.items.length;
|
||||
this.rssTotal = this._stats.rss_items;
|
||||
}
|
||||
} catch (e) {}
|
||||
BriefingCharts.renderLive(document.getElementById('briefing-dashboard'), this._stats, this._vizMode, window._dashboardKpis);
|
||||
|
||||
Reference in New Issue
Block a user