Ctrl K
ComunidadGuías…

templates/index.html

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
<!DOCTYPE html>
<html lang="{{ html_lang }}" dir="{{ html_dir }}" class="dark">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Traefik Manager</title>
    <meta name="csrf-token" content="{{ csrf_token }}">
    <meta name="tm-base-path" content="{{ base_path }}">
    <link rel="icon" type="image/png" sizes="192x192" href="{{ base_path }}/static/icons/icon.png">
    <link rel="apple-touch-icon" href="{{ base_path }}/static/icons/icon-192x192.png">
    <link rel="manifest" href="{{ base_path }}/static/manifest.json">
    <meta name="theme-color" content="#0d1117">
    <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="Traefik Manager">
    <script type="application/json" id="i18n-catalog">{{ i18n_catalog | tojson }}</script>
    <script src="{{ base_path }}/static/js/i18n.js?v={{ asset_version }}"></script>
    <script>
        window.TM_DEFAULT_THEME = '{{ default_theme }}' || 'dark';
        window.TM_UI_PREFS = {{ ui_prefs | tojson }};
        window.TM_LANGUAGE = {{ language_setting | tojson }};
        window.TM_LANGUAGE_TAGS = {{ language_options | map(attribute='tag') | list | tojson }};
        (function() {
            var server = window.TM_UI_PREFS || {};
            var t = localStorage.getItem('tm-theme') || window.TM_DEFAULT_THEME;
            var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
            var isDark = t === 'dark' || (t === 'system' && prefersDark);
            document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
            document.documentElement.setAttribute('data-tm-pref', t);

            function pref(key, dflt) {
                if (Object.prototype.hasOwnProperty.call(server, key)) return server[key];
                var v = localStorage.getItem(key);
                return v === null ? dflt : v !== 'false';
            }
            var d = document.documentElement.classList;
            if (!pref('showStatCards', true))    d.add('tm-hide-stats');
            if (pref('compactStatCards', false)) d.add('tm-compact-stats');
            if (!pref('showEntrypoints', true))  d.add('tm-hide-entrypoints');
            if (!pref('showDocsLink', true))     d.add('tm-hide-docs');
            if (!pref('showApiLink', false))     d.add('tm-hide-api');
            if (!pref('showShortcutsBtn', true)) d.add('tm-hide-shortcuts');
            if (!pref('showIpDiagBtn', true))    d.add('tm-hide-ipdiag');
            if (!pref('showLangPicker', true))   d.add('tm-hide-lang');
            var _lm = pref('layoutMode', 'fluid');
            if (_lm === 'classic') _lm = 'fixed';
            if (_lm === 'fixed') d.add('tm-fixed');
            if (localStorage.getItem('tm-nav-collapsed') === '1') d.add('tm-nav-collapsed');
            var sbs = pref('statBarScope', 'all');
            var sbSet = (sbs === 'all' || sbs === true || sbs === undefined || sbs === null || sbs === '')
                ? ['dashboard', 'services', 'middlewares', 'live']
                : (sbs === 'none' ? [] : String(sbs).split(','));
            if (sbSet.indexOf('services') !== -1) d.add('tm-stats-here');
        })();
    </script>
    <style>
        html.tm-hide-stats #statsPanel { display: none !important; }
        html.tm-hide-stats.tm-hide-entrypoints #overviewSection { display: none !important; }
        html.tm-hide-entrypoints #entrypointsBar { display: none !important; }
        html:not(.tm-stats-here) #overviewSection { display: none !important; }
        html:not(.tm-stats-here) #entrypointsBar { display: none !important; }
        html.tm-hide-docs .nav-docs-link { display: none !important; }
        html.tm-hide-api .nav-api-link { display: none !important; }
        html.tm-hide-shortcuts .shortcuts-btn-wrap { display: none !important; }
        html.tm-hide-ipdiag .nav-ipdiag-btn { display: none !important; }
        html.tm-hide-lang .nav-lang-picker { display: none !important; }
        .tm-theme-icon { display: none; }
        html[data-tm-pref="dark"] .tm-theme-icon[data-pref="dark"],
        html[data-tm-pref="light"] .tm-theme-icon[data-pref="light"],
        html[data-tm-pref="system"] .tm-theme-icon[data-pref="system"],
        html:not([data-tm-pref]) .tm-theme-icon[data-pref="dark"] { display: inline; }
        .tab-optional { display: none; }
        .tm-geo-map { width: 100%; min-width: 0; overflow: hidden; }
        .tm-geo-map svg { width: 100%; height: auto; display: block; max-width: 100%; }
        .tm-geo-grid { display: grid; gap: 16px; grid-template-columns: minmax(0,2fr) minmax(0,1fr); align-items: start; }
        @media (max-width: 640px) { .tm-geo-grid { grid-template-columns: 1fr; } }
        .tm-worldmap { width: 100%; height: auto; display: block; }
        .tm-worldmap path { fill: var(--input-bg); stroke: var(--card); stroke-width: 0.3; }
        [data-theme="light"] .tm-worldmap path { fill: #d8dee4; }
        .tm-worldmap path.tm-geo-active { cursor: pointer; }
        .tm-worldmap path.tm-geo-active:hover { stroke: var(--text); stroke-width: 0.6; }
        .tm-worldmap path.tm-geo-selected { stroke: var(--text); stroke-width: 0.8; }
        .tm-geo-tooltip { position: fixed; z-index: 400; pointer-events: none; background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; font-size: 12px; color: var(--text); box-shadow: 0 2px 10px rgba(0,0,0,0.3); white-space: nowrap; }
    </style>
    <link rel="preload" href="{{ base_path }}/static/vendor/phosphor/Phosphor-Bold.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="{{ base_path }}/static/vendor/phosphor/Phosphor-Fill.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="{{ base_path }}/static/vendor/phosphor/Phosphor-Light.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="stylesheet" href="{{ base_path }}/static/vendor/fonts/inter.css">
    <link rel="stylesheet" href="{{ base_path }}/static/vendor/fonts/jetbrains-mono.css">
    <link rel="stylesheet" href="{{ base_path }}/static/vendor/phosphor.css">
    <link rel="stylesheet" href="{{ base_path }}/static/css/app.css?v={{ asset_version }}">
    <link rel="stylesheet" href="{{ base_path }}/static/css/tailwind.css">
    <script src="{{ base_path }}/static/vendor/qrcode.min.js"></script>
</head>
<body class="min-h-screen font-sans">

{% if no_auth %}
<div id="noAuthBanner" style="background:var(--red);color:#fff;text-align:center;padding:8px 16px;font-size:13px;font-weight:600;position:sticky;top:0;z-index:99997;">
    <i class="ph-bold ph-warning" style="margin-right:6px"></i>
    {{ _('No authentication is active - this interface is publicly accessible. Enable a password or OIDC in Settings - Authentication.') }}
</div>
{% endif %}

<div id="customConfirmOverlay" style="display:none;position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,0.5);backdrop-filter:blur(3px);align-items:center;justify-content:center;">
    <div style="max-width:440px;width:90%;background:var(--card);border:1px solid var(--border);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,0.4);">
        <div style="display:flex;align-items:center;gap:10px;padding:18px 22px 0">
            <i id="customConfirmIcon" class="ph-bold ph-warning-circle" style="display:none;font-size:18px;color:var(--red);flex-shrink:0"></i>
            <p class="font-semibold text-sm" style="color:var(--text);margin:0" id="customConfirmTitle"></p>
        </div>
        <div style="padding:10px 22px 0">
            <p class="text-sm" style="color:var(--muted);line-height:1.6;margin:0" id="customConfirmMsg"></p>
        </div>
        <div id="customConfirmNotes" style="display:none;flex-direction:column;gap:8px;padding:14px 22px 0"></div>
        <div id="customConfirmCheckWrap" style="display:none;padding:14px 22px 0">
            <label class="confirm-opt">
                <input type="checkbox" id="customConfirmCheck" style="accent-color:var(--red);margin-top:1px;flex-shrink:0;width:15px;height:15px;cursor:pointer">
                <span id="customConfirmCheckLabel"></span>
            </label>
        </div>
        <div id="customConfirmTypeWrap" style="display:none;padding:16px 22px 0">
            <label class="text-xs block mb-1.5" style="color:var(--muted)">{% set confirm_word %}<button type="button" id="customConfirmWord" title="{{ _('Click to copy') }}" style="color:var(--red);font-weight:700;background:none;border:none;padding:0 2px;cursor:copy;font-size:inherit;font-family:inherit;word-break:break-all;text-align:left" translate="no"></button>{% endset %}{{ _('Type %(confirm_word)s to confirm', confirm_word=confirm_word) }}</label>
            <input type="text" id="customConfirmType" class="input-field" autocomplete="off" spellcheck="false" placeholder="">
        </div>
        <div class="flex gap-2 justify-end" style="padding:18px 22px;margin-top:4px">
            <button id="customConfirmCancel" class="btn-secondary text-xs">{{ pgettext('button', 'Cancel') }}</button>
            <button id="customConfirmOk" class="btn-primary text-xs">{{ pgettext('button', 'Confirm') }}</button>
        </div>
    </div>
</div>

<div id="staticYamlPopout" style="display:none;position:fixed;inset:0;z-index:9998;background:var(--bg);flex-direction:column;">
    <div class="flex items-center justify-between px-6 py-3.5" style="border-bottom:1px solid var(--border);flex-shrink:0;">
        <div class="flex items-center gap-2">
            <i class="ph-bold ph-code" style="color:var(--purple)"></i>
            <span class="font-semibold text-sm">{{ _('Raw YAML - traefik.yml') }}</span>
        </div>
        <div class="flex gap-2">
            <button onclick="saveStaticPopout()" class="btn-primary text-xs"><i class="ph-bold ph-floppy-disk mr-1"></i>{{ pgettext('button', 'Save') }}</button>
            <button onclick="closeStaticYamlPopout()" class="btn-secondary text-xs"><i class="ph-bold ph-arrows-in mr-1"></i>{{ pgettext('button', 'Close') }}</button>
        </div>
    </div>
    <div id="staticYamlPopoutEditor" style="flex:1;min-height:0;"></div>
</div>

<div id="routeYamlPopout" style="display:none;position:fixed;inset:0;z-index:9998;background:var(--bg);flex-direction:column;">
    <div class="flex items-center justify-between px-6 py-3.5" style="border-bottom:1px solid var(--border);flex-shrink:0;">
        <div class="flex items-center gap-2">
            <i class="ph-bold ph-code" style="color:var(--blue)"></i>
            <span class="font-semibold text-sm" id="routeYamlPopoutTitle">{{ _('Raw YAML') }}</span>
        </div>
        <div class="flex gap-2">
            <button onclick="saveRouteYaml()" class="btn-primary text-xs"><i class="ph-bold ph-floppy-disk mr-1"></i>{{ pgettext('button', 'Save') }}</button>
            <button onclick="closeRouteYamlEditor()" class="btn-secondary text-xs"><i class="ph-bold ph-arrows-in mr-1"></i>{{ pgettext('button', 'Close') }}</button>
        </div>
    </div>
    <div id="routeYamlOrigins" class="px-6 py-2 text-xs" style="display:none;flex-shrink:0;border-bottom:1px solid var(--border);color:var(--muted);"></div>
    <div id="routeYamlPopoutEditor" style="flex:1;min-height:0;"></div>
</div>

<div id="gitDiffPopout" style="display:none;position:fixed;inset:0;z-index:9999;background:var(--bg);flex-direction:column;">
    <div class="flex items-center justify-between px-6 py-3.5" style="border-bottom:1px solid var(--border);flex-shrink:0;">
        <div class="flex items-center gap-3" style="min-width:0;flex:1;">
            <div class="flex items-center gap-2 flex-shrink-0">
                <i class="ph-bold ph-git-diff" style="color:var(--blue)"></i>
                <span class="font-semibold text-sm" id="gitDiffPopoutTitle">{{ pgettext('label', 'Diff') }}</span>
            </div>
            <div id="gitDiffFileTabs" class="flex gap-1 flex-wrap"></div>
        </div>
        <button onclick="closeGitDiffPopout()" class="btn-secondary text-xs flex-shrink-0"><i class="ph-bold ph-arrows-in mr-1"></i>{{ pgettext('button', 'Close') }}</button>
    </div>
    <div id="gitDiffPopoutEditor" style="flex:1;min-height:0;"></div>
</div>

<div id="traefikRestartOverlay" style="display:none;position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.75);backdrop-filter:blur(4px);flex-direction:column;align-items:center;justify-content:center;gap:16px">
    <i class="ph-bold ph-arrows-clockwise text-5xl animate-spin" style="color:var(--blue);opacity:0.9"></i>
    <div class="text-center">
        <p class="text-lg font-semibold" style="color:var(--text)">{{ _('Restarting Traefik…') }}</p>
        <p class="text-sm mt-1" style="color:var(--muted)">{{ _('This page will reconnect automatically.') }}</p>
    </div>
    <button id="traefikRestartManualReload" onclick="location.reload()" style="display:none;margin-top:8px;padding:8px 20px;border-radius:8px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;font-size:13px;cursor:pointer">{{ _('Reload now') }}</button>
</div>

<div id="tmUpdatePopup" class="hidden" style="position:fixed;bottom:24px;right:24px;z-index:9999;background:var(--card);border:1px solid var(--orange);border-radius:10px;padding:14px 16px;min-width:240px;max-width:300px;box-shadow:0 4px 24px rgba(0,0,0,0.25);">
    <div class="flex items-start gap-3">
        <i class="ph-bold ph-arrow-circle-up" style="color:var(--orange);font-size:20px;flex-shrink:0;margin-top:1px"></i>
        <div style="flex:1;min-width:0;">
            <div class="text-sm font-medium" style="color:var(--text)">{{ _('Traefik Manager update available') }}</div>
            <div class="text-xs mt-0.5" style="color:var(--muted)"><span id="tmUpdatePopupVersion"></span></div>
            <a id="tmUpdatePopupLink" href="#" target="_blank" rel="noopener" class="text-xs mt-2 inline-block" style="color:var(--blue)">{{ _('View release notes') }}</a>
        </div>
        <button onclick="dismissTmUpdatePopup()" style="color:var(--muted);background:none;border:none;cursor:pointer;padding:0;line-height:1;flex-shrink:0">
            <i class="ph-bold ph-x text-sm"></i>
        </button>
    </div>
</div>

<div id="securityAdvisoryPopup" class="hidden" style="position:fixed;bottom:24px;right:24px;z-index:10000;background:var(--card);border:1px solid var(--red);border-radius:10px;padding:14px 16px;min-width:260px;max-width:340px;box-shadow:0 4px 24px rgba(0,0,0,0.25);">
    <div class="flex items-start gap-3">
        <i class="ph-bold ph-shield-warning" style="color:var(--red);font-size:20px;flex-shrink:0;margin-top:1px"></i>
        <div style="flex:1;min-width:0;">
            <div class="text-sm font-medium" style="color:var(--text)">{{ _('Traefik security advisory') }}</div>
            <div class="text-xs mt-0.5" style="color:var(--muted)"><span id="securityAdvisoryText"></span></div>
            <a id="securityAdvisoryLink" href="#" target="_blank" rel="noopener" class="text-xs mt-2 inline-block" style="color:var(--blue)">{{ _('View advisory') }}</a>
        </div>
        <button onclick="dismissSecurityAdvisory()" style="color:var(--muted);background:none;border:none;cursor:pointer;padding:0;line-height:1;flex-shrink:0">
            <i class="ph-bold ph-x text-sm"></i>
        </button>
    </div>
</div>

<div class="toast" id="toastContainer">
{% with messages = get_flashed_messages(with_categories=true) %}
  {% if messages %}
    {% for category, message in messages %}
    <div class="toast-item {{ category }}" onclick="this.remove()">
        {% if category == 'success' %}<i class="ph-fill ph-check-circle text-green-400 text-lg"></i>{% else %}<i class="ph-fill ph-warning-circle text-red-400 text-lg"></i>{% endif %}
        <span>{{ message }}</span>
        <button onclick="this.parentElement.remove()" class="ml-auto text-traefik-muted hover:text-white"><i class="ph-bold ph-x text-sm"></i></button>
    </div>
    {% endfor %}
  {% endif %}
{% endwith %}
</div>


{% include 'sections/navbar.html' %}



<div id="staticSetupBanner" style="display:none">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 py-2">
        <div class="flex items-center gap-3 rounded-lg px-4 py-2.5" style="background:rgba(9,105,218,0.08);border:1px solid rgba(9,105,218,0.25)">
            <i class="ph-bold ph-info text-sm shrink-0" style="color:var(--blue)"></i>
            <p class="text-xs flex-1" style="color:var(--text)">
                {{ _('%(new_in_v1)s Static Config Editor lets you manage your Traefik static config from the UI. %(set_it_up)s', new_in_v1=tag('strong', _('New in v1:')), set_it_up=tag('a', _('Set it up →'), href='https://traefik-manager.xyzlab.dev/static', target='_blank', style_='color:var(--blue);text-decoration:underline;margin-left:4px')) }}
            </p>
            <button onclick="document.getElementById('staticSetupBanner').style.display='none';localStorage.setItem('tm-static-setup-v1','1')" class="shrink-0" style="color:var(--muted);line-height:1" aria-label="{{ pgettext('label', 'Dismiss') }}">
                <i class="ph-bold ph-x text-sm"></i>
            </button>
        </div>
    </div>
</div>

<div class="side-nav-backdrop" id="sideNavBackdrop" onclick="closeSideNavDrawer()"></div>
<aside id="sideNav">
    <div class="side-nav-app" id="sideNavApp">
        <div class="side-nav-brand-row">
            <img src="{{ base_path }}/static/icons/icon.png" alt="{{ pgettext('label', 'logo') }}">
            <span>Traefik <span style="color:var(--blue)">Manager</span></span>
        </div>
        <div id="drawerServerWrap" style="display:none">
            <button class="drawer-srv-head" id="drawerServerBtn" onclick="toggleDrawerServerList()" title="{{ _('Switch server') }}">
                <span class="drawer-srv-mark" id="drawerServerMark">
                    <svg viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                        <rect x="206" y="206" width="100" height="100" rx="20" stroke="currentColor" stroke-width="34"/>
                        <line x1="256" y1="206" x2="256" y2="80" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
                        <line x1="256" y1="306" x2="256" y2="432" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
                        <line x1="206" y1="256" x2="80" y2="256" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
                        <line x1="306" y1="256" x2="432" y2="256" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
                        <circle cx="256" cy="60" r="26" fill="currentColor"/>
                        <circle cx="256" cy="452" r="26" fill="currentColor"/>
                        <circle cx="60" cy="256" r="26" fill="currentColor"/>
                        <circle cx="452" cy="256" r="26" fill="currentColor"/>
                    </svg>
                    <i class="ph-bold ph-hard-drives" id="drawerServerAgentIcon"></i>
                    <span class="drawer-srv-badge" id="drawerServerBadge" style="background:var(--green)"></span>
                </span>
                <span class="drawer-srv-dot" id="drawerServerDot" style="background:var(--green)"></span>
                <span class="drawer-srv-t">
                    <span class="drawer-srv-n" id="drawerServerName">{{ pgettext('button', 'Host') }}</span>
                    <span class="drawer-srv-s" id="drawerServerSub">Traefik Manager</span>
                </span>
                <i class="ph-bold ph-caret-down" id="drawerServerCaret"></i>
            </button>
            <div class="drawer-srv-list" id="drawerServerList" style="display:none"></div>
        </div>
    </div>
    <nav id="sideNavItems"></nav>
    <div class="side-nav-foot">
        <button onclick="toggleSideNavCollapse()" id="sideNavCollapseBtn" title="{{ _('Collapse navigation') }}">
            <i class="ph-bold ph-caret-double-left" id="sideNavCollapseIcon"></i><span class="side-nav-toggle-label">{{ pgettext('button', 'Collapse') }}</span>
        </button>
        {% if auth_enabled %}
        <form method="POST" action="{{ url_for('logout') }}" onsubmit="tabCacheClear()">
            <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
            <button type="submit" id="sideNavLogoutBtn" title="{{ _('Sign out') }}">
                <i class="ph-bold ph-sign-out"></i><span class="side-nav-toggle-label">{{ _('Sign out') }}</span>
            </button>
        </form>
        {% endif %}
        <button onclick="closeSideNavDrawer(); openSettingsModal()" id="sideNavSettingsBtn" title="{{ pgettext('tooltip', 'Settings') }}">
            <i class="ph-bold ph-gear"></i><span class="side-nav-toggle-label">{{ pgettext('button', 'Settings') }}</span>
        </button>
    </div>
</aside>

<main class="max-w-7xl mx-auto px-4 sm:px-6 py-6">

    <div id="storageBanner" style="display:none;background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.35);border-radius:10px;padding:12px 16px;margin-bottom:16px"></div>

    {% include 'sections/stats.html' %}

        
    {% include 'tabs/tab_services.html' %}

    
    {% include 'tabs/tab_middlewares.html' %}


    {% include 'tabs/tab_routemap.html' %}


    {% include 'tabs/tab_dashboard.html' %}


    {% include 'tabs/tab_live.html' %}


    {% include 'tabs/tab_docker.html' %}

    {% include 'tabs/tab_kubernetes.html' %}

    {% include 'tabs/tab_swarm.html' %}

    {% include 'tabs/tab_nomad.html' %}
    {% include 'tabs/tab_internal.html' %}

    {% include 'tabs/tab_ecs.html' %}

    {% include 'tabs/tab_consulcatalog.html' %}

    {% include 'tabs/tab_redis.html' %}

    {% include 'tabs/tab_etcd.html' %}

    {% include 'tabs/tab_consul.html' %}

    {% include 'tabs/tab_zookeeper.html' %}

    {% include 'tabs/tab_http_provider.html' %}

    {% include 'tabs/tab_file_external.html' %}


    {% include 'tabs/tab_certs.html' %}

    {% include 'tabs/tab_tls_options.html' %}

    {% include 'tabs/tab_crowdsec.html' %}


    {% include 'tabs/tab_plugins.html' %}

    {% include 'tabs/tab_static.html' %}

    
    {% include 'tabs/tab_logs.html' %}

</main>


{% include 'modals/route_modal.html' %}

{% include 'modals/mw_modal.html' %}
{% include 'modals/service_modal.html' %}

{% include 'modals/tls_options_modal.html' %}

{% include 'modals/ip_diagnostic_modal.html' %}

{% include 'modals/trusted_ips_modal.html' %}

{% include 'modals/detail_panels.html' %}

<script>
const APP_DATA             = JSON.parse({{ apps | tojson | tojson }});
const availableDomains     = {{ domains | tojson | safe }};
const availableCertResolvers = {{ cert_resolvers | tojson | safe }};
const MULTI_CONFIG         = {{ multi_config | tojson }};
const CONFIG_DIR_SET       = {{ config_dir_set | tojson }};
const CONFIG_PATHS_LIST    = {{ config_paths_list | tojson | safe }};
let _allMiddlewares        = {{ middlewares | tojson | safe }};

function _csrfHeaders() {
    const token = document.querySelector('meta[name="csrf-token"]')?.content || '';
    return { 'X-CSRF-Token': token };
}

window._tmAssetVersion = '{{ asset_version }}';
let _activeAgent = null;
const _agentRegistry = {};
let _agentList = [];
const _agentHealthCache = {};

function agentFetch(path, opts) {
    const merged = { ...opts, headers: { ...(opts && opts.headers), ..._csrfHeaders() } };
    if (!_activeAgent) return fetch(path, merged);
    const proxyPath = '/api/agents/proxy/' + _activeAgent.id + '/' + path.replace(/^\/api\//, '');
    return fetch(proxyPath, merged);
}

async function refreshStaticTabAvailability() {
    const row = document.getElementById('staticTabToggleRow');
    let available = false;
    try {
        if (_activeAgent) {
            available = (await agentFetch('/api/static')).ok;
        } else {
            available = !!(await (await fetch('/api/static/available')).json()).available;
        }
    } catch(e) { available = false; }
    if (row) row.style.display = available ? '' : 'none';
    if (!available && _activeTab === 'static') switchTab('services');
    if (typeof buildSideNav === 'function') buildSideNav();
    _staticAvailable = available;
    applyStaticPlacement(getStaticPlacement());
}

let _staticAvailable = false;

function getStaticPlacement() {
    const v = tmPref('staticPlacement');
    if (v === 'off' || v === 'settings' || v === 'tab') return v;
    const tabs = _activeAgent ? (_activeAgent.visible_tabs || {}) : _localTabsCache;
    return (tabs || {}).static ? 'tab' : 'off';
}

function applyStaticPlacement(mode) {
    const el = document.getElementById('tab-static');
    const sidebarBtn = document.getElementById('msb-static');
    const mount = document.getElementById('staticSettingsMount');
    const home = document.querySelector('main');
    if (sidebarBtn) sidebarBtn.style.display = (mode === 'settings' && _staticAvailable) ? '' : 'none';
    const mobileGroup = document.getElementById('staticMobileGroup');
    if (mobileGroup) mobileGroup.style.display = (mode === 'settings' && _staticAvailable) ? '' : 'none';
    document.documentElement.classList.toggle('tm-static-in-settings', mode === 'settings');
    ['off', 'settings', 'tab'].forEach(m => {
        document.getElementById('scPlace-' + m)?.classList.toggle('active-http', m === mode);
    });
    if (el) {
        if (mode === 'settings' && mount && el.parentElement !== mount) {
            el.classList.remove('tab-content');
            el.classList.remove('active');
            el.dataset.scMounted = '1';
            mount.appendChild(el);
        } else if (mode !== 'settings' && el.dataset.scMounted === '1') {
            delete el.dataset.scMounted;
            el.classList.add('tab-content');
            if (home) home.appendChild(el);
        }
    }
    if (typeof applyTabVisibility === 'function') {
        const tabs = _activeAgent ? (_activeAgent.visible_tabs || {}) : (_localTabsCache || {});
        tabs.static = mode === 'tab';
        applyTabVisibility(tabs);
    }
    if (typeof buildSideNav === 'function') buildSideNav();
    if (mode === 'settings' && _staticAvailable && typeof refreshStaticTab === 'function') {
        if (typeof _staticLoadedFor !== 'undefined') _staticLoadedFor = null;
        refreshStaticTab();
    }
}

function setStaticPlacement(mode) {
    tmSetPref('staticPlacement', mode);
    const tabs = _activeAgent ? (_activeAgent.visible_tabs || {}) : (_localTabsCache || {});
    tabs.static = mode === 'tab';
    if (!_activeAgent) {
        _localTabsCache = tabs;
        localStorage.setItem('tm_tabs_local', JSON.stringify(tabs));
        fetch('/api/settings/tabs', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', ..._csrfHeaders() },
            body: JSON.stringify({ visible_tabs: tabs }),
        }).catch(() => {});
    }
    applyStaticPlacement(mode);
}

function _renderServerSwitcherList(runHealthChecks) {
    const agents = _agentList;
    const wrap = document.getElementById('serverSwitcherWrap');
    const list = document.getElementById('serverSwitcherList');
    if (!wrap || !list) return;
    wrap.style.display = agents.length ? '' : 'none';
    const localActive = !_activeAgent;
    const desktopItem = (id, label, dotColor, active, dotId) =>
        `<div class="notif-item${active ? ' active' : ''}" onclick="${id ? `switchServer(${_jsArg(id)})` : 'switchServer(null)'}" style="cursor:pointer;${active?'background:var(--input-bg);':''}">
            <div class="flex items-center gap-2 py-1">
                <span class="w-2 h-2 rounded-full flex-shrink-0" style="background:${dotColor}"${dotId ? ` id="${dotId}"` : ''}></span>
                <div style="min-width:0;flex:1;"><div class="text-xs font-medium truncate" style="color:var(--text)">${label}</div></div>
                ${active ? '<i class="ph-bold ph-check text-xs ml-auto flex-shrink-0" style="color:var(--blue)"></i>' : ''}
            </div>
        </div>`;
    const hostDot = 'var(--green)';
    list.innerHTML = desktopItem(null, thc('button', 'Host'), hostDot, localActive, null) +
        agents.map(a => desktopItem(a.id, _esc(a.name), _agentHealthCache[a.id] || 'var(--muted)', _activeAgent && _activeAgent.id === a.id, 'ss-dot-' + a.id)).join('');
    if (runHealthChecks) {
        agents.forEach(a => fetch('/api/agents/' + a.id + '/health').then(r => r.json()).then(d => {
            _agentHealthCache[a.id] = d.ok ? 'var(--green)' : 'var(--red)';
            _agentHealthInfo[a.id] = d;
            const dot = document.getElementById('ss-dot-' + a.id);
            if (dot) dot.style.background = _agentHealthCache[a.id];
            _renderDrawerServerList();
        }).catch(() => { _agentHealthCache[a.id] = 'var(--red)'; _renderDrawerServerList(); }));
    }
    _renderDrawerServerList();
}

const _agentHealthInfo = {};

function _agentSubLine(a) {
    const d = _agentHealthInfo[a.id];
    if (!d) return '';
    if (!d.ok) return tc('label', 'offline');
    const v  = d.version ? 'v' + d.version : '';
    const ms = (typeof d.latency_ms === 'number' && d.latency_ms >= 0) ? d.latency_ms + 'ms' : '';
    return [v, ms].filter(Boolean).join(' · ');
}

function _renderDrawerServerList() {
    const wrap = document.getElementById('drawerServerWrap');
    const list = document.getElementById('drawerServerList');
    if (!wrap || !list) return;
    wrap.style.display = _agentList.length ? '' : 'none';
    if (!_agentList.length) return;
    const name = document.getElementById('drawerServerName');
    const sub  = document.getElementById('drawerServerSub');
    const dot  = document.getElementById('drawerServerDot');
    if (name) name.textContent = _activeAgent ? _activeAgent.name : tc('button', 'Host');
    if (sub)  sub.textContent  = _activeAgent ? (_agentSubLine(_activeAgent) || tc('label', 'Agent')) : 'Traefik Manager';
    if (dot)  dot.style.background = _activeAgent ? (_agentHealthCache[_activeAgent.id] || 'var(--muted)') : 'var(--green)';
    const mark  = document.getElementById('drawerServerMark');
    const badge = document.getElementById('drawerServerBadge');
    if (mark)  mark.classList.toggle('is-agent', !!_activeAgent);
    if (badge) badge.style.background = _activeAgent ? (_agentHealthCache[_activeAgent.id] || 'var(--muted)') : 'var(--green)';
    const TM_MARK = `<span class="drawer-srv-glyph"><svg viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
            <rect x="206" y="206" width="100" height="100" rx="20" stroke="currentColor" stroke-width="34"/>
            <line x1="256" y1="206" x2="256" y2="80" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
            <line x1="256" y1="306" x2="256" y2="432" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
            <line x1="206" y1="256" x2="80" y2="256" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
            <line x1="306" y1="256" x2="432" y2="256" stroke="currentColor" stroke-width="34" stroke-linecap="round"/>
            <circle cx="256" cy="60" r="26" fill="currentColor"/>
            <circle cx="256" cy="452" r="26" fill="currentColor"/>
            <circle cx="60" cy="256" r="26" fill="currentColor"/>
            <circle cx="452" cy="256" r="26" fill="currentColor"/>
        </svg></span>`;
    const item = (id, icon, title, subText, active, dotColor) =>
        `<button class="drawer-srv-item${active ? ' active' : ''}" onclick="_drawerPickServer(${id ? `${_jsArg(id)}` : 'null'})">
            ${icon ? `<i class="ph-bold ${icon}"></i>` : TM_MARK}
            <span class="drawer-srv-t">
                <span class="drawer-srv-n">${title}</span>
                ${subText ? `<span class="drawer-srv-s">${subText}</span>` : ''}
            </span>
            ${active ? '<i class="ph-bold ph-check drawer-srv-check"></i>' : `<span class="drawer-srv-dot" style="background:${dotColor}"></span>`}
        </button>`;
    list.innerHTML =
        `<div class="drawer-srv-title">${th('Switch server')}</div>` +
        item(null, null, thc('button', 'Host'), 'Traefik Manager', !_activeAgent, 'var(--green)') +
        _agentList.map(a => item(a.id, 'ph-hard-drives', _esc(a.name), _esc(_agentSubLine(a)), _activeAgent && _activeAgent.id === a.id, _agentHealthCache[a.id] || 'var(--muted)')).join('');
}

function _drawerPickServer(id) {
    const list  = document.getElementById('drawerServerList');
    const caret = document.getElementById('drawerServerCaret');
    if (list) list.style.display = 'none';
    if (caret) caret.classList.remove('open');
    switchServer(id);
}

function toggleDrawerServerList() {
    const list  = document.getElementById('drawerServerList');
    const caret = document.getElementById('drawerServerCaret');
    const btn   = document.getElementById('drawerServerBtn');
    if (!list) return;
    const opening = list.style.display === 'none';
    list.style.display = opening ? '' : 'none';
    if (caret) caret.classList.toggle('open', opening);
    const railed = document.documentElement.classList.contains('tm-nav-collapsed') && window.innerWidth >= 768;
    if (opening && btn && railed) {
        const r = btn.getBoundingClientRect();
        list.style.left = (r.right + 8) + 'px';
        list.style.top  = Math.min(r.top, window.innerHeight - list.offsetHeight - 12) + 'px';
    } else {
        list.style.left = '';
        list.style.top  = '';
    }
}

function _updateServerSwitcherList(agents) {
    agents.forEach(a => _agentRegistry[a.id] = a);
    _agentList = agents;
    if (_activeAgent && _agentRegistry[_activeAgent.id]) _activeAgent = _agentRegistry[_activeAgent.id];
    _renderServerSwitcherList(true);
}

function _warnIfAgentUnreachable(agent) {
    const d = _agentHealthInfo[agent.id];
    if (!d || d.ok) return;
    if (typeof showToast === 'function') {
        showToast(d.error
            ? t('{name} is offline - {error}. Its tabs will stay empty until it responds.', { name: agent.name, error: d.error })
            : t('{name} is offline - it is not responding. Its tabs will stay empty until it responds.', { name: agent.name }),
            'error', false);
    }
}

function switchServer(agentId) {
    const agent = agentId ? (_agentRegistry[agentId] || null) : null;
    if (agent) _warnIfAgentUnreachable(agent);
    _activeAgent = agent;
    _currentVersion = null;
    if (typeof _clearEpVersion === 'function') _clearEpVersion();
    if (agent) localStorage.setItem('tm_active_agent', agent.id);
    else localStorage.removeItem('tm_active_agent');
    _cachedEntrypoints = null;
    _cachedMiddlewares = null;
    _cachedCertResolvers = null;
    window._tmServices = null;
    try { if (typeof renderRouteGrid === 'function') renderRouteGrid([]); } catch (e) {}
    try { if (typeof renderMwGrid === 'function') renderMwGrid([]); } catch (e) {}
    _inflightEntrypoints = null;
    _inflightMiddlewares = null;
    _inflightCertResolvers = null;
    _inflightServicesList = null;
    if (typeof _dropConfigFilesCache === 'function') _dropConfigFilesCache();
    _customDomains = [];
    if (agent) document.getElementById('selfRouteEpWarning')?.remove();
    if (typeof tabCacheForgetAll === 'function') tabCacheForgetAll();
    if (typeof window.rmServerChanged === 'function') window.rmServerChanged();
    if (typeof window._sdServerChanged === 'function') window._sdServerChanged();
    if (typeof window._certServerChanged === 'function') window._certServerChanged();
    if (agent) {
        const stored = JSON.parse(localStorage.getItem('tm_tabs_agent_' + agent.id) || '{}');
        const hub = agent.visible_tabs || {};
        if (Object.keys(hub).length) {
            applyTabVisibility(hub);
            localStorage.setItem('tm_tabs_agent_' + agent.id, JSON.stringify(hub));
        } else {
            applyTabVisibility(stored);
            if (Object.keys(stored).length) {
                agent.visible_tabs = stored;
                fetch('/api/agents/' + agent.id, { method: 'PUT', headers: { 'Content-Type': 'application/json', ..._csrfHeaders() }, body: JSON.stringify({ visible_tabs: stored }) }).catch(() => {});
            }
        }
    } else {
        applyTabVisibility(_localTabsCache);
    }
    const label = document.getElementById('serverSwitcherLabel');
    if (label) label.textContent = agent ? agent.name : tc('button', 'Host');
    document.getElementById('serverSwitcherMenu')?.classList.remove('open');
    const chip = document.getElementById('agentServerLabel');
    if (chip) { chip.textContent = agent ? agent.name : ''; chip.style.display = agent ? '' : 'none'; }
    _renderServerSwitcherList(false);
    if (typeof _staticLoadedFor !== 'undefined') _staticLoadedFor = null;
    refreshStaticTabAvailability();
    const _cur = _activeTab;
    if (_cur !== 'services' && _cur !== 'middlewares') refreshRoutes();
    refreshCurrentTab();
    loadOverviewStats();
    if (typeof updateTmVersionBadge === 'function') updateTmVersionBadge();
    const sm = document.getElementById('settingsModal');
    if (sm && sm.style.display !== 'none' && typeof _loadAboutAgentInfo === 'function') _loadAboutAgentInfo();
}


function _setAgentEditControls(_agentActive) {}

function refreshCurrentTab() {
    switchTab(_activeTab);
}

function toggleServerMenu() {
    const menu = document.getElementById('serverSwitcherMenu');
    if (!menu) return;
    const opening = !menu.classList.contains('open');
    document.querySelectorAll('.notif-panel.open').forEach(p => p.classList.remove('open'));
    if (opening) menu.classList.add('open');
}

document.addEventListener('click', e => {
    const wrap = document.getElementById('serverSwitcherWrap');
    if (wrap && !wrap.contains(e.target)) {
        document.getElementById('serverSwitcherMenu')?.classList.remove('open');
    }
});
</script>

<script src="{{ base_path }}/static/js/core.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/routes.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/middlewares.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/services.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/settings.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/logs.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/crowdsec.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/dashboard.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/providers.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/certs.js?v={{ asset_version }}"></script>
<script src="{{ base_path }}/static/js/init.js?v={{ asset_version }}"></script>

{% include 'modals/settings_modal.html' %}


<footer style="text-align:center;padding:20px 0 24px;font-size:12px;color:var(--muted)">
    <a href="https://github.com/traefik/traefik" target="_blank"
       style="color:var(--muted);text-decoration:none" onmouseover="this.style.color='var(--text)'" onmouseout="this.style.color='var(--muted)'">Traefik</a>
    {{ pgettext('label', '·') }}
    <a href="https://github.com/chr0nzz/traefik-manager" target="_blank"
       style="color:var(--muted);text-decoration:none" onmouseover="this.style.color='var(--text)'" onmouseout="this.style.color='var(--muted)'">Traefik Manager</a>
    {{ pgettext('label', '·') }}
    <span id="footerManagerVer" style="cursor:pointer" title="{{ pgettext('tooltip', 'About') }}" onclick="openSettingsModal('about')">-</span>
</footer>

<div id="_copyToast" style="position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--text);color:var(--bg);padding:6px 16px;border-radius:8px;font-size:13px;font-weight:500;pointer-events:none;opacity:0;transition:opacity 0.2s;z-index:9999"></div>

<script src="{{ base_path }}/static/vendor/dagre.min.js"></script>
<script src="{{ base_path }}/static/vendor/monaco/vs/loader.js"></script>
<script src="{{ base_path }}/static/js/static-config.js?v={{ asset_version }}"></script>

<div id="routeActionsMenu" class="route-ctx-menu" style="display:none;background:var(--card);border:1px solid var(--border)"></div>
</body>
</html>