From 7a659dd36366a242f8c9395772100e3a2dcc2f86 Mon Sep 17 00:00:00 2001
From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com>
Date: Wed, 13 Aug 2025 12:42:14 +0200
Subject: [PATCH 01/20] changed log level for translation not found errors to
debug
---
src/localization/localization.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/localization/localization.go b/src/localization/localization.go
index 49baa043..512871b0 100644
--- a/src/localization/localization.go
+++ b/src/localization/localization.go
@@ -120,6 +120,6 @@ func GetString(key string) string {
}
// Return key as final fallback
- logger.Localization.Warn("Translation not found for key: " + key)
+ logger.Localization.Debug("Translation not found for key: " + key)
return key
}
From fcb3cb9080fe5daa7d83d9f5078eaa924cd24a3f Mon Sep 17 00:00:00 2001
From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com>
Date: Wed, 13 Aug 2025 12:42:30 +0200
Subject: [PATCH 02/20] added missing UIText_Login_HeaderTitle to english loc
file
---
UIMod/onboard_bundled/localization/en-US.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/UIMod/onboard_bundled/localization/en-US.json b/UIMod/onboard_bundled/localization/en-US.json
index 9ddbceb5..e55a71ae 100644
--- a/UIMod/onboard_bundled/localization/en-US.json
+++ b/UIMod/onboard_bundled/localization/en-US.json
@@ -284,6 +284,7 @@
"UIText_Finalize_SubmitButton": "Return to Start",
"UIText_Finalize_SkipButton": "Skip Authentication",
"UIText_Login_Title": "Stationeers Server UI",
+ "UIText_Login_HeaderTitle": "Login",
"UIText_Login_PrimaryLabel": "Username",
"UIText_Login_SecondaryLabel": "Password",
"UIText_Login_PrimaryPlaceholder": "Enter Username",
From f4ccf95c20ca87cef89c4c89d8a6482092d8a79b Mon Sep 17 00:00:00 2001
From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com>
Date: Wed, 13 Aug 2025 12:43:03 +0200
Subject: [PATCH 03/20] bump version to 5.5.9
---
src/config/config.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/config/config.go b/src/config/config.go
index 15710012..888221db 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.5.8"
+ Version = "5.5.9"
Branch = "release"
)
From b47c5e65862fdf2ff02bda94793d00a408f0ce70 Mon Sep 17 00:00:00 2001
From: JacksonTheMaster <81807824+JacksonTheMaster@users.noreply.github.com>
Date: Wed, 13 Aug 2025 13:14:25 +0200
Subject: [PATCH 04/20] updated readme, finally
---
readme.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/readme.md b/readme.md
index e7e8ec28..a94a5ec7 100644
--- a/readme.md
+++ b/readme.md
@@ -30,6 +30,8 @@
Explore your fututre Stationeers Server UI in actionโno setup required!
And the "best part?" The Demo current is not on the V5 but V4, so if you are convinced by the Demo, it will only get better when you actually try it out!
+### Preview is not available any more, but will be available soon!
+
[](https://jacksonthemaster.github.io/StationeersServerUI/server.html)
[](https://jacksonthemaster.github.io/StationeersServerUI)
@@ -52,7 +54,7 @@ And the "best part?" The Demo current is not on the V5 but V4, so if you are con
2. ๐ Place in empty folder and run it on Linux or Windows (chmod +x on linux)
3. ๐ Access UI at `https://<>:8443`
4. ๐ See [First-Time Setup](https://github.com/JacksonTheMaster/StationeersServerUI/wiki/First-Time-Setup) in the wiki
-5. ๐ If you set this up too quickly and are in search of the Default Username/Password, see [Security Considerations](https://github.com/JacksonTheMaster/StationeersServerUI/wiki/Security-Considerations) for more details!
+5. ๐ Read the [Wiki](https://github.com/JacksonTheMaster/StationeersServerUI/wiki) and follow the chained pages (links at bottom of page)!
## What is This?
From 62b3bfc7d9c2f3018e7e4fcd92faa106ff812f4f Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Fri, 22 Aug 2025 00:13:32 +0200
Subject: [PATCH 05/20] fix mod time shenenigans: reliably make the gameserver
load the restored save by modifiying mod time timestamps inside the .save
file.
Stationeers Beta uses the MOD TIME inside a dotsave insted of the dotsave file mod time to determine the "latest" save
https://discordapp.com/channels/276525882049429515/392080751648178188/1407157281606336602
---
src/managers/backupmgr/restore.go | 117 +++++++++++++++++++++++++++++-
1 file changed, 115 insertions(+), 2 deletions(-)
diff --git a/src/managers/backupmgr/restore.go b/src/managers/backupmgr/restore.go
index ed0fed23..981cb9ab 100644
--- a/src/managers/backupmgr/restore.go
+++ b/src/managers/backupmgr/restore.go
@@ -1,7 +1,9 @@
package backupmgr
import (
+ "archive/zip"
"fmt"
+ "io"
"os"
"path/filepath"
"strings"
@@ -64,12 +66,123 @@ func (m *BackupManager) RestoreBackup(index int) error {
}
}
- // Now copy the new .save file
- if err := copyFile(backupFile, destFile); err != nil {
+ // Create temp directory for mod time shenenigans (https://discordapp.com/channels/276525882049429515/392080751648178188/1407157281606336602)
+ tempDir := filepath.Join("./saves", m.config.WorldName, "tmp")
+ if err := os.MkdirAll(tempDir, os.ModePerm); err != nil {
+ return fmt.Errorf("failed to create temp directory %s: %w", tempDir, err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ // Extract .save (zip) file to tempDir
+ r, err := zip.OpenReader(backupFile)
+ if err != nil {
+ return fmt.Errorf("failed to open zip reader for %s: %w", backupFile, err)
+ }
+ defer r.Close()
+
+ for _, f := range r.File {
+ path := filepath.Join(tempDir, f.Name)
+ if f.FileInfo().IsDir() {
+ if err := os.MkdirAll(path, f.Mode()); err != nil {
+ m.revertRestore(restoredFiles)
+ return fmt.Errorf("failed to create directory %s: %w", path, err)
+ }
+ continue
+ }
+
+ if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
+ m.revertRestore(restoredFiles)
+ return fmt.Errorf("failed to create parent directory for %s: %w", path, err)
+ }
+
+ outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
+ if err != nil {
+ m.revertRestore(restoredFiles)
+ return fmt.Errorf("failed to create file %s: %w", path, err)
+ }
+
+ rc, err := f.Open()
+ if err != nil {
+ outFile.Close()
+ m.revertRestore(restoredFiles)
+ return fmt.Errorf("failed to open file in zip %s: %w", f.Name, err)
+ }
+
+ if _, err := io.Copy(outFile, rc); err != nil {
+ rc.Close()
+ outFile.Close()
+ m.revertRestore(restoredFiles)
+ return fmt.Errorf("failed to extract file %s: %w", path, err)
+ }
+ rc.Close()
+ outFile.Close()
+ }
+
+ // Modify timestamps of extracted files to current system time
+ now := time.Now()
+ if err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return nil
+ }
+ return os.Chtimes(path, now, now)
+ }); err != nil {
+ m.revertRestore(restoredFiles)
+ return fmt.Errorf("failed to modify timestamps in %s: %w", tempDir, err)
+ }
+
+ // Create new .save (zip) file at destFile with updated timestamps
+ dest, err := os.Create(destFile)
+ if err != nil {
+ m.revertRestore(restoredFiles)
+ return fmt.Errorf("failed to create destination .save file %s: %w", destFile, err)
+ }
+ defer dest.Close()
+
+ w := zip.NewWriter(dest)
+ defer w.Close()
+
+ if err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return nil
+ }
+ relPath, err := filepath.Rel(tempDir, path)
+ if err != nil {
+ return fmt.Errorf("failed to get relative path for %s: %w", path, err)
+ }
+ relPath = filepath.ToSlash(relPath)
+
+ // Create zip entry with current system timestamp
+ fw, err := w.CreateHeader(&zip.FileHeader{
+ Name: relPath,
+ Method: zip.Deflate,
+ Modified: now,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create zip entry %s: %w", relPath, err)
+ }
+
+ srcFile, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("failed to open file %s: %w", path, err)
+ }
+ defer srcFile.Close()
+
+ if _, err := io.Copy(fw, srcFile); err != nil {
+ return fmt.Errorf("failed to write file %s to zip: %w", relPath, err)
+ }
+ return nil
+ }); err != nil {
m.revertRestore(restoredFiles)
return fmt.Errorf("failed to restore .save file %s: %w", backupFile, err)
}
restoredFiles[destFile] = backupFile
+ return nil // restore and mod time shenanigans successful, no need to return an error
} else {
// Old-style trio (world_meta.xml, world.xml, world.bin)
files := []struct {
From 2595013a290deb55525339f260f7ca473f072b69 Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Fri, 22 Aug 2025 00:25:14 +0200
Subject: [PATCH 06/20] added feature request to customize "Login" text on
loging page to something like "Login@MySSUIServer1"
-config.AdditionalLoginHeaderText
- can ONLY be set via config file since disclosing server information would be a possible security risk.
https://discordapp.com/channels/1357524183260729404/1383585271366287410/1408125765152804985
---
src/config/config.go | 2 ++
src/config/vars.go | 37 +++++++++++++++++++------------------
src/web/TwoBoxForm.go | 2 +-
3 files changed, 22 insertions(+), 19 deletions(-)
diff --git a/src/config/config.go b/src/config/config.go
index 888221db..2aec9eba 100644
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -74,6 +74,7 @@ type JsonConfig struct {
IsConsoleEnabled *bool `json:"IsConsoleEnabled"`
LanguageSetting string `json:"LanguageSetting"`
AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"`
+ AdditionalLoginHeaderText string `json:"AdditionalLoginHeaderText"`
}
type CustomDetection struct {
@@ -160,6 +161,7 @@ func applyConfig(cfg *JsonConfig) {
GamePort = getString(cfg.GamePort, "GAME_PORT", "27016")
UpdatePort = getString(cfg.UpdatePort, "UPDATE_PORT", "27015")
LanguageSetting = getString(cfg.LanguageSetting, "LANGUAGE_SETTING", "en-US")
+ AdditionalLoginHeaderText = getString(cfg.AdditionalLoginHeaderText, "ADDITIONAL_LOGIN_HEADER_TEXT", "")
upnpEnabledVal := getBool(cfg.UPNPEnabled, "UPNP_ENABLED", false)
UPNPEnabled = upnpEnabledVal
diff --git a/src/config/vars.go b/src/config/vars.go
index 995d45e7..a2f40fdd 100644
--- a/src/config/vars.go
+++ b/src/config/vars.go
@@ -51,24 +51,25 @@ var (
// Logging, debugging and misc
var (
- IsDebugMode bool //only used for pprof server, keep it like this and check the log level instead. Debug = 10
- CreateSSUILogFile bool
- LogLevel int
- LogMessageBuffer string
- IsFirstTimeSetup bool
- BufferFlushTicker *time.Ticker
- SSEMessageBufferSize = 2000
- MaxSSEConnections = 20
- GameServerAppID = "600760"
- ExePath string
- GameBranch string
- SubsystemFilters []string
- GameServerUUID uuid.UUID // Assined at startup to the current instance of the server we are managing. Currently unused.
- AutoRestartServerTimer string
- IsConsoleEnabled bool
- LogClutterToConsole bool // surpresses clutter mono logs from the gameserver
- LanguageSetting string
- AutoStartServerOnStartup bool
+ IsDebugMode bool //only used for pprof server, keep it like this and check the log level instead. Debug = 10
+ CreateSSUILogFile bool
+ LogLevel int
+ LogMessageBuffer string
+ IsFirstTimeSetup bool
+ BufferFlushTicker *time.Ticker
+ SSEMessageBufferSize = 2000
+ MaxSSEConnections = 20
+ GameServerAppID = "600760"
+ ExePath string
+ GameBranch string
+ SubsystemFilters []string
+ GameServerUUID uuid.UUID // Assined at startup to the current instance of the server we are managing. Currently unused.
+ AutoRestartServerTimer string
+ IsConsoleEnabled bool
+ LogClutterToConsole bool // surpresses clutter mono logs from the gameserver
+ LanguageSetting string
+ AutoStartServerOnStartup bool
+ AdditionalLoginHeaderText string
)
// Discord integration
diff --git a/src/web/TwoBoxForm.go b/src/web/TwoBoxForm.go
index 4fc7efe7..fbeb3a9b 100644
--- a/src/web/TwoBoxForm.go
+++ b/src/web/TwoBoxForm.go
@@ -468,7 +468,7 @@ func ServeTwoBoxFormTemplate(w http.ResponseWriter, r *http.Request) {
default:
data.Title = localization.GetString("UIText_Login_Title")
- data.HeaderTitle = localization.GetString("UIText_Login_HeaderTitle")
+ data.HeaderTitle = localization.GetString("UIText_Login_HeaderTitle") + config.AdditionalLoginHeaderText
data.PrimaryLabel = localization.GetString("UIText_Login_PrimaryLabel")
data.SecondaryLabel = localization.GetString("UIText_Login_SecondaryLabel")
data.PrimaryPlaceholderText = localization.GetString("UIText_Login_PrimaryPlaceholder")
From 4d03b81cb5c45f91bebbad883aa678246b0c7701 Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Fri, 22 Aug 2025 00:40:52 +0200
Subject: [PATCH 07/20] added ability to change webUI port
(https://discordapp.com/channels/1357524183260729404/1383585271366287410/1406791779570941952)
---
src/cli/terminalmsg.go | 2 +-
src/config/config.go | 2 ++
src/config/vars.go | 1 +
src/web/start.go | 2 +-
4 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/cli/terminalmsg.go b/src/cli/terminalmsg.go
index 9c89873d..4d9c87a5 100644
--- a/src/cli/terminalmsg.go
+++ b/src/cli/terminalmsg.go
@@ -36,7 +36,7 @@ func PrintStartupMessage() {
fmt.Println(" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
// Web UI info
- fmt.Println("\n ๐ Web UI available at: https://localhost:8443 (default) or https://:8443")
+ fmt.Println("\n ๐ Web UI available at: https://localhost:8443 (default) or https://:" + config.SSUIWebPort)
fmt.Println("\n ๐ Support available at: https://discord.gg/8n3vN92MyJ")
// Quote
diff --git a/src/config/config.go b/src/config/config.go
index 2aec9eba..2f592b5b 100644
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -75,6 +75,7 @@ type JsonConfig struct {
LanguageSetting string `json:"LanguageSetting"`
AutoStartServerOnStartup *bool `json:"AutoStartServerOnStartup"`
AdditionalLoginHeaderText string `json:"AdditionalLoginHeaderText"`
+ SSUIWebPort string `json:"SSUIWebPort"`
}
type CustomDetection struct {
@@ -162,6 +163,7 @@ func applyConfig(cfg *JsonConfig) {
UpdatePort = getString(cfg.UpdatePort, "UPDATE_PORT", "27015")
LanguageSetting = getString(cfg.LanguageSetting, "LANGUAGE_SETTING", "en-US")
AdditionalLoginHeaderText = getString(cfg.AdditionalLoginHeaderText, "ADDITIONAL_LOGIN_HEADER_TEXT", "")
+ SSUIWebPort = getString(cfg.SSUIWebPort, "SSUI_WEB_PORT", "8443")
upnpEnabledVal := getBool(cfg.UPNPEnabled, "UPNP_ENABLED", false)
UPNPEnabled = upnpEnabledVal
diff --git a/src/config/vars.go b/src/config/vars.go
index a2f40fdd..26df6c36 100644
--- a/src/config/vars.go
+++ b/src/config/vars.go
@@ -110,6 +110,7 @@ var (
JwtKey string
AuthTokenLifetime int
Users map[string]string
+ SSUIWebPort string
)
// SSUI Updates
diff --git a/src/web/start.go b/src/web/start.go
index d2e5c93e..95476f93 100644
--- a/src/web/start.go
+++ b/src/web/start.go
@@ -90,7 +90,7 @@ func StartWebServer(wg *sync.WaitGroup) {
logger.Web.Error("Error setting up TLS certificates: " + err.Error())
//os.Exit(1)
}
- err := http.ListenAndServeTLS("0.0.0.0:8443", config.TLSCertPath, config.TLSKeyPath, mux)
+ err := http.ListenAndServeTLS("0.0.0.0:"+config.SSUIWebPort, config.TLSCertPath, config.TLSKeyPath, mux)
if err != nil {
logger.Web.Error("Error starting HTTPS server: " + err.Error())
}
From b4b300aec6bae029ac741150801bf1af646058ec Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Fri, 22 Aug 2025 09:33:45 +0200
Subject: [PATCH 08/20] oldHeadSaveBackups are now saved as
re-restore-HEAD-timestamp-originalfilename
---
src/managers/backupmgr/restore.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/managers/backupmgr/restore.go b/src/managers/backupmgr/restore.go
index 981cb9ab..f02a1046 100644
--- a/src/managers/backupmgr/restore.go
+++ b/src/managers/backupmgr/restore.go
@@ -58,7 +58,7 @@ func (m *BackupManager) RestoreBackup(index int) error {
existingFile := filepath.Join(saveDir, file.Name())
// Move existing .save file to SafeBackupDir with timestamp to avoid overwrites
timestamp := time.Now().Format("2006-01-02_15-04-05")
- savedPreviousHeadSaveFilePath := filepath.Join(m.config.SafeBackupDir, fmt.Sprintf("%s_%s_%s", "oldHeadSaveBackup", timestamp, file.Name()))
+ savedPreviousHeadSaveFilePath := filepath.Join(m.config.SafeBackupDir, fmt.Sprintf("%s_%s_%s", "pre-restore-HEAD-", timestamp, file.Name()))
if err := os.Rename(existingFile, savedPreviousHeadSaveFilePath); err != nil {
return fmt.Errorf("failed to move existing HEAD .save file %s to %s: %w", existingFile, savedPreviousHeadSaveFilePath, err)
}
From 094e30563a932252c745dfa26da76050b25e6fbe Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Fri, 22 Aug 2025 10:06:33 +0200
Subject: [PATCH 09/20] add simple backup refresh button to the backup manager
UI
---
UIMod/onboard_bundled/assets/css/home.css | 14 ++++++++++++++
UIMod/onboard_bundled/ui/index.html | 1 +
2 files changed, 15 insertions(+)
diff --git a/UIMod/onboard_bundled/assets/css/home.css b/UIMod/onboard_bundled/assets/css/home.css
index 84723b40..499f4328 100644
--- a/UIMod/onboard_bundled/assets/css/home.css
+++ b/UIMod/onboard_bundled/assets/css/home.css
@@ -204,6 +204,7 @@
/* Backups */
#backups {
+ position: relative;
margin-top: 40px;
background-color: rgba(0, 255, 171, 0.05);
padding: 20px;
@@ -216,6 +217,19 @@
transform: translateY(-2px);
}
+#backupRefreshButton {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ padding: 5px 10px;
+ font-size: 1.3rem;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
.backup-item {
background-color: rgba(0, 0, 0, 0.4);
padding: 15px;
diff --git a/UIMod/onboard_bundled/ui/index.html b/UIMod/onboard_bundled/ui/index.html
index 2e62b574..f7dbf1b7 100644
--- a/UIMod/onboard_bundled/ui/index.html
+++ b/UIMod/onboard_bundled/ui/index.html
@@ -140,6 +140,7 @@
Stationeers Server UI v{{.Version}} ({{.Branch}})
{{.UIText_Backup_Manager}}
+
From 173eff80d47086c2f703ddc849bb44b4e2a76751 Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Fri, 22 Aug 2025 12:48:29 +0200
Subject: [PATCH 10/20] refactor: update InstallAndRunSteamCMD to return exit
status and error; handle response in (http) update handler
---
src/setup/steamcmd.go | 56 ++++++++++++++++++++++++-------------------
src/web/http.go | 9 +++++--
2 files changed, 38 insertions(+), 27 deletions(-)
diff --git a/src/setup/steamcmd.go b/src/setup/steamcmd.go
index 7db0d37a..962176a2 100644
--- a/src/setup/steamcmd.go
+++ b/src/setup/steamcmd.go
@@ -1,6 +1,7 @@
package setup
import (
+ "fmt"
"io"
"os"
"os/exec"
@@ -26,24 +27,25 @@ const (
)
// InstallAndRunSteamCMD installs and runs SteamCMD based on the platform (Windows/Linux).
-// It automatically detects the OS and calls the appropriate installation function.
-func InstallAndRunSteamCMD() {
+// It returns the exit status of the SteamCMD execution and any error encountered.
+func InstallAndRunSteamCMD() (int, error) {
if config.Branch == "indev-no-steamcmd" {
logger.Install.Info("๐ Detected indev-no-steamcmd branch, skipping SteamCMD installation")
- return
+ return 0, nil
}
if runtime.GOOS == "windows" {
- installSteamCMDWindows()
+ return installSteamCMDWindows()
} else if runtime.GOOS == "linux" {
- installSteamCMDLinux()
+ return installSteamCMDLinux()
} else {
- logger.Install.Error("โ SteamCMD installation is not supported on this OS.\n")
- return
+ err := fmt.Errorf("SteamCMD installation is not supported on this OS")
+ logger.Install.Error("โ " + err.Error() + "\n")
+ return -1, err
}
}
-func installSteamCMD(platform string, steamCMDDir string, downloadURL string, extractFunc ExtractorFunc) {
+func installSteamCMD(platform string, steamCMDDir string, downloadURL string, extractFunc ExtractorFunc) (int, error) {
// Check if SteamCMD is already installed
if _, err := os.Stat(steamCMDDir); os.IsNotExist(err) {
logger.Install.Warn("โ ๏ธ SteamCMD not found for " + platform + ", downloading...\n")
@@ -51,7 +53,7 @@ func installSteamCMD(platform string, steamCMDDir string, downloadURL string, ex
// Create SteamCMD directory
if err := createSteamCMDDirectory(steamCMDDir); err != nil {
logger.Install.Error("โ Error creating SteamCMD directory: " + err.Error() + "\n")
- return
+ return -1, err
}
// Ensure cleanup on failure
@@ -66,55 +68,54 @@ func installSteamCMD(platform string, steamCMDDir string, downloadURL string, ex
// Install required libraries
if err := installRequiredLibraries(); err != nil {
logger.Install.Error("โ Error installing required libraries: " + err.Error() + "\n")
- return
+ return -1, err
}
// Download and extract SteamCMD
if err := downloadAndExtractSteamCMD(downloadURL, steamCMDDir, extractFunc); err != nil {
logger.Install.Error("โ " + err.Error() + "\n")
- return
+ return -1, err
}
// Set executable permissions for SteamCMD files
if err := setExecutablePermissions(steamCMDDir); err != nil {
logger.Install.Error("โ Error setting executable permissions: " + err.Error() + "\n")
- return
+ return -1, err
}
// Verify the steamcmd binary
if err := verifySteamCMDBinary(steamCMDDir); err != nil {
logger.Install.Error("โ " + err.Error() + "\n")
- return
+ return -1, err
}
// Mark installation as successful
success = true
logger.Install.Info("โ SteamCMD installed successfully.\n")
} else {
-
logger.Install.Info("โ SteamCMD is already installed.")
}
- // Run SteamCMD
- runSteamCMD(steamCMDDir)
+ // Run SteamCMD and return its exit status and error
+ return runSteamCMD(steamCMDDir)
}
// installSteamCMDLinux downloads and installs SteamCMD on Linux.
-func installSteamCMDLinux() {
- installSteamCMD("Linux", SteamCMDLinuxDir, SteamCMDLinuxURL, untarWrapper)
+func installSteamCMDLinux() (int, error) {
+ return installSteamCMD("Linux", SteamCMDLinuxDir, SteamCMDLinuxURL, untarWrapper)
}
// installSteamCMDWindows downloads and installs SteamCMD on Windows.
-func installSteamCMDWindows() {
- installSteamCMD("Windows", SteamCMDWindowsDir, SteamCMDWindowsURL, unzip)
+func installSteamCMDWindows() (int, error) {
+ return installSteamCMD("Windows", SteamCMDWindowsDir, SteamCMDWindowsURL, unzip)
}
-// runSteamCMD runs the SteamCMD command to update the game.
-func runSteamCMD(steamCMDDir string) {
+// runSteamCMD runs the SteamCMD command to update the game and returns its exit status and any error.
+func runSteamCMD(steamCMDDir string) (int, error) {
currentDir, err := os.Getwd()
if err != nil {
logger.Install.Error("โ Error getting current working directory: " + err.Error() + "\n")
- return
+ return -1, err
}
logger.Install.Debug("โ Current working directory: " + currentDir + "\n")
@@ -122,7 +123,7 @@ func runSteamCMD(steamCMDDir string) {
if runtime.GOOS != "windows" {
if err := setExecutablePermissions(steamCMDDir); err != nil {
logger.Install.Error("โ Error setting executable permissions, your Steamcmd install might be broken: " + err.Error() + "\n")
- return
+ return -1, err
}
}
@@ -142,10 +143,15 @@ func runSteamCMD(steamCMDDir string) {
}
err = cmd.Run()
if err != nil {
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ logger.Install.Error("โ SteamCMD exited unsuccessfully: " + err.Error() + "\n")
+ return exitErr.ExitCode(), err
+ }
logger.Install.Error("โ Error running SteamCMD: " + err.Error() + "\n")
- return
+ return -1, err
}
logger.Install.Info("โ SteamCMD executed successfully.\n")
+ return 0, nil
}
// buildSteamCMDCommand constructs the SteamCMD command based on the OS.
diff --git a/src/web/http.go b/src/web/http.go
index 38f5f4ee..17959b27 100644
--- a/src/web/http.go
+++ b/src/web/http.go
@@ -162,7 +162,7 @@ func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) {
time.Sleep(10000 * time.Millisecond)
}
logger.Core.Info("Running SteamCMD")
- setup.InstallAndRunSteamCMD()
+ _, err := setup.InstallAndRunSteamCMD()
// Update last execution time
lastSteamCMDExecution = time.Now()
@@ -170,5 +170,10 @@ func HandleRunSteamCMD(w http.ResponseWriter, r *http.Request) {
// Success: return 202 Accepted and JSON
w.WriteHeader(http.StatusAccepted)
w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Accepted", "message": "SteamCMD ran successfully."})
+ if err == nil {
+ json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Success", "message": "SteamCMD ran successfully."})
+ return
+ }
+ // Failure: return 202 Accepted and JSON with the error message
+ json.NewEncoder(w).Encode(map[string]string{"statuscode": "202", "status": "Failed", "message": "SteamCMD ran unsuccessfully:" + err.Error()})
}
From 2f46165d704600f8c895253a70a133b72832e26b Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Fri, 22 Aug 2025 18:03:35 +0200
Subject: [PATCH 11/20] removed unused script.js
---
UIMod/onboard_bundled/assets/script.js | 520 -------------------------
1 file changed, 520 deletions(-)
delete mode 100644 UIMod/onboard_bundled/assets/script.js
diff --git a/UIMod/onboard_bundled/assets/script.js b/UIMod/onboard_bundled/assets/script.js
deleted file mode 100644
index b171f63a..00000000
--- a/UIMod/onboard_bundled/assets/script.js
+++ /dev/null
@@ -1,520 +0,0 @@
-// /static/script.js
-document.addEventListener('DOMContentLoaded', () => {
- window.GPUSaverEnabled = localStorage.getItem('GPUSaverEnabled') === 'true' || false;
- typeText(document.querySelector('h1'), 30);
- setupTabs();
- fetchDetectionEvents();
- fetchBackups();
- handleConsole();
- pollServerStatus();
- // Create planets with size, orbit radius, speed, and color
- const planetContainer = document.getElementById('planet-container');
- createPlanet(planetContainer, 80, 650, 34, 'rgba(200, 100, 50, 0.7)');
- createPlanet(planetContainer, 50, 1000, 46, 'rgba(100, 200, 150, 0.5)');
- createPlanet(planetContainer, 30, 1250, 63, 'rgba(50, 150, 250, 0.6)');
- createPlanet(planetContainer, 70, 400, 28, 'rgba(200, 150, 200, 0.7)');
- console.warn("If you see errors for sscm.js or sscm.css, you may want to enable SSCM.");
-
-});
-
-// Global references to EventSource objects
-let outputEventSource = null;
-let detectionEventSource = null;
-
-// Utility function for typing text
-function typeText(element, speed) {
- // Check if typing is already in progress
- if (element.dataset.isTyping === 'true') {
- // Optionally, clear the previous timeout (requires storing it)
- clearTimeout(element.dataset.timeoutId);
- }
-
- const fullText = element.textContent;
- element.textContent = '';
- element.dataset.isTyping = 'true'; // Mark as typing
- let i = 0;
-
- const typeChar = () => {
- if (i < fullText.length) {
- element.textContent += fullText.charAt(i++);
- const timeoutId = setTimeout(typeChar, speed);
- element.dataset.timeoutId = timeoutId; // Store timeout ID
- } else {
- element.dataset.isTyping = 'false'; // Done typing
- delete element.dataset.timeoutId;
- }
- };
- typeChar();
-}
-
-// Utility function for typing text with a callback
-function typeTextWithCallback(element, text, speed, callback) {
- if (element.dataset.isTyping === 'true') {
- clearTimeout(element.dataset.timeoutId);
- }
-
- element.textContent = '';
- element.dataset.isTyping = 'true';
- let i = 0;
-
- const typeChar = () => {
- if (i < text.length) {
- element.textContent += text.charAt(i++);
- const timeoutId = setTimeout(typeChar, speed);
- element.dataset.timeoutId = timeoutId;
- } else {
- element.dataset.isTyping = 'false';
- delete element.dataset.timeoutId;
- if (callback) setTimeout(callback, 50);
- }
- };
- typeChar();
-}
-
-// Tab management
-function setupTabs() {
- showTab('console-tab');
-}
-
-function showTab(tabId) {
- document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
- document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
- const tab = document.getElementById(tabId);
- tab.classList.add('active');
- document.querySelector(`.tab-button[onclick*="showTab('${tabId}')"]`).classList.add('active');
-}
-
-// Server control functions
-function startServer() {
- toggleServer('/start');
-}
-
-function stopServer() {
- toggleServer('/stop');
-}
-
-function toggleServer(endpoint) {
- const status = document.getElementById('status');
- fetch(endpoint)
- .then(response => response.text())
- .then(data => {
- status.hidden = false;
- typeTextWithCallback(status, data, 20, () => {
- setTimeout(() => status.hidden = true, 10000);
- });
- })
- .catch(err => console.error(`Failed to ${endpoint}:`, err));
-}
-
-// EventSource management
-function closeEventSources() {
- [outputEventSource, detectionEventSource].forEach(source => {
- if (source) {
- source.close();
- console.log(`${source === outputEventSource ? 'Output' : 'Detection events'} stream closed`);
- }
- });
- outputEventSource = detectionEventSource = null;
-}
-
-function navigateTo(url) {
- closeEventSources();
- window.location.href = url;
-}
-
-// Detection events streaming
-function fetchDetectionEvents() {
- const maxMessages = 500;
- const detectionConsole = document.getElementById('detection-console');
-
- const connect = () => {
- detectionEventSource = new EventSource('/events');
-
- detectionEventSource.onmessage = event => {
- const message = document.createElement('div');
- message.className = `detection-event ${getEventClassName(event.data)}`;
-
- const timestamp = document.createElement('span');
- timestamp.className = 'event-timestamp';
- timestamp.textContent = `${new Date().toLocaleTimeString()}: `;
-
- const content = document.createElement('span');
- content.textContent = event.data;
-
- message.append(timestamp, content);
- detectionConsole.appendChild(message);
-
- while (detectionConsole.childElementCount > maxMessages) {
- detectionConsole.firstChild.remove();
- }
- detectionConsole.scrollTop = detectionConsole.scrollHeight;
-
- const detectionTab = document.getElementById('detection-tab');
- if (!detectionTab.classList.contains('active')) {
- const tabButton = document.querySelector('.tab-button[onclick*="detection-tab"]');
- tabButton.classList.add('notification');
- setTimeout(() => tabButton.classList.remove('notification'), 3000);
- }
- };
-
- detectionEventSource.onopen = () => console.log("Detection events stream connected");
-
- detectionEventSource.onerror = () => {
- console.error("Detection events stream disconnected");
- detectionEventSource.close();
- detectionEventSource = null;
- if (window.location.pathname === '/') {
- setTimeout(connect, 2000);
- }
- };
- };
- connect();
-}
-
-function getEventClassName(eventText) {
- const checks = [
- ['Server is ready', 'event-server-ready'],
- ['Server is starting', 'event-server-starting'],
- ['Server error', 'event-server-error'],
- ['Player', 'connecting', 'event-player-connecting'],
- ['Player', 'ready', 'event-player-ready'],
- ['Player', 'disconnected', 'event-player-disconnect'],
- ['World Saved', 'event-world-saved'],
- ['Exception', 'event-exception']
- ];
-
- return checks.find(([text, , condition]) =>
- condition ? eventText.includes(text) && eventText.includes(condition) : eventText.includes(text)
- )?.[1] || '';
-}
-
-// Backup management
-function fetchBackups() {
- fetch('/api/v2/backups?mode=classic')
- .then(response => response.text())
- .then(data => {
- const backupList = document.getElementById('backupList');
- backupList.innerHTML = '';
-
- if (data.trim() === "No valid backup files found.") {
- backupList.textContent = data;
- } else {
- data.split('\n').filter(Boolean).forEach(backup => {
- const li = document.createElement('li');
- li.className = 'backup-item';
- li.innerHTML = `${backup} `;
- backupList.appendChild(li);
- });
- }
- })
- .catch(err => console.error("Failed to fetch backups:", err));
-}
-
-function extractIndex(backupText) {
- return backupText.match(/Index: (\d+)/)?.[1] || null;
-}
-
-function restoreBackup(index) {
- const status = document.getElementById('status');
- fetch(`/api/v2/backups/restore?index=${index}`)
- .then(response => response.text())
- .then(data => {
- status.hidden = false;
- typeTextWithCallback(status, data, 20, () => {
- setTimeout(() => status.hidden = true, 30000);
- });
- })
- .catch(err => console.error(`Failed to restore backup ${index}:`, err));
-}
-
-// Console initialization with SSE stream setup
-function handleConsole() {
- const consoleElement = document.getElementById('console');
- consoleElement.innerHTML = '';
- const bootTitle = "Interface initializing...";
- const bootCompleteMessage = "Interface ready.๐ฎ Happy gaming! ๐ฎ";
- const bugChance = Math.random();
- const bugMessage = "ERROR: Nuclear parts in airflow detected! Initiating repair sequence...";
-
- const funMessages = [
- "Calibrating quantum flux capacitors...",
- "Initializing player happiness modules...",
- "Checking for monsters under the server...",
- "Brewing coffee for the CPU...",
- "Charging laser sharks...",
- "Teaching AI to say 'please' and 'thank you'...",
- "Polishing pixels to a mirror shine...",
- "Convincing electrons to flow in the right direction...",
- "Rebooting atmospheric systems for the 17th time...",
- "Attempting to locate your body after that last airlock malfunction...",
- "Converting oxygen to errors at alarming efficiency...",
- "Persuading physics engine to acknowledge gravity exists...",
- "Calculating ways your base will catastrophically depressurize...",
- "Optimizing unity garbage collection (good luck with that)...",
- "Aligning planetary rotation with server tick rate...",
- "Patching holes in space-time continuum and your habitat...",
- "Convincing solar panels that 'sun' is not just a theoretical concept...",
- "Negotiating peace treaty between logic circuits and the laws of thermodynamics...",
- "Compressing atmosphere until your CPU begs for mercy...",
- "Measuring distance between you and nearest fatal bug...",
- "Attempting to explain 'pipe networks' to confused server hamsters...",
- "Calculating probability of survival (spoiler: it's low)...",
- "Wrangling rogue Unity instances back into containment...",
- "Sacrificing RAM to the gods of stable framerates...",
- "Convincing electrons to flow in the right direction... nope, the power grid's borked.",
- "Patching hull breaches with duct tape and prayers...",
- "Recalculating O2 levels... wait, why is it all CO2 now?",
- "Spinning up the fabricator... hope it doesnโt eat the server this time.",
- "Debugging Unity physics... object launched into orbit, send help.",
- "Warming up the furnace... or just setting the base on fire, 50/50 shot.",
- "Rerouting pipes... because who needs logical fluid dynamics anyway?",
- "Loading terrain... oh look, itโs floating 3 meters above the ground again.",
- "Processing ore... into a fine paste of lag and despair.",
- "Stabilizing frame rate... lol, just kidding, welcome to 12 FPS city.",
- "Checking for updates... new bug introduced, feature still broken!",
- "Assembling solar tracker... now itโs tracking the admin instead.",
- "Balancing gas mixtures... kaboom imminent, run you fool!"
- ];
-
- const addMessage = (text, color, style = 'normal') => {
- const div = document.createElement('div');
- div.textContent = text;
- div.style.color = color;
- div.style.fontStyle = style;
- consoleElement.appendChild(div);
- consoleElement.scrollTop = consoleElement.scrollHeight;
- };
-
- // Dynamically create SSCM command input
- const createCommandInput = async () => {
- try {
- // Make API call to check if SSCM is enabled
- const response = await fetch('/api/v2/SSCM/enabled', {
- method: 'GET',
- headers: {
- 'Accept': 'application/json'
- }
- });
-
- // If status is not 200, exit the function
- if (response.status !== 200) {
- console.log('SSCM is not enabled, status:', response.status);
- return;
- }
-
- // Proceed to create command input UI if status is 200
- console.log("Creating command input...");
- const commandContainer = document.createElement('div');
- commandContainer.className = 'sscm-command-container';
-
- const prompt = document.createElement('span');
- prompt.className = 'prompt';
- prompt.textContent = '>';
-
- const input = document.createElement('input');
- input.id = 'sscm-command-input';
- input.type = 'text';
- input.placeholder = 'Enter command..';
- input.setAttribute('autocomplete', 'off');
-
- const suggestions = document.createElement('div');
- suggestions.id = 'sscm-autocomplete-suggestions';
- suggestions.className = 'sscm-suggestions';
-
- commandContainer.append(prompt, input, suggestions);
- consoleElement.appendChild(commandContainer);
- } catch (error) {
- console.error('Error checking SSCM enabled status:', error);
- return; // Exit on error
- }
- };
-
- // Start with initializing message
- typeTextWithCallback(consoleElement, bootTitle, 30, () => {
- // Show two funny messages while connecting
- const messageIndex1 = Math.floor(Math.random() * funMessages.length);
- addMessage(funMessages[messageIndex1], '#0af', 'italic');
-
- let messageIndex2;
- do {
- messageIndex2 = Math.floor(Math.random() * funMessages.length);
- } while (messageIndex2 === messageIndex1);
- addMessage(funMessages[messageIndex2], '#0af', 'italic');
-
- // Set up the persistent console stream
- outputEventSource = new EventSource('/console');
-
- // Persistent message handler
- outputEventSource.onmessage = event => {
- const message = document.createElement('div');
- message.textContent = event.data;
- consoleElement.insertBefore(message, consoleElement.querySelector('.sscm-command-container')); // Insert before input
- // Auto-scroll only if at bottom
- if (consoleElement.scrollTop + consoleElement.clientHeight >= consoleElement.scrollHeight - 10) {
- consoleElement.scrollTop = consoleElement.scrollHeight;
- }
- };
-
- outputEventSource.onopen = () => {
- console.log("Console stream connected");
- finishInitialization();
- };
-
- outputEventSource.onerror = () => {
- console.error("Console stream disconnected");
- outputEventSource.close();
- outputEventSource = null;
- addMessage("Warning: Console stream unavailable. Retrying...", '#ff0');
- if (window.location.pathname === '/') {
- setTimeout(() => {
- if (!outputEventSource) {
- // Re-run setup to reconnect
- consoleElement.innerHTML = ''; // Clear console for fresh start
- handleConsole();
- }
- }, 2000);
- }
- };
- });
-
- function finishInitialization() {
- if (bugChance < 0.05) {
- addMessage(bugMessage, 'red');
- setTimeout(() => {
- addMessage("Repair complete. Continuing initialization...", 'green');
- completeBoot();
- }, 1000);
- } else {
- completeBoot();
- }
- }
-
- function completeBoot() {
- setTimeout(() => {
- createCommandInput(); // Add input after boot
- addMessage(bootCompleteMessage, '#0f0');
- consoleElement.scrollTop = consoleElement.scrollHeight;
- }, 500);
- }
-}
-
-function createPlanet(container, size, orbitRadius, speed, color) {
- const orbit = document.createElement('div');
- orbit.classList.add('orbit');
- orbit.style.width = `${orbitRadius * 2}px`;
- orbit.style.height = `${orbitRadius * 2}px`;
- orbit.style.position = 'absolute';
- orbit.style.left = '50%';
- orbit.style.top = '50%';
- orbit.style.transform = 'translate(-50%, -50%)';
-
- // Add random delay to start animation at different points
- const randomDelay = -(Math.random() * speed); // Negative delay to offset start
- orbit.style.animation = `orbit ${speed}s linear infinite ${randomDelay}s`;
-
- const planet = document.createElement('div');
- planet.classList.add('planet');
- planet.style.width = `${size}px`;
- planet.style.height = `${size}px`;
- planet.style.position = 'absolute';
- planet.style.left = '0%';
- planet.style.top = '50%';
- planet.style.backgroundColor = color;
- planet.style.borderRadius = '50%';
- planet.style.boxShadow = `0 0 20px ${color}`;
-
- orbit.appendChild(planet);
- container.appendChild(orbit);
-}
-
-
-function pollServerStatus() {
- window.gamserverstate = false;
- const statusInterval = setInterval(() => {
- fetch('/api/v2/server/status')
- .then(response => response.json())
- .then(data => {
- updateStatusIndicator(data.isRunning);
- if (data.uuid) {
- localStorage.setItem('gameserverrunID', data.uuid);
- }
- })
- .catch(err => {
- console.error("Failed to fetch server status:", err);
- updateStatusIndicator(false, true); // Set error state
- });
- }, 3500); // Poll every 3.5 seconds (adjusted from 1000 to reduce server load checking the status each time)
-
- // Store the interval ID so we can clear it if needed
- window.statusPollingInterval = statusInterval;
-}
-
-function updateStatusIndicator(isRunning, isError = false) {
- const indicator = document.getElementById('status-indicator');
-
- if (isError) {
- indicator.className = 'status-indicator error';
- indicator.title = 'Error fetching server status';
- window.gamserverstate = false;
- return;
- }
-
- if (isRunning) {
- indicator.className = 'status-indicator online';
- indicator.title = 'Server is running';
- window.gamserverstate = true;
- } else {
- indicator.className = 'status-indicator offline';
- indicator.title = 'Server is offline';
- window.gamserverstate = false;
- }
-}
-
-function resourceSaver(pause) {
- // Get space background once outside the loop
- const spaceBackground = document.getElementById('space-background');
-
- // Handle animation states for all elements
- document.querySelectorAll('*').forEach(element => {
- element.style.animationPlayState = pause ? 'paused' : 'running';
- });
-
- // Fade the space background in/out instead of abrupt display change
- if (pause) {
- // Fade out
- spaceBackground.style.transition = 'opacity 0.5s ease';
- spaceBackground.style.opacity = '0';
- // Only hide it after the fade completes
- setTimeout(() => {
- if (document.hasFocus() === false) { // Double-check we're still unfocused
- spaceBackground.style.display = 'none';
- }
- }, 500);
- } else {
- // Make it visible first, then fade in
- spaceBackground.style.display = 'block';
- // Use setTimeout to ensure the display change is processed before starting the fade
- setTimeout(() => {
- spaceBackground.style.transition = 'opacity 0.5s ease';
- spaceBackground.style.opacity = '1';
- }, 10);
- }
-}
-
-function toggleGPUSaver() {
- window.GPUSaverEnabled = !window.GPUSaverEnabled;
- localStorage.setItem('GPUSaverEnabled', window.GPUSaverEnabled);
-}
-
-// Event listeners for window focus and blur
-window.addEventListener('focus', () => {
- if (window.GPUSaverEnabled) {
- resourceSaver(false); // Resume animations when page is in focus
- }
-});
-
-window.addEventListener('blur', () => {
- if (window.GPUSaverEnabled) {
- resourceSaver(true); // Pause animations when page loses focus
- }
-});
\ No newline at end of file
From da40185d326c18849412037b97e316dc6a8a1a31 Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Sat, 23 Aug 2025 17:11:03 +0200
Subject: [PATCH 12/20] moved info notice css to its own file
---
static/css/info-notice.css | 88 ++++++++++++++++++++++++++++++++++++++
1 file changed, 88 insertions(+)
create mode 100644 static/css/info-notice.css
diff --git a/static/css/info-notice.css b/static/css/info-notice.css
new file mode 100644
index 00000000..47913877
--- /dev/null
+++ b/static/css/info-notice.css
@@ -0,0 +1,88 @@
+.info-notice {
+ border: 1px solid #4a90e2;
+ border-radius: 8px;
+ padding: 15px;
+ margin: 15px 0;
+ background: rgba(74, 144, 226, 0.1);
+ color: #e0e0e0;
+ box-shadow: 0 2px 8px rgba(74, 144, 226, 0.2);
+}
+
+.info-notice h3 {
+ margin: 0 0 12px 0;
+ font-size: 1.1em;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #4a90e2;
+}
+
+.info-notice p {
+ margin: 8px 0;
+ line-height: 1.4;
+ font-size: 0.9em;
+}
+
+.info-notice a {
+ color: #4a90e2;
+ text-decoration: underline;
+}
+
+.info-notice a:hover {
+ color: #7bb3f0;
+}
+
+.notice-icon {
+ display: inline-block;
+ font-size: 1.1em;
+}
+
+.update {
+ border: 1px solid #af534c;
+ border-radius: 8px;
+ padding: 12px;
+ margin: 10px 0;
+ background: #784a47;
+ color: #c8e6c9;
+ box-shadow: 0 2px 6px rgba(76, 175, 80, 0.2);
+}
+
+.update h3 {
+ margin: 0 0 8px 0;
+ font-size: 1em;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: #4caf50;
+}
+
+.update p {
+ margin: 5px 0;
+ font-size: 0.85em;
+ line-height: 1.3;
+}
+
+.status-good {
+ color: #4caf50;
+ font-weight: 600;
+}
+
+@media (max-width: 768px) {
+ .info-notice {
+ margin: 10px 5px;
+ padding: 12px;
+ }
+
+ .info-notice h3 {
+font-size: 1em;
+ }
+
+ .info-notice p {
+font-size: 0.85em;
+ }
+
+ .positive-update {
+ margin: 8px 5px;
+ padding: 10px;
+ }
+}
\ No newline at end of file
From e67541e2dc2efef3a58ff8d914cebfd142d34eed Mon Sep 17 00:00:00 2001
From: JacksonTheMaster
Date: Sat, 23 Aug 2025 17:11:35 +0200
Subject: [PATCH 13/20] added universal popup (currently only for steamcmd
button)
---
UIMod/onboard_bundled/assets/css/popup.css | 78 +++++++++++++
UIMod/onboard_bundled/assets/js/popup.js | 34 ++++++
UIMod/onboard_bundled/assets/js/server-api.js | 6 +-
UIMod/onboard_bundled/ui/index.html | 108 +++---------------
src/web/http.go | 4 +-
5 files changed, 129 insertions(+), 101 deletions(-)
create mode 100644 UIMod/onboard_bundled/assets/css/popup.css
create mode 100644 UIMod/onboard_bundled/assets/js/popup.js
diff --git a/UIMod/onboard_bundled/assets/css/popup.css b/UIMod/onboard_bundled/assets/css/popup.css
new file mode 100644
index 00000000..6b304c2a
--- /dev/null
+++ b/UIMod/onboard_bundled/assets/css/popup.css
@@ -0,0 +1,78 @@
+@import '/static/css/variables.css';
+
+.popup {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(8px);
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.popup-content {
+ background-color: var(--bg-dark);
+ padding: 20px;
+ border-radius: 10px;
+ text-align: center;
+ max-width: 30vw;
+ box-shadow: 0 5px 30px var(--primary-glow);
+ overflow: hidden;
+}
+.popup-content h2 {
+ margin: 10px 0;
+ font-size: 1.5em;
+}
+
+.popup-content p {
+ margin: 10px 0;
+}
+
+.popup-content button {
+ margin-top: 15px;
+ padding: 10px 20px;
+ color: white;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+}
+
+.popup.error .popup-content {
+ border-left: 2px solid #ff4d4d;
+}
+
+.popup.error button {
+ background-color: #ff4d4d;
+}
+
+.popup.error button:hover {
+ background-color: #e60000;
+}
+
+.popup.success .popup-content {
+ border-left: 2px solid #28a745;
+}
+
+.popup.success button {
+ background-color: #28a745;
+}
+
+.popup.success button:hover {
+ background-color: #218838;
+}
+
+.popup.info .popup-content {
+ border-left: 2px solid #17a2b8;
+}
+
+.popup.info button {
+ background-color: #17a2b8;
+}
+
+.popup.info button:hover {
+ background-color: #138496;
+}
\ No newline at end of file
diff --git a/UIMod/onboard_bundled/assets/js/popup.js b/UIMod/onboard_bundled/assets/js/popup.js
new file mode 100644
index 00000000..9b018c86
--- /dev/null
+++ b/UIMod/onboard_bundled/assets/js/popup.js
@@ -0,0 +1,34 @@
+function showPopup(status, message) {
+ const popup = document.getElementById('universalPopup');
+ const popupTitle = document.getElementById('popupTitle');
+ const popupMessage = document.getElementById('popupMessage');
+
+ popup.className = 'popup';
+ popupTitle.textContent = '';
+ popupMessage.textContent = message;
+
+ switch(status.toLowerCase()) {
+ case 'error':
+ popup.classList.add('error');
+ popupTitle.textContent = 'Error';
+ break;
+ case 'success':
+ popup.classList.add('success');
+ popupTitle.textContent = 'Success';
+ break;
+ case 'info':
+ popup.classList.add('info');
+ popupTitle.textContent = 'Info';
+ break;
+ default:
+ popup.classList.add('info');
+ popupTitle.textContent = 'Info';
+ }
+
+ popup.style.display = 'flex';
+}
+
+function closePopup() {
+ const popup = document.getElementById('universalPopup');
+ popup.style.display = 'none';
+}
\ No newline at end of file
diff --git a/UIMod/onboard_bundled/assets/js/server-api.js b/UIMod/onboard_bundled/assets/js/server-api.js
index 9ac55440..ca6722dd 100644
--- a/UIMod/onboard_bundled/assets/js/server-api.js
+++ b/UIMod/onboard_bundled/assets/js/server-api.js
@@ -25,13 +25,11 @@ function toggleServer(endpoint) {
function triggerSteamCMD() {
const status = document.getElementById('status');
status.hidden = false;
- typeTextWithCallback(status, 'Triggering SteamCMD, please wait. SteamCMD will print log output only to the CLI ', 20, () => {
+ typeTextWithCallback(status, 'Running SteamCMD, please wait. ', 20, () => {
fetch('/api/v2/steamcmd/run')
.then(response => response.json())
.then(data => {
- typeTextWithCallback(status, data.message, 20, () => {
- setTimeout(() => status.hidden = true, 10000);
- });
+ showPopup("info", data.message);
})
.catch(err => {
typeTextWithCallback(status, 'Error: Failed to trigger SteamCMD', 20, () => {
diff --git a/UIMod/onboard_bundled/ui/index.html b/UIMod/onboard_bundled/ui/index.html
index f7dbf1b7..bea72354 100644
--- a/UIMod/onboard_bundled/ui/index.html
+++ b/UIMod/onboard_bundled/ui/index.html
@@ -11,102 +11,21 @@
+
+
-
+
+
+
+
+
+
+
@@ -151,7 +70,7 @@
โ ๏ธ
๐ข
- T his box is left over from v5.4.35, but will be kept here with display none for future use.
+ This box is left over from v5.4.35, but will be kept here with display none for possible future use.
Timeline: A Stable SteamServerUI release planned for Q4-2025. Christmas, probably. StationeersServerUI will continue receiving long time support updates until Q42026. See the Roadmap here for more
details.
-
-
@@ -224,5 +141,6 @@