Skip to content

Conversation

@deviantony
Copy link
Owner

No description provided.

…hboard

Implemented comprehensive interactive build features to enhance user experience:

- Created full-featured TUI dashboard using Bubbletea for real-time build monitoring
- Added progress bar support for tracking build steps across parallel services
- Included simple progress bar implementation example for quick adoption
- Provided integration examples showing how to enable interactive mode
- Added comprehensive implementation guide with testing checklist

The implementation leverages existing streaming infrastructure and Charmbracelet
libraries (bubbletea, bubbles, lipgloss) already in the codebase. Users can choose
between simple inline progress bars or a full interactive dashboard based on their
needs. All features gracefully degrade to non-interactive mode for CI/CD pipelines.

Files added:
- internal/build/dashboard.go: Full interactive TUI dashboard
- internal/build/dashboard_integration_example.go: Integration examples
- example_simple_progress.go: Minimal progress bar implementation
- INTERACTIVE_BUILDS_GUIDE.md: Complete implementation guide
Updated the build dashboard to be completely passive - it displays information
in real-time without requiring any user interaction (except q to quit).

Key changes:
- Removed keyboard navigation (up/down to select services)
- Display all services at once with progress bars
- Show last 3 log lines under each actively building service
- Auto-updates as builds progress - no interaction needed
- Color-coded status indicators (queued/building/complete/failed)
- Real-time duration tracking per service
- Cleaner, simpler UI focused on information display

The dashboard now matches the requested design:
- Progress bars with percentage for each service
- Auto-scrolling logs shown inline under building services
- Consistent service order throughout the build
- Graceful fallback to regular logs in non-interactive mode

Updated integration examples and documentation to reflect the passive nature
of the dashboard and simplified the implementation guide.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds interactive feedback capabilities for remote Docker builds by introducing a passive TUI dashboard that monitors parallel build progress in real-time. The implementation leverages the existing Charmbracelet bubbletea/bubbles libraries to display build status, progress bars, and live logs for multiple services without requiring user interaction.

  • Implements a new BuildDashboard TUI component with automatic status updates and progress tracking
  • Adds DashboardBuildLogger that integrates with the existing BuildLogger interface
  • Provides comprehensive documentation and integration examples for adopting the dashboard in build commands

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.

File Description
internal/build/dashboard.go Core implementation of passive TUI dashboard with build status tracking, progress bars, and real-time log display
internal/build/dashboard_integration_example.go Integration examples showing how to use the dashboard with BuildOrchestrator in deploy commands
example_simple_progress.go Alternative simpler approach using inline progress bars without full TUI
INTERACTIVE_BUILDS_GUIDE.md Comprehensive guide covering implementation options, integration steps, and troubleshooting

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 37 to 43
// Mark all services as queued initially
for _, name := range serviceNames {
dashboardLogger.MarkServiceQueued(name)
}
// Start dashboard TUI (runs in background)
dashboard.Start()
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Race condition: dashboardLogger.MarkServiceQueued() is called before dashboard.Start(), but this will call UpdateService() which attempts to send messages to bd.program. Since bd.program is only initialized in Start() at line 43, these early calls to MarkServiceQueued will silently fail (the nil check in UpdateService prevents a panic but the updates are lost). Move the queued marking calls to after dashboard.Start(), or add a small delay to ensure the dashboard is initialized.

Suggested change
// Mark all services as queued initially
for _, name := range serviceNames {
dashboardLogger.MarkServiceQueued(name)
}
// Start dashboard TUI (runs in background)
dashboard.Start()
// Start dashboard TUI (runs in background)
dashboard.Start()
// Mark all services as queued initially (after dashboard is started)
for _, name := range serviceNames {
dashboardLogger.MarkServiceQueued(name)
}

Copilot uses AI. Check for mistakes.
Comment on lines 327 to 330
var m map[string]interface{}
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
return strings.TrimSpace(err[1])
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The variable name m shadows the map type used in the function. The regex FindStringSubmatch returns a slice of strings (matches), not an error. This code will never work as intended - the condition err != nil will always be false since err is actually a slice.

This should be:

if matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
    .FindStringSubmatch(line); matches != nil && len(matches) > 1 {
    return strings.TrimSpace(matches[1])
}
Suggested change
var m map[string]interface{}
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
return strings.TrimSpace(err[1])
if matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); matches != nil && len(matches) > 1 {
return strings.TrimSpace(matches[1])

Copilot uses AI. Check for mistakes.
Comment on lines 328 to 329
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Creating a new regex compilation on every call to cleanLogLine is inefficient. Consider compiling this regex once as a package-level variable or field in the struct that uses it, similar to how stepRegex is compiled once in NewDashboardBuildLogger.

Copilot uses AI. Check for mistakes.
Comment on lines 69 to 72
// 1. Add SetLogger method to BuildOrchestrator:
func (bo *BuildOrchestrator) SetLogger(logger BuildLogger) {
bo.logger = logger
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The example SetLogger method allows changing the logger on a BuildOrchestrator after initialization, but this could lead to race conditions if builds are already in progress. The logger field in BuildOrchestrator is accessed without synchronization (based on the orchestrator interface). Consider documenting that this should only be called before any build operations start, or implement proper synchronization if runtime logger changes are needed.

Suggested change
// 1. Add SetLogger method to BuildOrchestrator:
func (bo *BuildOrchestrator) SetLogger(logger BuildLogger) {
bo.logger = logger
}
// 1. Add SetLogger method to BuildOrchestrator:
// To make SetLogger thread-safe, add a sync.RWMutex field (e.g., loggerMu) to BuildOrchestrator:
// type BuildOrchestrator struct {
// ...
// logger BuildLogger
// loggerMu sync.RWMutex
// ...
// }
//
// Then, update SetLogger to use the mutex:
func (bo *BuildOrchestrator) SetLogger(logger BuildLogger) {
bo.loggerMu.Lock()
defer bo.loggerMu.Unlock()
bo.logger = logger
}
//
// NOTE: All accesses to bo.logger elsewhere (e.g., in buildService) should also use bo.loggerMu.RLock()/RUnlock()
// to ensure thread safety.

Copilot uses AI. Check for mistakes.
if _, exists := l.progressBars[serviceName]; !exists {
l.progressBars[serviceName] = progress.New(progress.WithDefaultGradient())
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Division by zero risk: if total is 0, this will cause a panic. Add a check before calculating the percentage:

if total == 0 {
    return
}
percent := float64(current) / float64(total)
Suggested change
if total == 0 {
return
}

Copilot uses AI. Check for mistakes.
- Ensure `tea.WindowSizeMsg` handler updates viewport dimensions

### Works locally but not in CI
- Add terminal detection: `isatty.IsTerminal(os.Stdout.Fd())`
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The documentation references isatty.IsTerminal(os.Stdout.Fd()) but this package is not used elsewhere in the guide. The correct import based on the code examples is golang.org/x/term with term.IsTerminal(int(os.Stdout.Fd())). Update this to match the actual implementation used in the dashboard integration example.

Suggested change
- Add terminal detection: `isatty.IsTerminal(os.Stdout.Fd())`
- Add terminal detection: `term.IsTerminal(int(os.Stdout.Fd()))` (import `"golang.org/x/term"`)

Copilot uses AI. Check for mistakes.
Comment on lines 123 to 127
go func() {
if _, err := bd.program.Run(); err != nil {
fmt.Printf("Error running dashboard: %v\n", err)
}
}()
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Potential race condition: bd.program is assigned at line 122 and then accessed in a goroutine at line 124, but other methods like UpdateService and Stop also access bd.program without synchronization. If UpdateService or Stop are called immediately after Start() and before the goroutine runs, they might see bd.program as nil or in an inconsistent state. Consider using a mutex to protect access to bd.program or ensuring it's fully initialized before being accessible.

Copilot uses AI. Check for mistakes.
Comment on lines 330 to 331
return strings.TrimSpace(err[1])
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This statement is unreachable.

Copilot uses AI. Check for mistakes.
var m map[string]interface{}
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
return strings.TrimSpace(err[1])
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This statement is unreachable.

Copilot uses AI. Check for mistakes.
…mands

Removed temporary documentation and example files and integrated the passive
TUI dashboard directly into the production code.

Changes:
- Removed INTERACTIVE_BUILDS_GUIDE.md (temporary documentation)
- Removed example_simple_progress.go (temporary example)
- Removed internal/build/dashboard_integration_example.go (temporary example)
- Added golang.org/x/term dependency for terminal detection
- Integrated dashboard into cmd/deploy/deploy.go
- Integrated dashboard into cmd/redeploy/redeploy.go

The dashboard now activates automatically when:
- Running in an interactive terminal (detected via term.IsTerminal)
- Building 2 or more services (single service uses regular logger)

For non-interactive environments (CI/CD, piped output), the system
automatically falls back to the regular styled logger.

The dashboard provides:
- Real-time progress bars for each service
- Auto-scrolling logs (last 3 lines per building service)
- Build duration tracking
- Color-coded status indicators
- Fully passive operation (no interaction required)

Users can quit the dashboard early with 'q' or Ctrl+C if needed.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

internal/build/dashboard.go:327

  • The variable m is declared but never used. This declaration should be removed as it serves no purpose and makes the code confusing, especially given the bug in the subsequent if condition.
		var m map[string]interface{}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if err != nil {
// Stop dashboard before returning error
if dashboard != nil {
time.Sleep(1 * time.Second)
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:

  1. Defining these as named constants at the package level (e.g., dashboardErrorDisplayDuration, dashboardSuccessDisplayDuration)
  2. Making them configurable if user experience feedback suggests different timing would be better

This would improve code readability and make it easier to adjust these values in the future.

Copilot uses AI. Check for mistakes.

// Keep dashboard visible for a moment before stopping
if dashboard != nil {
time.Sleep(2 * time.Second)
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:

  1. Defining these as named constants at the package level (e.g., dashboardErrorDisplayDuration, dashboardSuccessDisplayDuration)
  2. Making them configurable if user experience feedback suggests different timing would be better

This would improve code readability and make it easier to adjust these values in the future.

Copilot uses AI. Check for mistakes.
Comment on lines 223 to 224
for _, svc := range m.services {
switch svc.Status {
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Race condition: Reading svc.Status without holding the service's mutex lock. While m.mu.RLock() is held (line 201), individual service fields should be protected by their own mutex (svc.mu) when accessed. The code correctly locks svc.mu later at line 247, but this early access at line 224 is unprotected and could race with updates in the Update() method (line 166).

Consider either:

  1. Locking each service mutex before reading Status in this loop
  2. Moving the status counting logic to after the individual service locks in the main loop

Copilot uses AI. Check for mistakes.

// Keep dashboard visible for a moment before stopping
if dashboard != nil {
time.Sleep(2 * time.Second)
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:

  1. Defining these as named constants at the package level (e.g., dashboardErrorDisplayDuration, dashboardSuccessDisplayDuration)
  2. Making them configurable if user experience feedback suggests different timing would be better

This would improve code readability and make it easier to adjust these values in the future.

Copilot uses AI. Check for mistakes.
if err != nil {
// Stop dashboard before returning error
if dashboard != nil {
time.Sleep(1 * time.Second)
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

[nitpick] The hardcoded sleep durations (1 second on error, 2 seconds on success) are magic numbers that make the code less maintainable. Consider:

  1. Defining these as named constants at the package level (e.g., dashboardErrorDisplayDuration, dashboardSuccessDisplayDuration)
  2. Making them configurable if user experience feedback suggests different timing would be better

This would improve code readability and make it easier to adjust these values in the future.

Copilot uses AI. Check for mistakes.
Comment on lines 327 to 330
var m map[string]interface{}
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
return strings.TrimSpace(err[1])
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This statement is unreachable.

Suggested change
var m map[string]interface{}
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
return strings.TrimSpace(err[1])
matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`).FindStringSubmatch(line)
if len(matches) > 1 {
return strings.TrimSpace(matches[1])

Copilot uses AI. Check for mistakes.
Comment on lines 328 to 330
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
return strings.TrimSpace(err[1])
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

This statement is unreachable.

Suggested change
if err := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); err != nil && len(err) > 1 {
return strings.TrimSpace(err[1])
if matches := regexp.MustCompile(`"stream"\s*:\s*"([^"]*)"`)
.FindStringSubmatch(line); matches != nil && len(matches) > 1 {
return strings.TrimSpace(matches[1])

Copilot uses AI. Check for mistakes.
Fixed all issues identified in PR review:

1. **Race condition in dashboard initialization**
   - Moved bd.program initialization before goroutine start in Start()
   - Added comment explaining the fix
   - Prevents race between UpdateService calls and program initialization

2. **Fixed incorrect regex logic in cleanLogLine()**
   - Changed from treating FindStringSubmatch result as error to proper slice handling
   - Now correctly checks: matches != nil && len(matches) > 1
   - Returns matches[1] instead of attempting to use slice as error

3. **Fixed race condition on service status reads**
   - Added mutex locking when reading svc.Status in status counting loop
   - Prevents concurrent read/write on Status field

4. **Optimized regex compilation**
   - Moved regex compilation to package level (streamRegex variable)
   - Compiles once at initialization instead of on every cleanLogLine() call
   - Significant performance improvement for log processing

5. **Extracted magic numbers to constants**
   - Defined DashboardErrorDisplayDuration (1s) and DashboardSuccessDisplayDuration (2s)
   - Updated deploy.go and redeploy.go to use these constants
   - Improved code maintainability

6. **Division by zero protection**
   - Already protected: checks svc.TotalSteps > 0 before division
   - No changes needed, verified existing safeguard

7. **Removed unreachable code**
   - Cleaned up unused variable declaration in cleanLogLine()
   - Simplified logic flow

All changes maintain backward compatibility and improve thread safety,
performance, and code maintainability.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 104 to 139
// Create build logger (use dashboard if interactive terminal with multiple services)
var logger build.BuildLogger = build.NewStyledBuildLogger("BUILD")
var dashboard *build.BuildDashboard

if term.IsTerminal(int(os.Stdout.Fd())) && len(servicesWithBuild) > 1 {
// Extract service names for dashboard
serviceNames := make([]string, len(servicesWithBuild))
for i, svc := range servicesWithBuild {
serviceNames[i] = svc.ServiceName
}

// Create and start passive TUI dashboard
dashboard = build.NewBuildDashboard(serviceNames)
logger = build.NewDashboardBuildLogger(dashboard)
dashboard.Start()
}

// Create build orchestrator
logger := build.NewStyledBuildLogger("BUILD")
orchestrator := build.NewBuildOrchestrator(client, buildConfig, cfg.EnvironmentID, cfg.StackName, logger)

// Build services
imageTags, err := orchestrator.BuildServices(servicesWithBuild)
if err != nil {
// Stop dashboard before returning error
if dashboard != nil {
time.Sleep(build.DashboardErrorDisplayDuration)
dashboard.Stop()
}
return fmt.Errorf("build failed: %w", err)
}

// Keep dashboard visible for a moment before stopping
if dashboard != nil {
time.Sleep(build.DashboardSuccessDisplayDuration)
dashboard.Stop()
}
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

The dashboard initialization and lifecycle management logic (lines 104-139) is duplicated between cmd/deploy/deploy.go and cmd/redeploy/redeploy.go. Consider extracting this logic into a shared helper function in the internal/build package to reduce code duplication and improve maintainability. For example:

// In internal/build/dashboard.go
func CreateDashboardLogger(servicesWithBuild []compose.ServiceBuildInfo) (BuildLogger, *BuildDashboard) {
    if term.IsTerminal(int(os.Stdout.Fd())) && len(servicesWithBuild) > 1 {
        serviceNames := make([]string, len(servicesWithBuild))
        for i, svc := range servicesWithBuild {
            serviceNames[i] = svc.ServiceName
        }
        dashboard := NewBuildDashboard(serviceNames)
        logger := NewDashboardBuildLogger(dashboard)
        dashboard.Start()
        return logger, dashboard
    }
    return NewStyledBuildLogger("BUILD"), nil
}

Copilot uses AI. Check for mistakes.
Comment on lines 221 to 232
// Define styles
borderStyle := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("238")).
Padding(0, 1)

headerStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("13"))

dimStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("8"))
Copy link

Copilot AI Nov 23, 2025

Choose a reason for hiding this comment

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

Style objects are being created on every render call (lines 222-232, 269-271, 303-305, 318-320), which is inefficient. Consider defining these styles as fields in the dashboardModel struct and initializing them once in NewBuildDashboard, similar to how it's done in StyledBuildLogger (see internal/build/logger.go lines 14-35). This will improve performance during frequent redraws.

Copilot uses AI. Check for mistakes.
Implemented performance optimizations and code quality improvements
based on code review feedback.

Performance Improvements:
1. **Pre-computed Lipgloss styles**
   - Added style fields to dashboardModel struct
   - Styles now computed once during initialization instead of on every render
   - Eliminates repeated style object creation (borderStyle, headerStyle, dimStyle, etc.)
   - Significant performance gain for high-frequency renders during active builds

Code Quality Improvements:
2. **Extracted dashboard initialization logic**
   - Created SetupBuildLogger() helper function in internal/build/dashboard.go
   - Consolidates duplicate code from deploy.go and redeploy.go
   - Added DashboardSetup struct to encapsulate logger and optional dashboard
   - StopDashboard() method handles cleanup with configurable duration
   - Cleaner, more maintainable command implementations

3. **Removed unused imports**
   - Removed "time" import from deploy.go and redeploy.go
   - No longer needed after refactoring to use DashboardSetup

Benefits:
- Reduced memory allocations during render cycles
- Better code reusability and maintainability
- Cleaner command implementations with less boilerplate
- Easier to extend dashboard functionality in the future

All changes maintain backward compatibility and improve overall code quality.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Foreground(svc.Status.Color()).
Bold(true)

line := fmt.Sprintf("%s %-20s ", statusStyle.Render(svc.Status.String()), svc.Name)
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[nitpick] Service names longer than 20 characters will cause display misalignment. Consider truncating long service names or making the width dynamic based on the longest service name:

// In NewBuildDashboard, calculate max name length:
maxNameLen := 20
for _, name := range services {
    if len(name) > maxNameLen {
        maxNameLen = len(name)
    }
}
// Store maxNameLen in the model and use it in formatting

Copilot uses AI. Check for mistakes.
Comment on lines 203 to 214
func (bd *BuildDashboard) UpdateService(serviceName string, status BuildStatus, currentStep, totalSteps int, logLine string, err error) {
if bd.program != nil {
bd.program.Send(UpdateMsg{
ServiceName: serviceName,
Status: status,
Step: currentStep,
Total: totalSteps,
LogLine: logLine,
Error: err,
})
}
}
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Potential race condition: The UpdateService and Stop methods both access bd.program without synchronization. If Stop() is called concurrently with UpdateService(), there's a race on the nil check and the method call. Consider adding a mutex to protect bd.program access, or use atomic operations.

Example:

type BuildDashboard struct {
    program      *tea.Program
    model        *dashboardModel
    serviceOrder []string
    mu           sync.RWMutex // Add mutex
}

func (bd *BuildDashboard) UpdateService(...) {
    bd.mu.RLock()
    defer bd.mu.RUnlock()
    if bd.program != nil {
        bd.program.Send(...)
    }
}

func (bd *BuildDashboard) Stop() {
    bd.mu.Lock()
    defer bd.mu.Unlock()
    if bd.program != nil {
        bd.program.Quit()
    }
}

Copilot uses AI. Check for mistakes.
if msg.LogLine != "" {
// Keep only last 3 log lines per service
svc.Logs = append(svc.Logs, msg.LogLine)
if len(svc.Logs) > 3 {
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider extracting the magic number 3 to a named constant for better maintainability:

const maxLogLinesPerService = 3

This would make it easier to adjust this limit in the future and improves code clarity.

Copilot uses AI. Check for mistakes.

go func() {
if _, err := bd.program.Run(); err != nil {
fmt.Printf("Error running dashboard: %v\n", err)
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Dashboard errors should be written to stderr instead of stdout using fmt.Fprintf(os.Stderr, ...) to avoid mixing error messages with normal program output. This is especially important in CI/CD pipelines where stdout and stderr are often processed separately.

Copilot uses AI. Check for mistakes.
Addressed final code review feedback to ensure thread safety and
better error handling in the dashboard implementation.

Thread Safety Improvements:
1. **Added mutex protection for bd.program access**
   - Added sync.RWMutex to BuildDashboard struct
   - Protected all bd.program access in Start(), UpdateService(), and Stop()
   - Prevents race conditions between concurrent calls to these methods
   - Used RLock for read operations and Lock for write operations

Error Handling:
2. **Dashboard errors now written to stderr**
   - Changed fmt.Printf to fmt.Fprintf(os.Stderr, ...) in Start()
   - Dashboard errors no longer pollute stdout
   - Better separation of concerns for error reporting
   - Added os import for os.Stderr

Code Quality:
3. **Extracted remaining magic numbers to constants**
   - Added MaxLogLinesPerService constant (was hardcoded 3)
   - Added ServiceNameColumnWidth constant (was hardcoded 20)
   - Improved maintainability and documentation of these values
   - Used fmt dynamic width formatting (%-*s) for service name column

Benefits:
- Eliminates all race conditions in dashboard operations
- Better error reporting (errors to stderr, not stdout)
- More maintainable code with named constants
- Safer concurrent access patterns throughout

All thread safety issues have been resolved and the dashboard is now
production-ready with proper synchronization.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants