SysOps: deploy-all — 2026-06-09 10:41 UTC
This commit is contained in:
@@ -2,14 +2,25 @@
|
||||
{% 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=8" />
|
||||
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
|
||||
{% 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 · klanten · briefing · retail · alles op een rij</p>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<section class="panel herman-briefing" x-data="dashboardBriefing()" x-init="init()">
|
||||
@@ -24,6 +35,7 @@
|
||||
<div class="briefing-meta" id="briefing-meta"></div>
|
||||
</div>
|
||||
<div class="herman-briefing-actions">
|
||||
<button type="button" class="btn btn-sm" @click="editLayout = !editLayout" x-text="editLayout ? 'Klaar ordenen' : '⋮⋮ Ordenen'"></button>
|
||||
<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>
|
||||
@@ -32,29 +44,38 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="viz-toolbar" class="viz-toolbar"></div>
|
||||
|
||||
<div id="briefing-dashboard" class="hm-dash-layout">
|
||||
|
||||
<section class="hm-dash-section hm-dash-full">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<section class="hm-dash-section hm-dash-full">
|
||||
<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>
|
||||
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Alles op een rij</h3>
|
||||
<div class="hm-chart-panel">
|
||||
<div class="hm-chart-head"><h4>Executive samenvatting</h4><small>live database · geen RSS — zie Marketing Hub</small></div>
|
||||
<div class="hm-chart-head"><h4>Executive samenvatting</h4><small>live database</small></div>
|
||||
<div id="briefing-executive-summary"></div>
|
||||
</div>
|
||||
<div id="briefing-clients-mini" class="beurs-mini-row" style="margin-top:0.75rem"></div>
|
||||
</section>
|
||||
|
||||
<section class="hm-dash-section hm-dash-full">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<div class="briefing-horizon-col">
|
||||
<div class="horizon-card short">
|
||||
<h4>⚡ Korte termijn</h4>
|
||||
@@ -68,12 +89,14 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hm-dash-section">
|
||||
<section class="hm-dash-section hm-widget-draggable" data-widget="retail">
|
||||
<div class="hm-widget-handle" x-show="editLayout">Sleep widget · Retail</div>
|
||||
<h3 class="hm-section-title">Retail operatie</h3>
|
||||
<div class="hm-dash-two-col">
|
||||
<div class="hm-chart-panel">
|
||||
<div class="hm-chart-head"><h4>Top retail kansen</h4><small><a href="/retail">Retail 360 →</a></small></div>
|
||||
<div class="hm-chart-head"><h4>Trending food retail</h4><small><a href="/marketing">Marketing Hub →</a></small></div>
|
||||
<div id="briefing-retail"></div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<div class="hm-chart-panel">
|
||||
<div class="hm-chart-head"><h4>Sales milestones</h4><small>live DB</small></div>
|
||||
@@ -86,72 +109,153 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="hm-dash-section hm-dash-full">
|
||||
<h3 class="hm-section-title">Data & analytics</h3>
|
||||
<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>
|
||||
<div class="hm-neo-charts">
|
||||
<div class="hm-chart-panel">
|
||||
<div class="hm-chart-panel hm-analytics-panel">
|
||||
<div class="hm-chart-head"><h4>Pipeline</h4><small>live deals</small></div>
|
||||
<div class="hm-chart-wrap"><canvas id="chart-pipeline"></canvas></div>
|
||||
<div class="hm-eq"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||||
</div>
|
||||
<div class="hm-chart-panel">
|
||||
<div class="hm-chart-head"><h4>NAS sentiment</h4><small>documenten</small></div>
|
||||
<div class="hm-chart-wrap"><canvas id="chart-sentiment"></canvas></div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="hm-chart-panel">
|
||||
<div class="hm-chart-panel hm-analytics-panel">
|
||||
<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>
|
||||
<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">
|
||||
<div class="hm-chart-head"><h4>Agent activiteit</h4><small>laatste events</small></div>
|
||||
<div class="hm-chart-wrap"><canvas id="chart-agents"></canvas></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 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>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="hub-grid hm-dash-feed">
|
||||
<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>Wacht op OK</h2>
|
||||
{% for item in approvals[:8] %}
|
||||
<div class="approval-item"><span class="badge">{{ item.agent_name }}</span> {{ item.title or item.event_type }}</div>
|
||||
{% else %}<p class="empty-state">Geen goedkeuringen 🎉</p>{% endfor %}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/briefing-charts.js?v=8"></script>
|
||||
<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>
|
||||
<script>
|
||||
const INITIAL_BRIEFING = {{ briefing_payload | tojson }};
|
||||
function dashboardBriefing() {
|
||||
return {
|
||||
loading: false, loadingMsg: 'Herman werkt…',
|
||||
loading: false, loadingMsg: 'Herman werkt…', editLayout: false,
|
||||
content: INITIAL_BRIEFING.content || '', createdAt: INITIAL_BRIEFING.created_at || '',
|
||||
_pollStop: null, _wsStop: null,
|
||||
_pollStop: null, _wsStop: null, _vizMode: 'neo-bars', _stats: null,
|
||||
async init() {
|
||||
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);
|
||||
});
|
||||
await this.refreshLive(false);
|
||||
await this.loadApprovals();
|
||||
await this.loadProjectsMini();
|
||||
if (this.content) BriefingCharts.renderText(document.getElementById('briefing-dashboard'), 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); 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'}));
|
||||
});
|
||||
},
|
||||
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>';
|
||||
},
|
||||
renderClientsMini(stats) {
|
||||
const el = document.getElementById('briefing-clients-mini');
|
||||
if (!el) return;
|
||||
@@ -166,8 +270,10 @@ function dashboardBriefing() {
|
||||
try {
|
||||
const r = await fetch('/api/herman/briefing/stats').then(x => x.json());
|
||||
if (r.stats) {
|
||||
BriefingCharts.renderLive(document.getElementById('briefing-dashboard'), r.stats);
|
||||
this._stats = r.stats;
|
||||
BriefingCharts.renderLive(document.getElementById('briefing-dashboard'), r.stats, this._vizMode);
|
||||
this.renderClientsMini(r.stats);
|
||||
this.renderActivityLog(r.stats.activity_log || []);
|
||||
document.getElementById('briefing-meta').textContent = 'Live sync: ' + (r.at || '').substring(0, 19).replace('T', ' ') + ' UTC';
|
||||
}
|
||||
if (toast !== false) Cockpit.toast('Live data bijgewerkt', 'success');
|
||||
@@ -182,7 +288,7 @@ function dashboardBriefing() {
|
||||
const r = await Cockpit.api('/api/herman/briefing', { method: 'POST' });
|
||||
this.content = r.content || '';
|
||||
this.createdAt = r.generated_at || new Date().toISOString();
|
||||
BriefingCharts.render(document.getElementById('briefing-dashboard'), r.stats, r.content, this.createdAt);
|
||||
BriefingCharts.render(document.getElementById('briefing-dashboard'), r.stats, r.content, this.createdAt, this._vizMode);
|
||||
this.renderClientsMini(r.stats || {});
|
||||
Cockpit.toast('Dagrapport klaar!', 'success');
|
||||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||||
|
||||
Reference in New Issue
Block a user