Ship War Room UX, CEO inbox, mobile chat, and Agents NAS workspace.

Make A2A togglable, keep reply context while reading, route agent data to //10.4.7.11/share/Agents, and polish themes plus export-intel sync cancel.
This commit is contained in:
Aissa
2026-08-16 22:40:32 +00:00
parent 910ba465d7
commit c5ea8cbb63
26 changed files with 2891 additions and 407 deletions
+140 -34
View File
@@ -4,12 +4,12 @@
<link rel="stylesheet" href="/static/css/hermes.css" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=13" />
<link rel="stylesheet" href="/static/css/agents-mesh.css?v=8" />
<link rel="stylesheet" href="/static/css/agent-characters.css?v=4" />
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=18" />
<script src="/static/js/agent-avatars.js?v=4"></script>
<link rel="stylesheet" href="/static/css/agent-characters.css?v=8" />
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=40" />
<script src="/static/js/agent-avatars.js?v=5"></script>
{% endblock %}
{% block content %}
<div class="hm-neo" x-data="agentsSoulHub()" x-init="init()">
<div class="hm-neo" :class="{ 'tab-warroom-active': tab==='warroom' }" x-data="agentsSoulHub()" x-init="init()">
<header class="hm-neo-header page-header" x-show="tab!=='warroom'" x-cloak>
<h1>Agents · Soul Registry</h1>
<p class="hm-sub">Animated team · duidelijke rollen · soul.md · live activiteit</p>
@@ -32,22 +32,22 @@
<div class="vtabs-layout" style="--vt-color:#ffd700" x-ref="vtabsWrap">
<nav class="vtabs-nav" x-ref="vtabsNav" :style="'--vt-indicator-top:'+tabIndicatorTop+'px;--vt-indicator-h:'+tabIndicatorH+'px'">
<div class="vtabs-nav-header"><strong>Agents</strong></div>
<button class="vtab-btn" :class="{active: tab==='warroom'}" @click="setTab('warroom', $el)" data-tab="warroom">
<span class="vtab-icon vtab-icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 4 6v6c0 5 3.5 8.5 8 10 4.5-1.5 8-5 8-10V6l-8-4Z"/><path d="m9 12 2 2 4-4"/></svg></span><span>War Room</span>
</button>
<button class="vtab-btn" :class="{active: tab==='souls'}" @click="setTab('souls', $el)">
<span class="vtab-icon">🧬</span><span>Souls</span>
<span class="vtab-icon vtab-icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></span><span>Souls</span>
</button>
<button class="vtab-btn" :class="{active: tab==='mesh'}" @click="setTab('mesh', $el)">
<span class="vtab-icon">🕸️</span><span>Netwerk</span>
<span class="vtab-icon vtab-icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="2"/><circle cx="5" cy="19" r="2"/><circle cx="19" cy="19" r="2"/><path d="M12 7v4"/><path d="m6.7 17.4 3.6-2.4"/><path d="m17.3 17.4-3.6-2.4"/><circle cx="12" cy="12" r="2"/></svg></span><span>Netwerk</span>
</button>
<button class="vtab-btn" :class="{active: tab==='terminals'}" @click="setTab('terminals', $el)" data-tab="terminals">
<span class="vtab-icon">🖥️</span><span>Terminals</span>
<span class="vtab-icon vtab-icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/><path d="m7 8 3 3-3 3"/><path d="M13 14h3"/></svg></span><span>Terminals</span>
</button>
<button class="vtab-btn" :class="{active: tab==='approvals'}" @click="setTab('approvals', $el)">
<span class="vtab-icon">✓</span><span>Goedkeuringen</span>
<span class="vtab-icon vtab-icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg></span><span>Goedkeuringen</span>
<span class="vtab-badge" x-show="pendingCount" x-text="pendingCount"></span>
</button>
<button class="vtab-btn" :class="{active: tab==='warroom'}" @click="setTab('warroom', $el)" data-tab="warroom">
<span class="vtab-icon">⚔️</span><span>War Room</span>
</button>
</nav>
<div class="vtabs-content">
@@ -275,11 +275,19 @@
<section x-show="tab==='warroom'" x-cloak>
<div x-data="warRoomHub()" x-init="init()" class="wr-hub-root">
<div class="panel" style="padding:0.75rem">
<div class="wr-layout">
<div class="wr-layout" :class="{'roster-collapsed': rosterCollapsed}">
<div class="wr-mobile-bar">
<button type="button" class="btn btn-sm" @click="rosterCollapsed = !rosterCollapsed"
x-text="rosterCollapsed ? '👥 Agents' : '▾ Agents'"></button>
<button type="button" class="btn btn-sm wr-a2a-btn"
:class="autoDebate ? 'is-on' : 'is-off'"
@click="toggleA2A()"
x-text="autoDebate ? 'A2A · aan' : 'A2A · uit'"></button>
</div>
<aside class="wr-roster">
<h3>Kanalen</h3>
<button type="button" class="wr-room-btn" :class="{active: room && room.kind==='warroom'}" @click="openWarroom()">
<span>⚔️</span><strong>#war-room</strong>
<span class="wr-chan-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 4 6v6c0 5 3.5 8.5 8 10 4.5-1.5 8-5 8-10V6l-8-4Z"/><path d="m9 12 2 2 4-4"/></svg></span><strong>#war-room</strong>
</button>
<h3 style="margin-top:0.85rem">Agents</h3>
<template x-for="s in souls" :key="'wr-'+s.agent_key">
@@ -301,22 +309,62 @@
</aside>
<div class="wr-main">
<div class="wr-head">
<div>
<div class="wr-head-title">
<h2 x-text="room ? (room.title || ('#'+room.kind)) : 'War Room'"></h2>
<small class="muted" x-show="status" x-text="status"></small>
<small class="wr-msg-count muted" x-show="messages.length"
x-text="(screenCleared ? '0' : visibleMessages().length) + ' / ' + messages.length + ' berichten'"></small>
</div>
<div class="wr-head-actions">
<label class="btn btn-sm" style="display:inline-flex;align-items:center;gap:0.3rem;cursor:pointer;margin:0" title="Agents praten onderling door"><input type="checkbox" x-model="autoDebate" @change="autoDebate && scheduleContinue(1500)" style="margin:0" /> A2A</label>
<button type="button" class="btn btn-sm btn-pulse" @click="openVoiceRoom()" :disabled="!room" title="Spraakmeeting met agents">🎙️ Voice Room</button><button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy || !room" title="Snel praten">🎤</button>
<button type="button" class="btn btn-sm" @click="popOut()" title="Open op tweede scherm">Pop-out ↗</button>
<button type="button" class="btn btn-sm" @click="room && openRoom(room.id)" :disabled="busy">↻</button>
<button type="button" class="btn btn-sm wr-tool-btn" :class="{active: searchOpen}" @click="toggleSearch()" title="Zoeken in chat">🔍 Zoek</button>
<div class="wr-tools-wrap">
<button type="button" class="btn btn-sm wr-tool-btn" @click="toolsOpen=!toolsOpen" title="Meer acties">⋯</button>
<div class="wr-tools-menu" x-show="toolsOpen" @click.outside="toolsOpen=false" x-cloak>
<button type="button" @click="downloadChat('txt')">⬇ Download .txt</button>
<button type="button" @click="downloadChat('md')">⬇ Download .md</button>
<button type="button" @click="downloadChat('json')">⬇ Download .json</button>
<hr />
<button type="button" @click="clearScreen(); toolsOpen=false">🧹 Scherm legen</button>
<button type="button" x-show="screenCleared" @click="restoreScreen(); toolsOpen=false">↩ Scherm herstellen</button>
<button type="button" class="wr-danger" @click="clearChatHistory(); toolsOpen=false">🗑 Geschiedenis wissen</button>
</div>
</div>
<button type="button" class="btn btn-sm wr-a2a-btn"
:class="autoDebate ? 'is-on' : 'is-off'"
@click="toggleA2A()"
:title="autoDebate ? 'A2A staat aan — klik om uit te zetten' : 'A2A staat uit — klik om aan te zetten'">
<span x-text="autoDebate ? 'A2A · aan' : 'A2A · uit'"></span>
</button>
<button type="button" class="btn btn-sm wr-ceo-btn" @click="openCeoInbox()" title="Direct aan jou (@aissa)">
📬 CEO-inbox
<span class="wr-ceo-badge" x-show="ceoUnread > 0" x-text="ceoUnread"></span>
</button>
<button type="button" class="btn btn-sm btn-pulse" @click="openVoiceRoom()" title="Spraakmeeting met agents">🎙️ Voice</button>
<button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy || !room" title="Snel praten">🎤</button>
<button type="button" class="btn btn-sm" @click="popOut()" title="Open op tweede scherm">↗</button>
<button type="button" class="btn btn-sm" @click="room && openRoom(room.id)" :disabled="busy" title="Vernieuwen">↻</button>
</div>
</div>
<div class="wr-log" x-ref="wrLog">
<p class="wr-empty" x-show="!messages.length && !typing">Nog geen berichten — tag agents met @marketing of deel een bestand.</p>
<template x-for="m in messages" :key="m.id">
<div class="wr-searchbar" x-show="searchOpen" x-cloak>
<input type="search" class="form-input" x-ref="wrSearch" x-model="chatSearch"
placeholder="Zoek in berichten, @agent, tekst…" />
<button type="button" class="btn btn-sm" @click="chatSearch=''" x-show="chatSearch">Wis</button>
<button type="button" class="btn btn-sm" @click="toggleSearch()">Sluit</button>
</div>
<div class="wr-log-wrap"><div class="wr-log" x-ref="wrLog" @scroll.passive="onLogScroll()">
<p class="wr-empty" x-show="screenCleared && !typing">
Scherm is geleegd.
<button type="button" class="btn btn-sm" @click="restoreScreen()">Herstellen</button>
</p>
<p class="wr-empty" x-show="!screenCleared && !visibleMessages().length && !typing && messages.length">Geen treffers voor deze zoekopdracht.</p>
<p class="wr-empty" x-show="!screenCleared && !messages.length && !typing">Nog geen berichten — tag agents met @marketing of deel een bestand.</p>
<template x-for="(m, mi) in visibleMessages()" :key="'msg-'+m.id">
<div class="wr-msg-wrap" :data-msg-id="m.id">
<div class="wr-day-sep" x-show="showDaySep(mi)">
<span x-text="formatMsgDate(m)"></span>
</div>
<div class="wr-bubble"
:class="{'is-user': m.sender_type==='user', 'is-agent': m.sender_type==='agent', 'is-system': m.sender_type==='system'}">
:class="{'is-user': m.sender_type==='user', 'is-agent': m.sender_type==='agent', 'is-system': m.sender_type==='system', 'is-ceo-direct': isCeoDirect(m), 'is-reply-target': replyHighlightId == m.id}">
<div class="wr-avatar" x-show="m.sender_type!=='system'">
<template x-if="m.sender_type==='agent'">
<div class="agent-char-wrap"
@@ -333,10 +381,17 @@
</div>
<div class="wr-bubble-body">
<div class="wr-bubble-meta">
<span x-text="labelForMessage(m)"></span>
<span x-show="m.agent_key" x-text="' · @'+m.agent_key"></span>
<button type="button" class="wr-msg-del" title="Verwijderen"
@click.stop="deleteMessage(m)">🗑</button>
<span class="wr-who" x-text="labelForMessage(m)"></span>
<span class="wr-handle" x-show="m.agent_key" x-text="'@'+m.agent_key"></span>
<time class="wr-time" x-text="formatMsgTime(m)" :title="formatMsgDate(m) + ' ' + formatMsgTime(m)"></time>
<button type="button" class="wr-msg-reply" title="Beantwoorden"
@click.stop="setReply(m)">↩ Reply</button>
<button type="button" class="wr-msg-del" title="Bericht verwijderen"
@click.stop.prevent="deleteMessage(m)">🗑</button>
</div>
<div class="wr-reply-quote" x-show="replyPreview(m)" x-cloak>
<span class="wr-reply-quote-who" x-text="'↩ ' + ((replyPreview(m)||{}).agent_key ? ('@'+(replyPreview(m).agent_key)) : ((replyPreview(m)||{}).username || 'bericht'))"></span>
<span class="wr-reply-quote-text" x-text="(replyPreview(m)||{}).excerpt"></span>
</div>
<div class="wr-bubble-text" x-text="m.content"></div>
<div class="wr-attach-list" x-show="attachmentsOf(m).length">
@@ -349,6 +404,7 @@
</div>
</div>
</div>
</div>
</template>
<div class="wr-bubble is-agent is-typing" x-show="typing" x-cloak>
<div class="wr-avatar">
@@ -365,7 +421,13 @@
</div>
</div>
</div>
<div class="wr-composer">
<button type="button" class="wr-jump-latest" x-show="showJumpLatest" x-cloak
@click="jumpToLatest()" title="Naar nieuwste berichten">
↓ Nieuwste
</button>
</div>
<div class="wr-composer" @click="focusComposer()">
<div class="wr-composer-tools">
<button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy" title="Houd in om te praten">🎤</button>
<button type="button" class="btn btn-sm" @click="emojiOpen=!emojiOpen" :disabled="busy">😊</button>
@@ -388,6 +450,13 @@
</span>
</template>
</div>
<div class="wr-reply-bar" x-show="replyTo" x-cloak>
<div class="wr-reply-bar-text">
<strong x-text="'↩ Reply aan ' + (replyTo && (replyTo.agent_key ? ('@'+replyTo.agent_key) : (replyTo.label || 'bericht')))"></strong>
<span x-text="replyTo && replyTo.excerpt"></span>
</div>
<button type="button" class="btn btn-sm" @click="clearReply()" title="Annuleer reply">×</button>
</div>
<div class="wr-composer-wrap">
<div class="wr-mention-pop" x-show="mentionOpen && mentionSuggestions().length" x-cloak>
<template x-for="(s, si) in mentionSuggestions()" :key="'m-'+s.agent_key">
@@ -400,15 +469,51 @@
</template>
</div>
<textarea class="form-input" rows="2" x-ref="wrInput" x-model="draft"
@input="onDraftInput()" @keydown="onKey($event)"
placeholder="Typ @bizdev of @finance… agent antwoordt hier in de War Room"
:disabled="!room"></textarea>
@input="onDraftInput()" @keydown="onKey($event)" @keydown.enter.prevent="onEnter($event)"
placeholder="Typ hier… bv. @herman of @retail"
></textarea>
</div>
<button type="button" class="btn btn-pulse-green" @click="send()" :disabled="!canSend()">Stuur</button>
<button type="button" class="btn btn-pulse-green" @click="send()" :disabled="!canSend() || _sendLock">Stuur</button>
</div>
<div class="wr-voice-overlay" :class="{ 'is-open': voiceRoomOpen }" x-show="voiceRoomOpen" x-cloak @keydown.escape.window="closeVoiceRoom()">
<!-- CEO inbox (@aissa) -->
<div class="wr-ceo-overlay" x-show="ceoInboxOpen" x-cloak @keydown.escape.window="closeCeoInbox()">
<div class="wr-ceo-panel" @click.stop>
<div class="wr-ceo-head">
<div>
<h3>📬 CEO-inbox · @aissa</h3>
<small class="muted">Direct van agents aan jou (@aissa) — ook ongelezen van toen je offline was</small>
</div>
<div class="wr-ceo-head-actions">
<button type="button" class="btn btn-sm" @click="markAllCeoRead()" :disabled="!ceoUnread">Alles gelezen</button>
<button type="button" class="btn btn-sm" @click="loadCeoInbox()">↻</button>
<button type="button" class="btn btn-sm" @click="closeCeoInbox()">Sluit</button>
</div>
</div>
<div class="wr-ceo-list">
<p class="wr-empty" x-show="!(ceoInbox||[]).length">Nog geen directe berichten. Agents gebruiken @aissa als ze jou echt nodig hebben.</p>
<template x-for="item in ceoInbox" :key="'ceo-'+item.id">
<article class="wr-ceo-item" :class="{ unread: !item.read_at }">
<div class="wr-ceo-item-meta">
<strong x-text="item.agent_display_name || ('@'+(item.agent_key||'agent'))"></strong>
<span x-text="'@'+(item.agent_key||'')"></span>
<time x-text="formatMsgTime({created_at: item.created_at})"></time>
<span class="wr-ceo-unread-dot" x-show="!item.read_at">nieuw</span>
</div>
<div class="wr-ceo-item-body" x-text="item.content"></div>
<div class="wr-ceo-item-actions">
<button type="button" class="btn btn-sm" x-show="!item.read_at" @click="markCeoRead(item.id)">Markeer gelezen</button>
</div>
</article>
</template>
</div>
</div>
</div>
<template x-if="voiceRoomOpen">
<div class="wr-voice-overlay is-open" @keydown.escape.window="closeVoiceRoom()">
<div class="wr-voice-panel" @click.stop>
<div class="wr-voice-head">
<div>
@@ -468,6 +573,7 @@
</div>
</div>
</div>
</template>
</div>
</div>
@@ -477,7 +583,7 @@
</div>
</div>
<div class="agent-events-backdrop" x-show="eventsOpen" x-cloak @click="closeEvents()"></div>
<div class="agent-events-backdrop" :class="{ 'is-open': eventsOpen }" x-show="eventsOpen" x-cloak @click="closeEvents()"></div>
<aside class="agent-events-drawer" :class="{ open: eventsOpen }" x-cloak>
<header class="agent-events-head">
<div>
@@ -516,7 +622,7 @@
<script src="/static/js/page-viz.js?v=1"></script>
<script src="/static/js/agents-mesh.js?v=11"></script>
<script src="/static/js/agents-terminals.js?v=6"></script>
<script src="/static/js/agents-warroom.js?v=18"></script>
<script src="/static/js/agents-warroom.js?v=40"></script>
<script>
function agentsSoulHub() {
return {
+11 -9
View File
@@ -9,8 +9,8 @@
<link rel="stylesheet" href="/static/css/flk-themes.css?v=1" />
<link rel="stylesheet" href="/static/css/pulse-theme.css?v=2" />
<link rel="stylesheet" href="/static/css/agent-characters.css?v=4" />
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=18" />
<script defer src="/static/js/agents-warroom.js?v=18"></script>
<link rel="stylesheet" href="/static/css/agents-warroom.css?v=31" />
<script defer src="/static/js/agents-warroom.js?v=31"></script>
<script defer src="/static/js/alpine.min.js?v=2"></script>
</head>
<body class="wr-popup-body">
@@ -47,7 +47,7 @@
</div>
<div class="wr-head-actions">
<label class="btn btn-sm" style="display:inline-flex;align-items:center;gap:0.3rem;cursor:pointer;margin:0" title="Agents praten onderling door"><input type="checkbox" x-model="autoDebate" @change="autoDebate && scheduleContinue(1500)" style="margin:0" /> A2A</label>
<button type="button" class="btn btn-sm btn-pulse" @click="openVoiceRoom()" :disabled="!room" title="Spraakmeeting met agents">🎙️ Voice Room</button><button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy || !room" title="Snel praten">🎤</button>
<button type="button" class="btn btn-sm btn-pulse" @click="openVoiceRoom()" title="Spraakmeeting met agents">🎙️ Voice Room</button><button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy || !room" title="Snel praten">🎤</button>
<a class="btn btn-sm" href="/agents?tab=warroom" target="_blank">In cockpit</a>
<button type="button" class="btn btn-sm" @click="room && openRoom(room.id)" :disabled="busy">↻</button>
</div>
@@ -99,7 +99,7 @@
</div>
</div>
</div>
<div class="wr-composer">
<div class="wr-composer" @click="focusComposer()">
<div class="wr-composer-tools">
<button type="button" class="btn btn-sm" :class="{'is-recording': voiceRecording}" @mousedown.prevent="startVoiceTalk($event)" @mouseup.prevent="stopVoiceTalk($event)" @touchstart.prevent="startVoiceTalk($event)" @touchend.prevent="stopVoiceTalk($event)" :disabled="voiceBusy || busy" title="Houd in om te praten">🎤</button>
<button type="button" class="btn btn-sm" @click="emojiOpen=!emojiOpen" :disabled="busy">😊</button>
@@ -134,15 +134,16 @@
</template>
</div>
<textarea class="form-input" rows="2" x-ref="wrInput" x-model="draft"
@input="onDraftInput()" @keydown="onKey($event)"
placeholder="Typ @bizdev… antwoord komt in deze War Room"
:disabled="!room"></textarea>
@input="onDraftInput()" @keydown="onKey($event)" @keydown.enter.prevent="onEnter($event)"
placeholder="Typ hier… bv. @herman"
></textarea>
</div>
<button type="button" class="btn btn-pulse-green" @click="send()" :disabled="!canSend()">Stuur</button>
<button type="button" class="btn btn-pulse-green" @click="send()" :disabled="!canSend() || _sendLock">Stuur</button>
</div>
<div class="wr-voice-overlay" :class="{ 'is-open': voiceRoomOpen }" x-show="voiceRoomOpen" x-cloak @keydown.escape.window="closeVoiceRoom()">
<template x-if="voiceRoomOpen">
<div class="wr-voice-overlay is-open" @keydown.escape.window="closeVoiceRoom()">
<div class="wr-voice-panel" @click.stop>
<div class="wr-voice-head">
<div>
@@ -202,6 +203,7 @@
</div>
</div>
</div>
</template>
</div>
</div>
+129 -28
View File
@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="nl">
<script>(function(){try{var t=localStorage.getItem('flk-theme');if(t)document.documentElement.setAttribute('data-theme',t);var m=document.querySelector('meta[name=theme-color]');}catch(e){}})();</script>
<script>(function(){try{var t=localStorage.getItem('flk-theme');if(t)document.documentElement.setAttribute('data-theme',t);var s=localStorage.getItem('flk-sidebar-theme');if(s)document.documentElement.setAttribute('data-sidebar',s);var b=localStorage.getItem('flk-page-bg');if(b)document.documentElement.setAttribute('data-bg',b);var ss=localStorage.getItem('flk-sidebar-style');if(ss==='plain')document.documentElement.setAttribute('data-sidebar-style','plain');else document.documentElement.removeAttribute('data-sidebar-style');}catch(e){}})();</script>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=5" />
@@ -22,20 +22,20 @@
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png" sizes="180x180" />
<link rel="apple-touch-icon" href="/static/icons/icon-192.png" sizes="192x192" />
<link rel="stylesheet" href="/static/css/palantir-theme.css?v=4" />
<link rel="stylesheet" href="/static/css/flk-themes.css?v=1" />
<link rel="stylesheet" href="/static/css/flk-themes.css?v=8" />
<link rel="stylesheet" href="/static/css/pulse-theme.css?v=2" />
<link rel="stylesheet" href="/static/css/topnav-neo.css?v=6" />
<link rel="stylesheet" href="/static/css/vertical-tabs.css?v=9" />
<link rel="stylesheet" href="/static/css/vertical-tabs.css?v=10" />
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=34" />
<script defer src="/static/js/herman-assistant.js?v=17"></script>
<script defer src="/static/js/alpine.min.js?v=2"></script>
<script defer src="/static/js/htmx.min.js?v=2"></script>
{% block extra_head %}{% endblock %}
<link rel="stylesheet" href="/static/css/app-sidebar-neo.css?v=12" />
<link rel="stylesheet" href="/static/css/app-sidebar-neo.css?v=13" />
<link rel="stylesheet" href="/static/css/cockpit-typography.css?v=1" />
<link rel="stylesheet" href="/static/css/herman-assistant.css?v=16" />
<!-- mobile last: wins over sidebar/dashboard cascade -->
<link rel="stylesheet" href="/static/css/mobile.css?v=7" />
<link rel="stylesheet" href="/static/css/mobile.css?v=12" />
<link rel="stylesheet" href="/static/css/user-avatar.css?v=3" />
</head>
<body x-data="appShell()" x-init="initApp()" :class="{ 'nav-open': navOpen, 'drawer-open': drawerOpen, 'brand-cucina': brand === 'cucina', 'brand-foodlinkk': brand === 'foodlinkk' }">
@@ -60,36 +60,36 @@
<div class="persona-nav">
<div class="app-sidebar-group" x-show="brand === 'foodlinkk'">
<div class="app-sidebar-label">Foodlinkk B2B</div>
<a href="/export-intel" data-perm="export" class="nav-pill nav-pill-wereldexport {% if request.url.path.startswith('/export-intel') %}active{% endif %}"><span class="sidebar-nav-icon">🌍</span><span class="sidebar-nav-label">Wereldexport</span></a>
<a href="/clients" data-perm="clients" class="nav-pill nav-pill-clients {% if request.url.path.startswith('/clients') %}active{% endif %}"><span class="sidebar-nav-icon">👥</span><span class="sidebar-nav-label" data-i18n="nav.clients">Klanten</span></a>
<a href="/deals" data-perm="deals" class="nav-pill nav-pill-deals {% if request.url.path.startswith('/deals') %}active{% endif %}"><span class="sidebar-nav-icon">🤝</span><span class="sidebar-nav-label" data-i18n="nav.deals">Deals</span></a>
<a href="/products" data-perm="products" class="nav-pill nav-pill-products {% if request.url.path.startswith('/products') %}active{% endif %}"><span class="sidebar-nav-icon">📦</span><span class="sidebar-nav-label" data-i18n="nav.products">Producten</span></a>
<a href="/suppliers" data-perm="suppliers" class="nav-pill nav-pill-suppliers {% if request.url.path.startswith('/suppliers') %}active{% endif %}"><span class="sidebar-nav-icon">🏭</span><span class="sidebar-nav-label" data-i18n="nav.suppliers">Leveranciers</span></a>
<a href="/reports" data-perm="reports" class="nav-pill nav-pill-reports {% if request.url.path.startswith('/reports') %}active{% endif %}"><span class="sidebar-nav-icon">📊</span><span class="sidebar-nav-label" data-i18n="nav.reports">Rapporten</span></a>
<a href="/export-intel" data-perm="export" class="nav-pill nav-pill-wereldexport {% if request.url.path.startswith('/export-intel') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span><span class="sidebar-nav-label">Wereldexport</span></a>
<a href="/clients" data-perm="clients" class="nav-pill nav-pill-clients {% if request.url.path.startswith('/clients') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.clients">Klanten</span></a>
<a href="/deals" data-perm="deals" class="nav-pill nav-pill-deals {% if request.url.path.startswith('/deals') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 6"/><path d="m3 13 1.68-1.68a2.41 2.41 0 0 1 3.41 0L12 16"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.deals">Deals</span></a>
<a href="/products" data-perm="products" class="nav-pill nav-pill-products {% if request.url.path.startswith('/products') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"/><path d="M12 22V12"/><path d="m3.3 7 7.7 4.7L18.7 7"/><path d="m7.5 4.3 9 5.2"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.products">Producten</span></a>
<a href="/suppliers" data-perm="suppliers" class="nav-pill nav-pill-suppliers {% if request.url.path.startswith('/suppliers') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7 5V8l-7 5V4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M17 18h1"/><path d="M12 18h1"/><path d="M7 18h1"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.suppliers">Leveranciers</span></a>
<a href="/reports" data-perm="reports" class="nav-pill nav-pill-reports {% if request.url.path.startswith('/reports') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.reports">Rapporten</span></a>
</div>
<div class="app-sidebar-group" x-show="brand === 'cucina'">
<div class="app-sidebar-label">Cucina B2C</div>
<a href="/" data-perm="dashboard" class="nav-pill nav-pill-herman {% if request.url.path == '/' %}active{% endif %}"><span class="sidebar-nav-icon">🏠</span><span class="sidebar-nav-label" data-i18n="nav.ceo_home">Overzicht</span></a>
<a href="/revenue-cockpit" data-perm="revenue" class="nav-pill nav-pill-deals {% if request.url.path.startswith('/revenue-cockpit') %}active{% endif %}"><span class="sidebar-nav-icon">💰</span><span class="sidebar-nav-label" data-i18n="nav.revenue_cockpit">Revenue Cockpit</span></a>
<a href="/retail" data-perm="retail" class="nav-pill nav-pill-retail {% if request.url.path.startswith('/retail') %}active{% endif %}"><span class="sidebar-nav-icon">🛒</span><span class="sidebar-nav-label" data-i18n="nav.retail">Retail NL</span></a>
<a href="/marketing" data-perm="marketing" class="nav-pill nav-pill-marketing {% if request.url.path.startswith('/marketing') %}active{% endif %}"><span class="sidebar-nav-icon">📣</span><span class="sidebar-nav-label" data-i18n="nav.marketing">Marketing</span></a>
<a href="/packaging" data-perm="packaging" class="nav-pill nav-pill-studio {% if request.url.path.startswith('/packaging') %}active{% endif %}"><span class="sidebar-nav-icon">📦</span><span class="sidebar-nav-label" data-i18n="nav.packaging">Packaging</span></a>
<a href="/beurs" data-perm="beurs" class="nav-pill nav-pill-beurs {% if request.url.path.startswith('/beurs') %}active{% endif %}"><span class="sidebar-nav-icon">🎪</span><span class="sidebar-nav-label" data-i18n="nav.beurs">Beurs Live</span></a>
<a href="/documents" data-perm="documents" class="nav-pill nav-pill-documents {% if request.url.path.startswith('/documents') %}active{% endif %}"><span class="sidebar-nav-icon">📄</span><span class="sidebar-nav-label" data-i18n="nav.documents">Documenten</span></a>
<a href="/projects" data-perm="projects" class="nav-pill nav-pill-documents {% if request.url.path.startswith('/projects') %}active{% endif %}"><span class="sidebar-nav-icon">📁</span><span class="sidebar-nav-label" data-i18n="nav.projects">Projecten</span></a>
<a href="/hermes" data-perm="hermes" class="nav-pill nav-pill-telegram {% if request.url.path.startswith('/hermes') %}active{% endif %}"><span class="sidebar-nav-icon">✈️</span><span class="sidebar-nav-label" data-i18n="nav.telegram">Telegram</span></a>
<a href="/analytics" data-perm="analytics" class="nav-pill nav-pill-analytics {% if request.url.path.startswith('/analytics') %}active{% endif %}"><span class="sidebar-nav-icon">📈</span><span class="sidebar-nav-label" data-i18n="nav.analytics">Analytics</span></a>
<a href="/" data-perm="dashboard" class="nav-pill nav-pill-herman {% if request.url.path == '/' %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.ceo_home">Overzicht</span></a>
<a href="/revenue-cockpit" data-perm="revenue" class="nav-pill nav-pill-deals {% if request.url.path.startswith('/revenue-cockpit') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.revenue_cockpit">Revenue Cockpit</span></a>
<a href="/retail" data-perm="retail" class="nav-pill nav-pill-retail {% if request.url.path.startswith('/retail') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.retail">Retail NL</span></a>
<a href="/marketing" data-perm="marketing" class="nav-pill nav-pill-marketing {% if request.url.path.startswith('/marketing') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m3 11 18-5v12L3 13v-2z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.marketing">Marketing</span></a>
<a href="/packaging" data-perm="packaging" class="nav-pill nav-pill-studio {% if request.url.path.startswith('/packaging') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.packaging">Packaging</span></a>
<a href="/beurs" data-perm="beurs" class="nav-pill nav-pill-beurs {% if request.url.path.startswith('/beurs') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 21 14 3"/><path d="M20.5 21 10 3"/><path d="M15.5 21 12 15l-3.5 6"/><path d="M2 21h20"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.beurs">Beurs Live</span></a>
<a href="/documents" data-perm="documents" class="nav-pill nav-pill-documents {% if request.url.path.startswith('/documents') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.documents">Documenten</span></a>
<a href="/projects" data-perm="projects" class="nav-pill nav-pill-documents {% if request.url.path.startswith('/projects') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.projects">Projecten</span></a>
<a href="/hermes" data-perm="hermes" class="nav-pill nav-pill-telegram {% if request.url.path.startswith('/hermes') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.telegram">Telegram</span></a>
<a href="/analytics" data-perm="analytics" class="nav-pill nav-pill-analytics {% if request.url.path.startswith('/analytics') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.analytics">Analytics</span></a>
</div>
<div class="app-sidebar-group">
<div class="app-sidebar-label" data-i18n="nav.platform">Platform</div>
<a href="/ops" data-perm="ops" class="nav-pill nav-pill-ops {% if request.url.path.startswith('/ops') %}active{% endif %}"><span class="sidebar-nav-icon">⚙️</span><span class="sidebar-nav-label" data-i18n="nav.ops">IT Ops</span></a>
<a href="/agents" data-perm="agents" class="nav-pill nav-pill-agents {% if request.url.path.startswith('/agents') %}active{% endif %}"><span class="sidebar-nav-icon">🤖</span><span class="sidebar-nav-label" data-i18n="nav.agents">Agents</span></a>
<a href="/browser" data-perm="browser" class="nav-pill nav-pill-browser {% if request.url.path.startswith('/browser') or request.url.path.startswith('/monitor') %}active{% endif %}"><span class="sidebar-nav-icon">🌐</span><span class="sidebar-nav-label" data-i18n="nav.browser">Browser</span></a>
<a href="/studio" data-perm="studio" class="nav-pill nav-pill-studio {% if request.url.path.startswith('/studio') %}active{% endif %}"><span class="sidebar-nav-icon">🎨</span><span class="sidebar-nav-label" data-i18n="nav.studio">Studio</span></a>
<a href="/voice" data-perm="voice" class="nav-pill nav-pill-voice {% if request.url.path.startswith('/voice') %}active{% endif %}"><span class="sidebar-nav-icon">🎙️</span><span class="sidebar-nav-label" data-i18n="nav.voice">Voice</span></a>
<a href="/settings" data-perm="settings" class="nav-pill nav-pill-settings {% if request.url.path.startswith('/settings') %}active{% endif %}"><span class="sidebar-nav-icon">🔧</span><span class="sidebar-nav-label" data-i18n="nav.settings">Instellingen</span></a>
<a href="/ops" data-perm="ops" class="nav-pill nav-pill-ops {% if request.url.path.startswith('/ops') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.ops">IT Ops</span></a>
<a href="/agents?tab=warroom" data-perm="agents" class="nav-pill nav-pill-agents {% if request.url.path.startswith('/agents') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.agents">Agents</span></a>
<a href="/browser" data-perm="browser" class="nav-pill nav-pill-browser {% if request.url.path.startswith('/browser') or request.url.path.startswith('/monitor') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.browser">Browser</span></a>
<a href="/studio" data-perm="studio" class="nav-pill nav-pill-studio {% if request.url.path.startswith('/studio') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.studio">Studio</span></a>
<a href="/voice" data-perm="voice" class="nav-pill nav-pill-voice {% if request.url.path.startswith('/voice') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.voice">Voice</span></a>
<a href="/settings" data-perm="settings" class="nav-pill nav-pill-settings {% if request.url.path.startswith('/settings') %}active{% endif %}"><span class="sidebar-nav-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="4" y1="21" y2="14"/><line x1="4" x2="4" y1="10" y2="3"/><line x1="12" x2="12" y1="21" y2="12"/><line x1="12" x2="12" y1="8" y2="3"/><line x1="20" x2="20" y1="21" y2="16"/><line x1="20" x2="20" y1="12" y2="3"/><line x1="2" x2="6" y1="14" y2="14"/><line x1="10" x2="14" y1="8" y2="8"/><line x1="18" x2="22" y1="16" y2="16"/></svg></span><span class="sidebar-nav-label" data-i18n="nav.settings">Instellingen</span></a>
</div>
</div>
</aside>
@@ -536,23 +536,124 @@ window.FlkThemes = {
{ id: 'aurora', name: 'Aurora', desc: 'Blauw-violet met cyaan highlights' },
{ id: 'neon', name: 'Neon Tide', desc: 'Donker neon cyaan + magenta' },
{ id: 'glacier', name: 'Glacier', desc: 'IJsblauw en helder sky-cyan' },
{ id: 'cobalt', name: 'Cobalt Pulse', desc: 'Kobaltblauw met aqua en amber' }
{ id: 'cobalt', name: 'Cobalt Pulse', desc: 'Kobaltblauw met aqua en amber' },
{ id: 'reef', name: 'Reef', desc: 'Teal-diep met helder mint' },
{ id: 'plasma', name: 'Plasma', desc: 'Paars-nacht met cyaan vonken' },
{ id: 'lagoon', name: 'Lagoon', desc: 'Tropisch blauwgroen water' },
{ id: 'indigo', name: 'Indigo Night', desc: 'Indigo met lavendel accents' },
{ id: 'skyline', name: 'Skyline', desc: 'Stad-nacht blauw + sky' },
{ id: 'mintvolt', name: 'Mint Volt', desc: 'Donker groen met mint-energie' },
{ id: 'sapphire', name: 'Sapphire', desc: 'Klassiek saffierblauw' },
{ id: 'arctic', name: 'Arctic Glow', desc: 'Koud grijsblauw met ice-cyan' },
{ id: 'hyperion', name: 'Hyperion', desc: 'Cyaan met lime-flits' },
{ id: 'meridian', name: 'Meridian', desc: 'Violet-meridiaan met aqua' }
],
sidebars: [
{ id: 'aqua', name: 'Deep Aqua', desc: 'Diep aqua, strak en koel' },
{ id: 'azure', name: 'Royal Azure', desc: 'Diep koningsblauw' },
{ id: 'turquoise', name: 'Abyss Teal', desc: 'Diep teal, premium' },
{ id: 'cerulean', name: 'Ocean Depth', desc: 'Diep oceaanblauw' },
{ id: 'electric', name: 'Electric Cyan', desc: 'Donker elektrisch cyaan' },
{ id: 'violet', name: 'Indigo Night', desc: 'Diep indigo-violet (geen roze)' },
{ id: 'emerald', name: 'Jade Depth', desc: 'Diep jade smaragd' },
{ id: 'midnight', name: 'Midnight Navy', desc: 'Diep marinezwart-blauw' },
{ id: 'steel', name: 'Steel Blue', desc: 'Strak staalblauw' },
{ id: 'nordic', name: 'Nordic Frost', desc: 'Koel nordic blauw' }
],
backgrounds: [
{ id: 'navy', name: 'Deep Navy', desc: 'Diep marineblauw' },
{ id: 'royal', name: 'Royal Blue', desc: 'Koningsblauw' },
{ id: 'azure', name: 'Azure Sea', desc: 'Helder azuur' },
{ id: 'sky', name: 'Sky Blue', desc: 'Heldere luchtblauw' },
{ id: 'cerulean', name: 'Cerulean', desc: 'Ceruleum' },
{ id: 'teal', name: 'Deep Teal', desc: 'Diep teal' },
{ id: 'lagoon', name: 'Lagoon Blue', desc: 'Lagune blauwgroen' },
{ id: 'cyan', name: 'Cyan Depth', desc: 'Cyaan diepte' },
{ id: 'indigo', name: 'Indigo Field', desc: 'Indigo veld' },
{ id: 'violet', name: 'Violet Dusk', desc: 'Violet schemer' },
{ id: 'sapphire', name: 'Sapphire', desc: 'Saffierblauw' },
{ id: 'midnight', name: 'Midnight', desc: 'Nachtblauw (niet zwart)' },
{ id: 'emerald', name: 'Emerald Sea', desc: 'Smaragd zee' },
{ id: 'slate', name: 'Slate Blue', desc: 'Leisteenblauw' },
{ id: 'graphite', name: 'Graphite Blue', desc: 'Grafietblauw' }
],
current: function () {
try { return localStorage.getItem('flk-theme') || 'ocean'; } catch (e) { return 'ocean'; }
},
currentSidebar: function () {
try { return localStorage.getItem('flk-sidebar-theme') || ''; } catch (e) { return ''; }
},
apply: function (id) {
var ok = this.list.some(function (t) { return t.id === id; });
if (!ok) id = 'ocean';
document.documentElement.setAttribute('data-theme', id);
try { localStorage.setItem('flk-theme', id); } catch (e) {}
return id;
},
applySidebar: function (id) {
if (!id) {
document.documentElement.removeAttribute('data-sidebar');
try { localStorage.removeItem('flk-sidebar-theme'); } catch (e) {}
return '';
}
var migrate = { coral: 'midnight', fuchsia: 'nordic', lime: 'steel' };
if (migrate[id]) id = migrate[id];
var ok = this.sidebars.some(function (t) { return t.id === id; });
if (!ok) {
document.documentElement.removeAttribute('data-sidebar');
try { localStorage.removeItem('flk-sidebar-theme'); } catch (e) {}
return '';
}
document.documentElement.setAttribute('data-sidebar', id);
try { localStorage.setItem('flk-sidebar-theme', id); } catch (e) {}
return id;
},
currentSidebarStyle: function () {
try { return localStorage.getItem('flk-sidebar-style') || 'gradient'; } catch (e) { return 'gradient'; }
},
applySidebarStyle: function (mode) {
if (mode !== 'plain') mode = 'gradient';
if (mode === 'plain') {
document.documentElement.setAttribute('data-sidebar-style', 'plain');
} else {
document.documentElement.removeAttribute('data-sidebar-style');
}
try { localStorage.setItem('flk-sidebar-style', mode); } catch (e) {}
return mode;
},
currentBg: function () {
try { return localStorage.getItem('flk-page-bg') || ''; } catch (e) { return ''; }
},
applyBg: function (id) {
if (!id) {
document.documentElement.removeAttribute('data-bg');
try { localStorage.removeItem('flk-page-bg'); } catch (e) {}
return '';
}
var ok = (this.backgrounds || []).some(function (t) { return t.id === id; });
if (!ok) {
document.documentElement.removeAttribute('data-bg');
try { localStorage.removeItem('flk-page-bg'); } catch (e) {}
return '';
}
document.documentElement.setAttribute('data-bg', id);
try { localStorage.setItem('flk-page-bg', id); } catch (e) {}
return id;
}
};
if (!document.documentElement.getAttribute('data-theme')) {
window.FlkThemes.apply(window.FlkThemes.current());
}
(function () {
var s = window.FlkThemes.currentSidebar();
if (s) window.FlkThemes.applySidebar(s);
if (window.FlkThemes.applySidebarStyle) window.FlkThemes.applySidebarStyle(window.FlkThemes.currentSidebarStyle());
var b = window.FlkThemes.currentBg();
if (b) window.FlkThemes.applyBg(b);
})();
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+11 -6
View File
@@ -4,24 +4,28 @@
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css?v=1" />
<link rel="stylesheet" href="/static/vendor/leaflet.markercluster/MarkerCluster.css?v=1" />
<link rel="stylesheet" href="/static/vendor/leaflet.markercluster/MarkerCluster.Default.css?v=1" />
<link rel="stylesheet" href="/static/css/export-intel.css?v=32" />
<link rel="stylesheet" href="/static/css/export-intel-tabs.css?v=20" />
<link rel="stylesheet" href="/static/css/export-intel.css?v=23" />
<link rel="stylesheet" href="/static/css/export-intel-tabs.css?v=21" />
<script src="/static/vendor/leaflet/leaflet.js?v=1"></script>
<script src="/static/vendor/leaflet.markercluster/leaflet.markercluster.js?v=1"></script>
<script src="/static/js/export-intel-map.js?v=21"></script>
<script src="/static/js/export-intel.js?v=32"></script>
<script src="/static/js/export-intel.js?v=48"></script>
<script src="/static/js/export-intel-profile.js?v=7"></script>
{% endblock %}
{% block content %}
<div class="ei-page" x-data="exportIntelApp()" x-init="init()">
<div class="ei-progress-overlay" x-show="isWorking()" x-cloak>
<div class="ei-progress-overlay" x-show="isWorking()" x-cloak>
<div class="ei-progress-card">
<div class="ei-progress-spinner"></div>
<p x-text="progressLabel()"></p>
<div class="ei-progress-bar"><div class="ei-progress-bar-fill" :style="'width:' + (syncMonitorPercent() || 100) + '%'"></div></div>
<div class="ei-progress-bar"><div class="ei-progress-bar-fill" :style="'width:' + (syncMonitorPercent() || 8) + '%'"></div></div>
<div class="ei-progress-actions">
<button type="button" class="btn btn-sm ei-btn-cancel" @click="cancelSync()" x-show="busy || (syncMonitor && syncMonitor.any_running)">⏹ Annuleer sync</button>
<button type="button" class="btn btn-sm" @click="dismissOverlay()" title="Overlay sluiten — sync loopt door in monitor">Verberg</button>
</div>
</div>
</div>
<header class="ei-header page-header">
<header class="ei-header page-header">
<div>
<h1>Wereldexport <span class="live-badge">Live</span></h1>
<p class="ei-subtitle">Foodlinkk B2B · wereldwijde markten · halal vlees · cateraars · distributeurs</p>
@@ -49,6 +53,7 @@
<div class="ei-sync-monitor-head">
<strong>Sync voortgang</strong>
<span x-show="syncMonitor && syncMonitor.any_running" class="ei-sync-live">● Live</span>
<button type="button" class="btn btn-sm ei-btn-cancel" style="margin-left:auto" @click="cancelSync()" x-show="syncMonitor && syncMonitor.any_running">⏹ Annuleer</button>
</div>
<template x-if="syncMonitor && syncMonitor.active_jobs && syncMonitor.active_jobs.length">
<div class="ei-sync-jobs">
+136 -5
View File
@@ -289,7 +289,7 @@
<div class="settings-panel-head">
<div>
<h2>UI Themes</h2>
<p class="page-tip">Kies een kleurrijk cyaan/blauw thema voor de hele cockpit. Direct toegepast op alle pagina's.</p>
<p class="page-tip">UI-theme, pagina-achtergrond en sidebar-kleur zijn los van elkaar te kiezen.</p>
</div>
</div>
<div class="flk-theme-grid">
@@ -301,6 +301,48 @@
</button>
</template>
</div>
<div class="flk-theme-section">
<h3>Pagina achtergrond</h3>
<p class="page-tip">15 echte kleuren voor alle subpagina’s (niet zwart). Los van UI-theme en sidebar.</p>
<div class="flk-theme-grid">
<button type="button" class="flk-theme-card" :class="{ 'is-active': !bg }" @click="selectBg('')">
<div class="flk-theme-swatch" style="background:linear-gradient(135deg,#07131f,#123049)"></div>
<strong>Standaard</strong>
<small>Volgt UI-theme achtergrond</small>
</button>
<template x-for="b in backgrounds" :key="'bg-'+b.id">
<button type="button" class="flk-theme-card" :class="{ 'is-active': bg === b.id }" @click="selectBg(b.id)">
<div class="flk-theme-swatch" :style="bgSwatch(b.id)"></div>
<strong x-text="b.name"></strong>
<small x-text="b.desc"></small>
</button>
</template>
</div>
</div>
<div class="flk-theme-section">
<h3>Sidebar Themes</h3>
<p class="page-tip">Los van het UI-theme — 10 diepe, strakke kleuren (geen rood/roze). Kies gradient of effen.</p>
<div class="flk-style-toggle" role="group" aria-label="Sidebar stijl">
<button type="button" :class="{ 'is-active': sidebarStyle === 'gradient' }" @click="selectSidebarStyle('gradient')">Gradient</button>
<button type="button" :class="{ 'is-active': sidebarStyle === 'plain' }" @click="selectSidebarStyle('plain')">Effen kleur</button>
</div>
<div class="flk-theme-grid">
<button type="button" class="flk-theme-card" :class="{ 'is-active': !sidebar }" @click="selectSidebar('')">
<div class="flk-theme-swatch" style="background:linear-gradient(180deg,#080c12,#121820)"></div>
<strong>Standaard</strong>
<small>Geen aparte sidebar-kleur (volgt UI)</small>
</button>
<template x-for="s in sidebars" :key="'sb-'+s.id">
<button type="button" class="flk-theme-card" :class="{ 'is-active': sidebar === s.id }" @click="selectSidebar(s.id)">
<div class="flk-theme-swatch" :data-sb="s.id" :data-sb-style="sidebarStyle" :style="sidebarSwatch(s.id)"></div>
<strong x-text="s.name"></strong>
<small x-text="s.desc"></small>
</button>
</template>
</div>
</div>
</section>
{% elif active_tab == 'general' %}
@@ -318,14 +360,42 @@
function flkThemesPage() {
return {
themes: (window.FlkThemes && window.FlkThemes.list) || [],
sidebars: (window.FlkThemes && window.FlkThemes.sidebars) || [],
backgrounds: (window.FlkThemes && window.FlkThemes.backgrounds) || [],
current: 'ocean',
sidebar: '',
sidebarStyle: 'gradient',
bg: '',
init() {
this.themes = (window.FlkThemes && window.FlkThemes.list) || [];
this.sidebars = (window.FlkThemes && window.FlkThemes.sidebars) || [];
this.backgrounds = (window.FlkThemes && window.FlkThemes.backgrounds) || [];
this.current = (window.FlkThemes && window.FlkThemes.current()) || 'ocean';
if (window.FlkThemes) window.FlkThemes.apply(this.current);
this.sidebar = (window.FlkThemes && window.FlkThemes.currentSidebar()) || '';
this.sidebarStyle = (window.FlkThemes && window.FlkThemes.currentSidebarStyle && window.FlkThemes.currentSidebarStyle()) || 'gradient';
this.bg = (window.FlkThemes && window.FlkThemes.currentBg && window.FlkThemes.currentBg()) || '';
if (window.FlkThemes) {
window.FlkThemes.apply(this.current);
window.FlkThemes.applySidebar(this.sidebar);
if (window.FlkThemes.applySidebarStyle) window.FlkThemes.applySidebarStyle(this.sidebarStyle);
if (window.FlkThemes.applyBg) window.FlkThemes.applyBg(this.bg);
}
},
select(id) {
this.current = window.FlkThemes.apply(id);
if (window.Cockpit) Cockpit.toast('Theme: ' + id, 'ok');
if (window.Cockpit) Cockpit.toast('UI theme: ' + id, 'ok');
},
selectSidebar(id) {
this.sidebar = window.FlkThemes.applySidebar(id || '');
if (window.Cockpit) Cockpit.toast(id ? ('Sidebar: ' + id) : 'Sidebar: standaard', 'ok');
},
selectSidebarStyle(mode) {
this.sidebarStyle = window.FlkThemes.applySidebarStyle(mode || 'gradient');
if (window.Cockpit) Cockpit.toast(mode === 'plain' ? 'Sidebar: effen kleur' : 'Sidebar: gradient', 'ok');
},
selectBg(id) {
this.bg = window.FlkThemes.applyBg(id || '');
if (window.Cockpit) Cockpit.toast(id ? ('Achtergrond: ' + id) : 'Achtergrond: standaard', 'ok');
},
swatch(id) {
var map = {
@@ -333,9 +403,70 @@ function flkThemesPage() {
aurora: 'linear-gradient(135deg,#0a1020,#1a2550 45%,#a78bfa 75%,#22d3ee)',
neon: 'linear-gradient(135deg,#050b14,#10243f 40%,#06b6d4 70%,#e879f9)',
glacier: 'linear-gradient(135deg,#0b1522,#1a2f4a 45%,#38bdf8 80%,#7dd3fc)',
cobalt: 'linear-gradient(135deg,#08101c,#172642 40%,#3b82f6 70%,#22d3ee)'
cobalt: 'linear-gradient(135deg,#08101c,#172642 40%,#3b82f6 70%,#22d3ee)',
reef: 'linear-gradient(135deg,#04161a,#11353f 40%,#2dd4bf)',
plasma: 'linear-gradient(135deg,#0c0618,#26114a 40%,#c084fc 75%,#22d3ee)',
lagoon: 'linear-gradient(135deg,#031a1f,#0c3a45 40%,#22d3ee)',
indigo: 'linear-gradient(135deg,#07091a,#1a2250 40%,#818cf8 75%,#22d3ee)',
skyline: 'linear-gradient(135deg,#06101c,#142d4d 40%,#38bdf8)',
mintvolt: 'linear-gradient(135deg,#04140f,#10352a 40%,#34d399 70%,#22d3ee)',
sapphire: 'linear-gradient(135deg,#050d1f,#122650 40%,#3b82f6 70%,#38bdf8)',
arctic: 'linear-gradient(135deg,#0a121c,#1c2a3d 45%,#67e8f9)',
hyperion: 'linear-gradient(135deg,#041018,#0e2d42 40%,#22d3ee 70%,#a3e635)',
meridian: 'linear-gradient(135deg,#0a0716,#221645 40%,#c084fc 70%,#22d3ee)'
};
return 'background:' + (map[id] || map.ocean);
return 'background: ' + (map[id] || map.ocean) + ' !important;';
},
sidebarSwatch(id) {
var style = this.sidebarStyle || 'gradient';
var solids = {
aqua: '#0a4f5c',
azure: '#163a7a',
turquoise: '#0a453f',
cerulean: '#0c4a6e',
electric: '#0a3d4a',
violet: '#2a1f6b',
emerald: '#0a4a38',
midnight: '#0d1f3c',
steel: '#1a2d42',
nordic: '#0e3a4f',
};
var grads = {
aqua: 'linear-gradient(180deg, #062a32 0%, #0a4f5c 55%, #0d6b7a 100%)',
azure: 'linear-gradient(180deg, #0a1f45 0%, #163a7a 55%, #1e4f9e 100%)',
turquoise: 'linear-gradient(180deg, #052925 0%, #0a453f 55%, #0f5f56 100%)',
cerulean: 'linear-gradient(180deg, #06263a 0%, #0c4a6e 55%, #126087 100%)',
electric: 'linear-gradient(180deg, #041820 0%, #0a3d4a 50%, #0e5a6b 100%)',
violet: 'linear-gradient(180deg, #150e3a 0%, #2a1f6b 55%, #3b2f8a 100%)',
emerald: 'linear-gradient(180deg, #05291e 0%, #0a4a38 55%, #0f6349 100%)',
midnight: 'linear-gradient(180deg, #060e1c 0%, #0d1f3c 55%, #153056 100%)',
steel: 'linear-gradient(180deg, #0d1622 0%, #1a2d42 55%, #243b55 100%)',
nordic: 'linear-gradient(180deg, #071f2c 0%, #0e3a4f 55%, #15506a 100%)',
};
var bg = (style === 'plain')
? (solids[id] || solids.electric)
: (grads[id] || grads.electric);
return 'background: ' + bg + ' !important;';
},
bgSwatch(id) {
var map = {
navy: 'linear-gradient(135deg,#0d2b5c,#134078)',
royal: 'linear-gradient(135deg,#1a3a8f,#274bb8)',
azure: 'linear-gradient(135deg,#0a4f8a,#0d6cb8)',
sky: 'linear-gradient(135deg,#0e5f9e,#1880c9)',
cerulean: 'linear-gradient(135deg,#0c6a9e,#1490c8)',
teal: 'linear-gradient(135deg,#0c5c68,#128090)',
lagoon: 'linear-gradient(135deg,#0a6b7a,#0f8fa3)',
cyan: 'linear-gradient(135deg,#0a5568,#0e7a94)',
indigo: 'linear-gradient(135deg,#2f2a7a,#4338ca)',
violet: 'linear-gradient(135deg,#4a1d7a,#6d28d9)',
sapphire: 'linear-gradient(135deg,#123a8c,#1d4ed8)',
midnight: 'linear-gradient(135deg,#1a2f55,#274878)',
emerald: 'linear-gradient(135deg,#0d5c45,#059669)',
slate: 'linear-gradient(135deg,#243447,#334155)',
graphite: 'linear-gradient(135deg,#1e2a3a,#2c3b52)'
};
return 'background: ' + (map[id] || map.navy) + ' !important;';
}
};
}