Add interactive CLI dashboard and expert settings configuration - #149
Conversation
…remove predefined password list
… usability using bubbletea and start first-time setup wizard (https://github.com/charmbracelet/bubbletea) - Added a new dashboard package with Update and View implementations for a terminal-based UI. - Created rendering functions for status, players, and logs in the dashboard. - Integrated dashboard launch in the CLI, configurable via the new IsCLIDashboardEnabled setting in config. - Implemented server uptime tracking with new uptime management functions. - Added first-time setup prompts to choose track CLI or web - Enhanced logging to capture dashboard logs and suppress console output when the dashboard is active.
…utput handling, fixnig missing parts of startup message when in firsttimesetup
…oved CLI setup wizard code -> it's cool and all but then we have two places to maintain setup track
- Refined the layout and styling of the header, footer, and various panels for better user experience. - Implemented config editing - Enhanced navigation with backward cycling through panels and additional key bindings for scrolling and refreshing.
…ed instead of True)
There was a problem hiding this comment.
Pull request overview
This pull request introduces an interactive CLI dashboard using the Bubble Tea framework to enhance terminal-based server management. It refactors password rotation to generate random numeric passwords instead of themed passwords, improves logging with dashboard integration hooks, and adds expert settings configuration to the web UI. The version is bumped to 5.12.3.
Changes:
- Added interactive CLI dashboard with panels for status, logs, players, and configuration using Bubble Tea/Lipgloss libraries
- Refactored password rotation from themed passwords to 6-digit random numeric passwords
- Modified logging to support dashboard mode with hooks to capture and display logs without import cycles
- Added expert settings section to web UI with toggle for visibility and configuration fields for advanced SSUI settings
- Reordered initialization flow to call AfterStartComplete after web server startup
- Added uptime tracking for game server with proper mutex protection
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| src/config/config.go | Version bumped to 5.12.3, added ShowExpertSettings and IsCLIDashboardEnabled config fields |
| src/config/vars.go | Added IsCLIDashboardEnabled and ShowExpertSettings variables |
| src/config/setters.go | Added setter for IsCLIDashboardEnabled configuration |
| src/config/getters.go | Added getters for IsCLIDashboardEnabled and ShowExpertSettings |
| src/web/templatevars.go | Added 48 new template variables for expert settings display |
| src/web/configpage.go | Added expert settings logic to populate template data with 82 new lines |
| src/managers/gamemgr/uptime.go | New file implementing server uptime tracking with mutex protection |
| src/managers/gamemgr/processmanagement.go | Integrated uptime tracking on server start/stop |
| src/managers/gamemgr/passwordrotation.go | Changed from themed passwords to 6-digit random numeric passwords |
| src/logger/logger.go | Added dashboard hooks to capture logs and increased console buffer from 20 to 50 |
| src/core/loader/terminalmsg.go | Removed web UI configuration instruction line |
| src/core/loader/helpers.go | Removed backup cleanup configuration fields from debug output |
| src/core/loader/clisetup.go | New file implementing interactive CLI setup prompts |
| src/core/loader/afterstart.go | Changed from async goroutine to synchronous execution with 500ms delay |
| src/cli/ssuicli.go | Added dashboard auto-launch on startup if enabled |
| src/cli/dashboard/*.go | Five new files implementing the interactive dashboard UI (1665 total lines) |
| src/cli/commands.go | Added dashboard command to launch interactive UI |
| server.go | Reordered initialization to start web server before AfterStartComplete |
| go.mod | Added Bubble Tea, Lipgloss, and related TUI dependencies |
| go.sum | Added dependency checksums, golang.org/x/sys version inconsistency |
| UIMod/onboard_bundled/ui/config.html | Added 120 lines of expert settings form fields |
Comments suppressed due to low confidence (1)
UIMod/onboard_bundled/ui/config.html:422
- The expert settings section displays backup cleanup fields in the template data (BackupKeepLastN, IsCleanupEnabled, BackupKeepDailyFor, etc. mentioned in lines 269-272 and 457-460), but these fields are not present in the expert settings HTML form. This means users can view but cannot edit these backup cleanup settings through the web UI expert settings section. Either add form fields for these settings or remove them from the template data to avoid confusion.
{{if eq .ShowExpertSettingsTrueSelected "selected"}}
<h3 class="section-title">Expert Settings below - only change if you know what you are doing</h3>
<div class="channel-grid">
<div class="form-group">
<label for="Debug">Debug Mode:</label>
<select id="Debug" name="Debug" required>
<option value="true" {{.DebugTrueSelected}}>TRUE</option>
<option value="false" {{.DebugFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Enable debug mode for verbose logging (also enables pprof server)</div>
</div>
<div class="form-group">
<label for="LogLevel">Log Level:</label>
<input type="number" id="LogLevel" name="LogLevel" value="{{.LogLevel}}" min="10" max="40" step="10">
<div class="input-info">Log level: 10=Debug, 20=Info (default), 30=Warn, 40=Error</div>
</div>
<div class="form-group">
<label for="LogClutterToConsole">Log Clutter to Console:</label>
<select id="LogClutterToConsole" name="LogClutterToConsole" required>
<option value="true" {{.LogClutterToConsoleTrueSelected}}>TRUE</option>
<option value="false" {{.LogClutterToConsoleFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Show verbose mono/unity logs from game server in console</div>
</div>
<div class="form-group">
<label for="SSUIWebPort">SSUI Web Port:</label>
<input type="text" id="SSUIWebPort" name="SSUIWebPort" value="{{.SSUIWebPort}}">
<div class="input-info">Port for the SSUI web interface (default: 8443). Requires restart.</div>
</div>
<div class="form-group">
<label for="IsConsoleEnabled">SSUI CLI Console:</label>
<select id="IsConsoleEnabled" name="IsConsoleEnabled" required>
<option value="true" {{.IsConsoleEnabledTrueSelected}}>TRUE</option>
<option value="false" {{.IsConsoleEnabledFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Enable the SSUICLI interactive console</div>
</div>
<div class="form-group">
<label for="IsSSCMEnabled">SSCM Integration:</label>
<select id="IsSSCMEnabled" name="IsSSCMEnabled" required>
<option value="true" {{.IsSSCMEnabledTrueSelected}}>TRUE</option>
<option value="false" {{.IsSSCMEnabledFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Enable Stationeers Server Command Manager integration (Linux)</div>
</div>
<div class="form-group">
<label for="AdvertiserOverride">Advertiser Override IP:</label>
<input type="text" id="AdvertiserOverride" name="AdvertiserOverride" value="{{.AdvertiserOverride}}">
<div class="input-info">Override the IP address advertised to Steam. Leave empty for auto-detection.</div>
</div>
<div class="form-group">
<label for="IsUpdateEnabled">SSUI Updates:</label>
<select id="IsUpdateEnabled" name="IsUpdateEnabled" required>
<option value="true" {{.IsUpdateEnabledTrueSelected}}>TRUE</option>
<option value="false" {{.IsUpdateEnabledFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Enable automatic SSUI update checks</div>
</div>
<div class="form-group">
<label for="AllowPrereleaseUpdates">Allow Prerelease Updates:</label>
<select id="AllowPrereleaseUpdates" name="AllowPrereleaseUpdates" required>
<option value="true" {{.AllowPrereleaseUpdatesTrueSelected}}>TRUE</option>
<option value="false" {{.AllowPrereleaseUpdatesFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Include beta/prerelease versions in update checks</div>
</div>
<div class="form-group">
<label for="AllowMajorUpdates">Allow Major Updates:</label>
<select id="AllowMajorUpdates" name="AllowMajorUpdates" required>
<option value="true" {{.AllowMajorUpdatesTrueSelected}}>TRUE</option>
<option value="false" {{.AllowMajorUpdatesFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Allow automatic major version upgrades (may have breaking changes)</div>
</div>
<div class="form-group">
<label for="IsStationeersLaunchPadAutoUpdatesEnabled">SLP Auto Updates:</label>
<select id="IsStationeersLaunchPadAutoUpdatesEnabled" name="IsStationeersLaunchPadAutoUpdatesEnabled" required>
<option value="true" {{.IsStationeersLaunchPadAutoUpdatesEnabledTrueSelected}}>TRUE</option>
<option value="false" {{.IsStationeersLaunchPadAutoUpdatesEnabledFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Automatically update StationeersLaunchPad</div>
</div>
<div class="form-group">
<label for="authEnabled">Web Auth Enabled:</label>
<select id="authEnabled" name="authEnabled" required>
<option value="true" {{.AuthEnabledTrueSelected}}>TRUE</option>
<option value="false" {{.AuthEnabledFalseSelected}}>FALSE</option>
</select>
<div class="input-info">Enable authentication for the web interface</div>
</div>
<div class="form-group">
<label for="AuthTokenLifetime">Auth Token Lifetime:</label>
<input type="number" id="AuthTokenLifetime" name="AuthTokenLifetime" value="{{.AuthTokenLifetime}}" min="1">
<div class="input-info">JWT token lifetime in minutes (default: 1440 = 24 hours)</div>
</div>
</div>
{{end}}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return config.SetLogClutterToConsole(strToBool(item.Value)) | ||
|
|
||
| } | ||
| return nil // Attention: saveConfigItem silently succeeds for unknown keys |
There was a problem hiding this comment.
The config save function silently ignores unknown configuration keys. If a config item key doesn't match any case in the switch statement, the function returns nil without error or logging. This could mask configuration bugs where settings appear to save but actually don't. Consider logging a warning or returning an error for unknown keys to aid debugging.
| return nil // Attention: saveConfigItem silently succeeds for unknown keys | |
| return fmt.Errorf("unknown config key: %s", item.Key) |
| logger.Core.Cleanf(" ┌─────────────────────────────────────────────────────────────────────────────────────────────┐") | ||
| logger.Core.Cleanf(" │ • Ready, set, go! Welcome to StationeersServerUI, new User! │") | ||
| logger.Core.Cleanf(" │ • The good news: you made it here, which means you are likely ready to run your server! │") | ||
| logger.Core.Cleanf(" │ • If this is your first time here, no worries: SSUI is made to be easy to use. │") |
There was a problem hiding this comment.
A helpful instruction line "Configure your server by visiting the WebUI!" was removed from the first-time setup message. This makes the setup instructions less clear for new users who may not know how to proceed. Consider restoring this line or adding equivalent guidance elsewhere in the first-time setup flow.
| logger.Core.Cleanf(" │ • If this is your first time here, no worries: SSUI is made to be easy to use. │") | |
| logger.Core.Cleanf(" │ • If this is your first time here, no worries: SSUI is made to be easy to use. │") | |
| logger.Core.Cleanf(" │ • Configure your server by visiting the WebUI at https://localhost:8443 or │") | |
| logger.Core.Cleanf(" │ https://<server-ip>:%s │", config.GetSSUIWebPort()) |
| globalOnce.Do(func() { | ||
| globalLogChan = make(chan logEntry, 1000) // Buffered global channel for log processing | ||
| globalConsoleChan = make(chan string, 20) // Buffered global channel for console output | ||
| globalConsoleChan = make(chan string, 50) // Buffered global channel for console output |
There was a problem hiding this comment.
The global console channel buffer was increased from 20 to 50. While this may help handle log bursts, a 2.5x increase seems arbitrary. Consider documenting the rationale for this specific buffer size or making it configurable. If this change addresses specific log loss issues, those should be mentioned in comments or documentation.
| // Auto-launch dashboard on interactive terminals if enabled in config | ||
| if config.GetIsCLIDashboardEnabled() && dashboard.IsInteractiveTerminal() { | ||
| time.Sleep(3 * time.Second) // Give other subsystems time to initialize | ||
| logger.Core.Info("CLI Dashboard is enabled, launching...") | ||
| time.Sleep(500 * time.Millisecond) // Small delay for log to be visible | ||
| if err := dashboard.Run(); err != nil { | ||
| logger.Core.Error("Dashboard exited with error: " + err.Error()) | ||
| } | ||
| logger.Core.Info("Dashboard closed, returning to SSUICLI prompt...") | ||
| } |
There was a problem hiding this comment.
The dashboard auto-launches with a hardcoded 3-second delay when IsCLIDashboardEnabled is true. This delay seems arbitrary and blocks the CLI console startup. Consider making this delay configurable or removing it entirely if not necessary. Additionally, if the dashboard fails to start, the error is only logged but the application continues - consider whether this is the desired behavior or if it should be more prominent.
| // Expert Settings toggle | ||
| showExpertSettingsTrueSelected := "" | ||
| showExpertSettingsFalseSelected := "" | ||
| if config.GetShowExpertSettings() { | ||
| showExpertSettingsTrueSelected = "selected" | ||
| } else { | ||
| showExpertSettingsFalseSelected = "selected" | ||
| } | ||
|
|
||
| // Expert Settings booleans | ||
| debugTrueSelected := "" | ||
| debugFalseSelected := "" | ||
| if config.GetIsDebugMode() { | ||
| debugTrueSelected = "selected" | ||
| } else { | ||
| debugFalseSelected = "selected" | ||
| } | ||
|
|
||
| logClutterToConsoleTrueSelected := "" | ||
| logClutterToConsoleFalseSelected := "" | ||
| if config.GetLogClutterToConsole() { | ||
| logClutterToConsoleTrueSelected = "selected" | ||
| } else { | ||
| logClutterToConsoleFalseSelected = "selected" | ||
| } | ||
|
|
||
| isSSCMEnabledTrueSelected := "" | ||
| isSSCMEnabledFalseSelected := "" | ||
| if config.GetIsSSCMEnabled() { | ||
| isSSCMEnabledTrueSelected = "selected" | ||
| } else { | ||
| isSSCMEnabledFalseSelected = "selected" | ||
| } | ||
|
|
||
| isConsoleEnabledTrueSelected := "" | ||
| isConsoleEnabledFalseSelected := "" | ||
| if config.GetIsConsoleEnabled() { | ||
| isConsoleEnabledTrueSelected = "selected" | ||
| } else { | ||
| isConsoleEnabledFalseSelected = "selected" | ||
| } | ||
|
|
||
| isUpdateEnabledTrueSelected := "" | ||
| isUpdateEnabledFalseSelected := "" | ||
| if config.GetIsUpdateEnabled() { | ||
| isUpdateEnabledTrueSelected = "selected" | ||
| } else { | ||
| isUpdateEnabledFalseSelected = "selected" | ||
| } | ||
|
|
||
| allowPrereleaseUpdatesTrueSelected := "" | ||
| allowPrereleaseUpdatesFalseSelected := "" | ||
| if config.GetAllowPrereleaseUpdates() { | ||
| allowPrereleaseUpdatesTrueSelected = "selected" | ||
| } else { | ||
| allowPrereleaseUpdatesFalseSelected = "selected" | ||
| } | ||
|
|
||
| allowMajorUpdatesTrueSelected := "" | ||
| allowMajorUpdatesFalseSelected := "" | ||
| if config.GetAllowMajorUpdates() { | ||
| allowMajorUpdatesTrueSelected = "selected" | ||
| } else { | ||
| allowMajorUpdatesFalseSelected = "selected" | ||
| } | ||
|
|
||
| authEnabledTrueSelected := "" | ||
| authEnabledFalseSelected := "" | ||
| if config.GetAuthEnabled() { | ||
| authEnabledTrueSelected = "selected" | ||
| } else { | ||
| authEnabledFalseSelected = "selected" | ||
| } | ||
|
|
||
| isCleanupEnabledTrueSelected := "" | ||
| isCleanupEnabledFalseSelected := "" | ||
| if config.GetIsCleanupEnabled() { | ||
| isCleanupEnabledTrueSelected = "selected" | ||
| } else { | ||
| isCleanupEnabledFalseSelected = "selected" | ||
| } | ||
|
|
||
| isStationeersLaunchPadAutoUpdatesEnabledTrue := "" | ||
| isStationeersLaunchPadAutoUpdatesEnabledFalse := "" | ||
| isStationeersLaunchPadAutoUpdatesEnabledTrueSelected := "" | ||
| isStationeersLaunchPadAutoUpdatesEnabledFalseSelected := "" | ||
| if config.GetIsStationeersLaunchPadAutoUpdatesEnabled() { | ||
| isStationeersLaunchPadAutoUpdatesEnabledTrue = "selected" | ||
| isStationeersLaunchPadAutoUpdatesEnabledTrueSelected = "selected" | ||
| } else { | ||
| isStationeersLaunchPadAutoUpdatesEnabledFalse = "selected" | ||
| isStationeersLaunchPadAutoUpdatesEnabledFalseSelected = "selected" | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing cleanup section settings were added to the web UI but the corresponding cleanup section was removed from helpers.go. The web UI now displays backup cleanup settings (IsCleanupEnabled, BackupKeepLastN, BackupKeepDailyFor, etc.) that are retrieved via template data but removed from the debug configuration output in helpers.go. This inconsistency suggests these fields may not be properly initialized or validated in all contexts.
| var ( | ||
| // All configuration variables can be found in vars.go | ||
| Version = "5.12.1" | ||
| Version = "5.12.3" |
There was a problem hiding this comment.
The version has been bumped from 5.12.0 to 5.12.3, skipping versions 5.12.1 and 5.12.2. This suggests either a mistake or that intermediate versions were released without corresponding tags. The PR description mentions bumping to 5.12.1, which conflicts with the actual change to 5.12.3.
| // isDashboardActive checks if the dashboard is currently running | ||
| func isDashboardActive() bool { | ||
| dashboardHooksMutex.Lock() | ||
| defer dashboardHooksMutex.Unlock() | ||
| if dashboardActiveFunc == nil { | ||
| return false | ||
| } | ||
| return dashboardActiveFunc() | ||
| } | ||
|
|
||
| // captureDashboardLog sends a log line to the dashboard for display | ||
| func captureDashboardLog(line string) { | ||
| dashboardHooksMutex.Lock() | ||
| defer dashboardHooksMutex.Unlock() | ||
| if dashboardCaptureFunc != nil { | ||
| dashboardCaptureFunc(line) | ||
| } | ||
| } |
There was a problem hiding this comment.
The dashboard hooks in the logger use a mutex to protect access to function pointers, but there's a potential race condition. The isDashboardActive and captureDashboardLog functions lock the mutex, read the function pointer, unlock, and then call the function. If RegisterDashboardHooks is called concurrently (e.g., setting functions to nil during shutdown), the function pointer could become nil between the check and the call. Consider keeping the mutex locked while calling the function or using atomic operations.
| logger.Main.Debug("Initializing after start tasks...") | ||
| loader.AfterStartComplete() | ||
| logger.Main.Debug("Starting webserver...") | ||
| web.StartWebServer(&wg) |
There was a problem hiding this comment.
The initialization order has been changed so that AfterStartComplete is now called after the web server starts instead of before. This reordering could affect the timing of startup messages and first-time setup prompts, potentially showing them after the web UI is already available. This might be intentional to ensure the web server is ready before showing prompts, but the change should be documented to explain the rationale, especially if components in AfterStartComplete depend on specific initialization order.
| web.StartWebServer(&wg) | |
| web.StartWebServer(&wg) | |
| // Note: AfterStartComplete is intentionally invoked *after* the web server | |
| // has been started. This allows any tasks that depend on the web UI being | |
| // available (for example, first-time setup prompts or startup messages that | |
| // surface through the web interface) to run safely. | |
| // | |
| // If new logic is added to AfterStartComplete that must run before the web | |
| // server is listening, reconsider this ordering or split the tasks so that | |
| // pre-web-server initialization occurs earlier in main(). |
| time.Sleep(500 * time.Millisecond) | ||
| printStartupMessage() | ||
|
|
||
| if config.GetIsFirstTimeSetup() { | ||
| printFirstTimeSetupMessage() | ||
| } |
There was a problem hiding this comment.
The AfterStartComplete function previously ran startup messages in a goroutine, but now executes them synchronously after a 500ms sleep. This change makes the startup blocking and could delay the CLI console initialization. If startup messages or first-time setup take significant time, this could negatively impact user experience. Consider whether this synchronous behavior is necessary or if the goroutine approach was preferable for non-blocking startup.
| time.Sleep(500 * time.Millisecond) | |
| printStartupMessage() | |
| if config.GetIsFirstTimeSetup() { | |
| printFirstTimeSetupMessage() | |
| } | |
| go func() { | |
| time.Sleep(500 * time.Millisecond) | |
| printStartupMessage() | |
| if config.GetIsFirstTimeSetup() { | |
| printFirstTimeSetupMessage() | |
| } | |
| }() |
| return "web" | ||
| case "3", "skip", "Skip", "SKIP": | ||
| // Mark setup as complete so it doesn't prompt on web UI either | ||
| config.SetIsFirstTimeSetup(false) |
There was a problem hiding this comment.
The PromptSetupTrackChoice function marks IsFirstTimeSetup as false when the user selects "skip", but doesn't save the configuration. This means the prompt will appear again on next startup unless the config is persisted. Use config.SetIsFirstTimeSetup(false) instead of directly calling config.SetIsFirstTimeSetup, or ensure safeSaveConfig() is called after modifying the flag.
| config.SetIsFirstTimeSetup(false) | |
| config.SetIsFirstTimeSetup(false) | |
| safeSaveConfig() |
Introduces an interactive CLI dashboard to enhance usability. also refactors password rotation to generate a random password and remove unnecessary configurations. Improve logging and initialization flow, while adding expert settings options in the web UI. Bump version to 5.12.1.