🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
504 lines
16 KiB
Markdown
504 lines
16 KiB
Markdown
# 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
|
|
//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:
|
|
```go
|
|
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)
|
|
|
|
```go
|
|
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)
|
|
|
|
```go
|
|
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**:
|
|
```typescript
|
|
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**:
|
|
```typescript
|
|
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
|
|
|
|
```bash
|
|
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`
|
|
|
|
```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`
|
|
|
|
```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`
|
|
|
|
```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). |