mirror of
https://github.com/swissmakers/fail2ban-ui.git
synced 2026-04-17 05:53:15 +02:00
Implemented a real-time console log streaming via WebSocket for debugging purposes. Users can enable console output in settings to view application logs directly in the web interface.
This commit is contained in:
@@ -82,6 +82,15 @@ func main() {
|
|||||||
wsHub := web.NewHub()
|
wsHub := web.NewHub()
|
||||||
go wsHub.Run()
|
go wsHub.Run()
|
||||||
|
|
||||||
|
// Setup console log writer to capture stdout and send via WebSocket
|
||||||
|
web.SetupConsoleLogWriter(wsHub)
|
||||||
|
// Update enabled state based on current settings
|
||||||
|
web.UpdateConsoleLogEnabled()
|
||||||
|
// Register callback to update console log state when settings change
|
||||||
|
config.SetUpdateConsoleLogStateFunc(func(enabled bool) {
|
||||||
|
web.SetConsoleLogEnabled(enabled)
|
||||||
|
})
|
||||||
|
|
||||||
// Register all application routes, including the static files and templates.
|
// Register all application routes, including the static files and templates.
|
||||||
web.RegisterRoutes(router, wsHub)
|
web.RegisterRoutes(router, wsHub)
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ type AppSettings struct {
|
|||||||
// Email alert preferences
|
// Email alert preferences
|
||||||
EmailAlertsForBans bool `json:"emailAlertsForBans"` // Enable email alerts for ban events (default: true)
|
EmailAlertsForBans bool `json:"emailAlertsForBans"` // Enable email alerts for ban events (default: true)
|
||||||
EmailAlertsForUnbans bool `json:"emailAlertsForUnbans"` // Enable email alerts for unban events (default: false)
|
EmailAlertsForUnbans bool `json:"emailAlertsForUnbans"` // Enable email alerts for unban events (default: false)
|
||||||
|
|
||||||
|
// Console output preferences
|
||||||
|
ConsoleOutput bool `json:"consoleOutput"` // Enable console output in web UI (default: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdvancedActionsConfig struct {
|
type AdvancedActionsConfig struct {
|
||||||
@@ -425,6 +428,7 @@ func applyAppSettingsRecordLocked(rec storage.AppSettingsRecord) {
|
|||||||
currentSettings.CallbackSecret = rec.CallbackSecret
|
currentSettings.CallbackSecret = rec.CallbackSecret
|
||||||
currentSettings.EmailAlertsForBans = rec.EmailAlertsForBans
|
currentSettings.EmailAlertsForBans = rec.EmailAlertsForBans
|
||||||
currentSettings.EmailAlertsForUnbans = rec.EmailAlertsForUnbans
|
currentSettings.EmailAlertsForUnbans = rec.EmailAlertsForUnbans
|
||||||
|
currentSettings.ConsoleOutput = rec.ConsoleOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyServerRecordsLocked(records []storage.ServerRecord) {
|
func applyServerRecordsLocked(records []storage.ServerRecord) {
|
||||||
@@ -511,6 +515,8 @@ func toAppSettingsRecordLocked() (storage.AppSettingsRecord, error) {
|
|||||||
GeoIPProvider: currentSettings.GeoIPProvider,
|
GeoIPProvider: currentSettings.GeoIPProvider,
|
||||||
GeoIPDatabasePath: currentSettings.GeoIPDatabasePath,
|
GeoIPDatabasePath: currentSettings.GeoIPDatabasePath,
|
||||||
MaxLogLines: currentSettings.MaxLogLines,
|
MaxLogLines: currentSettings.MaxLogLines,
|
||||||
|
// Console output settings
|
||||||
|
ConsoleOutput: currentSettings.ConsoleOutput,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1529,9 +1535,31 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
|
|||||||
}
|
}
|
||||||
DebugLog("New settings applied: %v", currentSettings) // Log settings applied
|
DebugLog("New settings applied: %v", currentSettings) // Log settings applied
|
||||||
|
|
||||||
|
// Update console log enabled state if it changed
|
||||||
|
if old.ConsoleOutput != new.ConsoleOutput {
|
||||||
|
// Import web package to update console log state
|
||||||
|
// We'll handle this via a callback or direct call
|
||||||
|
updateConsoleLogState(new.ConsoleOutput)
|
||||||
|
}
|
||||||
|
|
||||||
if err := persistAllLocked(); err != nil {
|
if err := persistAllLocked(); err != nil {
|
||||||
fmt.Println("Error saving settings:", err)
|
fmt.Println("Error saving settings:", err)
|
||||||
return currentSettings, err
|
return currentSettings, err
|
||||||
}
|
}
|
||||||
return currentSettings, nil
|
return currentSettings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateConsoleLogState updates the console log writer enabled state
|
||||||
|
// This is called from UpdateSettings when console output setting changes
|
||||||
|
var updateConsoleLogStateFunc func(bool)
|
||||||
|
|
||||||
|
// SetUpdateConsoleLogStateFunc sets the callback function to update console log state
|
||||||
|
func SetUpdateConsoleLogStateFunc(fn func(bool)) {
|
||||||
|
updateConsoleLogStateFunc = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateConsoleLogState(enabled bool) {
|
||||||
|
if updateConsoleLogStateFunc != nil {
|
||||||
|
updateConsoleLogStateFunc(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"settings.port_env_hint": "Um den Port über die Weboberfläche zu ändern, entfernen Sie die PORT-Umgebungsvariable und starten Sie den Container neu.",
|
"settings.port_env_hint": "Um den Port über die Weboberfläche zu ändern, entfernen Sie die PORT-Umgebungsvariable und starten Sie den Container neu.",
|
||||||
"settings.port_restart_hint": "⚠️ Port-Änderungen erfordern einen Neustart des Containers, um wirksam zu werden.",
|
"settings.port_restart_hint": "⚠️ Port-Änderungen erfordern einen Neustart des Containers, um wirksam zu werden.",
|
||||||
"settings.enable_debug": "Debug-Protokoll aktivieren",
|
"settings.enable_debug": "Debug-Protokoll aktivieren",
|
||||||
|
"settings.enable_console": "Konsolenausgabe aktivieren",
|
||||||
|
"settings.console.title": "Konsolenausgabe",
|
||||||
|
"settings.console.clear": "Löschen",
|
||||||
|
"settings.console.save_hint": "Bitte speichern Sie zuerst Ihre Einstellungen, bevor hier Logs angezeigt werden.",
|
||||||
"settings.alert": "Alarm-Einstellungen",
|
"settings.alert": "Alarm-Einstellungen",
|
||||||
"settings.callback_url": "Fail2ban Callback-URL",
|
"settings.callback_url": "Fail2ban Callback-URL",
|
||||||
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"settings.port_env_hint": "Um de Port über d Weboberflächi z ändere, entferne d PORT-Umgebigsvariable und start de Container neu.",
|
"settings.port_env_hint": "Um de Port über d Weboberflächi z ändere, entferne d PORT-Umgebigsvariable und start de Container neu.",
|
||||||
"settings.port_restart_hint": "⚠️ Port-Änderige erfordere ä Neustart vom Container, zum wirksam z werde.",
|
"settings.port_restart_hint": "⚠️ Port-Änderige erfordere ä Neustart vom Container, zum wirksam z werde.",
|
||||||
"settings.enable_debug": "Debug-Modus aktivierä",
|
"settings.enable_debug": "Debug-Modus aktivierä",
|
||||||
|
"settings.enable_console": "Konsolenusgab aktivierä",
|
||||||
|
"settings.console.title": "Konsolenusgab",
|
||||||
|
"settings.console.clear": "Löschä",
|
||||||
|
"settings.console.save_hint": "Bitte speichere zerscht dini Istellige, bevor hiä Logs azeigt wärde.",
|
||||||
"settings.alert": "Alarm-Istellige",
|
"settings.alert": "Alarm-Istellige",
|
||||||
"settings.callback_url": "Fail2ban Callback-URL",
|
"settings.callback_url": "Fail2ban Callback-URL",
|
||||||
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"settings.port_env_hint": "To change the port via Web UI, remove the PORT environment variable and restart the container.",
|
"settings.port_env_hint": "To change the port via Web UI, remove the PORT environment variable and restart the container.",
|
||||||
"settings.port_restart_hint": "⚠️ Port changes require a container restart to take effect.",
|
"settings.port_restart_hint": "⚠️ Port changes require a container restart to take effect.",
|
||||||
"settings.enable_debug": "Enable Debug Log",
|
"settings.enable_debug": "Enable Debug Log",
|
||||||
|
"settings.enable_console": "Enable Console Output",
|
||||||
|
"settings.console.title": "Console Output",
|
||||||
|
"settings.console.clear": "Clear",
|
||||||
|
"settings.console.save_hint": "Please save your settings first before logs will be displayed here.",
|
||||||
"settings.alert": "Alert Settings",
|
"settings.alert": "Alert Settings",
|
||||||
"settings.callback_url": "Fail2ban Callback URL",
|
"settings.callback_url": "Fail2ban Callback URL",
|
||||||
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"settings.port_env_hint": "Para cambiar el puerto mediante la interfaz web, elimine la variable de entorno PORT y reinicie el contenedor.",
|
"settings.port_env_hint": "Para cambiar el puerto mediante la interfaz web, elimine la variable de entorno PORT y reinicie el contenedor.",
|
||||||
"settings.port_restart_hint": "⚠️ Los cambios de puerto requieren un reinicio del contenedor para surtir efecto.",
|
"settings.port_restart_hint": "⚠️ Los cambios de puerto requieren un reinicio del contenedor para surtir efecto.",
|
||||||
"settings.enable_debug": "Habilitar el modo de depuración",
|
"settings.enable_debug": "Habilitar el modo de depuración",
|
||||||
|
"settings.enable_console": "Habilitar salida de consola",
|
||||||
|
"settings.console.title": "Salida de consola",
|
||||||
|
"settings.console.clear": "Limpiar",
|
||||||
|
"settings.console.save_hint": "Por favor, guarde primero su configuración antes de que se muestren los registros aquí.",
|
||||||
"settings.alert": "Configuración de alertas",
|
"settings.alert": "Configuración de alertas",
|
||||||
"settings.callback_url": "URL de retorno de Fail2ban",
|
"settings.callback_url": "URL de retorno de Fail2ban",
|
||||||
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"settings.port_env_hint": "Pour modifier le port via l'interface Web, supprimez la variable d'environnement PORT et redémarrez le conteneur.",
|
"settings.port_env_hint": "Pour modifier le port via l'interface Web, supprimez la variable d'environnement PORT et redémarrez le conteneur.",
|
||||||
"settings.port_restart_hint": "⚠️ Les modifications du port nécessitent un redémarrage du conteneur pour prendre effet.",
|
"settings.port_restart_hint": "⚠️ Les modifications du port nécessitent un redémarrage du conteneur pour prendre effet.",
|
||||||
"settings.enable_debug": "Activer le mode débogage",
|
"settings.enable_debug": "Activer le mode débogage",
|
||||||
|
"settings.enable_console": "Activer la sortie console",
|
||||||
|
"settings.console.title": "Sortie console",
|
||||||
|
"settings.console.clear": "Effacer",
|
||||||
|
"settings.console.save_hint": "Veuillez d'abord enregistrer vos paramètres avant que les journaux ne s'affichent ici.",
|
||||||
"settings.alert": "Paramètres d'alerte",
|
"settings.alert": "Paramètres d'alerte",
|
||||||
"settings.callback_url": "URL de rappel Fail2ban",
|
"settings.callback_url": "URL de rappel Fail2ban",
|
||||||
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
||||||
|
|||||||
@@ -115,6 +115,10 @@
|
|||||||
"settings.port_env_hint": "Per modificare la porta tramite l'interfaccia Web, rimuovere la variabile d'ambiente PORT e riavviare il contenitore.",
|
"settings.port_env_hint": "Per modificare la porta tramite l'interfaccia Web, rimuovere la variabile d'ambiente PORT e riavviare il contenitore.",
|
||||||
"settings.port_restart_hint": "⚠️ Le modifiche alla porta richiedono un riavvio del contenitore per avere effetto.",
|
"settings.port_restart_hint": "⚠️ Le modifiche alla porta richiedono un riavvio del contenitore per avere effetto.",
|
||||||
"settings.enable_debug": "Abilita debug",
|
"settings.enable_debug": "Abilita debug",
|
||||||
|
"settings.enable_console": "Abilita output console",
|
||||||
|
"settings.console.title": "Output console",
|
||||||
|
"settings.console.clear": "Pulisci",
|
||||||
|
"settings.console.save_hint": "Si prega di salvare prima le impostazioni prima che i log vengano visualizzati qui.",
|
||||||
"settings.alert": "Impostazioni di allarme",
|
"settings.alert": "Impostazioni di allarme",
|
||||||
"settings.callback_url": "URL di callback Fail2ban",
|
"settings.callback_url": "URL di callback Fail2ban",
|
||||||
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
"settings.callback_url_placeholder": "http://127.0.0.1:8080",
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ type AppSettingsRecord struct {
|
|||||||
AlertCountriesJSON string
|
AlertCountriesJSON string
|
||||||
EmailAlertsForBans bool
|
EmailAlertsForBans bool
|
||||||
EmailAlertsForUnbans bool
|
EmailAlertsForUnbans bool
|
||||||
|
// Console output settings
|
||||||
|
ConsoleOutput bool
|
||||||
// SMTP settings
|
// SMTP settings
|
||||||
SMTPHost string
|
SMTPHost string
|
||||||
SMTPPort int
|
SMTPPort int
|
||||||
@@ -190,17 +192,17 @@ func GetAppSettings(ctx context.Context) (AppSettingsRecord, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
row := db.QueryRowContext(ctx, `
|
row := db.QueryRowContext(ctx, `
|
||||||
SELECT language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines
|
SELECT language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, console_output
|
||||||
FROM app_settings
|
FROM app_settings
|
||||||
WHERE id = 1`)
|
WHERE id = 1`)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lang, callback, callbackSecret, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath sql.NullString
|
lang, callback, callbackSecret, alerts, smtpHost, smtpUser, smtpPass, smtpFrom, ignoreIP, bantime, findtime, destemail, banaction, banactionAllports, advancedActions, geoipProvider, geoipDatabasePath sql.NullString
|
||||||
port, smtpPort, maxretry, maxLogLines sql.NullInt64
|
port, smtpPort, maxretry, maxLogLines sql.NullInt64
|
||||||
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn, emailAlertsForBans, emailAlertsForUnbans sql.NullInt64
|
debug, restartNeeded, smtpTLS, bantimeInc, defaultJailEn, emailAlertsForBans, emailAlertsForUnbans, consoleOutput sql.NullInt64
|
||||||
)
|
)
|
||||||
|
|
||||||
err := row.Scan(&lang, &port, &debug, &restartNeeded, &callback, &callbackSecret, &alerts, &emailAlertsForBans, &emailAlertsForUnbans, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines)
|
err := row.Scan(&lang, &port, &debug, &restartNeeded, &callback, &callbackSecret, &alerts, &emailAlertsForBans, &emailAlertsForUnbans, &smtpHost, &smtpPort, &smtpUser, &smtpPass, &smtpFrom, &smtpTLS, &bantimeInc, &defaultJailEn, &ignoreIP, &bantime, &findtime, &maxretry, &destemail, &banaction, &banactionAllports, &advancedActions, &geoipProvider, &geoipDatabasePath, &maxLogLines, &consoleOutput)
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
return AppSettingsRecord{}, false, nil
|
return AppSettingsRecord{}, false, nil
|
||||||
}
|
}
|
||||||
@@ -243,6 +245,8 @@ WHERE id = 1`)
|
|||||||
GeoIPProvider: stringFromNull(geoipProvider),
|
GeoIPProvider: stringFromNull(geoipProvider),
|
||||||
GeoIPDatabasePath: stringFromNull(geoipDatabasePath),
|
GeoIPDatabasePath: stringFromNull(geoipDatabasePath),
|
||||||
MaxLogLines: intFromNull(maxLogLines),
|
MaxLogLines: intFromNull(maxLogLines),
|
||||||
|
// Console output settings
|
||||||
|
ConsoleOutput: intToBool(intFromNull(consoleOutput)),
|
||||||
}
|
}
|
||||||
|
|
||||||
return rec, true, nil
|
return rec, true, nil
|
||||||
@@ -254,9 +258,9 @@ func SaveAppSettings(ctx context.Context, rec AppSettingsRecord) error {
|
|||||||
}
|
}
|
||||||
_, err := db.ExecContext(ctx, `
|
_, err := db.ExecContext(ctx, `
|
||||||
INSERT INTO app_settings (
|
INSERT INTO app_settings (
|
||||||
id, language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines
|
id, language, port, debug, restart_needed, callback_url, callback_secret, alert_countries, email_alerts_for_bans, email_alerts_for_unbans, smtp_host, smtp_port, smtp_username, smtp_password, smtp_from, smtp_use_tls, bantime_increment, default_jail_enable, ignore_ip, bantime, findtime, maxretry, destemail, banaction, banaction_allports, advanced_actions, geoip_provider, geoip_database_path, max_log_lines, console_output
|
||||||
) VALUES (
|
) VALUES (
|
||||||
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
) ON CONFLICT(id) DO UPDATE SET
|
) ON CONFLICT(id) DO UPDATE SET
|
||||||
language = excluded.language,
|
language = excluded.language,
|
||||||
port = excluded.port,
|
port = excluded.port,
|
||||||
@@ -285,7 +289,8 @@ INSERT INTO app_settings (
|
|||||||
advanced_actions = excluded.advanced_actions,
|
advanced_actions = excluded.advanced_actions,
|
||||||
geoip_provider = excluded.geoip_provider,
|
geoip_provider = excluded.geoip_provider,
|
||||||
geoip_database_path = excluded.geoip_database_path,
|
geoip_database_path = excluded.geoip_database_path,
|
||||||
max_log_lines = excluded.max_log_lines
|
max_log_lines = excluded.max_log_lines,
|
||||||
|
console_output = excluded.console_output
|
||||||
`, rec.Language,
|
`, rec.Language,
|
||||||
rec.Port,
|
rec.Port,
|
||||||
boolToInt(rec.Debug),
|
boolToInt(rec.Debug),
|
||||||
@@ -314,7 +319,7 @@ INSERT INTO app_settings (
|
|||||||
rec.GeoIPProvider,
|
rec.GeoIPProvider,
|
||||||
rec.GeoIPDatabasePath,
|
rec.GeoIPDatabasePath,
|
||||||
rec.MaxLogLines,
|
rec.MaxLogLines,
|
||||||
)
|
boolToInt(rec.ConsoleOutput))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,7 +866,9 @@ CREATE TABLE IF NOT EXISTS app_settings (
|
|||||||
advanced_actions TEXT,
|
advanced_actions TEXT,
|
||||||
geoip_provider TEXT,
|
geoip_provider TEXT,
|
||||||
geoip_database_path TEXT,
|
geoip_database_path TEXT,
|
||||||
max_log_lines INTEGER
|
max_log_lines INTEGER,
|
||||||
|
-- Console output settings
|
||||||
|
console_output INTEGER DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS servers (
|
CREATE TABLE IF NOT EXISTS servers (
|
||||||
@@ -933,6 +940,13 @@ CREATE INDEX IF NOT EXISTS idx_perm_blocks_status ON permanent_blocks(status);
|
|||||||
// return err
|
// return err
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Migration: Add console_output column if it doesn't exist
|
||||||
|
if _, err := db.ExecContext(ctx, `ALTER TABLE app_settings ADD COLUMN console_output INTEGER DEFAULT 0`); err != nil {
|
||||||
|
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column name") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
_ = strings.Contains // Keep strings import for migration example above
|
_ = strings.Contains // Keep strings import for migration example above
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
105
pkg/web/console_logger.go
Normal file
105
pkg/web/console_logger.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Fail2ban UI - A Swiss made, management interface for Fail2ban.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch)
|
||||||
|
//
|
||||||
|
// Licensed under the GNU General Public License, Version 3 (GPL-3.0)
|
||||||
|
// You may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/swissmakers/fail2ban-ui/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConsoleLogWriter is a multi-writer that writes to both the original log output
|
||||||
|
// and broadcasts to WebSocket clients when console output is enabled
|
||||||
|
type ConsoleLogWriter struct {
|
||||||
|
originalWriter io.Writer
|
||||||
|
hub *Hub
|
||||||
|
mu sync.RWMutex
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConsoleLogWriter creates a new console log writer
|
||||||
|
func NewConsoleLogWriter(hub *Hub, originalWriter io.Writer) *ConsoleLogWriter {
|
||||||
|
return &ConsoleLogWriter{
|
||||||
|
originalWriter: originalWriter,
|
||||||
|
hub: hub,
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled enables or disables console output broadcasting
|
||||||
|
func (c *ConsoleLogWriter) SetEnabled(enabled bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.enabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer interface
|
||||||
|
func (c *ConsoleLogWriter) Write(p []byte) (n int, err error) {
|
||||||
|
// Always write to original writer
|
||||||
|
n, err = c.originalWriter.Write(p)
|
||||||
|
|
||||||
|
// Broadcast to WebSocket if enabled
|
||||||
|
c.mu.RLock()
|
||||||
|
enabled := c.enabled
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
if enabled && c.hub != nil {
|
||||||
|
// Remove trailing newline for cleaner display
|
||||||
|
message := string(p)
|
||||||
|
if len(message) > 0 && message[len(message)-1] == '\n' {
|
||||||
|
message = message[:len(message)-1]
|
||||||
|
}
|
||||||
|
if len(message) > 0 {
|
||||||
|
c.hub.BroadcastConsoleLog(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalConsoleLogWriter *ConsoleLogWriter
|
||||||
|
var consoleLogWriterOnce sync.Once
|
||||||
|
|
||||||
|
// SetupConsoleLogWriter sets up the console log writer and replaces the standard log output
|
||||||
|
// This captures all log.Printf, log.Println, etc. output
|
||||||
|
func SetupConsoleLogWriter(hub *Hub) {
|
||||||
|
consoleLogWriterOnce.Do(func() {
|
||||||
|
// Create a multi-writer that writes to both original stdout and our console writer
|
||||||
|
globalConsoleLogWriter = NewConsoleLogWriter(hub, os.Stdout)
|
||||||
|
|
||||||
|
// Replace log output - this captures all log.Printf, log.Println, etc.
|
||||||
|
log.SetOutput(globalConsoleLogWriter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConsoleLogEnabled updates the enabled state based on settings
|
||||||
|
func UpdateConsoleLogEnabled() {
|
||||||
|
if globalConsoleLogWriter != nil {
|
||||||
|
settings := config.GetSettings()
|
||||||
|
globalConsoleLogWriter.SetEnabled(settings.ConsoleOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConsoleLogEnabled directly sets the enabled state
|
||||||
|
func SetConsoleLogEnabled(enabled bool) {
|
||||||
|
if globalConsoleLogWriter != nil {
|
||||||
|
globalConsoleLogWriter.SetEnabled(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -462,6 +462,11 @@ button.bg-red-500:hover, button.bg-red-600:hover {
|
|||||||
color: rgb(107 114 128);
|
color: rgb(107 114 128);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Console output text colors */
|
||||||
|
.text-gray-100 {
|
||||||
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
#wsTooltip .border-gray-700 {
|
#wsTooltip .border-gray-700 {
|
||||||
border-color: rgb(55 65 81);
|
border-color: rgb(55 65 81);
|
||||||
}
|
}
|
||||||
|
|||||||
213
pkg/web/static/js/console.js
Normal file
213
pkg/web/static/js/console.js
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
// Console Output Handler for Fail2ban UI
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let consoleOutputContainer = null;
|
||||||
|
let consoleOutputElement = null;
|
||||||
|
let maxConsoleLines = 1000; // Maximum number of lines to keep in console
|
||||||
|
let wasConsoleEnabledOnLoad = false; // Track if console was enabled when page loaded
|
||||||
|
|
||||||
|
function initConsoleOutput() {
|
||||||
|
consoleOutputContainer = document.getElementById('consoleOutputContainer');
|
||||||
|
consoleOutputElement = document.getElementById('consoleOutputWindow');
|
||||||
|
|
||||||
|
if (!consoleOutputContainer || !consoleOutputElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register WebSocket callback for console logs
|
||||||
|
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||||
|
wsManager.onConsoleLog(function(message, timestamp) {
|
||||||
|
appendConsoleLog(message, timestamp);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Wait for WebSocket manager to be available
|
||||||
|
const wsCheckInterval = setInterval(function() {
|
||||||
|
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||||
|
wsManager.onConsoleLog(function(message, timestamp) {
|
||||||
|
appendConsoleLog(message, timestamp);
|
||||||
|
});
|
||||||
|
clearInterval(wsCheckInterval);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Stop checking after 5 seconds
|
||||||
|
setTimeout(function() {
|
||||||
|
clearInterval(wsCheckInterval);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConsoleOutput(userClicked) {
|
||||||
|
const checkbox = document.getElementById('consoleOutput');
|
||||||
|
const container = document.getElementById('consoleOutputContainer');
|
||||||
|
|
||||||
|
if (!checkbox || !container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
// Initialize console if not already done
|
||||||
|
if (!consoleOutputElement) {
|
||||||
|
initConsoleOutput();
|
||||||
|
} else {
|
||||||
|
// Re-register WebSocket callback in case it wasn't registered before
|
||||||
|
if (typeof wsManager !== 'undefined' && wsManager) {
|
||||||
|
// Remove any existing callbacks and re-register
|
||||||
|
if (!wsManager.consoleLogCallbacks) {
|
||||||
|
wsManager.consoleLogCallbacks = [];
|
||||||
|
}
|
||||||
|
// Check if callback already exists
|
||||||
|
let callbackExists = false;
|
||||||
|
for (let i = 0; i < wsManager.consoleLogCallbacks.length; i++) {
|
||||||
|
if (wsManager.consoleLogCallbacks[i].toString().includes('appendConsoleLog')) {
|
||||||
|
callbackExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!callbackExists) {
|
||||||
|
wsManager.onConsoleLog(function(message, timestamp) {
|
||||||
|
appendConsoleLog(message, timestamp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show save hint only if user just clicked to enable (not on page load)
|
||||||
|
const consoleEl = document.getElementById('consoleOutputWindow');
|
||||||
|
if (consoleEl && userClicked && !wasConsoleEnabledOnLoad) {
|
||||||
|
// Clear initial placeholder message
|
||||||
|
const placeholder = consoleEl.querySelector('.text-gray-500');
|
||||||
|
if (placeholder && placeholder.textContent === 'Console output will appear here...') {
|
||||||
|
placeholder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show save hint message
|
||||||
|
const hintDiv = document.createElement('div');
|
||||||
|
hintDiv.className = 'text-yellow-400 italic text-center py-4';
|
||||||
|
hintDiv.id = 'consoleSaveHint';
|
||||||
|
const hintText = typeof t !== 'undefined' ? t('settings.console.save_hint', 'Please save your settings first before logs will be displayed here.') : 'Please save your settings first before logs will be displayed here.';
|
||||||
|
hintDiv.textContent = hintText;
|
||||||
|
consoleEl.appendChild(hintDiv);
|
||||||
|
} else if (consoleEl) {
|
||||||
|
// Just clear initial placeholder if it exists
|
||||||
|
const placeholder = consoleEl.querySelector('.text-gray-500');
|
||||||
|
if (placeholder && placeholder.textContent === 'Console output will appear here...') {
|
||||||
|
placeholder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendConsoleLog(message, timestamp) {
|
||||||
|
if (!consoleOutputElement) {
|
||||||
|
consoleOutputElement = document.getElementById('consoleOutputWindow');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!consoleOutputElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove initial placeholder message
|
||||||
|
const placeholder = consoleOutputElement.querySelector('.text-gray-500');
|
||||||
|
if (placeholder && placeholder.textContent === 'Console output will appear here...') {
|
||||||
|
placeholder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove save hint when first log arrives
|
||||||
|
const saveHint = document.getElementById('consoleSaveHint');
|
||||||
|
if (saveHint) {
|
||||||
|
saveHint.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create log line
|
||||||
|
const logLine = document.createElement('div');
|
||||||
|
logLine.className = 'text-green-400 leading-relaxed';
|
||||||
|
|
||||||
|
// Format timestamp if provided
|
||||||
|
let timeStr = '';
|
||||||
|
if (timestamp) {
|
||||||
|
try {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
timeStr = '<span class="text-gray-500">[' + date.toLocaleTimeString() + ']</span> ';
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore timestamp parsing errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape HTML to prevent XSS (but preserve timestamp HTML)
|
||||||
|
let escapedMessage = message;
|
||||||
|
if (typeof escapeHtml === 'function') {
|
||||||
|
escapedMessage = escapeHtml(escapedMessage);
|
||||||
|
} else {
|
||||||
|
// Fallback HTML escaping
|
||||||
|
escapedMessage = escapedMessage
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color code different log levels
|
||||||
|
let logClass = 'text-green-400';
|
||||||
|
const msgLower = message.toLowerCase();
|
||||||
|
if (msgLower.includes('error') || msgLower.includes('fatal')) {
|
||||||
|
logClass = 'text-red-400';
|
||||||
|
} else if (msgLower.includes('warning') || msgLower.includes('warn')) {
|
||||||
|
logClass = 'text-yellow-400';
|
||||||
|
} else if (msgLower.includes('info') || msgLower.includes('debug')) {
|
||||||
|
logClass = 'text-blue-400';
|
||||||
|
}
|
||||||
|
|
||||||
|
logLine.className = logClass + ' leading-relaxed';
|
||||||
|
logLine.innerHTML = timeStr + escapedMessage;
|
||||||
|
|
||||||
|
// Add to console
|
||||||
|
consoleOutputElement.appendChild(logLine);
|
||||||
|
|
||||||
|
// Limit number of lines
|
||||||
|
const lines = consoleOutputElement.children;
|
||||||
|
if (lines.length > maxConsoleLines) {
|
||||||
|
consoleOutputElement.removeChild(lines[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-scroll to bottom
|
||||||
|
consoleOutputElement.scrollTop = consoleOutputElement.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConsole() {
|
||||||
|
if (!consoleOutputElement) {
|
||||||
|
consoleOutputElement = document.getElementById('consoleOutputWindow');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consoleOutputElement) {
|
||||||
|
consoleOutputElement.textContent = '';
|
||||||
|
// Note: wasConsoleEnabledOnLoad remains true, so hint won't show again after clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on page load
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Check if console output is already enabled on page load
|
||||||
|
const checkbox = document.getElementById('consoleOutput');
|
||||||
|
if (checkbox && checkbox.checked) {
|
||||||
|
wasConsoleEnabledOnLoad = true; // Mark that it was enabled on load
|
||||||
|
toggleConsoleOutput(false); // false = not a user click
|
||||||
|
}
|
||||||
|
initConsoleOutput();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Check if console output is already enabled on page load
|
||||||
|
const checkbox = document.getElementById('consoleOutput');
|
||||||
|
if (checkbox && checkbox.checked) {
|
||||||
|
wasConsoleEnabledOnLoad = true; // Mark that it was enabled on load
|
||||||
|
toggleConsoleOutput(false); // false = not a user click
|
||||||
|
}
|
||||||
|
initConsoleOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,15 @@ function loadSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('debugMode').checked = data.debug || false;
|
document.getElementById('debugMode').checked = data.debug || false;
|
||||||
|
const consoleOutputEl = document.getElementById('consoleOutput');
|
||||||
|
if (consoleOutputEl) {
|
||||||
|
consoleOutputEl.checked = data.consoleOutput || false;
|
||||||
|
// Mark that console was enabled on load (settings were already saved)
|
||||||
|
if (typeof wasConsoleEnabledOnLoad !== 'undefined') {
|
||||||
|
wasConsoleEnabledOnLoad = consoleOutputEl.checked;
|
||||||
|
}
|
||||||
|
toggleConsoleOutput(false); // false = not a user click, loading from saved settings
|
||||||
|
}
|
||||||
|
|
||||||
// Set callback URL and add auto-update listener for port changes
|
// Set callback URL and add auto-update listener for port changes
|
||||||
const callbackURLInput = document.getElementById('callbackURL');
|
const callbackURLInput = document.getElementById('callbackURL');
|
||||||
@@ -201,6 +210,7 @@ function saveSettings(event) {
|
|||||||
language: document.getElementById('languageSelect').value,
|
language: document.getElementById('languageSelect').value,
|
||||||
port: currentPort,
|
port: currentPort,
|
||||||
debug: document.getElementById('debugMode').checked,
|
debug: document.getElementById('debugMode').checked,
|
||||||
|
consoleOutput: document.getElementById('consoleOutput') ? document.getElementById('consoleOutput').checked : false,
|
||||||
destemail: document.getElementById('destEmail').value.trim(),
|
destemail: document.getElementById('destEmail').value.trim(),
|
||||||
callbackUrl: callbackUrl,
|
callbackUrl: callbackUrl,
|
||||||
callbackSecret: document.getElementById('callbackSecret').value.trim(),
|
callbackSecret: document.getElementById('callbackSecret').value.trim(),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class WebSocketManager {
|
|||||||
this.lastBanEventId = null;
|
this.lastBanEventId = null;
|
||||||
this.statusCallbacks = [];
|
this.statusCallbacks = [];
|
||||||
this.banEventCallbacks = [];
|
this.banEventCallbacks = [];
|
||||||
|
this.consoleLogCallbacks = [];
|
||||||
|
|
||||||
// Connection metrics for tooltip
|
// Connection metrics for tooltip
|
||||||
this.connectedAt = null;
|
this.connectedAt = null;
|
||||||
@@ -58,11 +59,20 @@ class WebSocketManager {
|
|||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
this.ws.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data);
|
// WebSocket may send multiple JSON messages separated by newlines
|
||||||
this.messageCount++;
|
// Split by newlines and parse each message separately
|
||||||
this.handleMessage(message);
|
const messages = event.data.split('\n').filter(line => line.trim().length > 0);
|
||||||
|
for (const messageText of messages) {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(messageText);
|
||||||
|
this.messageCount++;
|
||||||
|
this.handleMessage(message);
|
||||||
|
} catch (parseErr) {
|
||||||
|
console.error('Error parsing individual WebSocket message:', parseErr, 'Raw:', messageText);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error parsing WebSocket message:', err);
|
console.error('Error processing WebSocket message:', err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,11 +122,27 @@ class WebSocketManager {
|
|||||||
case 'heartbeat':
|
case 'heartbeat':
|
||||||
this.handleHeartbeat(message);
|
this.handleHeartbeat(message);
|
||||||
break;
|
break;
|
||||||
|
case 'console_log':
|
||||||
|
this.handleConsoleLog(message);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Unknown message type:', message.type);
|
console.log('Unknown message type:', message.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleConsoleLog(message) {
|
||||||
|
// Notify all registered console log callbacks
|
||||||
|
if (this.consoleLogCallbacks) {
|
||||||
|
this.consoleLogCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(message.message, message.time);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in console log callback:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleBanEvent(eventData) {
|
handleBanEvent(eventData) {
|
||||||
// Check if we've already processed this event (prevent duplicates)
|
// Check if we've already processed this event (prevent duplicates)
|
||||||
// Only check if event has an ID and we have a lastBanEventId
|
// Only check if event has an ID and we have a lastBanEventId
|
||||||
@@ -170,6 +196,13 @@ class WebSocketManager {
|
|||||||
this.banEventCallbacks.push(callback);
|
this.banEventCallbacks.push(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onConsoleLog(callback) {
|
||||||
|
if (!this.consoleLogCallbacks) {
|
||||||
|
this.consoleLogCallbacks = [];
|
||||||
|
}
|
||||||
|
this.consoleLogCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
|
|||||||
@@ -231,9 +231,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Debug Log Output -->
|
<!-- Debug Log Output -->
|
||||||
<div class="flex items-center border border-gray-200 rounded-lg p-2 overflow-x-auto bg-gray-50">
|
<div class="flex items-center gap-4 border border-gray-200 rounded-lg p-2 overflow-x-auto bg-gray-50">
|
||||||
<input type="checkbox" id="debugMode" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out">
|
<div class="flex items-center">
|
||||||
<label for="debugMode" class="ml-2 block text-sm text-gray-700" data-i18n="settings.enable_debug">Enable Debug Log</label>
|
<input type="checkbox" id="debugMode" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out">
|
||||||
|
<label for="debugMode" class="ml-2 block text-sm text-gray-700" data-i18n="settings.enable_debug">Enable Debug Log</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input type="checkbox" id="consoleOutput" class="h-4 w-7 text-blue-600 transition duration-150 ease-in-out" onchange="toggleConsoleOutput(true)">
|
||||||
|
<label for="consoleOutput" class="ml-2 block text-sm text-gray-700" data-i18n="settings.enable_console">Enable Console Output</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Console Output Window -->
|
||||||
|
<div id="consoleOutputContainer" class="hidden mt-4 border border-gray-700 rounded-lg bg-gray-900 shadow-lg overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between bg-gray-800 px-4 py-2 border-b border-gray-700">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex gap-1.5">
|
||||||
|
<div class="w-3 h-3 rounded-full bg-red-500"></div>
|
||||||
|
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||||
|
<div class="w-3 h-3 rounded-full bg-green-500"></div>
|
||||||
|
</div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-100 ml-2" data-i18n="settings.console.title">Console Output</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="clearConsole()" class="text-xs text-gray-100 hover:text-white px-2 py-1 rounded hover:bg-gray-700 transition-colors" data-i18n="settings.console.clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="consoleOutputWindow" class="overflow-y-auto p-4 font-mono text-sm text-green-400 bg-gray-900" style="max-height: 430px; min-height: 200px;">
|
||||||
|
<div class="text-gray-500">Console output will appear here...</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1355,6 +1379,7 @@
|
|||||||
<script src="/static/js/dashboard.js?v={{.version}}"></script>
|
<script src="/static/js/dashboard.js?v={{.version}}"></script>
|
||||||
<script src="/static/js/servers.js?v={{.version}}"></script>
|
<script src="/static/js/servers.js?v={{.version}}"></script>
|
||||||
<script src="/static/js/jails.js?v={{.version}}"></script>
|
<script src="/static/js/jails.js?v={{.version}}"></script>
|
||||||
|
<script src="/static/js/console.js?v={{.version}}"></script>
|
||||||
<script src="/static/js/settings.js?v={{.version}}"></script>
|
<script src="/static/js/settings.js?v={{.version}}"></script>
|
||||||
<script src="/static/js/filters.js?v={{.version}}"></script>
|
<script src="/static/js/filters.js?v={{.version}}"></script>
|
||||||
<script src="/static/js/websocket.js?v={{.version}}"></script>
|
<script src="/static/js/websocket.js?v={{.version}}"></script>
|
||||||
|
|||||||
@@ -73,6 +73,26 @@ type Hub struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BroadcastConsoleLog broadcasts a console log message to all connected clients
|
||||||
|
func (h *Hub) BroadcastConsoleLog(message string) {
|
||||||
|
logMsg := map[string]interface{}{
|
||||||
|
"type": "console_log",
|
||||||
|
"message": message,
|
||||||
|
"time": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(logMsg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error marshaling console log: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case h.broadcast <- data:
|
||||||
|
default:
|
||||||
|
// Channel full, drop message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewHub creates a new WebSocket hub
|
// NewHub creates a new WebSocket hub
|
||||||
func NewHub() *Hub {
|
func NewHub() *Hub {
|
||||||
return &Hub{
|
return &Hub{
|
||||||
|
|||||||
Reference in New Issue
Block a user