Implement automatic dark mode based on system preferences and fixed some translations and design-issues

This commit is contained in:
2026-03-14 19:08:16 +01:00
parent f210eabc3c
commit e113294298
18 changed files with 642 additions and 91 deletions

View File

@@ -39,6 +39,7 @@ services:
- PORT=3080
- BIND_ADDRESS=0.0.0.0
- CALLBACK_URL=http://10.88.0.1:3080
- AUTODARK=true
#- CALLBACK_SECRET=**************************************************
volumes:
# Required for fail2ban-ui: Stores SQLite database, application settings, and SSH keys of the fail2ban-ui container

View File

@@ -46,6 +46,9 @@ services:
# the web UI to unprotected networks. Set to a specific IP (e.g., 127.0.0.1
# or a specific interface IP) to restrict access.
# - BIND_ADDRESS=127.0.0.1
# Optional: Enable automatic dark mode based on system preferences (default: false).
# When set to true, the web UI will automatically switch to dark mode based on the system's preferred color scheme.
# - AUTODARK=true
# ============================================
# Privacy Settings

View File

@@ -27,6 +27,9 @@ services:
# the web UI to unprotected networks. Set to a specific IP (e.g., 127.0.0.1
# or a specific interface IP) to restrict access.
# - BIND_ADDRESS=127.0.0.1
# Optional: Enable automatic dark mode based on system preferences (default: false).
# When set to true, the web UI will automatically switch to dark mode based on the system's preferred color scheme.
# - AUTODARK=true
# ============================================
# Privacy Settings

View File

@@ -257,7 +257,7 @@
"settings.webhook.headers_hint": "Ein Header pro Zeile im Format Schlüssel: Wert.",
"settings.webhook.skip_tls": "TLS-Zertifikatsprüfung überspringen",
"settings.webhook.test": "Test-Webhook senden",
"settings.webhook.test_hint": "⚠️ Bitte speichern Sie die Webhook-Einstellungen zuerst, bevor Sie testen.",
"settings.webhook.test_hint": "Bitte speichern Sie die Webhook-Einstellungen zuerst, bevor Sie testen.",
"settings.elasticsearch.title": "Elasticsearch-Konfiguration",
"settings.elasticsearch.url": "Elasticsearch-URL",
"settings.elasticsearch.url_placeholder": "https://elasticsearch.example.com:9200",
@@ -272,7 +272,7 @@
"settings.elasticsearch.password": "Passwort",
"settings.elasticsearch.skip_tls": "TLS-Zertifikatsprüfung überspringen",
"settings.elasticsearch.test": "Verbindung testen",
"settings.elasticsearch.test_hint": "⚠️ Bitte speichern Sie die Elasticsearch-Einstellungen zuerst, bevor Sie testen.",
"settings.elasticsearch.test_hint": "Bitte speichern Sie die Elasticsearch-Einstellungen zuerst, bevor Sie testen.",
"settings.elasticsearch.help_title": "Elasticsearch Einrichtungsanleitung",
"settings.elasticsearch.help_template_title": "1. Index-Template erstellen",
"settings.elasticsearch.help_template_desc": "Erstellen Sie ein Index-Template in Kibana Dev Tools oder über die API, damit Elasticsearch alle Felder korrekt zuordnet. Das Template umfasst Event-Felder, angereicherte Log-Felder (HTTP, SSH, etc.) und geparste WHOIS-Felder:",
@@ -299,7 +299,7 @@
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN wird für Office365/Gmail empfohlen. PLAIN ist die Standard-SMTP-Authentifizierung. CRAM-MD5 ist challenge-response-basiert.",
"settings.smtp_insecure_skip_verify": "TLS-Zertifikatsüberprüfung überspringen",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Nicht für Produktion empfohlen",
"settings.smtp_insecure_skip_verify_warning": "Nicht für Produktion empfohlen",
"settings.smtp_username": "SMTP-Benutzername",
"settings.smtp_username_placeholder": "z.B. user@example.com",
"settings.smtp_password": "SMTP-Passwort",
@@ -308,7 +308,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "TLS verwenden (empfohlen)",
"settings.send_test_email": "Test-E-Mail senden",
"settings.send_test_email_hint": "⚠️ Bitte speichern Sie zuerst Ihre SMTP-Einstellungen, bevor Sie eine Test-E-Mail senden.",
"settings.send_test_email_hint": "Bitte speichern Sie zuerst Ihre SMTP-Einstellungen, bevor Sie eine Test-E-Mail senden.",
"settings.fail2ban": "Globale Standard-Fail2Ban-Konfigurationen",
"settings.fail2ban.description": "Diese Einstellungen werden auf allen aktivierten Fail2Ban-Servern angewendet und in deren jail.local [DEFAULT]-Abschnitt gespeichert.",
"settings.enable_bantime_increment": "Bantime-Inkrement aktivieren",

View File

@@ -257,7 +257,7 @@
"settings.webhook.headers_hint": "Ei Header pro Zile im Format Schlüssel: Wert.",
"settings.webhook.skip_tls": "TLS-Zertifikatsprüefig überspringä",
"settings.webhook.test": "Test-Webhook schicke",
"settings.webhook.test_hint": "⚠️ Bitte zersch d'Webhook-Iistellige spichere, bevor testet wird.",
"settings.webhook.test_hint": "Bitte zersch d'Webhook-Iistellige spichere, bevor testet wird.",
"settings.elasticsearch.title": "Elasticsearch-Konfiguration",
"settings.elasticsearch.url": "Elasticsearch-Adrässe",
"settings.elasticsearch.url_placeholder": "https://elasticsearch.example.com:9200",
@@ -272,7 +272,7 @@
"settings.elasticsearch.password": "Passwort",
"settings.elasticsearch.skip_tls": "TLS-Zertifikatsprüefig überspringä",
"settings.elasticsearch.test": "Verbindig teste",
"settings.elasticsearch.test_hint": "⚠️ Bitte zersch d'Elasticsearch-Iistellige spichere, bevor testet wird.",
"settings.elasticsearch.test_hint": "Bitte zersch d'Elasticsearch-Iistellige spichere, bevor testet wird.",
"settings.elasticsearch.help_title": "Elasticsearch Iirichtigsaleitig",
"settings.elasticsearch.help_template_title": "1. Index-Template erstelle",
"settings.elasticsearch.help_template_desc": "Ersteu es neus Index-Template us de Kibana Dev Tools, damit Elasticsearch alli Fälder korrekt zuordnet. S'Template umfasst Event-Fälder, aagrichereti Log-Fälder (HTTP, SSH, etc.) und geparsti WHOIS-Fälder:",
@@ -299,7 +299,7 @@
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN wird für Office365/Gmail empfohle. PLAIN isch d Standard-SMTP-Authentifizierung. CRAM-MD5 isch challenge-response-basiert.",
"settings.smtp_insecure_skip_verify": "TLS-Zertifikatsüberprüfig überspringe",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Nid fürd Produktion empfohle",
"settings.smtp_insecure_skip_verify_warning": "Nid fürd Produktion empfohle",
"settings.smtp_username": "SMTP-Benutzername",
"settings.smtp_username_placeholder": "z.B. user@example.com",
"settings.smtp_password": "SMTP-Passwort",
@@ -308,7 +308,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "TLS bruuche (empfohlen)",
"settings.send_test_email": "Test-Email schicke",
"settings.send_test_email_hint": "⚠️ Bitte spicher zersch dini SMTP-Iistellige, bevor du e Test-Email schicksch.",
"settings.send_test_email_hint": "Bitte spicher zersch dini SMTP-Iistellige, bevor du e Test-Email schicksch.",
"settings.fail2ban": "Globale Standard-Fail2Ban-Konfiguratione",
"settings.fail2ban.description": "Die Einstellige werde uf aui aktivierte Fail2Ban-Server aagwändet und i däre jail.local [DEFAULT]-Abschnitt g'spicheret.",
"settings.enable_bantime_increment": "Bantime-Inkrement aktivierä",

View File

