switch to toast instead of alert messages, implement serverIDs and restart tracking for every remote-server

This commit is contained in:
2025-11-14 10:22:44 +01:00
parent bff920e5b3
commit a24e0779d2
7 changed files with 359 additions and 188 deletions

View File

@@ -77,6 +77,7 @@ const (
jailDFile = "/etc/fail2ban/jail.d/ui-custom-action.conf"
actionFile = "/etc/fail2ban/action.d/ui-custom-action.conf"
actionCallbackPlaceholder = "__CALLBACK_URL__"
actionServerIDPlaceholder = "__SERVER_ID__"
)
const fail2banActionTemplate = `[INCLUDES]
@@ -95,13 +96,14 @@ norestored = 1
actionban = /usr/bin/curl -X POST __CALLBACK_URL__/api/ban \
-H "Content-Type: application/json" \
-d "$(jq -n --arg ip '<ip>' \
-d "$(jq -n --arg serverId '__SERVER_ID__' \
--arg ip '<ip>' \
--arg jail '<name>' \
--arg hostname '<fq-hostname>' \
--arg failures '<failures>' \
--arg whois "$(whois <ip> || echo 'missing whois program')" \
--arg logs "$(tac <logpath> | grep <grepopts> -wF <ip>)" \
'{ip: $ip, jail: $jail, hostname: $hostname, failures: $failures, whois: $whois, logs: $logs}')"
'{serverId: $serverId, ip: $ip, jail: $jail, hostname: $hostname, failures: $failures, whois: $whois, logs: $logs}')"
[Init]
@@ -143,6 +145,7 @@ type Fail2banServer struct {
Tags []string `json:"tags,omitempty"`
IsDefault bool `json:"isDefault"`
Enabled bool `json:"enabled"`
RestartNeeded bool `json:"restartNeeded"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
@@ -323,6 +326,7 @@ func applyServerRecordsLocked(records []storage.ServerRecord) {
Tags: tags,
IsDefault: rec.IsDefault,
Enabled: rec.Enabled,
RestartNeeded: rec.NeedsRestart,
CreatedAt: rec.CreatedAt,
UpdatedAt: rec.UpdatedAt,
enabledSet: true,
@@ -399,6 +403,7 @@ func toServerRecordsLocked() ([]storage.ServerRecord, error) {
TagsJSON: string(tagBytes),
IsDefault: srv.IsDefault,
Enabled: srv.Enabled,
NeedsRestart: srv.RestartNeeded,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
@@ -563,6 +568,9 @@ func normalizeServersLocked() {
}
}
server.enabledSet = true
if !server.Enabled {
server.RestartNeeded = false
}
if server.IsDefault && !server.Enabled {
server.IsDefault = false
}
@@ -584,6 +592,8 @@ func normalizeServersLocked() {
sort.SliceStable(currentSettings.Servers, func(i, j int) bool {
return currentSettings.Servers[i].CreatedAt.Before(currentSettings.Servers[j].CreatedAt)
})
updateGlobalRestartFlagLocked()
}
func generateServerID() string {
@@ -595,7 +605,7 @@ func generateServerID() string {
}
// ensureFail2banActionFiles writes the local action files if Fail2ban is present.
func ensureFail2banActionFiles(callbackURL string) error {
func ensureFail2banActionFiles(callbackURL, serverID string) error {
DebugLog("----------------------------")
DebugLog("ensureFail2banActionFiles called (settings.go)")
@@ -609,7 +619,7 @@ func ensureFail2banActionFiles(callbackURL string) error {
if err := ensureJailDConfig(); err != nil {
return err
}
return writeFail2banAction(callbackURL)
return writeFail2banAction(callbackURL, serverID)
}
// setupGeoCustomAction checks and replaces the default action in jail.local with our from fail2ban-UI
@@ -725,14 +735,14 @@ action_mwlg = %(action_)s
}
// writeFail2banAction creates or updates the action file with the AlertCountries.
func writeFail2banAction(callbackURL string) error {
func writeFail2banAction(callbackURL, serverID string) error {
DebugLog("Running initial writeFail2banAction()") // entry point
DebugLog("----------------------------")
if err := os.MkdirAll(filepath.Dir(actionFile), 0o755); err != nil {
return fmt.Errorf("failed to ensure action.d directory: %w", err)
}
actionConfig := BuildFail2banActionConfig(callbackURL)
actionConfig := BuildFail2banActionConfig(callbackURL, serverID)
err := os.WriteFile(actionFile, []byte(actionConfig), 0644)
if err != nil {
return fmt.Errorf("failed to write action file: %w", err)
@@ -751,12 +761,16 @@ func cloneServer(src Fail2banServer) Fail2banServer {
return dst
}
func BuildFail2banActionConfig(callbackURL string) string {
func BuildFail2banActionConfig(callbackURL, serverID string) string {
trimmed := strings.TrimRight(strings.TrimSpace(callbackURL), "/")
if trimmed == "" {
trimmed = "http://127.0.0.1:8080"
}
return strings.ReplaceAll(fail2banActionTemplate, actionCallbackPlaceholder, trimmed)
if serverID == "" {
serverID = "local"
}
config := strings.ReplaceAll(fail2banActionTemplate, actionCallbackPlaceholder, trimmed)
return strings.ReplaceAll(config, actionServerIDPlaceholder, serverID)
}
func getCallbackURLLocked() string {
@@ -786,7 +800,7 @@ func EnsureLocalFail2banAction(server Fail2banServer) error {
settingsLock.RLock()
callbackURL := getCallbackURLLocked()
settingsLock.RUnlock()
return ensureFail2banActionFiles(callbackURL)
return ensureFail2banActionFiles(callbackURL, server.ID)
}
func serverByIDLocked(id string) (Fail2banServer, bool) {
@@ -934,6 +948,35 @@ func clearDefaultLocked() {
}
}
func setServerRestartFlagLocked(serverID string, value bool) bool {
for idx := range currentSettings.Servers {
if currentSettings.Servers[idx].ID == serverID {
currentSettings.Servers[idx].RestartNeeded = value
return true
}
}
return false
}
func anyServerNeedsRestartLocked() bool {
for _, srv := range currentSettings.Servers {
if srv.RestartNeeded {
return true
}
}
return false
}
func updateGlobalRestartFlagLocked() {
currentSettings.RestartNeeded = anyServerNeedsRestartLocked()
}
func markAllServersRestartLocked() {
for idx := range currentSettings.Servers {
currentSettings.Servers[idx].RestartNeeded = true
}
}
// DeleteServer removes a server by ID.
func DeleteServer(id string) error {
settingsLock.Lock()
@@ -998,21 +1041,43 @@ func GetSettings() AppSettings {
return currentSettings
}
// MarkRestartNeeded sets restartNeeded = true and saves JSON
func MarkRestartNeeded() error {
// MarkRestartNeeded marks the specified server as requiring a restart.
func MarkRestartNeeded(serverID string) error {
settingsLock.Lock()
defer settingsLock.Unlock()
currentSettings.RestartNeeded = true
if serverID == "" {
return fmt.Errorf("server id must be provided")
}
if !setServerRestartFlagLocked(serverID, true) {
return fmt.Errorf("server %s not found", serverID)
}
updateGlobalRestartFlagLocked()
if err := persistServersLocked(); err != nil {
return err
}
return persistAppSettingsLocked()
}
// MarkRestartDone sets restartNeeded = false and saves JSON
func MarkRestartDone() error {
// MarkRestartDone marks the specified server as no longer requiring a restart.
func MarkRestartDone(serverID string) error {
settingsLock.Lock()
defer settingsLock.Unlock()
currentSettings.RestartNeeded = false
if serverID == "" {
return fmt.Errorf("server id must be provided")
}
if !setServerRestartFlagLocked(serverID, false) {
return fmt.Errorf("server %s not found", serverID)
}
updateGlobalRestartFlagLocked()
if err := persistServersLocked(); err != nil {
return err
}
return persistAppSettingsLocked()
}
@@ -1026,16 +1091,15 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
old := currentSettings
// If certain fields change, we mark reload needed
if old.BantimeIncrement != new.BantimeIncrement ||
restartTriggered := old.BantimeIncrement != new.BantimeIncrement ||
old.IgnoreIP != new.IgnoreIP ||
old.Bantime != new.Bantime ||
old.Findtime != new.Findtime ||
//old.Maxretry != new.Maxretry ||
old.Maxretry != new.Maxretry {
old.Maxretry != new.Maxretry
if restartTriggered {
new.RestartNeeded = true
} else {
// preserve previous RestartNeeded if it was already true
new.RestartNeeded = new.RestartNeeded || old.RestartNeeded
new.RestartNeeded = anyServerNeedsRestartLocked()
}
new.CallbackURL = strings.TrimSpace(new.CallbackURL)
@@ -1047,6 +1111,10 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
}
currentSettings = new
setDefaultsLocked()
if currentSettings.RestartNeeded && restartTriggered {
markAllServersRestartLocked()
updateGlobalRestartFlagLocked()
}
DebugLog("New settings applied: %v", currentSettings) // Log settings applied
if err := persistAllLocked(); err != nil {

View File

@@ -80,9 +80,18 @@ func ReloadFail2ban() error {
return conn.Reload(context.Background())
}
// RestartFail2ban restarts the Fail2ban service using the default connector.
func RestartFail2ban() error {
conn, err := GetManager().DefaultConnector()
// RestartFail2ban restarts the Fail2ban service using the provided server or default connector.
func RestartFail2ban(serverID string) error {
manager := GetManager()
var (
conn Connector
err error
)
if serverID != "" {
conn, err = manager.Connector(serverID)
} else {
conn, err = manager.DefaultConnector()
}
if err != nil {
return err
}

View File

@@ -63,7 +63,7 @@ func (ac *AgentConnector) Server() config.Fail2banServer {
func (ac *AgentConnector) ensureAction(ctx context.Context) error {
payload := map[string]any{
"name": "ui-custom-action",
"config": config.BuildFail2banActionConfig(config.GetCallbackURL()),
"config": config.BuildFail2banActionConfig(config.GetCallbackURL(), ac.server.ID),
"callbackUrl": config.GetCallbackURL(),
"setDefault": true,
}

View File

@@ -199,7 +199,7 @@ func (sc *SSHConnector) FetchBanEvents(ctx context.Context, limit int) ([]BanEve
func (sc *SSHConnector) ensureAction(ctx context.Context) error {
callbackURL := config.GetCallbackURL()
actionConfig := config.BuildFail2banActionConfig(callbackURL)
actionConfig := config.BuildFail2banActionConfig(callbackURL, sc.server.ID)
payload := base64.StdEncoding.EncodeToString([]byte(actionConfig))
script := strings.ReplaceAll(sshEnsureActionScript, "__PAYLOAD__", payload)
// Base64 encode the entire script to avoid shell escaping issues

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -82,6 +83,7 @@ type ServerRecord struct {
TagsJSON string
IsDefault bool
Enabled bool
NeedsRestart bool
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -242,7 +244,7 @@ func ListServers(ctx context.Context) ([]ServerRecord, error) {
}
rows, err := db.QueryContext(ctx, `
SELECT id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, created_at, updated_at
SELECT id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
FROM servers
ORDER BY created_at`)
if err != nil {
@@ -257,7 +259,7 @@ ORDER BY created_at`)
var name, serverType sql.NullString
var created, updated sql.NullString
var port sql.NullInt64
var isDefault, enabled sql.NullInt64
var isDefault, enabled, needsRestart sql.NullInt64
if err := rows.Scan(
&rec.ID,
@@ -275,6 +277,7 @@ ORDER BY created_at`)
&tags,
&isDefault,
&enabled,
&needsRestart,
&created,
&updated,
); err != nil {
@@ -295,6 +298,7 @@ ORDER BY created_at`)
rec.TagsJSON = stringFromNull(tags)
rec.IsDefault = intToBool(intFromNull(isDefault))
rec.Enabled = intToBool(intFromNull(enabled))
rec.NeedsRestart = intToBool(intFromNull(needsRestart))
if created.Valid {
if t, err := time.Parse(time.RFC3339Nano, created.String); err == nil {
@@ -334,9 +338,9 @@ func ReplaceServers(ctx context.Context, servers []ServerRecord) error {
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO servers (
id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, created_at, updated_at
id, name, type, host, port, socket_path, log_path, ssh_user, ssh_key_path, agent_url, agent_secret, hostname, tags, is_default, enabled, needs_restart, created_at, updated_at
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)`)
if err != nil {
return err
@@ -368,6 +372,7 @@ INSERT INTO servers (
srv.TagsJSON,
boolToInt(srv.IsDefault),
boolToInt(srv.Enabled),
boolToInt(srv.NeedsRestart),
createdAt.Format(time.RFC3339Nano),
updatedAt.Format(time.RFC3339Nano),
); err != nil {
@@ -568,6 +573,7 @@ CREATE TABLE IF NOT EXISTS servers (
tags TEXT,
is_default INTEGER,
enabled INTEGER,
needs_restart INTEGER DEFAULT 0,
created_at TEXT,
updated_at TEXT
);
@@ -591,10 +597,20 @@ CREATE INDEX IF NOT EXISTS idx_ban_events_server_id ON ban_events(server_id);
CREATE INDEX IF NOT EXISTS idx_ban_events_occurred_at ON ban_events(occurred_at);
`
_, err := db.ExecContext(ctx, createTable)
if _, err := db.ExecContext(ctx, createTable); err != nil {
return err
}
// Backfill needs_restart column for existing databases that predate it.
if _, err := db.ExecContext(ctx, `ALTER TABLE servers ADD COLUMN needs_restart INTEGER DEFAULT 0`); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
return err
}
}
return nil
}
func ensureDirectory(path string) error {
if path == ":memory:" {
return nil

View File

@@ -516,7 +516,12 @@ func GetJailFilterConfigHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("GetJailFilterConfigHandler called (handlers.go)") // entry point
jail := c.Param("jail")
cfg, err := fail2ban.GetFilterConfig(jail)
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cfg, err := conn.GetFilterConfig(c.Request.Context(), jail)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -532,6 +537,11 @@ func SetJailFilterConfigHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("SetJailFilterConfigHandler called (handlers.go)") // entry point
jail := c.Param("jail")
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Parse JSON body (containing the new filter content)
var req struct {
@@ -542,25 +552,17 @@ func SetJailFilterConfigHandler(c *gin.Context) {
return
}
// Write the filter config file to /etc/fail2ban/filter.d/<jail>.conf
if err := fail2ban.SetFilterConfig(jail, req.Config); err != nil {
if err := conn.SetFilterConfig(c.Request.Context(), jail, req.Config); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Mark reload needed in our UI settings
// if err := config.MarkRestartNeeded(); err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
// return
// }
if err := conn.Reload(c.Request.Context()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "filter saved but reload failed: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "jail config updated"})
// Return a simple JSON response without forcing a blocking alert
// c.JSON(http.StatusOK, gin.H{
// "message": "Filter updated, reload needed",
// "restartNeeded": true,
// })
c.JSON(http.StatusOK, gin.H{"message": "Filter updated and fail2ban reloaded"})
}
// ManageJailsHandler returns a list of all jails (from jail.local and jail.d)
@@ -602,7 +604,7 @@ func UpdateJailManagementHandler(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update jail settings: " + err.Error()})
return
}
if err := config.MarkRestartNeeded(); err != nil {
if err := config.MarkRestartNeeded(conn.Server().ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
@@ -739,17 +741,18 @@ func RestartFail2banHandler(c *gin.Context) {
config.DebugLog("----------------------------")
config.DebugLog("ApplyFail2banSettings called (handlers.go)") // entry point
// First we write our new settings to /etc/fail2ban/jail.local
// if err := fail2ban.ApplyFail2banSettings("/etc/fail2ban/jail.local"); err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
// return
// }
conn, err := resolveConnector(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
server := conn.Server()
// Attempt to restart the fail2ban service.
restartErr := fail2ban.RestartFail2ban()
restartErr := fail2ban.RestartFail2ban(server.ID)
if restartErr != nil {
// Check if running inside a container.
if _, container := os.LookupEnv("CONTAINER"); container {
if _, container := os.LookupEnv("CONTAINER"); container && server.Type == "local" {
// In a container, the restart command may fail (since fail2ban runs on the host).
// Log the error and continue, so we can mark the restart as done.
log.Printf("Warning: restart failed inside container (expected behavior): %v", restartErr)
@@ -761,7 +764,7 @@ func RestartFail2banHandler(c *gin.Context) {
}
// Only call MarkRestartDone if we either successfully restarted the service or we are in a container.
if err := config.MarkRestartDone(); err != nil {
if err := config.MarkRestartDone(server.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

View File

@@ -136,6 +136,48 @@
padding: 0.1em 0.2em;
border-radius: 0.25em;
}
/* Toast notifications */
#toast-container {
position: fixed;
top: 1.5rem;
right: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 10000;
pointer-events: none;
}
.toast {
min-width: 240px;
max-width: 360px;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
color: #fff;
font-weight: 500;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: translateY(-6px);
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast-success {
background-color: #047857;
}
.toast-error {
background-color: #b91c1c;
}
.toast-info {
background-color: #1d4ed8;
}
</style>
</head>
@@ -154,6 +196,8 @@
</div>
</div>
<div id="toast-container"></div>
<!-- ******************************************************************* -->
<!-- Navigation START -->
<!-- ******************************************************************* -->
@@ -821,7 +865,7 @@
getTranslationsSettingsOnPageload()
])
.then(function() {
checkRestartNeeded();
updateRestartBanner();
return refreshData({ silent: true });
})
.catch(function(err) {
@@ -854,6 +898,25 @@
}
}
function showToast(message, type) {
var container = document.getElementById('toast-container');
if (!container || !message) return;
var toast = document.createElement('div');
var variant = type || 'info';
toast.className = 'toast toast-' + variant;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(function() {
toast.classList.add('show');
});
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() {
toast.remove();
}, 300);
}, 5000);
}
// Fetch and display own external IP for webUI
function displayExternalIP() {
fetch('https://api.ipify.org?format=json')
@@ -905,18 +968,14 @@
}
}
// Check if there is still a reload of the fail2ban service needed
function checkRestartNeeded() {
fetch('/api/settings')
.then(res => res.json())
.then(data => {
if (data.restartNeeded) {
document.getElementById('restartBanner').style.display = 'block';
function updateRestartBanner() {
var banner = document.getElementById('restartBanner');
if (!banner) return;
if (currentServer && currentServer.restartNeeded) {
banner.style.display = 'block';
} else {
document.getElementById('restartBanner').style.display = 'none';
banner.style.display = 'none';
}
})
.catch(err => console.error('Error checking restartNeeded:', err));
}
// Load dynamically the other pages when navigating in nav
@@ -997,6 +1056,7 @@
}
renderServerSelector();
renderServerSubtitle();
updateRestartBanner();
})
.catch(function(err) {
console.error('Error loading servers:', err);
@@ -1005,6 +1065,7 @@
currentServer = null;
renderServerSelector();
renderServerSubtitle();
updateRestartBanner();
});
}
@@ -1088,6 +1149,7 @@
}
renderServerSelector();
renderServerSubtitle();
updateRestartBanner();
refreshData();
}
@@ -1643,10 +1705,10 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error saving server: ' + data.error);
showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error');
return;
}
alert(t('servers.form.success', 'Server saved successfully.'));
showToast(t('servers.form.success', 'Server saved successfully.'), 'success');
var saved = data.server || {};
currentServerId = saved.id || currentServerId;
return loadServers().then(function() {
@@ -1660,7 +1722,7 @@
});
})
.catch(function(err) {
alert('Error saving server: ' + err);
showToast('Error saving server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1733,7 +1795,7 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error saving server: ' + data.error);
showToast('Error saving server: ' + (data.error || 'Unknown error'), 'error');
return;
}
if (!enabled && currentServerId === serverId) {
@@ -1748,7 +1810,7 @@
});
})
.catch(function(err) {
alert('Error saving server: ' + err);
showToast('Error saving server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1764,13 +1826,13 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert(t(data.messageKey || 'servers.actions.test_failure', data.error));
showToast(t(data.messageKey || 'servers.actions.test_failure', data.error), 'error');
return;
}
alert(t(data.messageKey || 'servers.actions.test_success', data.message || 'Connection successful'));
showToast(t(data.messageKey || 'servers.actions.test_success', data.message || 'Connection successful'), 'success');
})
.catch(function(err) {
alert(t('servers.actions.test_failure', 'Connection failed') + ': ' + err);
showToast(t('servers.actions.test_failure', 'Connection failed') + ': ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1784,7 +1846,7 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error deleting server: ' + data.error);
showToast('Error deleting server: ' + (data.error || 'Unknown error'), 'error');
return;
}
if (currentServerId === serverId) {
@@ -1796,10 +1858,12 @@
renderServerSelector();
renderServerSubtitle();
return refreshData({ silent: true });
}).then(function() {
showToast(t('servers.actions.delete_success', 'Server removed'), 'success');
});
})
.catch(function(err) {
alert('Error deleting server: ' + err);
showToast('Error deleting server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1812,7 +1876,7 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert('Error setting default server: ' + data.error);
showToast('Error setting default server: ' + (data.error || 'Unknown error'), 'error');
return;
}
currentServerId = data.server ? data.server.id : serverId;
@@ -1821,10 +1885,12 @@
renderServerSelector();
renderServerSubtitle();
return refreshData({ silent: true });
}).then(function() {
showToast(t('servers.actions.set_default_success', 'Server set as default'), 'success');
});
})
.catch(function(err) {
alert('Error setting default server: ' + err);
showToast('Error setting default server: ' + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1910,14 +1976,14 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error: " + data.error);
showToast("Error unbanning IP: " + data.error, 'error');
} else {
alert(data.message || "IP unbanned successfully");
showToast(data.message || "IP unbanned successfully", 'success');
}
return refreshData({ silent: true });
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1943,14 +2009,14 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error loading config: " + data.error);
} else {
showToast("Error loading config: " + data.error, 'error');
return;
}
textArea.value = data.config;
openModal('jailConfigModal');
}
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1971,15 +2037,15 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error saving config: " + data.error);
} else {
console.log("Filter saved successfully. Restart needed? " + data.restartNeeded);
closeModal('jailConfigModal');
document.getElementById('restartBanner').style.display = 'block';
showToast("Error saving config: " + data.error, 'error');
return;
}
closeModal('jailConfigModal');
showToast(t('filter_debug.save_success', 'Filter saved and reloaded'), 'success');
return refreshData({ silent: true });
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -1990,7 +2056,7 @@
// Fetches the full-list of all jails (from /jails/manage) and builds a list with toggle switches.
function openManageJailsModal() {
if (!currentServerId) {
alert(t('servers.selector.none', 'Please add and select a Fail2ban server first.'));
showToast(t('servers.selector.none', 'Please add and select a Fail2ban server first.'), 'info');
return;
}
showLoading(true);
@@ -2000,7 +2066,7 @@
.then(res => res.json())
.then(data => {
if (!data.jails?.length) {
alert("No jails found.");
showToast("No jails found for this server.", 'info');
return;
}
@@ -2039,7 +2105,7 @@
document.getElementById('jailsList').innerHTML = html;
openModal('manageJailsModal');
})
.catch(err => alert("Error fetching jails: " + err))
.catch(err => showToast("Error fetching jails: " + err, 'error'))
.finally(() => showLoading(false));
}
@@ -2063,14 +2129,17 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error saving jail settings: " + data.error);
} else {
// A restart of fail2ban is needed, to enable or disable jails - a reload is not enough
document.getElementById('restartBanner').style.display = 'block';
showToast("Error saving jail settings: " + data.error, 'error');
return;
}
showToast(t('jails.manage.save_success', 'Jail settings saved. Please restart Fail2ban.'), 'info');
return loadServers().then(function() {
updateRestartBanner();
return refreshData({ silent: true });
});
})
.catch(function(err) {
alert("Error: " + err);
showToast("Error: " + err, 'error');
})
.finally(function() {
showLoading(false);
@@ -2126,7 +2195,7 @@
document.getElementById('ignoreIP').value = data.ignoreip || '';
})
.catch(err => {
alert('Error loading settings: ' + err);
showToast('Error loading settings: ' + err, 'error');
})
.finally(() => showLoading(false));
}
@@ -2173,17 +2242,20 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error saving settings: ' + data.error + data.details);
showToast('Error saving settings: ' + (data.error + (data.details || '')), 'error');
} else {
var selectedLang = $('#languageSelect').val();
loadTranslations(selectedLang);
console.log("Settings saved successfully. Restart needed? " + data.restartNeeded);
showToast(t('settings.save_success', 'Settings saved'), 'success');
if (data.restartNeeded) {
document.getElementById('restartBanner').style.display = 'block';
loadServers().then(function() {
updateRestartBanner();
});
}
}
})
.catch(err => alert('Error: ' + err))
.catch(err => showToast('Error saving settings: ' + err, 'error'))
.finally(() => showLoading(false));
}
@@ -2199,7 +2271,7 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error loading filters: ' + data.error);
showToast('Error loading filters: ' + data.error, 'error');
return;
}
const select = document.getElementById('filterSelect');
@@ -2229,7 +2301,7 @@
}
})
.catch(err => {
alert('Error loading filters: ' + err);
showToast('Error loading filters: ' + err, 'error');
})
.finally(() => showLoading(false));
}
@@ -2244,12 +2316,12 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error sending test email: ' + data.error);
showToast('Error sending test email: ' + data.error, 'error');
} else {
alert('Test email sent successfully!');
showToast('Test email sent successfully!', 'success');
}
})
.catch(error => alert('Error: ' + error))
.catch(error => showToast('Error sending test email: ' + error, 'error'))
.finally(() => showLoading(false));
}
@@ -2259,7 +2331,7 @@
const lines = document.getElementById('logLinesTextarea').value.split('\n');
if (!filterName) {
alert('Please select a filter.');
showToast('Please select a filter.', 'info');
return;
}
@@ -2275,13 +2347,13 @@
.then(res => res.json())
.then(data => {
if (data.error) {
alert('Error: ' + data.error);
showToast('Error testing filter: ' + data.error, 'error');
return;
}
renderTestResults(data.matches);
})
.catch(err => {
alert('Error: ' + err);
showToast('Error testing filter: ' + err, 'error');
})
.finally(() => showLoading(false));
}
@@ -2332,14 +2404,17 @@
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.error) {
alert("Error: " + data.error);
} else {
document.getElementById('restartBanner').style.display = 'none';
return refreshData({ silent: true });
showToast("Failed to restart Fail2ban: " + data.error, 'error');
return;
}
return loadServers().then(function() {
updateRestartBanner();
showToast(t('restart_banner.success', 'Fail2ban restart triggered'), 'success');
return refreshData({ silent: true });
});
})
.catch(function(err) {
alert("Error: " + err);
showToast("Failed to restart Fail2ban: " + err, 'error');
})
.finally(function() {
showLoading(false);