Polish CEO dashboard UI with fixed live dock and cleaner layout.

Remove page scroll hijacking during briefing generation, replace nested scroll panels with timeline and grid layouts, and refine the command bar and hero for a more professional feel.
This commit is contained in:
Aissa
2026-07-19 18:26:17 +00:00
parent 4dd1b8265f
commit 171878ee1d
2 changed files with 226 additions and 47 deletions
+188 -20
View File
@@ -1,6 +1,174 @@
/* Herman dashboard — Hermes neo style (KPI cards animate, grote panelen vast) */ /* Herman dashboard — Hermes neo style (KPI cards animate, grote panelen vast) */
.hm-neo { --hm-cyan: #00e5ff; --hm-lime: #b8ff3c; --hm-orange: #ff9f43; --hm-purple: #a855f7; --hm-gold: #ffd700; --hm-panel: #0d1219; --hm-border: rgba(0, 229, 255, 0.12); } .hm-neo { --hm-cyan: #00e5ff; --hm-lime: #b8ff3c; --hm-orange: #ff9f43; --hm-purple: #a855f7; --hm-gold: #ffd700; --hm-panel: #0d1219; --hm-border: rgba(0, 229, 255, 0.12); --hm-radius: 16px; --hm-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); }
.herman-shell.hm-neo { scroll-behavior: auto; }
.hm-hero-pro {
padding: 1.35rem 1.5rem 1.1rem;
box-shadow: var(--hm-shadow);
}
.hm-hero-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 0.85rem;
}
.hm-hero-eyebrow {
margin: 0 0 0.35rem;
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #64748b;
font-weight: 600;
}
.hm-hero-status {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.85rem;
border-radius: 999px;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(0, 229, 255, 0.18);
font-size: 0.72rem;
color: #cbd5e1;
white-space: nowrap;
}
.hm-live-badge {
font-size: 0.62rem;
vertical-align: middle;
margin-left: 0.35rem;
padding: 0.18rem 0.55rem;
border-radius: 999px;
background: rgba(34, 197, 94, 0.12);
border: 1px solid rgba(34, 197, 94, 0.35);
color: #86efac;
letter-spacing: 0.04em;
}
.hm-command-bar {
position: sticky;
top: 0.5rem;
z-index: 30;
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: var(--hm-radius);
background: rgba(8, 12, 18, 0.92);
border: 1px solid var(--hm-border);
backdrop-filter: blur(12px);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
}
.briefing-meta {
margin-top: 0.25rem;
font-size: 0.72rem;
color: #94a3b8;
letter-spacing: 0.02em;
}
.hm-card-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.65rem;
}
.hm-card-head h4 { margin: 0; }
.hm-card-link {
font-size: 0.68rem;
color: #38bdf8;
text-decoration: none;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.hm-card-link:hover { color: #7dd3fc; }
.hm-activity-card { border: 1px solid rgba(148, 163, 184, 0.12); }
.hm-activity-timeline {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.hm-activity-item {
display: grid;
grid-template-columns: 10px 1fr;
gap: 0.65rem;
align-items: start;
padding: 0.45rem 0.55rem;
border-radius: 10px;
background: rgba(0, 0, 0, 0.18);
border: 1px solid rgba(255, 255, 255, 0.04);
}
.hm-activity-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-top: 0.35rem;
background: var(--hm-cyan);
box-shadow: 0 0 8px rgba(0, 229, 255, 0.45);
}
.hm-activity-text {
font-size: 0.78rem;
line-height: 1.45;
color: #dbeafe;
}
.hm-activity-empty {
font-size: 0.78rem;
color: #64748b;
padding: 0.35rem 0;
}
.hm-live-dock {
position: fixed;
right: 1.25rem;
bottom: 1.25rem;
width: min(420px, calc(100vw - 2rem));
z-index: 120;
padding: 0.85rem 1rem;
background: rgba(8, 12, 18, 0.97);
border: 1px solid rgba(0, 229, 255, 0.35);
border-radius: 14px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55), 0 0 24px rgba(0, 229, 255, 0.1);
backdrop-filter: blur(14px);
animation: hmDockIn 0.28s ease-out;
}
@keyframes hmDockIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.hm-live-dock-active { border-color: var(--hm-cyan); }
.hm-live-dock-done { border-color: rgba(34, 197, 94, 0.45); }
.hm-live-dock-head {
display: flex;
align-items: center;
gap: 0.65rem;
flex-wrap: wrap;
margin-bottom: 0.45rem;
}
.hm-live-log-compact {
max-height: min(36vh, 280px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(0, 229, 255, 0.35) transparent;
}
.hm-live-log-compact::-webkit-scrollbar { width: 6px; }
.hm-live-log-compact::-webkit-scrollbar-thumb {
background: rgba(0, 229, 255, 0.28);
border-radius: 999px;
}
@media (max-width: 720px) {
.hm-live-dock {
right: 0.75rem;
left: 0.75rem;
width: auto;
}
.hm-command-bar { position: relative; top: auto; }
}
.herman-shell.hm-neo .herman-hero { .herman-shell.hm-neo .herman-hero {
background: linear-gradient(160deg, rgba(13, 18, 25, 0.95), rgba(8, 12, 18, 0.98)); background: linear-gradient(160deg, rgba(13, 18, 25, 0.95), rgba(8, 12, 18, 0.98));
@@ -114,8 +282,11 @@
} }
.briefing-card.summary, .briefing-card.summary,
.horizon-card { .horizon-card,
background: var(--hm-panel); border-radius: 14px; padding: 1rem; .briefing-card.hm-activity-card {
background: var(--hm-panel); border-radius: var(--hm-radius); padding: 1rem 1.05rem;
border: 1px solid rgba(148, 163, 184, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
} }
.briefing-card.summary { border: 1px solid var(--hm-border); } .briefing-card.summary { border: 1px solid var(--hm-border); }
.briefing-card.summary h4, .briefing-card.summary h4,
@@ -237,7 +408,8 @@
.hm-chart-panel { .hm-chart-panel {
background: var(--hm-panel); border: 1px solid var(--hm-border); background: var(--hm-panel); border: 1px solid var(--hm-border);
border-radius: 14px; padding: 1rem 1.1rem; min-height: 260px; border-radius: var(--hm-radius); padding: 1rem 1.1rem; min-height: 260px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
} }
.hm-chart-head { .hm-chart-head {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem;
@@ -276,16 +448,7 @@
.page-viz-panel.viz-mode-alt .viz-eq-row { font-size: 0.82rem; padding: 0.15rem 0; } .page-viz-panel.viz-mode-alt .viz-eq-row { font-size: 0.82rem; padding: 0.15rem 0; }
.hm-live-bar { .hm-live-bar {
position: sticky; display: none;
top: 0;
z-index: 40;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: rgba(8, 12, 18, 0.97);
border: 1px solid rgba(0, 229, 255, 0.35);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), 0 0 20px rgba(0, 229, 255, 0.08);
backdrop-filter: blur(8px);
} }
.hm-live-bar-active { border-color: var(--hm-cyan); animation: hmLivePulse 2s ease-in-out infinite; } .hm-live-bar-active { border-color: var(--hm-cyan); animation: hmLivePulse 2s ease-in-out infinite; }
.hm-live-bar-done { border-color: rgba(34, 197, 94, 0.45); } .hm-live-bar-done { border-color: rgba(34, 197, 94, 0.45); }
@@ -509,9 +672,17 @@
.app-topbar .page-tip { display: none; } .app-topbar .page-tip { display: none; }
} }
.hm-highlight-scroll { max-height: 220px; overflow-y: auto; } .hm-highlight-scroll,
.hm-highlight-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 0.65rem;
}
.hm-highlight-item { .hm-highlight-item {
padding: 0.55rem 0; border-bottom: 1px solid rgba(255,255,255,0.05); padding: 0.75rem 0.85rem;
border-radius: 12px;
background: rgba(0, 0, 0, 0.22);
border: 1px solid rgba(255, 255, 255, 0.06);
} }
.hm-highlight-item strong { color: #e2e8f0; font-size: 0.85rem; text-decoration: none; } .hm-highlight-item strong { color: #e2e8f0; font-size: 0.85rem; text-decoration: none; }
.hm-highlight-item a { color: inherit; text-decoration: none; } .hm-highlight-item a { color: inherit; text-decoration: none; }
@@ -707,13 +878,10 @@
align-items: start; align-items: start;
} }
@media (max-width: 1000px) { .herman-rss-grid { grid-template-columns: 1fr; } } @media (max-width: 1000px) { .herman-rss-grid { grid-template-columns: 1fr; } }
.herman-rss-list { .hm-rss-list {
max-height: 68vh;
overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.55rem; gap: 0.55rem;
padding-right: 0.25rem;
} }
.herman-rss-card { margin: 0; } .herman-rss-card { margin: 0; }
.herman-rss-side-item { .herman-rss-side-item {
+38 -27
View File
@@ -2,15 +2,24 @@
{% block extra_head %} {% block extra_head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script> <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/hermes.css" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=21" /> <link rel="stylesheet" href="/static/css/herman-dashboard.css?v=22" />
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="herman-shell hm-neo"> <div class="herman-shell hm-neo">
<div class="herman-hero"> <div class="herman-hero hm-hero-pro">
<h1>CEO Dashboard · Cucina <span class="live-badge">Live</span></h1> <div class="hm-hero-top">
<p class="hero-sub">Business overzicht · retail · deals · briefing · marketing</p> <div>
<div class="hm-hero-links beurs-mini-row" style="margin-top:0.85rem"> <p class="hm-hero-eyebrow">Foodlinkk Command Center</p>
<h1>CEO Dashboard <span class="live-badge hm-live-badge">Live</span></h1>
<p class="hero-sub">Cucina B2C · retail · pipeline · Herman briefing</p>
</div>
<div class="hm-hero-status" id="hero-live-status">
<span class="hm-live-dot"></span>
<span id="briefing-meta-hero">Live data laden…</span>
</div>
</div>
<div class="hm-hero-links beurs-mini-row">
<a href="/clients" class="beurs-mini-chip">👥 Klanten</a> <a href="/clients" class="beurs-mini-chip">👥 Klanten</a>
<a href="/deals" class="beurs-mini-chip">💼 Deals</a> <a href="/deals" class="beurs-mini-chip">💼 Deals</a>
<a href="/packaging" class="beurs-mini-chip">📐 Packaging</a> <a href="/packaging" class="beurs-mini-chip">📐 Packaging</a>
@@ -34,7 +43,7 @@
</button> </button>
</nav> </nav>
<header class="herman-briefing-header"> <header class="herman-briefing-header hm-command-bar">
<div> <div>
<h2>Dagelijkse briefing</h2> <h2>Dagelijkse briefing</h2>
<div class="briefing-meta" id="briefing-meta"></div> <div class="briefing-meta" id="briefing-meta"></div>
@@ -49,14 +58,14 @@
</div> </div>
</header> </header>
<div id="hm-live-bar" class="hm-live-bar" x-show="loading || liveLog.length" x-cloak :class="loading ? 'hm-live-bar-active' : 'hm-live-bar-done'"> <div id="hm-live-bar" class="hm-live-dock" x-show="loading || liveLog.length" x-cloak :class="loading ? 'hm-live-dock-active' : 'hm-live-dock-done'">
<div class="hm-live-bar-head"> <div class="hm-live-dock-head">
<span class="hm-live-dot"></span> <span class="hm-live-dot"></span>
<strong class="hm-live-title" x-text="loading ? loadingMsg : 'Dagrapport voltooid'"></strong> <strong class="hm-live-title" x-text="loading ? loadingMsg : 'Dagrapport voltooid'"></strong>
<span class="hm-live-progress" x-show="loading" x-text="liveProgress"></span> <span class="hm-live-progress" x-show="loading" x-text="liveProgress"></span>
<button type="button" class="btn btn-sm hm-live-close" x-show="!loading && liveLog.length" @click="liveLog = []"></button> <button type="button" class="btn btn-sm hm-live-close" x-show="!loading && liveLog.length" @click="liveLog = []" title="Sluiten"></button>
</div> </div>
<ol id="hm-live-log" class="hm-live-log"> <ol id="hm-live-log" class="hm-live-log hm-live-log-compact">
<template x-for="(entry, idx) in liveLog" :key="idx"> <template x-for="(entry, idx) in liveLog" :key="idx">
<li :class="'hm-live-entry hm-live-' + (entry.status || 'ok') + (entry.type === 'agent_msg' ? ' hm-live-agent' : '')"> <li :class="'hm-live-entry hm-live-' + (entry.status || 'ok') + (entry.type === 'agent_msg' ? ' hm-live-agent' : '')">
<span class="hm-live-src" x-text="entry.source || entry.agent"></span> <span class="hm-live-src" x-text="entry.source || entry.agent"></span>
@@ -96,9 +105,12 @@
<h4>Samenvatting · live</h4> <h4>Samenvatting · live</h4>
<div id="briefing-summary-text" class="live-digest-body"></div> <div id="briefing-summary-text" class="live-digest-body"></div>
</div> </div>
<div class="briefing-card" style="max-height:220px;overflow-y:auto"> <div class="briefing-card hm-activity-card">
<h4>📋 Herman activiteitenlog</h4> <div class="hm-card-head">
<ul id="briefing-activity-log" class="hm-exec-list" style="margin:0;padding-left:1.1rem;font-size:0.82rem"></ul> <h4>Activiteitenlog</h4>
<a href="/agents" class="hm-card-link">Agents →</a>
</div>
<ul id="briefing-activity-log" class="hm-activity-timeline"></ul>
</div> </div>
<div class="briefing-horizon-col"> <div class="briefing-horizon-col">
<div class="horizon-card short"> <div class="horizon-card short">
@@ -129,7 +141,7 @@
</div> </div>
<div class="hm-chart-panel hm-dash-trends-only" style="margin-top:1rem"> <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 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 id="briefing-food-highlights" class="hm-highlight-grid"></div>
</div> </div>
</section> </section>
@@ -442,9 +454,12 @@ function dashboardBriefing() {
renderActivityLog(entries) { renderActivityLog(entries) {
const el = document.getElementById('briefing-activity-log'); const el = document.getElementById('briefing-activity-log');
if (!el) return; if (!el) return;
el.innerHTML = (entries || []).length const rows = (entries || []).slice(0, 10);
? entries.slice(0, 12).map(e => '<li>' + e + '</li>').join('') el.innerHTML = rows.length
: '<li class="muted">Nog geen agent-acties vandaag — Herman documenteert automatisch.</li>'; ? rows.map(function (e, i) {
return '<li class="hm-activity-item"><span class="hm-activity-dot"></span><span class="hm-activity-text">' + e + '</span></li>';
}).join('')
: '<li class="hm-activity-empty">Nog geen agent-acties vandaag — Herman documenteert automatisch.</li>';
}, },
renderClientsMini(stats) { renderClientsMini(stats) {
const el = document.getElementById('briefing-clients-mini'); const el = document.getElementById('briefing-clients-mini');
@@ -458,12 +473,13 @@ function dashboardBriefing() {
updateLiveMeta(at, digest) { updateLiveMeta(at, digest) {
const tsRaw = at || (digest && digest.updated_at) || ''; const tsRaw = at || (digest && digest.updated_at) || '';
const ts = String(tsRaw).substring(0, 19).replace('T', ' '); const ts = String(tsRaw).substring(0, 19).replace('T', ' ');
const n = digest && digest.activity_count != null ? digest.activity_count + ' activiteiten' : '';
const line = ts ? ('Ververst ' + ts + ' UTC' + (n ? ' · ' + n : '')) : 'Live data laden…';
const meta = document.getElementById('briefing-meta'); const meta = document.getElementById('briefing-meta');
if (meta && ts) { if (meta) meta.textContent = line;
const n = digest && digest.activity_count != null ? ' · ' + digest.activity_count + ' activiteiten' : ''; const heroMeta = document.getElementById('briefing-meta-hero');
meta.textContent = 'Live · ververst ' + ts + ' UTC' + n; if (heroMeta) heroMeta.textContent = line;
} const badge = document.querySelector('.hm-live-badge');
const badge = document.querySelector('.herman-hero .live-badge');
if (badge && tsRaw) { if (badge && tsRaw) {
try { try {
badge.textContent = 'Live · ' + new Date(tsRaw).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' }); badge.textContent = 'Live · ' + new Date(tsRaw).toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' });
@@ -510,8 +526,6 @@ function dashboardBriefing() {
this.$nextTick(() => { this.$nextTick(() => {
const log = document.getElementById('hm-live-log'); const log = document.getElementById('hm-live-log');
if (log) log.scrollTop = log.scrollHeight; if (log) log.scrollTop = log.scrollHeight;
const bar = document.getElementById('hm-live-bar');
if (bar && this.loading) bar.scrollIntoView({ behavior: 'smooth', block: 'start' });
}); });
}, },
async generate() { async generate() {
@@ -519,9 +533,6 @@ function dashboardBriefing() {
this.liveLog = []; this.liveLog = [];
this.liveProgress = ''; this.liveProgress = '';
this.loadingMsg = 'Herman start dagrapport…'; this.loadingMsg = 'Herman start dagrapport…';
window.scrollTo({ top: 0, behavior: 'smooth' });
const bar = document.getElementById('hm-live-bar');
if (bar) bar.scrollIntoView({ behavior: 'smooth', block: 'start' });
try { try {
const resp = await fetch('/api/herman/briefing/stream', { method: 'POST' }); const resp = await fetch('/api/herman/briefing/stream', { method: 'POST' });
if (!resp.ok) throw new Error('Stream mislukt (' + resp.status + ')'); if (!resp.ok) throw new Error('Stream mislukt (' + resp.status + ')');