From daa6d6e53804b957e1ccf61d13995b74fe960088 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 3 Feb 2026 15:38:51 +0100 Subject: [PATCH 01/25] Add server info panel and password rotation features - Updated HTML config page to include server info panel settings. - Implemented server info panel functionality in Discord bot with interactive buttons. - Added password rotation logic to automatically set a new password on server start if feature is enabled - Introduced localization for server info panel and password rotation in de-DE, en-US, and sv-SE JSON files. --- UIMod/onboard_bundled/localization/de-DE.json | 4 + UIMod/onboard_bundled/localization/en-US.json | 4 + UIMod/onboard_bundled/localization/sv-SE.json | 4 + UIMod/onboard_bundled/ui/config.html | 16 ++ src/config/config.go | 33 ++-- src/config/getters.go | 12 ++ src/config/vars.go | 28 ++-- src/discordbot/interface.go | 4 +- src/discordbot/serverinfopanel.go | 156 ++++++++++++++++++ src/managers/gamemgr/passwordrotation.go | 51 ++++++ src/managers/gamemgr/processmanagement.go | 3 + src/web/configpage.go | 16 ++ src/web/templatevars.go | 8 + 13 files changed, 313 insertions(+), 26 deletions(-) create mode 100644 src/discordbot/serverinfopanel.go create mode 100644 src/managers/gamemgr/passwordrotation.go diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index bdca48cf..fd086983 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -117,6 +117,10 @@ "UIText_AdminCommandChannelInfo": "Channel für Admin-Befehle", "UIText_ControlPanelChannel": "Kontrollpanel Channel", "UIText_ControlPanelChannelInfo": "Channel für Kontrollpanel", + "UIText_ServerInfoPanelChannel": "Server-Info-Panel Channel", + "UIText_ServerInfoPanelChannelInfo": "Optionaler Channel, der den Servernamen für Benutzer (nicht Admins) anzeigt und Benutzern die Möglichkeit gibt, das aktuelle Server-Passwort abzurufen.", + "UIText_RotateServerPassword": "Server-Passwort rotieren", + "UIText_RotateServerPasswordInfo": "Rotiert das Server-Passwort automatisch bei jedem Serverstart. Spieler können das aktuelle Passwort über einen Button im \"Server-Info-Panel Channel\" abrufen.", "UIText_StatusChannel": "Statuskanal", "UIText_StatusChannelInfo": "Server Status Updates", "UIText_ConnectionListChannel": "Verbindungslisten Channel", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 7cf978d3..3efd03e6 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -117,6 +117,10 @@ "UIText_AdminCommandChannelInfo": "Channel for admin commands", "UIText_ControlPanelChannel": "Control Panel Channel", "UIText_ControlPanelChannelInfo": "Channel for control panel", + "UIText_ServerInfoPanelChannel": "Server Info Panel Channel", + "UIText_ServerInfoPanelChannelInfo": "Optional channel which shows server name for users (not admins) and gives users the option to retrieve the current server password.", + "UIText_RotateServerPassword": "Rotate Server Password", + "UIText_RotateServerPasswordInfo": "Automatically rotates the server password on each server start. Players can get the current password via a button in the \"Server Info Panel Channel\"", "UIText_StatusChannel": "Status Channel", "UIText_StatusChannelInfo": "Server status updates", "UIText_ConnectionListChannel": "Connection List Channel", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 63865092..12c70f86 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -115,6 +115,10 @@ "UIText_AdminCommandChannelInfo": "Kanal för admin-kommandon", "UIText_ControlPanelChannel": "Kontrollpanelkanal", "UIText_ControlPanelChannelInfo": "Kanal för kontrollpanel", + "UIText_ServerInfoPanelChannel": "Serverinfo-panelkanal", + "UIText_ServerInfoPanelChannelInfo": "Valfri kanal som visar servernamn för användare (inte administratörer) och ger användare möjligheten att hämta det aktuella serverlösenordet.", + "UIText_RotateServerPassword": "Rotera serverlösenord", + "UIText_RotateServerPasswordInfo": "Roterar automatiskt serverlösenordet vid varje serverstart. Spelare kan hämta det aktuella lösenordet via en knapp i \"Serverinfo-panelkanalen\".", "UIText_StatusChannel": "Statuskanal", "UIText_StatusChannelInfo": "Uppdateringar om serverstatus", "UIText_ConnectionListChannel": "Anslutningslistkanal", diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 206182a6..9cc029e7 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -388,6 +388,22 @@

{{.UIText_ChannelConfiguration}}

