base.html
228 lignes · 9290 octets
<!DOCTYPE html> <html lang="fr" data-theme="light"> <head> <script> (function() { var lm = document.cookie.match(/(?:^|;\s*)locale=([^;]*)/); if (lm && lm[1]) document.documentElement.lang = lm[1]; var m = document.cookie.match(/(?:^|;\s*)theme=([^;]*)/); var t = m ? m[1] : 'auto'; if (t === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); } else if (t === 'light') { document.documentElement.setAttribute('data-theme', 'light'); } else { var d = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', d); } })(); </script> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content="{% if app_description != "" %}{{ app_description }}{% else %}{{ app_name }}{% endif %}"> <!-- CSRF token exposed for JS clients; mirrored on <body hx-headers> for HTMX. --> <meta name="csrf-token" content="{{ csrf_token }}"> <title>{% block title %}{{ page_title }} - {{ app_name }}{% endblock %}</title> <!-- Favicon --> <link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg"> <link rel="icon" type="image/png" href="/static/images/favicon.png"> <link rel="apple-touch-icon" href="/static/images/apple-touch-icon.png"> <!-- Tailwind CSS + DaisyUI --> <link href="/static/css/tailwind.css" rel="stylesheet"> <!-- Custom CSS --> <link href="/static/css/custom.css" rel="stylesheet"> <!-- HTMX (local) --> <script src="/static/js/htmx.min.js"></script> <!-- Alpine.js CSP build (no unsafe-eval). Components register on alpine:init. --> <script src="/static/js/alpine-components.js" defer></script> <script src="/static/js/alpine-csp.min.js" defer></script> {% block head_extra %}{% endblock %} </head> <body class="bg-base-100 min-h-screen flex flex-col" hx-headers='{"X-CSRF-Token": "{{ csrf_token }}"}'> <!-- Navbar (fixe en haut) - Include in child templates that need it --> {% block navbar %}{% endblock %} <!-- Breadcrumb --> {% block breadcrumb %} {% endblock %} <!-- Main Layout: Sidebar + Content --> <div class="flex flex-1 overflow-hidden"> <!-- Sidebar (gauche) --> {% block sidebar %} {% endblock %} <!-- Content Area (principal) --> <main class="flex-1 overflow-y-auto"> <div class="container mx-auto px-4 py-6"> {% block content %} <!-- Contenu spécifique de la page --> {% endblock %} </div> </main> </div> <!-- Footer (optionnel) --> {% block footer %} <footer class="bg-base-200 border-t border-base-300 py-4 mt-8"> <div class="container mx-auto px-4 text-center text-base-content text-sm"> <p>{{ app_name }} {{ app_version }}{% if app_domain != "" %} - {{ app_domain }}{% endif %}</p> {% if app_description != "" %} <p class="text-base-content/70 mt-1">{{ app_description }}</p> {% endif %} {% include "components/analytics_opt_out.html" %} </div> </footer> {% endblock %} <!-- Scripts additionnels --> {% block scripts %}{% endblock %} <!-- Alpine.js initialization check and fallback --> <script> // Vérifier que Alpine.js est chargé document.addEventListener('DOMContentLoaded', function() { // Attendre un peu pour qu'Alpine.js s'initialise setTimeout(function() { if (typeof Alpine === 'undefined') { console.warn('Alpine.js n\'est pas chargé. Le dropdown utilisateur pourrait ne pas fonctionner.'); } }, 100); }); // Language selector functions (Feature 014) function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); return null; } function updateLanguageDisplay() { const locale = getCookie('locale') || 'fr'; const langNames = { 'fr': 'FR', 'en': 'EN', 'de': 'DE', 'es': 'ES' }; const displayName = langNames[locale] || 'FR'; // Update desktop display const desktopDisplay = document.getElementById('navbar-lang-display'); if (desktopDisplay) { desktopDisplay.textContent = displayName; } // Update mobile display const mobileDisplay = document.getElementById('navbar-lang-display-mobile'); if (mobileDisplay) { mobileDisplay.textContent = displayName; } // Update active states ['fr', 'en', 'de', 'es'].forEach(code => { const desktopLink = document.getElementById('lang-' + code); const mobileLink = document.getElementById('lang-' + code + '-mobile'); if (desktopLink) { if (code === locale) { desktopLink.classList.add('active'); } else { desktopLink.classList.remove('active'); } } if (mobileLink) { if (code === locale) { mobileLink.classList.add('active'); } else { mobileLink.classList.remove('active'); } } }); } window.changeLanguage = function(locale) { if (!['fr', 'en', 'de', 'es'].includes(locale)) { return false; } document.cookie = 'locale=' + locale + '; path=/; max-age=31536000; SameSite=Lax'; fetch('/settings/preferences', { method: 'PUT', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': '{{ csrf_token }}' }, credentials: 'include', body: 'language=' + encodeURIComponent(locale) }).then(function() { window.location.reload(); }).catch(function() { window.location.reload(); }); return false; // Prevent default link behavior }; // Initialize language display on page load document.addEventListener('DOMContentLoaded', function() { updateLanguageDisplay(); }); // --- Auto-refresh JWT (Remember Me) --- // Silently refreshes the JWT access token before it expires, // so users with "remember me" keep their session alive. (function() { var refreshToken = localStorage.getItem('refresh_token'); var rememberMe = localStorage.getItem('remember_me'); if (!refreshToken || rememberMe !== 'true') return; // JWT lifetime in seconds (stored at login, default 15 min = 900s) var expiresIn = parseInt(localStorage.getItem('jwt_expires_in') || '900', 10); // Refresh 2 minutes before expiration (minimum 30s) var intervalMs = Math.max((expiresIn - 120), 30) * 1000; function doRefresh() { var token = localStorage.getItem('refresh_token'); if (!token || localStorage.getItem('remember_me') !== 'true') return; fetch('/api/v1/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ refresh_token: token }) }) .then(function(response) { if (response.ok) return response.json(); throw new Error('refresh_failed'); }) .then(function(data) { // Rotate refresh token if (data.refresh_token) { localStorage.setItem('refresh_token', data.refresh_token); } // Update interval if server reports different expires_in if (data.expires_in) { localStorage.setItem('jwt_expires_in', String(data.expires_in)); } }) .catch(function() { // Token expired or revoked — clean up and redirect to login localStorage.removeItem('refresh_token'); localStorage.removeItem('remember_me'); localStorage.removeItem('jwt_expires_in'); window.location.href = '/login'; }); } setInterval(doRefresh, intervalMs); })(); </script> </body> </html>
GitRust