{#
@System/layouts/main_layout.html.twig
Base layout for CoPilot module and AI cockpit views.
Usage: {% extends '@System/layouts/main_layout.html.twig' %}
Blocks: title, breadcrumb, body, page_actions
#}
{% set page_title = block('title') %}
{% include '@Application/codecovers_header.html.twig' %}
{# ── CoPilot design tokens (scoped, separate from legacy ERP tokens) ─────── #}
<style>
:root {
--cp-primary: #854f00;
--cp-primary-hover: #6d4000;
--cp-on-primary: #ffffff;
--cp-amber: #ffb86a;
--cp-amber-dim: rgba(133,79,0,.10);
--cp-surface: #f9f9f8;
--cp-surface-low: #f3f4f3;
--cp-surface-container:#eeeeed;
--cp-surface-high: #e8e8e7;
--cp-surface-white: #ffffff;
--cp-text: #1a1c1c;
--cp-text-muted: #524437;
--cp-text-faint: #847465;
--cp-outline: #847465;
--cp-outline-variant: #E5E5E5;
--cp-error: #ba1a1a;
--cp-success: #2e7d32;
--cp-warn: #f57c00;
--cp-sidebar-w: 200px;
--cp-topbar-h: 52px;
--cp-radius: 8px;
--cp-radius-lg: 12px;
--cp-shadow-card: 0 1px 6px rgba(26,28,28,.07), 0 0 0 1px rgba(133,79,0,.05);
--cp-font-head: 'Montserrat', 'DM Sans', system-ui, sans-serif;
--cp-font-body: 'DM Sans', system-ui, sans-serif;
--cp-font-mono: 'JetBrains Mono', 'Courier New', monospace;
/* Typography SIZE scale (--cp-fs-*) is defined globally in
@Application/codecovers_header.html.twig (included above, and by ~395
other templates) so every page shares one scale — not just cp-shell
pages. Use those tokens; never hard-code font-size or bare rem. */
}
/* ── Layout shell ─────────────────────────────────────────────────────── */
.cp-shell {
display: flex;
height: 100vh;
overflow: hidden;
background: var(--cp-surface);
font-family: var(--cp-font-body);
color: var(--cp-text);
}
/* ── Sidebar ───────────────────────────────────────────────────────────── */
.cp-sidebar {
width: var(--cp-sidebar-w);
min-width: var(--cp-sidebar-w);
background: var(--cp-surface-white);
border-right: 1px solid var(--cp-outline-variant);
display: flex;
flex-direction: column;
overflow-y: auto;
z-index: 100;
}
.cp-sidebar-brand {
padding: 16px 14px 10px;
border-bottom: 1px solid var(--cp-outline-variant);
}
.cp-sidebar-brand-name {
font-family: var(--cp-font-head);
font-size: var(--cp-fs-base, 13px);
font-weight: 700;
color: var(--cp-primary);
letter-spacing: -0.01em;
}
.cp-sidebar-context {
font-size: var(--cp-fs-xs, 11.5px);
font-weight: 500;
color: var(--cp-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-top: 2px;
}
.cp-sidebar-section {
padding: 14px 14px 6px;
}
.cp-sidebar-section-label {
font-family: var(--cp-font-mono);
font-size: 11px;
font-weight: 600;
color: var(--cp-text-faint);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 6px;
}
.cp-badge-active {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--cp-amber-dim);
color: var(--cp-primary);
font-family: var(--cp-font-mono);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 2px 7px;
border-radius: 99px;
border: 1px solid rgba(133,79,0,.18);
margin-top: 4px;
}
.cp-badge-active::before {
content: '';
width: 5px;
height: 5px;
border-radius: 50%;
background: #2e7d32;
display: inline-block;
}
.cp-nav {
list-style: none;
margin: 0;
padding: 0 8px;
}
.cp-nav li { margin: 1px 0; }
.cp-nav-link {
display: flex;
align-items: center;
gap: 9px;
padding: 8px 10px;
border-radius: var(--cp-radius);
font-size: var(--cp-fs-sm, 12.5px);
font-weight: 500;
color: var(--cp-text-muted);
text-decoration: none;
transition: background 0.12s, color 0.12s;
white-space: nowrap;
overflow: hidden;
}
.cp-nav-link:hover {
background: var(--cp-surface-low);
color: var(--cp-text);
text-decoration: none;
}
.cp-nav-link.active {
background: var(--cp-amber-dim);
color: var(--cp-primary);
font-weight: 600;
}
.cp-nav-link .cp-nav-icon {
font-size: 14px;
width: 16px;
text-align: center;
flex-shrink: 0;
}
.cp-nav-divider {
height: 1px;
background: var(--cp-outline-variant);
margin: 8px 10px;
}
.cp-nav-step-badge {
margin-left: auto;
font-family: var(--cp-font-mono);
font-size: 11px;
font-weight: 600;
color: var(--cp-text-faint);
}
.cp-sidebar-footer {
margin-top: auto;
padding: 10px 8px 12px;
border-top: 1px solid var(--cp-outline-variant);
}
/* ── Main area ─────────────────────────────────────────────────────────── */
.cp-main {
flex: 1 1 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Topbar ────────────────────────────────────────────────────────────── */
.cp-topbar {
height: var(--cp-topbar-h);
background: var(--cp-surface-white);
border-bottom: 1px solid var(--cp-outline-variant);
display: flex;
align-items: center;
gap: 0;
padding: 0;
flex-shrink: 0;
z-index: 90;
}
.cp-topbar-logo {
display: flex;
align-items: center;
gap: 7px;
padding: 0 16px 0 20px;
font-family: var(--cp-font-head);
font-size: var(--cp-fs-base, 13px);
font-weight: 700;
color: var(--cp-primary);
letter-spacing: -0.01em;
white-space: nowrap;
}
.cp-topbar-logo .cp-logo-mark {
width: 24px;
height: 24px;
background: var(--cp-primary);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: var(--cp-on-primary, #fff);
font-size: 12px;
}
/* WHICH COMPANY AM I IN — users on multi-company logins could not tell, because this
topbar hard-coded the product name and never showed the tenant. Reuses the SAME
session keys as framework/_cp_topstrip.html.twig; no second source of truth. */
.cp-topbar-company {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 11px;
margin-left: 2px;
border-radius: 999px;
border: 1px solid var(--cp-outline-variant);
background: var(--cp-surface-white);
font-family: var(--cp-font-body);
font-size: var(--cp-fs-sm);
font-weight: 700;
color: var(--cp-text);
white-space: nowrap;
max-width: 260px;
text-decoration: none;
}
.cp-topbar-company:hover { text-decoration: none; color: var(--cp-text); }
.cp-topbar-company .material-icons { font-size: var(--cp-fs-md, 15px); color: var(--cp-text-faint); }
.cp-topbar-company-nm { overflow: hidden; text-overflow: ellipsis; }
a.cp-topbar-company:hover { border-color: var(--cp-primary); }
@media (max-width: 720px) { .cp-topbar-company { max-width: 130px; } }
.cp-topbar-nav {
display: flex;
align-items: center;
gap: 0;
padding: 0 4px;
border-right: 1px solid var(--cp-outline-variant);
}
.cp-topbar-nav-item {
padding: 0 12px;
height: var(--cp-topbar-h);
display: flex;
align-items: center;
font-size: var(--cp-fs-sm, 12.5px);
font-weight: 500;
color: var(--cp-text-muted);
cursor: pointer;
border-bottom: 2px solid transparent;
text-decoration: none;
transition: color 0.1s, border-color 0.1s;
}
.cp-topbar-nav-item:hover { color: var(--cp-text); text-decoration: none; }
.cp-topbar-nav-item.active {
color: var(--cp-primary);
border-bottom-color: var(--cp-primary);
font-weight: 600;
}
.cp-topbar-ai-bar {
flex: 1 1 0;
padding: 0 16px;
}
.cp-topbar-ai-input {
width: 100%;
max-width: 480px;
height: 32px;
background: var(--cp-surface-low);
border: 1px solid var(--cp-outline-variant);
border-radius: 99px;
padding: 0 14px 0 34px;
font-size: 12px;
color: var(--cp-text);
font-family: var(--cp-font-body);
position: relative;
outline: none;
}
.cp-topbar-ai-wrap {
position: relative;
display: inline-flex;
align-items: center;
}
.cp-topbar-ai-wrap .cp-ai-icon {
position: absolute;
left: 10px;
font-size: var(--cp-fs-base, 13px);
color: var(--cp-primary);
pointer-events: none;
}
.cp-topbar-ai-input:focus {
background: var(--cp-surface-white);
border-color: var(--cp-primary);
box-shadow: 0 0 0 3px var(--cp-amber-dim);
}
.cp-topbar-bell { position: relative; text-decoration: none; }
.cp-topbar-bell:hover { text-decoration: none; }
.cp-topbar-bell-badge {
position: absolute; top: 1px; right: 0;
min-width: 15px; height: 15px; padding: 0 4px;
border-radius: 999px; background: var(--cp-error); color: var(--cp-on-primary, #fff);
font-family: var(--cp-font-mono); font-size: 9px; font-weight: 700;
line-height: 15px; text-align: center;
}
.cp-topbar-help { display: inline-block; }
.cp-topbar-help-menu { min-width: 190px; }
.cp-topbar-help-menu > li > a {
display: flex; align-items: center; gap: 9px;
padding: 8px 14px; font-size: var(--cp-fs-sm, 12.5px); color: var(--cp-text-muted);
}
.cp-topbar-help-menu > li > a:hover { background: var(--cp-surface-low); color: var(--cp-text); }
/* The report widget's FAB is redundant now it is in the Help menu — and it was pinned
bottom-left ON TOP of the sidebar footer's "Back to ERP". */
#si-fab { display: none !important; }
.cp-topbar-actions {
display: flex;
align-items: center;
gap: 4px;
padding: 0 14px;
}
.cp-topbar-icon-btn {
width: 32px;
height: 32px;
border-radius: var(--cp-radius);
border: none;
background: transparent;
color: var(--cp-text-muted);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: var(--cp-fs-md, 15px);
transition: background 0.1s, color 0.1s;
}
.cp-topbar-icon-btn:hover {
background: var(--cp-surface-low);
color: var(--cp-text);
}
.cp-start-anywhere-btn {
height: 30px;
padding: 0 12px;
background: var(--cp-primary);
color: var(--cp-on-primary);
border: none;
border-radius: var(--cp-radius);
font-size: 12px;
font-weight: 600;
font-family: var(--cp-font-head);
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: background 0.12s;
white-space: nowrap;
}
.cp-start-anywhere-btn:hover { background: var(--cp-primary-hover); }
/* ── Content area ──────────────────────────────────────────────────────── */
.cp-content {
flex: 1 1 0;
overflow-y: auto;
background: var(--cp-surface);
}
/* Render finance/central tool pages (those that swap in a custom sidebar) a bit larger. */
.cp-content.cp-content-zoom { zoom: 1.12; }
/* ── Page head ─────────────────────────────────────────────────────────── */
.cp-page-head {
padding: 22px 28px 16px;
background: var(--cp-surface-white);
border-bottom: 1px solid var(--cp-outline-variant);
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.cp-page-title {
font-family: var(--cp-font-head);
font-size: var(--cp-fs-xl, 20px);
font-weight: 700;
color: var(--cp-text);
letter-spacing: -0.02em;
margin: 0 0 3px;
}
.cp-page-subtitle {
font-size: var(--cp-fs-sm, 12.5px);
color: var(--cp-text-muted);
margin: 0;
}
.cp-page-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
/* ── Card ──────────────────────────────────────────────────────────────── */
.cp-card {
background: var(--cp-surface-white);
border-radius: var(--cp-radius-lg);
border: 1px solid var(--cp-outline-variant);
box-shadow: var(--cp-shadow-card);
transition: border-color 0.18s, box-shadow 0.18s, transform 0.18s;
}
.cp-card:hover {
box-shadow: 0 4px 14px rgba(0,0,0,0.07);
transform: translateY(-2px);
}
.cp-card-body { padding: 18px 20px; }
.cp-card-head {
padding: 14px 18px;
border-bottom: 1px solid var(--cp-surface-container);
font-size: var(--cp-fs-sm, 12.5px);
font-weight: 600;
color: var(--cp-text);
font-family: var(--cp-font-head);
}
/* ── Buttons ───────────────────────────────────────────────────────────── */
.cp-btn {
display: inline-flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 0 16px;
border-radius: var(--cp-radius);
border: 1px solid transparent;
font-size: var(--cp-fs-sm, 12.5px);
font-weight: 600;
font-family: var(--cp-font-head);
cursor: pointer;
text-decoration: none;
transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.cp-btn-primary {
background: var(--cp-primary);
color: var(--cp-on-primary);
border-color: var(--cp-primary);
}
.cp-btn-primary:hover {
background: var(--cp-primary-hover);
border-color: var(--cp-primary-hover);
color: var(--cp-on-primary, #fff);
text-decoration: none;
}
.cp-btn-outline {
background: transparent;
color: var(--cp-primary);
border-color: var(--cp-primary);
}
.cp-btn-outline:hover {
background: var(--cp-amber-dim);
text-decoration: none;
color: var(--cp-primary);
}
.cp-btn-ghost {
background: transparent;
color: var(--cp-text-muted);
border-color: var(--cp-outline-variant);
}
.cp-btn-ghost:hover {
background: var(--cp-surface-low);
color: var(--cp-text);
text-decoration: none;
}
/* ── Status chips ──────────────────────────────────────────────────────── */
.cp-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 9px;
border-radius: 99px;
font-family: var(--cp-font-mono);
font-size: var(--cp-fs-xs, 11.5px);
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.cp-chip-success { background: rgba(46,125,50,.10); color: #2e7d32; }
.cp-chip-warn { background: rgba(245,124,0,.10); color: #e65100; }
.cp-chip-error { background: rgba(186,26,26,.10); color: #ba1a1a; }
.cp-chip-info { background: rgba(133,79,0,.10); color: #854f00; }
.cp-chip-neutral { background: var(--cp-surface-container); color: var(--cp-text-muted); }
/* ── Metric widget ─────────────────────────────────────────────────────── */
.cp-metric-value {
font-family: var(--cp-font-head);
font-size: 28px;
font-weight: 700;
color: var(--cp-text);
letter-spacing: -0.03em;
line-height: 1;
}
.cp-metric-label {
font-family: var(--cp-font-mono);
font-size: var(--cp-fs-xs, 11.5px);
font-weight: 700;
color: var(--cp-text-faint);
text-transform: uppercase;
letter-spacing: 0.09em;
margin-top: 4px;
}
/* ── Score ring ────────────────────────────────────────────────────────── */
.cp-score-ring {
width: 64px;
height: 64px;
border-radius: 50%;
background: conic-gradient(var(--cp-primary) calc(var(--score) * 3.6deg), var(--cp-surface-high) 0deg);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.cp-score-ring::before {
content: '';
position: absolute;
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--cp-surface-white);
}
.cp-score-ring-value {
position: relative;
z-index: 1;
font-family: var(--cp-font-head);
font-size: var(--cp-fs-md, 15px);
font-weight: 700;
color: var(--cp-primary);
}
/* ── Responsive ────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
.cp-sidebar { width: 0; min-width: 0; overflow: hidden; position: absolute; z-index: 200; transition: width 0.2s; }
.cp-sidebar.open { width: var(--cp-sidebar-w); min-width: var(--cp-sidebar-w); }
.cp-topbar-nav { display: none; }
}
/* ── Global module rail (folds CoPilot/CRM onto the app-wide rail) ────────── */
/* Scoped to this shell so it borrows CoPilot's amber tokens and never collides
with the global cp-shell's own .cp-rail rules (different page, same names). */
.cp-shell > .cp-rail { width: 60px; flex-shrink: 0; background: var(--cp-surface-low);
border-right: 1px solid var(--cp-outline-variant); display: flex; flex-direction: column;
align-items: center; padding: 10px 0; gap: 3px; overflow-y: auto; z-index: 101; }
.cp-shell > .cp-rail .cp-rail-item { width: 46px; padding: 8px 0 6px; border-radius: 10px;
display: flex; flex-direction: column; align-items: center; gap: 3px;
color: var(--cp-text-faint); text-decoration: none; transition: background .12s, color .12s; }
.cp-shell > .cp-rail .cp-rail-item:hover { background: var(--cp-surface-white); color: var(--cp-text); text-decoration: none; }
.cp-shell > .cp-rail .cp-rail-item.active { background: var(--cp-amber-dim); color: var(--cp-primary); }
.cp-shell > .cp-rail .cp-rail-item .material-icons { font-size: var(--cp-fs-xl, 20px); }
.cp-shell > .cp-rail .cp-rail-lbl { font-size: var(--cp-fs-micro, 10px); font-weight: 600; text-align: center; line-height: 1.05; }
@media (max-width: 768px) { .cp-shell > .cp-rail { display: none; } }
</style>
{# Montserrat + JetBrains Mono (DM Sans already loaded by codecovers_header) #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<div class="cp-shell">
{# ── Global module rail — folds CoPilot/CRM into the app-wide navigation.
CoPilot lives under the Projects area, so highlight that unless a page overrides. #}
{% set active_module = active_module|default('projects') %}
{% include '@Application/framework/_cp_global_rail.html.twig' %}
{# ── Left sidebar (swappable; defaults to the CoPilot rail) ──────────────── #}
{% include sidebar_partial|default('@System/inc/_copilot_sidebar.html.twig') %}
{# ── Main column ───────────────────────────────────────────────────────── #}
<div class="cp-main">
{% include '@Application/inc/_support_session_banner.html.twig' %}
{# ── Topbar ──────────────────────────────────────────────────────────── #}
<div class="cp-topbar">
<div class="cp-topbar-logo">
<span class="cp-logo-mark"><i class="zmdi zmdi-hive"></i></span>
HoneyBee ERP
</div>
{# ── WHICH COMPANY AM I IN ────────────────────────────────────────────
Users on multi-company logins reported not being able to tell which
company they were working in: this topbar showed only the product
name. The company switcher already existed in
framework/_cp_topstrip.html.twig, but that partial is NOT included by
this layout — so every page built on the cp-shell was anonymous.
Same session keys, so the two can never disagree. Shown ALWAYS (the
topstrip only shows it when >1, which is exactly when a single-company
user still needs to know which tenant they are in); becomes a switcher
only when there really is something to switch to. #}
{% set _cpCompanyNames = session[UserConstants.USER_COMPANY_NAME_LIST]|default('[]')|jsonDecode %}
{% set _cpCompanyId = session[UserConstants.USER_COMPANY_ID]|default(0) %}
{% set _cpCompanyName = _cpCompanyNames[_cpCompanyId]|default('') %}
{% if _cpCompanyName is not empty %}
{% if _cpCompanyNames|length > 1 %}
<div class="dropdown" style="display:inline-block;">
<a href="#" data-toggle="dropdown" class="cp-topbar-company"
title="You are working in {{ _cpCompanyName }} — click to switch company">
<i class="material-icons">business</i>
<span class="cp-topbar-company-nm">{{ _cpCompanyName }}</span>
<i class="material-icons">arrow_drop_down</i>
</a>
<ul class="dropdown-menu">
<li class="dropdown-header">Switch company</li>
{% for _cid, _cname in _cpCompanyNames %}
<li><a href="{{ url('change_company_dashboard') }}/{{ _cid }}">{{ _cname }}</a></li>
{% endfor %}
</ul>
</div>
{% else %}
<span class="cp-topbar-company" title="You are working in {{ _cpCompanyName }}">
<i class="material-icons">business</i>
<span class="cp-topbar-company-nm">{{ _cpCompanyName }}</span>
</span>
{% endif %}
{% endif %}
<nav class="cp-topbar-nav">
<a href="{{ path('dashboard') }}" class="cp-topbar-nav-item">Workspaces</a>
<a href="{{ path('copilot_dashboard') }}" class="cp-topbar-nav-item active">Stages</a>
<a href="{{ path('system_admin_user_list') }}" class="cp-topbar-nav-item">Roles</a>
</nav>
<div class="cp-topbar-ai-bar">
<div class="cp-topbar-ai-wrap">
<span class="cp-ai-icon zmdi zmdi-flare"></span>
<input type="text" class="cp-topbar-ai-input" id="cp-ai-command"
placeholder="Ask HoneyBee AI to check, fix, explain…">
</div>
</div>
<div class="cp-topbar-actions">
{# NOTICES — this bell was a <button> with no href and no handler: the notices page
existed at /notification and nothing in the new shell linked to it, which is why
"the notices board is not in the main layout". Now a real link with a live unread
count from /api/notifications/unread_count. #}
<a href="{{ path('my_notification') }}" class="cp-topbar-icon-btn cp-topbar-bell" title="Notices">
<i class="zmdi zmdi-notifications-none"></i>
<span class="cp-topbar-bell-badge" id="cpTopbarNoticeCount" hidden>0</span>
</a>
{# HELP — was also a dead button. Now a dropdown, and it is where "Report an issue"
lives: the widget's own floating FAB is pinned bottom-left over the sidebar
footer, which is both easy to miss and covers "Back to ERP". #}
<div class="dropdown cp-topbar-help">
<button class="cp-topbar-icon-btn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Help">
<i class="zmdi zmdi-help-outline"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right cp-topbar-help-menu">
{# WHAT'S NEW — /changelog/ was reachable only through the Administration nav, so the
people who asked for it (users, and developers who are not admins) could not find
it. Same cp_can_access gate as every other link; module 468 is eFA=1. #}
{% if cp_can_access('change_log') %}
<li><a href="{{ path('change_log') }}"><i class="zmdi zmdi-collection-text"></i> What's new</a></li>
{% endif %}
<li><a href="#" onclick="if(window.siOpen){siOpen();}return false;"><i class="zmdi zmdi-bug"></i> Report an issue</a></li>
<li><a href="{{ path('my_notification') }}"><i class="zmdi zmdi-notifications-none"></i> Notices</a></li>
</ul>
</div>
<button class="cp-start-anywhere-btn" data-toggle="modal" data-target="#startAnywhereModal">
<i class="zmdi zmdi-plus"></i> New Entry
</button>
</div>
</div>
{# ── Page content ────────────────────────────────────────────────────── #}
<div class="cp-content{{ sidebar_partial is defined ? ' cp-content-zoom' : '' }}">
{% block body %}{% endblock %}
</div>
</div>{# /.cp-main #}
</div>{# /.cp-shell #}
{# SENT-HUB — "Report an issue" widget for every logged-in user (testers). Server-side session-gated. #}
{% if app.session is defined and app.session.get('userId')|default(0) %}
{% include '@AgentOS/sentinel/_report_issue_widget.html.twig' %}
{% endif %}
{# ── Start Anywhere modal ─────────────────────────────────────────────────── #}
{% include '@System/inc/_start_anywhere_modal.html.twig' %}
<script>
/* Unread notice count for the topbar bell. Fails silently: a decorative badge must never
surface an error on every page, and a stale/absent count is not worth a console full of
noise. Polls modestly rather than on a tight timer. */
(function () {
var el = document.getElementById('cpTopbarNoticeCount');
if (!el || !window.fetch) { return; }
function refresh() {
fetch('{{ path('api_notifications_unread_count') }}', {credentials: 'same-origin'})
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var n = d && d.count ? parseInt(d.count, 10) : 0;
if (n > 0) { el.textContent = n > 99 ? '99+' : n; el.hidden = false; }
else { el.hidden = true; }
})
.catch(function () { /* silent by design */ });
}
refresh();
setInterval(refresh, 120000);
})();
</script>
{% include '@Application/footer/footer_js_codecovers.html.twig' %}
{# Bootstrap Modal JS (already have jQuery from header) #}
{#<script src="{{ absolute_url(path('dashboard')) }}condensed_assets/javascript_codecovers_minimal.js?version={{ constant('ApplicationBundle\\Constants\\GeneralConstant::ENTITY_APP_VERSION') }}"></script>#}
<script>
(function () {
'use strict';
var aiInput = document.getElementById('cp-ai-command');
if (aiInput) {
aiInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && this.value.trim()) {
var q = encodeURIComponent(this.value.trim());
window.location.href = '/ai/insights/feed?q=' + q;
}
});
}
var mobileToggle = document.getElementById('cp-mobile-sidebar-toggle');
var sidebar = document.querySelector('.cp-sidebar');
if (mobileToggle && sidebar) {
mobileToggle.addEventListener('click', function () {
sidebar.classList.toggle('open');
});
}
}());
</script>
{% include '@Application/inc/menu/_spotlight_search.html.twig' ignore missing %}
{% block page_scripts %}{% endblock %}