@@ -257,7 +257,7 @@
"settings.webhook.headers_hint": "One header per line in Key: Value format.",
"settings.webhook.skip_tls": "Skip TLS Certificate Verification",
"settings.webhook.test": "Send Test Webhook",
"settings.webhook.test_hint": "⚠️ Please save your webhook settings first before testing.",
"settings.webhook.test_hint": "Please save your webhook settings first before testing.",
"settings.elasticsearch.title": "Elasticsearch Configuration",
"settings.elasticsearch.url": "Elasticsearch URL",
"settings.elasticsearch.url_placeholder": "https://elasticsearch.example.com:9200",
@@ -272,7 +272,7 @@
"settings.elasticsearch.password": "Password",
"settings.elasticsearch.skip_tls": "Skip TLS Certificate Verification",
"settings.elasticsearch.test": "Test Connection",
"settings.elasticsearch.test_hint": "⚠️ Please save your Elasticsearch settings first before testing.",
"settings.elasticsearch.test_hint": "Please save your Elasticsearch settings first before testing.",
"settings.elasticsearch.help_title": "Elasticsearch Setup Guide",
"settings.elasticsearch.help_template_title": "1. Create an Index Template",
"settings.elasticsearch.help_template_desc": "Create an index template in Kibana Dev Tools or via the API so Elasticsearch maps all fields correctly. The template includes core event fields, enriched log fields (HTTP, SSH, etc.), and parsed WHOIS fields:",
@@ -299,7 +299,7 @@
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN is recommended for Office365/Gmail. PLAIN is standard SMTP auth. CRAM-MD5 is challenge-response based.",
"settings.smtp_insecure_skip_verify": "Skip TLS Certificate Verification",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Not recommended for production",
"settings.smtp_insecure_skip_verify_warning": "Not recommended for production",
"settings.smtp_username": "SMTP Username",
"settings.smtp_username_placeholder": "e.g., user@example.com",
"settings.smtp_password": "SMTP Password",
@@ -308,7 +308,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Use TLS (Recommended)",
"settings.send_test_email": "Send Test Email",
"settings.send_test_email_hint": "⚠️ Please save your SMTP settings first before sending a test email.",
"settings.send_test_email_hint": "Please save your SMTP settings first before sending a test email.",
"settings.fail2ban": "Global Default Fail2Ban Configurations",
"settings.fail2ban.description": "These settings will be applied to all enabled Fail2Ban servers and stored in their jail.local [DEFAULT] section.",
"settings.enable_bantime_increment": "Enable Bantime Increment",

View File

@@ -257,7 +257,7 @@
"settings.webhook.headers_hint": "Un encabezado por línea en formato Clave: Valor.",
"settings.webhook.skip_tls": "Omitir verificación de certificado TLS",
"settings.webhook.test": "Enviar webhook de prueba",
"settings.webhook.test_hint": "⚠️ Guarde la configuración del webhook antes de probar.",
"settings.webhook.test_hint": "Guarde la configuración del webhook antes de probar.",
"settings.elasticsearch.title": "Configuración de Elasticsearch",
"settings.elasticsearch.url": "URL de Elasticsearch",
"settings.elasticsearch.url_placeholder": "https://elasticsearch.example.com:9200",
@@ -272,7 +272,7 @@
"settings.elasticsearch.password": "Contraseña",
"settings.elasticsearch.skip_tls": "Omitir verificación de certificado TLS",
"settings.elasticsearch.test": "Probar conexión",
"settings.elasticsearch.test_hint": "⚠️ Guarde la configuración de Elasticsearch antes de probar.",
"settings.elasticsearch.test_hint": "Guarde la configuración de Elasticsearch antes de probar.",
"settings.elasticsearch.help_title": "Guía de configuración de Elasticsearch",
"settings.elasticsearch.help_template_title": "1. Crear una plantilla de índice",
"settings.elasticsearch.help_template_desc": "Cree una plantilla de índice en Kibana Dev Tools o a través de la API para que Elasticsearch mapee correctamente todos los campos. La plantilla incluye campos de evento, campos de log enriquecidos (HTTP, SSH, etc.) y campos WHOIS analizados:",
@@ -299,7 +299,7 @@
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN se recomienda para Office365/Gmail. PLAIN es la autenticación SMTP estándar. CRAM-MD5 está basado en challenge-response.",
"settings.smtp_insecure_skip_verify": "Omitir Verificación de Certificado TLS",
"settings.smtp_insecure_skip_verify_warning": "⚠️ No recomendado para producción",
"settings.smtp_insecure_skip_verify_warning": "No recomendado para producción",
"settings.smtp_username": "Nombre de usuario SMTP",
"settings.smtp_username_placeholder": "p.ej., usuario@example.com",
"settings.smtp_password": "Contraseña SMTP",
@@ -308,7 +308,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Usar TLS (recomendado)",
"settings.send_test_email": "Enviar correo de prueba",
"settings.send_test_email_hint": "⚠️ Por favor, guarde primero su configuración SMTP antes de enviar un correo de prueba.",
"settings.send_test_email_hint": "Por favor, guarde primero su configuración SMTP antes de enviar un correo de prueba.",
"settings.fail2ban": "Configuraciones Globales Predeterminadas de Fail2Ban",
"settings.fail2ban.description": "Estas configuraciones se aplicarán a todos los servidores Fail2Ban habilitados y se almacenarán en su sección [DEFAULT] de jail.local.",
"settings.enable_bantime_increment": "Habilitar incremento de Bantime",

View File

@@ -257,7 +257,7 @@
"settings.webhook.headers_hint": "Un en-tête par ligne au format Clé: Valeur.",
"settings.webhook.skip_tls": "Ignorer la vérification du certificat TLS",
"settings.webhook.test": "Envoyer un webhook de test",
"settings.webhook.test_hint": "⚠️ Veuillez enregistrer vos paramètres webhook avant de tester.",
"settings.webhook.test_hint": "Veuillez enregistrer vos paramètres webhook avant de tester.",
"settings.elasticsearch.title": "Configuration Elasticsearch",
"settings.elasticsearch.url": "URL Elasticsearch",
"settings.elasticsearch.url_placeholder": "https://elasticsearch.example.com:9200",
@@ -272,7 +272,7 @@
"settings.elasticsearch.password": "Mot de passe",
"settings.elasticsearch.skip_tls": "Ignorer la vérification du certificat TLS",
"settings.elasticsearch.test": "Tester la connexion",
"settings.elasticsearch.test_hint": "⚠️ Veuillez enregistrer vos paramètres Elasticsearch avant de tester.",
"settings.elasticsearch.test_hint": "Veuillez enregistrer vos paramètres Elasticsearch avant de tester.",
"settings.elasticsearch.help_title": "Guide de configuration Elasticsearch",
"settings.elasticsearch.help_template_title": "1. Créer un modèle d'index",
"settings.elasticsearch.help_template_desc": "Créez un modèle d'index dans Kibana Dev Tools ou via l'API pour que Elasticsearch mappe correctement tous les champs. Le modèle inclut les champs d'événement, les champs de log enrichis (HTTP, SSH, etc.) et les champs WHOIS analysés :",
@@ -299,7 +299,7 @@
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN est recommandé pour Office365/Gmail. PLAIN est l'authentification SMTP standard. CRAM-MD5 est basé sur challenge-response.",
"settings.smtp_insecure_skip_verify": "Ignorer la Vérification du Certificat TLS",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Non recommandé pour la production",
"settings.smtp_insecure_skip_verify_warning": "Non recommandé pour la production",
"settings.smtp_username": "Nom d'utilisateur SMTP",
"settings.smtp_username_placeholder": "par exemple, utilisateur@example.com",
"settings.smtp_password": "Mot de passe SMTP",
@@ -308,7 +308,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Utiliser TLS (recommandé)",
"settings.send_test_email": "Envoyer un email de test",
"settings.send_test_email_hint": "⚠️ Veuillez d'abord enregistrer vos paramètres SMTP avant d'envoyer un email de test.",
"settings.send_test_email_hint": "Veuillez d'abord enregistrer vos paramètres SMTP avant d'envoyer un email de test.",
"settings.fail2ban": "Configurations Globales par Défaut de Fail2Ban",
"settings.fail2ban.description": "Ces paramètres seront appliqués à tous les serveurs Fail2Ban activés et stockés dans leur section [DEFAULT] de jail.local.",
"settings.enable_bantime_increment": "Activer l'incrémentation du Bantime",

View File