{{.UIText_ControlPanelChannelInfo}}
+
+ + +
{{.UIText_ServerInfoPanelChannelInfo}}
+
+ +
+ + +
{{.UIText_RotateServerPasswordInfo}}
+
+
diff --git a/src/config/config.go b/src/config/config.go index ddb525cb..ffbdd547 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -11,7 +11,7 @@ import ( var ( // All configuration variables can be found in vars.go - Version = "5.11.1" + Version = "5.12.0" Branch = "release" ) @@ -87,17 +87,19 @@ type JsonConfig struct { IsStationeersLaunchPadAutoUpdatesEnabled *bool `json:"IsStationeersLaunchPadAutoUpdatesEnabled"` // Discord Settings - DiscordToken string `json:"discordToken"` - ControlChannelID string `json:"controlChannelID"` - StatusChannelID string `json:"statusChannelID"` - ConnectionListChannelID string `json:"connectionListChannelID"` - LogChannelID string `json:"logChannelID"` - SaveChannelID string `json:"saveChannelID"` - ControlPanelChannelID string `json:"controlPanelChannelID"` - DiscordCharBufferSize int `json:"DiscordCharBufferSize"` - BlackListFilePath string `json:"blackListFilePath"` - IsDiscordEnabled *bool `json:"isDiscordEnabled"` - ErrorChannelID string `json:"errorChannelID"` + DiscordToken string `json:"discordToken"` + ControlChannelID string `json:"controlChannelID"` + StatusChannelID string `json:"statusChannelID"` + ConnectionListChannelID string `json:"connectionListChannelID"` + LogChannelID string `json:"logChannelID"` + SaveChannelID string `json:"saveChannelID"` + ControlPanelChannelID string `json:"controlPanelChannelID"` + ServerInfoPanelChannelID string `json:"serverInfoPanelChannelID"` + DiscordCharBufferSize int `json:"DiscordCharBufferSize"` + BlackListFilePath string `json:"blackListFilePath"` + IsDiscordEnabled *bool `json:"isDiscordEnabled"` + RotateServerPassword *bool `json:"rotateServerPassword"` + ErrorChannelID string `json:"errorChannelID"` //Backup Settings BackupKeepLastN int `json:"backupKeepLastN"` // Number of most recent backups to keep (default: 2000) @@ -146,6 +148,7 @@ func applyConfig(cfg *JsonConfig) { LogChannelID = getString(cfg.LogChannelID, "LOG_CHANNEL_ID", "") SaveChannelID = getString(cfg.SaveChannelID, "SAVE_CHANNEL_ID", "") ControlPanelChannelID = getString(cfg.ControlPanelChannelID, "CONTROL_PANEL_CHANNEL_ID", "") + ServerInfoPanelChannelID = getString(cfg.ServerInfoPanelChannelID, "SERVER_INFO_PANEL_CHANNEL_ID", "") DiscordCharBufferSize = getInt(cfg.DiscordCharBufferSize, "DISCORD_CHAR_BUFFER_SIZE", 1000) BlackListFilePath = getString(cfg.BlackListFilePath, "BLACKLIST_FILE_PATH", "./Blacklist.txt") @@ -153,6 +156,10 @@ func applyConfig(cfg *JsonConfig) { IsDiscordEnabled = isDiscordEnabledVal cfg.IsDiscordEnabled = &isDiscordEnabledVal + rotateServerPasswordVal := getBool(cfg.RotateServerPassword, "ROTATE_SERVER_PASSWORD", false) + RotateServerPassword = rotateServerPasswordVal + cfg.RotateServerPassword = &rotateServerPasswordVal + ErrorChannelID = getString(cfg.ErrorChannelID, "ERROR_CHANNEL_ID", "") BackupKeepLastN = getInt(cfg.BackupKeepLastN, "BACKUP_KEEP_LAST_N", 2000) @@ -345,9 +352,11 @@ func safeSaveConfig() error { LogChannelID: LogChannelID, SaveChannelID: SaveChannelID, ControlPanelChannelID: ControlPanelChannelID, + ServerInfoPanelChannelID: ServerInfoPanelChannelID, DiscordCharBufferSize: DiscordCharBufferSize, BlackListFilePath: BlackListFilePath, IsDiscordEnabled: &IsDiscordEnabled, + RotateServerPassword: &RotateServerPassword, ErrorChannelID: ErrorChannelID, BackupKeepLastN: BackupKeepLastN, IsCleanupEnabled: &IsCleanupEnabled, diff --git a/src/config/getters.go b/src/config/getters.go index 16a12724..bc5c256b 100644 --- a/src/config/getters.go +++ b/src/config/getters.go @@ -46,6 +46,12 @@ func GetControlPanelChannelID() string { return ControlPanelChannelID } +func GetServerInfoPanelChannelID() string { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return ServerInfoPanelChannelID +} + func GetDiscordCharBufferSize() int { ConfigMu.RLock() defer ConfigMu.RUnlock() @@ -64,6 +70,12 @@ func GetIsDiscordEnabled() bool { return IsDiscordEnabled } +func GetRotateServerPassword() bool { + ConfigMu.RLock() + defer ConfigMu.RUnlock() + return RotateServerPassword +} + func GetErrorChannelID() string { ConfigMu.RLock() defer ConfigMu.RUnlock() diff --git a/src/config/vars.go b/src/config/vars.go index e9f6f57b..7ec6fd76 100644 --- a/src/config/vars.go +++ b/src/config/vars.go @@ -80,19 +80,21 @@ var ( // Discord integration var ( - DiscordToken string - DiscordSession *discordgo.Session - IsDiscordEnabled bool - ControlChannelID string - StatusChannelID string - LogChannelID string - ErrorChannelID string - ConnectionListChannelID string - SaveChannelID string - ControlPanelChannelID string - DiscordCharBufferSize int - ExceptionMessageID string - BlackListFilePath string + DiscordToken string + DiscordSession *discordgo.Session + IsDiscordEnabled bool + RotateServerPassword bool + ControlChannelID string + StatusChannelID string + LogChannelID string + ErrorChannelID string + ConnectionListChannelID string + SaveChannelID string + ControlPanelChannelID string + ServerInfoPanelChannelID string + DiscordCharBufferSize int + ExceptionMessageID string + BlackListFilePath string ) // Backup and cleanup settings diff --git a/src/discordbot/interface.go b/src/discordbot/interface.go index 773f8f51..1d47da84 100644 --- a/src/discordbot/interface.go +++ b/src/discordbot/interface.go @@ -50,11 +50,13 @@ func InitializeDiscordBot() { // Register handlers and commands after session is open config.DiscordSession.AddHandler(listenToDiscordReactions) config.DiscordSession.AddHandler(listenToSlashCommands) + config.DiscordSession.AddHandler(handleServerInfoButtonInteraction) // Handle button interactions registerSlashCommands(config.DiscordSession) logger.Discord.Info("Bot is now running.") SendMessageToStatusChannel("🤖 SSUI Version " + config.GetVersion() + " connected to Discord.") - sendControlPanel() // Send control panel message to Discord + sendControlPanel() // Send control panel message to Discord + sendServerInfoPanel() // Send server info panel with buttons to Discord UpdateBotStatusWithMessage("StationeersServerUI v" + config.GetVersion()) // Start buffer flush ticker BufferFlushTicker = time.NewTicker(5 * time.Second) diff --git a/src/discordbot/serverinfopanel.go b/src/discordbot/serverinfopanel.go new file mode 100644 index 00000000..ff2f3110 --- /dev/null +++ b/src/discordbot/serverinfopanel.go @@ -0,0 +1,156 @@ +package discordbot + +import ( + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" + "github.com/bwmarrin/discordgo" +) + +// ServerInfoMessageID stores the ID of the server info panel message +var ServerInfoMessageID string + +// Custom IDs for button interactions +const ( + ButtonGetPassword = "ssui_get_password" +) + +// sendServerInfoPanel sends the server info panel with interactive buttons +func sendServerInfoPanel() { + if !config.GetIsDiscordEnabled() { + return + } + + channelID := config.GetServerInfoPanelChannelID() + if channelID == "" { + logger.Discord.Debug("Server info panel channel ID not configured, skipping panel") + return + } + + serverName := config.GetServerName() + + // Create an embed for the server info panel + embed := &discordgo.MessageEmbed{ + Title: "🎮 Server Information", + Description: "Your Stationeers server is running as **" + serverName + "**", + Color: 0x5865F2, // Discord blurple color + Fields: []*discordgo.MessageEmbedField{ + { + Name: "Server Name", + Value: serverName, + Inline: true, + }, + { + Name: "SSUI Version", + Value: config.GetVersion(), + Inline: true, + }, + }, + Timestamp: time.Now().Format(time.RFC3339), + } + + // Build message components (buttons in an action row) + var components []discordgo.MessageComponent + + components = append(components, discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.Button{ + Label: "🔑 Get Current Server Password", + Style: discordgo.PrimaryButton, + CustomID: ButtonGetPassword, + }, + }, + }) + + // Send the message with embed and buttons + msg, err := config.DiscordSession.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ + Embeds: []*discordgo.MessageEmbed{embed}, + Components: components, + }) + if err != nil { + logger.Discord.Error("Error sending server info panel: " + err.Error()) + return + } + + clearMessagesAboveLastN(channelID, 1) // Clear all old server info panel messages + ServerInfoMessageID = msg.ID + logger.Discord.Info("Server info panel sent successfully") +} + +// handleServerInfoButtonInteraction handles button interactions from the server info panel +func handleServerInfoButtonInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { + // Only handle component interactions (buttons) + if i.Type != discordgo.InteractionMessageComponent { + return + } + + // Get the custom ID of the button that was clicked + customID := i.MessageComponentData().CustomID + + switch customID { + case ButtonGetPassword: + handleGetPasswordButton(s, i) + default: + // Not our button, ignore + return + } +} + +// handleGetPasswordButton sends the current server password as an ephemeral message (only visible to the user) +func handleGetPasswordButton(s *discordgo.Session, i *discordgo.InteractionCreate) { + + // Get the current server password from config + password := config.GetServerPassword() + + // Build the embed based on whether password is set + var embed *discordgo.MessageEmbed + if password == "" { + embed = &discordgo.MessageEmbed{ + Title: "🔓 No Password Set", + Description: "No password is currently configured for this server.", + Color: 0xFFA500, // Orange + Footer: &discordgo.MessageEmbedFooter{ + Text: "This message will disappear in 30 seconds", + }, + } + } else { + embed = &discordgo.MessageEmbed{ + Title: "🔑 Current Server Password", + Description: "Use this password to connect to the server.", + Color: 0x00FF00, // Green + Fields: []*discordgo.MessageEmbedField{ + { + Name: "Password", + Value: "```" + password + "```", + Inline: false, + }, + }, + Footer: &discordgo.MessageEmbedFooter{ + Text: "🔒 Only visible to you • Disappears in 30 seconds", + }, + Timestamp: time.Now().Format(time.RFC3339), + } + } + + // Respond with an ephemeral message (only visible to the user who clicked) + err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + logger.Discord.Error("Error responding to get password button: " + err.Error()) + return + } + + go func() { + time.Sleep(30 * time.Second) + err := s.InteractionResponseDelete(i.Interaction) + if err != nil { + logger.Discord.Debug("Could not delete ephemeral password message: " + err.Error()) + } + }() +} diff --git a/src/managers/gamemgr/passwordrotation.go b/src/managers/gamemgr/passwordrotation.go new file mode 100644 index 00000000..8afb8ae1 --- /dev/null +++ b/src/managers/gamemgr/passwordrotation.go @@ -0,0 +1,51 @@ +// passwordrotation.go +package gamemgr + +import ( + "math/rand" + "time" + + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" + "github.com/JacksonTheMaster/StationeersServerUI/v5/src/logger" +) + +// Predefined list of Stationeers-themed passwords +var stationeersPasswords = []string{ + "Stationeer", + "Mimas", + "Europa", + "Lunar", + "Vulcan", + "Venus", + "Rocket", + "Oxygen", + "Nitrogen", + "Volatiles", + "Hardsuit", + "Jetpack", + "Airlock", + "Station", +} + +// rotatePasswordIfEnabled checks if password rotation is enabled and sets a new random password +func rotatePasswordIfEnabled() { + + if !config.GetIsDiscordEnabled() || config.GetServerInfoPanelChannelID() == "" || !config.GetRotateServerPassword() { + return + } + + // Seed the random number generator + rng := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Select a random password from the list + newPassword := stationeersPasswords[rng.Intn(len(stationeersPasswords))] + + // Set the new password in config + err := config.SetServerPassword(newPassword) + if err != nil { + logger.Core.Error("Failed to set rotated server password: " + err.Error()) + return + } + + logger.Core.Info("Password rotation enabled - new server password set: " + newPassword) +} diff --git a/src/managers/gamemgr/processmanagement.go b/src/managers/gamemgr/processmanagement.go index 280eba9c..fbd46bff 100644 --- a/src/managers/gamemgr/processmanagement.go +++ b/src/managers/gamemgr/processmanagement.go @@ -33,6 +33,9 @@ func InternalStartServer() error { return fmt.Errorf("server is already running") } + // Rotate password if enabled (sets new random password before building args) + rotatePasswordIfEnabled() + args := buildCommandArgs() logger.Core.Info("=== GAMESERVER STARTING ===") diff --git a/src/web/configpage.go b/src/web/configpage.go index c39755bb..88224fd5 100644 --- a/src/web/configpage.go +++ b/src/web/configpage.go @@ -126,6 +126,14 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { isStationeersLaunchPadEnabled = "true" } + rotateServerPasswordTrueSelected := "" + rotateServerPasswordFalseSelected := "" + if config.GetRotateServerPassword() { + rotateServerPasswordTrueSelected = "selected" + } else { + rotateServerPasswordFalseSelected = "selected" + } + data := ConfigTemplateData{ // Config values DiscordToken: config.GetDiscordToken(), @@ -135,11 +143,15 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { LogChannelID: config.GetLogChannelID(), SaveChannelID: config.GetSaveChannelID(), ControlPanelChannelID: config.GetControlPanelChannelID(), + ServerInfoPanelChannelID: config.GetServerInfoPanelChannelID(), BlackListFilePath: config.GetBlackListFilePath(), ErrorChannelID: config.GetErrorChannelID(), IsDiscordEnabled: fmt.Sprintf("%v", config.GetIsDiscordEnabled()), IsDiscordEnabledTrueSelected: discordTrueSelected, IsDiscordEnabledFalseSelected: discordFalseSelected, + RotateServerPassword: fmt.Sprintf("%v", config.GetRotateServerPassword()), + RotateServerPasswordTrueSelected: rotateServerPasswordTrueSelected, + RotateServerPasswordFalseSelected: rotateServerPasswordFalseSelected, GameBranch: config.GetGameBranch(), Difficulty: config.GetDifficulty(), StartCondition: config.GetStartCondition(), @@ -276,6 +288,10 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { UIText_AdminCommandChannelInfo: localization.GetString("UIText_AdminCommandChannelInfo"), UIText_ControlPanelChannel: localization.GetString("UIText_ControlPanelChannel"), UIText_ControlPanelChannelInfo: localization.GetString("UIText_ControlPanelChannelInfo"), + UIText_ServerInfoPanelChannel: localization.GetString("UIText_ServerInfoPanelChannel"), + UIText_ServerInfoPanelChannelInfo: localization.GetString("UIText_ServerInfoPanelChannelInfo"), + UIText_RotateServerPassword: localization.GetString("UIText_RotateServerPassword"), + UIText_RotateServerPasswordInfo: localization.GetString("UIText_RotateServerPasswordInfo"), UIText_StatusChannel: localization.GetString("UIText_StatusChannel"), UIText_StatusChannelInfo: localization.GetString("UIText_StatusChannelInfo"), UIText_ConnectionListChannel: localization.GetString("UIText_ConnectionListChannel"), diff --git a/src/web/templatevars.go b/src/web/templatevars.go index dc1a9ea5..cf741c74 100644 --- a/src/web/templatevars.go +++ b/src/web/templatevars.go @@ -35,11 +35,15 @@ type ConfigTemplateData struct { LogChannelID string SaveChannelID string ControlPanelChannelID string + ServerInfoPanelChannelID string BlackListFilePath string ErrorChannelID string IsDiscordEnabled string IsDiscordEnabledTrueSelected string IsDiscordEnabledFalseSelected string + RotateServerPassword string + RotateServerPasswordTrueSelected string + RotateServerPasswordFalseSelected string GameBranch string Difficulty string StartCondition string @@ -178,6 +182,10 @@ type ConfigTemplateData struct { UIText_AdminCommandChannelInfo string UIText_ControlPanelChannel string UIText_ControlPanelChannelInfo string + UIText_ServerInfoPanelChannel string + UIText_ServerInfoPanelChannelInfo string + UIText_RotateServerPassword string + UIText_RotateServerPasswordInfo string UIText_StatusChannel string UIText_StatusChannelInfo string UIText_ConnectionListChannel string From 695ebf9a2c3f1991201f6ecc6f55f70160c251b5 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Tue, 3 Feb 2026 16:01:43 +0100 Subject: [PATCH 02/25] Add Expert Settings configuration options to the web UI --- UIMod/onboard_bundled/ui/config.html | 122 ++++++++++++++++++++++- src/config/config.go | 6 ++ src/config/getters.go | 13 +++ src/config/vars.go | 1 + src/web/configpage.go | 138 +++++++++++++++++++++++++++ src/web/templatevars.go | 48 ++++++++++ 6 files changed, 326 insertions(+), 2 deletions(-) diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 9cc029e7..94c5f042 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -300,9 +300,126 @@

{{.UIText_AdvancedConfiguration}}

{{.UIText_AdditionalParamsInfo}}
- - +
+ + +
Show the Expert Settings to access special configuration options.
+
+ + {{if eq .ShowExpertSettingsTrueSelected "selected"}} +

Expert Settings below - only change if you know what you are doing

+
+ +
+ + +
Enable debug mode for verbose logging (also enables pprof server)
+
+ +
+ + +
Log level: 10=Debug, 20=Info (default), 30=Warn, 40=Error
+
+ +
+ + +
Show verbose mono/unity logs from game server in console
+
+ +
+ + +
Port for the SSUI web interface (default: 8443). Requires restart.
+
+ +
+ + +
Enable the SSUICLI interactive console
+
+ +
+ + +
Enable Stationeers Server Command Manager integration (Linux)
+
+ +
+ + +
Override the IP address advertised to Steam. Leave empty for auto-detection.
+
+ +
+ + +
Enable automatic SSUI update checks
+
+ +
+ + +
Include beta/prerelease versions in update checks
+
+ +
+ + +
Allow automatic major version upgrades (may have breaking changes)
+
+ +
+ + +
Automatically update StationeersLaunchPad
+
+ +
+ + +
Enable authentication for the web interface
+
+ +
+ + +
JWT token lifetime in minutes (default: 1440 = 24 hours)
+
+
+ {{end}}
@@ -344,6 +461,7 @@

{{.UIText_TerrainSettingsHeader}}

+
- - +
+ + +
Show the Expert Settings to access special configuration options.
+
+ + {{if eq .ShowExpertSettingsTrueSelected "selected"}} +

Expert Settings below - only change if you know what you are doing

+
+ +
+ + +
Enable debug mode for verbose logging (also enables pprof server)
+
+ +
+ + +
Log level: 10=Debug, 20=Info (default), 30=Warn, 40=Error
+
+ +
+ + +
Show verbose mono/unity logs from game server in console
+
+ +
+ + +
Port for the SSUI web interface (default: 8443). Requires restart.
+
+ +
+ + +
Enable the SSUICLI interactive console
+
+ +
+ + +
Enable Stationeers Server Command Manager integration (Linux)
+
+ +
+ + +
Override the IP address advertised to Steam. Leave empty for auto-detection.
+
+ +
+ + +
Enable automatic SSUI update checks
+
+ +
+ + +
Include beta/prerelease versions in update checks
+
+ +
+ + +
Allow automatic major version upgrades (may have breaking changes)
+
+ +
+ + +
Automatically update StationeersLaunchPad
+
+ +
+ + +
Enable authentication for the web interface
+
+ +
+ + +
JWT token lifetime in minutes (default: 1440 = 24 hours)
+
+
+ {{end}}
@@ -344,6 +461,7 @@

{{.UIText_TerrainSettingsHeader}}

+
- +
+ ${isDotsave ? `` : ''} + +
`; backupList.appendChild(li); @@ -208,6 +212,48 @@ function restoreBackup(index) { .catch(err => console.error(`Failed to restore backup ${index}:`, err)); } +function downloadBackup(index) { + const status = document.getElementById('status'); + status.hidden = false; + typeTextWithCallback(status, 'Preparing download...', 20, () => {}); + + fetch('/api/v2/backups/download', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ index: index }) + }) + .then(response => { + if (!response.ok) { + return response.json().then(err => { throw new Error(err.error || 'Download failed'); }); + } + const disposition = response.headers.get('Content-Disposition'); + let filename = `backup_${index}.save`; + if (disposition) { + const match = disposition.match(/filename="(.+)"/); + if (match) filename = match[1]; + } + return response.blob().then(blob => ({ blob, filename })); + }) + .then(({ blob, filename }) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + a.remove(); + status.hidden = true; + }) + .catch(err => { + console.error(`Failed to download backup ${index}:`, err); + showPopup('error', 'Download failed: ' + err.message); + status.hidden = true; + }); +} + function pollRecurringTasks() { window.gamserverstate = false; diff --git a/src/managers/backupmgr/backuphttp.go b/src/managers/backupmgr/backuphttp.go index 9fbc7cf9..e4b519ab 100644 --- a/src/managers/backupmgr/backuphttp.go +++ b/src/managers/backupmgr/backuphttp.go @@ -90,3 +90,45 @@ func (h *HTTPHandler) RestoreBackupHandler(w http.ResponseWriter, r *http.Reques w.Write([]byte("Server stopped & Backup restored successfully, Start the server to load the restored backup")) } + +// DownloadBackupRequest represents the JSON request for downloading a backup +type DownloadBackupRequest struct { + Index int `json:"index"` +} + +// DownloadBackupHandler handles requests to download a backup file +func (h *HTTPHandler) DownloadBackupHandler(w http.ResponseWriter, r *http.Request) { + logger.Web.Debug("Received backup download request") + + if r.Method != http.MethodPost { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusMethodNotAllowed) + json.NewEncoder(w).Encode(map[string]string{"error": "method not allowed, use POST"}) + return + } + + var req DownloadBackupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid JSON request body"}) + return + } + + backupData, err := h.manager.GetBackupFileData(req.Index) + if err != nil { + w.Header().Set("Content-Type", "application/json") + if strings.Contains(err.Error(), "out of range") { + w.WriteHeader(http.StatusNotFound) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", backupData.Filename)) + w.Header().Set("Content-Length", fmt.Sprintf("%d", backupData.Size)) + w.Write(backupData.Data) +} diff --git a/src/managers/backupmgr/manager.go b/src/managers/backupmgr/manager.go index 02a0a957..c1488f57 100644 --- a/src/managers/backupmgr/manager.go +++ b/src/managers/backupmgr/manager.go @@ -223,6 +223,38 @@ func (m *BackupManager) ListBackups(limit int) ([]BackupSaveFile, error) { return saves, nil } +// GetBackupFileData retrieves backup file data by index for download/transfer +func (m *BackupManager) GetBackupFileData(index int) (*BackupFileData, error) { + m.mu.Lock() + defer m.mu.Unlock() + + saves, err := m.getBackupSaveFiles() + if err != nil { + return nil, fmt.Errorf("failed to get backup files: %w", err) + } + + if index < 0 || index >= len(saves) { + return nil, fmt.Errorf("backup index %d out of range (0-%d)", index, len(saves)-1) + } + + targetSave := saves[index] + filePath := targetSave.SaveFile + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read backup file: %w", err) + } + + filename := filepath.Base(filePath) + + return &BackupFileData{ + Data: data, + Filename: filename, + Size: int64(len(data)), + SaveTime: targetSave.SaveTime, + }, nil +} + // Shutdown stops all backup operations func (m *BackupManager) Shutdown() { logger.Backup.Debug("Shutting down previous backup manager...") diff --git a/src/managers/backupmgr/types.go b/src/managers/backupmgr/types.go index 721b861c..842fea9c 100644 --- a/src/managers/backupmgr/types.go +++ b/src/managers/backupmgr/types.go @@ -35,6 +35,14 @@ type BackupSaveFile struct { SaveTime time.Time } +// BackupFileData contains the backup file bytes and metadata for download/transfer +type BackupFileData struct { + Data []byte + Filename string + Size int64 + SaveTime time.Time +} + // BackupManager manages backup operations type BackupManager struct { config BackupConfig diff --git a/src/web/routes.go b/src/web/routes.go index 0011c0ea..94ab1fed 100644 --- a/src/web/routes.go +++ b/src/web/routes.go @@ -58,6 +58,7 @@ func SetupRoutes() (*http.ServeMux, *http.ServeMux) { backupHandler := backupmgr.NewHTTPHandler(backupmgr.GlobalBackupManager) protectedMux.HandleFunc("/api/v2/backups", backupHandler.ListBackupsHandler) protectedMux.HandleFunc("/api/v2/backups/restore", backupHandler.RestoreBackupHandler) + protectedMux.HandleFunc("/api/v2/backups/download", backupHandler.DownloadBackupHandler) // Configuration protectedMux.HandleFunc("/saveconfigasjson", configchanger.SaveConfigForm) // legacy, used on config page From 0227b596e07223c83e4acef1b5b471b840504fc0 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 6 Feb 2026 17:46:11 +0100 Subject: [PATCH 19/25] feat: Add Discord download command and button interaction for backup downloads (uploads file to discord) --- src/discordbot/handleSlashcommands.go | 123 +++++++++++++++++++++++- src/discordbot/interface.go | 1 + src/discordbot/registerSlashcommands.go | 12 +++ src/discordbot/serverinfopanel.go | 3 +- 4 files changed, 137 insertions(+), 2 deletions(-) diff --git a/src/discordbot/handleSlashcommands.go b/src/discordbot/handleSlashcommands.go index d2b0b9ff..88647a02 100644 --- a/src/discordbot/handleSlashcommands.go +++ b/src/discordbot/handleSlashcommands.go @@ -1,6 +1,7 @@ package discordbot import ( + "bytes" "fmt" "sort" "strconv" @@ -27,6 +28,7 @@ var handlers = map[string]commandHandler{ "help": handleHelp, "restore": handleRestore, "list": handleList, + "download": handleDownload, "bansteamid": handleBan, "unbansteamid": handleUnban, "update": handleUpdate, @@ -142,6 +144,7 @@ func handleHelp(s *discordgo.Session, i *discordgo.InteractionCreate, data Embed {Name: "/update", Value: "Updates the gameserver via SteamCMD"}, {Name: "/list [limit]", Value: "Lists recent backups (default: 5)"}, {Name: "/restore ", Value: "Restores a backup"}, + {Name: "/download [index]", Value: "Downloads a backup (most recent if no index)"}, {Name: "/bansteamid ", Value: "Bans a player"}, {Name: "/unbansteamid ", Value: "Unbans a player"}, {Name: "/command ", Value: "Sends a command to the gameserver console"}, @@ -174,6 +177,63 @@ func handleRestore(s *discordgo.Session, i *discordgo.InteractionCreate, data Em return nil } +const maxDiscordFileSize = 10 * 1024 * 1024 // 10MB Discord file upload limit + +func handleDownload(s *discordgo.Session, i *discordgo.InteractionCreate, data EmbedData) error { + index := -1 // -1 means most recent + + if len(i.ApplicationCommandData().Options) > 0 { + index = int(i.ApplicationCommandData().Options[0].IntValue()) + } + + // If no index provided, get the most recent backup index + if index == -1 { + backups, err := backupmgr.GlobalBackupManager.ListBackups(1) + if err != nil || len(backups) == 0 { + data.Title, data.Description = "Download Failed", "No backups available" + data.Fields = []EmbedField{{Name: "Error", Value: "Could not find any backups", Inline: true}} + return respond(s, i, data) + } + index = backups[0].Index + } + + data.Title, data.Description, data.Color = "📥 Backup Download", fmt.Sprintf("Preparing backup #%d for download...", index), 0xFFA500 + data.Fields = []EmbedField{{Name: "Status", Value: "🕛 Processing", Inline: true}} + if err := respond(s, i, data); err != nil { + return err + } + + sendBackupToChannel(s, i.ChannelID, index) + return nil +} + +func sendBackupToChannel(s *discordgo.Session, channelID string, index int) { + backupData, err := backupmgr.GlobalBackupManager.GetBackupFileData(index) + if err != nil { + s.ChannelMessageSend(channelID, fmt.Sprintf("❌ Failed to download backup #%d: %v", index, err)) + return + } + + if backupData.Size > maxDiscordFileSize { + s.ChannelMessageSend(channelID, fmt.Sprintf("❌ Backup #%d is too large to upload (%.2f MB > 10 MB limit)", index, float64(backupData.Size)/(1024*1024))) + return + } + + file := &discordgo.File{ + Name: backupData.Filename, + ContentType: "application/octet-stream", + Reader: bytes.NewReader(backupData.Data), + } + + _, err = s.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{ + Content: fmt.Sprintf("📦 Backup #%d (%s)", index, backupData.SaveTime.Format("Jan 2, 2006 3:04 PM")), + Files: []*discordgo.File{file}, + }) + if err != nil { + s.ChannelMessageSend(channelID, fmt.Sprintf("❌ Failed to upload backup #%d: %v", index, err)) + } +} + func handleList(s *discordgo.Session, i *discordgo.InteractionCreate, data EmbedData) error { limit := 5 if len(i.ApplicationCommandData().Options) > 0 { @@ -217,9 +277,27 @@ func handleList(s *discordgo.Session, i *discordgo.InteractionCreate, data Embed Color: 0xFFD700, Fields: fields, })) } + + // Add download buttons if showing 5 or fewer backups + var components []discordgo.MessageComponent + if len(backups) <= 5 { + var buttons []discordgo.MessageComponent + for _, b := range backups { + buttons = append(buttons, discordgo.Button{ + Label: fmt.Sprintf("📥 Download #%d", b.Index), + Style: discordgo.SecondaryButton, + CustomID: fmt.Sprintf("%s%d", ButtonDownloadBackupPfx, b.Index), + }) + } + components = append(components, discordgo.ActionsRow{Components: buttons}) + } + if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{Embeds: []*discordgo.MessageEmbed{embeds[0]}}, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embeds[0]}, + Components: components, + }, }); err != nil { return err } @@ -274,3 +352,46 @@ func handleCommand(s *discordgo.Session, i *discordgo.InteractionCreate, data Em } return nil } + +// handleDownloadButtonInteraction handles button interactions for downloading backups +func handleDownloadButtonInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { + if i.Type != discordgo.InteractionMessageComponent { + return + } + + customID := i.MessageComponentData().CustomID + if !strings.HasPrefix(customID, ButtonDownloadBackupPfx) { + return + } + + indexStr := strings.TrimPrefix(customID, ButtonDownloadBackupPfx) + index, err := strconv.Atoi(indexStr) + if err != nil { + respondToButtonError(s, i, "Invalid backup index") + return + } + + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("📥 Preparing backup #%d for download...", index), + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + if err != nil { + logger.Discord.Error("Error responding to download button: " + err.Error()) + return + } + + go sendBackupToChannel(s, config.GetControlChannelID(), index) +} + +func respondToButtonError(s *discordgo.Session, i *discordgo.InteractionCreate, message string) { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "❌ " + message, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} diff --git a/src/discordbot/interface.go b/src/discordbot/interface.go index 1d47da84..1c162efe 100644 --- a/src/discordbot/interface.go +++ b/src/discordbot/interface.go @@ -51,6 +51,7 @@ func InitializeDiscordBot() { config.DiscordSession.AddHandler(listenToDiscordReactions) config.DiscordSession.AddHandler(listenToSlashCommands) config.DiscordSession.AddHandler(handleServerInfoButtonInteraction) // Handle button interactions + config.DiscordSession.AddHandler(handleDownloadButtonInteraction) // Handle download button interactions registerSlashCommands(config.DiscordSession) logger.Discord.Info("Bot is now running.") diff --git a/src/discordbot/registerSlashcommands.go b/src/discordbot/registerSlashcommands.go index 8756d3ce..bf4ebbba 100644 --- a/src/discordbot/registerSlashcommands.go +++ b/src/discordbot/registerSlashcommands.go @@ -68,6 +68,18 @@ func registerSlashCommands(s *discordgo.Session) { }, }, }, + { + Name: "download", + Description: "Download a backup file (most recent if no index given)", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionInteger, + Name: "index", + Description: "Backup index to download (default: most recent)", + Required: false, + }, + }, + }, { Name: "bansteamid", Description: "Bans a player by their SteamID. Needs a Server restart to take effect.", diff --git a/src/discordbot/serverinfopanel.go b/src/discordbot/serverinfopanel.go index ff2f3110..a3b4beea 100644 --- a/src/discordbot/serverinfopanel.go +++ b/src/discordbot/serverinfopanel.go @@ -13,7 +13,8 @@ var ServerInfoMessageID string // Custom IDs for button interactions const ( - ButtonGetPassword = "ssui_get_password" + ButtonGetPassword = "ssui_get_password" + ButtonDownloadBackupPfx = "ssui_download_backup_" // Prefix for download backup button. Has NOTHING TO DO WITH SERVERINFOPANEL, but this file felt like the best place to put it for now ) // sendServerInfoPanel sends the server info panel with interactive buttons From ddd5064b627d372548835de7c7635cf2abcbb703 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 6 Feb 2026 17:51:30 +0100 Subject: [PATCH 20/25] fix: Update ghcr docker builds for nightly to include '-nightly' suffix in package name --- .github/workflows/ghcr-build-nightly.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ghcr-build-nightly.yaml b/.github/workflows/ghcr-build-nightly.yaml index 7fcfe13e..7edbcb07 100644 --- a/.github/workflows/ghcr-build-nightly.yaml +++ b/.github/workflows/ghcr-build-nightly.yaml @@ -22,7 +22,7 @@ jobs: - name: Lowercase the repo name run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}-nightly" >> $GITHUB_ENV - name: Extract metadata id: meta @@ -31,8 +31,7 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=nightly - flavor: | - latest=false + type=raw,value=latest - name: Set up QEMU uses: docker/setup-qemu-action@v3 From 8d984697955185c517cfe847c99a412ee8d57cb9 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 6 Feb 2026 18:28:26 +0100 Subject: [PATCH 21/25] improved localized config page headline --- UIMod/onboard_bundled/localization/de-DE.json | 5 +++-- UIMod/onboard_bundled/localization/en-US.json | 6 ++++++ UIMod/onboard_bundled/localization/sv-SE.json | 5 +++-- UIMod/onboard_bundled/ui/config.html | 3 +-- src/web/configpage.go | 1 + src/web/templatevars.go | 1 + 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index fd086983..7752e5f7 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -22,6 +22,7 @@ "UIText_UpdateFailed": "Aktualisierung fehlgeschlagen. Bitte versuche es später erneut." }, "config": { + "UIText_ConfigHeadline": "Einstellungen", "UIText_ServerConfig": "Server Konfiguration", "UIText_DiscordIntegration": "Discord-Integration", "UIText_SLPModIntegration": "Launchpad Mods", @@ -32,6 +33,7 @@ "UIText_BasicSettings": "Basis", "UIText_NetworkSettings": "Netzwerk", "UIText_AdvancedSettings": "Erweitert", + "UIText_TerrainSettings": "Weltgenerierung", "basic": { "UIText_BasicServerSettings": "Grundlegende Servereinstellungen", "UIText_ServerName": "Servername", @@ -161,8 +163,7 @@ "UIText_SLP_UpdateWorkshopModsDesc": "Aktualisiert alle installierten Workshop-Mods auf die neueste Version. Dieser Vorgang kann je nach Anzahl der Mods einige Zeit dauern.", "UIText_SLP_UpdateButton": "Workshop-Mods aktualisieren", "UIText_SLP_InstalledMods": "Installierte Mods" - }, - "UIText_TerrainSettings": "Weltgenerierung" + } }, "setup": { "UIText_FooterText": "Hilfe benötigt? Schaue ins Stationeers Server UI Github Wiki.", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 3efd03e6..75f5210d 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -22,6 +22,7 @@ "UIText_UpdateFailed": "Update failed. Please try again later." }, "config": { + "UIText_ConfigHeadline": "Settings & Configuration", "UIText_ServerConfig": "Server Configuration", "UIText_DiscordIntegration": "Discord Integration", "UIText_SLPModIntegration": "Launchpad Mods", @@ -33,6 +34,11 @@ "UIText_NetworkSettings": "Network Settings", "UIText_AdvancedSettings": "Advanced Features", "UIText_TerrainSettings": "World generation", + "tabDescriptions": { + "UIText_ServerConfigTabDescription": "Here you can configure settings related to your gameserver like basic settings like server name and password, network settings like ports and UPnP, and SSUI settings like automatic SSUI updates, scheduled gameserver restarts and more.", + "UIText_DiscordIntegrationTabDescription": "Configure the Discord integration for real-time server status updates, remote management, player connection tracking, and community engagement. Learn more about the Discord integration and setup instructions in the Wiki.", + "UIText_SLPModIntegrationTabDescription": "Easily manage your Stationeers mods with our Stationeers Launch Pad (SLP) integration. Learn more about the SLP integration in the Wiki." + }, "basic": { "UIText_BasicServerSettings": "Basic Server Settings", "UIText_ServerName": "Server Name", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 12c70f86..67ed12f3 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -22,6 +22,7 @@ "UIText_UpdateFailed": "Uppdateringen misslyckades. Försök igen senare." }, "config": { + "UIText_ConfigHeadline": "Inställningar", "UIText_ServerConfig": "Konfiguration", "UIText_DiscordIntegration": "Discord-integration", "UIText_SLPModIntegration": "Launchpad Mods", @@ -32,6 +33,7 @@ "UIText_BasicSettings": "Grund", "UIText_NetworkSettings": "Nätverk", "UIText_AdvancedSettings": "Avancerad", + "UIText_TerrainSettings": "Världsgenerering", "basic": { "UIText_BasicServerSettings": "Grundläggande serverinställningar", "UIText_ServerName": "Servernamn", @@ -159,8 +161,7 @@ "UIText_SLP_UpdateWorkshopModsDesc": "Uppdatera alla installerade workshop-mods till deras senaste versioner. Denna process kan ta lite tid beroende på antalet mods.", "UIText_SLP_UpdateButton": "Uppdatera Workshop-mods", "UIText_SLP_InstalledMods": "Installerade Mods" - }, - "UIText_TerrainSettings": "Världsgenerering" + } }, "setup": { "UIText_FooterText": "Behöver du hjälp? Kolla Stationeers Server UI Github Wiki.", diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 52f14c3c..47703afd 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -36,7 +36,7 @@

-

{{.UIText_ServerConfig}}

+

{{.UIText_ConfigHeadline}}

@@ -659,7 +659,6 @@

{{.UIText_SLP_InstalledMods}}

-

Custom Detection Manager

Custom detections allow you to create custom patterns for event detection. These patterns can be used to detect specific events in the server log and send alerts to the "Status" channel in Discord and will be shown in the Events tab on the main dashboard.

To create a custom detection, you can either define a regex pattern or diff --git a/src/web/configpage.go b/src/web/configpage.go index da176c35..9d02d7cb 100644 --- a/src/web/configpage.go +++ b/src/web/configpage.go @@ -286,6 +286,7 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { CreateGameServerLogFileFalseSelected: createGameServerLogFileFalseSelected, // Localized UI text + UIText_ConfigHeadline: localization.GetString("UIText_ConfigHeadline"), UIText_ServerConfig: localization.GetString("UIText_ServerConfig"), UIText_DiscordIntegration: localization.GetString("UIText_DiscordIntegration"), UIText_SLPModIntegration: localization.GetString("UIText_SLPModIntegration"), diff --git a/src/web/templatevars.go b/src/web/templatevars.go index 810c7eb7..c9344301 100644 --- a/src/web/templatevars.go +++ b/src/web/templatevars.go @@ -96,6 +96,7 @@ type ConfigTemplateData struct { CreateGameServerLogFileTrueSelected string CreateGameServerLogFileFalseSelected string + UIText_ConfigHeadline string UIText_ServerConfig string UIText_DiscordIntegration string UIText_SLPModIntegration string From 854df765dae61e1f40fad76e57e0b8941bd044a3 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Fri, 6 Feb 2026 18:45:45 +0100 Subject: [PATCH 22/25] feat: Add back button functionality to sliding tabs on config page and according localization for it also added a new fun fact to console managers funMessages --- UIMod/onboard_bundled/assets/css/config.css | 7 +++++++ UIMod/onboard_bundled/assets/icons/back.webp | Bin 0 -> 440 bytes .../onboard_bundled/assets/js/console-manager.js | 1 + UIMod/onboard_bundled/localization/de-DE.json | 1 + UIMod/onboard_bundled/localization/en-US.json | 1 + UIMod/onboard_bundled/localization/sv-SE.json | 1 + UIMod/onboard_bundled/ui/config.html | 4 ++++ src/web/configpage.go | 1 + src/web/templatevars.go | 1 + 9 files changed, 17 insertions(+) create mode 100644 UIMod/onboard_bundled/assets/icons/back.webp diff --git a/UIMod/onboard_bundled/assets/css/config.css b/UIMod/onboard_bundled/assets/css/config.css index 13de4173..f4dea0d1 100644 --- a/UIMod/onboard_bundled/assets/css/config.css +++ b/UIMod/onboard_bundled/assets/css/config.css @@ -140,6 +140,13 @@ background-position: center; } +/*Back icon - left arrow*/ +.back-icon { + background-image: url("/static/icons/back.webp"); + background-repeat: no-repeat; + background-position: center; +} + /* Detection Manager Icon - radar/search icon */ .detection-icon { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z'/%3E%3Cpath d='M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-1 5h2v2h-2v-2zm0-2h2v1h-2v-1zm0-1h2v1h-2v-1z'/%3E%3C/svg%3E"); diff --git a/UIMod/onboard_bundled/assets/icons/back.webp b/UIMod/onboard_bundled/assets/icons/back.webp new file mode 100644 index 0000000000000000000000000000000000000000..312a3456c6c3220f1d4583c8b54cba1e304d58d7 GIT binary patch literal 440 zcmWIYbaUIl$iNWp>J$(bU=hK^z`!5@#P(q1=o8>k3gj{J8~hg(Jd~^AIVtFmQiuJF zBE5%<>9=NWQd@IkbD`>)-VV8o|2ZGbx1T)Er?WUYXws}LPv(|{8&5vmBjr}gDpJZG z@|Dq~l;PehZNUsrUT%#<)?}v}jKP<#+J4w}&gA7nuWANYFNxNM0)F8b=l0}^LyCVg z8G!Cl2m{(4z{W6_QOkkBfx)g+XM+2ZMGF^NzF?TZ*O|#6cE`=im~YE71&=;wsVlW* zZZFf%Z%cK5^UB(y>_hHq28MsX9WVd?-@tkQ``)Mj{~us^|I~q_{#ADa1A}XXr`tB5 z>7Jfi3>-k3l|hP;l>rzO3|>Gi4P^%b*&2+@U~wiO+mMloK>$cc0dZzK3s^i0$OeG~ zAO`7&(E*8>c_n&&t|1DhdWL$I49pA+6WAG8fGP}(j13qUKuiVcU|j$)X$p`H0!%=2 Xn7}H7EG>X6s4hbT1CXrG`2!vRg$;9) literal 0 HcmV?d00001 diff --git a/UIMod/onboard_bundled/assets/js/console-manager.js b/UIMod/onboard_bundled/assets/js/console-manager.js index 87baab40..4c65fdd2 100644 --- a/UIMod/onboard_bundled/assets/js/console-manager.js +++ b/UIMod/onboard_bundled/assets/js/console-manager.js @@ -98,6 +98,7 @@ function handleConsole() { "Balancing gas mixtures... kaboom imminent, run you fool!", "Spoiler: object reference not set to an instance of an object, lol", "Fun fact: SSUI was originally a simple powershell script", + "Fun fact: This dashboard was supposed to look like the retro computer from Stationeers. We tried, ok?", "Convincing server that 'out of memory' is just a state of mind.", "Moo, Moo! I'm a cow!", "Welcome home, Sir!" diff --git a/UIMod/onboard_bundled/localization/de-DE.json b/UIMod/onboard_bundled/localization/de-DE.json index 7752e5f7..d6e14cd8 100644 --- a/UIMod/onboard_bundled/localization/de-DE.json +++ b/UIMod/onboard_bundled/localization/de-DE.json @@ -22,6 +22,7 @@ "UIText_UpdateFailed": "Aktualisierung fehlgeschlagen. Bitte versuche es später erneut." }, "config": { + "UIText_BackToDashboard": "Zurück zum Dashboard", "UIText_ConfigHeadline": "Einstellungen", "UIText_ServerConfig": "Server Konfiguration", "UIText_DiscordIntegration": "Discord-Integration", diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json index 75f5210d..b0674872 100644 --- a/UIMod/onboard_bundled/localization/en-US.json +++ b/UIMod/onboard_bundled/localization/en-US.json @@ -22,6 +22,7 @@ "UIText_UpdateFailed": "Update failed. Please try again later." }, "config": { + "UIText_BackToDashboard": "Back to Dashboard", "UIText_ConfigHeadline": "Settings & Configuration", "UIText_ServerConfig": "Server Configuration", "UIText_DiscordIntegration": "Discord Integration", diff --git a/UIMod/onboard_bundled/localization/sv-SE.json b/UIMod/onboard_bundled/localization/sv-SE.json index 67ed12f3..41ea06b8 100644 --- a/UIMod/onboard_bundled/localization/sv-SE.json +++ b/UIMod/onboard_bundled/localization/sv-SE.json @@ -22,6 +22,7 @@ "UIText_UpdateFailed": "Uppdateringen misslyckades. Försök igen senare." }, "config": { + "UIText_BackToDashboard": "Tillbaka till dashboard", "UIText_ConfigHeadline": "Inställningar", "UIText_ServerConfig": "Konfiguration", "UIText_DiscordIntegration": "Discord-integration", diff --git a/UIMod/onboard_bundled/ui/config.html b/UIMod/onboard_bundled/ui/config.html index 47703afd..674ed14a 100644 --- a/UIMod/onboard_bundled/ui/config.html +++ b/UIMod/onboard_bundled/ui/config.html @@ -40,6 +40,10 @@

{{.UIText_ConfigHeadline}}

+
- + -
Show the Expert Settings to access special configuration options.
+
{{.UIText_ShowExpertSettingsInfo}}
diff --git a/src/web/configpage.go b/src/web/configpage.go index c3bcc82d..3e0af67b 100644 --- a/src/web/configpage.go +++ b/src/web/configpage.go @@ -342,6 +342,8 @@ func ServeConfigPage(w http.ResponseWriter, r *http.Request) { UIText_ServerExePathInfo2: localization.GetString("UIText_ServerExePathInfo2"), UIText_AdditionalParams: localization.GetString("UIText_AdditionalParams"), UIText_AdditionalParamsInfo: localization.GetString("UIText_AdditionalParamsInfo"), + UIText_ShowExpertSettings: localization.GetString("UIText_ShowExpertSettings"), + UIText_ShowExpertSettingsInfo: localization.GetString("UIText_ShowExpertSettingsInfo"), UIText_AutoRestartServerTimer: localization.GetString("UIText_AutoRestartServerTimer"), UIText_AutoRestartServerTimerInfo: localization.GetString("UIText_AutoRestartServerTimerInfo"), UIText_GameBranch: localization.GetString("UIText_GameBranch"), diff --git a/src/web/templatevars.go b/src/web/templatevars.go index 635b2481..b274ea84 100644 --- a/src/web/templatevars.go +++ b/src/web/templatevars.go @@ -152,6 +152,8 @@ type ConfigTemplateData struct { UIText_ServerExePathInfo2 string UIText_AdditionalParams string UIText_AdditionalParamsInfo string + UIText_ShowExpertSettings string + UIText_ShowExpertSettingsInfo string UIText_AutoRestartServerTimer string UIText_AutoRestartServerTimerInfo string UIText_GameBranch string From 5a322973abf42abaf55728aede79138f6a3df608 Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 7 Feb 2026 01:11:05 +0100 Subject: [PATCH 24/25] fix: Update fetchStatusCmd to use GetGameBranch to correctly show game branch in CLI dashboard --- src/cli/dashboard/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/dashboard/update.go b/src/cli/dashboard/update.go index cb2c9c3d..8708ce91 100644 --- a/src/cli/dashboard/update.go +++ b/src/cli/dashboard/update.go @@ -340,7 +340,7 @@ func fetchStatusCmd() tea.Cmd { // Game info gameVersion: config.GetExtractedGameVersion(), - gameBranch: config.GetBranch(), + gameBranch: config.GetGameBranch(), buildID: config.GetCurrentBranchBuildID(), // SSUI info From 7bfe9ea591a0b095f8849d940b0777282501bf3d Mon Sep 17 00:00:00 2001 From: JacksonTheMaster Date: Sat, 7 Feb 2026 01:17:26 +0100 Subject: [PATCH 25/25] fix: reduce deadlock risk by optimizing dashboard hooks by reducing lock duration in isDashboardActive and captureDashboardLog functions: The mutex is only held for the brief moment needed to copy the function pointer. The function pointer copy is atomic and safe. Even if the dashboard unregisters hooks between copying the pointer and calling it, the code handles nil gracefully --- src/logger/logger.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/logger/logger.go b/src/logger/logger.go index 45fdd7a9..dd362aae 100644 --- a/src/logger/logger.go +++ b/src/logger/logger.go @@ -288,18 +288,20 @@ func RegisterDashboardHooks(activeFunc func() bool, captureFunc func(string)) { // isDashboardActive checks if the dashboard is currently running func isDashboardActive() bool { dashboardHooksMutex.Lock() - defer dashboardHooksMutex.Unlock() - if dashboardActiveFunc == nil { + fn := dashboardActiveFunc + dashboardHooksMutex.Unlock() + if fn == nil { return false } - return dashboardActiveFunc() + return fn() } // captureDashboardLog sends a log line to the dashboard for display func captureDashboardLog(line string) { dashboardHooksMutex.Lock() - defer dashboardHooksMutex.Unlock() - if dashboardCaptureFunc != nil { - dashboardCaptureFunc(line) + fn := dashboardCaptureFunc + dashboardHooksMutex.Unlock() + if fn != nil { + fn(line) } }