2026-06-09 00:41:27 +00:00
<!DOCTYPE html>
< html lang = "nl" >
< head >
< meta charset = "UTF-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=5" />
< meta name = "theme-color" content = "#0a0e14" />
< meta name = "mobile-web-app-capable" content = "yes" />
< meta name = "apple-mobile-web-app-capable" content = "yes" />
< meta name = "apple-mobile-web-app-status-bar-style" content = "black-translucent" />
< meta name = "apple-mobile-web-app-title" content = "Foodlinkk" />
2026-06-23 10:04:23 +00:00
< title > {% block title %}{{ page_title }} · Cucina{% endblock %}</ title >
< link rel = "manifest" href = "/static/manifest.json?v=2" />
2026-06-09 00:41:27 +00:00
< link rel = "icon" href = "/static/icons/app-icon.svg" type = "image/svg+xml" />
< link rel = "apple-touch-icon" href = "/static/icons/app-icon.svg" />
2026-06-23 10:04:23 +00:00
< link rel = "stylesheet" href = "/static/css/palantir-theme.css?v=2" />
< 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=8" />
< link rel = "stylesheet" href = "/static/css/mobile.css?v=5" />
< link rel = "stylesheet" href = "/static/css/herman-dashboard.css?v=18" />
< script defer src = "/static/js/alpine.min.js?v=2" ></ script >
< script defer src = "/static/js/htmx.min.js?v=2" ></ script >
2026-06-09 00:41:27 +00:00
{% block extra_head %}{% endblock %}
2026-06-23 10:04:23 +00:00
< link rel = "stylesheet" href = "/static/css/app-sidebar-neo.css?v=6" />
< link rel = "stylesheet" href = "/static/css/herman-assistant.css?v=1" />
2026-06-09 00:41:27 +00:00
</ head >
2026-06-23 10:04:23 +00:00
< body x-data = "appShell()" x-init = "initApp()" :class = "{ 'nav-open': navOpen, 'drawer-open': drawerOpen, 'brand-cucina': brand === 'cucina', 'brand-foodlinkk': brand === 'foodlinkk' }" >
2026-06-09 00:41:27 +00:00
< div class = "app-shell" >
< aside class = "app-sidebar" @ click = "if($event.target.closest('a')) navOpen=false" >
< div class = "app-sidebar-brand" >
2026-06-23 10:04:23 +00:00
< p class = "brand-title" > Cucina · Foodlinkk</ p >
< p class = "brand-name" x-text = "brand === 'foodlinkk' ? 'Foodlinkk B2B' : 'Cucina B2C'" > Cucina B2C</ p >
2026-06-09 00:41:27 +00:00
</ div >
2026-06-23 10:04:23 +00:00
< div class = "persona-switch brand-switch" role = "tablist" aria-label = "Brand" >
< button type = "button" role = "tab" class = "persona-btn brand-btn-cucina"
:class = "{ active: brand === 'cucina' }" @ click = "setBrand('cucina')"
:aria-selected = "brand === 'cucina'" > Cucina · B2C</ button >
< button type = "button" role = "tab" class = "persona-btn brand-btn-foodlinkk"
:class = "{ active: brand === 'foodlinkk' }" @ click = "setBrand('foodlinkk')"
:aria-selected = "brand === 'foodlinkk'" > Foodlinkk · B2B</ button >
2026-06-09 00:41:27 +00:00
</ div >
2026-06-23 10:04:23 +00:00
< 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" 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" 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" 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" 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" 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 = "/herman" class = "nav-pill nav-pill-chat {% if request.url.path.startswith('/herman') %}active{% endif %}" >< span class = "sidebar-nav-icon" > 💬</ span >< span class = "sidebar-nav-label" data-i18n = "nav.chat" > Herman</ span ></ a >
< a href = "/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 >
</ div >
2026-06-09 00:41:27 +00:00
2026-06-23 10:04:23 +00:00
< div class = "app-sidebar-group" x-show = "brand === 'cucina'" >
< div class = "app-sidebar-label" > Cucina B2C</ div >
< a href = "/" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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 >
</ div >
2026-06-09 00:41:27 +00:00
2026-06-23 10:04:23 +00:00
< div class = "app-sidebar-group" >
< div class = "app-sidebar-label" data-i18n = "nav.platform" > Platform</ div >
< a href = "/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" 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" 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" 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" 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" 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 >
</ div >
2026-06-09 00:41:27 +00:00
</ div >
</ aside >
< div class = "mobile-nav-overlay" x-show = "navOpen" @ click = "navOpen=false" x-cloak ></ div >
< div class = "app-main" >
< header class = "app-topbar" >
< button type = "button" class = "mobile-menu-btn" @ click = "navOpen = !navOpen" aria-label = "Menu openen" > ☰</ button >
2026-06-23 10:04:23 +00:00
< span class = "page-tip" >
< span class = "persona-badge" :class = "brand === 'foodlinkk' ? 'brand-badge-foodlinkk' : 'brand-badge-cucina'" x-text = "brand === 'foodlinkk' ? 'B2B' : 'B2C'" ></ span >
Cucina · Foodlinkk
</ span >
2026-06-09 10:41:13 +00:00
< select class = "form-input" style = "width:auto;min-width:110px;padding:0.2rem 0.5rem;font-size:0.72rem;margin:0 0.5rem"
x-model = "locale" @ change = "setLocale(locale)" :title = "$t ? $t('common.language') : 'Taal'" >
< option value = "nl" > NL</ option >
< option value = "en" > EN</ option >
</ select >
2026-06-09 00:41:27 +00:00
< div class = "app-clock" >< strong x-text = "clockDate" > —</ strong > · < span x-text = "clockTime" > —</ span ></ div >
</ header >
< main class = "main-topnav" > {% block content %}{% endblock %}</ main >
</ div >
</ div >
< div class = "pwa-install-bar" x-show = "showInstall" x-cloak >
< p > 📱 Installeer Foodlinkk als app op je telefoon</ p >
< button type = "button" class = "btn btn-sm btn-pulse-green" @ click = "installPwa()" > Installeren</ button >
< button type = "button" class = "btn btn-sm" @ click = "showInstall=false" > Later</ button >
</ div >
2026-06-23 10:04:23 +00:00
<!-- Herman browser assistant (elke pagina) -->
< div x-data = "hermanBrowserWidget()" x-init = "initWidget()" x-show = "!hidden" x-cloak >
< button type = "button" class = "ha-fab" :class = "{ 'is-open': open }" @ click = "toggleOpen()" title = "Praat met Herman" aria-label = "Herman assistant" > 💬</ button >
< div class = "ha-panel" x-show = "open" x-transition @ click . outside = "open = false" >
< header class = "ha-head" >
< h2 > Herman < span style = "font-size:0.65rem;color:#a78bfa" > browser</ span ></ h2 >
< div class = "ha-head-actions" >
< a href = "/voice" > Voice live</ a >
< button type = "button" @ click = "open = false" aria-label = "Sluiten" > × </ button >
</ div >
</ header >
< div class = "ha-confirm" x-show = "pendingAction" >
< span x-text = "pendingAction ? (pendingAction.type === 'webbuilder_build' ? ('Agy website: ' + (pendingAction.project || '')) : ('Zoeken: ' + (pendingAction.entity_label || '') + ' in ' + (pendingAction.location_label || ''))) : ''" ></ span >
< div class = "ha-confirm-actions" >
< button type = "button" class = "btn btn-sm btn-pulse" @ click = "confirmPending()" :disabled = "busy" > Ja</ button >
< button type = "button" class = "btn btn-sm" @ click = "cancelPending()" :disabled = "busy" > Nee</ button >
</ div >
</ div >
< div class = "ha-log" x-ref = "chatLog" >
< template x-for = "(t, i) in turns" :key = "i" >
< div class = "ha-bubble" :class = "t.role === 'user' ? 'ha-bubble-user' : 'ha-bubble-agent'" >
< div class = "ha-bubble-meta" x-text = "t.at + (t.role === 'user' ? ' · jij' : ' · Herman')" ></ div >
< p x-text = "t.text" ></ p >
</ div >
</ template >
< p x-show = "!turns.length" class = "page-tip" style = "font-size:0.75rem;margin:0" > Typ of spreek — bv. “zoek distributeurs in Dubai”</ p >
</ div >
< div class = "ha-preview" :class = "{ 'is-interim': liveInterim }" x-show = "livePreview || recording" x-text = "livePreview || 'Luisteren…'" ></ div >
< p class = "ha-status" x-show = "statusDetail" x-text = "statusDetail" ></ p >
< div class = "ha-input-row" >
< button type = "button" class = "ha-mic" :class = "micClass()"
@ mousedown . prevent = "onMicDown()" @ mouseup . prevent = "onMicUp()"
@ touchstart . prevent = "onMicDown()" @ touchend . prevent = "onMicUp()"
@ click . prevent = "onMicClick()" title = "Microfoon" > 🎙️</ button >
< input class = "form-input" type = "text" placeholder = "Vraag aan Herman…" x-model = "manualText"
@ keydown . enter . prevent = "sendText()" :disabled = "busy" />
< button type = "button" class = "btn btn-sm btn-pulse" @ click = "sendText()" :disabled = "busy || !manualText.trim()" > →</ button >
</ div >
</ div >
< div class = "ei-modal-root ha-results-modal" x-show = "resultsOpen" @ click . self = "closeResultsModal()" >
< div class = "ei-modal-card ha-results-card" @ click . stop >
< h3 x-text = "resultsTitle" ></ h3 >
< p >< strong x-text = "resultsTotal" ></ strong > resultaten</ p >
< div class = "voice-results-table-wrap" style = "max-height:40vh;overflow:auto" >
< table class = "voice-results-table" style = "width:100%;font-size:0.78rem" >
< thead >< tr >< th > Naam</ th >< th > Type</ th >< th > Stad</ th ></ tr ></ thead >
< tbody >
< template x-for = "(e, i) in resultsEntities" :key = "i" >
< tr >
< td >< a :href = "'/export-intel?open=' + e.id" x-text = "e.name" ></ a ></ td >
< td x-text = "entityTypeLabel(e.entity_type)" ></ td >
< td x-text = "e.city || '—'" ></ td >
</ tr >
</ template >
</ tbody >
</ table >
</ div >
< div class = "ei-modal-actions" >
< a class = "btn btn-sm" :href = "resultsOpenUrl" > Wereldexport</ a >
< button type = "button" class = "btn btn-sm" @ click = "closeResultsModal()" > Sluiten</ button >
</ div >
</ div >
</ div >
< div class = "ei-modal-root ha-results-modal" x-show = "webbuilderOpen" @ click . self = "closeWebbuilderModal()" >
< div class = "ei-modal-card ha-results-card" @ click . stop >
< h3 x-text = "webbuilderTitle" ></ h3 >
< p >< strong x-text = "webbuilderProject" ></ strong > · Agy / Antigravity op Hermes</ p >
< p class = "page-tip" style = "font-size:0.78rem" > Preview na afloop: < a :href = "webbuilderPreview" target = "_blank" rel = "noopener" x-text = "webbuilderPreview" ></ a ></ p >
< p class = "page-tip" x-show = "webbuilderNas" style = "font-size:0.72rem" x-text = "'NAS: ' + webbuilderNas" ></ p >
< div class = "ei-modal-actions" >
< a class = "btn btn-sm btn-pulse" :href = "webbuilderAgentsUrl" > Agents terminal</ a >
< a class = "btn btn-sm" :href = "webbuilderPreview" target = "_blank" rel = "noopener" > Preview</ a >
< button type = "button" class = "btn btn-sm" @ click = "closeWebbuilderModal()" > Sluiten</ button >
</ div >
</ div >
</ div >
</ div >
< script defer src = "/static/js/herman-assistant.js?v=2" ></ script >
< script src = "/static/js/i18n.js?v=5" ></ script >
2026-06-09 10:41:13 +00:00
< script src = "/static/js/cockpit.js?v=2" ></ script >
< script src = "/static/js/viz-engine.js?v=3" ></ script >
< script src = "/static/js/global-viz.js?v=1" ></ script >
2026-07-19 18:25:01 +00:00
< script src = "/static/js/live-pulse.js?v=3" ></ script >
2026-06-09 00:41:27 +00:00
< script >
2026-06-23 10:04:23 +00:00
window . FoodlinkkBrand = ( function () {
var FOODLINKK_PREFIXES = [ '/export-intel' , '/foodlinkk' , '/clients' , '/deals' , '/products' , '/suppliers' ];
var CUCINA_PREFIXES = [ '/retail' , '/marketing' , '/packaging' , '/revenue-cockpit' , '/beurs' ];
function isFoodlinkkPath ( path ) {
return FOODLINKK_PREFIXES . some ( function ( p ) { return path === p || path . indexOf ( p + '/' ) === 0 ; });
}
function isCucinaPath ( path ) {
return path === '/' || CUCINA_PREFIXES . some ( function ( p ) { return path === p || path . indexOf ( p + '/' ) === 0 ; });
}
function detect ( path ) {
var stored = localStorage . getItem ( 'foodlinkk-brand' );
if ( stored === 'cucina' || stored === 'foodlinkk' ) return stored ;
if ( isFoodlinkkPath ( path )) return 'foodlinkk' ;
return 'cucina' ;
}
return { isFoodlinkkPath : isFoodlinkkPath , isCucinaPath : isCucinaPath , detect : detect };
})();
/* legacy persona helper → brand */
window . FoodlinkkPersona = window . FoodlinkkBrand ;
2026-06-09 00:41:27 +00:00
function appShell () {
return {
2026-06-09 10:41:13 +00:00
locale : 'nl' ,
2026-06-23 10:04:23 +00:00
brand : 'cucina' ,
2026-06-09 00:41:27 +00:00
drawerOpen : false ,
navOpen : false ,
showInstall : false ,
_deferredPrompt : null ,
clockDate : '—' ,
clockTime : '—' ,
_offset : 0 ,
2026-06-09 10:41:13 +00:00
async initApp () {
2026-06-23 10:04:23 +00:00
this . brand = window . FoodlinkkBrand . detect ( location . pathname );
if ( ! localStorage . getItem ( 'flk-cache-v3' )) {
if ( 'serviceWorker' in navigator ) {
const regs = await navigator . serviceWorker . getRegistrations ();
await Promise . all ( regs . map (( r ) => r . unregister ()));
}
if ( window . caches ) {
const keys = await caches . keys ();
await Promise . all ( keys . map (( k ) => caches . delete ( k )));
}
localStorage . setItem ( 'flk-cache-v3' , '1' );
}
2026-06-09 10:41:13 +00:00
if ( window . I18n ) await window . I18n . init ();
this . locale = window . I18n ? window . I18n . locale () : 'nl' ;
2026-06-09 00:41:27 +00:00
this . initClock ();
this . initPwa ();
},
2026-06-23 10:04:23 +00:00
setBrand ( b ) {
if ( b === this . brand ) return ;
this . brand = b ;
localStorage . setItem ( 'foodlinkk-brand' , b );
var path = location . pathname ;
if ( b === 'foodlinkk' && window . FoodlinkkBrand . isCucinaPath ( path ) && ! window . FoodlinkkBrand . isFoodlinkkPath ( path )) {
location . href = '/export-intel' ;
return ;
}
if ( b === 'cucina' && window . FoodlinkkBrand . isFoodlinkkPath ( path )) {
location . href = '/' ;
return ;
}
},
2026-06-09 10:41:13 +00:00
async setLocale ( loc ) {
if ( window . I18n ) await window . I18n . setLocale ( loc );
this . locale = loc ;
},
2026-06-09 00:41:27 +00:00
async initClock () {
try {
const r = await fetch ( '/api/server-time' ). then ( x => x . json ());
this . _offset = new Date ( r . utc ). getTime () - Date . now ();
} catch ( e ) { this . _offset = 0 ; }
const tick = () => {
const d = new Date ( Date . now () + this . _offset );
this . clockDate = d . toLocaleDateString ( 'nl-NL' , { timeZone : 'Europe/Amsterdam' , weekday : 'short' , day : 'numeric' , month : 'short' });
this . clockTime = d . toLocaleTimeString ( 'nl-NL' , { timeZone : 'Europe/Amsterdam' , hour : '2-digit' , minute : '2-digit' , second : '2-digit' });
};
tick ();
setInterval ( tick , 1000 );
},
initPwa () {
if ( window . matchMedia ( '(display-mode: standalone)' ). matches ) return ;
if ( /Android|iPhone|iPad|iPod/i . test ( navigator . userAgent )) {
this . showInstall = ! localStorage . getItem ( 'pwa-dismiss' );
}
window . addEventListener ( 'beforeinstallprompt' , ( e ) => {
e . preventDefault ();
this . _deferredPrompt = e ;
this . showInstall = true ;
});
},
async installPwa () {
if ( this . _deferredPrompt ) {
this . _deferredPrompt . prompt ();
await this . _deferredPrompt . userChoice ;
this . _deferredPrompt = null ;
this . showInstall = false ;
return ;
}
if ( /iPhone|iPad|iPod/i . test ( navigator . userAgent )) {
alert ( 'Tik op Deel ↗ en kies «Zet op beginscherm» om Foodlinkk als app te installeren.' );
} else {
alert ( 'Tik op ⋮ menu → «Toevoegen aan startscherm» of «Installeren».' );
}
this . showInstall = false ;
localStorage . setItem ( 'pwa-dismiss' , '1' );
}
};
}
</ script >
2026-06-23 10:04:23 +00:00
< script >
window . addEventListener ( 'load' , function () {
setTimeout ( function () {
if ( window . Alpine ) return ;
document . querySelectorAll ( '[x-cloak]' ). forEach ( function ( el ) { el . removeAttribute ( 'x-cloak' ); });
document . body . classList . add ( 'alpine-missing' );
console . error ( 'Alpine.js niet geladen — interactieve UI beperkt. Hard refresh (Ctrl+Shift+R).' );
}, 2500 );
});
</ script >
2026-06-09 00:41:27 +00:00
{% block scripts %}{% endblock %}
</ body >
</ html >