diff --git a/UIMod/onboard_bundled/assets/css/home.css b/UIMod/onboard_bundled/assets/css/home.css index 2bc78208..1578a958 100644 --- a/UIMod/onboard_bundled/assets/css/home.css +++ b/UIMod/onboard_bundled/assets/css/home.css @@ -21,6 +21,21 @@ transition: opacity var(--transition-fast); } +.uptime-display { + position: absolute; + right: 50px; + top: 22px; + font-family: 'Share Tech Mono', monospace; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.75); + background-color: rgba(0, 0, 0, 0.35); + padding: 2px 8px; + border-radius: 4px; + letter-spacing: 0.5px; + white-space: nowrap; + transition: opacity 0.3s ease; +} + .status-indicator { width: 16px; height: 16px; diff --git a/UIMod/onboard_bundled/assets/js/server-api.js b/UIMod/onboard_bundled/assets/js/server-api.js index 38d91e8d..5d1bb125 100644 --- a/UIMod/onboard_bundled/assets/js/server-api.js +++ b/UIMod/onboard_bundled/assets/js/server-api.js @@ -262,7 +262,7 @@ function pollRecurringTasks() { fetch('/api/v2/server/status') .then(response => response.json()) .then(data => { - updateStatusIndicator(data.isRunning); + updateStatusIndicator(data.isRunning, false, data.uptime); if (data.uuid) { localStorage.setItem('gameserverrunID', data.uuid); } @@ -290,13 +290,15 @@ function pollRecurringTasks() { }, 30000); } -function updateStatusIndicator(isRunning, isError = false) { +function updateStatusIndicator(isRunning, isError = false, uptime = '') { const indicator = document.getElementById('status-indicator'); + const uptimeDisplay = document.getElementById('uptime-display'); if (isError) { indicator.className = 'status-indicator error'; indicator.title = 'Error fetching server status'; window.gamserverstate = false; + if (uptimeDisplay) uptimeDisplay.style.display = 'none'; return; } @@ -309,4 +311,14 @@ function updateStatusIndicator(isRunning, isError = false) { indicator.title = 'Server is offline'; window.gamserverstate = false; } + + // Show uptime only when server is running and uptime is not "0s" + if (uptimeDisplay) { + if (isRunning && uptime && uptime !== '0s') { + uptimeDisplay.textContent = uptime; + uptimeDisplay.style.display = 'inline-block'; + } else { + uptimeDisplay.style.display = 'none'; + } + } } \ No newline at end of file diff --git a/UIMod/onboard_bundled/ui/index.html b/UIMod/onboard_bundled/ui/index.html index 19b65dd0..aa7b51dc 100644 --- a/UIMod/onboard_bundled/ui/index.html +++ b/UIMod/onboard_bundled/ui/index.html @@ -63,6 +63,7 @@

+

Stationeers Server UI v{{.Version}}{{.SSUIIdentifier}}

diff --git a/src/web/http.go b/src/web/http.go index 1b214fe6..6e6e7666 100644 --- a/src/web/http.go +++ b/src/web/http.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strings" + "time" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/config" "github.com/JacksonTheMaster/StationeersServerUI/v5/src/localization" @@ -49,8 +50,9 @@ func StopServer(w http.ResponseWriter, r *http.Request) { func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { runState := config.GetIsGameServerRunning() - response := map[string]interface{}{ + response := map[string]any{ "isRunning": runState, + "uptime": prettyUptime(gamemgr.GetServerUptime()), "uuid": gamemgr.GameServerUUID.String(), } w.Header().Set("Content-Type", "application/json") @@ -60,6 +62,40 @@ func GetGameServerRunState(w http.ResponseWriter, r *http.Request) { } } +// helper to format the uptime in a more human readable way, e.g. "1d2h3m4s" instead of "26h3m4s" +func prettyUptime(d time.Duration) string { + if d <= 0 { + return "0s" + } + + d = d.Round(time.Second) // optional: round to nearest second + var parts []string + + days := int64(d / (24 * time.Hour)) + d -= time.Duration(days) * 24 * time.Hour + + hours := int64(d / time.Hour) + d -= time.Duration(hours) * time.Hour + + minutes := int64(d / time.Minute) + d -= time.Duration(minutes) * time.Minute + + seconds := int64(d / time.Second) + + if days > 0 { + parts = append(parts, fmt.Sprintf("%dd", days)) + } + if hours > 0 || days > 0 { + parts = append(parts, fmt.Sprintf("%dh", hours)) + } + if minutes > 0 || hours > 0 || days > 0 { + parts = append(parts, fmt.Sprintf("%dm", minutes)) + } + parts = append(parts, fmt.Sprintf("%ds", seconds)) + + return strings.Join(parts, "") +} + // CommandHandler handles POST requests to execute commands via commandmgr. // Expects a command in the request body. Returns 204 on success or error details. func CommandHandler(w http.ResponseWriter, r *http.Request) {