@@ -257,7 +257,7 @@
"settings.webhook.headers_hint": "Un header per riga nel formato Chiave: Valore.",
"settings.webhook.skip_tls": "Ignora la verifica del certificato TLS",
"settings.webhook.test": "Invia webhook di test",
"settings.webhook.test_hint": "⚠️ Salva le impostazioni webhook prima di testare.",
"settings.webhook.test_hint": "Salva le impostazioni webhook prima di testare.",
"settings.elasticsearch.title": "Configurazione Elasticsearch",
"settings.elasticsearch.url": "URL Elasticsearch",
"settings.elasticsearch.url_placeholder": "https://elasticsearch.example.com:9200",
@@ -272,7 +272,7 @@
"settings.elasticsearch.password": "Password",
"settings.elasticsearch.skip_tls": "Ignora la verifica del certificato TLS",
"settings.elasticsearch.test": "Testa connessione",
"settings.elasticsearch.test_hint": "⚠️ Salva le impostazioni Elasticsearch prima di testare.",
"settings.elasticsearch.test_hint": "Salva le impostazioni Elasticsearch prima di testare.",
"settings.elasticsearch.help_title": "Guida alla configurazione Elasticsearch",
"settings.elasticsearch.help_template_title": "1. Creare un template di indice",
"settings.elasticsearch.help_template_desc": "Crea un template di indice in Kibana Dev Tools o tramite API per mappare correttamente tutti i campi in Elasticsearch. Il template include campi evento, campi log arricchiti (HTTP, SSH, ecc.) e campi WHOIS analizzati:",
@@ -299,7 +299,7 @@
"settings.smtp_auth_method_cram_md5": "CRAM-MD5",
"settings.smtp_auth_method_hint": "LOGIN è raccomandato per Office365/Gmail. PLAIN è l'autenticazione SMTP standard. CRAM-MD5 è basato su challenge-response.",
"settings.smtp_insecure_skip_verify": "Ignora Verifica Certificato TLS",
"settings.smtp_insecure_skip_verify_warning": "⚠️ Non raccomandato per la produzione",
"settings.smtp_insecure_skip_verify_warning": "Non raccomandato per la produzione",
"settings.smtp_username": "Nome utente SMTP",
"settings.smtp_username_placeholder": "es. utente@example.com",
"settings.smtp_password": "Password SMTP",
@@ -308,7 +308,7 @@
"settings.smtp_sender_placeholder": "noreply@swissmakers.ch",
"settings.smtp_tls": "Usa TLS (raccomandato)",
"settings.send_test_email": "Invia email di test",
"settings.send_test_email_hint": "⚠️ Si prega di salvare prima le impostazioni SMTP prima di inviare un'email di test.",
"settings.send_test_email_hint": "Si prega di salvare prima le impostazioni SMTP prima di inviare un'email di test.",
"settings.fail2ban": "Configurazioni Globali Predefinite di Fail2Ban",
"settings.fail2ban.description": "Queste impostazioni verranno applicate a tutti i server Fail2Ban abilitati e memorizzate nella loro sezione [DEFAULT] di jail.local.",
"settings.enable_bantime_increment": "Abilita incremento del Bantime",

View File

@@ -297,7 +297,7 @@ func BanNotificationHandler(c *gin.Context) {
log.Printf("----------------------------------------------------")
config.DebugLog("📩 Incoming Ban Notification: %s\n", string(body))
config.DebugLog("Incoming ban notification: %s\n", string(body))
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
@@ -320,7 +320,7 @@ func BanNotificationHandler(c *gin.Context) {
}
// Logs the parsed request
log.Printf("Parsed ban request - IP: %s, Jail: %s, Hostname: %s, Failures: %s",
log.Printf("Parsed ban request successfully - IP: %s, Jail: %s, Hostname: %s, Failures: %s",
request.IP, request.Jail, request.Hostname, request.Failures)
if err := integrations.ValidateIP(request.IP); err != nil {
@@ -376,7 +376,7 @@ func UnbanNotificationHandler(c *gin.Context) {
}
body, _ := io.ReadAll(c.Request.Body)
config.DebugLog("📩 Incoming unban notification: %s\n", string(body))
config.DebugLog("Incoming unban notification: %s\n", string(body))
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
@@ -394,7 +394,7 @@ func UnbanNotificationHandler(c *gin.Context) {
return
}
log.Printf("Parsed unban request - IP: %s, Jail: %s, Hostname: %s",
log.Printf("Parsed unban request successfully - IP: %s, Jail: %s, Hostname: %s",
request.IP, request.Jail, request.Hostname)
if err := integrations.ValidateIP(request.IP); err != nil {
@@ -1172,7 +1172,7 @@ func HandleUnbanNotification(ctx context.Context, server config.Fail2banServer,
}
if !settings.EmailAlertsForUnbans {
log.Printf("🔕 Alerts for unbans are disabled. No alert sent for IP %s", ip)
log.Printf("Alerts for unbans are disabled. No alert sent for IP %s", ip)
return nil
}
@@ -1182,7 +1182,7 @@ func HandleUnbanNotification(ctx context.Context, server config.Fail2banServer,
}
if !shouldAlertForCountry(country, settings.AlertCountries) {
log.Printf("🔕 IP %s belongs to %s, which is NOT in alert countries (%v). No alert sent.", ip, displayCountry, settings.AlertCountries)
log.Printf("IP %s belongs to %s, which is NOT in alert countries (%v). No alert sent.", ip, displayCountry, settings.AlertCountries)
return nil
}
@@ -1578,6 +1578,7 @@ func shouldAlertForCountry(country string, alertCountries []string) bool {
// Renders the main SPA page with template variables.
func renderIndexPage(c *gin.Context) {
disableExternalIP := os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "true" || os.Getenv("DISABLE_EXTERNAL_IP_LOOKUP") == "1"
autoDark := os.Getenv("AUTODARK") == "true" || os.Getenv("AUTODARK") == "1"
// Checks if OIDC is enabled and skip login page setting
oidcEnabled := auth.IsEnabled()
@@ -1598,6 +1599,7 @@ func renderIndexPage(c *gin.Context) {
"appVersion": version.Version,
"updateCheckEnabled": updateCheckEnabled,
"disableExternalIP": disableExternalIP,
"autoDark": autoDark,
"oidcEnabled": oidcEnabled,
"skipLoginPage": skipLoginPage,
})

View File

@@ -1,3 +1,460 @@
/* =========================================================================
Theme Tokens
========================================================================= */
:root {
/* Base theme scope (light/default)
--f2b-* -> component tokens used by custom Fail2Ban UI styles.
--f2b-ui-* -> utility-override tokens for tailwind classes. */
--f2b-page-bg: #f3f4f6;
--f2b-scrollbar-track: #f1f1f1;
--f2b-scrollbar-thumb: #888;
--f2b-scrollbar-thumb-hover: #555;
--f2b-select-border: #d1d5db;
--f2b-select-bg: #ffffff;
--f2b-modal-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--f2b-log-highlight-bg: #d97706;
--f2b-log-highlight-text: #fef3c7;
--f2b-tooltip-bg: #333;
--f2b-tooltip-text: #fff;
--f2b-mark-bg: #fef08a;
--f2b-mark-text: #111827;
--f2b-toast-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--f2b-toast-hover-shadow: 0 15px 20px -3px rgba(0, 0, 0, 0.15);
--f2b-toast-ban-bg: #7f1d1d;
--f2b-toast-ban-hover-bg: #991b1b;
--f2b-toast-unban-bg: #14532d;
--f2b-toast-unban-hover-bg: #166534;
--f2b-threat-border-strong: #cbd5e1;
--f2b-threat-border: #e5e7eb;
--f2b-threat-card-border: #d1d5db;
--f2b-threat-danger-border: #fca5a5;
--f2b-threat-safe-border: #86efac;
--f2b-threat-hero-bg: linear-gradient(120deg, #f8fafc 0%, #eef2ff 100%);
--f2b-threat-hero-danger-bg: linear-gradient(120deg, #fef2f2 0%, #ffe4e6 100%);
--f2b-threat-hero-safe-bg: linear-gradient(120deg, #f0fdf4 0%, #dcfce7 100%);
--f2b-threat-section-bg: #f9fafb;
--f2b-threat-card-bg: #ffffff;
--f2b-threat-danger-bg: #fef2f2;
--f2b-threat-safe-bg: #f0fdf4;
--f2b-threat-text-title: #0f172a;
--f2b-threat-text: #111827;
--f2b-threat-text-muted: #6b7280;
--f2b-threat-text-soft: #334155;
--f2b-threat-kicker: #64748b;
--f2b-threat-raw-bg: #111827;
--f2b-threat-raw-text: #e5e7eb;
/* Utility override tokens with light defaults. */
--f2b-ui-bg-surface: #ffffff;
--f2b-ui-bg-surface-soft: #f9fafb;
--f2b-ui-bg-surface-muted: #f3f4f6;
--f2b-ui-bg-gray-200: #e5e7eb;
--f2b-ui-bg-gray-300: #d1d5db;
--f2b-ui-bg-gray-600: #4b5563;
--f2b-ui-bg-blue-50: #eff6ff;
--f2b-ui-bg-blue-100: #dbeafe;
--f2b-ui-bg-blue-500: rgb(59 130 246);
--f2b-ui-bg-blue-600: rgb(37 99 235);
--f2b-ui-bg-blue-700: rgb(29 78 216);
--f2b-ui-bg-green-600: #16a34a;
--f2b-ui-bg-red-500: #ef4444;
--f2b-ui-bg-green-soft: #ecfdf5;
--f2b-ui-bg-amber-100: #fef3c7;
--f2b-ui-bg-yellow-soft: #fefce8;
--f2b-ui-bg-red-soft: #fef2f2;
--f2b-ui-bg-hover: #f3f4f6;
--f2b-ui-bg-blue-50-hover: #dbeafe;
--f2b-ui-bg-blue-500-hover: rgb(37 99 235);
--f2b-ui-bg-blue-600-hover: rgb(29 78 216);
--f2b-ui-bg-blue-700-hover: rgb(30 64 175);
--f2b-ui-border: #d1d5db;
--f2b-ui-text: #111827;
--f2b-ui-text-muted: #6b7280;
--f2b-ui-link: #2563eb;
--f2b-ui-link-hover: #1d4ed8;
--f2b-ui-text-blue-700: #1d4ed8;
--f2b-ui-text-blue-800: #1e40af;
--f2b-ui-text-amber-600: #d97706;
--f2b-ui-text-amber-700: #b45309;
--f2b-ui-text-yellow: #a16207;
--f2b-ui-text-yellow-800: #854d0e;
--f2b-ui-text-red: #b91c1c;
--f2b-ui-text-green: #166534;
--f2b-ui-text-red-600: #dc2626;
--f2b-ui-text-green-600: #16a34a;
--f2b-ui-input-bg: #ffffff;
--f2b-ui-input-placeholder: #9ca3af;
--f2b-ui-ring-offset: #ffffff;
--f2b-ui-ring-color: rgba(59, 130, 246, 0.5);
--f2b-ui-jail-toggle-track-bg: #e5e7eb;
--f2b-ui-jail-toggle-track-border: #d1d5db;
--f2b-ui-jail-toggle-track-checked-bg: #2563eb;
--f2b-ui-jail-toggle-track-checked-border: #3b82f6;
--f2b-ui-jail-toggle-thumb-bg: #ffffff;
--f2b-ui-jail-toggle-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
--f2b-ui-modal-backdrop: rgba(107, 114, 128, 0.75);
--f2b-ui-bg-yellow-500: #eab308;
--f2b-ui-bg-yellow-600-hover: #ca8a04;
--f2b-ui-bg-red-600: #dc2626;
--f2b-ui-bg-red-700-hover: #b91c1c;
--f2b-ui-footer-bg: #f9fafb;
--f2b-ui-code-text: #1f2937;
--f2b-ui-ws-tooltip-bg: #1f2937;
--f2b-ui-select2-choice-text: #ffffff;
--f2b-ui-select2-arrow: #6b7280;
--f2b-ui-select2-option-text: #111827;
--f2b-ui-select2-option-selected-bg: #e5e7eb;
--f2b-ui-select2-option-selected-text: #1f2937;
--f2b-ui-select2-option-highlight-bg: #2563eb;
--f2b-ui-select2-option-highlight-text: #ffffff;
--f2b-ui-theme-badge-warning-bg: #fef3c7;
--f2b-ui-theme-badge-warning-text: #92400e;
--f2b-ui-theme-badge-warning-hover-bg: #fde68a;
--f2b-ui-theme-badge-success-bg: #dcfce7;
--f2b-ui-theme-badge-success-text: #166534;
--f2b-ui-prism-bg: #f8fafc;
--f2b-ui-toast-success-bg: #047857;
--f2b-ui-toast-info-bg: #1d4ed8;
--f2b-ui-toast-warning-bg: #d97706;
}
html[data-theme="dark"] body:not(.lotr-mode) {
/* Dark theme scope.
Same two token groups as :root, but with dark values.
--f2b-* -> custom component tokens for dark mode
--f2b-ui-* -> utility class overrides for tailwind classes in dark mode */
--f2b-page-bg: #0b1220;
--f2b-scrollbar-track: #1f2937;
--f2b-scrollbar-thumb: #475569;
--f2b-scrollbar-thumb-hover: #64748b;
--f2b-select-border: #334155;
--f2b-select-bg: #111827;
--f2b-modal-shadow: 0 20px 38px -10px rgba(2, 6, 23, 0.72), 0 10px 18px -8px rgba(2, 6, 23, 0.58);
--f2b-log-highlight-bg: #92400e;
--f2b-log-highlight-text: #fde68a;
--f2b-tooltip-bg: #0f172a;
--f2b-tooltip-text: #e2e8f0;
--f2b-mark-bg: #92400e;
--f2b-mark-text: #fde68a;
--f2b-toast-shadow: 0 12px 24px -8px rgba(2, 6, 23, 0.6);
--f2b-toast-hover-shadow: 0 16px 28px -8px rgba(2, 6, 23, 0.68);
--f2b-toast-ban-bg: #7f1d1d;
--f2b-toast-ban-hover-bg: #991b1b;
--f2b-toast-unban-bg: #14532d;
--f2b-toast-unban-hover-bg: #166534;
--f2b-threat-border-strong: #334155;
--f2b-threat-border: #374151;
--f2b-threat-card-border: #475569;
--f2b-threat-danger-border: #7f1d1d;
--f2b-threat-safe-border: #166534;
--f2b-threat-hero-bg: linear-gradient(120deg, #111827 0%, #1e293b 100%);
--f2b-threat-hero-danger-bg: linear-gradient(120deg, #2d1117 0%, #3d1a1d 100%);
--f2b-threat-hero-safe-bg: linear-gradient(120deg, #0f2d27 0%, #123d33 100%);
--f2b-threat-section-bg: #1f2937;
--f2b-threat-card-bg: #111827;
--f2b-threat-danger-bg: #2d1117;
--f2b-threat-safe-bg: #0f2d27;
--f2b-threat-text-title: #e2e8f0;
--f2b-threat-text: #e5e7eb;
--f2b-threat-text-muted: #94a3b8;
--f2b-threat-text-soft: #cbd5e1;
--f2b-threat-kicker: #93c5fd;
--f2b-threat-raw-bg: #020617;
--f2b-threat-raw-text: #cbd5e1;
--f2b-ui-bg-surface: #151e30;
/* --f2b-ui-bg-surface-soft: #192639; */
--f2b-ui-bg-surface-soft: #1f2a41;
--f2b-ui-bg-surface-muted: #223248;
--f2b-ui-bg-gray-200: #344b68;
--f2b-ui-bg-gray-300: #3e5674;
--f2b-ui-bg-gray-600: #4d6078;
--f2b-ui-bg-blue-50: #1d314b;
--f2b-ui-bg-blue-100: #233a58;
--f2b-ui-bg-blue-500: rgb(52 85 150);
--f2b-ui-bg-blue-600: rgb(45 73 134);
--f2b-ui-bg-blue-700: rgb(39 64 118);
--f2b-ui-bg-green-600: #1a4129;
--f2b-ui-bg-red-500: #813535;
--f2b-ui-bg-green-soft: #113329;
--f2b-ui-bg-amber-100: #4a3310;
--f2b-ui-bg-yellow-soft: #3f3113;
--f2b-ui-bg-red-soft: #3a1a20;
--f2b-ui-bg-hover: #253a55;
--f2b-ui-bg-blue-50-hover: #2a4566;
--f2b-ui-bg-blue-500-hover: rgb(45 73 134);
--f2b-ui-bg-blue-600-hover: rgb(39 64 118);
--f2b-ui-bg-blue-700-hover: rgb(33 56 103);
--f2b-ui-border: #334a67;
--f2b-ui-text: #e6edf7;
--f2b-ui-text-muted: #9cb1c9;
--f2b-ui-link: #7cc2ff;
--f2b-ui-link-hover: #a9d7ff;
--f2b-ui-text-blue-700: #93c5fd;
--f2b-ui-text-blue-800: #bfdbfe;
--f2b-ui-text-amber-600: #f59e0b;
--f2b-ui-text-amber-700: #fcd34d;
--f2b-ui-text-yellow: #fcd34d;
--f2b-ui-text-yellow-800: #fde68a;
--f2b-ui-text-red: #fca5a5;
--f2b-ui-text-green: #86efac;
--f2b-ui-text-red-600: #fca5a5;
--f2b-ui-text-green-600: #86efac;
--f2b-ui-input-bg: #101a2a;
--f2b-ui-input-placeholder: #6e88a5;
--f2b-ui-ring-offset: #101a2a;
--f2b-ui-ring-color: rgba(96, 165, 250, 0.45);
--f2b-ui-jail-toggle-track-bg: #2a3d58;
--f2b-ui-jail-toggle-track-border: #456182;
--f2b-ui-jail-toggle-track-checked-bg: #1d4ed8;
--f2b-ui-jail-toggle-track-checked-border: #3b82f6;
--f2b-ui-jail-toggle-thumb-bg: #e6edf7;
--f2b-ui-jail-toggle-thumb-shadow: 0 1px 3px rgba(2, 6, 23, 0.6);
--f2b-ui-modal-backdrop: rgba(2, 6, 23, 0.76);
--f2b-ui-bg-yellow-500: #a16207;
--f2b-ui-bg-yellow-600-hover: #ca8a04;
--f2b-ui-bg-red-600: #991b1b;
--f2b-ui-bg-red-700-hover: #b91c1c;
--f2b-ui-footer-bg: #121c2d;
--f2b-ui-code-text: #dbe7f4;
--f2b-ui-ws-tooltip-bg: #0f172a;
--f2b-ui-select2-choice-text: #eaf2ff;
--f2b-ui-select2-arrow: #9cb1c9;
--f2b-ui-select2-option-text: #d3dfec;
--f2b-ui-select2-option-selected-bg: #22344e;
--f2b-ui-select2-option-selected-text: #9ecaff;
--f2b-ui-select2-option-highlight-bg: #1d4ed8;
--f2b-ui-select2-option-highlight-text: #fff;
--f2b-ui-theme-badge-warning-bg: #78350f;
--f2b-ui-theme-badge-warning-text: #fcd34d;
--f2b-ui-theme-badge-warning-hover-bg: #92400e;
--f2b-ui-theme-badge-success-bg: #14532d;
--f2b-ui-theme-badge-success-text: #bbf7d0;
--f2b-ui-prism-bg: #020617;
--f2b-ui-toast-success-bg: #14532d;
--f2b-ui-toast-info-bg: #1e40af;
--f2b-ui-toast-warning-bg: #a16207;
background-color: var(--f2b-page-bg);
color: var(--f2b-ui-text);
}
html[data-theme="dark"] body:not(.lotr-mode) #mainContent,
html[data-theme="dark"] body:not(.lotr-mode) #loginPage {
background-color: transparent;
}
html[data-theme="dark"] body:not(.lotr-mode) .bg-white { background-color: var(--f2b-ui-bg-surface) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-gray-50 { background-color: var(--f2b-ui-bg-surface-soft) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-gray-100 { background-color: var(--f2b-ui-bg-surface-muted) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-gray-200 { background-color: var(--f2b-ui-bg-gray-200) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-gray-300 { background-color: var(--f2b-ui-bg-gray-300) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-gray-600 { background-color: var(--f2b-ui-bg-gray-600) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-blue-50 { background-color: var(--f2b-ui-bg-blue-50) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-blue-100 { background-color: var(--f2b-ui-bg-blue-100) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-blue-500 { background-color: var(--f2b-ui-bg-blue-500) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-blue-600 { background-color: var(--f2b-ui-bg-blue-600) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-blue-700 { background-color: var(--f2b-ui-bg-blue-700) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-red-500 { background-color: var(--f2b-ui-bg-red-500) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-green-600 { background-color: var(--f2b-ui-bg-green-600) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-amber-100 { background-color: var(--f2b-ui-bg-amber-100) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-green-50,
html[data-theme="dark"] body:not(.lotr-mode) .bg-green-100 { background-color: var(--f2b-ui-bg-green-soft) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-yellow-50,
html[data-theme="dark"] body:not(.lotr-mode) .bg-yellow-100 { background-color: var(--f2b-ui-bg-yellow-soft) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-red-50,
html[data-theme="dark"] body:not(.lotr-mode) .bg-red-100 { background-color: var(--f2b-ui-bg-red-soft) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .border-gray-100,
html[data-theme="dark"] body:not(.lotr-mode) .border-gray-200,
html[data-theme="dark"] body:not(.lotr-mode) .border-gray-300 {
border-color: var(--f2b-ui-border) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .text-gray-900,
html[data-theme="dark"] body:not(.lotr-mode) .text-gray-800,
html[data-theme="dark"] body:not(.lotr-mode) .text-gray-700 { color: var(--f2b-ui-text) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-gray-600,
html[data-theme="dark"] body:not(.lotr-mode) .text-gray-500,
html[data-theme="dark"] body:not(.lotr-mode) .text-gray-400 { color: var(--f2b-ui-text-muted) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-blue-600 { color: var(--f2b-ui-link) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-blue-700 { color: var(--f2b-ui-text-blue-700) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-blue-800 { color: var(--f2b-ui-text-blue-800) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:text-blue-800:hover { color: var(--f2b-ui-link-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-amber-600 { color: var(--f2b-ui-text-amber-600) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-amber-700 { color: var(--f2b-ui-text-amber-700) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-yellow-700 { color: var(--f2b-ui-text-yellow) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-yellow-800 { color: var(--f2b-ui-text-yellow-800) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-red-700,
html[data-theme="dark"] body:not(.lotr-mode) .text-red-800 { color: var(--f2b-ui-text-red) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-green-800 { color: var(--f2b-ui-text-green) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-red-600 { color: var(--f2b-ui-text-red-600) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .text-green-600 { color: var(--f2b-ui-text-green-600) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-gray-50:hover,
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-gray-100:hover,
html[data-theme="dark"] body:not(.lotr-mode) tr.hover\:bg-gray-50:hover {
background-color: var(--f2b-ui-bg-hover) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-blue-50:hover { background-color: var(--f2b-ui-bg-blue-50-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-blue-500:hover { background-color: var(--f2b-ui-bg-blue-500-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-blue-600:hover { background-color: var(--f2b-ui-bg-blue-600-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-blue-700:hover { background-color: var(--f2b-ui-bg-blue-700-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:text-gray-600:hover { color: var(--f2b-ui-text) !important; }
html[data-theme="dark"] body:not(.lotr-mode) input,
html[data-theme="dark"] body:not(.lotr-mode) textarea,
html[data-theme="dark"] body:not(.lotr-mode) select {
background-color: var(--f2b-ui-input-bg);
color: var(--f2b-ui-text);
border-color: var(--f2b-ui-border);
}
html[data-theme="dark"] body:not(.lotr-mode) input::placeholder,
html[data-theme="dark"] body:not(.lotr-mode) textarea::placeholder { color: var(--f2b-ui-input-placeholder); }
html[data-theme="dark"] body:not(.lotr-mode) .focus\:ring-offset-2:focus,
html[data-theme="dark"] body:not(.lotr-mode) .focus\:ring-offset-white:focus {
--tw-ring-offset-color: var(--f2b-ui-ring-offset);
}
html[data-theme="dark"] body:not(.lotr-mode) .focus\:ring-blue-500:focus {
--tw-ring-color: var(--f2b-ui-ring-color);
}
html[data-theme="dark"] body:not(.lotr-mode) .jail-toggle-track {
background-color: var(--f2b-ui-jail-toggle-track-bg) !important;
border: 1px solid var(--f2b-ui-jail-toggle-track-border);
}
html[data-theme="dark"] body:not(.lotr-mode) .peer:checked + .jail-toggle-track {
background-color: var(--f2b-ui-jail-toggle-track-checked-bg) !important;
border-color: var(--f2b-ui-jail-toggle-track-checked-border);
}
html[data-theme="dark"] body:not(.lotr-mode) .jail-toggle-thumb {
background-color: var(--f2b-ui-jail-toggle-thumb-bg) !important;
box-shadow: var(--f2b-ui-jail-toggle-thumb-shadow);
}
html[data-theme="dark"] body:not(.lotr-mode) .modal-content {
background-color: var(--f2b-ui-bg-surface) !important;
border: 1px solid var(--f2b-ui-border);
}
html[data-theme="dark"] body:not(.lotr-mode) [aria-hidden="true"].fixed.inset-0.bg-gray-500.opacity-75 {
background-color: var(--f2b-ui-modal-backdrop) !important;
opacity: 1 !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .bg-yellow-500 { background-color: var(--f2b-ui-bg-yellow-500) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-yellow-600:hover { background-color: var(--f2b-ui-bg-yellow-600-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .bg-red-600 { background-color: var(--f2b-ui-bg-red-600) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .hover\:bg-red-700:hover { background-color: var(--f2b-ui-bg-red-700-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) #footer {
background-color: var(--f2b-ui-footer-bg) !important;
border-top: 1px solid var(--f2b-ui-border);
}
html[data-theme="dark"] body:not(.lotr-mode) #footer .text-gray-600 { color: var(--f2b-ui-text-muted) !important; }
html[data-theme="dark"] body:not(.lotr-mode) #footer a { color: var(--f2b-ui-link) !important; }
html[data-theme="dark"] body:not(.lotr-mode) #footer a:hover { color: var(--f2b-ui-link-hover) !important; }
html[data-theme="dark"] body:not(.lotr-mode) pre,
html[data-theme="dark"] body:not(.lotr-mode) code { color: var(--f2b-ui-code-text); }
html[data-theme="dark"] body:not(.lotr-mode) #wsTooltip {
background: var(--f2b-ui-ws-tooltip-bg) !important;
border-color: var(--f2b-ui-border) !important;
color: var(--f2b-ui-text) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-selection--single,
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-selection--multiple {
background-color: var(--f2b-ui-input-bg);
border-color: var(--f2b-ui-border);
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-selection--single .select2-selection__rendered,
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-selection--multiple .select2-selection__rendered {
color: var(--f2b-ui-text);
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-selection--multiple .select2-selection__choice {
background-color: var(--f2b-ui-bg-blue-600);
border-color: var(--f2b-ui-bg-blue-600);
color: var(--f2b-ui-select2-choice-text);
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-selection--single .select2-selection__arrow b {
border-color: var(--f2b-ui-select2-arrow) transparent transparent transparent;
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-dropdown {
background-color: var(--f2b-ui-input-bg);
border-color: var(--f2b-ui-border);
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-results__option {
color: var(--f2b-ui-select2-option-text);
background-color: transparent;
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-results__option[aria-selected=true] {
background-color: var(--f2b-ui-select2-option-selected-bg);
color: var(--f2b-ui-select2-option-selected-text);
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: var(--f2b-ui-select2-option-highlight-bg);
color: var(--f2b-ui-select2-option-highlight-text);
}
html[data-theme="dark"] body:not(.lotr-mode) .select2-container--default .select2-search--dropdown .select2-search__field {
background-color: var(--f2b-ui-ws-tooltip-bg);
border-color: var(--f2b-ui-border);
color: var(--f2b-ui-text);
}
html[data-theme="dark"] body:not(.lotr-mode) .theme-badge-warning {
background-color: var(--f2b-ui-theme-badge-warning-bg) !important;
color: var(--f2b-ui-theme-badge-warning-text) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .theme-badge-warning:hover { background-color: var(--f2b-ui-theme-badge-warning-hover-bg) !important; }
html[data-theme="dark"] body:not(.lotr-mode) .theme-badge-success {
background-color: var(--f2b-ui-theme-badge-success-bg) !important;
color: var(--f2b-ui-theme-badge-success-text) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .toast-success {
background-color: var(--f2b-ui-toast-success-bg) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .toast-info {
background-color: var(--f2b-ui-toast-info-bg) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) .toast-warning {
background-color: var(--f2b-ui-toast-warning-bg) !important;
}
html[data-theme="dark"] body:not(.lotr-mode) pre[class*="language-"],
html[data-theme="dark"] body:not(.lotr-mode) code[class*="language-"] { background: var(--f2b-ui-prism-bg); }
html[data-theme="dark"] body:not(.lotr-mode) .token.operator,
html[data-theme="dark"] body:not(.lotr-mode) .token.entity,
html[data-theme="dark"] body:not(.lotr-mode) .token.url,
html[data-theme="dark"] body:not(.lotr-mode) .language-css .token.string,
html[data-theme="dark"] body:not(.lotr-mode) .style .token.string { background: transparent; }
/* =========================================================================
Loading Overlay
========================================================================= */
@@ -33,7 +490,7 @@
display: flex;
align-items: center;
justify-content: center;
background-color: #f3f4f6;
background-color: var(--f2b-page-bg);
padding: 3rem 1rem;
position: relative;
z-index: 1;
@@ -66,7 +523,7 @@ body[data-skip-login-page="true"] #loginPage {
}
body:has(#loginPage:not(.hidden)) {
background-color: #f3f4f6;
background-color: var(--f2b-page-bg);
overflow: hidden;
}
@@ -88,16 +545,16 @@ body:has(#loginPage:not(.hidden)) {
}
::-webkit-scrollbar-track {
background: #f1f1f1;
background: var(--f2b-scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: #888;
background: var(--f2b-scrollbar-thumb);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
background: var(--f2b-scrollbar-thumb-hover);
}
/* =========================================================================
@@ -105,10 +562,11 @@ body:has(#loginPage:not(.hidden)) {
========================================================================= */
.select2-container--default .select2-selection--multiple {
border: 1px solid #d1d5db;
border: 1px solid var(--f2b-select-border);
border-radius: 0.375rem;
padding: 0.25rem 0.5rem;
min-height: 42px;
background-color: var(--f2b-select-bg);
}
.select2-container--default .select2-selection--multiple .select2-selection__choice {
@@ -129,7 +587,16 @@ body:has(#loginPage:not(.hidden)) {
========================================================================= */
.modal-content {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
box-shadow: var(--f2b-modal-shadow);
}
.logs-highlighted-line {
display: block;
background-color: var(--f2b-log-highlight-bg);
color: var(--f2b-log-highlight-text);
padding: 0.25rem 0.5rem;
margin: 0.125rem 0;
border-radius: 0.25rem;
}
body.modal-open {
@@ -143,21 +610,21 @@ body.modal-open {
}
.threat-intel-hero {
border: 1px solid #cbd5e1;
border: 1px solid var(--f2b-threat-border-strong);
border-radius: 0.75rem;
background: linear-gradient(120deg, #f8fafc 0%, #eef2ff 100%);
background: var(--f2b-threat-hero-bg);
padding: 1rem;
margin-bottom: 0.9rem;
}
.threat-intel-hero.threat-intel-card-danger {
border-color: #fca5a5;
background: linear-gradient(120deg, #fef2f2 0%, #ffe4e6 100%);
border-color: var(--f2b-threat-danger-border);
background: var(--f2b-threat-hero-danger-bg);
}
.threat-intel-hero.threat-intel-card-safe {
border-color: #86efac;
background: linear-gradient(120deg, #f0fdf4 0%, #dcfce7 100%);
border-color: var(--f2b-threat-safe-border);
background: var(--f2b-threat-hero-safe-bg);
}
.threat-intel-hero-main {
@@ -168,7 +635,7 @@ body.modal-open {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #64748b;
color: var(--f2b-threat-kicker);
font-weight: 700;
}
@@ -176,14 +643,14 @@ body.modal-open {
margin-top: 0.25rem;
font-size: 1.35rem;
font-weight: 700;
color: #0f172a;
color: var(--f2b-threat-text-title);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.threat-intel-hero-subtitle {
margin-top: 0.25rem;
font-size: 0.88rem;
color: #334155;
color: var(--f2b-threat-text-soft);
}
.threat-intel-priority-grid {
@@ -193,23 +660,23 @@ body.modal-open {
}
.threat-intel-section {
border: 1px solid #e5e7eb;
border: 1px solid var(--f2b-threat-border);
border-radius: 0.5rem;
background: #f9fafb;
background: var(--f2b-threat-section-bg);
padding: 1rem;
margin-bottom: 0.9rem;
}
.threat-intel-section h4 {
font-weight: 600;
color: #111827;
color: var(--f2b-threat-text);
margin-bottom: 0.75rem;
}
.threat-intel-card {
border: 1px solid #d1d5db;
border: 1px solid var(--f2b-threat-card-border);
border-radius: 0.5rem;
background: #ffffff;
background: var(--f2b-threat-card-bg);
padding: 0.75rem;
}
@@ -217,13 +684,13 @@ body.modal-open {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #6b7280;
color: var(--f2b-threat-text-muted);
}
.threat-intel-card-value {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
color: var(--f2b-threat-text);
margin-top: 0.25rem;
}
@@ -233,19 +700,19 @@ body.modal-open {
.threat-intel-card-text {
font-size: 0.95rem;
color: #111827;
color: var(--f2b-threat-text);
margin-top: 0.25rem;
overflow-wrap: anywhere;
}
.threat-intel-card-danger {
border-color: #fca5a5;
background: #fef2f2;
border-color: var(--f2b-threat-danger-border);
background: var(--f2b-threat-danger-bg);
}
.threat-intel-card-safe {
border-color: #86efac;
background: #f0fdf4;
border-color: var(--f2b-threat-safe-border);
background: var(--f2b-threat-safe-bg);
}
.threat-intel-list {
@@ -262,19 +729,19 @@ body.modal-open {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid #e5e7eb;
border: 1px solid var(--f2b-threat-border);
border-radius: 0.375rem;
background: #ffffff;
background: var(--f2b-threat-card-bg);
padding: 0.4rem 0.65rem;
font-size: 0.85rem;
}
.threat-intel-raw {
margin-top: 0.75rem;
border: 1px solid #d1d5db;
border: 1px solid var(--f2b-threat-card-border);
border-radius: 0.5rem;
background: #111827;
color: #e5e7eb;
background: var(--f2b-threat-raw-bg);
color: var(--f2b-threat-raw-text);
padding: 0.75rem;
font-size: 0.8rem;
white-space: pre-wrap;
@@ -293,8 +760,8 @@ body.modal-open {
.tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: #333;
color: #fff;
background-color: var(--f2b-tooltip-bg);
color: var(--f2b-tooltip-text);
text-align: center;
border-radius: 6px;
padding: 5px;
@@ -317,7 +784,8 @@ body.modal-open {
========================================================================= */
mark {
background-color: #fef08a;
background-color: var(--f2b-mark-bg);
color: var(--f2b-mark-text);
padding: 0.1em 0em 0.1em 0.2em;
border-radius: 0.25em;
}
@@ -345,7 +813,7 @@ mark {
color: #fff;
pointer-events: auto;
font-weight: 500;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
box-shadow: var(--f2b-toast-shadow);
opacity: 0;
transform: translateY(-6px);
transition: opacity 0.25s ease, transform 0.25s ease;
@@ -373,27 +841,27 @@ mark {
}
.toast-ban-event {
background-color: #7f1d1d;
background-color: var(--f2b-toast-ban-bg);
pointer-events: auto;
cursor: pointer;
}
.toast-ban-event:hover {
background-color: #991b1b;
background-color: var(--f2b-toast-ban-hover-bg);
transform: translateY(-2px);
box-shadow: 0 15px 20px -3px rgba(0, 0, 0, 0.15);
box-shadow: var(--f2b-toast-hover-shadow);
}
.toast-unban-event {
background-color: #14532d;
background-color: var(--f2b-toast-unban-bg);
pointer-events: auto;
cursor: pointer;
}
.toast-unban-event:hover {
background-color: #166534;
background-color: var(--f2b-toast-unban-hover-bg);
transform: translateY(-2px);
box-shadow: 0 15px 20px -3px rgba(0, 0, 0, 0.15);
box-shadow: var(--f2b-toast-hover-shadow);
}
/* =========================================================================

View File

@@ -368,7 +368,7 @@ function renderDashboard() {
+ ' </div>'
+ ' </div>'
+ ' </div>'
+ ' <div id="manualBlockFormContainer" class="hidden mt-4">'
+ ' <div id="manualBlockFormContainer" class="hidden" style="margin-top: 35px;">'
+ ' <form id="manualBlockForm" onsubmit="return false;">'
+ ' <div class="grid grid-cols-1 md:grid-cols-3 gap-4">'
+ ' <div>'

View File

@@ -120,6 +120,11 @@ function renderInsightsGlobe() {
countries.forEach(function(s) {
if (s.count > maxCount) maxCount = s.count;
});
var isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark'
&& !(document.body && document.body.classList.contains('lotr-mode'));
var labelBg = isDarkTheme ? 'rgba(15,23,42,0.92)' : 'rgba(248,250,252,0.97)';
var labelColor = isDarkTheme ? '#f1f5f9' : '#0f172a';
var labelBorder = isDarkTheme ? '1px solid rgba(148,163,184,0.25)' : '1px solid rgba(100,116,139,0.35)';
var points = [];
countries.forEach(function(s) {
@@ -133,7 +138,7 @@ function renderInsightsGlobe() {
alt: 0.06 + 0.7 * ratio,
radius: 0.4 + 0.6 * ratio,
color: _threatColor(ratio),
label: '<div style="padding:6px 10px;background:rgba(15,23,42,0.92);color:#f1f5f9;' +
label: '<div style="padding:6px 10px;background:' + labelBg + ';color:' + labelColor + ';border:' + labelBorder + ';' +
'border-radius:6px;font-size:13px;line-height:1.4;pointer-events:none;">' +
'<b>' + escapeHtml(s.country || '??') + '</b><br>' +
formatNumber(s.count) + ' ban' + (s.count !== 1 ? 's' : '') + '</div>',

View File

@@ -6,6 +6,9 @@
// =========================================================================
window.addEventListener('DOMContentLoaded', function() {
if (typeof initThemeManager === 'function') {
initThemeManager();
}
showLoading(true);
if (typeof checkAuthStatus === 'function') {
checkAuthStatus().then(function(authStatus) {
@@ -87,9 +90,9 @@ function initializeApp() {
? translations['footer.update_available'].replace('{version}', data.latest_version || '')
: ('Update available: v' + (data.latest_version || ''));
if (data.update_available && data.latest_version) {
versionContainer.innerHTML = '<a href="https://github.com/swissmakers/fail2ban-ui/releases" target="_blank" rel="noopener" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" title="' + updateHint + '">' + updateHint + '</a>';
versionContainer.innerHTML = '<a href="https://github.com/swissmakers/fail2ban-ui/releases" target="_blank" rel="noopener" class="theme-badge-warning inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200" title="' + updateHint + '">' + updateHint + '</a>';
} else {
versionContainer.innerHTML = '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="' + latestLabel + '">' + latestLabel + '</span>';
versionContainer.innerHTML = '<span class="theme-badge-success inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" title="' + latestLabel + '">' + latestLabel + '</span>';
}
})
.catch(function() { });

View File

@@ -28,8 +28,14 @@ function applyLOTRTheme(active) {
lotrCSS.disabled = true;
}
isLOTRModeActive = false;
if (typeof syncSystemTheme === 'function') {
syncSystemTheme();
}
console.log('🎭 LOTR Mode Deactivated');
}
if (active && typeof syncSystemTheme === 'function') {
syncSystemTheme();
}
void body.offsetHeight;
}

View File

@@ -88,7 +88,7 @@ function openLogsModal(eventIndex) {
for (var j = 0; j < logLines.length; j++) {
var safeLine = escapeHtml(logLines[j] || '');
if (highlightMap[j]) {
html += '<span style="display: block; background-color: #d97706; color: #fef3c7; padding: 0.25rem 0.5rem; margin: 0.125rem 0; border-radius: 0.25rem;">' + safeLine + '</span>';
html += '<span class="logs-highlighted-line">' + safeLine + '</span>';
} else {
html += safeLine + '\n';
}
@@ -336,10 +336,10 @@ function openManageJailsModal() {
+ isEnabled
+ ' />'
+ ' <div'
+ ' class="w-11 h-6 bg-gray-200 rounded-full peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:bg-blue-600 transition-colors"'
+ ' class="jail-toggle-track w-11 h-6 bg-gray-200 rounded-full peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:bg-blue-600 transition-colors"'
+ ' ></div>'
+ ' <span'
+ ' class="absolute left-1 top-1/2 -translate-y-1/2 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"'
+ ' class="jail-toggle-thumb absolute left-1 top-1/2 -translate-y-1/2 bg-white w-4 h-4 rounded-full transition-transform peer-checked:translate-x-5"'
+ ' ></span>'
+ ' </label>'
+ ' </div>'

View File

@@ -0,0 +1,49 @@
"use strict";
var themeMediaQuery = null;
function isAutoDarkEnabled() {
return document.documentElement.getAttribute('data-auto-dark') === 'true';
}
function getSystemTheme() {
if (!isAutoDarkEnabled()) {
return 'light';
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function applyTheme(theme) {
var resolvedTheme = theme === 'dark' ? 'dark' : 'light';
var root = document.documentElement;
root.setAttribute('data-theme', resolvedTheme);
}
function syncSystemTheme() {
if (document.body && document.body.classList.contains('lotr-mode')) {
return;
}
applyTheme(getSystemTheme());
}
function initThemeManager() {
syncSystemTheme();
if (!isAutoDarkEnabled()) {
return;
}
if (!window.matchMedia || themeMediaQuery) {
return;
}
themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
if (typeof themeMediaQuery.addEventListener === 'function') {
themeMediaQuery.addEventListener('change', syncSystemTheme);
}
}
window.initThemeManager = initThemeManager;
window.syncSystemTheme = syncSystemTheme;

View File

@@ -16,12 +16,24 @@
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-auto-dark="{{if .autoDark}}true{{else}}false{{end}}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title data-i18n="page.title">Fail2ban UI Dashboard</title>
<script>
(function() {
var theme = 'light';
var autoDark = document.documentElement.getAttribute('data-auto-dark') === 'true';
try {
if (autoDark && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
}
} catch (e) { }
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<link rel="stylesheet" href="/static/vendor/prism/prism-tomorrow.min.css?v={{.version}}" />
<script src="/static/vendor/prism/prism-core.min.js?v={{.version}}"></script>
<script src="/static/vendor/prism/prism-autoloader.min.js?v={{.version}}"></script>
@@ -238,7 +250,7 @@
</div>
<p class="text-xs text-gray-500 mb-2" data-i18n="filter_debug.filter_content_hint_readonly">Filter content is shown read-only. Click 'Edit' to modify for testing. Changes are temporary and not saved.</p>
<p class="text-xs text-gray-500 mb-2 hidden" id="filterContentHintEditable" data-i18n="filter_debug.filter_content_hint">Edit the filter regex below for testing. Changes are temporary and not saved.</p>
<textarea id="filterContentTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40 font-mono text-sm bg-gray-50"
<textarea id="filterContentTextarea" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 h-40 font-mono text-sm bg-gray-50" style="min-height: 300px;"
placeholder="Filter content will appear here when a filter is selected..." readonly></textarea>
</div>
<div class="mb-4">
@@ -788,11 +800,11 @@
<div class="flex items-center mb-4">
<input type="checkbox" id="smtpInsecureSkipVerify" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed">
<label for="smtpInsecureSkipVerify" class="ml-2 block text-sm text-gray-700" data-i18n="settings.smtp_insecure_skip_verify">Skip TLS Certificate Verification</label>
<span class="ml-2 text-xs text-red-600" data-i18n="settings.smtp_insecure_skip_verify_warning">⚠️ Not recommended for production</span>
<span class="ml-2 text-xs text-red-600" data-i18n="settings.smtp_insecure_skip_verify_warning">Not recommended for production</span>
</div>
<div class="mb-4">
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed" onclick="sendTestEmail()" id="sendTestEmailBtn" data-i18n="settings.send_test_email">Send Test Email</button>
<p class="mt-2 text-xs text-amber-600" data-i18n="settings.send_test_email_hint">⚠️ Please save your SMTP settings first before sending a test email.</p>
<p class="mt-2 text-xs text-gray-500" data-i18n="settings.send_test_email_hint">Please save your SMTP settings first before sending a test email.</p>
</div>
</div>
</div>
@@ -819,11 +831,10 @@
<div class="flex items-center mb-4">
<input type="checkbox" id="webhookSkipTLS" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out">
<label for="webhookSkipTLS" class="ml-2 block text-sm text-gray-700" data-i18n="settings.webhook.skip_tls">Skip TLS Certificate Verification</label>
<span class="ml-2 text-xs text-red-600" data-i18n="settings.smtp_insecure_skip_verify_warning">⚠️ Not recommended for production</span>
</div>
<div class="mb-4">
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors" onclick="sendTestWebhook()" id="sendTestWebhookBtn" data-i18n="settings.webhook.test">Send Test Webhook</button>
<p class="mt-2 text-xs text-amber-600" data-i18n="settings.webhook.test_hint">⚠️ Please save your webhook settings first before testing.</p>
<p class="mt-2 text-xs text-gray-500" data-i18n="settings.webhook.test_hint">Please save your webhook settings first before testing.</p>
</div>
</div>
@@ -858,11 +869,10 @@
<div class="flex items-center mb-4">
<input type="checkbox" id="elasticsearchSkipTLS" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out">
<label for="elasticsearchSkipTLS" class="ml-2 block text-sm text-gray-700" data-i18n="settings.elasticsearch.skip_tls">Skip TLS Certificate Verification</label>
<span class="ml-2 text-xs text-red-600" data-i18n="settings.smtp_insecure_skip_verify_warning">⚠️ Not recommended for production</span>
</div>
<div class="mb-4">
<button type="button" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors" onclick="sendTestElasticsearch()" id="sendTestElasticsearchBtn" data-i18n="settings.elasticsearch.test">Test Connection</button>
<p class="mt-2 text-xs text-amber-600" data-i18n="settings.elasticsearch.test_hint">⚠️ Please save your Elasticsearch settings first before testing.</p>
<p class="mt-2 text-xs text-gray-500" data-i18n="settings.elasticsearch.test_hint">Please save your Elasticsearch settings first before testing.</p>
</div>
</div>
@@ -996,7 +1006,7 @@
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2" data-i18n="settings.ignore_ips">Ignore IPs</label>
<p class="text-xs text-gray-500 mb-2" data-i18n="settings.ignore_ips.description">Space separated list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which matches an address in this list.</p>
<div class="border border-gray-300 rounded-md p-2 min-h-[60px] bg-gray-50" id="ignoreIPsContainer">
<div class="border border-gray-300 rounded-md p-2 min-h-[60px]" id="ignoreIPsContainer">
<div id="ignoreIPsTags" class="flex flex-wrap gap-2 mb-2"></div>
<input type="text" id="ignoreIPInput" class="w-full border-0 bg-transparent focus:outline-none focus:ring-0 text-sm"
data-i18n-placeholder="settings.ignore_ips_placeholder" placeholder="Enter IP address and press Enter" />
@@ -1013,7 +1023,7 @@
<!-- ******************************************************************* -->
<!-- Footer -->
<!-- ******************************************************************* -->
<footer id="footer" class="hidden bg-gray-100 py-4">
<footer id="footer" class="hidden bg-gray-100 py-4" style="display:none;">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-gray-600 text-sm">
<p class="mb-0">
Fail2ban-UI v<span id="footer-app-version">{{.appVersion}}</span>
@@ -1584,7 +1594,7 @@
<div id="elasticsearchHelpModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="relative flex min-h-full w-full items-center justify-center p-4 sm:p-6">
<div class="fixed inset-0 bg-gray-500 opacity-75" aria-hidden="true"></div>
<div class="modal-content relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 680px;">
<div class="modal-content relative z-10 w-full rounded-lg bg-white text-left shadow-xl transition-all" style="max-width: 980px;">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
@@ -1792,6 +1802,7 @@
<script src="/static/vendor/select2/select2.min.js?v={{.version}}"></script>
<script src="/static/vendor/globe/globe.gl.min.js?v={{.version}}"></script>
<script src="/static/js/globals.js?v={{.version}}"></script>
<script src="/static/js/theme.js?v={{.version}}"></script>
<script src="/static/js/core.js?v={{.version}}"></script>
<script src="/static/js/api.js?v={{.version}}"></script>
<script src="/static/js/utils.js?v={{.version}}"></script>