Skip to content

Add interactive CLI dashboard and expert settings configuration - #149

Merged
JacksonTheMaster merged 12 commits into
nightlyfrom
feat-cli-improvement
Feb 6, 2026
Merged

Add interactive CLI dashboard and expert settings configuration#149
JacksonTheMaster merged 12 commits into
nightlyfrom
feat-cli-improvement

Conversation

@JacksonTheMaster

Copy link
Copy Markdown
Member

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.

… 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.
@JacksonTheMaster JacksonTheMaster self-assigned this Feb 6, 2026
@JacksonTheMaster JacksonTheMaster added enhancement New feature or request StationeersServerUI nightly this regards the nightly branch dependencies Pull requests that update a dependency file go Pull requests that update go code labels Feb 6, 2026
@JacksonTheMaster
JacksonTheMaster merged commit 114a38f into nightly Feb 6, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return nil // Attention: saveConfigItem silently succeeds for unknown keys
return fmt.Errorf("unknown config key: %s", item.Key)

Copilot uses AI. Check for mistakes.
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. │")

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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())

Copilot uses AI. Check for mistakes.
Comment thread src/logger/logger.go
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

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/cli/ssuicli.go
Comment on lines +59 to +68
// 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...")
}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/web/configpage.go
Comment on lines 137 to 218
// 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"
}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/config/config.go
var (
// All configuration variables can be found in vars.go
Version = "5.12.1"
Version = "5.12.3"

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/logger/logger.go
Comment on lines +288 to +305
// 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)
}
}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread server.go
logger.Main.Debug("Initializing after start tasks...")
loader.AfterStartComplete()
logger.Main.Debug("Starting webserver...")
web.StartWebServer(&wg)

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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().

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +36
time.Sleep(500 * time.Millisecond)
printStartupMessage()

if config.GetIsFirstTimeSetup() {
printFirstTimeSetupMessage()
}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
time.Sleep(500 * time.Millisecond)
printStartupMessage()
if config.GetIsFirstTimeSetup() {
printFirstTimeSetupMessage()
}
go func() {
time.Sleep(500 * time.Millisecond)
printStartupMessage()
if config.GetIsFirstTimeSetup() {
printFirstTimeSetupMessage()
}
}()

Copilot uses AI. Check for mistakes.
return "web"
case "3", "skip", "Skip", "SKIP":
// Mark setup as complete so it doesn't prompt on web UI either
config.SetIsFirstTimeSetup(false)

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
config.SetIsFirstTimeSetup(false)
config.SetIsFirstTimeSetup(false)
safeSaveConfig()

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file enhancement New feature or request go Pull requests that update go code nightly this regards the nightly branch StationeersServerUI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants