Files
bzzz/install/BZZZ_WEB_UI_PLAN.md
anthonyrawlins c177363a19 Save current BZZZ config-ui state before CHORUS branding update
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 00:19:00 +10:00

16 KiB

BZZZ Built-in Web Configuration UI Development Plan

Clear Separation of Responsibilities

chorus.services (Installer Host)

  • Already Complete: Hosts install-chorus-enhanced.sh
  • Single Purpose: One-line installation script download
  • Working: curl -fsSL https://chorus.services/install.sh | sh

BZZZ (This Plan)

  • 🔄 To Implement: Built-in web UI for cluster configuration
  • 🎯 Purpose: Post-installation setup wizard when no config exists
  • 🌐 Location: Served by BZZZ itself at :8080/setup

Architecture Overview

The BZZZ binary will serve its own web UI for configuration when:

  1. No configuration file exists (first run)
  2. User explicitly accesses :8080/setup
  3. Configuration is incomplete or invalid

Implementation Strategy

Phase 1: Integrate Web UI into BZZZ Binary

1.1 Embed Web UI in Go Binary

File: main.go and new pkg/webui/

//go:embed web-ui/dist/*
var webUIFiles embed.FS

func main() {
    // Check if configuration exists
    if !configExists() {
        log.Println("🌐 No configuration found, starting setup wizard...")
        startWebUI()
    }
    
    // Normal BZZZ startup
    startBZZZServices()
}

func startWebUI() {
    // Serve embedded web UI
    // Start configuration wizard
    // Block until configuration is complete
}

1.2 Extend Existing HTTP Server

File: api/http_server.go

Add setup routes to existing server:

func (h *HTTPServer) setupRoutes(router *mux.Router) {
    // Existing API routes
    api := router.PathPrefix("/api").Subrouter()
    // ... existing routes ...
    
    // Setup wizard routes (only if no config exists)
    if !h.hasValidConfig() {
        h.setupWizardRoutes(router)
    }
}

func (h *HTTPServer) setupWizardRoutes(router *mux.Router) {
    // Serve static web UI files
    router.PathPrefix("/setup").Handler(http.StripPrefix("/setup", 
        http.FileServer(http.FS(webUIFiles))))
    
    // Setup API endpoints
    setup := router.PathPrefix("/api/setup").Subrouter()
    setup.HandleFunc("/system", h.handleSystemDetection).Methods("GET")
    setup.HandleFunc("/repository", h.handleRepositoryConfig).Methods("GET", "POST")
    setup.HandleFunc("/cluster", h.handleClusterConfig).Methods("GET", "POST")
    setup.HandleFunc("/validate", h.handleValidateConfig).Methods("POST")
    setup.HandleFunc("/save", h.handleSaveConfig).Methods("POST")
}

Phase 2: Configuration Management Integration

2.1 Leverage Existing Config System

File: pkg/config/setup.go (new)

type SetupManager struct {
    configPath     string
    systemInfo     *SystemInfo
    repoFactory    *repository.DefaultProviderFactory
    taskCoordinator *coordinator.TaskCoordinator
}

func (s *SetupManager) DetectSystem() (*SystemInfo, error) {
    // Hardware detection (GPU, CPU, memory)
    // Network interface discovery
    // Software prerequisites check
    return systemInfo, nil
}

func (s *SetupManager) ValidateRepositoryConfig(config RepositoryConfig) error {
    // Use existing repository factory
    provider, err := s.repoFactory.CreateProvider(ctx, &repository.Config{
        Provider:    config.Provider,
        BaseURL:     config.BaseURL,
        AccessToken: config.Token,
        // ...
    })
    
    // Test connectivity
    return provider.ListAvailableTasks()
}

func (s *SetupManager) SaveConfiguration(config *SetupConfig) error {
    // Convert to existing config.Config format
    bzzzConfig := &config.Config{
        Node:       config.NodeConfig,
        Repository: config.RepositoryConfig,
        Agent:      config.AgentConfig,
        // ...
    }
    
    // Save to YAML file
    return bzzzConfig.SaveToFile(s.configPath)
}

2.2 Repository Integration

File: api/setup_repository.go (new)

func (h *HTTPServer) handleRepositoryConfig(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
        // Return current repository configuration
        config := h.setupManager.GetRepositoryConfig()
        json.NewEncoder(w).Encode(config)
        return
    }
    
    // POST: Validate and save repository configuration
    var repoConfig RepositoryConfig
    if err := json.NewDecoder(r.Body).Decode(&repoConfig); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    // Validate using existing repository factory
    if err := h.setupManager.ValidateRepositoryConfig(repoConfig); err != nil {
        http.Error(w, fmt.Sprintf("Repository validation failed: %v", err), http.StatusBadRequest)
        return
    }
    
    // Save configuration
    h.setupManager.SaveRepositoryConfig(repoConfig)
    json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}

Phase 3: Enhanced Web UI Components

3.1 Complete Existing Components

The config-ui framework exists but needs implementation:

SystemDetection.tsx:

const SystemDetection = ({ onComplete, onBack }) => {
    const [systemInfo, setSystemInfo] = useState(null);
    const [loading, setLoading] = useState(true);
    
    useEffect(() => {
        fetch('/api/setup/system')
            .then(res => res.json())
            .then(data => {
                setSystemInfo(data);
                setLoading(false);
            });
    }, []);
    
    return (
        <div>
            <h3>System Detection Results</h3>
            {systemInfo && (
                <div>
                    <div>OS: {systemInfo.os}</div>
                    <div>Architecture: {systemInfo.arch}</div>
                    <div>GPUs: {systemInfo.gpus?.length || 0}</div>
                    <div>Memory: {systemInfo.memory}</div>
                    {systemInfo.gpus?.length > 1 && (
                        <div className="alert alert-info">
                            💡 Multi-GPU detected! Consider Parallama for optimal performance.
                        </div>
                    )}
                </div>
            )}
            <button onClick={() => onComplete(systemInfo)}>Continue</button>
        </div>
    );
};

RepositoryConfiguration.tsx:

const RepositoryConfiguration = ({ onComplete, onBack, configData }) => {
    const [provider, setProvider] = useState('gitea');
    const [config, setConfig] = useState({
        provider: 'gitea',
        baseURL: 'http://ironwood:3000',
        owner: '',
        repository: '',
        token: ''
    });
    const [validating, setValidating] = useState(false);
    
    const validateConfig = async () => {
        setValidating(true);
        try {
            const response = await fetch('/api/setup/repository', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(config)
            });
            
            if (response.ok) {
                onComplete(config);
            } else {
                const error = await response.text();
                alert(`Validation failed: ${error}`);
            }
        } catch (error) {
            alert(`Connection failed: ${error.message}`);
        }
        setValidating(false);
    };
    
    return (
        <div>
            <h3>Repository Configuration</h3>
            <form onSubmit={(e) => { e.preventDefault(); validateConfig(); }}>
                <div>
                    <label>Provider:</label>
                    <select value={provider} onChange={(e) => setProvider(e.target.value)}>
                        <option value="gitea">GITEA (Self-hosted)</option>
                        <option value="github">GitHub</option>
                    </select>
                </div>
                
                {provider === 'gitea' && (
                    <div>
                        <label>GITEA URL:</label>
                        <input 
                            value={config.baseURL} 
                            onChange={(e) => setConfig({...config, baseURL: e.target.value})}
                            placeholder="http://ironwood:3000"
                        />
                    </div>
                )}
                
                <div>
                    <label>Repository Owner:</label>
                    <input 
                        value={config.owner} 
                        onChange={(e) => setConfig({...config, owner: e.target.value})}
                        placeholder="username or organization"
                    />
                </div>
                
                <div>
                    <label>Repository Name:</label>
                    <input 
                        value={config.repository} 
                        onChange={(e) => setConfig({...config, repository: e.target.value})}
                        placeholder="repository name"
                    />
                </div>
                
                <div>
                    <label>Access Token:</label>
                    <input 
                        type="password"
                        value={config.token} 
                        onChange={(e) => setConfig({...config, token: e.target.value})}
                        placeholder="access token"
                    />
                </div>
                
                <div>
                    <button type="button" onClick={onBack}>Back</button>
                    <button type="submit" disabled={validating}>
                        {validating ? 'Validating...' : 'Validate & Continue'}
                    </button>
                </div>
            </form>
        </div>
    );
};

Phase 4: Build Process Integration

4.1 Web UI Build Integration

File: Makefile or build script

build-web-ui:
	cd install/config-ui && npm install && npm run build
	mkdir -p web-ui/dist
	cp -r install/config-ui/.next/static web-ui/dist/
	cp -r install/config-ui/out/* web-ui/dist/ 2>/dev/null || true

build-bzzz: build-web-ui
	go build -o bzzz main.go

4.2 Embed Files in Binary

File: pkg/webui/embed.go

package webui

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed dist/*
var webUIFiles embed.FS

func GetWebUIHandler() http.Handler {
    // Get the web-ui subdirectory
    webUIFS, err := fs.Sub(webUIFiles, "dist")
    if err != nil {
        panic(err)
    }
    
    return http.FileServer(http.FS(webUIFS))
}

Phase 5: Configuration Flow Integration

5.1 Startup Logic

File: main.go

func main() {
    configPath := getConfigPath()
    
    // Check if configuration exists and is valid
    config, err := loadConfig(configPath)
    if err != nil || !isValidConfig(config) {
        fmt.Println("🌐 Starting BZZZ setup wizard...")
        fmt.Printf("📱 Open your browser to: http://localhost:8080/setup\n")
        
        // Start HTTP server in setup mode
        startSetupMode(configPath)
        
        // Wait for configuration to be saved
        waitForConfiguration(configPath)
        
        // Reload configuration
        config, err = loadConfig(configPath)
        if err != nil {
            log.Fatal("Failed to load configuration after setup:", err)
        }
    }
    
    // Normal BZZZ startup with configuration
    fmt.Println("🚀 Starting BZZZ with configuration...")
    startNormalMode(config)
}

func startSetupMode(configPath string) {
    // Start HTTP server in setup mode
    setupManager := NewSetupManager(configPath)
    httpServer := api.NewHTTPServer(8080, nil, nil)
    httpServer.SetSetupManager(setupManager)
    
    go func() {
        if err := httpServer.Start(); err != nil {
            log.Fatal("Failed to start setup server:", err)
        }
    }()
}

func waitForConfiguration(configPath string) {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            if fileExists(configPath) {
                if config, err := loadConfig(configPath); err == nil && isValidConfig(config) {
                    fmt.Println("✅ Configuration saved successfully!")
                    return
                }
            }
        case <-time.After(30 * time.Minute):
            log.Fatal("Setup timeout - no configuration provided within 30 minutes")
        }
    }
}

5.2 Configuration Validation

File: pkg/config/validation.go

func isValidConfig(config *Config) bool {
    // Check required fields
    if config.Node.ID == "" || config.Node.Role == "" {
        return false
    }
    
    // Check repository configuration
    if config.Repository.Provider == "" || config.Repository.Config.Owner == "" {
        return false
    }
    
    // Check agent configuration
    if config.Agent.ID == "" || len(config.Agent.Expertise) == 0 {
        return false
    }
    
    return true
}

Integration with Existing Systems

Leverage Without Changes

  1. HTTP Server (api/http_server.go) - Extend with setup routes
  2. Configuration System (pkg/config/) - Use existing YAML structure
  3. Repository Factory (repository/factory.go) - Validate credentials
  4. Task Coordinator (coordinator/task_coordinator.go) - Agent management

🔧 Minimal Extensions

  1. Setup Manager - New component using existing systems
  2. Web UI Routes - Add to existing HTTP server
  3. Embedded Files - Add web UI to binary
  4. Startup Logic - Add configuration check to main.go

File Structure

/home/tony/chorus/project-queues/active/BZZZ/
├── main.go                              # Enhanced startup logic
├── api/
│   ├── http_server.go                   # Extended with setup routes
│   ├── setup_handlers.go                # New setup API handlers
│   └── setup_repository.go              # Repository configuration API
├── pkg/
│   ├── config/
│   │   ├── config.go                    # Existing
│   │   ├── setup.go                     # New setup manager
│   │   └── validation.go                # New validation logic
│   └── webui/
│       └── embed.go                     # New embedded web UI
├── install/config-ui/                   # Existing React app
│   ├── app/setup/page.tsx               # Existing wizard framework
│   ├── app/setup/components/            # Complete component implementations
│   └── package.json                     # Existing
└── web-ui/                              # Build output (embedded)
    └── dist/                            # Generated by build process

Implementation Timeline

Week 1: Core Integration

  • Day 1: Setup manager and configuration validation
  • Day 2: Extend HTTP server with setup routes
  • Day 3: Repository configuration API integration
  • Day 4: System detection and hardware profiling
  • Day 5: Build process and file embedding

Week 2: Web UI Implementation

  • Day 1-2: Complete SystemDetection and RepositoryConfiguration components
  • Day 3-4: Cluster formation and validation components
  • Day 5: UI polish and error handling

Week 3: Integration and Testing

  • Day 1-2: End-to-end configuration flow testing
  • Day 3-4: Multi-node cluster testing
  • Day 5: Documentation and deployment

Success Criteria

Technical

  • BZZZ binary serves web UI when no config exists
  • Complete 8-step setup wizard functional
  • Repository integration with GITEA/GitHub validation
  • Configuration saves to existing YAML format
  • Seamless transition from setup to normal operation

User Experience

  • Zero-config startup launches web wizard
  • Intuitive setup flow with validation
  • Professional UI matching BZZZ branding
  • Clear error messages and recovery options
  • Mobile-responsive design

This plan keeps the clear separation between the chorus.services installer (already working) and BZZZ's built-in configuration system (to be implemented).