diff --git a/internal/locales/de.json b/internal/locales/de.json index 4f30e0f..d575892 100644 --- a/internal/locales/de.json +++ b/internal/locales/de.json @@ -351,6 +351,8 @@ "servers.form.no_keys": "Keine SSH-Schlüssel gefunden; Pfad manuell eingeben", "filter_debug.not_available": "Filter-Debug ist nur verfügbar, wenn mindestens ein registrierter Fail2ban-Server aktiviert ist.", "filter_debug.local_missing": "Das lokale Fail2ban-Filterverzeichnis wurde auf diesem Host nicht gefunden.", + "filter_debug.save_success": "Filter- und Jail-Konfiguration gespeichert und Fail2ban neu geladen", + "filter_debug.save_reload_warning": "Konfiguration gespeichert, aber Fail2ban-Reload fehlgeschlagen", "email.ban.title": "Achtung: Fail2Ban hat eine neue IP-Adresse blockiert", "email.ban.intro": "Fail2Ban-UI hat eine fehlerhafte Anfrage oder wiederholte Authentifizierungsfehler erkannt und die Quell-IP automatisch blockiert. Überprüfen Sie die Metadaten und Log-Auszüge unten.", "email.ban.subject.banned": "Gesperrt", diff --git a/internal/locales/de_ch.json b/internal/locales/de_ch.json index 92f8bef..d269d26 100644 --- a/internal/locales/de_ch.json +++ b/internal/locales/de_ch.json @@ -351,6 +351,8 @@ "servers.form.no_keys": "Kei SSH-Schlüssel gfunde; Pfad selber igäh", "filter_debug.not_available": "Filter-Debug funktioniert nur, we mindestens ei registrierte Fail2ban-Server aktiviert isch.", "filter_debug.local_missing": "S lokale Fail2ban-Filterverzeichnis isch uf däm Host nid gfunde worde.", + "filter_debug.save_success": "Filter- und Jail-Konfiguration gspicheret und Fail2ban neu glade", + "filter_debug.save_reload_warning": "Konfiguration gspicheret, aber z'Reload isch fähugschlage", "email.ban.title": "Achtung: Fail2Ban het e nöi IP-Adrässe blockiert", "email.ban.intro": "Fail2Ban-UI het e fehlerhafti Aafrag oder widerholti Authentifizierigsfähler erkennt und d Quell-IP automatisch blockiert. Überprüef d Metadate und Log-Uuszüg unge.", "email.ban.subject.banned": "G'sperrt", diff --git a/internal/locales/en.json b/internal/locales/en.json index cfae0db..ac24958 100644 --- a/internal/locales/en.json +++ b/internal/locales/en.json @@ -351,6 +351,8 @@ "servers.form.no_keys": "No SSH keys found; enter path manually", "filter_debug.not_available": "Filter debug is only available when at least one registered Fail2ban server is enabled.", "filter_debug.local_missing": "The local Fail2ban filter directory was not found on this host.", + "filter_debug.save_success": "Filter and jail config saved and reloaded", + "filter_debug.save_reload_warning": "Config saved, but fail2ban reload failed", "email.ban.title": "Security alert: Fail2Ban blocked a new IP-address", "email.ban.intro": "Fail2Ban-UI detected a bad request or repeated authentication failures and automatically blocked the source-IP. Review the metadata and log excerpts below.", "email.ban.subject.banned": "Banned", diff --git a/internal/locales/es.json b/internal/locales/es.json index d3f1acb..d85b6dd 100644 --- a/internal/locales/es.json +++ b/internal/locales/es.json @@ -351,6 +351,8 @@ "servers.form.no_keys": "No se encontraron claves SSH; introduzca la ruta manualmente", "filter_debug.not_available": "La depuración de filtros solo está disponible cuando al menos un servidor Fail2ban registrado está activado.", "filter_debug.local_missing": "No se encontró el directorio de filtros local de Fail2ban en este host.", + "filter_debug.save_success": "Configuración de filtro y jail guardada y Fail2ban recargado", + "filter_debug.save_reload_warning": "Configuración guardada, pero la recarga de Fail2ban falló", "email.ban.title": "Alerta de seguridad: Fail2Ban bloqueó una nueva dirección IP", "email.ban.intro": "Fail2Ban-UI detectó una solicitud incorrecta o fallos de autenticación repetidos y bloqueó automáticamente la IP de origen. Revise los metadatos y extractos de registro a continuación.", "email.ban.subject.banned": "Bloqueado", diff --git a/internal/locales/fr.json b/internal/locales/fr.json index 700319e..f5a8322 100644 --- a/internal/locales/fr.json +++ b/internal/locales/fr.json @@ -351,6 +351,8 @@ "servers.form.no_keys": "Aucune clé SSH trouvée ; saisissez le chemin manuellement", "filter_debug.not_available": "Le débogage des filtres n'est disponible que lorsqu'au moins un serveur Fail2ban enregistré est activé.", "filter_debug.local_missing": "Le répertoire de filtres Fail2ban local est introuvable sur cet hôte.", + "filter_debug.save_success": "Configuration de filtre et jail enregistrée et Fail2ban rechargé", + "filter_debug.save_reload_warning": "Configuration enregistrée, mais le rechargement de Fail2ban a échoué", "email.ban.title": "Alerte de sécurité : Fail2Ban a bloqué une nouvelle adresse IP", "email.ban.intro": "Fail2Ban-UI a détecté une requête suspecte ou des échecs d'authentification répétés et a automatiquement bloqué l'IP source. Consultez les métadonnées et extraits de journaux ci-dessous.", "email.ban.subject.banned": "Bloqué", diff --git a/internal/locales/it.json b/internal/locales/it.json index ba59e95..6a9e0ac 100644 --- a/internal/locales/it.json +++ b/internal/locales/it.json @@ -351,6 +351,8 @@ "servers.form.no_keys": "Nessuna chiave SSH trovata; inserire il percorso manualmente", "filter_debug.not_available": "Il debug dei filtri è disponibile solo quando almeno un server Fail2ban registrato è attivato.", "filter_debug.local_missing": "La directory dei filtri Fail2ban locale non è stata trovata su questo host.", + "filter_debug.save_success": "Configurazione di filtro e jail salvata e Fail2ban ricaricato", + "filter_debug.save_reload_warning": "Configurazione salvata, ma il ricaricamento di Fail2ban non è riuscito", "email.ban.title": "Allerta di sicurezza: Fail2Ban ha bloccato un nuovo indirizzo IP", "email.ban.intro": "Fail2Ban-UI ha rilevato una richiesta sospetta o ripetuti fallimenti di autenticazione e ha automaticamente bloccato l'IP sorgente. Rivedere i metadati e gli estratti di log di seguito.", "email.ban.subject.banned": "Bloccato", diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index 8dfe875..995f7e6 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -1458,12 +1458,10 @@ func SetJailFilterConfigHandler(c *gin.Context) { // Reload fail2ban config.DebugLog("Reloading fail2ban") if err := conn.Reload(c.Request.Context()); err != nil { - config.DebugLog("Failed to reload fail2ban: %v", err) - // Still return success but warn about reload failure - // The config was saved successfully, user can manually reload + log.Printf("⚠️ Config saved but fail2ban reload failed: %v", err) c.JSON(http.StatusOK, gin.H{ "message": "Config saved successfully, but fail2ban reload failed", - "warning": "Please check the fail2ban configuration and reload manually: " + err.Error(), + "warning": err.Error(), }) return } @@ -1908,13 +1906,9 @@ func UpdateJailManagementHandler(c *gin.Context) { } } - // Update errMsg with detailed error output when debug mode is enabled - settings := config.GetSettings() - if settings.Debug && detailedErrorOutput != "" { - errMsg = fmt.Sprintf("%s\n\nDetailed error output:\n%s", errMsg, detailedErrorOutput) - } else if detailedErrorOutput != "" { - // Even without debug mode, include basic error info - errMsg = fmt.Sprintf("%s (check debug mode for details)", errMsg) + if detailedErrorOutput != "" { + // We use only the extracted error output + errMsg = strings.TrimSpace(detailedErrorOutput) } // If any jails were enabled in this request and reload failed, disable them all diff --git a/pkg/web/static/fail2ban-ui.css b/pkg/web/static/fail2ban-ui.css index 8b080cb..d561fbc 100644 --- a/pkg/web/static/fail2ban-ui.css +++ b/pkg/web/static/fail2ban-ui.css @@ -179,6 +179,7 @@ mark { padding: 0.75rem 1rem; border-radius: 0.5rem; color: #fff; + pointer-events: auto; font-weight: 500; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); opacity: 0; diff --git a/pkg/web/static/js/core.js b/pkg/web/static/js/core.js index 2a1c85b..fe6eaa9 100644 --- a/pkg/web/static/js/core.js +++ b/pkg/web/static/js/core.js @@ -28,12 +28,37 @@ function showToast(message, type, duration) { var toast = document.createElement('div'); var variant = type || 'info'; toast.className = 'toast toast-' + variant; - toast.textContent = message; + + // Build inner layout with close button + var wrapper = document.createElement('div'); + wrapper.className = 'flex items-start'; + + var textSpan = document.createElement('span'); + textSpan.className = 'flex-1'; + textSpan.textContent = message; + + var closeBtn = document.createElement('button'); + closeBtn.className = 'flex-shrink-0 ml-2 mt-0.5 opacity-60 hover:opacity-100 focus:outline-none'; + closeBtn.setAttribute('aria-label', 'Close'); + closeBtn.innerHTML = ''; + + wrapper.appendChild(textSpan); + wrapper.appendChild(closeBtn); + toast.appendChild(wrapper); + + // Close button handler + closeBtn.addEventListener('click', function(e) { + e.stopPropagation(); + clearTimeout(autoRemoveTimer); + toast.classList.remove('show'); + setTimeout(function() { toast.remove(); }, 300); + }); + container.appendChild(toast); requestAnimationFrame(function() { toast.classList.add('show'); }); - setTimeout(function() { + var autoRemoveTimer = setTimeout(function() { toast.classList.remove('show'); setTimeout(function() { toast.remove(); diff --git a/pkg/web/static/js/jails.js b/pkg/web/static/js/jails.js index 8a18327..f1580fa 100644 --- a/pkg/web/static/js/jails.js +++ b/pkg/web/static/js/jails.js @@ -155,7 +155,11 @@ function saveJailConfig() { return; } closeModal('jailConfigModal'); - showToast(t('filter_debug.save_success', 'Filter and jail config saved and reloaded'), 'success'); + if (data.warning) { + showToast(t('filter_debug.save_reload_warning', 'Config saved, but fail2ban reload failed') + ': ' + data.warning, 'warning', 12000); + } else { + showToast(t('filter_debug.save_success', 'Filter and jail config saved and reloaded'), 'success'); + } return refreshData({ silent: true }); }) .catch(function(err) { @@ -468,17 +472,17 @@ function saveManageJailsSingle(checkbox) { console.error('Could not find jail name span'); return; } - + const jailName = nameSpan.textContent.trim(); if (!jailName) { console.error('Jail name is empty'); return; } - + const isEnabled = checkbox.checked; const updatedJails = {}; updatedJails[jailName] = isEnabled; - + console.log('Saving jail state:', jailName, 'enabled:', isEnabled, 'payload:', updatedJails); // Send updated state to the API endpoint /api/jails/manage. @@ -500,22 +504,20 @@ function saveManageJailsSingle(checkbox) { if (data.error) { var errorMsg = data.error; var toastType = 'error'; - + // If jails were auto-disabled, check if this jail was one of them var wasAutoDisabled = data.autoDisabled && data.enabledJails && Array.isArray(data.enabledJails) && data.enabledJails.indexOf(jailName) !== -1; - + if (wasAutoDisabled) { checkbox.checked = false; toastType = 'warning'; - // Use the message if available, otherwise use the error - errorMsg = data.message || errorMsg; } else { // Revert checkbox state on error checkbox.checked = !isEnabled; } - - showToast(errorMsg, toastType); - + + showToast(errorMsg, toastType, wasAutoDisabled ? 15000 : undefined); + // Still reload the jail list to reflect the actual state return fetch(withServerParam('/api/jails/manage'), { headers: serverHeaders()