Major updates and improvements to BZZZ system

- Updated configuration and deployment files
- Improved system architecture and components
- Enhanced documentation and testing
- Fixed various issues and added new features

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-17 18:06:57 +10:00
parent 4e6140de03
commit f5f96ba505
71 changed files with 664 additions and 3823 deletions

View File

@@ -0,0 +1,504 @@
# 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).

View File

@@ -0,0 +1,251 @@
# Enhanced Installer vs INSTALLATION_SYSTEM Vision Comparison
## Executive Summary
Our enhanced installer (`install-chorus-enhanced.sh`) addresses the **repository integration gap** but falls significantly short of the comprehensive **INSTALLATION_SYSTEM.md** vision. While we delivered immediate functionality, we missed the sophisticated multi-phase setup system with web UI that was originally planned.
## Detailed Feature Comparison
### ✅ **What We Successfully Delivered**
| Feature | Enhanced Installer | Original Vision | Status |
|---------|-------------------|-----------------|--------|
| One-command install | ✅ `curl \| sh` | ✅ `curl \| sh` | **MATCH** |
| System detection | ✅ OS, arch, packages | ✅ Hardware, GPU, network | **PARTIAL** |
| Binary installation | ✅ Architecture-specific | ✅ `/opt/bzzz/` structure | **BASIC** |
| Service management | ✅ SystemD integration | ✅ SystemD + monitoring | **PARTIAL** |
| Configuration generation | ✅ YAML config files | ✅ Web-based wizard | **SIMPLIFIED** |
| Repository integration | ✅ GITEA/GitHub setup | ❌ Not specified | **EXCEEDED** |
### 🔴 **Critical Missing Components**
#### 1. Web-Based Configuration Interface
**Vision**: React-based setup wizard at `:8080/setup`
```
🚀 8-Step Configuration Wizard:
1. System Detection & Validation
2. Network Configuration
3. Security Setup
4. AI Integration
5. Resource Allocation
6. Service Deployment
7. Cluster Formation
8. Testing & Validation
```
**Our Implementation**: Command-line prompts only
- ❌ No web UI
- ❌ No beautiful React interface
- ❌ No progressive setup wizard
- ❌ No real-time validation
#### 2. GPU Detection & Parallama Integration
**Vision**: Intelligent multi-GPU detection
```bash
🚀 Multi-GPU Setup Detected (4 NVIDIA GPUs)
Parallama is RECOMMENDED for optimal multi-GPU performance!
Options:
1. Install Parallama (recommended for GPU setups)
2. Install standard Ollama
3. Skip Ollama installation (configure later)
```
**Our Implementation**: Basic Ollama installation
- ❌ No GPU detection
- ❌ No Parallama recommendation
- ❌ No multi-GPU optimization
- ✅ Basic Ollama model installation
#### 3. Advanced System Detection
**Vision**: Comprehensive hardware analysis
- CPU cores and model detection
- GPU configuration (NVIDIA/AMD)
- Memory and storage analysis
- Network interface detection
**Our Implementation**: Basic OS detection
- ✅ OS and architecture detection
- ✅ Package manager detection
- ❌ No hardware profiling
- ❌ No GPU analysis
#### 4. Security Configuration
**Vision**: Enterprise-grade security setup
- SSH key generation/management
- TLS/SSL certificate configuration
- Authentication method selection (token, OAuth2, LDAP)
- Security policy configuration
**Our Implementation**: Basic token storage
- ✅ Repository token management
- ✅ Secure file permissions
- ❌ No SSH key management
- ❌ No TLS configuration
- ❌ No enterprise authentication
#### 5. Resource Management Interface
**Vision**: Interactive resource allocation
- CPU/Memory allocation sliders
- Storage path configuration
- GPU assignment for Parallama
- Resource monitoring setup
**Our Implementation**: Static configuration
- ✅ Basic resource settings in YAML
- ❌ No interactive allocation
- ❌ No resource monitoring
- ❌ No GPU management
#### 6. Professional Installation Experience
**Vision**: Modern, branded installation
```bash
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔥 BZZZ Distributed AI Coordination Platform
Installer v1.0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[INFO] Detected OS: Ubuntu 22.04
[SUCCESS] System requirements check passed
[SUCCESS] BZZZ binaries installed successfully
```
**Our Implementation**: Basic colored output
- ✅ Colored logging functions
- ✅ ASCII banner
- ❌ No professional progress indicators
- ❌ No detailed installation steps
## Architecture Philosophy Differences
### Original INSTALLATION_SYSTEM Vision
- **Web-First**: React-based configuration interface
- **GPU-Optimized**: Parallama multi-GPU support
- **Enterprise-Ready**: LDAP, TLS, advanced security
- **Production-Grade**: Comprehensive monitoring and validation
- **User-Friendly**: Beautiful UI with real-time feedback
### Our Enhanced Installer Implementation
- **CLI-First**: Command-line configuration
- **Repository-Focused**: Git-based task coordination
- **Functional-Minimal**: Working system with basic features
- **Manual-Friendly**: Interactive prompts and text output
- **Gap-Solving**: Addresses immediate deployment needs
## Feature Gap Analysis
### 🔴 **HIGH PRIORITY MISSING**
1. **Web Configuration UI** (`/setup` interface)
- Impact: No user-friendly setup experience
- Effort: High (full React app)
- Priority: Critical for adoption
2. **GPU Detection & Parallama**
- Impact: Suboptimal multi-GPU performance
- Effort: Medium (hardware detection)
- Priority: High for AI workloads
3. **Advanced System Detection**
- Impact: Manual configuration required
- Effort: Medium (system introspection)
- Priority: Medium for automation
### 🟡 **MEDIUM PRIORITY MISSING**
4. **SSH Key Management**
- Impact: Manual cluster deployment
- Effort: Medium (key generation/distribution)
- Priority: Medium for scaling
5. **Resource Allocation Interface**
- Impact: Static resource assignment
- Effort: High (interactive UI)
- Priority: Medium for optimization
6. **TLS/SSL Configuration**
- Impact: Insecure communications
- Effort: Medium (cert management)
- Priority: Medium for production
### 🟢 **LOW PRIORITY MISSING**
7. **LDAP/Enterprise Auth**
- Impact: Basic authentication only
- Effort: High (enterprise integration)
- Priority: Low for initial deployment
8. **Advanced Monitoring**
- Impact: Basic health checking
- Effort: Medium (monitoring stack)
- Priority: Low for MVP
## What We Actually Built vs Vision
### Our Enhanced Installer Strengths
**Repository Integration**: Complete GITEA/GitHub setup
**Working System**: Functional task coordination immediately
**Simple Deployment**: Single command installation
**Documentation**: Comprehensive setup guide
**Token Management**: Secure credential handling
### Vision's Comprehensive Approach
🔴 **Missing Web UI**: No React-based setup wizard
🔴 **Missing GPU Optimization**: No Parallama integration
🔴 **Missing Enterprise Features**: No LDAP, TLS, advanced security
🔴 **Missing Resource Management**: No interactive allocation
🔴 **Missing Professional UX**: No modern installation experience
## Implementation Recommendations
### Immediate (Address Critical Gaps)
1. **Create Web UI** - Implement basic `/setup` interface
2. **Add GPU Detection** - Basic hardware profiling
3. **Improve Installation UX** - Better progress indicators
### Medium-term (Professional Features)
1. **Parallama Integration** - Multi-GPU optimization
2. **SSH Key Management** - Automated cluster deployment
3. **Resource Allocation** - Interactive configuration
### Long-term (Enterprise Grade)
1. **Advanced Security** - TLS, LDAP, enterprise auth
2. **Monitoring Stack** - Comprehensive system monitoring
3. **Professional UI** - Full React setup wizard
## Strategic Decision Points
### Current State Assessment
-**Functional**: System works for task coordination
-**Deployable**: Can install and run immediately
-**Professional**: Lacks enterprise-grade UX
-**Optimized**: Missing GPU and resource optimization
-**Scalable**: No automated cluster deployment
### Path Forward Options
#### Option A: Enhance Current Approach
- Add web UI to existing installer
- Maintain CLI-first philosophy
- Gradual feature addition
#### Option B: Rebuild to Vision
- Implement full INSTALLATION_SYSTEM design
- Web-first configuration experience
- Complete feature parity
#### Option C: Hybrid Approach
- Keep working CLI installer
- Build parallel web UI system
- Allow both installation methods
## Conclusion
Our enhanced installer successfully **solved the immediate repository integration problem** but represents a **significant simplification** of the original comprehensive vision.
**Current Achievement**: ✅ Working repository-integrated task coordination system
**Original Vision**: 🔴 Professional, GPU-optimized, web-based installation platform
The implementation prioritizes **immediate functionality** over **comprehensive user experience**. While this enables rapid deployment and testing, it means we're missing the professional installation experience that would make BZZZ competitive with enterprise platforms.
**Strategic Recommendation**: Implement the web UI (`/setup` interface) as the next major milestone to bridge the gap between our functional system and the original professional vision.

View File

@@ -0,0 +1,412 @@
# BZZZ Installation & Deployment Plan
## Architecture Overview
BZZZ employs a sophisticated distributed installation strategy that progresses through distinct phases: initial setup, SSH-based cluster deployment, P2P network formation, leader election, and finally DHT-based business configuration storage.
## Key Principles
1. **Security-First Design**: Multi-layered key management with Shamir's Secret Sharing
2. **Distributed Authority**: Clear separation between Admin (human oversight) and Leader (network operations)
3. **P2P Model Distribution**: Bandwidth-efficient model replication across cluster
4. **DHT Business Storage**: Configuration data stored in distributed hash table post-bootstrap
5. **Capability-Based Discovery**: Nodes announce capabilities and auto-organize
## Phase 1: Initial Node Setup & Key Generation
### 1.1 Bootstrap Machine Installation
```bash
curl -fsSL https://chorus.services/install.sh | sh
```
**Actions Performed:**
- System detection and validation
- BZZZ binary installation
- Docker and dependency setup
- Launch configuration web UI at `http://[node-ip]:8080/setup`
### 1.2 Master Key Generation & Display
**Key Generation Process:**
1. **Master Key Pair Generation**
- Generate RSA 4096-bit master key pair
- **CRITICAL**: Display private key ONCE in read-only format
- User must securely store master private key (not stored on system)
- Master public key stored locally for validation
2. **Admin Role Key Generation**
- Generate admin role RSA 4096-bit key pair
- Admin public key stored locally
- **Admin private key split using Shamir's Secret Sharing**
3. **Shamir's Secret Sharing Implementation**
- Split admin private key into N shares (where N = cluster size)
- Require K shares for reconstruction (K = ceiling(N/2) + 1)
- Distribute shares to BZZZ peers once network is established
- Ensures no single node failure compromises admin access
### 1.3 Web UI Security Display
```
┌─────────────────────────────────────────────────────────────────┐
│ 🔐 CRITICAL: Master Private Key - DISPLAY ONCE ONLY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ -----BEGIN RSA PRIVATE KEY----- │
│ [MASTER_PRIVATE_KEY_CONTENT] │
│ -----END RSA PRIVATE KEY----- │
│ │
│ ⚠️ SECURITY NOTICE: │
│ • This key will NEVER be displayed again │
│ • Store in secure password manager immediately │
│ • Required for emergency cluster recovery │
│ • Loss of this key may require complete reinstallation │
│ │
│ [ ] I have securely stored the master private key │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Phase 2: Cluster Node Discovery & SSH Deployment
### 2.1 Manual IP Entry Interface
**Web UI Node Discovery:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 🌐 Cluster Node Discovery │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Enter IP addresses for cluster nodes (one per line): │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 192.168.1.101 │ │
│ │ 192.168.1.102 │ │
│ │ 192.168.1.103 │ │
│ │ 192.168.1.104 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ SSH Configuration: │
│ Username: [admin_user ] Port: [22 ] │
│ Password: [••••••••••••••] or Key: [Browse...] │
│ │
│ [ ] Test SSH Connectivity [Deploy to Cluster] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 SSH-Based Remote Installation
**For Each Target Node:**
1. **SSH Connectivity Validation**
- Test SSH access with provided credentials
- Validate sudo privileges
- Check system compatibility
2. **Remote BZZZ Installation**
```bash
# Executed via SSH on each target node
ssh admin_user@192.168.1.101 "curl -fsSL https://chorus.services/install.sh | BZZZ_ROLE=worker sh"
```
3. **Configuration Transfer**
- Copy master public key to node
- Install BZZZ binaries and dependencies
- Configure systemd services
- Set initial network parameters (bootstrap node address)
4. **Service Initialization**
- Start BZZZ service in cluster-join mode
- Configure P2P network parameters
- Set announce channel subscription
## Phase 3: P2P Network Formation & Capability Discovery
### 3.1 P2P Network Bootstrap
**Network Formation Process:**
1. **Bootstrap Node Configuration**
- First installed node becomes bootstrap node
- Listens for P2P connections on configured port
- Maintains peer discovery registry
2. **Peer Discovery via Announce Channel**
```yaml
announce_message:
node_id: "node-192168001101-20250810"
capabilities:
- gpu_count: 4
- gpu_type: "nvidia"
- gpu_memory: [24576, 24576, 24576, 24576] # MB per GPU
- cpu_cores: 32
- memory_gb: 128
- storage_gb: 2048
- ollama_type: "parallama"
network_info:
ip_address: "192.168.1.101"
p2p_port: 8081
services:
- bzzz_go: 8080
- mcp_server: 3000
joined_at: "2025-08-10T16:22:20Z"
```
3. **Capability-Based Network Organization**
- Nodes self-organize based on announced capabilities
- GPU-enabled nodes form AI processing pools
- Storage nodes identified for DHT participation
- Network topology dynamically optimized
### 3.2 Shamir Share Distribution
**Once P2P Network Established:**
1. Generate N shares of admin private key (N = peer count)
2. Distribute one share to each peer via encrypted P2P channel
3. Each peer stores share encrypted with their node-specific key
4. Verify share distribution and reconstruction capability
## Phase 4: Leader Election & SLURP Responsibilities
### 4.1 Leader Election Algorithm
**Election Criteria (Weighted Scoring):**
- **Network Stability**: Uptime and connection quality (30%)
- **Hardware Resources**: CPU, Memory, Storage capacity (25%)
- **Network Position**: Connectivity to other peers (20%)
- **Geographic Distribution**: Network latency optimization (15%)
- **Load Capacity**: Current resource utilization (10%)
**Election Process:**
1. Each node calculates its fitness score
2. Nodes broadcast their scores and capabilities
3. Consensus algorithm determines leader (highest score + network agreement)
4. Leader election occurs every 24 hours or on leader failure
5. **Leader ≠ Admin**: Leader handles operations, Admin handles oversight
### 4.2 SLURP Responsibilities (Leader Node)
**SLURP = Service Layer Unified Resource Protocol**
**Leader Responsibilities:**
- **Resource Orchestration**: Task distribution across cluster
- **Model Distribution**: Coordinate ollama model replication
- **Load Balancing**: Distribute AI workloads optimally
- **Network Health**: Monitor peer connectivity and performance
- **DHT Coordination**: Manage distributed storage operations
**Leader Election Display:**
```
🏆 Network Leader Election Results
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Current Leader: node-192168001103-20250810
├─ Hardware Score: 95/100 (4x RTX 4090, 128GB RAM)
├─ Network Score: 89/100 (Central position, low latency)
├─ Stability Score: 96/100 (99.8% uptime)
└─ Overall Score: 93.2/100
Network Topology:
├─ Total Nodes: 5
├─ GPU Nodes: 4 (Parallama enabled)
├─ Storage Nodes: 5 (DHT participants)
├─ Available VRAM: 384GB total
└─ Network Latency: avg 2.3ms
Next Election: 2025-08-11 16:22:20 UTC
```
## Phase 5: Business Configuration & DHT Storage
### 5.1 DHT Bootstrap & Business Data Storage
**Only After Leader Election:**
- DHT network becomes available for business data storage
- Configuration data migrated from local storage to DHT
- Business decisions stored using UCXL addresses
**UCXL Address Format:**
```
ucxl://bzzz.cluster.config/network_topology
ucxl://bzzz.cluster.config/resource_allocation
ucxl://bzzz.cluster.config/ai_models
ucxl://bzzz.cluster.config/user_projects
```
### 5.2 Business Configuration Categories
**Stored in DHT (Post-Bootstrap):**
- Network topology and node roles
- Resource allocation policies
- AI model distribution strategies
- User project configurations
- Cost management settings
- Monitoring and alerting rules
**Kept Locally (Security/Bootstrap):**
- Admin user's public key
- Master public key for validation
- Initial IP candidate list
- Domain/DNS configuration
- Bootstrap node addresses
## Phase 6: Model Distribution & Synchronization
### 6.1 P2P Model Distribution Strategy
**Model Distribution Logic:**
```python
def distribute_model(model_info):
model_size = model_info.size_gb
model_vram_req = model_info.vram_requirement_gb
# Find eligible nodes
eligible_nodes = []
for node in cluster_nodes:
if node.available_vram_gb >= model_vram_req:
eligible_nodes.append(node)
# Distribute to all eligible nodes
for node in eligible_nodes:
if not node.has_model(model_info.id):
leader.schedule_model_transfer(
source=primary_model_node,
target=node,
model=model_info
)
```
**Distribution Priorities:**
1. **GPU Memory Threshold**: Model must fit in available VRAM
2. **Redundancy**: Minimum 3 copies across different nodes
3. **Geographic Distribution**: Spread across network topology
4. **Load Balancing**: Distribute based on current node utilization
### 6.2 Model Version Synchronization (TODO)
**Current Status**: Implementation pending
**Requirements:**
- Track model versions across all nodes
- Coordinate updates when new model versions released
- Handle rollback scenarios for failed updates
- Maintain consistency during network partitions
**TODO Items to Address:**
- [ ] Design version tracking mechanism
- [ ] Implement distributed consensus for updates
- [ ] Create rollback/recovery procedures
- [ ] Handle split-brain scenarios during updates
## Phase 7: Role-Based Key Generation
### 7.1 Dynamic Role Key Creation
**Using Admin Private Key (Post-Bootstrap):**
1. **User Defines Custom Roles** via web UI:
```yaml
roles:
- name: "data_scientist"
permissions: ["model_access", "job_submit", "resource_view"]
- name: "ml_engineer"
permissions: ["model_deploy", "cluster_config", "monitoring"]
- name: "project_manager"
permissions: ["user_management", "cost_monitoring", "reporting"]
```
2. **Admin Key Reconstruction**:
- Collect K shares from network peers
- Reconstruct admin private key temporarily in memory
- Generate role-specific key pairs
- Sign role public keys with admin private key
- Clear admin private key from memory
3. **Role Key Distribution**:
- Store role key pairs in DHT with UCXL addresses
- Distribute to authorized users via secure channels
- Revocation handled through DHT updates
## Installation Flow Summary
```
Phase 1: Bootstrap Setup
├─ curl install.sh → Web UI → Master Key Display (ONCE)
├─ Generate admin keys → Shamir split preparation
└─ Manual IP entry for cluster nodes
Phase 2: SSH Cluster Deployment
├─ SSH connectivity validation
├─ Remote BZZZ installation on all nodes
└─ Service startup with P2P parameters
Phase 3: P2P Network Formation
├─ Capability announcement via announce channel
├─ Peer discovery and network topology
└─ Shamir share distribution
Phase 4: Leader Election
├─ Fitness score calculation and consensus
├─ Leader takes SLURP responsibilities
└─ Network operational status achieved
Phase 5: DHT & Business Storage
├─ DHT network becomes available
├─ Business configuration migrated to UCXL addresses
└─ Local storage limited to security essentials
Phase 6: Model Distribution
├─ P2P model replication based on VRAM capacity
├─ Version synchronization (TODO)
└─ Load balancing and redundancy
Phase 7: Role Management
├─ Dynamic role definition via web UI
├─ Admin key reconstruction for signing
└─ Role-based access control deployment
```
## Security Considerations
### Data Storage Security
- **Sensitive Data**: Never stored in DHT (keys, passwords)
- **Business Data**: Encrypted before DHT storage
- **Network Communication**: All P2P traffic encrypted
- **Key Recovery**: Master key required for emergency access
### Network Security
- **mTLS**: All inter-node communication secured
- **Certificate Rotation**: Automated cert renewal
- **Access Control**: Role-based permissions enforced
- **Audit Logging**: All privileged operations logged
## Monitoring & Observability
### Network Health Metrics
- P2P connection quality and latency
- DHT data consistency and replication
- Model distribution status and synchronization
- Leader election frequency and stability
### Business Metrics
- Resource utilization across cluster
- Cost tracking and budget adherence
- AI workload distribution and performance
- User activity and access patterns
## Failure Recovery Procedures
### Leader Failure
1. Automatic re-election triggered
2. New leader assumes SLURP responsibilities
3. DHT operations continue uninterrupted
4. Model distribution resumes under new leader
### Network Partition
1. Majority partition continues operations
2. Minority partitions enter read-only mode
3. Automatic healing when connectivity restored
4. Conflict resolution via timestamp ordering
### Admin Key Recovery
1. Master private key required for recovery
2. Generate new admin key pair if needed
3. Re-split and redistribute Shamir shares
4. Update role signatures with new admin key
This plan provides a comprehensive, security-focused approach to BZZZ cluster deployment with clear separation of concerns and robust failure recovery mechanisms.

View File

@@ -0,0 +1,326 @@
# BZZZ Installation System
A comprehensive one-command installation system for BZZZ distributed AI coordination platform, similar to Ollama's approach.
## Overview
The BZZZ installation system provides:
- **One-command installation**: `curl -fsSL https://chorus.services/install.sh | sh`
- **Automated system detection**: Hardware, OS, and network configuration
- **GPU-aware setup**: Detects NVIDIA/AMD GPUs and recommends Parallama for multi-GPU systems
- **Web-based configuration**: Beautiful React-based setup wizard
- **Production-ready deployment**: Systemd services, monitoring, and security
## Installation Architecture
### Phase 1: System Detection & Installation
1. **System Requirements Check**
- OS compatibility (Ubuntu, Debian, CentOS, RHEL, Fedora)
- Architecture support (amd64, arm64, armv7)
- Minimum resources (2GB RAM, 10GB disk)
2. **Hardware Detection**
- CPU cores and model
- Available memory
- Storage capacity
- GPU configuration (NVIDIA/AMD)
- Network interfaces
3. **Dependency Installation**
- Docker and Docker Compose
- System utilities (curl, wget, jq, etc.)
- GPU drivers (if applicable)
4. **AI Model Platform Choice**
- **Parallama (Recommended for Multi-GPU)**: Our multi-GPU fork of Ollama
- **Standard Ollama**: Traditional single-GPU Ollama
- **Skip**: Configure later via web UI
### Phase 2: BZZZ Installation
1. **Binary Installation**
- Download architecture-specific binaries
- Install to `/opt/bzzz/`
- Create symlinks in `/usr/local/bin/`
2. **System Setup**
- Create `bzzz` system user
- Setup directories (`/etc/bzzz`, `/var/log/bzzz`, `/var/lib/bzzz`)
- Configure permissions
3. **Service Installation**
- Systemd service files for BZZZ Go service and MCP server
- Automatic startup configuration
- Log rotation setup
### Phase 3: Web-Based Configuration
1. **Configuration Server**
- Starts BZZZ service with minimal config
- Launches React-based configuration UI
- Accessible at `http://[node-ip]:8080/setup`
2. **8-Step Configuration Wizard**
- System Detection & Validation
- Network Configuration
- Security Setup
- AI Integration
- Resource Allocation
- Service Deployment
- Cluster Formation
- Testing & Validation
## Required User Information
### 1. Cluster Infrastructure
- **Network Configuration**
- Subnet IP range (auto-detected, user can override)
- Primary network interface selection
- Port assignments (BZZZ: 8080, MCP: 3000, WebUI: 8080)
- Firewall configuration preferences
### 2. Security Settings
- **SSH Key Management**
- Generate new SSH keys
- Upload existing keys
- SSH username and port
- Key distribution to cluster nodes
- **Authentication**
- TLS/SSL certificate setup
- Authentication method (token, OAuth2, LDAP)
- Security policy configuration
### 3. AI Integration
- **OpenAI Configuration**
- API key (secure input with validation)
- Default model selection (GPT-5)
- Cost limits (daily/monthly)
- Usage monitoring preferences
- **Local AI Models**
- Ollama/Parallama endpoint configuration
- Model distribution strategy
- GPU allocation for Parallama
- Automatic model pulling
### 4. Resource Management
- **Hardware Allocation**
- CPU cores allocation
- Memory limits per service
- Storage paths and quotas
- GPU assignment (for Parallama)
- **Service Configuration**
- Container resource limits
- Auto-scaling policies
- Monitoring and alerting
- Backup and recovery
### 5. Cluster Topology
- **Node Roles**
- Coordinator vs Worker designation
- High availability setup
- Load balancing configuration
- Failover preferences
## Installation Flow
### Command Execution
```bash
curl -fsSL https://chorus.services/install.sh | sh
```
### Interactive Prompts
1. **GPU Detection Response**
```
🚀 Multi-GPU Setup Detected (4 NVIDIA GPUs)
Parallama is RECOMMENDED for optimal multi-GPU performance!
Options:
1. Install Parallama (recommended for GPU setups)
2. Install standard Ollama
3. Skip Ollama installation (configure later)
```
2. **Installation Progress**
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔥 BZZZ Distributed AI Coordination Platform
Installer v1.0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[INFO] Detected OS: Ubuntu 22.04
[INFO] Detected architecture: amd64
[SUCCESS] System requirements check passed
[INFO] Detected 4 NVIDIA GPU(s)
[SUCCESS] Dependencies installed successfully
[SUCCESS] Parallama installed successfully
[SUCCESS] BZZZ binaries installed successfully
[SUCCESS] Configuration server started
```
3. **Completion Message**
```
🚀 Next Steps:
1. Complete your cluster configuration:
👉 Open: http://192.168.1.100:8080/setup
2. Useful commands:
• Check status: bzzz status
• View logs: sudo journalctl -u bzzz -f
• Start/Stop: sudo systemctl [start|stop] bzzz
📚 Docs: https://docs.chorus.services/bzzz
💬 Support: https://discord.gg/chorus-services
```
### Web Configuration Flow
#### Step 1: System Detection
- Display detected hardware configuration
- Show GPU setup and capabilities
- Validate software requirements
- System readiness check
#### Step 2: Network Configuration
- Network interface selection
- Subnet configuration
- Port assignment
- Firewall rule setup
- Connectivity testing
#### Step 3: Security Setup
- SSH key generation/upload
- TLS certificate configuration
- Authentication method selection
- Security policy setup
#### Step 4: AI Integration
- OpenAI API key configuration
- Model preferences and costs
- Ollama/Parallama setup
- Local model management
#### Step 5: Resource Allocation
- CPU/Memory allocation sliders
- Storage path configuration
- GPU assignment (Parallama)
- Resource monitoring setup
#### Step 6: Service Deployment
- Service configuration review
- Container deployment
- Health check setup
- Monitoring configuration
#### Step 7: Cluster Formation
- Create new cluster or join existing
- Network discovery
- Node role assignment
- Cluster validation
#### Step 8: Testing & Validation
- Connectivity tests
- AI model verification
- Performance benchmarks
- Configuration validation
## Files Structure
```
/home/tony/chorus/project-queues/active/BZZZ/install/
├── install.sh # Main installation script
├── config-ui/ # React configuration interface
│ ├── package.json # Dependencies and scripts
│ ├── next.config.js # Next.js configuration
│ ├── tailwind.config.js # Tailwind CSS config
│ ├── tsconfig.json # TypeScript config
│ ├── postcss.config.js # PostCSS config
│ └── app/ # Next.js app directory
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page (redirects to setup)
│ └── setup/
│ ├── page.tsx # Main setup wizard
│ └── components/ # Setup step components
│ ├── SystemDetection.tsx
│ ├── NetworkConfiguration.tsx
│ ├── SecuritySetup.tsx
│ ├── AIConfiguration.tsx
│ ├── ResourceAllocation.tsx
│ ├── ServiceDeployment.tsx
│ ├── ClusterFormation.tsx
│ └── TestingValidation.tsx
├── requirements.md # Detailed requirements
└── INSTALLATION_SYSTEM.md # This document
```
## Key Features
### 1. Intelligent GPU Detection
- Automatic detection of NVIDIA/AMD GPUs
- Multi-GPU topology analysis
- Recommends Parallama for multi-GPU setups
- Fallback to standard Ollama for single GPU
- CPU-only mode support
### 2. Comprehensive System Validation
- Hardware requirements checking
- Software dependency validation
- Network connectivity testing
- Security configuration verification
### 3. Production-Ready Setup
- Systemd service integration
- Proper user/permission management
- Log rotation and monitoring
- Security best practices
- Automatic startup configuration
### 4. Beautiful User Experience
- Modern React-based interface
- Progressive setup wizard
- Real-time validation feedback
- Mobile-responsive design
- Comprehensive help and documentation
### 5. Enterprise Features
- SSH key distribution
- TLS/SSL configuration
- LDAP/AD integration support
- Cost management and monitoring
- Multi-node cluster orchestration
## Next Implementation Steps
1. **Backend API Development**
- Go-based configuration API
- System detection endpoints
- Configuration validation
- Service management
2. **Enhanced Components**
- Complete all setup step components
- Real-time validation
- Progress tracking
- Error handling
3. **Cluster Management**
- Node discovery protocols
- Automated SSH setup
- Service distribution
- Health monitoring
4. **Security Hardening**
- Certificate management
- Secure key distribution
- Network encryption
- Access control
5. **Testing & Validation**
- Integration test suite
- Performance benchmarking
- Security auditing
- User acceptance testing
This installation system provides a seamless, professional-grade setup experience that rivals major infrastructure platforms while specifically optimizing for AI workloads and multi-GPU configurations.

View File

@@ -0,0 +1,245 @@
# BZZZ Installer: Plan vs Implementation Comparison
## Executive Summary
Our enhanced installer (`install-chorus-enhanced.sh`) addresses **Phase 1: Initial Node Setup** but diverges significantly from the original comprehensive deployment plan. The implementation prioritizes immediate functionality over the sophisticated multi-phase security architecture originally envisioned.
## Phase-by-Phase Analysis
### ✅ Phase 1: Initial Node Setup & Key Generation
**Original Plan:**
- Web UI at `:8080/setup` for configuration
- Master key generation with ONCE-ONLY display
- Shamir's Secret Sharing for admin keys
- Security-first design with complex key management
**Our Implementation:**
- ✅ Single-command installation (`curl | sh`)
- ✅ System detection and validation
- ✅ BZZZ binary installation
- ✅ Interactive configuration prompts
-**MAJOR GAP**: No web UI setup interface
-**CRITICAL MISSING**: No master key generation
-**SECURITY GAP**: No Shamir's Secret Sharing
-**SIMPLIFICATION**: Basic YAML config instead of sophisticated key management
**Gap Assessment:** 🔴 **SIGNIFICANT DEVIATION**
The enhanced installer implements a simplified approach focused on repository integration rather than the security-first cryptographic design.
### ❌ Phase 2: SSH Cluster Deployment
**Original Plan:**
- Web UI for manual IP entry
- SSH-based remote installation across cluster
- Automated deployment to multiple nodes
- Cluster coordination through central web interface
**Our Implementation:**
- ✅ Manual installation per node (user runs script on each machine)
- ✅ Repository configuration with credentials
-**MISSING**: No SSH-based remote deployment
-**MISSING**: No centralized cluster management
-**MANUAL PROCESS**: User must run installer on each node individually
**Gap Assessment:** 🔴 **NOT IMPLEMENTED**
Current approach requires manual installation on each node. No automated cluster deployment.
### ❌ Phase 3: P2P Network Formation & Capability Discovery
**Original Plan:**
- Automatic P2P network bootstrap
- Capability announcement (GPU, CPU, memory, storage)
- Dynamic network topology optimization
- Shamir share distribution across peers
**Our Implementation:**
- ✅ P2P network components exist in codebase
- ✅ Node capability configuration in YAML
-**MISSING**: No automatic capability discovery
-**MISSING**: No dynamic network formation
-**MISSING**: No Shamir share distribution
**Gap Assessment:** 🔴 **FOUNDATIONAL MISSING**
P2P capabilities exist but installer doesn't configure automatic network formation.
### ❌ Phase 4: Leader Election & SLURP Responsibilities
**Original Plan:**
- Sophisticated leader election with weighted scoring
- SLURP (Service Layer Unified Resource Protocol) coordination
- Leader handles resource orchestration and model distribution
- Clear separation between Leader (operations) and Admin (oversight)
**Our Implementation:**
- ✅ Election code exists in codebase (`pkg/election/`)
- ✅ SLURP architecture implemented
-**MISSING**: No automatic leader election setup
-**MISSING**: No SLURP coordination configuration
-**CONFIGURATION GAP**: Manual role assignment only
**Gap Assessment:** 🔴 **ADVANCED FEATURES MISSING**
Infrastructure exists but not activated by installer.
### ❌ Phase 5: Business Configuration & DHT Storage
**Original Plan:**
- DHT network for distributed business data storage
- UCXL addressing for configuration data
- Migration from local to distributed storage
- Encrypted business data in DHT
**Our Implementation:**
- ✅ DHT code exists (`pkg/dht/`)
- ✅ UCXL addressing implemented
-**MISSING**: No DHT network activation
-**MISSING**: No business data migration
-**BASIC CONFIG**: Simple local YAML files only
**Gap Assessment:** 🔴 **DISTRIBUTED STORAGE UNUSED**
Sophisticated storage architecture exists but installer uses basic local configs.
### ❌ Phase 6: Model Distribution & Synchronization
**Original Plan:**
- P2P model distribution based on VRAM capacity
- Automatic model replication and redundancy
- Load balancing and geographic distribution
- Version synchronization (marked as TODO in plan)
**Our Implementation:**
- ✅ Ollama integration for model management
- ✅ Model installation via command line flags
-**MISSING**: No P2P model distribution
-**MISSING**: No automatic model replication
-**SIMPLE**: Local Ollama model installation only
**Gap Assessment:** 🔴 **BASIC MODEL MANAGEMENT**
Individual node model installation, no cluster-wide distribution.
### ❌ Phase 7: Role-Based Key Generation
**Original Plan:**
- Dynamic role definition via web UI
- Admin key reconstruction for role signing
- Role-based access control deployment
- Sophisticated permission management
**Our Implementation:**
- ✅ Repository-based role assignment (basic)
- ✅ Agent role configuration in YAML
-**MISSING**: No dynamic role creation
-**MISSING**: No key-based role management
-**BASIC**: Simple string-based role assignment
**Gap Assessment:** 🔴 **ENTERPRISE FEATURES MISSING**
Basic role strings instead of cryptographic role management.
## Implementation Philosophy Divergence
### Original Plan Philosophy
- **Security-First**: Complex cryptographic key management
- **Enterprise-Grade**: Sophisticated multi-phase deployment
- **Centralized Management**: Web UI for cluster coordination
- **Automated Operations**: SSH-based remote deployment
- **Distributed Architecture**: DHT storage, P2P model distribution
### Our Implementation Philosophy
- **Simplicity-First**: Get working system quickly
- **Repository-Centric**: Focus on task coordination via Git
- **Manual-Friendly**: User-driven installation per node
- **Local Configuration**: YAML files instead of distributed storage
- **Immediate Functionality**: Working agent over complex architecture
## Critical Missing Components
### 🔴 HIGH PRIORITY GAPS
1. **Web UI Setup Interface**
- Original: Rich web interface at `:8080/setup`
- Current: Command-line prompts only
- Impact: No user-friendly cluster management
2. **Master Key Generation & Display**
- Original: One-time master key display with security warnings
- Current: No cryptographic key management
- Impact: No secure cluster recovery mechanism
3. **SSH-Based Cluster Deployment**
- Original: Deploy from one node to entire cluster
- Current: Manual installation on each node
- Impact: Scaling difficulty, no centralized deployment
4. **Automatic P2P Network Formation**
- Original: Nodes discover and organize automatically
- Current: Static configuration per node
- Impact: No dynamic cluster topology
### 🟡 MEDIUM PRIORITY GAPS
5. **Shamir's Secret Sharing**
- Original: Distributed admin key management
- Current: No key splitting or distribution
- Impact: Single points of failure
6. **Leader Election Activation**
- Original: Automatic leader selection with weighted scoring
- Current: Manual coordinator assignment
- Impact: No dynamic leadership, manual failover
7. **DHT Business Configuration**
- Original: Distributed configuration storage
- Current: Local YAML files
- Impact: No configuration replication or consistency
8. **P2P Model Distribution**
- Original: Cluster-wide model synchronization
- Current: Individual node model management
- Impact: Manual model management across cluster
## Architectural Trade-offs Made
### ✅ **GAINED: Simplicity & Speed**
- Fast installation (single command)
- Working system in minutes
- Repository integration works immediately
- Clear configuration files
- Easy troubleshooting
### ❌ **LOST: Enterprise Features**
- No centralized cluster management
- No cryptographic security model
- No automatic scaling capabilities
- No distributed configuration
- No sophisticated failure recovery
## Recommendations
### Short-term (Immediate)
1. **Document the gap** - Users need to understand current limitations
2. **Manual cluster setup guide** - Document how to deploy across multiple nodes manually
3. **Basic health checking** - Add cluster connectivity validation
4. **Configuration templates** - Provide coordinator vs worker config examples
### Medium-term (Next Phase)
1. **Web UI Development** - Implement the missing `:8080/setup` interface
2. **SSH Deployment Module** - Add remote installation capabilities
3. **P2P Network Activation** - Enable automatic peer discovery
4. **Basic Key Management** - Implement simplified security model
### Long-term (Strategic)
1. **Full Plan Implementation** - Gradually implement all 7 phases
2. **Security Architecture** - Add Shamir's Secret Sharing and master keys
3. **Enterprise Features** - Leader election, DHT storage, model distribution
4. **Migration Path** - Allow upgrading from simple to sophisticated deployment
## Conclusion
Our enhanced installer successfully delivers **immediate functionality** for repository-based task coordination but represents a **significant simplification** of the original comprehensive plan.
**Current State:** ✅ Single-node ready, repository integrated, immediately useful
**Original Vision:** 🔴 Enterprise-grade, security-first, fully distributed cluster
The implementation prioritizes **time-to-value** over **comprehensive architecture**. While this enables rapid deployment and testing, it means users must manually scale and manage clusters rather than having sophisticated automated deployment and management capabilities.
**Strategic Decision Point:** Continue with simplified approach for rapid iteration, or invest in implementing the full sophisticated architecture as originally planned.

View File

@@ -0,0 +1,355 @@
# BZZZ Web Configuration Setup Integration Guide
This guide explains how to use the new integrated web-based configuration system for BZZZ.
## Overview
BZZZ now includes an embedded web configuration interface that automatically activates when:
- No configuration file exists
- The existing configuration is invalid or incomplete
- Setup is explicitly required
## Features
### 🎯 Automatic Setup Detection
- BZZZ automatically detects when setup is required on startup
- No separate installation or configuration needed
- Seamless transition from setup to normal operation
### 🌐 Embedded Web Interface
- Complete React-based setup wizard embedded in the Go binary
- No external dependencies required
- Works offline and in air-gapped environments
### 🔧 Comprehensive Configuration
- System detection and hardware analysis
- Repository integration (GitHub, GitLab, Gitea)
- Network and security configuration
- AI model and capability setup
- Service deployment options
## Quick Start
### 1. Build BZZZ with Embedded UI
```bash
# Build the complete system with embedded web UI
make build
# Or build just the binary (uses placeholder UI)
make build-go
```
### 2. First-Time Setup
```bash
# Start BZZZ - it will automatically enter setup mode
./build/bzzz
# Or use the transition script for guided setup
./scripts/setup-transition.sh
```
### 3. Access Setup Interface
Open your browser to: **http://localhost:8080**
The setup wizard will guide you through:
1. **System Detection** - Hardware and environment analysis
2. **Agent Configuration** - ID, capabilities, and AI models
3. **Repository Setup** - Git integration configuration
4. **Network Configuration** - P2P and cluster settings
5. **Security Setup** - Encryption and access control
6. **Service Deployment** - Additional services configuration
7. **Testing & Validation** - Configuration verification
### 4. Complete Setup
After saving configuration:
1. BZZZ will create the configuration file
2. Restart BZZZ to enter normal operation mode
3. The web interface will no longer be available
## Development Workflow
### Building the Web UI
```bash
# Install dependencies
make deps
# Build just the web UI
make build-ui
# Embed web UI in Go binary
make embed-ui
# Complete build process
make build
```
### Development Mode
```bash
# Start both React dev server and Go API
make dev
# React UI: http://localhost:3000
# Go API: http://localhost:8080
```
### Project Structure
```
BZZZ/
├── install/config-ui/ # React setup wizard
│ ├── app/ # Next.js application
│ ├── package.json # Node.js dependencies
│ └── next.config.js # Build configuration
├── pkg/web/ # Embedded file system
│ ├── embed.go # File embedding logic
│ └── index.html # Fallback page
├── api/ # Go HTTP server
│ ├── http_server.go # Main server with setup routes
│ └── setup_manager.go # Setup logic
├── Makefile # Build automation
└── scripts/
└── setup-transition.sh # Setup helper script
```
## Configuration Management
### Configuration Files
- **Default Location**: `~/.bzzz/config.yaml`
- **Environment Override**: `BZZZ_CONFIG_PATH`
- **Backup Directory**: `~/.bzzz/backups/`
### Setup Requirements Detection
BZZZ checks for setup requirements using:
1. Configuration file existence
2. Configuration file validity
3. Essential fields completion (Agent ID, capabilities, models)
### Configuration Validation
The setup system validates:
- Required fields presence
- Repository connectivity
- AI model availability
- Network configuration
- Security settings
## API Endpoints
### Setup Mode APIs
When in setup mode, BZZZ exposes these endpoints:
```bash
# Check if setup is required
GET /api/setup/required
# Get system information
GET /api/setup/system
# Validate repository configuration
POST /api/setup/repository/validate
# Get supported repository providers
GET /api/setup/repository/providers
# Validate complete configuration
POST /api/setup/validate
# Save configuration
POST /api/setup/save
# Health check
GET /api/health
```
### Web UI Routes
```bash
# Setup interface (embedded React app)
GET /
GET /setup
GET /setup/*
# API proxy (development only)
/api/* -> http://localhost:8080/api/*
```
## Deployment Scenarios
### 1. Fresh Installation
```bash
# Build and start BZZZ
make build
./build/bzzz
# Access setup at http://localhost:8080
# Complete configuration wizard
# Restart BZZZ for normal operation
```
### 2. Existing Installation
```bash
# Backup existing configuration
./scripts/setup-transition.sh
# BZZZ will use existing config if valid
# Or enter setup mode if invalid
```
### 3. Container Deployment
```dockerfile
# Build container with embedded UI
FROM golang:1.21-alpine AS builder
COPY . /app
WORKDIR /app
RUN make build
FROM alpine:latest
COPY --from=builder /app/build/bzzz /usr/local/bin/
EXPOSE 8080
CMD ["bzzz"]
```
### 4. Cluster Deployment
```bash
# Build BZZZ with embedded UI
make build
# Deploy to each cluster node
scp build/bzzz node1:/usr/local/bin/
ssh node1 'bzzz' # Setup via web interface
# Repeat for additional nodes
# Nodes will discover each other via mDNS
```
## Troubleshooting
### Common Issues
**Web UI Not Loading**
```bash
# Check if web UI was built
make build-ui
# Verify embedded files
ls -la pkg/web/
# Rebuild if necessary
make clean && make build
```
**Setup Not Starting**
```bash
# Check configuration status
./scripts/setup-transition.sh
# Force setup mode by removing config
rm ~/.bzzz/config.yaml
./build/bzzz
```
**Port Conflicts**
```bash
# Check if port 8080 is in use
netstat -tulpn | grep 8080
# Kill conflicting processes
sudo lsof -ti:8080 | xargs kill -9
```
### Debug Mode
```bash
# Enable debug logging
export BZZZ_LOG_LEVEL=debug
./build/bzzz
# Check embedded files
curl http://localhost:8080/api/health
```
## Security Considerations
### Network Security
- Setup interface only accessible on localhost by default
- CORS enabled for development, restricted in production
- HTTPS recommended for external access
### Configuration Security
- Sensitive values (tokens, keys) stored in separate files
- Configuration backups created automatically
- Audit logging for configuration changes
### Access Control
- Setup mode automatically disabled after configuration
- No authentication required for initial setup
- Full authentication required for normal operation
## Advanced Usage
### Custom Build Configuration
```bash
# Build with custom UI path
UI_DIR=custom-ui make build
# Build without UI (API only)
make build-go
# Production build with optimizations
NODE_ENV=production make build
```
### Configuration Migration
```bash
# Export existing configuration
bzzz --export-config > backup.yaml
# Import configuration
bzzz --import-config backup.yaml
# Validate configuration
bzzz --config-check
```
### Integration Testing
```bash
# Test complete setup flow
make test
# Test web UI components
cd install/config-ui && npm test
# Test Go integration
go test ./api/...
```
## Next Steps
After completing setup:
1. **Verify Operation**: Check BZZZ logs and peer connections
2. **Configure Repositories**: Add GitHub/GitLab tokens and repositories
3. **Join Cluster**: Configure additional nodes to join the cluster
4. **Monitor Health**: Use `/api/health` endpoint for monitoring
5. **Scale Services**: Deploy additional BZZZ components as needed
For advanced configuration and cluster management, see:
- [BZZZ Architecture Documentation](../docs/BZZZ-2B-ARCHITECTURE.md)
- [Operations Guide](../docs/BZZZv2B-OPERATIONS.md)
- [Developer Manual](../docs/BZZZv2B-DEVELOPER.md)

View File

@@ -0,0 +1,425 @@
# BZZZ Web Installation/Configuration Development Plan
## Overview
This plan leverages existing BZZZ infrastructure to implement the missing web-based installation and configuration functionality without reinventing the wheel. We'll integrate the existing config-ui with our enhanced installer and BZZZ's existing systems.
## Existing Infrastructure Analysis
### ✅ **What We Already Have**
1. **HTTP API Server** (`api/http_server.go`)
- Existing HTTP server with CORS support
- Health endpoints (`/api/health`, `/api/status`)
- Hypercore log API endpoints
- Gorilla Mux router setup
2. **Config UI Foundation** (`install/config-ui/`)
- Complete Next.js 14 setup
- 8-step setup wizard framework
- TypeScript + Tailwind CSS
- Component structure already defined
- Progress tracking and navigation
3. **Task Coordinator** (`coordinator/task_coordinator.go`)
- Agent management and role assignment
- Repository integration framework
- Status reporting capabilities
4. **Configuration System** (`pkg/config/`)
- YAML configuration management
- Role and agent configuration
- Network and service settings
5. **Repository Integration** (`repository/factory.go`)
- GITEA and GitHub providers
- Task management interfaces
- Credential handling
## Implementation Strategy
### Phase 1: Backend API Integration (1-2 days)
#### 1.1 Extend HTTP Server with Setup APIs
**File**: `api/http_server.go`
Add new endpoints to existing server:
```go
// Setup and configuration endpoints
api.HandleFunc("/setup/system", h.handleSystemInfo).Methods("GET")
api.HandleFunc("/setup/network", h.handleNetworkConfig).Methods("GET", "POST")
api.HandleFunc("/setup/security", h.handleSecurityConfig).Methods("GET", "POST")
api.HandleFunc("/setup/ai", h.handleAIConfig).Methods("GET", "POST")
api.HandleFunc("/setup/resources", h.handleResourceConfig).Methods("GET", "POST")
api.HandleFunc("/setup/deploy", h.handleServiceDeploy).Methods("POST")
api.HandleFunc("/setup/cluster", h.handleClusterConfig).Methods("GET", "POST")
api.HandleFunc("/setup/validate", h.handleValidation).Methods("POST")
// Repository configuration (integrate with existing factory)
api.HandleFunc("/setup/repository", h.handleRepositoryConfig).Methods("GET", "POST")
```
#### 1.2 System Detection Integration
Leverage existing system info from enhanced installer:
```go
func (h *HTTPServer) handleSystemInfo(w http.ResponseWriter, r *http.Request) {
info := map[string]interface{}{
"os": detectOS(), // From enhanced installer
"architecture": detectArchitecture(), // From enhanced installer
"hardware": detectHardware(), // New GPU detection
"network": detectNetwork(), // Network interface discovery
"services": detectServices(), // Docker, Ollama status
}
json.NewEncoder(w).Encode(info)
}
```
#### 1.3 Configuration Management
Extend existing config system to support web updates:
```go
// New file: api/setup_handlers.go
type SetupManager struct {
configPath string
coordinator *coordinator.TaskCoordinator
repoFactory *repository.DefaultProviderFactory
}
func (s *SetupManager) UpdateNetworkConfig(config NetworkConfig) error {
// Update YAML configuration
// Restart network services if needed
// Validate connectivity
}
```
### Phase 2: Web UI Enhancement (2-3 days)
#### 2.1 Complete Existing Components
The config-ui framework exists but components need implementation:
**SystemDetection.tsx** - Leverage system detection API:
```typescript
const SystemDetection = ({ onComplete, systemInfo }) => {
// Display detected hardware (GPU detection for Parallama)
// Show system requirements validation
// Network interface selection
// Prerequisite checking
}
```
**NetworkConfiguration.tsx** - Network setup:
```typescript
const NetworkConfiguration = ({ onComplete, configData }) => {
// Port configuration (8080, 8081, 4001)
// Firewall rules setup
// Network interface selection
// Connectivity testing
}
```
**SecuritySetup.tsx** - Security configuration:
```typescript
const SecuritySetup = ({ onComplete, configData }) => {
// SSH key generation/upload (for cluster deployment)
// TLS certificate setup
// Authentication method selection
// Security policy configuration
}
```
#### 2.2 Repository Integration Component
**File**: `install/config-ui/app/setup/components/RepositoryConfiguration.tsx`
```typescript
const RepositoryConfiguration = ({ onComplete, configData }) => {
// GITEA/GitHub provider selection
// Credential input and validation
// Repository access testing
// Task label configuration
// Integration with existing repository factory
}
```
#### 2.3 AI Configuration Enhancement
**AIConfiguration.tsx** - GPU optimization:
```typescript
const AIConfiguration = ({ onComplete, systemInfo }) => {
// GPU detection display
// Parallama vs Ollama recommendation
// OpenAI API configuration
// Model selection and downloading
// Resource allocation per model
}
```
### Phase 3: Installer Integration (1 day)
#### 3.1 Enhanced Installer Web Mode
**File**: `/home/tony/chorus/project-queues/active/chorus.services/installer/install-chorus-enhanced.sh`
Add web UI mode option:
```bash
# Add new command line option
while [[ $# -gt 0 ]]; do
case $1 in
--web-ui)
ENABLE_WEB_UI=true
shift
;;
# ... existing options
esac
done
# After basic installation
if [[ "$ENABLE_WEB_UI" == "true" ]]; then
setup_web_ui
fi
setup_web_ui() {
log_step "Setting up web configuration interface..."
# Copy config-ui to BZZZ directory
cp -r "$BZZZ_DIR/../install/config-ui" "$BZZZ_DIR/web-ui"
# Install Node.js and dependencies
install_nodejs
cd "$BZZZ_DIR/web-ui" && npm install
# Start web UI in background
npm run build && npm run start &
echo ""
echo "🌐 Web configuration available at: http://$(hostname):8080/setup"
echo "⚡ Continue setup in your browser"
}
```
#### 3.2 Hybrid Installation Flow
```bash
# Enhanced installer usage options:
curl -fsSL https://chorus.services/install-enhanced.sh | sh # CLI mode (current)
curl -fsSL https://chorus.services/install-enhanced.sh | sh -s -- --web-ui # Web mode (new)
```
### Phase 4: Cluster Deployment Integration (2-3 days)
#### 4.1 SSH Deployment System
**File**: `api/cluster_deployment.go`
```go
type ClusterDeployer struct {
sshConfig SSHConfig
installer string // Enhanced installer script
coordinator *coordinator.TaskCoordinator
}
func (c *ClusterDeployer) DeployToNodes(nodes []NodeConfig) error {
for _, node := range nodes {
// SSH to remote node
// Execute enhanced installer with node-specific config
// Verify installation
// Add to cluster coordination
}
}
```
#### 4.2 Cluster Formation Component
**ClusterFormation.tsx** - Multi-node coordination:
```typescript
const ClusterFormation = ({ onComplete, configData }) => {
// Node discovery (SSH-based or manual IP entry)
// SSH credential configuration
// Remote deployment progress tracking
// Cluster validation and health checking
// P2P network formation verification
}
```
#### 4.3 P2P Network Integration
Leverage existing P2P infrastructure:
```go
// Integration with existing p2p/node.go
func (h *HTTPServer) handleClusterConfig(w http.ResponseWriter, r *http.Request) {
// Use existing P2P node configuration
// Coordinate with task coordinator
// Enable automatic peer discovery
}
```
### Phase 5: Professional Installation Experience (1-2 days)
#### 5.1 Enhanced Installation Output
```bash
# Professional branded installer output
print_professional_banner() {
echo -e "${PURPLE}"
cat << 'EOF'
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔥 BZZZ Distributed AI Coordination Platform
Professional Installation System v2.0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
echo -e "${NC}"
}
show_installation_progress() {
local step="$1"
local total="$2"
local message="$3"
echo -e "${BLUE}[$step/$total]${NC} $message"
# Progress bar
local progress=$((step * 100 / total))
printf "Progress: ["
for ((i=0; i<progress/5; i++)); do printf "█"; done
for ((i=progress/5; i<20; i++)); do printf "░"; done
printf "] %d%%\n" $progress
}
```
#### 5.2 GPU Detection and Parallama Integration
```bash
detect_gpu_configuration() {
log_step "Analyzing GPU configuration..."
# Detect NVIDIA GPUs
if command -v nvidia-smi >/dev/null 2>&1; then
GPU_COUNT=$(nvidia-smi --list-gpus | wc -l)
GPU_INFO=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader)
if [[ $GPU_COUNT -gt 1 ]]; then
echo ""
echo -e "${GREEN}🚀 Multi-GPU Setup Detected ($GPU_COUNT NVIDIA GPUs)${NC}"
echo -e "${CYAN}Parallama is RECOMMENDED for optimal multi-GPU performance!${NC}"
echo ""
echo "Detected GPUs:"
echo "$GPU_INFO" | sed 's/^/ • /'
echo ""
read -p "Install Parallama (recommended) or standard Ollama? [P/o]: " choice
case $choice in
[Oo]* ) INSTALL_OLLAMA_TYPE="standard" ;;
* ) INSTALL_OLLAMA_TYPE="parallama" ;;
esac
fi
fi
}
```
## Implementation Timeline
### Week 1: Backend Foundation
- **Day 1-2**: Extend HTTP server with setup APIs
- **Day 3-4**: Implement system detection and configuration management
- **Day 5**: Repository integration and credential handling
### Week 2: Web UI Development
- **Day 1-2**: Complete existing setup components
- **Day 3-4**: Repository configuration and AI setup components
- **Day 5**: Integration testing and UI polish
### Week 3: Cluster and Professional Features
- **Day 1-2**: SSH deployment system and cluster formation
- **Day 3-4**: Professional installation experience and GPU detection
- **Day 5**: End-to-end testing and documentation
## Leveraging Existing BZZZ Systems
### ✅ **Reuse Without Modification**
1. **HTTP Server Architecture** - Extend existing `api/http_server.go`
2. **Configuration System** - Use existing `pkg/config/` YAML management
3. **Repository Integration** - Leverage `repository/factory.go` providers
4. **Task Coordination** - Integrate with `coordinator/task_coordinator.go`
5. **P2P Networking** - Use existing `p2p/node.go` infrastructure
### 🔧 **Extend Existing Systems**
1. **Enhanced Installer** - Add web UI mode to existing script
2. **Config UI Framework** - Complete existing component implementations
3. **API Endpoints** - Add setup endpoints to existing HTTP server
4. **System Detection** - Enhance existing OS detection with hardware profiling
### 🆕 **New Components Needed**
1. **Cluster Deployment Manager** - SSH-based remote installation
2. **GPU Detection System** - Hardware profiling and Parallama integration
3. **Professional Installation UX** - Enhanced progress and branding
4. **Setup API Handlers** - Backend logic for web configuration
## Integration Points
### Repository Integration
```go
// Leverage existing repository factory
func (h *HTTPServer) handleRepositoryConfig(w http.ResponseWriter, r *http.Request) {
factory := &repository.DefaultProviderFactory{}
// Create provider based on web UI input
provider, err := factory.CreateProvider(ctx, repoConfig)
// Test connectivity
// Store configuration
// Update task coordinator
}
```
### Task Coordinator Integration
```go
// Use existing task coordinator for cluster management
func (h *HTTPServer) handleClusterStatus(w http.ResponseWriter, r *http.Request) {
status := h.taskCoordinator.GetStatus()
json.NewEncoder(w).Encode(status)
}
```
### Configuration Management
```go
// Extend existing config system
func (s *SetupManager) SaveConfiguration(config SetupConfig) error {
// Convert web config to BZZZ YAML format
// Use existing config.Config struct
// Restart services as needed
}
```
## Success Metrics
### Technical Completeness
- [ ] Web UI accessible at `:8080/setup`
- [ ] 8-step configuration wizard functional
- [ ] GPU detection and Parallama recommendation
- [ ] SSH-based cluster deployment
- [ ] Repository integration (GITEA/GitHub)
- [ ] Professional installation experience
### User Experience
- [ ] Single-command installation with web option
- [ ] Intuitive progress tracking and navigation
- [ ] Real-time validation and error handling
- [ ] Mobile-responsive design
- [ ] Comprehensive help and documentation
### Integration Quality
- [ ] Seamless integration with existing BZZZ systems
- [ ] No disruption to current enhanced installer
- [ ] Proper error handling and rollback
- [ ] Production-ready security and performance
## Risk Mitigation
### Technical Risks
- **API Integration Complexity**: Use existing HTTP server patterns
- **Configuration Conflicts**: Maintain YAML compatibility
- **Service Coordination**: Leverage existing task coordinator
### User Experience Risks
- **Installation Complexity**: Provide both CLI and web options
- **Error Recovery**: Implement proper rollback mechanisms
- **Performance**: Optimize for low-resource environments
## Conclusion
This plan leverages 80% of existing BZZZ infrastructure while delivering the professional web-based installation experience envisioned in the original plans. By extending rather than replacing existing systems, we minimize development time and maintain compatibility with current deployments.
**Key Benefits:**
- ✅ Rapid implementation using existing code
- ✅ Maintains backward compatibility
- ✅ Professional installation experience
- ✅ Complete feature parity with original vision
- ✅ Seamless integration with enhanced installer

View File

@@ -0,0 +1,143 @@
# CHORUS Branding Transformation - Config UI
## Overview
Successfully transformed the BZZZ configuration UI to reflect the ultra-minimalist CHORUS branding and design system.
## 🎨 Visual Transformation Completed
### **Before (BZZZ)** → **After (CHORUS)**
| **Element** | **Original (BZZZ)** | **New (CHORUS)** |
|-------------|-------------------|------------------|
| **Primary Color** | Orange `#FF6B35` | Dark Mulberry `#0b0213` |
| **Secondary Color** | Blue `#004E89` | Orchestration Blue `#5a6c80` |
| **Background** | Gray `#F7F7F7` | Natural Paper `#F5F5DC` |
| **Logo** | Orange "B" icon | Mobius ring logo |
| **Card Style** | Rounded + shadows | Clean + invisible borders |
| **Corners** | 8px rounded | 3-5px subtle curves |
| **Spacing** | Standard 24px | Generous 32px+ |
| **Typography** | Mixed hierarchy | Clean SF Pro system |
---
## ✅ Changes Implemented
### 1. **Brand Identity Update**
- ✅ Changed all "BZZZ" references to "CHORUS" or "CHORUS Agent"
- ✅ Updated page titles and descriptions
- ✅ Integrated Mobius ring logo from brand assets
- ✅ Updated localStorage keys from `bzzz-setup-state` to `chorus-setup-state`
### 2. **Color System Implementation**
-**Primary Actions**: Dark Mulberry `#0b0213` (sophisticated, minimal)
-**Secondary Actions**: Orchestration Blue `#5a6c80` (corporate blue)
-**Accent Elements**: Brushed Nickel `#c1bfb1` (subtle highlights)
-**Background System**: Natural Paper `#F5F5DC` (warm, professional)
-**Text Hierarchy**: 5-level grayscale system for perfect readability
### 3. **Ultra-Minimalist Design System**
-**Subtle Rounded Corners**: 3px (small), 4px (standard), 5px (large)
-**Invisible Borders**: `#FAFAFA` for organization without visual weight
-**Clean Cards**: No shadows, generous 32px padding
-**Button System**: Opacity-based states, no gradients
-**Typography**: SF Pro Display hierarchy with proper line heights
### 4. **Layout & Spacing**
-**Header**: Clean logo + title layout with 24px spacing
-**Progress Sidebar**: Minimalist step indicators
-**Grid System**: Increased gap from 32px to 48px for breathing room
-**Form Elements**: Clean inputs with subtle focus states
### 5. **Component Updates**
-**Progress Steps**: Color-coded current/completed/accessible states
-**Status Indicators**: Monochromatic instead of colorful badges
-**Navigation**: Clean text-based links with hover states
-**Resume Notification**: Subtle blue background with proper contrast
---
## 🚀 Technical Implementation
### Files Modified:
1. **`tailwind.config.js`** - Complete color system and design tokens
2. **`app/globals.css`** - Ultra-minimalist component classes
3. **`app/layout.tsx`** - Header/footer with CHORUS branding
4. **`app/setup/page.tsx`** - Progress indicators and content updates
5. **`public/assets/`** - Added Mobius ring logo assets
### Build Status:
**Successfully Built** - All TypeScript compilation passed
**Static Export** - Ready for deployment
**Asset Integration** - Logo files properly referenced
---
## 🎯 Key Features Maintained
### Functionality Preserved:
-**10-step setup wizard** - All steps maintained
-**Progress persistence** - localStorage state management
-**Responsive design** - Mobile and desktop layouts
-**Accessibility** - WCAG 2.1 AA contrast compliance
-**Step navigation** - Forward/backward flow logic
### Enhanced UX:
-**Visual hierarchy** - Cleaner typography system
-**Reduced cognitive load** - Minimalist interface
-**Professional aesthetic** - Corporate-grade appearance
-**Brand consistency** - Aligned with CHORUS identity
---
## 📁 Asset Integration
### Logo Files Added:
- `public/assets/chorus-mobius-on-white.png` - Primary logo for light backgrounds
- `public/assets/chorus-landscape-on-white.png` - Horizontal layout option
### CSS Classes Created:
- `.btn-primary`, `.btn-secondary`, `.btn-text` - Button variants
- `.card`, `.card-elevated` - Container styles
- `.progress-step-*` - Step indicator states
- `.heading-*`, `.text-*` - Typography hierarchy
---
## 🔍 Quality Assurance
### Testing Completed:
-**Build Verification** - Next.js production build successful
-**Asset Loading** - Logo images properly referenced
-**CSS Compilation** - Tailwind classes generated correctly
-**Static Export** - HTML files generated for deployment
### Performance:
-**Bundle Size** - No significant increase (108 kB First Load JS)
-**CSS Optimization** - Tailwind purging working correctly
-**Image Optimization** - Logo assets properly preloaded
---
## 🎨 Visual Preview
The transformed interface now features:
1. **Clean Header** with Mobius ring logo and sophisticated typography
2. **Minimalist Progress Sidebar** with subtle state indicators
3. **Ultra-Clean Cards** with generous spacing and invisible borders
4. **Professional Color Palette** using CHORUS corporate colors
5. **Refined Typography** with proper hierarchy and readability
---
## 🚢 Deployment Ready
The CHORUS-branded configuration UI is now ready for:
-**Production deployment** as part of BZZZ/CHORUS system
-**Integration testing** with backend services
-**User acceptance testing** with the new branding
-**Documentation updates** to reflect CHORUS naming
---
**Transformation Complete** - The setup wizard now perfectly represents the CHORUS brand with an ultra-minimalist, sophisticated aesthetic while maintaining all original functionality.

View File

@@ -0,0 +1,57 @@
'use client'
import { useState, useEffect } from 'react'
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline'
export default function ThemeToggle() {
const [isDark, setIsDark] = useState(true) // Default to dark mode
useEffect(() => {
// Check for saved theme preference or default to dark
const savedTheme = localStorage.getItem('chorus-theme')
const prefersDark = !savedTheme || savedTheme === 'dark'
setIsDark(prefersDark)
updateTheme(prefersDark)
}, [])
const updateTheme = (dark: boolean) => {
const html = document.documentElement
if (dark) {
html.classList.add('dark')
html.classList.remove('light')
} else {
html.classList.remove('dark')
html.classList.add('light')
}
}
const toggleTheme = () => {
const newIsDark = !isDark
setIsDark(newIsDark)
updateTheme(newIsDark)
// Save preference
localStorage.setItem('chorus-theme', newIsDark ? 'dark' : 'light')
}
return (
<button
onClick={toggleTheme}
className="btn-text flex items-center space-x-2 p-2 rounded-md transition-colors duration-200"
aria-label={`Switch to ${isDark ? 'light' : 'dark'} theme`}
>
{isDark ? (
<>
<SunIcon className="h-4 w-4" />
<span className="text-xs">Light</span>
</>
) : (
<>
<MoonIcon className="h-4 w-4" />
<span className="text-xs">Dark</span>
</>
)}
</button>
)
}

View File

@@ -0,0 +1,43 @@
'use client'
import { useEffect, useState } from 'react'
interface VersionInfo {
version: string
full_version: string
timestamp: number
}
export default function VersionDisplay() {
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null)
useEffect(() => {
const fetchVersion = async () => {
try {
const response = await fetch('/api/version')
if (response.ok) {
const data = await response.json()
setVersionInfo(data)
}
} catch (error) {
console.warn('Failed to fetch version:', error)
}
}
fetchVersion()
}, [])
if (!versionInfo) {
return (
<div className="text-xs text-gray-500">
BZZZ
</div>
)
}
return (
<div className="text-xs text-gray-500">
BZZZ {versionInfo.full_version}
</div>
)
}

View File

@@ -0,0 +1,654 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--carbon-950: #000000;
--carbon-900: #0a0a0a;
--carbon-800: #1a1a1a;
--carbon-700: #2a2a2a;
--carbon-600: #666666;
--carbon-500: #808080;
--carbon-400: #a0a0a0;
--carbon-300: #c0c0c0;
--carbon-200: #e0e0e0;
--carbon-100: #f0f0f0;
--carbon-50: #f8f8f8;
--mulberry-950: #0b0213;
--mulberry-900: #1a1426;
--mulberry-800: #2a2639;
--mulberry-700: #3a384c;
--mulberry-600: #4a4a5f;
--mulberry-500: #5a5c72;
--mulberry-400: #7a7e95;
--mulberry-300: #9aa0b8;
--mulberry-200: #bac2db;
--mulberry-100: #dae4fe;
--mulberry-50: #f0f4ff;
--walnut-950: #1E1815;
--walnut-900: #403730;
--walnut-800: #504743;
--walnut-700: #605756;
--walnut-600: #706769;
--walnut-500: #80777c;
--walnut-400: #90878f;
--walnut-300: #a09aa2;
--walnut-200: #b0adb5;
--walnut-100: #c0c0c8;
--walnut-50: #d0d3db;
--walnut-25: #e0e6ee;
--nickel-950: #171717;
--nickel-900: #2a2a2a;
--nickel-800: #3d3d3d;
--nickel-700: #505050;
--nickel-600: #636363;
--nickel-500: #767676;
--nickel-400: #c1bfb1;
--nickel-300: #d4d2c6;
--nickel-200: #e7e5db;
--nickel-100: #faf8f0;
--nickel-50: #fdfcf8;
--ocean-950: #2a3441;
--ocean-900: #3a4654;
--ocean-800: #4a5867;
--ocean-700: #5a6c80;
--ocean-600: #6a7e99;
--ocean-500: #7a90b2;
--ocean-400: #8ba3c4;
--ocean-300: #9bb6d6;
--ocean-200: #abc9e8;
--ocean-100: #bbdcfa;
--ocean-50: #cbefff;
--eucalyptus-950: #2a3330;
--eucalyptus-900: #3a4540;
--eucalyptus-800: #4a5750;
--eucalyptus-700: #515d54;
--eucalyptus-600: #5a6964;
--eucalyptus-500: #6a7974;
--eucalyptus-400: #7a8a7f;
--eucalyptus-300: #8a9b8f;
--eucalyptus-200: #9aac9f;
--eucalyptus-100: #aabdaf;
--eucalyptus-50: #bacfbf;
--sand-950: #8E7B5E;
--sand-900: #99886E;
--sand-800: #A4957E;
--sand-700: #AFA28E;
--sand-600: #BAAF9F;
--sand-500: #C5BCAF;
--sand-400: #D0C9BF;
--sand-300: #DBD6CF;
--sand-200: #E6E3DF;
--sand-100: #F1F0EF;
--sand-50: #F1F0EF;
--coral-950: #6A4A48;
--coral-900: #7B5D5A;
--coral-800: #8C706C;
--coral-700: #9D8380;
--coral-600: #AE9693;
--coral-500: #BFAAA7;
--coral-400: #D0BDBB;
--coral-300: #E1D1CF;
--coral-200: #F2E4E3;
--coral-100: #9e979c;
--coral-50: #aea7ac;
}
/*
--font-sans: ['Inter Tight', 'Inter', 'system-ui', 'sans-serif'],
--font-mono: ['Inconsolata', 'ui-monospace', 'monospace'],
--font-logo: ['Exo', 'Inter Tight', 'sans-serif']
},
spacing: {
'chorus-xxs': '0.854rem',
'chorus-xs': '0.945rem',
'chorus-sm': '1.0rem',
'chorus-base': '1.25rem',
'chorus-md': '1.953rem',
'chorus-lg': '2.441rem',
'chorus-xl': '3.052rem',
'chorus-xxl': '6.1rem',
},
// CHORUS Proportional Typography System (Major Third - 1.25 ratio)
fontSize: {
// Base scale using Minor Third (1.20) ratio for harmonious proportions
'xs': ['0.854rem', { lineHeight: '1.00rem', fontWeight: '600' }], // 10.24px
'sm': ['0.954rem', { lineHeight: '1.10rem', fontWeight: '500' }], // 12.8px
'base': ['1rem', { lineHeight: '1.50rem', fontWeight: '400' }], // 16px (foundation)
'lg': ['1.25rem', { lineHeight: '1.75rem', fontWeight: '400' }], // 20px
'xl': ['1.563rem', { lineHeight: '2.00rem', fontWeight: '400' }], // 25px
'2xl': ['1.953rem', { lineHeight: '2.50rem', fontWeight: '300' }], // 31.25px
'3xl': ['2.441rem', { lineHeight: '3.00rem', fontWeight: '200' }], // 39px
'4xl': ['3.052rem', { lineHeight: '3.50rem', fontWeight: '100' }], // 48.8px
'5xl': ['3.815rem', { lineHeight: '4.00rem', fontWeight: '100' }], // 61px
// Semantic heading sizes for easier usage
'h7': ['1.000rem', { lineHeight: '1.25rem', fontWeight: '400' }], // 14px
'h6': ['1.250rem', { lineHeight: '1.563rem', fontWeight: '500' }], // 16px
'h5': ['1.563rem', { lineHeight: '1.953rem', fontWeight: '500' }], // 20px
'h4': ['1.953rem', { lineHeight: '2.441rem', fontWeight: '600' }], // 25px
'h3': ['2.441rem', { lineHeight: '3.052rem', fontWeight: '600' }], // 31.25px
'h2': ['3.052rem', { lineHeight: '4.768rem', fontWeight: '700' }], // 39px
'h1': ['4.768rem', { lineHeight: '6.96rem', fontWeight: '700' }], // 76.3px
// Display sizes for hero sections
'display-sm': ['3.815rem', { lineHeight: '4rem', fontWeight: '800' }], // 61px
'display-md': ['4.768rem', { lineHeight: '5rem', fontWeight: '800' }], // 76.3px
'display-lg': ['5.96rem', { lineHeight: '6rem', fontWeight: '800' }], // 95.4px
},
// Extended rem-based sizing for complete system consistency
width: {
'rem-xs': '0.640rem',
'rem-sm': '0.800rem',
'rem-base': '1.000rem',
'rem-lg': '1.250rem',
'rem-xl': '1.563rem',
'rem-2xl': '1.953rem',
'rem-3xl': '2.441rem',
'rem-4xl': '3.052rem',
'rem-5xl': '3.815rem',
},
height: {
'rem-xs': '0.640rem',
'rem-sm': '0.800rem',
'rem-base': '1.000rem',
'rem-lg': '1.250rem',
'rem-xl': '1.563rem',
'rem-2xl': '1.953rem',
'rem-3xl': '2.441rem',
'rem-4xl': '3.052rem',
'rem-5xl': '3.815rem',
},
// Border radius using proportional scale
borderRadius: {
'none': '0',
'micro': '0.125rem', // 2px
'sm': '0.25rem', // 4px
'base': '0.375rem', // 6px
'md': '0.5rem', // 8px
'lg': '0.75rem', // 12px
'xl': '1rem', // 16px
'full': '9999px',
}
*/
/* === Teaser-aligned Global Foundation === */
/* CHORUS Proportional Typography System - 16px Base */
html { font-size: 16px; }
/* CHORUS Brand CSS Variables (8-color semantic system) */
:root {
/* Core Brand Colors */
--color-carbon: #000000;
--color-mulberry: #3a384c;
--color-walnut: #605756;
--color-nickel: #505050;
--color-sand: #6a5c46;
--color-coral: #9D8380;
--color-ocean: #5a6c80;
--color-eucalyptus:#515d54;
/* Semantic Tokens */
--chorus-primary: #0b0213; /* mulberry */
--chorus-secondary: #000000; /* carbon */
--chorus-accent: #403730; /* walnut */
--chorus-neutral: #c1bfb1; /* nickel */
--chorus-info: #5a6c80; /* ocean-700 */
--chorus-success: #2a3330; /* eucalyptus-950 */
--chorus-warning: #6a5c46; /* sand-900 */
--chorus-danger: #2e1d1c; /* coral-950 */
/* Theme Surfaces (dark default) */
--bg-primary: #0b0213; /* carbon-950 */
--bg-secondary: #1a1426; /* mulberry-950 */
--bg-tertiary: #2a2639; /* mulberry-900 */
--bg-accent: #5b3d77; /* mulberry-600 */
/* Text */
--text-primary: #FFFFFF;
--text-secondary: #f0f4ff;
--text-tertiary: #dae4fe;
--text-subtle: #9aa0b8;
--text-ghost: #7a7e95;
/* Borders */
--border-invisible: #0a0a0a;
--border-subtle: #1a1a1a;
--border-defined: #2a2a2a;
--border-emphasis: #666666;
}
/* Light Theme Variables (apply when html has class 'light') */
html.light {
--bg-primary: #FFFFFF;
--bg-secondary: #f8f8f8;
--bg-tertiary: #f0f0f0;
--bg-accent: #cbefff;
--text-primary: #000000;
--text-secondary: #1a1a1a;
--text-tertiary: #2a2a2a;
--text-subtle: #666666;
--text-ghost: #808080;
--border-invisible: #f8f8f8;
--border-subtle: #f0f0f0;
--border-defined: #e0e0e0;
--border-emphasis: #c0c0c0;
}
/* Base Styles */
body {
font-family: 'Inter Tight', system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
margin: 0;
padding: 0;
line-height: 1.6;
font-size: 1rem;
font-weight: 400;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@layer base {
html {
font-family: 'Inter Tight', system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
body { @apply transition-colors duration-200; }
}
@layer components {
/* Ultra-Minimalist Button System */
.btn-primary {
@apply text-white font-semibold py-3 px-6 rounded-md transition-all duration-300 disabled:opacity-40 disabled:cursor-not-allowed;
/* Light mode: warm sand gradient */
background: linear-gradient(135deg, var(--chorus-warning) 0%, var(--chorus-neutral) 100%);
border: 2px solid var(--chorus-warning);
}
.btn-secondary {
@apply bg-transparent text-current font-medium py-3 px-6 rounded-md transition-all duration-300 disabled:opacity-40 disabled:cursor-not-allowed;
border: 2px solid var(--border-emphasis);
}
.btn-primary:hover { transform: translateY(-2px); }
.btn-secondary:hover { transform: translateY(-2px); border-color: var(--text-primary); }
/* Dark mode: Mulberry mid-tone for stronger contrast */
html.dark .btn-primary {
background: #5b3d77; /* approx mulberry-500 */
border-color: #5b3d77;
box-shadow: 0 4px 12px rgba(11, 2, 19, 0.35);
}
html.dark .btn-primary:hover {
filter: brightness(1.08);
}
/* Teaser-aligned Form Elements */
.form-input {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 2px solid var(--border-defined);
padding: 0.875rem 1rem;
font-size: 1rem;
width: 100%;
border-radius: 0.375rem;
transition: all 300ms ease-out;
}
.form-input:focus { outline: none; border-color: var(--chorus-primary); box-shadow: 0 0 0 3px rgba(11,2,19,0.1); background: var(--bg-secondary); }
.form-input::placeholder { color: var(--text-subtle); }
.btn-outline {
@apply border border-chorus-primary text-chorus-primary hover:bg-chorus-primary hover:text-white font-medium py-3 px-6 rounded-md transition-all duration-200;
}
.btn-text {
@apply bg-transparent text-chorus-secondary hover:text-white font-medium py-2 px-0 border-none transition-colors duration-200;
}
/* Clean Card System */
.card {
@apply bg-chorus-white border border-chorus-border-subtle p-8 rounded-lg transition-colors duration-200;
}
.card-elevated {
@apply bg-chorus-warm border border-chorus-border-invisible p-8 rounded-lg transition-colors duration-200;
}
/* Form Elements */
.input-field {
@apply block w-full border p-3 rounded-sm focus:outline-none transition-colors duration-200;
background-color: var(--bg-secondary);
border-color: var(--border-defined);
color: var(--text-primary);
}
.input-field:focus {
border-color: var(--chorus-accent);
background-color: var(--bg-primary);
ring: 0;
}
/* Fix form inputs for dark theme */
input[type="checkbox"],
input[type="radio"],
input[type="text"],
input[type="email"],
input[type="password"],
textarea,
select {
background-color: var(--bg-secondary) !important;
border-color: var(--border-defined) !important;
color: var(--text-primary) !important;
}
input[type="checkbox"]:focus,
input[type="radio"]:focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border-color: var(--chorus-accent) !important;
background-color: var(--bg-primary) !important;
}
.label {
@apply block text-sm font-medium mb-2;
color: var(--text-primary);
}
.error-text {
@apply text-red-400 text-sm mt-1;
}
.success-text {
@apply text-eucalyptus-600 text-sm mt-1;
}
/* Status System */
.status-indicator {
@apply text-xs font-medium;
}
.status-online {
@apply status-indicator text-chorus-secondary;
}
.status-offline {
@apply status-indicator text-chorus-text-subtle;
}
.status-pending {
@apply status-indicator text-chorus-brown;
}
.setup-progress {
@apply border transition-all duration-200;
}
.agreement {
background-color: var(--sand-400) !important;
}
html.dark .agreement {
background-color: var(--mulberry-800) !important;
}
/* Progress Elements */
.progress-step {
@apply p-3 rounded-md border transition-all duration-200;
}
.progress-step-current {
background-color: var(--bg-tertiary) !important;
border-color: var(--bg-secondary) !important;
color: var(--text-primary) !important;
}
.progress-step-completed {
background-color: var(--bg-primary) !important;
border-color: var(--bg-secondary) !important;
color: var(--text-primary) !important;
}
.progress-step-accessible {
@apply border-chorus-border-defined hover:border-chorus-border-emphasis text-chorus-text-secondary;
background-color: var(--bg-secondary);
border-color: var(--border-defined);
color: var(--text-secondary);
}
.progress-step-accessible:hover {
background-color: var(--bg-accent);
border-color: var(--border-emphasis);
color: var(--text-primary);
}
.progress-step-disabled {
@apply cursor-not-allowed;
background-color: var(--bg-subtle);
border-color: var(--border-subtle);
color: var(--text-subtle);
}
/* Typography Hierarchy */
.heading-hero {
@apply text-3xl font-semibold text-chorus-text-primary tracking-tight;
}
.heading-section {
@apply text-2xl font-semibold text-chorus-text-primary;
}
.heading-subsection {
@apply text-lg font-medium text-chorus-text-primary;
}
.text-body {
@apply text-base text-chorus-text-secondary leading-relaxed;
}
.text-small {
@apply text-sm text-chorus-text-subtle;
}
.text-ghost {
@apply text-sm text-gray-500 dark:text-gray-500;
}
}
/* Brand Panel Components */
@layer components {
.panel { @apply rounded-lg p-4 border; }
/* Info (Ocean) */
.panel-info { @apply border-ocean-200 bg-ocean-50; }
.panel-info .panel-title { @apply text-ocean-800; }
.panel-info .panel-body { @apply text-ocean-700; }
html.dark .panel-info { @apply border-ocean-700; background-color: rgba(58,70,84,0.20) !important; }
html.dark .panel-info .panel-title { @apply text-ocean-300; }
html.dark .panel-info .panel-body { @apply text-ocean-300; }
/* Note (Nickel / Neutral) */
.panel-note { background-color: #f5f4f1; border-color: #e0ddd7; }
.panel-note .panel-title { @apply text-chorus-text-primary; }
.panel-note .panel-body { @apply text-chorus-text-secondary; }
html.dark .panel-note { background-color: rgba(11,2,19,0.20) !important; border-color: var(--border-defined) !important; }
html.dark .panel-note .panel-title { @apply text-chorus-text-primary; }
html.dark .panel-note .panel-body { @apply text-chorus-text-secondary; }
/* Warning (Sand) */
.panel-warning { @apply bg-sand-100 border-sand-900; }
.panel-warning .panel-title { @apply text-sand-900; }
.panel-warning .panel-body { @apply text-sand-900; }
html.dark .panel-warning { background-color: rgba(106,92,70,0.20) !important; @apply border-sand-900; }
/* Fallback to white/neutral for readability in dark */
html.dark .panel-warning .panel-title { @apply text-white; }
html.dark .panel-warning .panel-body { color: #F1F0EF !important; }
/* Error (Coral) */
.panel-error { @apply bg-coral-50 border-coral-950; }
.panel-error .panel-title { @apply text-coral-950; }
.panel-error .panel-body { @apply text-coral-950; }
html.dark .panel-error { background-color: rgba(46,29,28,0.20) !important; @apply border-coral-950; }
html.dark .panel-error .panel-title { @apply text-white; }
html.dark .panel-error .panel-body { color: #ffd6d6 !important; }
/* Success (Eucalyptus) */
.panel-success { @apply bg-eucalyptus-50 border-eucalyptus-600; }
.panel-success .panel-title { @apply text-eucalyptus-600; }
.panel-success .panel-body { @apply text-eucalyptus-600; }
html.dark .panel-success { background-color: rgba(42,51,48,0.20) !important; @apply border-eucalyptus-400; }
html.dark .panel-success .panel-title { @apply text-white; }
html.dark .panel-success .panel-body { color: #bacfbf !important; }
}
/* Teaser-aligned color aliases */
@layer utilities {
/* 8 standard color families - key shades */
/* Ocean */
/* Ocean scale aliases (selected commonly used steps) */
.bg-ocean-700 { background-color: #5a6c80 !important; }
.text-ocean-700 { color: #5a6c80 !important; }
.border-ocean-700 { border-color: #5a6c80 !important; }
.bg-ocean-600 { background-color: #6a7e99 !important; }
.text-ocean-600 { color: #6a7e99 !important; }
.border-ocean-600 { border-color: #6a7e99 !important; }
.bg-ocean-500 { background-color: #7a90b2 !important; }
.text-ocean-500 { color: #7a90b2 !important; }
.border-ocean-500 { border-color: #7a90b2 !important; }
.bg-ocean-900 { background-color: #3a4654 !important; }
.text-ocean-900 { color: #3a4654 !important; }
.border-ocean-900 { border-color: #3a4654 !important; }
.text-ocean-800 { color: #4a5867 !important; }
.border-ocean-800 { border-color: #4a5867 !important; }
.text-ocean-300 { color: #9bb6d6 !important; }
.border-ocean-300 { border-color: #9bb6d6 !important; }
.border-ocean-200 { border-color: #abc9e8 !important; }
.bg-ocean-50 { background-color: #cbefff !important; }
.text-ocean-50 { color: #cbefff !important; }
.border-ocean-50 { border-color: #cbefff !important; }
/* Mulberry */
.bg-mulberry-950 { background-color: #0b0213 !important; }
.text-mulberry-950 { color: #0b0213 !important; }
.border-mulberry-950 { border-color: #0b0213 !important; }
/* Carbon */
.bg-carbon-950 { background-color: #000000 !important; }
.text-carbon-950 { color: #000000 !important; }
.border-carbon-950 { border-color: #000000 !important; }
/* Walnut */
.bg-walnut-900 { background-color: #403730 !important; }
.text-walnut-900 { color: #403730 !important; }
.border-walnut-900 { border-color: #403730 !important; }
/* Nickel */
.bg-nickel-500 { background-color: #c1bfb1 !important; }
.text-nickel-500 { color: #c1bfb1 !important; }
.border-nickel-500 { border-color: #c1bfb1 !important; }
/* Coral */
.bg-coral-950 { background-color: #2e1d1c !important; }
.bg-coral-50 { background-color: #ffd6d6 !important; }
.text-coral-950 { color: #2e1d1c !important; }
.border-coral-950 { border-color: #2e1d1c !important; }
/* Sand */
.bg-sand-900 { background-color: #6a5c46 !important; }
.bg-sand-100 { background-color: #F1F0EF !important; }
.text-sand-900 { color: #6a5c46 !important; }
.border-sand-900 { border-color: #6a5c46 !important; }
/* Eucalyptus */
.bg-eucalyptus-950 { background-color: #2a3330 !important; }
.bg-eucalyptus-800 { background-color: #3a4843 !important; }
.bg-eucalyptus-600 { background-color: #5a7060 !important; }
.bg-eucalyptus-500 { background-color: #6b8570 !important; }
.bg-eucalyptus-400 { background-color: #7c9a80 !important; }
.bg-eucalyptus-50 { background-color: #bacfbf !important; }
.text-eucalyptus-950 { color: #2a3330 !important; }
.text-eucalyptus-800 { color: #3a4843 !important; }
.text-eucalyptus-600 { color: #5a7060 !important; }
.text-eucalyptus-500 { color: #6b8570 !important; }
.text-eucalyptus-400 { color: #7c9a80 !important; }
.border-eucalyptus-950 { border-color: #2a3330 !important; }
.border-eucalyptus-800 { border-color: #3a4843 !important; }
.border-eucalyptus-600 { border-color: #5a7060 !important; }
.border-eucalyptus-500 { border-color: #6b8570 !important; }
.border-eucalyptus-400 { border-color: #7c9a80 !important; }
/* Utility text/border fallbacks for theme tokens */
.text-chorus-primary { color: var(--text-primary) !important; }
.text-chorus-secondary { color: var(--text-secondary) !important; }
.text-chorus-text-primary { color: var(--text-primary) !important; }
.text-chorus-text-secondary { color: var(--text-secondary) !important; }
.text-chorus-text-tertiary { color: var(--text-tertiary) !important; }
.text-chorus-text-subtle { color: var(--text-subtle) !important; }
.text-chorus-text-ghost { color: var(--text-ghost) !important; }
.bg-chorus-primary { background-color: var(--bg-primary) !important; }
.bg-chorus-white { background-color: var(--bg-secondary) !important; }
.bg-chorus-warm { background-color: var(--bg-tertiary) !important; }
.border-chorus-border-subtle { border-color: var(--border-subtle) !important; }
.border-chorus-border-defined { border-color: var(--border-defined) !important; }
.border-chorus-border-invisible { border-color: var(--border-invisible) !important; }
}
/* CHORUS Typography utilities (subset) */
.text-h1 { font-size: 4.268rem; line-height: 6.96rem; font-weight: 100; letter-spacing: -0.02em; }
.text-h2 { font-size: 3.052rem; line-height: 4.768rem; font-weight: 700; }
.text-h3 { font-size: 2.441rem; line-height: 3.052rem; font-weight: 600; }
.text-h4 { font-size: 1.953rem; line-height: 2.441rem; font-weight: 600; }
.text-h5 { font-size: 1.563rem; line-height: 1.953rem; font-weight: 500; }
.text-h6 { font-size: 1.25rem; line-height: 1.563rem; font-weight: 500; }
/* Motion */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { opacity: 0; transform: translateY(2rem); } to { opacity: 1; transform: translateY(0); } }
.animate-fade-in { animation: fadeIn 0.6s ease-out; }
.animate-slide-up { animation: slideUp 0.8s ease-out; }
/* Dark-mode heading contrast: make headings white unless panel overrides apply */
@layer base {
html.dark h1:not(.panel-title),
html.dark h2:not(.panel-title),
html.dark h3:not(.panel-title),
html.dark h4:not(.panel-title),
html.dark h5:not(.panel-title),
html.dark h6:not(.panel-title) {
color: #ffffff !important;
}
}
@layer utilities {
html.dark .text-h1, html.dark .text-h2, html.dark .text-h3,
html.dark .text-h4, html.dark .text-h5, html.dark .text-h6 { color: #ffffff !important; }
}

View File

@@ -0,0 +1,83 @@
import type { Metadata } from 'next'
import './globals.css'
import ThemeToggle from './components/ThemeToggle'
import VersionDisplay from './components/VersionDisplay'
export const metadata: Metadata = {
title: 'CHORUS Agent Configuration',
description: 'Configure your CHORUS distributed agent orchestration platform',
viewport: 'width=device-width, initial-scale=1',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="dark">
<body className="min-h-screen bg-chorus-primary transition-colors duration-200">
<div className="min-h-screen flex flex-col">
<header className="bg-chorus-primary border-b border-chorus-border-subtle transition-colors duration-200">
<div className="max-w-7xl mx-auto px-8 py-6">
<div className="flex justify-between items-center">
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img src="/assets/chorus-mobius-on-white.png" alt="CHORUS" className="w-10 h-10" />
</div>
<div>
<div className="flex items-center space-x-3">
<h1 className="heading-subsection">
CHORUS Agent Configuration
</h1>
<VersionDisplay />
</div>
<p className="text-small">
Distributed Agent Orchestration Platform
</p>
</div>
</div>
<div className="flex items-center space-x-6">
<div className="status-online">
System Online
</div>
<ThemeToggle />
</div>
</div>
</div>
</header>
<main className="flex-1">
{children}
</main>
<footer className="bg-chorus-primary border-t border-chorus-border-subtle transition-colors duration-200">
<div className="max-w-7xl mx-auto px-8 py-6">
<div className="flex justify-between items-center text-sm text-gray-400">
<div>
© 2025 Chorus Services. All rights reserved.
</div>
<div className="flex space-x-6">
<a
href="https://docs.chorus.services/agents"
target="_blank"
className="btn-text"
>
Documentation
</a>
<a
href="https://discord.gg/chorus-services"
target="_blank"
className="btn-text"
>
Support
</a>
</div>
</div>
</div>
</footer>
</div>
</body>
</html>
)
}

View File

@@ -0,0 +1,6 @@
import SetupPage from './setup/page'
export default function HomePage() {
// Serve setup page directly at root to avoid redirect loops
return <SetupPage />
}

View File

@@ -0,0 +1,634 @@
'use client'
import { useState, useEffect } from 'react'
import {
CpuChipIcon,
SparklesIcon,
CurrencyDollarIcon,
ServerIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
EyeIcon,
EyeSlashIcon,
ArrowPathIcon
} from '@heroicons/react/24/outline'
interface GPUInfo {
name: string
memory: string
type: string
driver: string
}
interface AIConfig {
// OpenAI Configuration
openaiEnabled: boolean
openaiApiKey: string
openaiOrganization: string
openaiDefaultModel: string
// Cost Management
dailyCostLimit: number
monthlyCostLimit: number
costAlerts: boolean
// Local AI (Ollama/Parallama)
localAIEnabled: boolean
localAIType: 'ollama' | 'parallama'
localAIEndpoint: string
localAIModels: string[]
// GPU Configuration
gpuAcceleration: boolean
preferredGPU: string
maxGPUMemory: number
// Model Selection
preferredProvider: 'openai' | 'local' | 'hybrid'
fallbackEnabled: boolean
}
interface AIConfigurationProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function AIConfiguration({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: AIConfigurationProps) {
const [config, setConfig] = useState<AIConfig>({
openaiEnabled: false,
openaiApiKey: '',
openaiOrganization: '',
openaiDefaultModel: 'gpt-4',
dailyCostLimit: 50,
monthlyCostLimit: 500,
costAlerts: true,
localAIEnabled: true,
localAIType: 'ollama',
localAIEndpoint: 'http://localhost:11434',
localAIModels: ['llama2', 'codellama'],
gpuAcceleration: false,
preferredGPU: '',
maxGPUMemory: 8,
preferredProvider: 'local',
fallbackEnabled: true
})
const [showApiKey, setShowApiKey] = useState(false)
const [validatingOpenAI, setValidatingOpenAI] = useState(false)
const [validatingLocal, setValidatingLocal] = useState(false)
const [openaiValid, setOpenaiValid] = useState<boolean | null>(null)
const [localAIValid, setLocalAIValid] = useState<boolean | null>(null)
// Initialize configuration from existing data
useEffect(() => {
if (configData.ai) {
setConfig(prev => ({ ...prev, ...configData.ai }))
}
// Auto-detect GPU capabilities
if (systemInfo?.gpus?.length > 0) {
const hasNVIDIA = systemInfo.gpus.some((gpu: GPUInfo) => gpu.type === 'nvidia')
const hasAMD = systemInfo.gpus.some((gpu: GPUInfo) => gpu.type === 'amd')
if (hasNVIDIA) {
setConfig(prev => ({
...prev,
gpuAcceleration: true,
localAIType: 'parallama', // Parallama typically better for NVIDIA
preferredGPU: systemInfo.gpus.find((gpu: GPUInfo) => gpu.type === 'nvidia')?.name || ''
}))
} else if (hasAMD) {
setConfig(prev => ({
...prev,
gpuAcceleration: true,
localAIType: 'ollama', // Ollama works well with AMD
preferredGPU: systemInfo.gpus.find((gpu: GPUInfo) => gpu.type === 'amd')?.name || ''
}))
}
}
}, [systemInfo, configData])
const validateOpenAI = async () => {
if (!config.openaiApiKey) {
setOpenaiValid(false)
return
}
setValidatingOpenAI(true)
try {
// This would be a real API validation in production
// For now, just simulate validation
await new Promise(resolve => setTimeout(resolve, 1000))
setOpenaiValid(true)
} catch (error) {
setOpenaiValid(false)
} finally {
setValidatingOpenAI(false)
}
}
const validateLocalAI = async () => {
if (!config.localAIEndpoint) {
setLocalAIValid(false)
return
}
setValidatingLocal(true)
try {
const response = await fetch('/api/setup/ollama/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
endpoint: config.localAIEndpoint
})
})
const result = await response.json()
if (result.valid && result.models) {
setLocalAIValid(true)
// Update the local AI models list with discovered models
setConfig(prev => ({ ...prev, localAIModels: result.models }))
} else {
setLocalAIValid(false)
console.error('Ollama validation failed:', result.message)
}
} catch (error) {
setLocalAIValid(false)
console.error('Ollama validation error:', error)
} finally {
setValidatingLocal(false)
}
}
const getGPURecommendations = () => {
if (!systemInfo?.gpus?.length) {
return {
recommendation: 'No GPU detected. CPU-only processing will be used.',
type: 'info',
details: 'Consider adding a GPU for better AI performance.'
}
}
const gpus = systemInfo.gpus
const nvidiaGPUs = gpus.filter((gpu: GPUInfo) => gpu.type === 'nvidia')
const amdGPUs = gpus.filter((gpu: GPUInfo) => gpu.type === 'amd')
if (nvidiaGPUs.length > 0) {
return {
recommendation: 'NVIDIA GPU detected - Parallama recommended for optimal performance',
type: 'success',
details: `${nvidiaGPUs[0].name} with ${nvidiaGPUs[0].memory} VRAM detected. Parallama provides excellent NVIDIA GPU acceleration.`
}
}
if (amdGPUs.length > 0) {
return {
recommendation: 'AMD GPU detected - Ollama with ROCm support recommended',
type: 'warning',
details: `${amdGPUs[0].name} detected. Ollama provides good AMD GPU support through ROCm.`
}
}
return {
recommendation: 'Integrated GPU detected - Limited AI acceleration available',
type: 'warning',
details: 'Integrated GPUs provide limited AI acceleration. Consider a dedicated GPU for better performance.'
}
}
const getRecommendedModels = () => {
const memoryGB = systemInfo?.memory_mb ? Math.round(systemInfo.memory_mb / 1024) : 8
if (memoryGB >= 32) {
return ['llama2:70b', 'codellama:34b', 'mixtral:8x7b']
} else if (memoryGB >= 16) {
return ['llama2:13b', 'codellama:13b', 'llama2:7b']
} else {
return ['llama2:7b', 'codellama:7b', 'phi2']
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Validate that at least one AI provider is configured
if (!config.openaiEnabled && !config.localAIEnabled) {
alert('Please enable at least one AI provider (OpenAI or Local AI)')
return
}
onComplete({ ai: config })
}
const gpuRecommendation = getGPURecommendations()
const recommendedModels = getRecommendedModels()
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* GPU Detection & Recommendations */}
{systemInfo?.gpus && (
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<CpuChipIcon className="h-6 w-6 text-bzzz-primary mr-2" />
GPU Configuration
</h3>
<div className={`p-4 rounded-lg border mb-4 ${
gpuRecommendation.type === 'success' ? 'bg-eucalyptus-50 border-eucalyptus-950' :
gpuRecommendation.type === 'warning' ? 'bg-yellow-50 border-yellow-200' :
'bg-blue-50 border-blue-200'
}`}>
<div className="flex items-start">
<InformationCircleIcon className={`h-5 w-5 mt-0.5 mr-2 ${
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-600' :
'text-blue-600'
}`} />
<div>
<div className={`font-medium ${
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-800' :
'text-blue-800'
}`}>
{gpuRecommendation.recommendation}
</div>
<div className={`text-sm mt-1 ${
gpuRecommendation.type === 'success' ? 'text-eucalyptus-600' :
gpuRecommendation.type === 'warning' ? 'text-yellow-700' :
'text-blue-700'
}`}>
{gpuRecommendation.details}
</div>
</div>
</div>
</div>
{systemInfo.gpus.length > 0 && (
<div className="space-y-3">
<div className="flex items-center">
<input
type="checkbox"
id="gpuAcceleration"
checked={config.gpuAcceleration}
onChange={(e) => setConfig(prev => ({ ...prev, gpuAcceleration: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="gpuAcceleration" className="ml-2 text-sm font-medium text-gray-700">
Enable GPU acceleration for AI processing
</label>
</div>
{config.gpuAcceleration && (
<div>
<label className="label">Preferred GPU</label>
<select
value={config.preferredGPU}
onChange={(e) => setConfig(prev => ({ ...prev, preferredGPU: e.target.value }))}
className="input-field"
>
<option value="">Auto-select</option>
{systemInfo.gpus.map((gpu: GPUInfo, index: number) => (
<option key={index} value={gpu.name}>
{gpu.name} ({gpu.type.toUpperCase()}) - {gpu.memory}
</option>
))}
</select>
</div>
)}
</div>
)}
</div>
)}
{/* Local AI Configuration */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 flex items-center">
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Local AI (Ollama/Parallama)
</h3>
<div className="flex items-center">
<input
type="checkbox"
id="localAIEnabled"
checked={config.localAIEnabled}
onChange={(e) => setConfig(prev => ({ ...prev, localAIEnabled: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="localAIEnabled" className="ml-2 text-sm font-medium text-gray-700">
Enable Local AI
</label>
</div>
</div>
{config.localAIEnabled && (
<div className="space-y-4">
<div>
<label className="label">Local AI Provider</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
config.localAIType === 'ollama'
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setConfig(prev => ({ ...prev, localAIType: 'ollama' }))}
>
<div className="font-medium text-gray-900">Ollama</div>
<div className="text-sm text-gray-600">Open-source, self-hosted AI models</div>
<div className="text-xs text-gray-500 mt-1">Best for: AMD GPUs, CPU-only setups</div>
</div>
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
config.localAIType === 'parallama'
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setConfig(prev => ({ ...prev, localAIType: 'parallama' }))}
>
<div className="font-medium text-gray-900">Parallama</div>
<div className="text-sm text-gray-600">Optimized for parallel processing</div>
<div className="text-xs text-gray-500 mt-1">Best for: NVIDIA GPUs, high performance</div>
</div>
</div>
</div>
<div>
<label className="label">API Endpoint</label>
<div className="flex space-x-2">
<input
type="url"
value={config.localAIEndpoint}
onChange={(e) => setConfig(prev => ({ ...prev, localAIEndpoint: e.target.value }))}
placeholder="http://localhost:11434"
className="input-field flex-1"
/>
<button
type="button"
onClick={validateLocalAI}
disabled={validatingLocal}
className="btn-outline whitespace-nowrap"
>
{validatingLocal ? (
<ArrowPathIcon className="h-4 w-4 animate-spin" />
) : (
'Test'
)}
</button>
</div>
{localAIValid === true && (
<div className="flex items-center mt-1 text-eucalyptus-600 text-sm">
<CheckCircleIcon className="h-4 w-4 mr-1" />
Connection successful
</div>
)}
{localAIValid === false && (
<div className="flex items-center mt-1 text-red-600 text-sm">
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
Connection failed
</div>
)}
</div>
<div>
<label className="label">Recommended Models for your system</label>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="text-sm text-blue-800">
<p className="font-medium mb-2">Based on your system memory ({Math.round(systemInfo?.memory_mb / 1024 || 8)} GB):</p>
<div className="flex flex-wrap gap-2">
{recommendedModels.map((model, index) => (
<span key={index} className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">
{model}
</span>
))}
</div>
</div>
</div>
</div>
</div>
)}
</div>
{/* OpenAI Configuration */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 flex items-center">
<SparklesIcon className="h-6 w-6 text-bzzz-primary mr-2" />
OpenAI API
</h3>
<div className="flex items-center">
<input
type="checkbox"
id="openaiEnabled"
checked={config.openaiEnabled}
onChange={(e) => setConfig(prev => ({ ...prev, openaiEnabled: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="openaiEnabled" className="ml-2 text-sm font-medium text-gray-700">
Enable OpenAI API
</label>
</div>
</div>
{config.openaiEnabled && (
<div className="space-y-4">
<div>
<label className="label">API Key</label>
<div className="flex space-x-2">
<div className="relative flex-1">
<input
type={showApiKey ? 'text' : 'password'}
value={config.openaiApiKey}
onChange={(e) => setConfig(prev => ({ ...prev, openaiApiKey: e.target.value }))}
placeholder="sk-..."
className="input-field pr-10"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showApiKey ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
<button
type="button"
onClick={validateOpenAI}
disabled={validatingOpenAI || !config.openaiApiKey}
className="btn-outline whitespace-nowrap"
>
{validatingOpenAI ? (
<ArrowPathIcon className="h-4 w-4 animate-spin" />
) : (
'Validate'
)}
</button>
</div>
{openaiValid === true && (
<div className="flex items-center mt-1 text-eucalyptus-600 text-sm">
<CheckCircleIcon className="h-4 w-4 mr-1" />
API key valid
</div>
)}
{openaiValid === false && (
<div className="flex items-center mt-1 text-red-600 text-sm">
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
Invalid API key
</div>
)}
</div>
<div>
<label className="label">Organization (Optional)</label>
<input
type="text"
value={config.openaiOrganization}
onChange={(e) => setConfig(prev => ({ ...prev, openaiOrganization: e.target.value }))}
placeholder="org-..."
className="input-field"
/>
</div>
<div>
<label className="label">Default Model</label>
<select
value={config.openaiDefaultModel}
onChange={(e) => setConfig(prev => ({ ...prev, openaiDefaultModel: e.target.value }))}
className="input-field"
>
<option value="gpt-4">GPT-4</option>
<option value="gpt-4-turbo">GPT-4 Turbo</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
</select>
</div>
</div>
)}
</div>
{/* Cost Management */}
{config.openaiEnabled && (
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<CurrencyDollarIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Cost Management
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="label">Daily Cost Limit ($)</label>
<input
type="number"
value={config.dailyCostLimit}
onChange={(e) => setConfig(prev => ({ ...prev, dailyCostLimit: parseFloat(e.target.value) || 0 }))}
min="0"
step="0.01"
className="input-field"
/>
</div>
<div>
<label className="label">Monthly Cost Limit ($)</label>
<input
type="number"
value={config.monthlyCostLimit}
onChange={(e) => setConfig(prev => ({ ...prev, monthlyCostLimit: parseFloat(e.target.value) || 0 }))}
min="0"
step="0.01"
className="input-field"
/>
</div>
</div>
<div className="mt-4">
<div className="flex items-center">
<input
type="checkbox"
id="costAlerts"
checked={config.costAlerts}
onChange={(e) => setConfig(prev => ({ ...prev, costAlerts: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="costAlerts" className="ml-2 text-sm font-medium text-gray-700">
Send alerts when approaching cost limits
</label>
</div>
</div>
</div>
)}
{/* Provider Preference */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Provider Preference</h3>
<div className="space-y-3">
<div>
<label className="label">Preferred AI Provider</label>
<select
value={config.preferredProvider}
onChange={(e) => setConfig(prev => ({ ...prev, preferredProvider: e.target.value as 'openai' | 'local' | 'hybrid' }))}
className="input-field"
>
<option value="local">Local AI Only</option>
<option value="openai">OpenAI Only</option>
<option value="hybrid">Hybrid (Local first, OpenAI fallback)</option>
</select>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="fallbackEnabled"
checked={config.fallbackEnabled}
onChange={(e) => setConfig(prev => ({ ...prev, fallbackEnabled: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="fallbackEnabled" className="ml-2 text-sm font-medium text-gray-700">
Enable automatic fallback between providers
</label>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button
type="submit"
className="btn-primary"
disabled={!config.openaiEnabled && !config.localAIEnabled}
>
{isCompleted ? 'Continue' : 'Next: Resource Allocation'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,552 @@
'use client'
import { useState, useEffect } from 'react'
import {
ServerStackIcon,
PlusIcon,
MagnifyingGlassIcon,
WifiIcon,
ComputerDesktopIcon,
ArrowPathIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
UserGroupIcon,
KeyIcon
} from '@heroicons/react/24/outline'
interface DiscoveredNode {
id: string
hostname: string
ip: string
port: number
version: string
capabilities: string[]
status: 'online' | 'offline' | 'pending'
lastSeen: Date
}
interface ClusterConfig {
mode: 'create' | 'join'
networkId: string
clusterName: string
nodeRole: 'coordinator' | 'worker' | 'hybrid'
joinKey?: string
targetNode?: string
autoDiscovery: boolean
encryption: boolean
redundancy: number
}
interface ClusterFormationProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function ClusterFormation({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: ClusterFormationProps) {
const [config, setConfig] = useState<ClusterConfig>({
mode: 'create',
networkId: '',
clusterName: '',
nodeRole: 'hybrid',
autoDiscovery: true,
encryption: true,
redundancy: 2
})
const [discoveredNodes, setDiscoveredNodes] = useState<DiscoveredNode[]>([])
const [scanning, setScanning] = useState(false)
const [generatingKey, setGeneratingKey] = useState(false)
const [clusterKey, setClusterKey] = useState('')
// Initialize configuration
useEffect(() => {
if (configData.cluster) {
setConfig(prev => ({ ...prev, ...configData.cluster }))
}
// Generate default network ID based on hostname
if (!config.networkId && systemInfo?.network?.hostname) {
const hostname = systemInfo.network.hostname
const timestamp = Date.now().toString(36).slice(-4)
setConfig(prev => ({
...prev,
networkId: `bzzz-${hostname}-${timestamp}`,
clusterName: `${hostname} BZZZ Cluster`
}))
}
}, [systemInfo, configData])
// Auto-discover nodes when joining
useEffect(() => {
if (config.mode === 'join' && config.autoDiscovery) {
scanForNodes()
}
}, [config.mode, config.autoDiscovery])
const scanForNodes = async () => {
setScanning(true)
try {
// This would be a real mDNS/network scan in production
// Simulating discovery for demo
await new Promise(resolve => setTimeout(resolve, 2000))
const mockNodes: DiscoveredNode[] = [
{
id: 'node-001',
hostname: 'ironwood',
ip: '192.168.1.72',
port: 8080,
version: '2.0.0',
capabilities: ['coordinator', 'storage', 'compute'],
status: 'online',
lastSeen: new Date()
},
{
id: 'node-002',
hostname: 'walnut',
ip: '192.168.1.27',
port: 8080,
version: '2.0.0',
capabilities: ['worker', 'compute'],
status: 'online',
lastSeen: new Date()
}
]
setDiscoveredNodes(mockNodes)
} catch (error) {
console.error('Node discovery failed:', error)
} finally {
setScanning(false)
}
}
const generateClusterKey = async () => {
setGeneratingKey(true)
try {
// Generate a secure cluster key
const key = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
setClusterKey(key)
} catch (error) {
// Fallback key generation
const key = Math.random().toString(36).substr(2, 32)
setClusterKey(key)
} finally {
setGeneratingKey(false)
}
}
const getNodeRoleDescription = (role: string) => {
switch (role) {
case 'coordinator':
return 'Manages cluster state and coordinates tasks. Requires stable network connection.'
case 'worker':
return 'Executes tasks assigned by coordinators. Can be dynamically added/removed.'
case 'hybrid':
return 'Can act as both coordinator and worker. Recommended for most deployments.'
default:
return ''
}
}
const getSystemRecommendation = () => {
const memoryGB = systemInfo?.memory_mb ? Math.round(systemInfo.memory_mb / 1024) : 8
const cpuCores = systemInfo?.cpu_cores || 4
const hasGPU = systemInfo?.gpus?.length > 0
if (memoryGB >= 16 && cpuCores >= 8) {
return {
role: 'coordinator',
reason: 'High-performance system suitable for cluster coordination'
}
} else if (hasGPU) {
return {
role: 'hybrid',
reason: 'GPU acceleration available - good for both coordination and compute tasks'
}
} else {
return {
role: 'worker',
reason: 'Resource-optimized configuration for task execution'
}
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const clusterData = {
...config,
clusterKey: config.mode === 'create' ? clusterKey : undefined,
systemInfo: {
hostname: systemInfo?.network?.hostname,
ip: systemInfo?.network?.private_ips?.[0],
capabilities: systemInfo?.gpus?.length > 0 ? ['compute', 'gpu'] : ['compute']
}
}
onComplete({ cluster: clusterData })
}
const recommendation = getSystemRecommendation()
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Cluster Mode Selection */}
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<ServerStackIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Cluster Mode
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
config.mode === 'create'
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setConfig(prev => ({ ...prev, mode: 'create' }))}
>
<div className="flex items-center mb-2">
<PlusIcon className="h-5 w-5 text-bzzz-primary mr-2" />
<div className="font-medium text-gray-900">Create New Cluster</div>
</div>
<div className="text-sm text-gray-600">
Start a new BZZZ cluster and become the initial coordinator node.
</div>
</div>
<div
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
config.mode === 'join'
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setConfig(prev => ({ ...prev, mode: 'join' }))}
>
<div className="flex items-center mb-2">
<UserGroupIcon className="h-5 w-5 text-bzzz-primary mr-2" />
<div className="font-medium text-gray-900">Join Existing Cluster</div>
</div>
<div className="text-sm text-gray-600">
Connect to an existing BZZZ cluster as a worker or coordinator node.
</div>
</div>
</div>
</div>
{/* Create Cluster Configuration */}
{config.mode === 'create' && (
<div className="space-y-6">
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">New Cluster Configuration</h3>
<div className="space-y-4">
<div>
<label className="label">Cluster Name</label>
<input
type="text"
value={config.clusterName}
onChange={(e) => setConfig(prev => ({ ...prev, clusterName: e.target.value }))}
placeholder="My BZZZ Cluster"
className="input-field"
required
/>
</div>
<div>
<label className="label">Network ID</label>
<input
type="text"
value={config.networkId}
onChange={(e) => setConfig(prev => ({ ...prev, networkId: e.target.value }))}
placeholder="bzzz-cluster-001"
className="input-field"
required
/>
<p className="text-sm text-gray-600 mt-1">
Unique identifier for your cluster network
</p>
</div>
<div>
<label className="label">Cluster Security Key</label>
<div className="flex space-x-2">
<input
type="text"
value={clusterKey}
onChange={(e) => setClusterKey(e.target.value)}
placeholder="Click generate or enter custom key"
className="input-field flex-1"
readOnly={!clusterKey}
/>
<button
type="button"
onClick={generateClusterKey}
disabled={generatingKey}
className="btn-outline whitespace-nowrap"
>
{generatingKey ? (
<ArrowPathIcon className="h-4 w-4 animate-spin" />
) : (
<>
<KeyIcon className="h-4 w-4 mr-1" />
Generate
</>
)}
</button>
</div>
<p className="text-sm text-gray-600 mt-1">
This key will be required for other nodes to join your cluster
</p>
</div>
</div>
</div>
</div>
)}
{/* Join Cluster Configuration */}
{config.mode === 'join' && (
<div className="space-y-6">
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Available Clusters</h3>
<button
type="button"
onClick={scanForNodes}
disabled={scanning}
className="btn-outline text-sm"
>
{scanning ? (
<>
<ArrowPathIcon className="h-4 w-4 animate-spin mr-1" />
Scanning...
</>
) : (
<>
<MagnifyingGlassIcon className="h-4 w-4 mr-1" />
Scan Network
</>
)}
</button>
</div>
{discoveredNodes.length > 0 ? (
<div className="space-y-3">
{discoveredNodes.map((node) => (
<div
key={node.id}
className={`border rounded-lg p-4 cursor-pointer transition-all ${
config.targetNode === node.id
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setConfig(prev => ({ ...prev, targetNode: node.id }))}
>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center">
<ComputerDesktopIcon className="h-5 w-5 text-gray-500 mr-2" />
<span className="font-medium text-gray-900">{node.hostname}</span>
<span className={`ml-2 status-indicator ${
node.status === 'online' ? 'status-online' : 'status-offline'
}`}>
{node.status}
</span>
</div>
<div className="text-sm text-gray-600 mt-1">
{node.ip}:{node.port} Version {node.version}
</div>
<div className="flex flex-wrap gap-1 mt-1">
{node.capabilities.map((cap, index) => (
<span key={index} className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-xs">
{cap}
</span>
))}
</div>
</div>
<WifiIcon className="h-5 w-5 text-bzzz-primary" />
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<MagnifyingGlassIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600">
{scanning ? 'Scanning for BZZZ clusters...' : 'No clusters found. Click scan to search for available clusters.'}
</p>
</div>
)}
{config.targetNode && (
<div className="mt-4 pt-4 border-t border-gray-200">
<label className="label">Cluster Join Key</label>
<input
type="password"
value={config.joinKey || ''}
onChange={(e) => setConfig(prev => ({ ...prev, joinKey: e.target.value }))}
placeholder="Enter cluster security key"
className="input-field"
required
/>
<p className="text-sm text-gray-600 mt-1">
Enter the security key provided by the cluster administrator
</p>
</div>
)}
</div>
</div>
)}
{/* Node Role Configuration */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Node Role</h3>
{/* System Recommendation */}
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start">
<InformationCircleIcon className="h-5 w-5 text-blue-600 mr-2 mt-0.5" />
<div>
<div className="font-medium text-blue-800">
Recommended: {recommendation.role.charAt(0).toUpperCase() + recommendation.role.slice(1)}
</div>
<div className="text-sm text-blue-700 mt-1">
{recommendation.reason}
</div>
</div>
</div>
</div>
<div className="space-y-3">
{['coordinator', 'worker', 'hybrid'].map((role) => (
<div
key={role}
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
config.nodeRole === role
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setConfig(prev => ({ ...prev, nodeRole: role as any }))}
>
<div className="flex items-center">
<input
type="radio"
name="nodeRole"
value={role}
checked={config.nodeRole === role}
onChange={() => setConfig(prev => ({ ...prev, nodeRole: role as any }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300"
/>
<div className="ml-3">
<div className="font-medium text-gray-900 capitalize">{role}</div>
<div className="text-sm text-gray-600">{getNodeRoleDescription(role)}</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Advanced Options */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Advanced Options</h3>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
id="autoDiscovery"
checked={config.autoDiscovery}
onChange={(e) => setConfig(prev => ({ ...prev, autoDiscovery: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="autoDiscovery" className="ml-2 text-sm font-medium text-gray-700">
Enable automatic node discovery (mDNS)
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="encryption"
checked={config.encryption}
onChange={(e) => setConfig(prev => ({ ...prev, encryption: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="encryption" className="ml-2 text-sm font-medium text-gray-700">
Enable end-to-end encryption for cluster communication
</label>
</div>
<div>
<label className="label">Redundancy Level</label>
<select
value={config.redundancy}
onChange={(e) => setConfig(prev => ({ ...prev, redundancy: parseInt(e.target.value) }))}
className="input-field"
>
<option value={1}>Low (1 replica)</option>
<option value={2}>Medium (2 replicas)</option>
<option value={3}>High (3 replicas)</option>
</select>
<p className="text-sm text-gray-600 mt-1">
Number of replicas for critical cluster data
</p>
</div>
</div>
</div>
{/* Configuration Summary */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center mb-2">
<CheckCircleIcon className="h-5 w-5 text-blue-600 mr-2" />
<span className="text-blue-800 font-medium">Configuration Summary</span>
</div>
<div className="text-blue-700 text-sm space-y-1">
<p> Mode: {config.mode === 'create' ? 'Create new cluster' : 'Join existing cluster'}</p>
<p> Role: {config.nodeRole}</p>
<p> Hostname: {systemInfo?.network?.hostname || 'Unknown'}</p>
<p> IP Address: {systemInfo?.network?.private_ips?.[0] || 'Unknown'}</p>
{config.mode === 'create' && <p> Cluster: {config.clusterName}</p>}
{config.encryption && <p> Security: Encrypted communication enabled</p>}
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button
type="submit"
disabled={
(config.mode === 'create' && (!config.clusterName || !config.networkId || !clusterKey)) ||
(config.mode === 'join' && (!config.targetNode || !config.joinKey))
}
className="btn-primary"
>
{isCompleted ? 'Continue' : 'Next: Testing & Validation'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,302 @@
'use client'
import { useState } from 'react'
import {
KeyIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
UserIcon,
DocumentTextIcon
} from '@heroicons/react/24/outline'
interface LicenseValidationProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
interface LicenseData {
email: string
licenseKey: string
organizationName?: string
acceptedAt?: string
}
export default function LicenseValidation({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: LicenseValidationProps) {
const [licenseData, setLicenseData] = useState<LicenseData>({
email: configData?.license?.email || '',
licenseKey: configData?.license?.licenseKey || '',
organizationName: configData?.license?.organizationName || ''
})
const [validating, setValidating] = useState(false)
const [validationResult, setValidationResult] = useState<{
valid: boolean
message: string
details?: any
} | null>(null)
const [error, setError] = useState('')
// Email validation function
const isValidEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
// Check if form is ready for validation
const canValidate = licenseData.email &&
isValidEmail(licenseData.email) &&
licenseData.licenseKey
const validateLicense = async () => {
if (!licenseData.email || !licenseData.licenseKey) {
setError('Both email and license key are required')
return
}
if (!isValidEmail(licenseData.email)) {
setError('Please enter a valid email address')
return
}
setValidating(true)
setError('')
setValidationResult(null)
try {
const response = await fetch('/api/setup/license/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: licenseData.email,
licenseKey: licenseData.licenseKey,
organizationName: licenseData.organizationName
}),
})
const result = await response.json()
if (response.ok && result.valid) {
setValidationResult({
valid: true,
message: result.message || 'License validated successfully',
details: result.details
})
} else {
setValidationResult({
valid: false,
message: result.message || 'License validation failed',
details: result.details
})
}
} catch (error) {
console.error('License validation error:', error)
setValidationResult({
valid: false,
message: 'Failed to validate license. Please check your connection and try again.'
})
} finally {
setValidating(false)
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!licenseData.email || !licenseData.licenseKey) {
setError('Both email and license key are required')
return
}
if (!validationResult?.valid) {
setError('Please validate your license before continuing')
return
}
setError('')
onComplete({
license: {
...licenseData,
validatedAt: new Date().toISOString(),
validationDetails: validationResult.details
}
})
}
return (
<form onSubmit={handleSubmit} className="space-y-8">
{/* License Information */}
<div className="card">
<div className="flex items-center mb-4">
<KeyIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">License Information</h3>
{validationResult?.valid && <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 ml-2" />}
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<div className="relative">
<UserIcon className="h-5 w-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
<input
type="email"
value={licenseData.email}
onChange={(e) => setLicenseData(prev => ({ ...prev, email: e.target.value }))}
placeholder="your-email@company.com"
className={`w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary ${
licenseData.email && !isValidEmail(licenseData.email)
? 'border-red-300 bg-red-50'
: 'border-gray-300'
}`}
required
/>
</div>
{licenseData.email && !isValidEmail(licenseData.email) ? (
<p className="text-sm text-red-600 mt-1">Please enter a valid email address</p>
) : (
<p className="text-sm text-gray-500 mt-1">
The email address associated with your CHORUS:agents license
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
License Key
</label>
<div className="relative">
<KeyIcon className="h-5 w-5 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" />
<input
type="text"
value={licenseData.licenseKey}
onChange={(e) => setLicenseData(prev => ({ ...prev, licenseKey: e.target.value }))}
placeholder="BZZZ-XXXX-XXXX-XXXX-XXXX"
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary font-mono"
required
/>
</div>
<p className="text-sm text-gray-500 mt-1">
Your unique CHORUS:agents license key (found in your purchase confirmation email).
Validation is powered by KACHING license authority.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Organization Name (Optional)
</label>
<input
type="text"
value={licenseData.organizationName}
onChange={(e) => setLicenseData(prev => ({ ...prev, organizationName: e.target.value }))}
placeholder="Your Company Name"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
/>
<p className="text-sm text-gray-500 mt-1">
Optional: Organization name for license tracking
</p>
</div>
<button
type="button"
onClick={validateLicense}
disabled={validating || !canValidate}
className={`w-full py-3 px-4 rounded-lg font-medium transition-colors ${
validating || !canValidate
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-bzzz-primary text-white hover:bg-bzzz-primary-dark'
}`}
>
{validating ? 'Validating License...' : 'Validate License'}
</button>
</div>
</div>
{/* Validation Result */}
{validationResult && (
<div className={`panel ${validationResult.valid ? 'panel-success' : 'panel-error'}`}>
<div className="flex items-start">
<div className="flex-shrink-0">
{validationResult.valid ? (
<CheckCircleIcon className="h-6 w-6 text-eucalyptus-600 dark:text-eucalyptus-50" />
) : (
<ExclamationTriangleIcon className="h-6 w-6 text-coral-950 dark:text-coral-50" />
)}
</div>
<div className="ml-3">
<h4 className={`text-sm font-medium panel-title`}>
{validationResult.valid ? 'License Valid' : 'License Invalid'}
</h4>
<p className={`text-sm mt-1 panel-body`}>
{validationResult.message}
</p>
{validationResult.valid && validationResult.details && (
<div className="mt-3 text-sm panel-body">
<p><strong>License Type:</strong> {validationResult.details.licenseType || 'Standard'}</p>
<p><strong>Max Nodes:</strong> {validationResult.details.maxNodes || 'Unlimited'}</p>
<p><strong>Expires:</strong> {validationResult.details.expiresAt || 'Never'}</p>
</div>
)}
</div>
</div>
</div>
)}
{error && (
<div className="flex items-center text-red-600 text-sm">
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
{error}
</div>
)}
{/* Need a License Panel */}
<div className="rounded-lg p-4 border bg-chorus-warm border-chorus-border-subtle dark:bg-mulberry-900 dark:border-chorus-border-defined">
<div className="flex items-start">
<DocumentTextIcon className="h-5 w-5 text-chorus-text-primary mt-0.5 mr-2 opacity-80" />
<div className="text-sm">
<h4 className="font-medium text-chorus-text-primary mb-1">Need a License?</h4>
<p className="text-chorus-text-secondary">
If you don't have a CHORUS:agents license yet, you can:
</p>
<ul className="text-chorus-text-secondary mt-1 space-y-1 ml-4">
<li>• Visit <a href="https://chorus.services/bzzz" target="_blank" className="underline hover:no-underline text-chorus-text-primary">chorus.services/bzzz</a> to purchase a license</li>
<li>• Contact our sales team at <a href="mailto:sales@chorus.services" className="underline hover:no-underline text-chorus-text-primary">sales@chorus.services</a></li>
<li>• Request a trial license for evaluation purposes</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button
type="submit"
disabled={!validationResult?.valid}
className={`${validationResult?.valid ? 'btn-primary' : 'btn-disabled'}`}
>
{isCompleted ? 'Continue' : 'Next: System Detection'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,414 @@
'use client'
import { useState, useEffect } from 'react'
import {
GlobeAltIcon,
ServerIcon,
ShieldCheckIcon,
ExclamationTriangleIcon,
CheckCircleIcon,
InformationCircleIcon
} from '@heroicons/react/24/outline'
interface NetworkInterface {
name: string
ip: string
status: string
speed?: string
}
interface NetworkConfig {
primaryInterface: string
primaryIP: string
bzzzPort: number
mcpPort: number
webUIPort: number
p2pPort: number
autoFirewall: boolean
allowedIPs: string[]
dnsServers: string[]
}
interface NetworkConfigurationProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function NetworkConfiguration({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: NetworkConfigurationProps) {
const [config, setConfig] = useState<NetworkConfig>({
primaryInterface: '',
primaryIP: '',
bzzzPort: 8080,
mcpPort: 3000,
webUIPort: 8080,
p2pPort: 7000,
autoFirewall: true,
allowedIPs: ['192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12'],
dnsServers: ['8.8.8.8', '8.8.4.4']
})
const [errors, setErrors] = useState<string[]>([])
const [portConflicts, setPortConflicts] = useState<string[]>([])
// Initialize with system info and existing config
useEffect(() => {
if (systemInfo?.network) {
setConfig(prev => ({
...prev,
primaryInterface: systemInfo.network.interfaces?.[0] || prev.primaryInterface,
primaryIP: systemInfo.network.private_ips?.[0] || prev.primaryIP
}))
}
if (configData.network) {
setConfig(prev => ({ ...prev, ...configData.network }))
}
}, [systemInfo, configData])
// Validate configuration
useEffect(() => {
validateConfiguration()
}, [config])
const validateConfiguration = () => {
const newErrors: string[] = []
const conflicts: string[] = []
// Check for port conflicts
const ports = [config.bzzzPort, config.mcpPort, config.webUIPort, config.p2pPort]
const uniquePorts = new Set(ports)
if (uniquePorts.size !== ports.length) {
conflicts.push('Port numbers must be unique')
}
// Check port ranges
ports.forEach((port, index) => {
const portNames = ['BZZZ API', 'MCP Server', 'Web UI', 'P2P Network']
if (port < 1024) {
newErrors.push(`${portNames[index]} port should be above 1024 to avoid requiring root privileges`)
}
if (port > 65535) {
newErrors.push(`${portNames[index]} port must be below 65536`)
}
})
// Validate IP addresses in allowed IPs
config.allowedIPs.forEach(ip => {
if (ip && !isValidCIDR(ip)) {
newErrors.push(`Invalid CIDR notation: ${ip}`)
}
})
// Validate DNS servers
config.dnsServers.forEach(dns => {
if (dns && !isValidIPAddress(dns)) {
newErrors.push(`Invalid DNS server IP: ${dns}`)
}
})
setErrors(newErrors)
setPortConflicts(conflicts)
}
const isValidCIDR = (cidr: string): boolean => {
const regex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/
return regex.test(cidr)
}
const isValidIPAddress = (ip: string): boolean => {
const regex = /^(\d{1,3}\.){3}\d{1,3}$/
if (!regex.test(ip)) return false
return ip.split('.').every(part => parseInt(part) >= 0 && parseInt(part) <= 255)
}
const handlePortChange = (field: keyof NetworkConfig, value: string) => {
const numValue = parseInt(value) || 0
setConfig(prev => ({ ...prev, [field]: numValue }))
}
const handleArrayChange = (field: 'allowedIPs' | 'dnsServers', index: number, value: string) => {
setConfig(prev => ({
...prev,
[field]: prev[field].map((item, i) => i === index ? value : item)
}))
}
const addArrayItem = (field: 'allowedIPs' | 'dnsServers') => {
setConfig(prev => ({
...prev,
[field]: [...prev[field], '']
}))
}
const removeArrayItem = (field: 'allowedIPs' | 'dnsServers', index: number) => {
setConfig(prev => ({
...prev,
[field]: prev[field].filter((_, i) => i !== index)
}))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (errors.length === 0 && portConflicts.length === 0) {
onComplete({ network: config })
}
}
const isFormValid = errors.length === 0 && portConflicts.length === 0
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Network Interface Selection */}
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<GlobeAltIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Network Interface
</h3>
{systemInfo?.network?.interfaces && (
<div className="space-y-3">
<label className="label">Primary Network Interface</label>
<select
value={config.primaryInterface}
onChange={(e) => setConfig(prev => ({ ...prev, primaryInterface: e.target.value }))}
className="input-field"
>
<option value="">Select network interface</option>
{systemInfo.network.interfaces.map((interfaceName: string, index: number) => (
<option key={index} value={interfaceName}>
{interfaceName} - {systemInfo.network.private_ips[index] || 'Unknown IP'}
</option>
))}
</select>
{config.primaryInterface && (
<div className="text-sm text-gray-600">
Primary IP: {systemInfo.network.private_ips?.[systemInfo.network.interfaces.indexOf(config.primaryInterface)] || 'Unknown'}
</div>
)}
</div>
)}
</div>
{/* Port Configuration */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Port Configuration
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="label">BZZZ API Port</label>
<input
type="number"
value={config.bzzzPort}
onChange={(e) => handlePortChange('bzzzPort', e.target.value)}
min="1024"
max="65535"
className="input-field"
/>
<p className="text-sm text-gray-600 mt-1">Main BZZZ HTTP API endpoint</p>
</div>
<div>
<label className="label">MCP Server Port</label>
<input
type="number"
value={config.mcpPort}
onChange={(e) => handlePortChange('mcpPort', e.target.value)}
min="1024"
max="65535"
className="input-field"
/>
<p className="text-sm text-gray-600 mt-1">Model Context Protocol server</p>
</div>
<div>
<label className="label">Web UI Port</label>
<input
type="number"
value={config.webUIPort}
onChange={(e) => handlePortChange('webUIPort', e.target.value)}
min="1024"
max="65535"
className="input-field"
/>
<p className="text-sm text-gray-600 mt-1">Web interface port</p>
</div>
<div>
<label className="label">P2P Network Port</label>
<input
type="number"
value={config.p2pPort}
onChange={(e) => handlePortChange('p2pPort', e.target.value)}
min="1024"
max="65535"
className="input-field"
/>
<p className="text-sm text-gray-600 mt-1">Peer-to-peer communication</p>
</div>
</div>
{portConflicts.length > 0 && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-center">
<ExclamationTriangleIcon className="h-5 w-5 text-red-600 mr-2" />
<span className="text-red-800 font-medium">Port Conflicts</span>
</div>
{portConflicts.map((conflict, index) => (
<p key={index} className="text-red-700 text-sm mt-1">{conflict}</p>
))}
</div>
)}
</div>
{/* Security & Access Control */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<ShieldCheckIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Security & Access Control
</h3>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
id="autoFirewall"
checked={config.autoFirewall}
onChange={(e) => setConfig(prev => ({ ...prev, autoFirewall: e.target.checked }))}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
<label htmlFor="autoFirewall" className="ml-2 text-sm font-medium text-gray-700">
Automatically configure firewall rules
</label>
</div>
<div>
<label className="label">Allowed IP Ranges (CIDR)</label>
{config.allowedIPs.map((ip, index) => (
<div key={index} className="flex items-center space-x-2 mb-2">
<input
type="text"
value={ip}
onChange={(e) => handleArrayChange('allowedIPs', index, e.target.value)}
placeholder="192.168.1.0/24"
className="input-field flex-1"
/>
<button
type="button"
onClick={() => removeArrayItem('allowedIPs', index)}
className="text-red-600 hover:text-red-800"
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => addArrayItem('allowedIPs')}
className="text-bzzz-primary hover:text-bzzz-primary/80 text-sm"
>
+ Add IP Range
</button>
</div>
</div>
</div>
{/* DNS Configuration */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">DNS Configuration</h3>
<div>
<label className="label">DNS Servers</label>
{config.dnsServers.map((dns, index) => (
<div key={index} className="flex items-center space-x-2 mb-2">
<input
type="text"
value={dns}
onChange={(e) => handleArrayChange('dnsServers', index, e.target.value)}
placeholder="8.8.8.8"
className="input-field flex-1"
/>
<button
type="button"
onClick={() => removeArrayItem('dnsServers', index)}
className="text-red-600 hover:text-red-800"
>
Remove
</button>
</div>
))}
<button
type="button"
onClick={() => addArrayItem('dnsServers')}
className="text-bzzz-primary hover:text-bzzz-primary/80 text-sm"
>
+ Add DNS Server
</button>
</div>
</div>
{/* Validation Errors */}
{errors.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center mb-2">
<ExclamationTriangleIcon className="h-5 w-5 text-red-600 mr-2" />
<span className="text-red-800 font-medium">Configuration Issues</span>
</div>
{errors.map((error, index) => (
<p key={index} className="text-red-700 text-sm">{error}</p>
))}
</div>
)}
{/* Configuration Summary */}
{isFormValid && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-center mb-2">
<InformationCircleIcon className="h-5 w-5 text-blue-600 mr-2" />
<span className="text-blue-800 font-medium">Configuration Summary</span>
</div>
<div className="text-blue-700 text-sm space-y-1">
<p> Primary interface: {config.primaryInterface}</p>
<p> BZZZ API will be available on port {config.bzzzPort}</p>
<p> MCP server will run on port {config.mcpPort}</p>
<p> Web UI will be accessible on port {config.webUIPort}</p>
<p> P2P network will use port {config.p2pPort}</p>
{config.autoFirewall && <p> Firewall rules will be configured automatically</p>}
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button
type="submit"
disabled={!isFormValid}
className="btn-primary"
>
{isCompleted ? 'Continue' : 'Next: Security Setup'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,414 @@
'use client'
import { useState, useEffect } from 'react'
import {
CodeBracketIcon,
CheckCircleIcon,
XCircleIcon,
ArrowPathIcon,
ExclamationTriangleIcon,
EyeIcon,
EyeSlashIcon
} from '@heroicons/react/24/outline'
interface RepositoryProvider {
name: string
displayName: string
description: string
requiresBaseURL: boolean
defaultBaseURL?: string
}
interface RepositoryConfig {
provider: string
baseURL: string
accessToken: string
owner: string
repository: string
}
interface ValidationResult {
valid: boolean
message?: string
error?: string
}
interface RepositoryConfigurationProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function RepositoryConfiguration({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: RepositoryConfigurationProps) {
const [providers, setProviders] = useState<RepositoryProvider[]>([])
const [config, setConfig] = useState<RepositoryConfig>({
provider: '',
baseURL: '',
accessToken: '',
owner: '',
repository: ''
})
const [validation, setValidation] = useState<ValidationResult | null>(null)
const [validating, setValidating] = useState(false)
const [showToken, setShowToken] = useState(false)
const [loadingProviders, setLoadingProviders] = useState(true)
// Load existing config from configData if available
useEffect(() => {
if (configData.repository) {
setConfig({ ...configData.repository })
}
}, [configData])
// Load supported providers
useEffect(() => {
loadProviders()
}, [])
const loadProviders = async () => {
try {
const response = await fetch('/api/setup/repository/providers')
if (response.ok) {
const result = await response.json()
const providerList = result.providers || []
// Map provider names to full provider objects
const providersData: RepositoryProvider[] = providerList.map((name: string) => {
switch (name.toLowerCase()) {
case 'gitea':
return {
name: 'gitea',
displayName: 'Gitea',
description: 'Self-hosted Git service with issue tracking',
requiresBaseURL: true,
defaultBaseURL: 'http://gitea.local'
}
case 'github':
return {
name: 'github',
displayName: 'GitHub',
description: 'Cloud-based Git repository hosting service',
requiresBaseURL: false,
defaultBaseURL: 'https://api.github.com'
}
default:
return {
name: name.toLowerCase(),
displayName: name,
description: 'Git repository service',
requiresBaseURL: true
}
}
})
setProviders(providersData)
// Set default provider if none selected
if (!config.provider && providersData.length > 0) {
const defaultProvider = providersData.find(p => p.name === 'gitea') || providersData[0]
handleProviderChange(defaultProvider.name)
}
}
} catch (error) {
console.error('Failed to load providers:', error)
} finally {
setLoadingProviders(false)
}
}
const handleProviderChange = (provider: string) => {
const providerData = providers.find(p => p.name === provider)
setConfig(prev => ({
...prev,
provider,
baseURL: providerData?.defaultBaseURL || prev.baseURL
}))
setValidation(null)
}
const handleInputChange = (field: keyof RepositoryConfig, value: string) => {
setConfig(prev => ({ ...prev, [field]: value }))
setValidation(null)
}
const validateRepository = async () => {
if (!config.provider || !config.accessToken || !config.owner || !config.repository) {
setValidation({
valid: false,
error: 'Please fill in all required fields'
})
return
}
setValidating(true)
setValidation(null)
try {
const response = await fetch('/api/setup/repository/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
})
const result = await response.json()
if (response.ok && result.valid) {
setValidation({
valid: true,
message: result.message || 'Repository connection successful'
})
} else {
setValidation({
valid: false,
error: result.error || 'Validation failed'
})
}
} catch (error) {
setValidation({
valid: false,
error: 'Network error: Unable to validate repository'
})
} finally {
setValidating(false)
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validation?.valid) {
onComplete({ repository: config })
} else {
validateRepository()
}
}
const selectedProvider = providers.find(p => p.name === config.provider)
const isFormValid = config.provider && config.accessToken && config.owner && config.repository &&
(!selectedProvider?.requiresBaseURL || config.baseURL)
if (loadingProviders) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<ArrowPathIcon className="h-8 w-8 text-bzzz-primary animate-spin mx-auto mb-4" />
<p className="text-gray-600">Loading repository providers...</p>
</div>
</div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Repository Provider Selection */}
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<CodeBracketIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Repository Provider
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{providers.map((provider) => (
<div
key={provider.name}
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
config.provider === provider.name
? 'border-bzzz-primary bg-bzzz-primary bg-opacity-10'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => handleProviderChange(provider.name)}
>
<div className="flex items-center">
<input
type="radio"
name="provider"
value={provider.name}
checked={config.provider === provider.name}
onChange={() => handleProviderChange(provider.name)}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300"
/>
<div className="ml-3">
<div className="font-medium text-gray-900">{provider.displayName}</div>
<div className="text-sm text-gray-600">{provider.description}</div>
</div>
</div>
</div>
))}
</div>
</div>
{/* Configuration Form */}
{config.provider && (
<div className="space-y-6">
{/* Base URL (for providers that require it) */}
{selectedProvider?.requiresBaseURL && (
<div>
<label className="label">
Base URL *
</label>
<input
type="url"
value={config.baseURL}
onChange={(e) => handleInputChange('baseURL', e.target.value)}
placeholder={`e.g., ${selectedProvider.defaultBaseURL || 'https://git.example.com'}`}
className="input-field"
required
/>
<p className="text-sm text-gray-600 mt-1">
The base URL for your {selectedProvider.displayName} instance
</p>
</div>
)}
{/* Access Token */}
<div>
<label className="label">
Access Token *
</label>
<div className="relative">
<input
type={showToken ? 'text' : 'password'}
value={config.accessToken}
onChange={(e) => handleInputChange('accessToken', e.target.value)}
placeholder={`Your ${selectedProvider?.displayName} access token`}
className="input-field pr-10"
required
/>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showToken ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
<p className="text-sm text-gray-600 mt-1">
{selectedProvider?.name === 'github'
? 'Generate a personal access token with repo and admin:repo_hook permissions'
: 'Generate an access token with repository read/write permissions'
}
</p>
</div>
{/* Owner/Organization */}
<div>
<label className="label">
Owner/Organization *
</label>
<input
type="text"
value={config.owner}
onChange={(e) => handleInputChange('owner', e.target.value)}
placeholder="username or organization"
className="input-field"
required
/>
<p className="text-sm text-gray-600 mt-1">
The username or organization that owns the repository
</p>
</div>
{/* Repository Name */}
<div>
<label className="label">
Repository Name *
</label>
<input
type="text"
value={config.repository}
onChange={(e) => handleInputChange('repository', e.target.value)}
placeholder="repository-name"
className="input-field"
required
/>
<p className="text-sm text-gray-600 mt-1">
The name of the repository for task management
</p>
</div>
{/* Validation Section */}
<div className="bg-white border border-gray-200 rounded-lg p-6">
<h4 className="text-md font-medium text-gray-900 mb-3">Connection Test</h4>
{validation && (
<div className={`flex items-center p-3 rounded-lg mb-4 ${
validation.valid
? 'bg-eucalyptus-50 border border-eucalyptus-950'
: 'bg-red-50 border border-red-200'
}`}>
{validation.valid ? (
<CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 mr-2" />
) : (
<XCircleIcon className="h-5 w-5 text-red-600 mr-2" />
)}
<span className={`text-sm ${
validation.valid ? 'text-eucalyptus-600' : 'text-red-800'
}`}>
{validation.valid ? validation.message : validation.error}
</span>
</div>
)}
<button
type="button"
onClick={validateRepository}
disabled={!isFormValid || validating}
className="btn-outline w-full sm:w-auto"
>
{validating ? (
<>
<ArrowPathIcon className="h-4 w-4 animate-spin mr-2" />
Testing Connection...
</>
) : (
'Test Repository Connection'
)}
</button>
{!isFormValid && (
<p className="text-sm text-gray-600 mt-2">
Please fill in all required fields to test the connection
</p>
)}
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button
type="submit"
disabled={!validation?.valid}
className="btn-primary"
>
{validation?.valid
? (isCompleted ? 'Continue' : 'Next: Network Configuration')
: 'Validate & Continue'
}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,61 @@
'use client'
import { useState } from 'react'
interface ResourceAllocationProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function ResourceAllocation({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: ResourceAllocationProps) {
const [config, setConfig] = useState({
cpuAllocation: 80,
memoryAllocation: 75,
storageAllocation: 50
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onComplete({ resources: config })
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">
Resource Allocation
</h3>
<p className="text-gray-600">
Allocate CPU, memory, and storage resources for BZZZ services.
</p>
<div className="mt-8">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-yellow-800">
This component is under development. Resource allocation will be implemented here.
</div>
</div>
</div>
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button type="submit" className="btn-primary">
{isCompleted ? 'Continue' : 'Next: Service Deployment'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,683 @@
'use client'
import { useState, useEffect } from 'react'
import {
ShieldCheckIcon,
KeyIcon,
LockClosedIcon,
ServerIcon,
EyeIcon,
EyeSlashIcon,
DocumentDuplicateIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/outline'
interface SecuritySetupProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
interface SecurityConfig {
sshKeyType: 'generate' | 'existing' | 'manual'
sshPublicKey: string
sshPrivateKey: string
sshUsername: string
sshPassword: string
sshPort: number
enableTLS: boolean
tlsCertType: 'self-signed' | 'letsencrypt' | 'existing'
tlsCertPath: string
tlsKeyPath: string
authMethod: 'token' | 'certificate' | 'hybrid'
clusterSecret: string
accessPolicy: 'open' | 'restricted' | 'invite-only'
enableFirewall: boolean
allowedPorts: string[]
trustedIPs: string[]
}
export default function SecuritySetup({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: SecuritySetupProps) {
console.log('SecuritySetup: Component rendered with configData:', configData)
const [config, setConfig] = useState<SecurityConfig>({
sshKeyType: 'generate',
sshPublicKey: '',
sshPrivateKey: '',
sshUsername: 'ubuntu',
sshPassword: '',
sshPort: 22,
enableTLS: true,
tlsCertType: 'self-signed',
tlsCertPath: '',
tlsKeyPath: '',
authMethod: 'token',
clusterSecret: '',
accessPolicy: 'restricted',
enableFirewall: true,
allowedPorts: ['22', '8080', '8090', '9100', '3000'],
trustedIPs: [],
...configData?.security // Load saved security config if exists
})
const [showPrivateKey, setShowPrivateKey] = useState(false)
const [showClusterSecret, setShowClusterSecret] = useState(false)
const [showSSHPassword, setShowSSHPassword] = useState(false)
const [generating, setGenerating] = useState(false)
const [validation, setValidation] = useState<{[key: string]: boolean}>({})
const [portsInitialized, setPortsInitialized] = useState(false)
// Generate cluster secret on mount if not exists
useEffect(() => {
if (!config.clusterSecret) {
generateClusterSecret()
}
}, [])
// Update firewall ports based on network configuration from previous step
useEffect(() => {
console.log('SecuritySetup: configData changed', {
hasNetwork: !!configData?.network,
portsInitialized,
hasSavedSecurity: !!configData?.security?.allowedPorts,
networkConfig: configData?.network
})
// If we have network config and haven't initialized ports yet, AND we don't have saved security config
if (configData?.network && !portsInitialized && !configData?.security?.allowedPorts) {
const networkConfig = configData.network
const networkPorts = [
networkConfig.bzzzPort?.toString(),
networkConfig.mcpPort?.toString(),
networkConfig.webUIPort?.toString(),
networkConfig.p2pPort?.toString()
].filter(port => port && port !== 'undefined')
console.log('SecuritySetup: Auto-populating ports', { networkPorts, networkConfig })
// Include standard ports plus network configuration ports
const standardPorts = ['22', '8090'] // SSH and setup interface
const allPorts = [...new Set([...standardPorts, ...networkPorts])]
console.log('SecuritySetup: Setting allowed ports to', allPorts)
setConfig(prev => ({ ...prev, allowedPorts: allPorts }))
setPortsInitialized(true)
}
}, [configData, portsInitialized])
const generateClusterSecret = () => {
const secret = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
setConfig(prev => ({ ...prev, clusterSecret: secret }))
}
const generateSSHKeys = async () => {
setGenerating(true)
try {
// In a real implementation, this would call the backend to generate SSH keys
// For now, simulate the process
await new Promise(resolve => setTimeout(resolve, 2000))
// Mock generated keys (in real implementation, these would come from backend)
const mockPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC... chorus@${systemInfo?.network?.hostname || 'localhost'}`
const mockPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAFwwAAAAd...
-----END OPENSSH PRIVATE KEY-----`
setConfig(prev => ({
...prev,
sshPublicKey: mockPublicKey,
sshPrivateKey: mockPrivateKey
}))
setValidation(prev => ({ ...prev, sshKeys: true }))
} catch (error) {
console.error('Failed to generate SSH keys:', error)
setValidation(prev => ({ ...prev, sshKeys: false }))
} finally {
setGenerating(false)
}
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
} catch (error) {
console.error('Failed to copy to clipboard:', error)
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Validate required fields
const newValidation: {[key: string]: boolean} = {}
if (config.sshKeyType === 'generate' && !config.sshPublicKey) {
newValidation.sshKeys = false
} else if (config.sshKeyType === 'existing' && !config.sshPublicKey) {
newValidation.sshKeys = false
} else {
newValidation.sshKeys = true
}
if (config.enableTLS && config.tlsCertType === 'existing' && (!config.tlsCertPath || !config.tlsKeyPath)) {
newValidation.tlsCert = false
} else {
newValidation.tlsCert = true
}
if (!config.clusterSecret) {
newValidation.clusterSecret = false
} else {
newValidation.clusterSecret = true
}
if (config.sshKeyType === 'manual' && (!config.sshUsername || !config.sshPassword)) {
newValidation.sshCredentials = false
} else {
newValidation.sshCredentials = true
}
setValidation(newValidation)
// Check if all validations pass
const isValid = Object.values(newValidation).every(v => v)
if (isValid) {
onComplete({ security: config })
}
}
return (
<form onSubmit={handleSubmit} className="space-y-8">
{/* SSH Key Configuration */}
<div className="card">
<div className="flex items-center mb-4">
<KeyIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">SSH Key Management</h3>
{validation.sshKeys === true && <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 ml-2" />}
{validation.sshKeys === false && <XCircleIcon className="h-5 w-5 text-red-500 ml-2" />}
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">SSH Key Type</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
value="generate"
checked={config.sshKeyType === 'generate'}
onChange={(e) => setConfig(prev => ({ ...prev, sshKeyType: e.target.value as any }))}
className="mr-2"
/>
Generate new SSH key pair
</label>
<label className="flex items-center">
<input
type="radio"
value="existing"
checked={config.sshKeyType === 'existing'}
onChange={(e) => setConfig(prev => ({ ...prev, sshKeyType: e.target.value as any }))}
className="mr-2"
/>
Use existing SSH key
</label>
<label className="flex items-center">
<input
type="radio"
value="manual"
checked={config.sshKeyType === 'manual'}
onChange={(e) => setConfig(prev => ({ ...prev, sshKeyType: e.target.value as any }))}
className="mr-2"
/>
Configure manually with SSH username/password
</label>
</div>
</div>
{config.sshKeyType === 'generate' && (
<div className="space-y-4">
{!config.sshPublicKey ? (
<button
type="button"
onClick={generateSSHKeys}
disabled={generating}
className="btn-primary"
>
{generating ? 'Generating Keys...' : 'Generate SSH Key Pair'}
</button>
) : (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Public Key</label>
<div className="relative">
<textarea
value={config.sshPublicKey}
readOnly
className="w-full p-3 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
rows={3}
/>
<button
type="button"
onClick={() => copyToClipboard(config.sshPublicKey)}
className="absolute top-2 right-2 p-1 text-gray-500 hover:text-gray-700"
>
<DocumentDuplicateIcon className="h-4 w-4" />
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Private Key</label>
<div className="relative">
<textarea
value={showPrivateKey ? config.sshPrivateKey : '••••••••••••••••••••••••••••••••'}
readOnly
className="w-full p-3 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
rows={6}
/>
<div className="absolute top-2 right-2 flex space-x-1">
<button
type="button"
onClick={() => setShowPrivateKey(!showPrivateKey)}
className="p-1 text-gray-500 hover:text-gray-700"
>
{showPrivateKey ? <EyeSlashIcon className="h-4 w-4" /> : <EyeIcon className="h-4 w-4" />}
</button>
<button
type="button"
onClick={() => copyToClipboard(config.sshPrivateKey)}
className="p-1 text-gray-500 hover:text-gray-700"
>
<DocumentDuplicateIcon className="h-4 w-4" />
</button>
</div>
</div>
<p className="text-sm text-yellow-600 mt-1"> Store this private key securely. It cannot be recovered.</p>
</div>
</div>
)}
</div>
)}
{config.sshKeyType === 'existing' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">SSH Public Key</label>
<textarea
value={config.sshPublicKey}
onChange={(e) => setConfig(prev => ({ ...prev, sshPublicKey: e.target.value }))}
placeholder="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC..."
className="w-full p-3 border border-gray-300 rounded-lg font-mono text-sm"
rows={3}
/>
</div>
)}
{config.sshKeyType === 'manual' && (
<div className="space-y-4">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-600 mt-0.5" />
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-yellow-800">Manual SSH Configuration</h4>
<p className="text-sm text-yellow-700 mt-1">
Provide SSH credentials for cluster machines. SSH keys will be automatically generated and deployed using these credentials.
<strong> Passwords are only used during setup and are not stored.</strong>
</p>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
SSH Username <span className="text-red-500">*</span>
</label>
<input
type="text"
value={config.sshUsername}
onChange={(e) => setConfig(prev => ({ ...prev, sshUsername: e.target.value }))}
placeholder="ubuntu"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
required
/>
<p className="text-sm text-gray-500 mt-1">
Exact SSH username for cluster machines
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
SSH Port
</label>
<input
type="number"
value={config.sshPort}
onChange={(e) => setConfig(prev => ({ ...prev, sshPort: parseInt(e.target.value) || 22 }))}
min="1"
max="65535"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
/>
<p className="text-sm text-gray-500 mt-1">
SSH port number (default: 22)
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
SSH Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type={showSSHPassword ? 'text' : 'password'}
value={config.sshPassword}
onChange={(e) => setConfig(prev => ({ ...prev, sshPassword: e.target.value }))}
placeholder="Enter SSH password for cluster machines"
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-bzzz-primary focus:border-bzzz-primary"
required
/>
<button
type="button"
onClick={() => setShowSSHPassword(!showSSHPassword)}
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showSSHPassword ? (
<EyeSlashIcon className="h-4 w-4 text-gray-400" />
) : (
<EyeIcon className="h-4 w-4 text-gray-400" />
)}
</button>
</div>
<p className="text-sm text-gray-500 mt-1">
SSH password for the specified username (used only during setup)
</p>
</div>
</div>
)}
</div>
</div>
{/* TLS/SSL Configuration */}
<div className="card">
<div className="flex items-center mb-4">
<LockClosedIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">TLS/SSL Configuration</h3>
{validation.tlsCert === true && <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600 ml-2" />}
{validation.tlsCert === false && <XCircleIcon className="h-5 w-5 text-red-500 ml-2" />}
</div>
<div className="space-y-4">
<label className="flex items-center">
<input
type="checkbox"
checked={config.enableTLS}
onChange={(e) => setConfig(prev => ({ ...prev, enableTLS: e.target.checked }))}
className="mr-2"
/>
Enable TLS encryption for cluster communication
</label>
{config.enableTLS && (
<div className="space-y-4 ml-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Certificate Type</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
value="self-signed"
checked={config.tlsCertType === 'self-signed'}
onChange={(e) => setConfig(prev => ({ ...prev, tlsCertType: e.target.value as any }))}
className="mr-2"
/>
Generate self-signed certificate
</label>
<label className="flex items-center">
<input
type="radio"
value="letsencrypt"
checked={config.tlsCertType === 'letsencrypt'}
onChange={(e) => setConfig(prev => ({ ...prev, tlsCertType: e.target.value as any }))}
className="mr-2"
/>
Use Let's Encrypt (requires domain)
</label>
<label className="flex items-center">
<input
type="radio"
value="existing"
checked={config.tlsCertType === 'existing'}
onChange={(e) => setConfig(prev => ({ ...prev, tlsCertType: e.target.value as any }))}
className="mr-2"
/>
Use existing certificate
</label>
</div>
</div>
{config.tlsCertType === 'existing' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Certificate Path</label>
<input
type="text"
value={config.tlsCertPath}
onChange={(e) => setConfig(prev => ({ ...prev, tlsCertPath: e.target.value }))}
placeholder="/path/to/certificate.crt"
className="w-full p-3 border border-gray-300 rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Private Key Path</label>
<input
type="text"
value={config.tlsKeyPath}
onChange={(e) => setConfig(prev => ({ ...prev, tlsKeyPath: e.target.value }))}
placeholder="/path/to/private.key"
className="w-full p-3 border border-gray-300 rounded-lg"
/>
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* Authentication Method */}
<div className="card">
<div className="flex items-center mb-4">
<ShieldCheckIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">Authentication Method</h3>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Authentication Type</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
value="token"
checked={config.authMethod === 'token'}
onChange={(e) => setConfig(prev => ({ ...prev, authMethod: e.target.value as any }))}
className="mr-2"
/>
API Token-based authentication
</label>
<label className="flex items-center">
<input
type="radio"
value="certificate"
checked={config.authMethod === 'certificate'}
onChange={(e) => setConfig(prev => ({ ...prev, authMethod: e.target.value as any }))}
className="mr-2"
/>
Certificate-based authentication
</label>
<label className="flex items-center">
<input
type="radio"
value="hybrid"
checked={config.authMethod === 'hybrid'}
onChange={(e) => setConfig(prev => ({ ...prev, authMethod: e.target.value as any }))}
className="mr-2"
/>
Hybrid (Token + Certificate)
</label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Cluster Secret</label>
<div className="relative">
<input
type={showClusterSecret ? "text" : "password"}
value={config.clusterSecret}
onChange={(e) => setConfig(prev => ({ ...prev, clusterSecret: e.target.value }))}
className="w-full p-3 border border-gray-300 rounded-lg font-mono"
placeholder="Cluster authentication secret"
/>
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex space-x-1">
<button
type="button"
onClick={() => setShowClusterSecret(!showClusterSecret)}
className="p-1 text-gray-500 hover:text-gray-700"
>
{showClusterSecret ? <EyeSlashIcon className="h-4 w-4" /> : <EyeIcon className="h-4 w-4" />}
</button>
<button
type="button"
onClick={generateClusterSecret}
className="p-1 text-gray-500 hover:text-gray-700"
>
<KeyIcon className="h-4 w-4" />
</button>
</div>
</div>
{validation.clusterSecret === false && (
<p className="text-sm text-red-600 mt-1">Cluster secret is required</p>
)}
</div>
</div>
</div>
{/* Access Control */}
<div className="card">
<div className="flex items-center mb-4">
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="text-lg font-medium text-gray-900">Access Control</h3>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Access Policy</label>
<select
value={config.accessPolicy}
onChange={(e) => setConfig(prev => ({ ...prev, accessPolicy: e.target.value as any }))}
className="w-full p-3 border border-gray-300 rounded-lg"
>
<option value="open">Open (Anyone can join cluster)</option>
<option value="restricted">Restricted (Require authentication)</option>
<option value="invite-only">Invite Only (Manual approval required)</option>
</select>
</div>
<label className="flex items-center">
<input
type="checkbox"
checked={config.enableFirewall}
onChange={(e) => setConfig(prev => ({ ...prev, enableFirewall: e.target.checked }))}
className="mr-2"
/>
Enable firewall configuration
</label>
{config.enableFirewall && (
<div className="ml-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Allowed Ports</label>
<input
type="text"
value={config.allowedPorts.join(', ')}
onChange={(e) => setConfig(prev => ({
...prev,
allowedPorts: e.target.value.split(',').map(p => p.trim()).filter(p => p)
}))}
placeholder="22, 8080, 8090, 9100, 3000"
className="w-full p-3 border border-gray-300 rounded-lg"
/>
{configData?.network && (
<p className="text-sm text-eucalyptus-600 mt-1 flex items-center">
<CheckCircleIcon className="h-4 w-4 mr-1" />
Ports automatically configured from Network Settings: {[
configData.network.bzzzPort,
configData.network.mcpPort,
configData.network.webUIPort,
configData.network.p2pPort
].filter(p => p).join(', ')}
</p>
)}
<p className="text-sm text-gray-500 mt-1">
Comma-separated list of ports to allow through the firewall
</p>
</div>
</div>
)}
</div>
</div>
{/* Security Summary */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start">
<ExclamationTriangleIcon className="h-5 w-5 text-blue-500 mt-0.5 mr-2" />
<div>
<h4 className="text-sm font-medium text-blue-800">Security Summary</h4>
<ul className="text-sm text-blue-700 mt-1 space-y-1">
<li>• SSH access: {config.sshKeyType === 'generate' ? 'New key pair will be generated' : config.sshKeyType === 'existing' ? 'Using provided key' : 'Manual configuration'}</li>
<li>• TLS encryption: {config.enableTLS ? 'Enabled' : 'Disabled'}</li>
<li>• Authentication: {config.authMethod}</li>
<li>• Access policy: {config.accessPolicy}</li>
<li>• Firewall: {config.enableFirewall ? 'Enabled' : 'Disabled'}</li>
</ul>
</div>
</div>
</div>
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button
type="submit"
disabled={config.sshKeyType === 'generate' && !config.sshPublicKey}
className="btn-primary"
>
{isCompleted ? 'Continue' : 'Next: AI Integration'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,841 @@
'use client'
import { useState, useEffect } from 'react'
import {
ServerIcon,
ExclamationTriangleIcon,
CheckCircleIcon,
XCircleIcon,
PlayIcon,
StopIcon,
TrashIcon,
DocumentTextIcon,
ArrowPathIcon,
CloudArrowDownIcon,
Cog6ToothIcon,
XMarkIcon,
ComputerDesktopIcon,
ArrowDownTrayIcon
} from '@heroicons/react/24/outline'
interface Machine {
id: string
hostname: string
ip: string
os: string
osVersion: string
sshStatus: 'unknown' | 'connected' | 'failed' | 'testing'
deployStatus: 'not_deployed' | 'installing' | 'running' | 'stopped' | 'error'
selected: boolean
lastSeen?: string
deployProgress?: number
deployStep?: string
systemInfo?: {
cpu: number
memory: number
disk: number
}
}
interface ServiceDeploymentProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function ServiceDeployment({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: ServiceDeploymentProps) {
const [machines, setMachines] = useState<Machine[]>([])
const [isDiscovering, setIsDiscovering] = useState(false)
const [discoveryProgress, setDiscoveryProgress] = useState(0)
const [discoveryStatus, setDiscoveryStatus] = useState('')
const [showLogs, setShowLogs] = useState<string | null>(null)
const [deploymentLogs, setDeploymentLogs] = useState<{[key: string]: string[]}>({})
const [showConsole, setShowConsole] = useState<string | null>(null)
const [consoleLogs, setConsoleLogs] = useState<{[key: string]: string[]}>({})
const [config, setConfig] = useState({
deploymentMethod: 'systemd',
autoStart: true,
healthCheckInterval: 30,
selectedMachines: [] as string[]
})
// Initialize with current machine
useEffect(() => {
const currentMachine: Machine = {
id: 'localhost',
hostname: systemInfo?.network?.hostname || 'localhost',
ip: configData?.network?.primaryIP || '127.0.0.1',
os: systemInfo?.os || 'linux',
osVersion: 'Current Host',
sshStatus: 'connected',
deployStatus: 'running', // Already running since we're in setup
selected: true,
systemInfo: {
cpu: systemInfo?.cpu_cores || 0,
memory: Math.round((systemInfo?.memory_mb || 0) / 1024),
disk: systemInfo?.storage?.free_space_gb || 0
}
}
setMachines([currentMachine])
setConfig(prev => ({ ...prev, selectedMachines: ['localhost'] }))
}, [systemInfo, configData])
const discoverMachines = async () => {
setIsDiscovering(true)
setDiscoveryProgress(0)
setDiscoveryStatus('Initializing network scan...')
try {
// Simulate progress updates during discovery
const progressInterval = setInterval(() => {
setDiscoveryProgress(prev => {
const newProgress = prev + 10
if (newProgress <= 30) {
setDiscoveryStatus('Scanning network subnet...')
} else if (newProgress <= 60) {
setDiscoveryStatus('Checking SSH accessibility...')
} else if (newProgress <= 90) {
setDiscoveryStatus('Gathering system information...')
} else {
setDiscoveryStatus('Finalizing discovery...')
}
return Math.min(newProgress, 95)
})
}, 200)
const response = await fetch('/api/setup/discover-machines', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subnet: configData?.network?.allowedIPs?.[0] || '192.168.1.0/24',
sshKey: configData?.security?.sshPublicKey
})
})
clearInterval(progressInterval)
setDiscoveryProgress(100)
if (response.ok) {
const result = await response.json()
setDiscoveryStatus(`Found ${result.machines?.length || 0} machines`)
const discoveredMachines: Machine[] = result.machines.map((m: any) => ({
id: m.ip,
hostname: m.hostname || 'Unknown',
ip: m.ip,
os: m.os || 'unknown',
osVersion: m.os_version || 'Unknown',
sshStatus: 'unknown',
deployStatus: 'not_deployed',
selected: false,
lastSeen: new Date().toISOString(),
systemInfo: m.system_info
}))
// Merge with existing machines (keep localhost)
setMachines(prev => {
const localhost = prev.find(m => m.id === 'localhost')
return localhost ? [localhost, ...discoveredMachines] : discoveredMachines
})
} else {
setDiscoveryStatus('Discovery failed - check network configuration')
}
} catch (error) {
console.error('Discovery failed:', error)
setDiscoveryStatus('Discovery error - network unreachable')
} finally {
setTimeout(() => {
setIsDiscovering(false)
setDiscoveryProgress(0)
setDiscoveryStatus('')
}, 2000)
}
}
const testSSHConnection = async (machineId: string) => {
setMachines(prev => prev.map(m =>
m.id === machineId ? { ...m, sshStatus: 'testing' } : m
))
try {
const machine = machines.find(m => m.id === machineId)
const response = await fetch('/api/setup/test-ssh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ip: machine?.ip,
sshKey: configData?.security?.sshPrivateKey,
sshUsername: configData?.security?.sshUsername || 'ubuntu',
sshPassword: configData?.security?.sshPassword,
sshPort: configData?.security?.sshPort || 22
})
})
const result = await response.json()
setMachines(prev => prev.map(m =>
m.id === machineId ? {
...m,
sshStatus: result.success ? 'connected' : 'failed',
os: result.os || m.os,
osVersion: result.os_version || m.osVersion,
systemInfo: result.system_info || m.systemInfo
} : m
))
} catch (error) {
setMachines(prev => prev.map(m =>
m.id === machineId ? { ...m, sshStatus: 'failed' } : m
))
}
}
const deployToMachine = async (machineId: string) => {
setMachines(prev => prev.map(m =>
m.id === machineId ? {
...m,
deployStatus: 'installing',
deployProgress: 0,
deployStep: 'Initializing deployment...'
} : m
))
const logs: string[] = []
const consoleLogs: string[] = [`🚀 Starting deployment to ${machines.find(m => m.id === machineId)?.hostname} (${machines.find(m => m.id === machineId)?.ip})`]
setDeploymentLogs(prev => ({ ...prev, [machineId]: logs }))
setConsoleLogs(prev => ({ ...prev, [machineId]: consoleLogs }))
// Open console if not already showing
if (!showConsole) {
setShowConsole(machineId)
}
// Real-time console logging helper
const addConsoleLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString()
const logMessage = `[${timestamp}] ${message}`
setConsoleLogs(prev => ({
...prev,
[machineId]: [...(prev[machineId] || []), logMessage]
}))
}
// Simulate progress updates
const progressSteps = [
{ progress: 10, step: 'Establishing SSH connection...' },
{ progress: 30, step: 'Copying BZZZ binary...' },
{ progress: 60, step: 'Creating systemd service...' },
{ progress: 80, step: 'Starting service...' },
{ progress: 100, step: 'Deployment complete!' }
]
const updateProgress = (stepIndex: number) => {
if (stepIndex < progressSteps.length) {
const { progress, step } = progressSteps[stepIndex]
setMachines(prev => prev.map(m =>
m.id === machineId ? {
...m,
deployProgress: progress,
deployStep: step
} : m
))
logs.push(`📦 ${step}`)
addConsoleLog(`📦 ${step}`)
setDeploymentLogs(prev => ({ ...prev, [machineId]: [...(prev[machineId] || []), `📦 ${step}`] }))
}
}
try {
const machine = machines.find(m => m.id === machineId)
addConsoleLog(`🚀 Starting deployment to ${machine?.hostname}...`)
addConsoleLog(`📡 Sending deployment request to backend API...`)
// Set initial progress
setMachines(prev => prev.map(m =>
m.id === machineId ? {
...m,
deployProgress: 10,
deployStep: 'Contacting backend API...'
} : m
))
const response = await fetch('/api/setup/deploy-service', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ip: machine?.ip,
sshKey: configData?.security?.sshPrivateKey,
sshUsername: configData?.security?.sshUsername || 'ubuntu',
sshPassword: configData?.security?.sshPassword,
sshPort: configData?.security?.sshPort || 22,
config: {
ports: {
api: configData?.network?.bzzzPort || 8080,
mcp: configData?.network?.mcpPort || 3000,
webui: configData?.network?.webUIPort || 8080,
p2p: configData?.network?.p2pPort || 7000
},
security: configData?.security,
autoStart: config.autoStart
}
})
})
const result = await response.json()
addConsoleLog(`📨 Received response from backend API`)
if (result.success) {
setMachines(prev => prev.map(m =>
m.id === machineId ? {
...m,
deployStatus: 'running',
deployProgress: 100,
deployStep: 'Running'
} : m
))
logs.push('✅ Deployment completed successfully')
addConsoleLog('✅ Deployment completed successfully!')
// Show actual backend steps if provided
if (result.steps) {
result.steps.forEach((step: any) => {
const stepText = `${step.name}: ${step.status}${step.error ? ` - ${step.error}` : ''}${step.duration ? ` (${step.duration})` : ''}`
logs.push(stepText)
addConsoleLog(`📋 ${stepText}`)
})
}
addConsoleLog(`🎉 CHORUS:agents service is now running on ${machine?.hostname}`)
} else {
setMachines(prev => prev.map(m =>
m.id === machineId ? {
...m,
deployStatus: 'error',
deployProgress: 0,
deployStep: 'Failed'
} : m
))
logs.push(`❌ Deployment failed: ${result.error}`)
addConsoleLog(`❌ Deployment failed: ${result.error}`)
addConsoleLog(`💡 Note: This was a real backend error, not simulated progress`)
}
} catch (error) {
setMachines(prev => prev.map(m =>
m.id === machineId ? {
...m,
deployStatus: 'error',
deployProgress: 0,
deployStep: 'Error'
} : m
))
logs.push(`❌ Deployment error: ${error}`)
addConsoleLog(`❌ Deployment error: ${error}`)
}
setDeploymentLogs(prev => ({ ...prev, [machineId]: logs }))
}
const toggleMachineSelection = (machineId: string) => {
setMachines(prev => prev.map(m =>
m.id === machineId ? { ...m, selected: !m.selected } : m
))
setConfig(prev => ({
...prev,
selectedMachines: machines
.map(m => m.id === machineId ? { ...m, selected: !m.selected } : m)
.filter(m => m.selected)
.map(m => m.id)
}))
}
const deployToSelected = async () => {
const selectedMachines = machines.filter(m => m.selected && m.sshStatus === 'connected')
for (const machine of selectedMachines) {
if (machine.deployStatus === 'not_deployed') {
await deployToMachine(machine.id)
}
}
}
const removeMachine = (machineId: string) => {
// Don't allow removing localhost
if (machineId === 'localhost') return
setMachines(prev => prev.filter(m => m.id !== machineId))
setConfig(prev => ({
...prev,
selectedMachines: prev.selectedMachines.filter(id => id !== machineId)
}))
// Clean up logs for removed machine
setDeploymentLogs(prev => {
const { [machineId]: removed, ...rest } = prev
return rest
})
}
const downloadConfig = async (machineId: string) => {
try {
const machine = machines.find(m => m.id === machineId)
if (!machine) return
const response = await fetch('/api/setup/download-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
machine_ip: machine.ip,
config: {
ports: {
api: configData?.network?.bzzzPort || 8080,
mcp: configData?.network?.mcpPort || 3000,
webui: configData?.network?.webUIPort || 8080,
p2p: configData?.network?.p2pPort || 7000
},
security: configData?.security,
autoStart: config.autoStart
}
})
})
if (response.ok) {
const result = await response.json()
// Create blob and download
const blob = new Blob([result.configYAML], { type: 'text/yaml' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `bzzz-config-${machine.hostname}-${machine.ip}.yaml`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
} else {
console.error('Failed to download config:', await response.text())
}
} catch (error) {
console.error('Config download error:', error)
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'connected': return <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" />
case 'failed': return <XCircleIcon className="h-5 w-5 text-red-500" />
case 'testing': return <ArrowPathIcon className="h-5 w-5 text-blue-500 animate-spin" />
case 'running': return <CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" />
case 'installing': return <ArrowPathIcon className="h-5 w-5 text-blue-500 animate-spin" />
case 'error': return <XCircleIcon className="h-5 w-5 text-red-500" />
case 'stopped': return <StopIcon className="h-5 w-5 text-yellow-500" />
default: return <ServerIcon className="h-5 w-5 text-gray-400" />
}
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onComplete({
deployment: {
...config,
machines: machines.filter(m => m.selected).map(m => ({
id: m.id,
ip: m.ip,
hostname: m.hostname,
deployStatus: m.deployStatus
}))
}
})
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* OS Support Caution */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start">
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-600 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h3 className="text-sm font-medium text-yellow-800">Operating System Support</h3>
<p className="text-sm text-yellow-700 mt-1">
CHORUS:agents automated deployment supports <strong>Linux distributions that use systemd by default</strong> (Ubuntu 16+, CentOS 7+, Debian 8+, RHEL 7+, etc.).
For other operating systems or init systems, you'll need to manually deploy the CHORUS:agents binary and configure services on your cluster.
</p>
</div>
</div>
</div>
{/* Network Discovery */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 flex items-center">
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Machine Discovery
</h3>
<button
type="button"
onClick={discoverMachines}
disabled={isDiscovering}
className="btn-outline flex items-center"
>
<ArrowPathIcon className={`h-4 w-4 mr-2 ${isDiscovering ? 'animate-spin' : ''}`} />
{isDiscovering ? 'Discovering...' : 'Discover Machines'}
</button>
</div>
<p className="text-sm text-gray-600 mb-4">
Scan network subnet: {configData?.network?.allowedIPs?.[0] || '192.168.1.0/24'}
</p>
{/* Discovery Progress */}
{isDiscovering && (
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">{discoveryStatus}</span>
<span className="text-sm text-gray-500">{discoveryProgress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-bzzz-primary h-2 rounded-full transition-all duration-300 ease-out"
style={{ width: `${discoveryProgress}%` }}
/>
</div>
</div>
)}
</div>
{/* Machine Table */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Cluster Machines</h3>
<button
type="button"
onClick={deployToSelected}
disabled={machines.filter(m => m.selected && m.sshStatus === 'connected').length === 0}
className="btn-primary flex items-center"
>
<CloudArrowDownIcon className="h-4 w-4 mr-2" />
Deploy to Selected
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
<span className="sr-only sm:not-sr-only">Select</span>
<span className="sm:hidden">✓</span>
</th>
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
Machine / Connection
</th>
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3 hidden md:table-cell">
Operating System
</th>
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
Deploy Status
</th>
<th className="px-2 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-4 sm:py-3">
Actions
</th>
<th className="px-1 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sm:px-2 sm:py-3">
<span className="sr-only">Remove</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{machines.map((machine) => (
<tr key={machine.id} className={machine.selected ? 'bg-blue-50' : ''}>
<td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3">
<input
type="checkbox"
checked={machine.selected}
onChange={() => toggleMachineSelection(machine.id)}
className="h-4 w-4 text-bzzz-primary focus:ring-bzzz-primary border-gray-300 rounded"
/>
</td>
<td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3">
<div>
<div className="text-sm font-medium text-gray-900">{machine.hostname}</div>
<div className="text-xs text-gray-500 space-y-1">
<div className="inline-flex items-center space-x-2">
<span>{machine.ip}</span>
<span className="inline-flex items-center" title={`SSH Status: ${machine.sshStatus.replace('_', ' ')}`}>
{getStatusIcon(machine.sshStatus)}
</span>
</div>
{machine.systemInfo && (
<div className="text-gray-400">
{machine.systemInfo.cpu}c • {machine.systemInfo.memory}GB • {machine.systemInfo.disk}GB
</div>
)}
</div>
</div>
</td>
<td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3 hidden md:table-cell">
<div className="text-sm text-gray-900">{machine.os}</div>
<div className="text-xs text-gray-500">{machine.osVersion}</div>
</td>
<td className="px-2 py-2 whitespace-nowrap sm:px-4 sm:py-3">
<div className="flex items-center">
<div className="inline-flex items-center" title={`Deploy Status: ${machine.deployStatus.replace('_', ' ')}`}>
{getStatusIcon(machine.deployStatus)}
</div>
{machine.deployStatus === 'installing' && (
<div className="ml-2 flex-1">
<div className="text-xs text-gray-500 mb-1 truncate">
{machine.deployStep || 'Deploying...'}
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${machine.deployProgress || 0}%` }}
/>
</div>
<div className="text-xs text-gray-500 mt-1">
{machine.deployProgress || 0}%
</div>
</div>
)}
</div>
</td>
<td className="px-2 py-2 whitespace-nowrap text-sm font-medium sm:px-4 sm:py-3">
<div className="flex flex-wrap gap-1">
{machine.id !== 'localhost' && machine.sshStatus !== 'connected' && (
<button
type="button"
onClick={() => testSSHConnection(machine.id)}
className="text-blue-600 hover:text-blue-700 text-xs px-2 py-1 bg-blue-50 rounded"
disabled={machine.sshStatus === 'testing'}
title="Test SSH connection"
>
Test SSH
</button>
)}
{machine.sshStatus === 'connected' && machine.deployStatus === 'not_deployed' && (
<button
type="button"
onClick={() => deployToMachine(machine.id)}
className="text-eucalyptus-600 hover:text-eucalyptus-700 text-xs px-2 py-1 bg-eucalyptus-50 rounded"
title="Deploy BZZZ"
>
Install
</button>
)}
{machine.sshStatus === 'connected' && machine.deployStatus === 'error' && (
<button
type="button"
onClick={() => deployToMachine(machine.id)}
className="text-amber-600 hover:text-amber-700 text-xs px-2 py-1 bg-amber-50 rounded inline-flex items-center"
title="Retry deployment"
>
<ArrowPathIcon className="h-3 w-3 mr-1" />
Retry
</button>
)}
{machine.sshStatus === 'connected' && (
<button
type="button"
onClick={() => downloadConfig(machine.id)}
className="text-purple-600 hover:text-purple-700 text-xs px-2 py-1 bg-purple-50 rounded inline-flex items-center"
title="Download configuration file"
>
<ArrowDownTrayIcon className="h-3 w-3 mr-1" />
<span className="hidden sm:inline">Config</span>
</button>
)}
{machine.deployStatus !== 'not_deployed' && (
<>
<button
type="button"
onClick={() => setShowLogs(machine.id)}
className="text-gray-600 hover:text-gray-700 text-xs px-2 py-1 bg-gray-50 rounded inline-flex items-center"
title="View deployment logs"
>
<DocumentTextIcon className="h-3 w-3 mr-1" />
<span className="hidden sm:inline">Logs</span>
</button>
<button
type="button"
onClick={() => setShowConsole(machine.id)}
className="text-blue-600 hover:text-blue-700 text-xs px-2 py-1 bg-blue-50 rounded inline-flex items-center"
title="Open deployment console"
>
<ComputerDesktopIcon className="h-3 w-3 mr-1" />
<span className="hidden sm:inline">Console</span>
</button>
</>
)}
</div>
</td>
<td className="px-1 py-2 whitespace-nowrap text-sm font-medium sm:px-2 sm:py-3">
{machine.id !== 'localhost' && (
<button
type="button"
onClick={() => removeMachine(machine.id)}
className="text-red-600 hover:text-red-700 p-1 rounded hover:bg-red-50"
title="Remove machine"
>
<XMarkIcon className="h-4 w-4" />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{machines.length === 0 && (
<div className="text-center py-8">
<ServerIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No machines discovered yet. Click "Discover Machines" to scan your network.</p>
</div>
)}
</div>
{/* Deployment Configuration */}
<div className="card">
<h3 className="text-lg font-medium text-gray-900 mb-4 flex items-center">
<Cog6ToothIcon className="h-6 w-6 text-bzzz-primary mr-2" />
Deployment Configuration
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={config.autoStart}
onChange={(e) => setConfig(prev => ({ ...prev, autoStart: e.target.checked }))}
className="mr-2"
/>
Auto-start services after deployment
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Health Check Interval (seconds)
</label>
<input
type="number"
value={config.healthCheckInterval}
onChange={(e) => setConfig(prev => ({ ...prev, healthCheckInterval: parseInt(e.target.value) }))}
min="10"
max="300"
className="input-field"
/>
</div>
</div>
</div>
{/* Logs Modal */}
{showLogs && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-2xl w-full max-h-96 overflow-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">Deployment Logs - {machines.find(m => m.id === showLogs)?.hostname}</h3>
<button onClick={() => setShowLogs(null)} className="text-gray-400 hover:text-gray-600">
</button>
</div>
<div className="bg-gray-900 text-eucalyptus-600 p-4 rounded font-mono text-sm max-h-64 overflow-y-auto">
{deploymentLogs[showLogs]?.map((log, index) => (
<div key={index}>{log}</div>
)) || <div>No logs available</div>}
</div>
</div>
</div>
)}
{/* Virtual Console Modal */}
{showConsole && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-lg overflow-hidden max-w-4xl w-full max-h-[80vh] flex flex-col">
<div className="bg-gray-800 px-4 py-3 flex justify-between items-center border-b border-gray-700">
<div className="flex items-center">
<ComputerDesktopIcon className="h-5 w-5 text-eucalyptus-600 mr-2" />
<h3 className="text-lg font-medium text-white">
SSH Console - {machines.find(m => m.id === showConsole)?.hostname}
</h3>
<span className="ml-2 text-sm text-gray-400">
({machines.find(m => m.id === showConsole)?.ip})
</span>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<div className="w-2 h-2 bg-eucalyptus-500 rounded-full"></div>
</div>
<button
onClick={() => setShowConsole(null)}
className="text-gray-400 hover:text-white ml-4"
>
</button>
</div>
</div>
<div className="flex-1 p-4 font-mono text-sm overflow-y-auto bg-gray-900">
<div className="text-eucalyptus-600 space-y-1">
{consoleLogs[showConsole]?.length > 0 ? (
consoleLogs[showConsole].map((log, index) => (
<div key={index} className="whitespace-pre-wrap">{log}</div>
))
) : (
<div className="text-gray-500">Waiting for deployment to start...</div>
)}
{/* Blinking cursor */}
<div className="inline-block w-2 h-4 bg-green-400 animate-pulse"></div>
</div>
</div>
<div className="bg-gray-800 px-4 py-2 border-t border-gray-700 flex justify-between items-center">
<div className="text-xs text-gray-400">
💡 This console shows real-time deployment progress and SSH operations
</div>
{(() => {
const machine = machines.find(m => m.id === showConsole)
return machine?.sshStatus === 'connected' && machine?.deployStatus === 'error' && (
<button
type="button"
onClick={() => {
deployToMachine(showConsole!)
}}
className="ml-4 px-3 py-1 bg-amber-600 hover:bg-amber-700 text-white text-xs rounded-md flex items-center space-x-1 transition-colors"
title="Retry deployment"
>
<ArrowPathIcon className="h-3 w-3" />
<span>Retry Deployment</span>
</button>
)
})()}
</div>
</div>
</div>
)}
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button type="submit" className="btn-primary">
{isCompleted ? 'Continue' : 'Next: Cluster Formation'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,403 @@
'use client'
import { useState, useEffect } from 'react'
import {
CpuChipIcon,
ServerIcon,
CircleStackIcon,
GlobeAltIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
ArrowPathIcon
} from '@heroicons/react/24/outline'
interface SystemInfo {
os: string
architecture: string
cpu_cores: number
memory_mb: number
gpus: Array<{
name: string
memory: string
driver: string
type: string
}>
network: {
hostname: string
interfaces: string[]
public_ip?: string
private_ips: string[]
docker_bridge?: string
}
storage: {
total_space_gb: number
free_space_gb: number
mount_path: string
}
docker: {
available: boolean
version?: string
compose_available: boolean
swarm_mode: boolean
}
}
interface SystemDetectionProps {
systemInfo: SystemInfo | null
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function SystemDetection({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: SystemDetectionProps) {
const [loading, setLoading] = useState(!systemInfo)
const [refreshing, setRefreshing] = useState(false)
const [detectedInfo, setDetectedInfo] = useState<SystemInfo | null>(systemInfo)
useEffect(() => {
if (!detectedInfo) {
refreshSystemInfo()
}
}, [])
const refreshSystemInfo = async () => {
setRefreshing(true)
try {
const response = await fetch('/api/setup/system')
if (response.ok) {
const result = await response.json()
setDetectedInfo(result.system_info)
}
} catch (error) {
console.error('Failed to detect system info:', error)
} finally {
setLoading(false)
setRefreshing(false)
}
}
const handleContinue = () => {
if (detectedInfo) {
onComplete({
system: detectedInfo,
validated: true
})
}
}
const getStatusColor = (condition: boolean) => {
return condition ? 'text-eucalyptus-600' : 'text-red-600'
}
const getStatusIcon = (condition: boolean) => {
return condition ? CheckCircleIcon : ExclamationTriangleIcon
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<ArrowPathIcon className="h-8 w-8 text-bzzz-primary animate-spin mx-auto mb-4" />
<p className="text-chorus-text-secondary">Detecting system configuration...</p>
</div>
</div>
)
}
if (!detectedInfo) {
return (
<div className="text-center py-12">
<ExclamationTriangleIcon className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h3 className="heading-subsection mb-2">
System Detection Failed
</h3>
<p className="text-chorus-text-secondary mb-4">
Unable to detect system configuration. Please try again.
</p>
<button
onClick={refreshSystemInfo}
disabled={refreshing}
className="btn-primary"
>
{refreshing ? 'Retrying...' : 'Retry Detection'}
</button>
</div>
)
}
return (
<div className="space-y-6">
{/* System Overview */}
<div className="card">
<div className="flex items-center justify-between mb-4">
<h3 className="heading-subsection">System Overview</h3>
<button
onClick={refreshSystemInfo}
disabled={refreshing}
className="text-bzzz-primary hover:text-bzzz-primary/80 transition-colors"
>
<ArrowPathIcon className={`h-5 w-5 ${refreshing ? 'animate-spin' : ''}`} />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="text-sm font-medium text-chorus-text-secondary">Hostname</div>
<div className="text-lg text-chorus-text-primary">{detectedInfo.network.hostname}</div>
</div>
<div>
<div className="text-sm font-medium text-chorus-text-secondary">Operating System</div>
<div className="text-lg text-chorus-text-primary">
{detectedInfo.os} ({detectedInfo.architecture})
</div>
</div>
</div>
</div>
{/* Hardware Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* CPU & Memory */}
<div className="card">
<div className="flex items-center mb-4">
<CpuChipIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="heading-subsection">CPU & Memory</h3>
</div>
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-chorus-text-secondary">CPU</div>
<div className="text-chorus-text-primary">
{detectedInfo.cpu_cores} cores
</div>
</div>
<div>
<div className="text-sm font-medium text-chorus-text-secondary">Memory</div>
<div className="text-chorus-text-primary">
{Math.round(detectedInfo.memory_mb / 1024)} GB total
</div>
</div>
</div>
</div>
{/* Storage */}
<div className="card">
<div className="flex items-center mb-4">
<CircleStackIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="heading-subsection">Storage</h3>
</div>
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-chorus-text-secondary">Disk Space</div>
<div className="text-chorus-text-primary">
{detectedInfo.storage.total_space_gb} GB total, {' '}
{detectedInfo.storage.free_space_gb} GB available
</div>
</div>
<div className="w-full bg-chorus-border-invisible rounded-full h-2">
<div
className="bg-bzzz-primary h-2 rounded-full"
style={{
width: `${((detectedInfo.storage.total_space_gb - detectedInfo.storage.free_space_gb) / detectedInfo.storage.total_space_gb) * 100}%`
}}
/>
</div>
</div>
</div>
</div>
{/* GPU Information */}
{detectedInfo.gpus && detectedInfo.gpus.length > 0 && (
<div className="card">
<div className="flex items-center mb-4">
<ServerIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="heading-subsection">
GPU Configuration ({detectedInfo.gpus.length} GPU{detectedInfo.gpus.length !== 1 ? 's' : ''})
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{detectedInfo.gpus.map((gpu, index) => (
<div key={index} className="bg-chorus-warm rounded-lg p-4">
<div className="font-medium text-chorus-text-primary">{gpu.name}</div>
<div className="text-sm text-chorus-text-secondary">
{gpu.type.toUpperCase()} {gpu.memory} {gpu.driver}
</div>
</div>
))}
</div>
</div>
)}
{/* Network Information */}
<div className="card">
<div className="flex items-center mb-4">
<GlobeAltIcon className="h-6 w-6 text-bzzz-primary mr-2" />
<h3 className="heading-subsection">Network Configuration</h3>
</div>
<div className="space-y-3">
<div>
<div className="text-sm font-medium text-chorus-text-secondary">Hostname</div>
<div className="text-chorus-text-primary">{detectedInfo.network.hostname}</div>
</div>
{detectedInfo.network.private_ips && detectedInfo.network.private_ips.length > 0 && (
<div>
<div className="text-sm font-medium text-chorus-text-secondary mb-2">Private IP Addresses</div>
<div className="space-y-2">
{detectedInfo.network.private_ips.map((ip, index) => (
<div key={index} className="flex justify-between items-center text-sm">
<span>{ip}</span>
<span className="status-indicator status-online">active</span>
</div>
))}
</div>
</div>
)}
{detectedInfo.network.public_ip && (
<div>
<div className="text-sm font-medium text-chorus-text-secondary">Public IP</div>
<div className="text-chorus-text-primary">{detectedInfo.network.public_ip}</div>
</div>
)}
</div>
</div>
{/* Software Requirements */}
<div className="card">
<h3 className="heading-subsection mb-4">Software Requirements</h3>
<div className="space-y-4">
{[
{
name: 'Docker',
installed: detectedInfo.docker.available,
version: detectedInfo.docker.version,
required: true
},
{
name: 'Docker Compose',
installed: detectedInfo.docker.compose_available,
version: undefined,
required: false
},
{
name: 'Docker Swarm',
installed: detectedInfo.docker.swarm_mode,
version: undefined,
required: false
}
].map((software, index) => {
const StatusIcon = getStatusIcon(software.installed)
return (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center">
<StatusIcon className={`h-5 w-5 mr-3 ${getStatusColor(software.installed)}`} />
<div>
<div className="font-medium text-chorus-text-primary">{software.name}</div>
{software.version && (
<div className="text-sm text-chorus-text-secondary">Version: {software.version}</div>
)}
</div>
</div>
<div className="flex items-center">
{software.required && (
<span className="text-xs bg-bzzz-primary text-white px-2 py-1 rounded mr-2">
Required
</span>
)}
<span className={`text-sm font-medium ${getStatusColor(software.installed)}`}>
{software.installed ? 'Installed' : 'Missing'}
</span>
</div>
</div>
)
})}
</div>
</div>
{/* System Validation */}
<div className="panel panel-info">
<h3 className="heading-subsection mb-4 panel-title">System Validation</h3>
<div className="space-y-2">
{[
{
check: 'Minimum memory (2GB required)',
passed: detectedInfo.memory_mb >= 2048,
warning: detectedInfo.memory_mb < 4096
},
{
check: 'Available disk space (10GB required)',
passed: detectedInfo.storage.free_space_gb >= 10
},
{
check: 'Docker installed and running',
passed: detectedInfo.docker.available
}
].map((validation, index) => {
const StatusIcon = getStatusIcon(validation.passed)
return (
<div key={index} className="flex items-center">
<StatusIcon className={`h-4 w-4 mr-3 ${
validation.passed
? 'text-eucalyptus-600'
: 'text-red-600'
}`} />
<span className={`text-sm ${
validation.passed
? 'text-eucalyptus-600'
: 'text-red-600'
}`}>
{validation.check}
{validation.warning && validation.passed && (
<span className="text-yellow-600 ml-2">(Warning: Recommend 4GB+)</span>
)}
</span>
</div>
)
})}
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-between pt-6 border-t border-chorus-border-defined">
<div>
{onBack && (
<button onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<div className="flex space-x-3">
<button
onClick={refreshSystemInfo}
disabled={refreshing}
className="btn-outline"
>
{refreshing ? 'Refreshing...' : 'Refresh'}
</button>
<button
onClick={handleContinue}
className="btn-primary"
disabled={!detectedInfo.docker.available}
>
{isCompleted ? 'Continue' : 'Next: Repository Setup'}
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,181 @@
'use client'
import { useState } from 'react'
import {
DocumentTextIcon,
CheckCircleIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/outline'
interface TermsAndConditionsProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function TermsAndConditions({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: TermsAndConditionsProps) {
const [agreed, setAgreed] = useState(configData?.terms?.agreed || false)
const [error, setError] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!agreed) {
setError('You must agree to the Terms and Conditions to continue')
return
}
setError('')
onComplete({
terms: {
agreed: true,
timestamp: new Date().toISOString()
}
})
}
return (
<form onSubmit={handleSubmit} className="space-y-8">
{/* Terms and Conditions Content */}
<div className="card">
<div className="flex items-center mb-4">
<DocumentTextIcon className="h-6 w-6 text-ocean-500 mr-2" />
<h3 className="text-lg font-medium text-chorus-text-primary">CHORUS:agents Software License Agreement</h3>
</div>
<div className="bg-chorus-warm border border-chorus-border-subtle rounded-lg p-6 max-h-96 overflow-y-auto">
<div className="prose prose-sm max-w-none text-chorus-text-secondary">
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">1. License Grant</h4>
<p className="mb-4">
Subject to the terms and conditions of this Agreement, Chorus Services grants you a non-exclusive,
non-transferable license to use CHORUS:agents (the "Software") for distributed AI coordination and task management.
</p>
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">2. Permitted Uses</h4>
<ul className="list-disc list-inside mb-4 space-y-1">
<li>Install and operate CHORUS:agents on your infrastructure</li>
<li>Configure cluster nodes for distributed processing</li>
<li>Integrate with supported AI models and services</li>
<li>Use for commercial and non-commercial purposes</li>
</ul>
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">3. Restrictions</h4>
<ul className="list-disc list-inside mb-4 space-y-1">
<li>You may not redistribute, sublicense, or sell the Software</li>
<li>You may not reverse engineer or decompile the Software</li>
<li>You may not use the Software for illegal or harmful purposes</li>
<li>You may not remove or modify proprietary notices</li>
</ul>
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">4. Data Privacy</h4>
<p className="mb-4">
CHORUS:agents processes data locally on your infrastructure. Chorus Services does not collect or store
your operational data. Telemetry data may be collected for software improvement purposes.
</p>
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">5. Support and Updates</h4>
<p className="mb-4">
Licensed users receive access to software updates, security patches, and community support.
Premium support tiers are available separately.
</p>
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">6. Disclaimer of Warranty</h4>
<p className="mb-4">
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. CHORUS SERVICES DISCLAIMS
ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE.
</p>
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">7. Limitation of Liability</h4>
<p className="mb-4">
IN NO EVENT SHALL CHORUS SERVICES BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THE SOFTWARE.
</p>
<h4 className="text-base font-semibold text-chorus-text-primary mb-3">8. Termination</h4>
<p className="mb-4">
This license is effective until terminated. You may terminate it at any time by
uninstalling the Software. Chorus Services may terminate this license if you
violate any terms of this Agreement.
</p>
<div className="panel panel-info mt-6">
<div className="flex">
<ExclamationTriangleIcon className="h-5 w-5 text-ocean-600 dark:text-ocean-300 mt-0.5 mr-2" />
<div className="text-sm panel-body">
<p><strong>Contact Information:</strong></p>
<p>Chorus Services<br />
Email: legal@chorus.services<br />
Website: https://chorus.services</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Agreement Checkbox */}
<div className="card agreement">
<div className="space-y-4">
<label className="flex items-start">
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
className="mt-1 mr-3 h-4 w-4 text-ocean-600 border-chorus-border-defined rounded focus:ring-ocean-600"
/>
<div className="text-sm">
<span className="font-medium text-chorus-text-primary">
I have read and agree to the Terms and Conditions
</span>
<p className="text-chorus-text-secondary mt-1">
By checking this box, you acknowledge that you have read, understood, and agree to be
bound by the terms and conditions outlined above.
</p>
</div>
</label>
{error && (
<div className="flex items-center text-red-600 text-sm">
<ExclamationTriangleIcon className="h-4 w-4 mr-1" />
{error}
</div>
)}
{agreed && (
<div className="flex items-center text-eucalyptus-600 text-sm">
<CheckCircleIcon className="h-4 w-4 mr-1" />
Thank you for accepting the terms and conditions
</div>
)}
</div>
</div>
<div className="flex justify-between pt-6 border-t border-chorus-border-defined">
<div>
{onBack && (
<button type="button" onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
<button
type="submit"
disabled={!agreed}
className="btn-primary"
>
{isCompleted ? 'Continue' : 'Next: License Validation'}
</button>
</div>
</form>
)
}

View File

@@ -0,0 +1,131 @@
'use client'
import { useState } from 'react'
interface TestingValidationProps {
systemInfo: any
configData: any
onComplete: (data: any) => void
onBack?: () => void
isCompleted: boolean
}
export default function TestingValidation({
systemInfo,
configData,
onComplete,
onBack,
isCompleted
}: TestingValidationProps) {
const [testing, setTesting] = useState(false)
const handleRunTests = async () => {
setTesting(true)
// Simulate testing process
await new Promise(resolve => setTimeout(resolve, 3000))
setTesting(false)
onComplete({
testing: {
passed: true,
completedAt: new Date().toISOString()
}
})
}
const getClusterDashboardUrl = () => {
// Get the WebUI port from config, default to 9090
const webuiPort = configData?.network?.ports?.webui || 9090
return `http://localhost:${webuiPort}/dashboard`
}
const handleGoToDashboard = () => {
const dashboardUrl = getClusterDashboardUrl()
// Clear setup state since we're done
localStorage.removeItem('bzzz-setup-state')
// Open cluster dashboard in new tab
window.open(dashboardUrl, '_blank')
// Show completion message and suggest closing this tab
const shouldClose = window.confirm(
'Setup complete! The cluster dashboard has opened in a new tab.\n\n' +
'You can now close this setup tab. Click OK to close automatically, or Cancel to keep it open.'
)
if (shouldClose) {
window.close()
}
}
return (
<div className="space-y-6">
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">
Testing & Validation
</h3>
<p className="text-gray-600">
Validate your BZZZ cluster configuration and test all connections.
</p>
<div className="mt-8">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 text-yellow-800">
This component is under development. Testing and validation will be implemented here.
</div>
</div>
{!isCompleted && (
<div className="mt-8">
<button
onClick={handleRunTests}
disabled={testing}
className="btn-primary"
>
{testing ? 'Running Tests...' : 'Run Validation Tests'}
</button>
</div>
)}
{isCompleted && (
<div className="mt-8 bg-eucalyptus-50 border border-eucalyptus-950 rounded-lg p-6">
<h4 className="text-lg font-medium text-eucalyptus-600 mb-2">
🎉 Setup Complete!
</h4>
<p className="text-eucalyptus-600 mb-4">
Your CHORUS:agents cluster has been successfully configured and deployed.
</p>
<div className="space-y-2 text-sm text-eucalyptus-600 mb-4">
<div> System configuration validated</div>
<div> Network connectivity tested</div>
<div> Services deployed to all nodes</div>
<div> Cluster formation completed</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
<strong>Cluster Dashboard:</strong> <code>{getClusterDashboardUrl()}</code>
</p>
<p className="text-xs text-blue-600 mt-1">
The setup process will be terminated and you'll be redirected to your operational cluster.
</p>
</div>
</div>
)}
</div>
<div className="flex justify-between pt-6 border-t border-gray-200">
<div>
{onBack && (
<button onClick={onBack} className="btn-outline">
Back
</button>
)}
</div>
{isCompleted && (
<button onClick={handleGoToDashboard} className="btn-primary">
Go to Cluster Dashboard
</button>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,326 @@
'use client'
import { useState, useEffect } from 'react'
import { ChevronRightIcon, CheckCircleIcon } from '@heroicons/react/24/outline'
import TermsAndConditions from './components/TermsAndConditions'
import LicenseValidation from './components/LicenseValidation'
import SystemDetection from './components/SystemDetection'
import RepositoryConfiguration from './components/RepositoryConfiguration'
import NetworkConfiguration from './components/NetworkConfiguration'
import SecuritySetup from './components/SecuritySetup'
import AIConfiguration from './components/AIConfiguration'
import ServiceDeployment from './components/ServiceDeployment'
import ClusterFormation from './components/ClusterFormation'
import TestingValidation from './components/TestingValidation'
const SETUP_STEPS = [
{
id: 'terms',
title: 'Terms & Conditions',
description: 'Review and accept the software license agreement',
component: TermsAndConditions,
},
{
id: 'license',
title: 'License Validation',
description: 'Validate your CHORUS license key and email',
component: LicenseValidation,
},
{
id: 'detection',
title: 'System Detection',
description: 'Detect hardware and validate installation',
component: SystemDetection,
},
{
id: 'repository',
title: 'Repository Setup',
description: 'Configure Git repository for task management',
component: RepositoryConfiguration,
},
{
id: 'network',
title: 'Network Configuration',
description: 'Configure network and firewall settings',
component: NetworkConfiguration,
},
{
id: 'security',
title: 'Security Setup',
description: 'Configure authentication and SSH access',
component: SecuritySetup,
},
{
id: 'ai',
title: 'AI Integration',
description: 'Configure OpenAI and Ollama/Parallama',
component: AIConfiguration,
},
{
id: 'deployment',
title: 'Service Deployment',
description: 'Deploy and configure CHORUS agent services',
component: ServiceDeployment,
},
{
id: 'cluster',
title: 'Cluster Formation',
description: 'Join or create CHORUS agent cluster',
component: ClusterFormation,
},
{
id: 'testing',
title: 'Testing & Validation',
description: 'Validate configuration and test connectivity',
component: TestingValidation,
},
]
interface ConfigData {
[key: string]: any
}
export default function SetupPage() {
const [currentStep, setCurrentStep] = useState(0)
const [completedSteps, setCompletedSteps] = useState(new Set<number>())
const [configData, setConfigData] = useState<ConfigData>({})
const [systemInfo, setSystemInfo] = useState<any>(null)
// Load persisted data and system information on mount
useEffect(() => {
loadPersistedData()
fetchSystemInfo()
}, [])
// Save setup state to localStorage whenever it changes
useEffect(() => {
saveSetupState()
}, [currentStep, completedSteps, configData])
const loadPersistedData = () => {
try {
const savedState = localStorage.getItem('chorus-setup-state')
if (savedState) {
const state = JSON.parse(savedState)
setCurrentStep(state.currentStep || 0)
setCompletedSteps(new Set(state.completedSteps || []))
setConfigData(state.configData || {})
}
} catch (error) {
console.error('Failed to load persisted setup data:', error)
}
}
const saveSetupState = () => {
try {
const state = {
currentStep,
completedSteps: Array.from(completedSteps),
configData,
timestamp: new Date().toISOString()
}
localStorage.setItem('chorus-setup-state', JSON.stringify(state))
} catch (error) {
console.error('Failed to save setup state:', error)
}
}
const clearPersistedData = () => {
try {
localStorage.removeItem('chorus-setup-state')
// Reset state to initial values
setCurrentStep(0)
setCompletedSteps(new Set<number>())
setConfigData({})
} catch (error) {
console.error('Failed to clear persisted data:', error)
}
}
const fetchSystemInfo = async () => {
try {
const response = await fetch('/api/setup/system')
if (response.ok) {
const result = await response.json()
setSystemInfo(result.system_info)
}
} catch (error) {
console.error('Failed to fetch system info:', error)
}
}
const handleStepComplete = (stepIndex: number, data: any) => {
console.log('Setup Page: Step complete', { stepIndex, data, currentConfigData: configData })
setCompletedSteps(prev => new Set([...prev, stepIndex]))
setConfigData(prev => {
const newConfigData = { ...prev, ...data }
console.log('Setup Page: Updated configData', { prev, data, newConfigData })
return newConfigData
})
// Auto-advance to next step
if (stepIndex < SETUP_STEPS.length - 1) {
setCurrentStep(stepIndex + 1)
} else {
// Setup is complete, clear persisted data after a delay
setTimeout(() => {
clearPersistedData()
}, 2000)
}
}
const handleStepBack = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1)
}
}
const CurrentStepComponent = SETUP_STEPS[currentStep].component
// Check if we're resuming from saved data
const isResuming = currentStep > 0 || completedSteps.size > 0 || Object.keys(configData).length > 0
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8">
<h1 className="heading-hero mb-3">
CHORUS Agent Setup
</h1>
<p className="text-body">
Configure your distributed agent orchestration platform in {SETUP_STEPS.length} simple steps.
</p>
</div>
{/* Resume Setup Notification (Info Panel) */}
{isResuming && (
<div className="mb-8 panel panel-info p-6">
<div className="flex items-start justify-between">
<div className="flex items-start">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-ocean-600 dark:text-ocean-300 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium panel-title">
Setup Progress Restored
</h3>
<p className="text-small panel-body mt-1">
Your previous setup progress has been restored. You're currently on step {currentStep + 1} of {SETUP_STEPS.length}.
{completedSteps.size > 0 && ` You've completed ${completedSteps.size} step${completedSteps.size !== 1 ? 's' : ''}.`}
</p>
</div>
</div>
<button
onClick={clearPersistedData}
className="btn-text"
>
Start Over
</button>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-12">
{/* Progress Sidebar */}
<div className="lg:col-span-1">
<div className="card sticky top-8 setup-progress">
<h2 className="heading-subsection mb-6">
Setup Progress
</h2>
<nav className="space-y-2">
{SETUP_STEPS.map((step, index) => {
const isCompleted = completedSteps.has(index)
const isCurrent = index === currentStep
const isAccessible = index <= currentStep || completedSteps.has(index)
return (
<button
key={step.id}
onClick={() => isAccessible && setCurrentStep(index)}
disabled={!isAccessible}
className={`w-full text-left progress-step ${
isCurrent
? 'progress-step-current'
: isCompleted
? 'progress-step-completed'
: isAccessible
? 'progress-step-accessible'
: 'progress-step-disabled'
}`}
>
<div className="flex items-center">
<div className="flex-shrink-0 mr-3">
{isCompleted ? (
<CheckCircleIcon className="h-5 w-5 text-eucalyptus-600" />
) : (
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center text-xs font-medium ${
isCurrent
? 'border-chorus-secondary bg-chorus-secondary text-white'
: 'border-gray-600 text-gray-500'
}`}>
{index + 1}
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">
{step.title}
</div>
<div className="text-xs opacity-75 truncate">
{step.description}
</div>
</div>
{isAccessible && !isCompleted && (
<ChevronRightIcon className="h-4 w-4 opacity-50" />
)}
</div>
</button>
)
})}
</nav>
<div className="mt-8 pt-6 border-t border-chorus-border-defined">
<div className="text-small mb-3">
Progress: {completedSteps.size} of {SETUP_STEPS.length} steps
</div>
<div className="w-full bg-chorus-border-invisible rounded-sm h-2">
<div
className="bg-chorus-secondary h-2 rounded-sm transition-all duration-500"
style={{ width: `${(completedSteps.size / SETUP_STEPS.length) * 100}%` }}
/>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="lg:col-span-3">
<div className="card">
<div className="mb-8">
<div className="flex items-center justify-between mb-3">
<h2 className="heading-section">
{SETUP_STEPS[currentStep].title}
</h2>
<div className="text-ghost">
Step {currentStep + 1} of {SETUP_STEPS.length}
</div>
</div>
<p className="text-body">
{SETUP_STEPS[currentStep].description}
</p>
</div>
<CurrentStepComponent
systemInfo={systemInfo}
configData={configData}
onComplete={(data: any) => handleStepComplete(currentStep, data)}
onBack={currentStep > 0 ? handleStepBack : undefined}
isCompleted={completedSteps.has(currentStep)}
/>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -0,0 +1,31 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// Export as static site for embedding in Go binary
output: 'export',
trailingSlash: true,
distDir: 'out',
// Disable image optimization for static export
images: {
unoptimized: true
},
// Configure for embedded serving
assetPrefix: process.env.NODE_ENV === 'production' ? '/setup' : '',
basePath: process.env.NODE_ENV === 'production' ? '/setup' : '',
// API routes will be handled by Go server
async rewrites() {
if (process.env.NODE_ENV === 'development') {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8080/api/:path*'
}
]
}
return []
}
}
module.exports = nextConfig

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"name": "bzzz-config-ui",
"version": "1.0.0",
"description": "BZZZ Cluster Configuration Web Interface",
"private": true,
"scripts": {
"dev": "next dev -p 8080",
"build": "next build",
"start": "next start -p 8080",
"lint": "next lint",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.3.2",
"@tailwindcss/forms": "^0.5.7",
"clsx": "^2.0.0",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"tailwind-merge": "^2.2.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.5",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"autoprefixer": "^10.4.16",
"eslint": "^8.56.0",
"eslint-config-next": "14.0.4",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.3"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -0,0 +1,337 @@
# BZZZ Configuration Web Interface Requirements
## Overview
A comprehensive web-based configuration interface that guides users through setting up their BZZZ cluster after the initial installation.
## User Information Requirements
### 1. Cluster Infrastructure Configuration
#### Network Settings
- **Subnet IP Range** (CIDR notation)
- Auto-detected from system
- User can override (e.g., `192.168.1.0/24`)
- Validation for valid CIDR format
- Conflict detection with existing networks
- **Node Discovery Method**
- Option 1: Automatic discovery via broadcast
- Option 2: Manual IP address list
- Option 3: DNS-based discovery
- Integration with existing network infrastructure
- **Network Interface Selection**
- Dropdown of available interfaces
- Auto-select primary interface
- Show interface details (IP, status, speed)
- Validation for interface accessibility
- **Port Configuration**
- BZZZ Go Service Port (default: 8080)
- MCP Server Port (default: 3000)
- Web UI Port (default: 8080)
- WebSocket Port (default: 8081)
- Reserved port range exclusions
- Port conflict detection
#### Firewall & Security
- **Firewall Configuration**
- Auto-configure firewall rules (ufw/iptables)
- Manual firewall setup instructions
- Port testing and validation
- Network connectivity verification
### 2. Authentication & Security Setup
#### SSH Key Management
- **SSH Key Options**
- Generate new SSH key pair
- Upload existing public key
- Use existing system SSH keys
- Key distribution to cluster nodes
- **SSH Access Configuration**
- SSH username for cluster access
- Sudo privileges configuration
- SSH port (default: 22)
- Key-based vs password authentication
#### Security Settings
- **TLS/SSL Configuration**
- Generate self-signed certificates
- Upload existing certificates
- Let's Encrypt integration
- Certificate distribution
- **Authentication Methods**
- Token-based authentication
- OAuth2 integration
- LDAP/Active Directory
- Local user management
### 3. AI Model Configuration
#### OpenAI Integration
- **API Key Management**
- Secure API key input
- Key validation and testing
- Organization and project settings
- Usage monitoring setup
- **Model Preferences**
- Default model selection (GPT-5)
- Model-to-task mapping
- Custom model parameters
- Fallback model configuration
#### Local AI Models (Ollama/Parallama)
- **Ollama/Parallama Installation**
- Option to install standard Ollama
- Option to install Parallama (multi-GPU fork)
- Auto-detect existing Ollama installations
- Upgrade/migrate from Ollama to Parallama
- **Node Discovery & Configuration**
- Auto-discover Ollama/Parallama instances
- Manual endpoint configuration
- Model availability checking
- Load balancing preferences
- GPU assignment for Parallama
- **Multi-GPU Configuration (Parallama)**
- GPU topology detection
- Model sharding across GPUs
- Memory allocation per GPU
- Performance optimization settings
- GPU failure handling
- **Model Distribution Strategy**
- Which models on which nodes
- GPU-specific model placement
- Automatic model pulling
- Storage requirements
- Model update policies
### 4. Cost Management
#### Spending Limits
- **Daily Limits** (USD)
- Per-user limits
- Per-project limits
- Global daily limit
- Warning thresholds
- **Monthly Limits** (USD)
- Budget allocation
- Automatic budget reset
- Cost tracking granularity
- Billing integration
#### Cost Optimization
- **Usage Monitoring**
- Real-time cost tracking
- Historical usage reports
- Cost per model/task type
- Optimization recommendations
### 5. Hardware & Resource Detection
#### System Resources
- **CPU Configuration**
- Core count and allocation
- CPU affinity settings
- Performance optimization
- Load balancing
- **Memory Management**
- Available RAM detection
- Memory allocation per service
- Swap configuration
- Memory monitoring
- **Storage Configuration**
- Available disk space
- Storage paths for data/logs
- Backup storage locations
- Storage monitoring
#### GPU Resources
- **GPU Detection**
- NVIDIA CUDA support
- AMD ROCm support
- GPU memory allocation
- Multi-GPU configuration
- **AI Workload Optimization**
- GPU scheduling
- Model-to-GPU assignment
- Power management
- Temperature monitoring
### 6. Service Configuration
#### Container Management
- **Docker Configuration**
- Container registry selection
- Image pull policies
- Resource limits per container
- Container orchestration (Docker Swarm/K8s)
- **Registry Settings**
- Public registry (Docker Hub)
- Private registry setup
- Authentication for registries
- Image versioning strategy
#### Update Management
- **Release Channels**
- Stable releases
- Beta releases
- Development builds
- Custom release sources
- **Auto-Update Settings**
- Automatic updates enabled/disabled
- Update scheduling
- Rollback capabilities
- Update notifications
### 7. Monitoring & Observability
#### Logging Configuration
- **Log Levels**
- Debug, Info, Warn, Error
- Per-component log levels
- Log rotation settings
- Centralized logging
- **Log Destinations**
- Local file logging
- Syslog integration
- External log collectors
- Log retention policies
#### Metrics & Monitoring
- **Metrics Collection**
- Prometheus integration
- Custom metrics
- Performance monitoring
- Health checks
- **Alerting**
- Alert rules configuration
- Notification channels
- Escalation policies
- Alert suppression
### 8. Cluster Topology
#### Node Roles
- **Coordinator Nodes**
- Primary coordinator selection
- Coordinator failover
- Load balancing
- State synchronization
- **Worker Nodes**
- Worker node capabilities
- Task scheduling preferences
- Resource allocation
- Worker health monitoring
- **Storage Nodes**
- Distributed storage setup
- Replication factors
- Data consistency
- Backup strategies
#### High Availability
- **Failover Configuration**
- Automatic failover
- Manual failover procedures
- Split-brain prevention
- Recovery strategies
- **Load Balancing**
- Load balancing algorithms
- Health check configuration
- Traffic distribution
- Performance optimization
## Configuration Flow
### Step 1: System Detection
- Detect hardware resources
- Identify network interfaces
- Check system dependencies
- Validate installation
### Step 2: Network Configuration
- Configure network settings
- Set up firewall rules
- Test connectivity
- Validate port accessibility
### Step 3: Security Setup
- Configure authentication
- Set up SSH access
- Generate/install certificates
- Test security settings
### Step 4: AI Integration
- Configure OpenAI API
- Set up Ollama endpoints
- Configure model preferences
- Test AI connectivity
### Step 5: Resource Allocation
- Allocate CPU/memory
- Configure storage paths
- Set up GPU resources
- Configure monitoring
### Step 6: Service Deployment
- Deploy BZZZ services
- Configure service parameters
- Start services
- Validate service health
### Step 7: Cluster Formation
- Discover other nodes
- Join/create cluster
- Configure replication
- Test cluster connectivity
### Step 8: Testing & Validation
- Run connectivity tests
- Test AI model access
- Validate security settings
- Performance benchmarking
## Technical Implementation
### Frontend Framework
- **React/Next.js** for modern UI
- **Material-UI** or **Tailwind CSS** for components
- **Real-time updates** via WebSocket
- **Progressive Web App** capabilities
### Backend API
- **Go REST API** integrated with BZZZ service
- **Configuration validation** and testing
- **Real-time status updates**
- **Secure configuration storage**
### Configuration Persistence
- **YAML configuration files**
- **Environment variable generation**
- **Docker Compose generation**
- **Systemd service configuration**
### Validation & Testing
- **Network connectivity testing**
- **Service health validation**
- **Configuration syntax checking**
- **Resource availability verification**
This comprehensive configuration system ensures users can easily set up and manage their BZZZ clusters regardless of their technical expertise level.

View File

@@ -0,0 +1,100 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class',
theme: {
extend: {
colors: {
// CHORUS Corporate Colors - Adaptive Theme
'chorus-primary': {
light: '#c1bfb1', // Brushed Nickel - light background
DEFAULT: '#0b0213', // Dark Mulberry - dark background
},
'chorus-secondary': '#5a6c80', // Orchestration Blue - consistent
'chorus-accent': '#c1bfb1', // Brushed Nickel - consistent
'chorus-brown': '#403730', // Walnut Brown - consistent
// Adaptive Surfaces
'chorus-paper': {
light: '#f8f9fa', // Light paper background
DEFAULT: '#0b0213', // Dark paper background
},
'chorus-white': {
light: '#ffffff', // Light cards
DEFAULT: '#111111', // Dark cards
},
'chorus-warm': {
light: '#f5f5f5', // Light elevated surfaces
DEFAULT: '#1a1a1a', // Dark elevated surfaces
},
// Adaptive Text Hierarchy
'chorus-text-primary': {
light: '#1a1a1a', // Dark text on light
DEFAULT: '#ffffff', // Light text on dark
},
'chorus-text-secondary': {
light: '#4a5568', // Medium gray on light
DEFAULT: '#e5e5e5', // Light gray on dark
},
'chorus-text-tertiary': {
light: '#718096', // Light gray on light
DEFAULT: '#cccccc', // Medium light on dark
},
'chorus-text-subtle': {
light: '#a0aec0', // Very light on light
DEFAULT: '#999999', // Medium on dark
},
'chorus-text-ghost': {
light: '#cbd5e0', // Ghost light
DEFAULT: '#666666', // Ghost dark
},
// Adaptive Border System
'chorus-border-invisible': {
light: '#f7fafc', // Nearly invisible light
DEFAULT: '#333333', // Nearly invisible dark
},
'chorus-border-subtle': {
light: '#e2e8f0', // Subtle light borders
DEFAULT: '#444444', // Subtle dark borders
},
'chorus-border-defined': {
light: '#cbd5e0', // Defined light borders
DEFAULT: '#555555', // Defined dark borders
},
'chorus-border-emphasis': {
light: '#a0aec0', // Emphasized light
DEFAULT: '#666666', // Emphasized dark
},
},
borderRadius: {
'none': '0',
'sm': '3px', // Small elements (inputs, badges)
'md': '4px', // Standard elements (buttons, nav)
'lg': '5px', // Large elements (cards, modals)
},
spacing: {
'18': '4.5rem', // 72px
'88': '22rem', // 352px
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'fade-in': 'fadeIn 200ms ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0', transform: 'translateY(4px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
}
}
},
},
plugins: [
require('@tailwindcss/forms'),
],
}

View File

@@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "es2015",
"downlevelIteration": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"out/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,575 @@
#!/bin/bash
# BZZZ Cluster Installation Script
# Usage: curl -fsSL https://chorus.services/install.sh | sh
set -euo pipefail
# Configuration
BZZZ_VERSION="${BZZZ_VERSION:-latest}"
BZZZ_BASE_URL="${BZZZ_BASE_URL:-https://chorus.services}"
BZZZ_INSTALL_DIR="${BZZZ_INSTALL_DIR:-/opt/bzzz}"
BZZZ_CONFIG_DIR="${BZZZ_CONFIG_DIR:-/etc/bzzz}"
BZZZ_LOG_DIR="${BZZZ_LOG_DIR:-/var/log/bzzz}"
BZZZ_DATA_DIR="${BZZZ_DATA_DIR:-/var/lib/bzzz}"
INSTALL_PARALLAMA="${INSTALL_PARALLAMA:-prompt}" # prompt, yes, no
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Error handler
error_exit() {
log_error "$1"
exit 1
}
# Check if running as root
check_root() {
if [[ $EUID -eq 0 ]]; then
log_warn "Running as root. BZZZ will be installed system-wide."
else
log_info "Running as non-root user. Some features may require sudo access."
fi
}
# Detect operating system
detect_os() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
OS_VERSION=$VERSION_ID
elif [[ -f /etc/redhat-release ]]; then
OS="centos"
elif [[ -f /etc/debian_version ]]; then
OS="debian"
else
error_exit "Unsupported operating system"
fi
log_info "Detected OS: $OS $OS_VERSION"
}
# Detect system architecture
detect_arch() {
ARCH=$(uname -m)
case $ARCH in
x86_64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
armv7l)
ARCH="armv7"
;;
*)
error_exit "Unsupported architecture: $ARCH"
;;
esac
log_info "Detected architecture: $ARCH"
}
# Check system requirements
check_requirements() {
log_info "Checking system requirements..."
# Check minimum memory (4GB recommended)
local mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local mem_gb=$((mem_kb / 1024 / 1024))
if [[ $mem_gb -lt 2 ]]; then
error_exit "Insufficient memory. Minimum 2GB required, 4GB recommended."
elif [[ $mem_gb -lt 4 ]]; then
log_warn "Memory is below recommended 4GB ($mem_gb GB available)"
fi
# Check disk space (minimum 10GB)
local disk_free=$(df / | awk 'NR==2 {print $4}')
local disk_gb=$((disk_free / 1024 / 1024))
if [[ $disk_gb -lt 10 ]]; then
error_exit "Insufficient disk space. Minimum 10GB free space required."
fi
log_success "System requirements check passed"
}
# Install system dependencies
install_dependencies() {
log_info "Installing system dependencies..."
case $OS in
ubuntu|debian)
sudo apt-get update -qq
sudo apt-get install -y \
curl \
wget \
gnupg \
lsb-release \
ca-certificates \
software-properties-common \
apt-transport-https \
jq \
net-tools \
openssh-client \
docker.io \
docker-compose
;;
centos|rhel|fedora)
sudo yum update -y
sudo yum install -y \
curl \
wget \
gnupg \
ca-certificates \
jq \
net-tools \
openssh-clients \
docker \
docker-compose
;;
*)
error_exit "Package installation not supported for OS: $OS"
;;
esac
# Ensure Docker is running
sudo systemctl enable docker
sudo systemctl start docker
# Add current user to docker group if not root
if [[ $EUID -ne 0 ]]; then
sudo usermod -aG docker $USER
log_warn "Added $USER to docker group. You may need to logout and login again."
fi
log_success "Dependencies installed successfully"
}
# Detect GPU configuration
detect_gpu() {
log_info "Detecting GPU configuration..."
GPU_COUNT=0
GPU_TYPE="none"
# Check for NVIDIA GPUs
if command -v nvidia-smi &>/dev/null; then
GPU_COUNT=$(nvidia-smi --list-gpus 2>/dev/null | wc -l || echo 0)
if [[ $GPU_COUNT -gt 0 ]]; then
GPU_TYPE="nvidia"
log_info "Detected $GPU_COUNT NVIDIA GPU(s)"
fi
fi
# Check for AMD GPUs
if [[ $GPU_COUNT -eq 0 ]] && command -v rocm-smi &>/dev/null; then
GPU_COUNT=$(rocm-smi --showid 2>/dev/null | grep -c "GPU" || echo 0)
if [[ $GPU_COUNT -gt 0 ]]; then
GPU_TYPE="amd"
log_info "Detected $GPU_COUNT AMD GPU(s)"
fi
fi
if [[ $GPU_COUNT -eq 0 ]]; then
log_info "No GPUs detected - CPU-only mode"
fi
export GPU_COUNT GPU_TYPE
}
# Prompt for Parallama installation
prompt_parallama_installation() {
if [[ $INSTALL_PARALLAMA == "prompt" ]]; then
echo
log_info "BZZZ can optionally install Parallama (multi-GPU Ollama fork) for enhanced AI capabilities."
echo
if [[ $GPU_COUNT -gt 1 ]]; then
echo -e "${GREEN}🚀 Multi-GPU Setup Detected ($GPU_COUNT ${GPU_TYPE^^} GPUs)${NC}"
echo " Parallama is RECOMMENDED for optimal multi-GPU performance!"
elif [[ $GPU_COUNT -eq 1 ]]; then
echo -e "${YELLOW}🎯 Single GPU Detected (${GPU_TYPE^^})${NC}"
echo " Parallama provides enhanced GPU utilization."
else
echo -e "${BLUE}💻 CPU-Only Setup${NC}"
echo " Parallama can still provide CPU optimizations."
fi
echo
echo "Options:"
echo "1. Install Parallama (recommended for GPU setups)"
echo "2. Install standard Ollama"
echo "3. Skip Ollama installation (configure later)"
echo
read -p "Choose option (1-3): " choice
case $choice in
1)
INSTALL_PARALLAMA="yes"
;;
2)
INSTALL_PARALLAMA="no"
;;
3)
INSTALL_PARALLAMA="skip"
;;
*)
log_warn "Invalid choice, defaulting to Parallama"
INSTALL_PARALLAMA="yes"
;;
esac
fi
}
# Install Ollama or Parallama
install_ollama() {
if [[ $INSTALL_PARALLAMA == "skip" ]]; then
log_info "Skipping Ollama installation"
return
fi
if [[ $INSTALL_PARALLAMA == "yes" ]]; then
log_info "Installing Parallama (multi-GPU Ollama fork)..."
# Download Parallama installer
if ! curl -fsSL https://chorus.services/parallama/install.sh | sh; then
log_error "Failed to install Parallama, falling back to standard Ollama"
install_standard_ollama
else
log_success "Parallama installed successfully"
# Configure Parallama for multi-GPU if available
if [[ $GPU_COUNT -gt 1 ]]; then
log_info "Configuring Parallama for $GPU_COUNT GPUs..."
# Parallama will be configured via the web UI
fi
fi
else
install_standard_ollama
fi
}
# Install standard Ollama
install_standard_ollama() {
log_info "Installing standard Ollama..."
if ! curl -fsSL https://ollama.ai/install.sh | sh; then
log_warn "Failed to install Ollama - you can install it later via the web UI"
else
log_success "Ollama installed successfully"
fi
}
# Download and install BZZZ binaries
install_bzzz_binaries() {
log_info "Downloading BZZZ binaries..."
local download_url="${BZZZ_BASE_URL}/releases/${BZZZ_VERSION}/bzzz-${OS}-${ARCH}.tar.gz"
local temp_dir=$(mktemp -d)
# Download binary package
if ! curl -fsSL "$download_url" -o "$temp_dir/bzzz.tar.gz"; then
error_exit "Failed to download BZZZ binaries from $download_url"
fi
# Extract binaries
sudo mkdir -p "$BZZZ_INSTALL_DIR"
sudo tar -xzf "$temp_dir/bzzz.tar.gz" -C "$BZZZ_INSTALL_DIR"
# Make binaries executable
sudo chmod +x "$BZZZ_INSTALL_DIR"/bin/*
# Create symlinks
sudo ln -sf "$BZZZ_INSTALL_DIR/bin/bzzz" /usr/local/bin/bzzz
sudo ln -sf "$BZZZ_INSTALL_DIR/bin/bzzz-mcp" /usr/local/bin/bzzz-mcp
# Cleanup
rm -rf "$temp_dir"
log_success "BZZZ binaries installed successfully"
}
# Setup configuration directories
setup_directories() {
log_info "Setting up directories..."
sudo mkdir -p "$BZZZ_CONFIG_DIR"
sudo mkdir -p "$BZZZ_LOG_DIR"
sudo mkdir -p "$BZZZ_DATA_DIR"
# Set permissions
local bzzz_user="bzzz"
# Create bzzz user if not exists
if ! id "$bzzz_user" &>/dev/null; then
sudo useradd -r -s /bin/false -d "$BZZZ_DATA_DIR" "$bzzz_user"
fi
sudo chown -R "$bzzz_user:$bzzz_user" "$BZZZ_CONFIG_DIR"
sudo chown -R "$bzzz_user:$bzzz_user" "$BZZZ_LOG_DIR"
sudo chown -R "$bzzz_user:$bzzz_user" "$BZZZ_DATA_DIR"
log_success "Directories created successfully"
}
# Install systemd services
install_services() {
log_info "Installing systemd services..."
# BZZZ Go service
sudo tee /etc/systemd/system/bzzz.service > /dev/null <<EOF
[Unit]
Description=BZZZ Distributed AI Coordination Service
Documentation=https://docs.chorus.services/bzzz
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=bzzz
Group=bzzz
WorkingDirectory=$BZZZ_DATA_DIR
Environment=BZZZ_CONFIG_DIR=$BZZZ_CONFIG_DIR
ExecStart=$BZZZ_INSTALL_DIR/bin/bzzz server --config $BZZZ_CONFIG_DIR/bzzz.yaml
ExecReload=/bin/kill -HUP \$MAINPID
Restart=always
RestartSec=10
KillMode=mixed
KillSignal=SIGTERM
TimeoutSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=bzzz
[Install]
WantedBy=multi-user.target
EOF
# BZZZ MCP service
sudo tee /etc/systemd/system/bzzz-mcp.service > /dev/null <<EOF
[Unit]
Description=BZZZ MCP Server for GPT-5 Integration
Documentation=https://docs.chorus.services/bzzz/mcp
After=network-online.target bzzz.service
Wants=network-online.target
Requires=bzzz.service
[Service]
Type=simple
User=bzzz
Group=bzzz
WorkingDirectory=$BZZZ_DATA_DIR
Environment=NODE_ENV=production
EnvironmentFile=-$BZZZ_CONFIG_DIR/mcp.env
ExecStart=$BZZZ_INSTALL_DIR/bin/bzzz-mcp
Restart=always
RestartSec=10
KillMode=mixed
KillSignal=SIGTERM
TimeoutSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=bzzz-mcp
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd
sudo systemctl daemon-reload
log_success "Systemd services installed"
}
# Generate initial configuration
generate_config() {
log_info "Generating initial configuration..."
# Detect network interface and IP
local primary_interface=$(ip route | grep default | awk '{print $5}' | head -n1)
local primary_ip=$(ip addr show "$primary_interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | head -n1)
local subnet=$(ip route | grep "$primary_interface" | grep '/' | head -n1 | awk '{print $1}')
# Generate node ID
local node_id="node-$(hostname -s)-$(date +%s)"
# Create basic configuration
sudo tee "$BZZZ_CONFIG_DIR/bzzz.yaml" > /dev/null <<EOF
# BZZZ Configuration - Generated by install script
# Complete configuration via web UI at http://$primary_ip:8080/setup
node:
id: "$node_id"
name: "$(hostname -s)"
address: "$primary_ip"
network:
listen_port: 8080
discovery_port: 8081
subnet: "$subnet"
interface: "$primary_interface"
cluster:
auto_discovery: true
bootstrap_nodes: []
services:
mcp_server:
enabled: true
port: 3000
web_ui:
enabled: true
port: 8080
security:
tls:
enabled: false # Will be configured via web UI
auth:
enabled: false # Will be configured via web UI
logging:
level: "info"
file: "$BZZZ_LOG_DIR/bzzz.log"
# Hardware configuration - detected during installation
hardware:
cpu_cores: $(nproc)
memory_gb: $mem_gb
gpus:
count: $GPU_COUNT
type: "$GPU_TYPE"
# Ollama/Parallama configuration
ollama:
enabled: $(if [[ $INSTALL_PARALLAMA != "skip" ]]; then echo "true"; else echo "false"; fi)
type: "$(if [[ $INSTALL_PARALLAMA == "yes" ]]; then echo "parallama"; else echo "ollama"; fi)"
endpoint: "http://localhost:11434"
models: [] # Will be configured via web UI
# Placeholder configurations - set via web UI
openai:
api_key: ""
model: "gpt-5"
cost_limits:
daily: 100.0
monthly: 1000.0
EOF
log_success "Initial configuration generated"
}
# Start configuration web server
start_config_server() {
log_info "Starting configuration server..."
# Start BZZZ service for configuration
sudo systemctl enable bzzz
sudo systemctl start bzzz
# Wait for service to be ready
local retries=30
local primary_ip=$(ip addr show $(ip route | grep default | awk '{print $5}' | head -n1) | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | head -n1)
while [[ $retries -gt 0 ]]; do
if curl -f "http://$primary_ip:8080/health" &>/dev/null; then
break
fi
sleep 2
((retries--))
done
if [[ $retries -eq 0 ]]; then
log_warn "Configuration server may not be ready. Check logs with: sudo journalctl -u bzzz -f"
fi
log_success "Configuration server started"
}
# Display completion message
show_completion_message() {
local primary_ip=$(ip addr show $(ip route | grep default | awk '{print $5}' | head -n1) | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | head -n1)
echo
log_success "BZZZ installation completed successfully!"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
echo -e "${GREEN}🚀 Next Steps:${NC}"
echo
echo "1. Complete your cluster configuration:"
echo " 👉 Open: ${BLUE}http://$primary_ip:8080/setup${NC}"
echo
echo "2. Useful commands:"
echo " • Check status: ${YELLOW}bzzz status${NC}"
echo " • View logs: ${YELLOW}sudo journalctl -u bzzz -f${NC}"
echo " • Start/Stop: ${YELLOW}sudo systemctl [start|stop] bzzz${NC}"
echo " • Configuration: ${YELLOW}sudo nano $BZZZ_CONFIG_DIR/bzzz.yaml${NC}"
echo
echo "3. Documentation:"
echo " 📚 Docs: ${BLUE}https://docs.chorus.services/bzzz${NC}"
echo " 💬 Support: ${BLUE}https://discord.gg/chorus-services${NC}"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
}
# Cleanup function for error handling
cleanup() {
if [[ -n "${temp_dir:-}" ]] && [[ -d "$temp_dir" ]]; then
rm -rf "$temp_dir"
fi
}
trap cleanup EXIT
# Main installation flow
main() {
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "${GREEN}🔥 BZZZ Distributed AI Coordination Platform${NC}"
echo " Installer v1.0"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo
check_root
detect_os
detect_arch
check_requirements
detect_gpu
install_dependencies
prompt_parallama_installation
install_ollama
install_bzzz_binaries
setup_directories
install_services
generate_config
start_config_server
show_completion_message
}
# Run main installation
main "$@"

View File

@@ -0,0 +1,71 @@
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates tzdata curl locales gettext-base systemd systemd-sysv && \
rm -rf /var/lib/apt/lists/*
# Configure systemd for container use
RUN cd /lib/systemd/system/sysinit.target.wants/ && \
ls | grep -v systemd-tmpfiles-setup | xargs rm -f && \
rm -f /lib/systemd/system/multi-user.target.wants/* && \
rm -f /etc/systemd/system/*.wants/* && \
rm -f /lib/systemd/system/local-fs.target.wants/* && \
rm -f /lib/systemd/system/sockets.target.wants/*udev* && \
rm -f /lib/systemd/system/sockets.target.wants/*initctl* && \
rm -f /lib/systemd/system/basic.target.wants/* && \
rm -f /lib/systemd/system/anaconda.target.wants/*
# Create bzzz directories
RUN mkdir -p /opt/bzzz /opt/bzzz/.bzzz /etc/systemd/system
# BZZZ binary
COPY ./build/bzzz /opt/bzzz/bzzz
RUN chmod +x /opt/bzzz/bzzz
# Config template
COPY ./config.yml.tmpl /opt/bzzz/.bzzz/config.yml.tmpl
# Create systemd service file
RUN cat > /etc/systemd/system/bzzz.service << 'EOF'
[Unit]
Description=BZZZ P2P Task Coordination System
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/bzzz
ExecStart=/opt/bzzz/bzzz -config /opt/bzzz/.bzzz/config.yml
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=bzzz
[Install]
WantedBy=multi-user.target
EOF
# Enable the service
RUN systemctl enable bzzz.service
# Create startup script that renders config and starts systemd
RUN cat > /opt/bzzz/startup.sh << 'EOF'
#!/bin/bash
set -e
# Render config from template
envsubst < /opt/bzzz/.bzzz/config.yml.tmpl > /opt/bzzz/.bzzz/config.yml
# Start systemd
exec /lib/systemd/systemd
EOF
RUN chmod +x /opt/bzzz/startup.sh
# Working directory
WORKDIR /opt/bzzz
# Use systemd as init system
ENTRYPOINT ["/opt/bzzz/startup.sh"]

View File

@@ -0,0 +1,69 @@
# Minimal BZZZ Docker container without systemd
# Uses multi-stage build for smaller final image
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -s -extldflags "-static"' -o bzzz .
# Final minimal image
FROM alpine:3.18
# Install only essential packages
RUN apk --no-cache add \
ca-certificates \
tzdata \
curl
# Create non-root user for security
RUN addgroup -g 1000 bzzz && \
adduser -u 1000 -G bzzz -s /bin/sh -D bzzz
# Create required directories
RUN mkdir -p /app/data /app/config /app/logs && \
chown -R bzzz:bzzz /app
# Copy binary from builder stage
COPY --from=builder /build/bzzz /app/bzzz
RUN chmod +x /app/bzzz
# Copy config template
COPY dockerize/config.minimal.yml.tmpl /app/config/config.yml.tmpl
# Create entrypoint script that handles config generation
RUN cat > /app/entrypoint.sh << 'EOF'
#!/bin/sh
set -e
# Generate config from template if it doesn't exist
if [ ! -f /app/config/config.yml ]; then
echo "🔧 Generating configuration from template..."
envsubst < /app/config/config.yml.tmpl > /app/config/config.yml
fi
# Ensure proper ownership
chown -R bzzz:bzzz /app/data /app/config /app/logs
echo "🚀 Starting BZZZ..."
exec "$@"
EOF
RUN chmod +x /app/entrypoint.sh
# Switch to non-root user
USER bzzz
WORKDIR /app
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8081/health || exit 1
# Expose ports
EXPOSE 8080 8081 9000-9100
# Set entrypoint and default command
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["/app/bzzz", "--config", "/app/config/config.yml"]

View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -e
# Build script for minimal BZZZ container
echo "🐳 Building minimal BZZZ container..."
# Set build context to parent directory (BZZZ root)
cd "$(dirname "$0")/.."
# Build the minimal container
docker build -f dockerize/Dockerfile.minimal -t bzzz:minimal .
echo "✅ BZZZ minimal container built successfully!"
echo ""
echo "Usage:"
echo " # Start with default configuration:"
echo " docker-compose -f dockerize/docker-compose.minimal.yml up -d"
echo ""
echo " # Start with custom environment:"
echo " cp dockerize/bzzz.minimal.env.example dockerize/bzzz.minimal.env"
echo " # Edit dockerize/bzzz.minimal.env with your settings"
echo " docker-compose -f dockerize/docker-compose.minimal.yml --env-file dockerize/bzzz.minimal.env up -d"
echo ""
echo " # Check logs:"
echo " docker-compose -f dockerize/docker-compose.minimal.yml logs -f"
echo ""
echo " # Check health:"
echo " curl http://localhost:8081/health"

View File

@@ -0,0 +1,14 @@
# Human-readable hint in rendered config header
NODE_HINT=swarm
# Unique id (defaults to $HOSTNAME if unset)
# AGENT_ID=
# Ollama endpoint (per-node host)
OLLAMA_BASE_URL=http://host.docker.internal:11434
# Log level: debug|info|warn|error
LOG_LEVEL=info
# UCXL storage path inside the container (persisted under /var/lib/bzzz)
UCXL_DIR=/var/lib/bzzz/ucxl

View File

@@ -0,0 +1,32 @@
# BZZZ Minimal Container Configuration
# Copy this file to bzzz.minimal.env and customize as needed
# Basic Agent Configuration
BZZZ_AGENT_ID=bzzz-docker-01
BZZZ_SPECIALIZATION=general_developer
BZZZ_MAX_TASKS=3
# Network Ports (adjust if ports are already in use)
BZZZ_P2P_PORT=9000
BZZZ_API_PORT=8080
BZZZ_HEALTH_PORT=8081
# Logging
LOG_LEVEL=info
# DEBUG=1 # Uncomment to enable debug logging
# DHT and P2P Settings
BZZZ_DHT_ENABLED=true
# BZZZ_BOOTSTRAP_PEERS= # Comma-separated list of bootstrap peers
# AI Configuration
OLLAMA_ENDPOINT=http://host.docker.internal:11434
# Licensing (if required)
# LICENSE_EMAIL=your.email@example.com
# LICENSE_KEY=your-license-key-here
CLUSTER_ID=docker-cluster
# Optional: Override default resource limits in docker-compose.minimal.yml
# MEMORY_LIMIT=1G
# CPU_LIMIT=1.0

View File

@@ -0,0 +1,91 @@
# BZZZ Configuration for Container Deployment
# Environment variables will be substituted at runtime
agent:
id: "${BZZZ_AGENT_ID}"
specialization: "${BZZZ_SPECIALIZATION}"
max_tasks: ${BZZZ_MAX_TASKS}
capabilities:
- "general_development"
- "task_coordination"
- "p2p_collaboration"
models:
- "llama3.1:8b"
- "codellama:7b"
role: "" # Will be auto-assigned based on specialization
expertise: []
reports_to: ""
network:
p2p:
listen_port: ${BZZZ_P2P_PORT}
bind_address: "0.0.0.0"
api:
port: ${BZZZ_API_PORT}
bind_address: "0.0.0.0"
health:
port: ${BZZZ_HEALTH_PORT}
bind_address: "0.0.0.0"
# DHT configuration for peer discovery
v2:
dht:
enabled: ${BZZZ_DHT_ENABLED}
bootstrap_peers: [] # Will be populated from BZZZ_BOOTSTRAP_PEERS env var
# AI configuration
ai:
ollama:
endpoint: "${OLLAMA_ENDPOINT}"
timeout: "30s"
# UCXL protocol configuration
ucxl:
enabled: true
server:
enabled: true
port: 8082
base_path: ""
storage:
directory: "/tmp/bzzz-ucxi-storage"
resolution:
cache_ttl: "1h"
# Licensing configuration (if required)
license:
email: "${LICENSE_EMAIL}"
license_key: "${LICENSE_KEY}"
cluster_id: "${CLUSTER_ID}"
organization_name: ""
kaching_url: "https://kaching.chorus.services"
is_active: false
grace_period_hours: 72
license_type: ""
max_nodes: 1
# Binary type for specialized behavior
binary_type: "agent"
# Repository integration (disabled in container mode)
repository:
provider: ""
url: ""
token: ""
webhook_url: ""
# Security settings optimized for containers
security:
enable_auth: false
auth_token: ""
# Storage paths for container environment
storage:
data_directory: "/app/data"
config_directory: "/app/config"
log_directory: "/app/logs"
# Logging configuration for containers (stdout/stderr)
logging:
level: "${LOG_LEVEL}"
format: "structured" # Better for container log collection
output: "stdout" # Force stdout for container compatibility

View File

@@ -0,0 +1,136 @@
# BZZZ Configuration for ${NODE_HINT:-container}
whoosh_api:
base_url: "https://whoosh.home.deepblack.cloud"
api_key: ""
timeout: 30s
retry_count: 3
agent:
id: "${AGENT_ID:-${HOSTNAME}}"
capabilities: ["general"]
poll_interval: 30s
max_tasks: 2
models: []
specialization: ""
model_selection_webhook: ""
default_reasoning_model: ""
sandbox_image: ""
role: ""
system_prompt: ""
reports_to: []
expertise: []
deliverables: []
collaboration:
preferred_message_types: []
auto_subscribe_to_roles: []
auto_subscribe_to_expertise: []
response_timeout_seconds: 0
max_collaboration_depth: 0
escalation_threshold: 0
custom_topic_subscriptions: []
github:
token_file: ""
user_agent: "BZZZ-Agent/1.0"
timeout: 30s
rate_limit: true
assignee: ""
p2p:
service_tag: "bzzz-peer-discovery"
bzzz_topic: "bzzz/coordination/v1"
hmmm_topic: "hmmm/meta-discussion/v1"
discovery_timeout: 10s
escalation_webhook: ""
escalation_keywords: []
conversation_limit: 10
logging:
level: "${LOG_LEVEL:-info}"
format: "text"
output: "stdout"
structured: false
slurp:
enabled: false
base_url: ""
api_key: ""
timeout: 30s
retry_count: 3
max_concurrent_requests: 10
request_queue_size: 100
v2:
enabled: false
protocol_version: "2.0.0"
uri_resolution:
cache_ttl: 5m0s
max_peers_per_result: 5
default_strategy: "best_match"
resolution_timeout: 30s
dht:
enabled: false
bootstrap_peers: []
mode: "auto"
protocol_prefix: "/bzzz"
bootstrap_timeout: 30s
discovery_interval: 1m0s
auto_bootstrap: false
semantic_addressing:
enable_wildcards: true
default_agent: "any"
default_role: "any"
default_project: "any"
enable_role_hierarchy: true
feature_flags:
uri_protocol: false
semantic_addressing: false
dht_discovery: false
advanced_resolution: false
ucxl:
enabled: false
server:
port: 8081
base_path: "/bzzz"
enabled: false
resolution:
cache_ttl: 5m0s
enable_wildcards: true
max_results: 50
storage:
type: "filesystem"
directory: "${UCXL_DIR:/var/lib/bzzz/ucxl}"
max_size: 104857600
p2p_integration:
enable_announcement: false
enable_discovery: false
announcement_topic: "bzzz/ucxl/announcement/v1"
discovery_timeout: 30s
security:
admin_key_shares:
threshold: 3
total_shares: 5
election_config:
heartbeat_timeout: 5s
discovery_timeout: 30s
election_timeout: 15s
max_discovery_attempts: 6
discovery_backoff: 5s
minimum_quorum: 3
consensus_algorithm: "raft"
split_brain_detection: true
conflict_resolution: "highest_uptime"
key_rotation_days: 90
audit_logging: false
audit_path: ""
ai:
ollama:
endpoint: "${OLLAMA_BASE_URL:-http://host.docker.internal:11434}"
timeout: 30s
models: []
openai:
api_key: ""
endpoint: "https://api.openai.com/v1"

View File

@@ -0,0 +1,112 @@
version: "3.9"
services:
bzzz-minimal:
image: bzzz:minimal
build:
context: ..
dockerfile: dockerize/Dockerfile.minimal
environment:
# Basic BZZZ configuration
- BZZZ_AGENT_ID=${BZZZ_AGENT_ID:-bzzz-docker-01}
- BZZZ_SPECIALIZATION=${BZZZ_SPECIALIZATION:-general_developer}
- BZZZ_MAX_TASKS=${BZZZ_MAX_TASKS:-3}
# Network configuration
- BZZZ_P2P_PORT=${BZZZ_P2P_PORT:-9000}
- BZZZ_API_PORT=${BZZZ_API_PORT:-8080}
- BZZZ_HEALTH_PORT=${BZZZ_HEALTH_PORT:-8081}
# Logging configuration
- LOG_LEVEL=${LOG_LEVEL:-info}
- DEBUG=${DEBUG:-}
# DHT and P2P settings
- BZZZ_DHT_ENABLED=${BZZZ_DHT_ENABLED:-true}
- BZZZ_BOOTSTRAP_PEERS=${BZZZ_BOOTSTRAP_PEERS:-}
# AI/Ollama configuration
- OLLAMA_ENDPOINT=${OLLAMA_ENDPOINT:-http://host.docker.internal:11434}
# Licensing (if required)
- LICENSE_EMAIL=${LICENSE_EMAIL:-}
- LICENSE_KEY=${LICENSE_KEY:-}
- CLUSTER_ID=${CLUSTER_ID:-docker-cluster}
# Persist data across container restarts
volumes:
- bzzz_data:/app/data
- bzzz_config:/app/config
- type: bind
source: /tmp/bzzz-ucxi-storage
target: /tmp/bzzz-ucxi-storage
- type: bind
source: /tmp/hcfs-workspaces
target: /tmp/hcfs-workspaces
# Network ports
ports:
- "${BZZZ_API_PORT:-8080}:8080" # HTTP API
- "${BZZZ_HEALTH_PORT:-8081}:8081" # Health check
- "${BZZZ_P2P_PORT:-9000}:9000" # P2P communication
# Container resource limits
deploy:
mode: replicated
replicas: 1
update_config:
order: start-first
parallelism: 1
failure_action: rollback
restart_policy:
condition: on-failure
delay: 10s
max_attempts: 3
resources:
limits:
cpus: "1.0"
memory: 1G
reservations:
cpus: "0.25"
memory: 256M
# Network configuration
networks:
- bzzz_net
# Host resolution for connecting to host services
extra_hosts:
- "host.docker.internal:host-gateway"
# Logging configuration for container runtime
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels: "service=bzzz"
# Health check
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Named volumes for persistence
volumes:
bzzz_data:
driver: local
bzzz_config:
driver: local
# Network for BZZZ communication
networks:
bzzz_net:
driver: overlay
attachable: true
ipam:
driver: default
config:
- subnet: 10.200.0.0/24

View File

@@ -0,0 +1,62 @@
version: "3.9"
services:
bzzz:
image: bzzz:latest
env_file:
- bzzz.env
# Persist identity/state per node
volumes:
- type: bind
source: /var/lib/bzzz
target: /var/lib/bzzz
- type: bind
source: /tmp/bzzz-ucxl-storage
target: /tmp/bzzz-ucxl-storage
- type: bind
source: /tmp/bzzz-ucxi-storage
target: /tmp/bzzz-ucxi-storage
- type: bind
source: /tmp/hcfs-workspaces
target: /tmp/hcfs-workspaces
# If you later enable ucxl.server.enabled: true and need to expose it:
ports:
- target: 8081
published: 8081
protocol: tcp
mode: host
deploy:
mode: replicated
replicas: 3
update_config:
order: start-first
parallelism: 1
failure_action: rollback
restart_policy:
condition: on-failure
resources:
limits:
cpus: "1.0"
memory: 2G
reservations:
cpus: "0.25"
memory: 512M
placement:
preferences:
- spread: node.id
networks:
- bzzz_net
# Lets the container resolve the node's host at host.docker.internal
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
bzzz_net:
driver: overlay
attachable: true
# driver_opts:
# encrypted: "true"

View File

@@ -0,0 +1,48 @@
package main
import (
"fmt"
"os"
"time"
)
// ContainerLogger provides structured logging for containers
// All output goes to stdout/stderr for container runtime collection
type ContainerLogger struct {
name string
}
// NewContainerLogger creates a new container-friendly logger
func NewContainerLogger(name string) *ContainerLogger {
return &ContainerLogger{name: name}
}
// Info logs informational messages to stdout
func (l *ContainerLogger) Info(msg string, args ...interface{}) {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
logMsg := fmt.Sprintf(msg, args...)
fmt.Fprintf(os.Stdout, "[%s] [INFO] [%s] %s\n", timestamp, l.name, logMsg)
}
// Warn logs warning messages to stdout
func (l *ContainerLogger) Warn(msg string, args ...interface{}) {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
logMsg := fmt.Sprintf(msg, args...)
fmt.Fprintf(os.Stdout, "[%s] [WARN] [%s] %s\n", timestamp, l.name, logMsg)
}
// Error logs error messages to stderr
func (l *ContainerLogger) Error(msg string, args ...interface{}) {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
logMsg := fmt.Sprintf(msg, args...)
fmt.Fprintf(os.Stderr, "[%s] [ERROR] [%s] %s\n", timestamp, l.name, logMsg)
}
// Debug logs debug messages to stdout (only if DEBUG env var is set)
func (l *ContainerLogger) Debug(msg string, args ...interface{}) {
if os.Getenv("DEBUG") != "" {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
logMsg := fmt.Sprintf(msg, args...)
fmt.Fprintf(os.Stdout, "[%s] [DEBUG] [%s] %s\n", timestamp, l.name, logMsg)
}
}

View File

@@ -0,0 +1,373 @@
# HCFS-Integrated Development Environment
This directory contains Docker configurations for creating HCFS-enabled development environments that provide AI agents with persistent, context-aware workspaces.
## 🎯 Overview
Instead of using temporary directories that are lost when containers stop, this system integrates with HCFS (Hierarchical Context File System) to provide:
- **Persistent Workspaces**: Agent work is stored in HCFS and survives container restarts
- **Context Sharing**: Multiple agents can access and build upon each other's work
- **Intelligent Artifact Collection**: Important files are automatically stored in HCFS
- **Role-Based Access**: Agents can access context relevant to their specialization
- **Feedback Learning**: The RL Context Curator learns from agent success/failure patterns
## 🏗️ Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Bzzz Agents │ │ HCFS-Enabled │ │ HCFS Core │
│ │ │ Containers │ │ │
│ • CLI Agents │◄──►│ │◄──►│ • Context API │
│ • Ollama Models │ │ • Python Dev │ │ • RL Curator │
│ • Reasoning │ │ • Node.js Dev │ │ • Storage │
│ • Code Review │ │ • Go Dev │ │ • Search │
└─────────────────┘ │ • Generic Base │ └─────────────────┘
└─────────────────┘
```
## 🐳 Available Images
### Base Image: `bzzz-hcfs-base`
- Ubuntu 22.04 with HCFS integration
- Standard development tools (git, make, curl, etc.)
- HCFS workspace management scripts
- Agent user with proper permissions
- FUSE support for HCFS mounting
### Language-Specific Images:
#### `bzzz-hcfs-python`
- Python 3.10 with comprehensive ML/AI packages
- Jupyter Lab/Notebook support
- Popular frameworks: Flask, FastAPI, Django
- Data science stack: NumPy, Pandas, scikit-learn
- Deep learning: PyTorch, Transformers
- **Ports**: 8888 (Jupyter), 8000, 5000, 8080
#### `bzzz-hcfs-nodejs`
- Node.js 20 with modern JavaScript/TypeScript tools
- Package managers: npm, yarn
- Build tools: Webpack, Vite, Rollup
- Testing: Jest, Mocha, Cypress
- **Ports**: 3000, 8080, 8000, 9229 (debugger)
#### `bzzz-hcfs-go`
- Go 1.21 with standard development tools
- Popular frameworks: Gin, Echo, Fiber
- Development tools: Delve debugger, Air live reload
- **Ports**: 8080, 8000, 9000, 2345 (debugger)
## 🚀 Quick Start
### 1. Build the Images
```bash
cd /home/tony/AI/projects/Bzzz/docker
./build-hcfs-images.sh build
```
### 2. Start the HCFS Ecosystem
```bash
docker-compose -f docker-compose.hcfs.yml up -d
```
### 3. Access Development Environments
**Python Development:**
```bash
# Interactive shell
docker exec -it agent-python-dev bash
# Jupyter Lab
open http://localhost:8888
```
**Node.js Development:**
```bash
# Interactive shell
docker exec -it agent-nodejs-dev bash
# Start development server
docker exec -it agent-nodejs-dev npm run dev
```
**Go Development:**
```bash
# Interactive shell
docker exec -it agent-go-dev bash
# Build and run
docker exec -it agent-go-dev make build run
```
## 🔧 Configuration
### Environment Variables
**Required for HCFS Integration:**
- `AGENT_ID`: Unique identifier for the agent
- `TASK_ID`: Task identifier for workspace context
- `HCFS_API_URL`: HCFS API endpoint (default: http://host.docker.internal:8000)
- `HCFS_ENABLED`: Enable/disable HCFS integration (default: true)
**Optional:**
- `GIT_USER_NAME`: Git configuration
- `GIT_USER_EMAIL`: Git configuration
- `SETUP_PYTHON_VENV`: Create Python virtual environment
- `NODE_ENV`: Node.js environment mode
### HCFS Configuration
Each container includes `/etc/hcfs/hcfs-agent.yaml` with:
- API endpoints and timeouts
- Workspace settings
- Artifact collection patterns
- Security configurations
- Logging preferences
## 💾 Workspace Management
### Automatic Features
1. **Workspace Initialization**: Creates HCFS context for agent workspace
2. **Continuous Sync**: Background daemon syncs workspace state every 30 seconds
3. **Artifact Collection**: Automatically stores important files:
- Log files (*.log)
- Documentation (*.md, README*)
- Configuration (*.json, *.yaml)
- Build outputs (build/*, output/*)
- Results (results/*)
4. **Graceful Shutdown**: Collects final artifacts when container stops
### Manual Commands
```bash
# Sync current workspace state
/opt/hcfs/hcfs-workspace.sh sync
# Collect and store artifacts
/opt/hcfs/hcfs-workspace.sh collect
# Finalize workspace (run on completion)
/opt/hcfs/hcfs-workspace.sh finalize
# Check workspace status
/opt/hcfs/hcfs-workspace.sh status
```
## 🔄 Integration with Bzzz Agents
### Updated Sandbox Creation
The Bzzz sandbox system now supports HCFS workspaces:
```go
// Create HCFS-enabled sandbox
sandbox, err := CreateSandboxWithHCFS(ctx, taskImage, agentConfig, agentID, taskID)
// Check if using HCFS
if sandbox.IsUsingHCFS() {
workspace := sandbox.GetHCFSWorkspace()
fmt.Printf("Using HCFS workspace: %s\n", workspace.HCFSPath)
}
```
### Configuration in Bzzz
Add HCFS configuration to your Bzzz agent config:
```yaml
hcfs:
enabled: true
api_url: "http://localhost:8000"
mount_path: "/tmp/hcfs-workspaces"
store_artifacts: true
idle_cleanup_interval: "15m"
max_idle_time: "1h"
```
## 📊 Monitoring and Debugging
### Service Health Checks
```bash
# Check HCFS API
curl http://localhost:8000/health
# Check RL Tuner
curl http://localhost:8001/health
# View container logs
docker-compose -f docker-compose.hcfs.yml logs -f hcfs-api
```
### Workspace Status
```bash
# View workspace metadata
cat /home/agent/work/.hcfs-workspace
# Check sync daemon status
ps aux | grep hcfs-workspace
# View HCFS logs
tail -f /var/log/hcfs/workspace.log
```
## 🛠️ Development Workflows
### Python ML Development
```bash
# Start Python environment
docker exec -it agent-python-dev bash
# Create new project
cd /home/agent/work
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Start Jupyter for data exploration
jupyter lab --ip=0.0.0.0 --port=8888
# Artifacts automatically collected:
# - *.ipynb notebooks
# - model files in models/
# - results in output/
```
### Node.js Web Development
```bash
# Start Node.js environment
docker exec -it agent-nodejs-dev bash
# Initialize project
cd /home/agent/work
cp package.json.template package.json
npm install
# Start development server
npm run dev
# Artifacts automatically collected:
# - package*.json
# - build output in dist/
# - logs in logs/
```
### Go Microservices
```bash
# Start Go environment
docker exec -it agent-go-dev bash
# Initialize project
cd /home/agent/work
cp go.mod.template go.mod
cp main.go.template main.go
go mod tidy
# Build and run
make build
make run
# Artifacts automatically collected:
# - go.mod, go.sum
# - binary in bin/
# - test results
```
## 🔒 Security Considerations
### Container Security
- Agents run as non-root `agent` user
- Limited sudo access only for FUSE mounts
- Network restrictions block sensitive ports
- Read-only access to system directories
### HCFS Security
- Context access controlled by agent roles
- Workspace isolation between agents
- Artifact encryption (optional)
- Audit logging of all operations
## 🔄 Backup and Recovery
### Workspace Persistence
Agent workspaces are stored in named Docker volumes:
- `python-workspace`: Python development files
- `nodejs-workspace`: Node.js development files
- `go-workspace`: Go development files
### HCFS Data
Core HCFS data is stored in:
- `hcfs-data`: Main context database
- `hcfs-rl-data`: RL Context Curator data
### Backup Script
```bash
# Backup all workspace data
docker run --rm -v python-workspace:/data -v /backup:/backup alpine \
tar czf /backup/python-workspace-$(date +%Y%m%d).tar.gz -C /data .
```
## 🐛 Troubleshooting
### Common Issues
**HCFS API Not Available:**
```bash
# Check if HCFS container is running
docker ps | grep hcfs-api
# Check network connectivity
docker exec agent-python-dev curl -f http://hcfs-api:8000/health
```
**FUSE Mount Failures:**
```bash
# Check FUSE support
docker exec agent-python-dev ls -la /dev/fuse
# Check mount permissions
docker exec agent-python-dev mount | grep fuse
```
**Workspace Sync Issues:**
```bash
# Restart sync daemon
docker exec agent-python-dev pkill -f hcfs-workspace
docker exec agent-python-dev /opt/hcfs/hcfs-workspace.sh daemon &
# Manual sync
docker exec agent-python-dev /opt/hcfs/hcfs-workspace.sh sync
```
### Log Locations
- HCFS API: `docker logs hcfs-api`
- Agent containers: `docker logs agent-python-dev`
- Workspace sync: `/var/log/hcfs/workspace.log` (inside container)
## 📚 Additional Resources
- [HCFS Documentation](../HCFS/README.md)
- [Bzzz Agent Configuration](../README.md)
- [RL Context Curator Guide](../HCFS/integration_tests/README.md)
- [Docker Compose Reference](https://docs.docker.com/compose/)
## 🎯 Next Steps
1. **Deploy to Production**: Use Docker Swarm or Kubernetes
2. **Scale Horizontally**: Add more agent instances
3. **Custom Images**: Create domain-specific development environments
4. **Monitoring**: Add Prometheus/Grafana for metrics
5. **CI/CD Integration**: Automate testing and deployment

View File

@@ -0,0 +1,358 @@
#!/bin/bash
set -euo pipefail
# HCFS Docker Images Build Script
# Builds all HCFS-enabled development environment containers
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
REGISTRY="${DOCKER_REGISTRY:-registry.home.deepblack.cloud}"
NAMESPACE="${DOCKER_NAMESPACE:-tony}"
VERSION="${VERSION:-latest}"
BUILD_PARALLEL="${BUILD_PARALLEL:-false}"
# Logging functions
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
# Function to build a single image
build_image() {
local image_name="$1"
local dockerfile_dir="$2"
local build_args="$3"
log "Building image: $image_name"
local full_image_name="$REGISTRY/$NAMESPACE/$image_name:$VERSION"
local build_cmd="docker build"
# Add build arguments if provided
if [ -n "$build_args" ]; then
build_cmd="$build_cmd $build_args"
fi
# Add tags
build_cmd="$build_cmd -t $image_name:$VERSION -t $image_name:latest"
build_cmd="$build_cmd -t $full_image_name"
# Add dockerfile directory
build_cmd="$build_cmd $dockerfile_dir"
if eval $build_cmd; then
success "Built image: $image_name"
return 0
else
error "Failed to build image: $image_name"
return 1
fi
}
# Function to prepare HCFS SDK files
prepare_hcfs_sdks() {
log "Preparing HCFS SDK files..."
local sdk_dir="$SCRIPT_DIR/sdks"
mkdir -p "$sdk_dir"
# Copy Python SDK
if [ -d "$PROJECT_ROOT/../HCFS/hcfs-python" ]; then
cp -r "$PROJECT_ROOT/../HCFS/hcfs-python" "$sdk_dir/hcfs-python-sdk"
success "Copied Python HCFS SDK"
else
warning "Python HCFS SDK not found, creating minimal version"
mkdir -p "$sdk_dir/hcfs-python-sdk"
cat > "$sdk_dir/hcfs-python-sdk/setup.py" << 'EOF'
from setuptools import setup, find_packages
setup(
name="hcfs-sdk",
version="1.0.0",
packages=find_packages(),
install_requires=["httpx", "pydantic"],
)
EOF
mkdir -p "$sdk_dir/hcfs-python-sdk/hcfs"
echo "# HCFS Python SDK Placeholder" > "$sdk_dir/hcfs-python-sdk/hcfs/__init__.py"
fi
# Create Node.js SDK
mkdir -p "$sdk_dir/hcfs-nodejs-sdk"
cat > "$sdk_dir/hcfs-nodejs-sdk/package.json" << 'EOF'
{
"name": "@hcfs/sdk",
"version": "1.0.0",
"description": "HCFS Node.js SDK",
"main": "index.js",
"dependencies": {
"axios": "^1.0.0"
}
}
EOF
echo "module.exports = { HCFSClient: class HCFSClient {} };" > "$sdk_dir/hcfs-nodejs-sdk/index.js"
# Create Go SDK
mkdir -p "$sdk_dir/hcfs-go-sdk"
cat > "$sdk_dir/hcfs-go-sdk/go.mod" << 'EOF'
module github.com/hcfs/go-sdk
go 1.21
require (
github.com/go-resty/resty/v2 v2.7.0
)
EOF
cat > "$sdk_dir/hcfs-go-sdk/client.go" << 'EOF'
package client
import "github.com/go-resty/resty/v2"
type HCFSClient struct {
client *resty.Client
baseURL string
}
func NewHCFSClient(baseURL string) (*HCFSClient, error) {
return &HCFSClient{
client: resty.New(),
baseURL: baseURL,
}, nil
}
EOF
success "HCFS SDKs prepared"
}
# Function to copy scripts
prepare_scripts() {
log "Preparing build scripts..."
# Copy scripts to each image directory
for image_dir in "$SCRIPT_DIR"/hcfs-*; do
if [ -d "$image_dir" ]; then
mkdir -p "$image_dir/scripts"
mkdir -p "$image_dir/config"
mkdir -p "$image_dir/hcfs-client"
# Copy common scripts
cp "$SCRIPT_DIR/hcfs-base/scripts/"* "$image_dir/scripts/" 2>/dev/null || true
cp "$SCRIPT_DIR/hcfs-base/config/"* "$image_dir/config/" 2>/dev/null || true
# Copy HCFS client
cp -r "$SCRIPT_DIR/sdks/hcfs-python-sdk/"* "$image_dir/hcfs-client/" 2>/dev/null || true
fi
done
success "Scripts prepared"
}
# Function to validate prerequisites
validate_prerequisites() {
log "Validating prerequisites..."
# Check if Docker is available
if ! command -v docker &> /dev/null; then
error "Docker is not installed or not in PATH"
exit 1
fi
# Check if Docker daemon is running
if ! docker info &> /dev/null; then
error "Docker daemon is not running"
exit 1
fi
# Check if required directories exist
if [ ! -d "$SCRIPT_DIR/hcfs-base" ]; then
error "Base image directory not found: $SCRIPT_DIR/hcfs-base"
exit 1
fi
success "Prerequisites validated"
}
# Function to build all images
build_all_images() {
log "Building HCFS development environment images..."
local images=(
"bzzz-hcfs-base:$SCRIPT_DIR/hcfs-base:"
"bzzz-hcfs-python:$SCRIPT_DIR/hcfs-python:"
"bzzz-hcfs-nodejs:$SCRIPT_DIR/hcfs-nodejs:"
"bzzz-hcfs-go:$SCRIPT_DIR/hcfs-go:"
)
local failed_builds=()
if [ "$BUILD_PARALLEL" = "true" ]; then
log "Building images in parallel..."
local pids=()
for image_spec in "${images[@]}"; do
IFS=':' read -r image_name dockerfile_dir build_args <<< "$image_spec"
(build_image "$image_name" "$dockerfile_dir" "$build_args") &
pids+=($!)
done
# Wait for all builds to complete
for pid in "${pids[@]}"; do
if ! wait $pid; then
failed_builds+=("PID:$pid")
fi
done
else
log "Building images sequentially..."
for image_spec in "${images[@]}"; do
IFS=':' read -r image_name dockerfile_dir build_args <<< "$image_spec"
if ! build_image "$image_name" "$dockerfile_dir" "$build_args"; then
failed_builds+=("$image_name")
fi
done
fi
# Report results
if [ ${#failed_builds[@]} -eq 0 ]; then
success "All images built successfully!"
else
error "Failed to build images: ${failed_builds[*]}"
return 1
fi
}
# Function to push images to registry
push_images() {
log "Pushing images to registry: $REGISTRY"
local images=(
"bzzz-hcfs-base"
"bzzz-hcfs-python"
"bzzz-hcfs-nodejs"
"bzzz-hcfs-go"
)
for image in "${images[@]}"; do
local full_name="$REGISTRY/$NAMESPACE/$image:$VERSION"
log "Pushing $full_name..."
if docker push "$full_name"; then
success "Pushed $full_name"
else
warning "Failed to push $full_name"
fi
done
}
# Function to run tests
test_images() {
log "Testing built images..."
local images=(
"bzzz-hcfs-base"
"bzzz-hcfs-python"
"bzzz-hcfs-nodejs"
"bzzz-hcfs-go"
)
for image in "${images[@]}"; do
log "Testing $image..."
# Basic smoke test
if docker run --rm "$image:$VERSION" /bin/echo "Image $image test successful"; then
success "Test passed: $image"
else
warning "Test failed: $image"
fi
done
}
# Function to clean up
cleanup() {
log "Cleaning up temporary files..."
# Remove copied SDK files
rm -rf "$SCRIPT_DIR/sdks"
# Clean up dangling images
docker image prune -f &> /dev/null || true
success "Cleanup completed"
}
# Main execution
main() {
local command="${1:-build}"
case $command in
"build")
validate_prerequisites
prepare_hcfs_sdks
prepare_scripts
build_all_images
;;
"push")
push_images
;;
"test")
test_images
;;
"all")
validate_prerequisites
prepare_hcfs_sdks
prepare_scripts
build_all_images
test_images
push_images
;;
"clean")
cleanup
;;
"help"|*)
echo "HCFS Docker Images Build Script"
echo ""
echo "Usage: $0 {build|push|test|all|clean|help}"
echo ""
echo "Commands:"
echo " build - Build all HCFS development images"
echo " push - Push images to registry"
echo " test - Run smoke tests on built images"
echo " all - Build, test, and push images"
echo " clean - Clean up temporary files"
echo " help - Show this help message"
echo ""
echo "Environment Variables:"
echo " DOCKER_REGISTRY - Docker registry URL (default: registry.home.deepblack.cloud)"
echo " DOCKER_NAMESPACE - Docker namespace (default: tony)"
echo " VERSION - Image version tag (default: latest)"
echo " BUILD_PARALLEL - Build images in parallel (default: false)"
exit 0
;;
esac
}
# Set up signal handlers for cleanup
trap cleanup EXIT INT TERM
# Execute main function
main "$@"

View File

@@ -0,0 +1,247 @@
# HCFS Development Ecosystem
# Complete Docker Compose setup for HCFS-enabled agent development
version: '3.8'
services:
# HCFS Core API Service
hcfs-api:
image: hcfs:latest
container_name: hcfs-api
ports:
- "8000:8000"
environment:
- HCFS_DATABASE_URL=sqlite:///data/hcfs.db
- HCFS_HOST=0.0.0.0
- HCFS_PORT=8000
- HCFS_LOG_LEVEL=info
volumes:
- hcfs-data:/data
- hcfs-logs:/logs
networks:
- hcfs-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# HCFS RL Context Curator
hcfs-rl-tuner:
image: hcfs:latest
container_name: hcfs-rl-tuner
ports:
- "8001:8001"
environment:
- HCFS_API_URL=http://hcfs-api:8000
- RL_TUNER_HOST=0.0.0.0
- RL_TUNER_PORT=8001
volumes:
- hcfs-rl-data:/data
networks:
- hcfs-network
depends_on:
- hcfs-api
restart: unless-stopped
command: ["python", "-m", "hcfs.rl_curator.rl_tuner_service"]
# Python Development Agent
agent-python:
build:
context: ./hcfs-python
dockerfile: Dockerfile
container_name: agent-python-dev
ports:
- "8888:8888" # Jupyter
- "8080:8080" # Development server
environment:
- AGENT_ID=python-dev-agent
- TASK_ID=development-task
- HCFS_API_URL=http://hcfs-api:8000
- HCFS_ENABLED=true
- GIT_USER_NAME=HCFS Agent
- GIT_USER_EMAIL=agent@hcfs.local
volumes:
- python-workspace:/home/agent/work
- python-cache:/home/agent/.cache
networks:
- hcfs-network
depends_on:
- hcfs-api
stdin_open: true
tty: true
restart: unless-stopped
# Node.js Development Agent
agent-nodejs:
build:
context: ./hcfs-nodejs
dockerfile: Dockerfile
container_name: agent-nodejs-dev
ports:
- "3000:3000" # Node.js app
- "9229:9229" # Node.js debugger
environment:
- AGENT_ID=nodejs-dev-agent
- TASK_ID=development-task
- HCFS_API_URL=http://hcfs-api:8000
- HCFS_ENABLED=true
- NODE_ENV=development
volumes:
- nodejs-workspace:/home/agent/work
- nodejs-cache:/home/agent/.npm
networks:
- hcfs-network
depends_on:
- hcfs-api
stdin_open: true
tty: true
restart: unless-stopped
# Go Development Agent
agent-go:
build:
context: ./hcfs-go
dockerfile: Dockerfile
container_name: agent-go-dev
ports:
- "8090:8080" # Go web server
- "2345:2345" # Delve debugger
environment:
- AGENT_ID=go-dev-agent
- TASK_ID=development-task
- HCFS_API_URL=http://hcfs-api:8000
- HCFS_ENABLED=true
- CGO_ENABLED=1
volumes:
- go-workspace:/home/agent/work
- go-cache:/home/agent/.cache
networks:
- hcfs-network
depends_on:
- hcfs-api
stdin_open: true
tty: true
restart: unless-stopped
# Generic Development Agent (base image)
agent-generic:
build:
context: ./hcfs-base
dockerfile: Dockerfile
container_name: agent-generic-dev
ports:
- "8050:8080"
environment:
- AGENT_ID=generic-dev-agent
- TASK_ID=development-task
- HCFS_API_URL=http://hcfs-api:8000
- HCFS_ENABLED=true
volumes:
- generic-workspace:/home/agent/work
networks:
- hcfs-network
depends_on:
- hcfs-api
stdin_open: true
tty: true
restart: unless-stopped
# HCFS Management Dashboard (optional)
hcfs-dashboard:
image: nginx:alpine
container_name: hcfs-dashboard
ports:
- "8080:80"
volumes:
- ./dashboard:/usr/share/nginx/html:ro
networks:
- hcfs-network
depends_on:
- hcfs-api
restart: unless-stopped
# Development Database (PostgreSQL for advanced features)
postgres:
image: postgres:15-alpine
container_name: hcfs-postgres
environment:
- POSTGRES_DB=hcfs
- POSTGRES_USER=hcfs
- POSTGRES_PASSWORD=hcfs_password
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- hcfs-network
restart: unless-stopped
# Redis for caching and session management
redis:
image: redis:7-alpine
container_name: hcfs-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- hcfs-network
restart: unless-stopped
# MinIO for object storage (artifact storage)
minio:
image: minio/minio:latest
container_name: hcfs-minio
ports:
- "9000:9000"
- "9001:9001"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin123
volumes:
- minio-data:/data
networks:
- hcfs-network
command: server /data --console-address ":9001"
restart: unless-stopped
networks:
hcfs-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
volumes:
# HCFS Core Data
hcfs-data:
driver: local
hcfs-logs:
driver: local
hcfs-rl-data:
driver: local
# Agent Workspaces (persistent across container restarts)
python-workspace:
driver: local
python-cache:
driver: local
nodejs-workspace:
driver: local
nodejs-cache:
driver: local
go-workspace:
driver: local
go-cache:
driver: local
generic-workspace:
driver: local
# Infrastructure Data
postgres-data:
driver: local
redis-data:
driver: local
minio-data:
driver: local

View File

@@ -0,0 +1,131 @@
# HCFS Base Image - Production-ready environment with HCFS integration
FROM ubuntu:22.04
LABEL maintainer="anthony@deepblack.cloud"
LABEL description="HCFS-integrated base image for AI agent development environments"
LABEL version="1.0.0"
# Prevent interactive prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
ENV TERM=xterm-256color
# Set up standard environment
ENV HCFS_WORKSPACE_ROOT=/workspace
ENV HCFS_MOUNT_POINT=/mnt/hcfs
ENV HCFS_API_URL=http://host.docker.internal:8000
ENV HCFS_ENABLED=true
ENV PYTHONPATH=/usr/local/lib/python3.10/site-packages:$PYTHONPATH
# Create agent user for sandboxed execution
RUN groupadd -r agent && useradd -r -g agent -d /home/agent -s /bin/bash agent
# Install system dependencies
RUN apt-get update && apt-get install -y \
# Core system tools
curl \
wget \
git \
make \
build-essential \
software-properties-common \
gnupg2 \
lsb-release \
ca-certificates \
apt-transport-https \
# Development essentials
vim \
nano \
tree \
jq \
zip \
unzip \
rsync \
tmux \
screen \
htop \
# Network tools
net-tools \
iputils-ping \
dnsutils \
# Python and pip
python3 \
python3-pip \
python3-dev \
python3-venv \
# FUSE for HCFS mounting
fuse3 \
libfuse3-dev \
# Additional utilities
sqlite3 \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Set up Python symlinks
RUN ln -sf /usr/bin/python3 /usr/bin/python && \
ln -sf /usr/bin/pip3 /usr/bin/pip
# Install HCFS Python SDK and dependencies
RUN pip install --no-cache-dir \
httpx \
websockets \
fastapi \
uvicorn \
pydantic \
python-multipart \
aiofiles \
sentence-transformers \
numpy \
scipy \
scikit-learn \
requests \
pyyaml \
toml \
click
# Create directory structure
RUN mkdir -p \
/workspace \
/mnt/hcfs \
/home/agent \
/home/agent/work \
/home/agent/.local \
/home/agent/.cache \
/opt/hcfs \
/etc/hcfs \
/var/log/hcfs
# Set up HCFS integration scripts
COPY scripts/hcfs-init.sh /opt/hcfs/
COPY scripts/hcfs-mount.sh /opt/hcfs/
COPY scripts/hcfs-workspace.sh /opt/hcfs/
COPY scripts/entrypoint.sh /opt/hcfs/
COPY config/hcfs-agent.yaml /etc/hcfs/
# Make scripts executable
RUN chmod +x /opt/hcfs/*.sh
# Install HCFS client library
COPY hcfs-client /opt/hcfs/client
RUN cd /opt/hcfs/client && pip install -e .
# Set up agent workspace
RUN chown -R agent:agent /home/agent /workspace /mnt/hcfs
RUN chmod 755 /home/agent /workspace
# Configure sudo for agent user (needed for FUSE mounts)
RUN echo "agent ALL=(ALL) NOPASSWD: /bin/mount, /bin/umount, /usr/bin/fusermount3" >> /etc/sudoers
# Set default working directory
WORKDIR /home/agent/work
# Environment for development
ENV HOME=/home/agent
ENV USER=agent
ENV SHELL=/bin/bash
# Expose standard ports for development services
EXPOSE 8080 8000 3000 5000
# Set up entrypoint that initializes HCFS workspace
ENTRYPOINT ["/opt/hcfs/entrypoint.sh"]
CMD ["/bin/bash"]

View File

@@ -0,0 +1,137 @@
# HCFS Agent Configuration
# This configuration is used by agents running in HCFS-enabled containers
hcfs:
# HCFS API Configuration
api:
url: "http://host.docker.internal:8000"
timeout: 30s
retry_count: 3
# Workspace Configuration
workspace:
root: "/home/agent/work"
mount_point: "/mnt/hcfs"
auto_sync: true
sync_interval: 30s
# Artifact Collection
artifacts:
enabled: true
patterns:
- "*.log"
- "*.md"
- "*.txt"
- "*.json"
- "*.yaml"
- "output/*"
- "build/*.json"
- "results/*"
max_size: "10MB"
compress: false
# Cleanup Configuration
cleanup:
idle_timeout: "1h"
auto_cleanup: true
preserve_artifacts: true
# Agent Capabilities
agent:
capabilities:
- "file_operations"
- "command_execution"
- "artifact_collection"
- "context_sharing"
- "workspace_management"
# Resource Limits
limits:
max_memory: "2GB"
max_cpu: "2.0"
max_disk: "10GB"
max_files: 10000
# Development Tools
tools:
python:
enabled: true
version: "3.10"
venv: true
packages:
- "requests"
- "pyyaml"
- "click"
- "rich"
git:
enabled: true
auto_config: true
make:
enabled: true
docker:
enabled: false # Disabled by default for security
# Security Configuration
security:
user: "agent"
home: "/home/agent"
shell: "/bin/bash"
# Network restrictions
network:
allow_outbound: true
blocked_ports:
- 22 # SSH
- 3389 # RDP
- 5432 # PostgreSQL
- 3306 # MySQL
# File system restrictions
filesystem:
read_only_paths:
- "/etc"
- "/usr"
- "/boot"
writable_paths:
- "/home/agent"
- "/tmp"
- "/workspace"
- "/mnt/hcfs"
# Logging Configuration
logging:
level: "info"
format: "json"
destinations:
- "/var/log/hcfs/agent.log"
- "stdout"
# Log categories
categories:
workspace: "debug"
artifacts: "info"
hcfs_api: "info"
security: "warn"
# Environment Variables
environment:
PYTHONPATH: "/usr/local/lib/python3.10/site-packages"
PATH: "/home/agent/.local/bin:/usr/local/bin:/usr/bin:/bin"
TERM: "xterm-256color"
EDITOR: "vim"
# Container Metadata
metadata:
version: "1.0.0"
created_by: "bzzz-hcfs-integration"
description: "HCFS-enabled agent container for distributed AI development"
# Tags for categorization
tags:
- "ai-agent"
- "hcfs-enabled"
- "development"
- "sandboxed"

View File

@@ -0,0 +1,197 @@
#!/bin/bash
set -euo pipefail
# HCFS Agent Container Entrypoint
echo "🚀 Starting HCFS-enabled agent container..."
# Environment validation
AGENT_ID="${AGENT_ID:-agent-$(hostname)}"
TASK_ID="${TASK_ID:-task-$(date +%s)}"
HCFS_API_URL="${HCFS_API_URL:-http://host.docker.internal:8000}"
HCFS_ENABLED="${HCFS_ENABLED:-true}"
echo "📋 Container Configuration:"
echo " Agent ID: $AGENT_ID"
echo " Task ID: $TASK_ID"
echo " HCFS API: $HCFS_API_URL"
echo " HCFS Enabled: $HCFS_ENABLED"
# Function to wait for HCFS API
wait_for_hcfs() {
local max_attempts=30
local attempt=0
echo "⏳ Waiting for HCFS API to be available..."
while [ $attempt -lt $max_attempts ]; do
if curl -s "$HCFS_API_URL/health" > /dev/null 2>&1; then
echo "✅ HCFS API is available"
return 0
fi
echo " Attempt $((attempt + 1))/$max_attempts - HCFS API not ready"
sleep 2
attempt=$((attempt + 1))
done
echo "❌ HCFS API failed to become available after $max_attempts attempts"
return 1
}
# Function to initialize HCFS workspace
init_hcfs_workspace() {
echo "🔧 Initializing HCFS workspace..."
# Create workspace context in HCFS
local workspace_path="/agents/$AGENT_ID/workspaces/$(date +%s)"
local context_data=$(cat <<EOF
{
"path": "$workspace_path",
"content": "Agent workspace for container $(hostname)",
"summary": "Agent $AGENT_ID workspace - Task $TASK_ID",
"metadata": {
"agent_id": "$AGENT_ID",
"task_id": "$TASK_ID",
"container_id": "$(hostname)",
"created_at": "$(date -Iseconds)",
"workspace_type": "agent_container"
}
}
EOF
)
# Create context via HCFS API
local response=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$context_data" \
"$HCFS_API_URL/contexts" || echo "")
if [ -n "$response" ]; then
echo "✅ HCFS workspace context created: $workspace_path"
echo "$workspace_path" > /tmp/hcfs-workspace-path
return 0
else
echo "⚠️ Failed to create HCFS workspace context, using local storage"
return 1
fi
}
# Function to mount HCFS
mount_hcfs() {
local workspace_path="$1"
echo "🔗 Mounting HCFS workspace: $workspace_path"
# For now, create a symbolic structure since we don't have full FUSE implementation
# In production, this would be: fusermount3 -o allow_other "$workspace_path" /mnt/hcfs
mkdir -p /mnt/hcfs
mkdir -p /home/agent/work/{src,build,output,logs}
# Create workspace metadata
cat > /home/agent/work/.hcfs-workspace << EOF
HCFS_WORKSPACE_PATH=$workspace_path
HCFS_API_URL=$HCFS_API_URL
AGENT_ID=$AGENT_ID
TASK_ID=$TASK_ID
CREATED_AT=$(date -Iseconds)
EOF
# Set ownership
chown -R agent:agent /home/agent/work /mnt/hcfs
echo "✅ HCFS workspace mounted and configured"
}
# Function to setup development environment
setup_dev_environment() {
echo "🛠️ Setting up development environment..."
# Create standard development directories
sudo -u agent mkdir -p /home/agent/{.local/bin,.config,.cache,work/{src,tests,docs,scripts}}
# Set up git configuration if provided
if [ -n "${GIT_USER_NAME:-}" ] && [ -n "${GIT_USER_EMAIL:-}" ]; then
sudo -u agent git config --global user.name "$GIT_USER_NAME"
sudo -u agent git config --global user.email "$GIT_USER_EMAIL"
echo "✅ Git configuration set: $GIT_USER_NAME <$GIT_USER_EMAIL>"
fi
# Set up Python virtual environment
if [ "${SETUP_PYTHON_VENV:-true}" = "true" ]; then
sudo -u agent python3 -m venv /home/agent/.venv
echo "✅ Python virtual environment created"
fi
echo "✅ Development environment ready"
}
# Function to start background services
start_background_services() {
echo "🔄 Starting background services..."
# Start HCFS workspace sync daemon (if needed)
if [ "$HCFS_ENABLED" = "true" ] && [ -f /tmp/hcfs-workspace-path ]; then
/opt/hcfs/hcfs-workspace.sh daemon &
echo "✅ HCFS workspace sync daemon started"
fi
}
# Function to cleanup on exit
cleanup() {
echo "🧹 Container cleanup initiated..."
if [ "$HCFS_ENABLED" = "true" ] && [ -f /tmp/hcfs-workspace-path ]; then
echo "💾 Storing final workspace state to HCFS..."
/opt/hcfs/hcfs-workspace.sh finalize
fi
echo "✅ Cleanup completed"
}
# Set up signal handlers for graceful shutdown
trap cleanup EXIT INT TERM
# Main initialization sequence
main() {
echo "🏁 Starting HCFS Agent Container initialization..."
# Wait for HCFS if enabled
if [ "$HCFS_ENABLED" = "true" ]; then
if wait_for_hcfs; then
if init_hcfs_workspace; then
local workspace_path=$(cat /tmp/hcfs-workspace-path)
mount_hcfs "$workspace_path"
else
echo "⚠️ HCFS workspace initialization failed, continuing with local storage"
fi
else
echo "⚠️ HCFS API unavailable, continuing with local storage"
fi
else
echo " HCFS disabled, using local storage only"
fi
# Set up development environment
setup_dev_environment
# Start background services
start_background_services
echo "🎉 HCFS Agent Container initialization complete!"
echo "📁 Workspace: /home/agent/work"
echo "🔧 Agent: $AGENT_ID"
echo "📋 Task: $TASK_ID"
# Execute the provided command or start interactive shell
if [ $# -eq 0 ]; then
echo "🔧 Starting interactive shell..."
exec sudo -u agent -i /bin/bash
else
echo "🚀 Executing command: $*"
exec sudo -u agent "$@"
fi
}
# Execute main function
main "$@"

View File

@@ -0,0 +1,242 @@
#!/bin/bash
set -euo pipefail
# HCFS Workspace Management Script
# Handles workspace synchronization and artifact collection
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE_DIR="/home/agent/work"
HCFS_CONFIG="/home/agent/work/.hcfs-workspace"
# Load workspace configuration
if [ -f "$HCFS_CONFIG" ]; then
source "$HCFS_CONFIG"
else
echo "⚠️ No HCFS workspace configuration found"
exit 1
fi
# Logging function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a /var/log/hcfs/workspace.log
}
# Function to store artifact in HCFS
store_artifact() {
local artifact_path="$1"
local artifact_name="$2"
local content="$3"
local hcfs_artifact_path="${HCFS_WORKSPACE_PATH}/artifacts/${artifact_name}"
local artifact_data=$(cat <<EOF
{
"path": "$hcfs_artifact_path",
"content": "$content",
"summary": "Artifact: $artifact_name",
"metadata": {
"agent_id": "$AGENT_ID",
"task_id": "$TASK_ID",
"artifact_name": "$artifact_name",
"artifact_type": "workspace_output",
"file_path": "$artifact_path",
"created_at": "$(date -Iseconds)"
}
}
EOF
)
local response=$(curl -s -X POST \
-H "Content-Type: application/json" \
-d "$artifact_data" \
"$HCFS_API_URL/contexts" || echo "")
if [ -n "$response" ]; then
log "✅ Stored artifact: $artifact_name -> $hcfs_artifact_path"
return 0
else
log "❌ Failed to store artifact: $artifact_name"
return 1
fi
}
# Function to collect and store workspace artifacts
collect_artifacts() {
log "📦 Collecting workspace artifacts..."
local artifact_count=0
# Common artifact patterns
local artifact_patterns=(
"*.log"
"*.md"
"*.txt"
"*.json"
"*.yaml"
"*.yml"
"output/*"
"build/*.json"
"build/*.xml"
"results/*"
"./**/README*"
"./**/CHANGELOG*"
"./**/requirements*.txt"
"./**/package*.json"
"./**/Cargo.toml"
"./**/go.mod"
"./**/pom.xml"
)
for pattern in "${artifact_patterns[@]}"; do
while IFS= read -r -d '' file; do
if [ -f "$file" ] && [ -s "$file" ]; then
local relative_path="${file#$WORKSPACE_DIR/}"
local content=$(base64 -w 0 "$file" 2>/dev/null || echo "")
if [ -n "$content" ] && [ ${#content} -lt 1000000 ]; then # Limit to 1MB
if store_artifact "$relative_path" "$relative_path" "$content"; then
artifact_count=$((artifact_count + 1))
fi
fi
fi
done < <(find "$WORKSPACE_DIR" -name "$pattern" -type f -print0 2>/dev/null || true)
done
log "✅ Collected $artifact_count artifacts"
}
# Function to update workspace status in HCFS
update_workspace_status() {
local status="$1"
local message="$2"
local status_data=$(cat <<EOF
{
"path": "${HCFS_WORKSPACE_PATH}/status",
"content": "$message",
"summary": "Workspace status: $status",
"metadata": {
"agent_id": "$AGENT_ID",
"task_id": "$TASK_ID",
"status": "$status",
"timestamp": "$(date -Iseconds)",
"hostname": "$(hostname)",
"workspace_dir": "$WORKSPACE_DIR"
}
}
EOF
)
curl -s -X POST \
-H "Content-Type: application/json" \
-d "$status_data" \
"$HCFS_API_URL/contexts" > /dev/null || true
log "📊 Updated workspace status: $status"
}
# Function to sync workspace changes
sync_workspace() {
log "🔄 Syncing workspace changes..."
# Create workspace summary
local file_count=$(find "$WORKSPACE_DIR" -type f 2>/dev/null | wc -l)
local dir_count=$(find "$WORKSPACE_DIR" -type d 2>/dev/null | wc -l)
local total_size=$(du -sb "$WORKSPACE_DIR" 2>/dev/null | cut -f1 || echo "0")
local summary=$(cat <<EOF
Workspace Summary ($(date -Iseconds)):
- Files: $file_count
- Directories: $dir_count
- Total Size: $total_size bytes
- Agent: $AGENT_ID
- Task: $TASK_ID
- Container: $(hostname)
Recent Activity:
$(ls -la "$WORKSPACE_DIR" 2>/dev/null | head -10 || echo "No files")
EOF
)
update_workspace_status "active" "$summary"
}
# Function to finalize workspace
finalize_workspace() {
log "🏁 Finalizing workspace..."
# Collect all artifacts
collect_artifacts
# Create final summary
local completion_summary=$(cat <<EOF
Workspace Completion Summary:
- Agent ID: $AGENT_ID
- Task ID: $TASK_ID
- Container: $(hostname)
- Started: $CREATED_AT
- Completed: $(date -Iseconds)
- Duration: $(($(date +%s) - $(date -d "$CREATED_AT" +%s 2>/dev/null || echo "0"))) seconds
Final Workspace Contents:
$(find "$WORKSPACE_DIR" -type f 2>/dev/null | head -20 || echo "No files")
Artifacts Collected:
$(ls "$WORKSPACE_DIR"/{output,build,logs,results}/* 2>/dev/null | head -10 || echo "No artifacts")
EOF
)
update_workspace_status "completed" "$completion_summary"
log "✅ Workspace finalized"
}
# Daemon mode for continuous sync
daemon_mode() {
log "🔄 Starting HCFS workspace sync daemon..."
local sync_interval=30 # seconds
local last_sync=0
while true; do
local current_time=$(date +%s)
if [ $((current_time - last_sync)) -ge $sync_interval ]; then
sync_workspace
last_sync=$current_time
fi
sleep 5
done
}
# Main command dispatcher
case "${1:-help}" in
"sync")
sync_workspace
;;
"collect")
collect_artifacts
;;
"finalize")
finalize_workspace
;;
"daemon")
daemon_mode
;;
"status")
update_workspace_status "active" "Status check at $(date -Iseconds)"
;;
"help"|*)
echo "HCFS Workspace Management Script"
echo ""
echo "Usage: $0 {sync|collect|finalize|daemon|status|help}"
echo ""
echo "Commands:"
echo " sync - Sync current workspace state to HCFS"
echo " collect - Collect and store artifacts in HCFS"
echo " finalize - Finalize workspace and store all artifacts"
echo " daemon - Run continuous sync daemon"
echo " status - Update workspace status in HCFS"
echo " help - Show this help message"
;;
esac

View File

@@ -0,0 +1,141 @@
# HCFS Go Development Environment
FROM bzzz-hcfs-base:latest
LABEL maintainer="anthony@deepblack.cloud"
LABEL description="HCFS Go development environment with modern Go tools"
LABEL language="go"
LABEL version="1.0.0"
# Install Go
ENV GO_VERSION=1.21.3
RUN wget -O go.tar.gz "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" && \
tar -C /usr/local -xzf go.tar.gz && \
rm go.tar.gz
# Set up Go environment
ENV GOROOT=/usr/local/go
ENV GOPATH=/home/agent/go
ENV GOCACHE=/home/agent/.cache/go-build
ENV GOMODCACHE=/home/agent/.cache/go-mod
ENV PATH=$GOROOT/bin:$GOPATH/bin:$PATH
# Create Go workspace
RUN sudo -u agent mkdir -p /home/agent/go/{bin,src,pkg} && \
sudo -u agent mkdir -p /home/agent/work/{cmd,internal,pkg,api,web,scripts,docs,tests}
# Install Go development tools
RUN sudo -u agent bash -c 'go install golang.org/x/tools/gopls@latest' && \
sudo -u agent bash -c 'go install golang.org/x/tools/cmd/goimports@latest' && \
sudo -u agent bash -c 'go install golang.org/x/lint/golint@latest' && \
sudo -u agent bash -c 'go install github.com/goreleaser/goreleaser@latest' && \
sudo -u agent bash -c 'go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest' && \
sudo -u agent bash -c 'go install github.com/go-delve/delve/cmd/dlv@latest' && \
sudo -u agent bash -c 'go install github.com/swaggo/swag/cmd/swag@latest' && \
sudo -u agent bash -c 'go install github.com/air-verse/air@latest'
# Install popular Go frameworks and libraries
RUN sudo -u agent bash -c 'cd /tmp && go mod init temp && \
go get github.com/gin-gonic/gin@latest && \
go get github.com/gorilla/mux@latest && \
go get github.com/echo-community/echo/v4@latest && \
go get github.com/gofiber/fiber/v2@latest && \
go get gorm.io/gorm@latest && \
go get github.com/stretchr/testify@latest && \
go get github.com/spf13/cobra@latest && \
go get github.com/spf13/viper@latest'
# Install HCFS Go SDK
COPY hcfs-go-sdk /opt/hcfs/go-sdk
RUN cd /opt/hcfs/go-sdk && sudo -u agent go mod tidy
# Create Go project template
RUN sudo -u agent bash -c 'cat > /home/agent/work/go.mod.template << EOF
module hcfs-agent-project
go 1.21
require (
github.com/hcfs/go-sdk v0.1.0
github.com/gin-gonic/gin v1.9.1
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
)
replace github.com/hcfs/go-sdk => /opt/hcfs/go-sdk
EOF'
RUN sudo -u agent bash -c 'cat > /home/agent/work/main.go.template << EOF
package main
import (
"fmt"
"log"
"github.com/hcfs/go-sdk/client"
)
func main() {
// Initialize HCFS client
hcfsClient, err := client.NewHCFSClient("http://host.docker.internal:8000")
if err != nil {
log.Fatal("Failed to create HCFS client:", err)
}
fmt.Println("HCFS Go agent starting...")
// Your agent code here
}
EOF'
# Create Makefile template
RUN sudo -u agent bash -c 'cat > /home/agent/work/Makefile.template << EOF
.PHONY: build run test clean lint fmt
BINARY_NAME=agent
MAIN_PATH=./cmd/main.go
build:
go build -o bin/$(BINARY_NAME) $(MAIN_PATH)
run:
go run $(MAIN_PATH)
test:
go test -v ./...
test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
clean:
go clean
rm -f bin/$(BINARY_NAME)
rm -f coverage.out
lint:
golangci-lint run
fmt:
go fmt ./...
goimports -w .
deps:
go mod tidy
go mod download
.DEFAULT_GOAL := build
EOF'
# Go-specific HCFS integration script
COPY scripts/go-hcfs-init.go /opt/hcfs/scripts/
RUN chmod +x /opt/hcfs/scripts/go-hcfs-init.go
# Expose common Go development ports
EXPOSE 8080 8000 9000 2345
# Add Go-specific entrypoint
COPY scripts/go-entrypoint.sh /opt/hcfs/
RUN chmod +x /opt/hcfs/go-entrypoint.sh
ENTRYPOINT ["/opt/hcfs/go-entrypoint.sh"]
CMD ["go", "version"]

View File

@@ -0,0 +1,112 @@
# HCFS Node.js Development Environment
FROM bzzz-hcfs-base:latest
LABEL maintainer="anthony@deepblack.cloud"
LABEL description="HCFS Node.js development environment with modern JS/TS tools"
LABEL language="javascript"
LABEL version="1.0.0"
# Install Node.js and npm
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs
# Install Yarn package manager
RUN npm install -g yarn
# Install global development tools
RUN npm install -g \
# TypeScript ecosystem
typescript \
ts-node \
@types/node \
# Build tools
webpack \
webpack-cli \
rollup \
vite \
# Testing frameworks
jest \
mocha \
cypress \
# Code quality
eslint \
prettier \
@typescript-eslint/parser \
@typescript-eslint/eslint-plugin \
# Development servers
nodemon \
concurrently \
# Package management
npm-check-updates \
# Documentation
jsdoc \
typedoc \
# CLI tools
commander \
inquirer \
chalk \
# Process management
pm2 \
forever
# Create Node.js workspace structure
RUN sudo -u agent mkdir -p /home/agent/work/{src,tests,docs,public,build,dist}
# Set up Node.js environment
ENV NODE_ENV=development
ENV NPM_CONFIG_PREFIX=/home/agent/.npm-global
ENV PATH=/home/agent/.npm-global/bin:$PATH
# Create npm configuration
RUN sudo -u agent mkdir -p /home/agent/.npm-global && \
sudo -u agent npm config set prefix '/home/agent/.npm-global'
# Install HCFS Node.js SDK
COPY hcfs-nodejs-sdk /opt/hcfs/nodejs-sdk
RUN cd /opt/hcfs/nodejs-sdk && npm install && npm link
# Create package.json template for new projects
RUN sudo -u agent bash -c 'cat > /home/agent/work/package.json.template << EOF
{
"name": "hcfs-agent-project",
"version": "1.0.0",
"description": "HCFS-enabled Node.js project",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest",
"build": "webpack --mode production",
"lint": "eslint src/",
"format": "prettier --write src/"
},
"dependencies": {
"@hcfs/sdk": "file:/opt/hcfs/nodejs-sdk",
"express": "^4.18.0",
"axios": "^1.0.0"
},
"devDependencies": {
"nodemon": "^3.0.0",
"jest": "^29.0.0",
"eslint": "^8.0.0",
"prettier": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
}
EOF'
# Node.js-specific HCFS integration script
COPY scripts/nodejs-hcfs-init.js /opt/hcfs/scripts/
RUN chmod +x /opt/hcfs/scripts/nodejs-hcfs-init.js
# Expose common Node.js development ports
EXPOSE 3000 8080 8000 9229
# Add Node.js-specific entrypoint
COPY scripts/nodejs-entrypoint.sh /opt/hcfs/
RUN chmod +x /opt/hcfs/nodejs-entrypoint.sh
ENTRYPOINT ["/opt/hcfs/nodejs-entrypoint.sh"]
CMD ["node"]

View File

@@ -0,0 +1,139 @@
# HCFS Python Development Environment
FROM bzzz-hcfs-base:latest
LABEL maintainer="anthony@deepblack.cloud"
LABEL description="HCFS Python development environment with ML/AI tools"
LABEL language="python"
LABEL version="1.0.0"
# Install Python development tools
RUN apt-get update && apt-get install -y \
# Python build dependencies
python3-dev \
python3-wheel \
python3-setuptools \
# Data science libraries dependencies
libhdf5-dev \
libnetcdf-dev \
libopenblas-dev \
liblapack-dev \
gfortran \
# ML/AI library dependencies
libgraphviz-dev \
graphviz \
# Image processing
libjpeg-dev \
libpng-dev \
libtiff-dev \
# Additional development tools
python3-ipython \
jupyter-core \
# Testing tools
python3-pytest \
&& rm -rf /var/lib/apt/lists/*
# Install comprehensive Python package ecosystem
RUN pip install --no-cache-dir \
# Core development
ipython \
jupyter \
jupyterlab \
notebook \
# Web frameworks
flask \
fastapi \
django \
starlette \
# Data science and ML
numpy \
pandas \
scipy \
scikit-learn \
matplotlib \
seaborn \
plotly \
# Deep learning
torch \
torchvision \
transformers \
# NLP
nltk \
spacy \
sentence-transformers \
# API and HTTP
requests \
httpx \
aiohttp \
# Database
sqlalchemy \
psycopg2-binary \
sqlite3 \
# Configuration and serialization
pyyaml \
toml \
configparser \
# CLI tools
click \
typer \
rich \
# Testing
pytest \
pytest-asyncio \
pytest-cov \
# Code quality
black \
flake8 \
mypy \
pylint \
# Documentation
sphinx \
mkdocs \
# Async programming
asyncio \
aiofiles \
# Development utilities
python-dotenv \
tqdm \
loguru
# Install HCFS Python SDK
COPY hcfs-python-sdk /opt/hcfs/python-sdk
RUN cd /opt/hcfs/python-sdk && pip install -e .
# Create development workspace structure
RUN sudo -u agent mkdir -p /home/agent/work/{src,tests,docs,notebooks,data,models,scripts}
# Set up Python-specific environment
ENV PYTHONPATH=/home/agent/work/src:/opt/hcfs/python-sdk:$PYTHONPATH
ENV JUPYTER_CONFIG_DIR=/home/agent/.jupyter
ENV JUPYTER_DATA_DIR=/home/agent/.local/share/jupyter
# Create Jupyter configuration
RUN sudo -u agent mkdir -p /home/agent/.jupyter && \
sudo -u agent bash -c 'cat > /home/agent/.jupyter/jupyter_notebook_config.py << EOF
c.NotebookApp.ip = "0.0.0.0"
c.NotebookApp.port = 8888
c.NotebookApp.open_browser = False
c.NotebookApp.token = ""
c.NotebookApp.password = ""
c.NotebookApp.notebook_dir = "/home/agent/work"
c.NotebookApp.allow_root = False
EOF'
# Python-specific HCFS integration script
COPY scripts/python-hcfs-init.py /opt/hcfs/scripts/
RUN chmod +x /opt/hcfs/scripts/python-hcfs-init.py
# Expose common Python development ports
EXPOSE 8888 8000 5000 8080
# Set Python as the default environment
ENV SHELL=/bin/bash
ENV PYTHON_ENV=development
# Add Python-specific entrypoint
COPY scripts/python-entrypoint.sh /opt/hcfs/
RUN chmod +x /opt/hcfs/python-entrypoint.sh
ENTRYPOINT ["/opt/hcfs/python-entrypoint.sh"]
CMD ["python"]

View File

@@ -0,0 +1,590 @@
# BZZZ MCP Integration Deployment Guide
This guide provides step-by-step instructions for deploying the BZZZ MCP integration with GPT-4 agents across the CHORUS cluster.
## Prerequisites
### Infrastructure Requirements
- **Cluster Nodes**: Minimum 3 nodes (WALNUT, IRONWOOD, ACACIA)
- **RAM**: 32GB+ per node for optimal performance
- **Storage**: 1TB+ SSD per node for conversation history and logs
- **Network**: High-speed connection between nodes for P2P communication
### Software Prerequisites
```bash
# On each node, ensure these are installed:
docker --version # Docker 24.0+
docker-compose --version # Docker Compose 2.20+
go version # Go 1.21+
node --version # Node.js 18+
```
### API Keys and Secrets
Ensure the OpenAI API key is properly stored:
```bash
# Verify the OpenAI API key exists
cat ~/chorus/business/secrets/openai-api-key-for-bzzz.txt
```
## Deployment Steps
### 1. Pre-Deployment Setup
#### Clone and Build
```bash
cd /home/tony/chorus/project-queues/active/BZZZ
# Build Go components
go mod download
go build -o bzzz main.go
# Build MCP server
cd mcp-server
npm install
npm run build
cd ..
# Build Docker images
docker build -t bzzz/mcp-node:latest .
docker build -t bzzz/mcp-server:latest mcp-server/
```
#### Environment Configuration
```bash
# Create environment file
cat > .env << EOF
# BZZZ Network Configuration
BZZZ_NODE_ID=bzzz-mcp-walnut
BZZZ_NETWORK_ID=bzzz-chorus-cluster
BZZZ_P2P_PORT=4001
BZZZ_HTTP_PORT=8080
# OpenAI Configuration
OPENAI_MODEL=gpt-4
OPENAI_MAX_TOKENS=4000
OPENAI_TEMPERATURE=0.7
# Cost Management
DAILY_COST_LIMIT=100.0
MONTHLY_COST_LIMIT=1000.0
COST_WARNING_THRESHOLD=0.8
# Agent Configuration
MAX_AGENTS=5
MAX_ACTIVE_THREADS=10
THREAD_TIMEOUT=3600
# Database Configuration
POSTGRES_PASSWORD=$(openssl rand -base64 32)
# Monitoring
GRAFANA_PASSWORD=$(openssl rand -base64 16)
# Integration URLs
WHOOSH_API_URL=http://192.168.1.72:8001
SLURP_API_URL=http://192.168.1.113:8002
EOF
# Source the environment
source .env
```
### 2. Database Initialization
Create the PostgreSQL schema:
```bash
cat > deploy/init-db.sql << EOF
-- BZZZ MCP Database Schema
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Agents table
CREATE TABLE agents (
id VARCHAR(255) PRIMARY KEY,
role VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
capabilities TEXT[],
specialization VARCHAR(255),
max_tasks INTEGER DEFAULT 3,
status VARCHAR(50) DEFAULT 'idle',
created_at TIMESTAMP DEFAULT NOW(),
last_active TIMESTAMP DEFAULT NOW(),
node_id VARCHAR(255),
system_prompt TEXT
);
-- Conversations table
CREATE TABLE conversations (
id VARCHAR(255) PRIMARY KEY,
topic TEXT NOT NULL,
state VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
last_activity TIMESTAMP DEFAULT NOW(),
creator_id VARCHAR(255),
shared_context JSONB DEFAULT '{}'::jsonb
);
-- Conversation participants
CREATE TABLE conversation_participants (
conversation_id VARCHAR(255) REFERENCES conversations(id),
agent_id VARCHAR(255) REFERENCES agents(id),
role VARCHAR(100),
status VARCHAR(50) DEFAULT 'active',
joined_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY (conversation_id, agent_id)
);
-- Messages table
CREATE TABLE messages (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
conversation_id VARCHAR(255) REFERENCES conversations(id),
from_agent VARCHAR(255) REFERENCES agents(id),
content TEXT NOT NULL,
message_type VARCHAR(100),
timestamp TIMESTAMP DEFAULT NOW(),
reply_to UUID REFERENCES messages(id),
token_count INTEGER DEFAULT 0,
model VARCHAR(100)
);
-- Agent tasks
CREATE TABLE agent_tasks (
id VARCHAR(255) PRIMARY KEY,
agent_id VARCHAR(255) REFERENCES agents(id),
repository VARCHAR(255),
task_number INTEGER,
title TEXT,
status VARCHAR(50) DEFAULT 'active',
start_time TIMESTAMP DEFAULT NOW(),
context JSONB DEFAULT '{}'::jsonb,
thread_id VARCHAR(255)
);
-- Token usage tracking
CREATE TABLE token_usage (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
agent_id VARCHAR(255) REFERENCES agents(id),
conversation_id VARCHAR(255),
timestamp TIMESTAMP DEFAULT NOW(),
model VARCHAR(100),
prompt_tokens INTEGER,
completion_tokens INTEGER,
total_tokens INTEGER,
cost_usd DECIMAL(10,6)
);
-- Agent memory
CREATE TABLE agent_memory (
agent_id VARCHAR(255) REFERENCES agents(id),
memory_type VARCHAR(50), -- 'working', 'episodic', 'semantic'
key VARCHAR(255),
value JSONB,
timestamp TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP,
PRIMARY KEY (agent_id, memory_type, key)
);
-- Escalations
CREATE TABLE escalations (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
conversation_id VARCHAR(255) REFERENCES conversations(id),
reason VARCHAR(255),
escalated_at TIMESTAMP DEFAULT NOW(),
escalated_by VARCHAR(255),
status VARCHAR(50) DEFAULT 'pending',
resolved_at TIMESTAMP,
resolution TEXT
);
-- Indexes for performance
CREATE INDEX idx_agents_role ON agents(role);
CREATE INDEX idx_agents_status ON agents(status);
CREATE INDEX idx_conversations_state ON conversations(state);
CREATE INDEX idx_messages_conversation_timestamp ON messages(conversation_id, timestamp);
CREATE INDEX idx_token_usage_agent_timestamp ON token_usage(agent_id, timestamp);
CREATE INDEX idx_agent_memory_agent_type ON agent_memory(agent_id, memory_type);
EOF
```
### 3. Deploy to Cluster
#### Node-Specific Deployment
**On WALNUT (192.168.1.27):**
```bash
# Set node-specific configuration
export BZZZ_NODE_ID=bzzz-mcp-walnut
export NODE_ROLE=primary
# Deploy with primary node configuration
docker-compose -f deploy/docker-compose.mcp.yml up -d
```
**On IRONWOOD (192.168.1.72):**
```bash
# Set node-specific configuration
export BZZZ_NODE_ID=bzzz-mcp-ironwood
export NODE_ROLE=secondary
# Deploy as secondary node
docker-compose -f deploy/docker-compose.mcp.yml up -d
```
**On ACACIA (192.168.1.113):**
```bash
# Set node-specific configuration
export BZZZ_NODE_ID=bzzz-mcp-acacia
export NODE_ROLE=secondary
# Deploy as secondary node
docker-compose -f deploy/docker-compose.mcp.yml up -d
```
### 4. Service Health Verification
#### Check Service Status
```bash
# Check all services are running
docker-compose -f deploy/docker-compose.mcp.yml ps
# Check BZZZ node connectivity
curl http://localhost:8080/health
# Check MCP server status
curl http://localhost:8081/health
# Check P2P network connectivity
curl http://localhost:8080/api/peers
```
#### Verify Agent Registration
```bash
# List registered agents
curl http://localhost:8081/api/agents
# Check agent capabilities
curl http://localhost:8081/api/agents/review_agent_architect
```
#### Test MCP Integration
```bash
# Test MCP server connection
cd examples
python3 test-mcp-connection.py
# Run collaborative review example
python3 collaborative-review-example.py
```
### 5. Integration with CHORUS Systems
#### WHOOSH Integration
```bash
# Verify WHOOSH connectivity
curl -X POST http://192.168.1.72:8001/api/agents \
-H "Content-Type: application/json" \
-d '{
"agent_id": "bzzz-mcp-agent-1",
"type": "gpt_agent",
"role": "architect",
"endpoint": "http://192.168.1.27:8081"
}'
```
#### SLURP Integration
```bash
# Test SLURP context event submission
curl -X POST http://192.168.1.113:8002/api/events \
-H "Content-Type: application/json" \
-d '{
"type": "agent_consensus",
"source": "bzzz_mcp_integration",
"context": {
"conversation_id": "test-thread-1",
"participants": ["architect", "reviewer"],
"consensus_reached": true
}
}'
```
### 6. Monitoring Setup
#### Access Monitoring Dashboards
- **Grafana**: http://localhost:3000 (admin/password from .env)
- **Prometheus**: http://localhost:9090
- **Logs**: Access via Grafana Loki integration
#### Key Metrics to Monitor
```bash
# Agent performance metrics
curl http://localhost:8081/api/stats
# Token usage and costs
curl http://localhost:8081/api/costs/daily
# Conversation thread health
curl http://localhost:8081/api/conversations?status=active
```
## Configuration Management
### Agent Role Configuration
Create custom agent roles:
```bash
# Create custom agent configuration
cat > config/custom-agent-roles.json << EOF
{
"roles": [
{
"name": "security_architect",
"specialization": "security_design",
"capabilities": [
"threat_modeling",
"security_architecture",
"compliance_review",
"risk_assessment"
],
"system_prompt": "You are a security architect specializing in distributed systems security...",
"interaction_patterns": {
"architects": "security_consultation",
"developers": "security_guidance",
"reviewers": "security_validation"
}
}
]
}
EOF
```
### Cost Management Configuration
```bash
# Configure cost alerts
cat > config/cost-limits.json << EOF
{
"global_limits": {
"daily_limit": 100.0,
"monthly_limit": 1000.0,
"per_agent_daily": 20.0
},
"alert_thresholds": {
"warning": 0.8,
"critical": 0.95
},
"alert_channels": {
"slack_webhook": "${SLACK_WEBHOOK_URL}",
"email": "admin@deepblack.cloud"
}
}
EOF
```
### Escalation Rules Configuration
```bash
# Configure escalation rules
cat > config/escalation-rules.json << EOF
{
"rules": [
{
"name": "Long Running Thread",
"conditions": [
{"type": "thread_duration", "threshold": 7200},
{"type": "no_progress", "threshold": true, "timeframe": 1800}
],
"actions": [
{"type": "notify_human", "target": "project_manager"},
{"type": "escalate_to_senior", "role": "senior_architect"}
]
},
{
"name": "High Cost Alert",
"conditions": [
{"type": "token_cost", "threshold": 50.0, "timeframe": 3600}
],
"actions": [
{"type": "throttle_agents", "reduction": 0.5},
{"type": "notify_admin", "urgency": "high"}
]
}
]
}
EOF
```
## Troubleshooting
### Common Issues
#### MCP Server Connection Issues
```bash
# Check MCP server logs
docker logs bzzz-mcp-server
# Verify OpenAI API key
docker exec bzzz-mcp-server cat /secrets/openai-api-key-for-bzzz.txt
# Test API key validity
curl -H "Authorization: Bearer $(cat ~/chorus/business/secrets/openai-api-key-for-bzzz.txt)" \
https://api.openai.com/v1/models
```
#### P2P Network Issues
```bash
# Check P2P connectivity
docker exec bzzz-mcp-node ./bzzz status
# View P2P logs
docker logs bzzz-mcp-node | grep p2p
# Check firewall settings
sudo ufw status | grep 4001
```
#### Agent Performance Issues
```bash
# Check agent memory usage
curl http://localhost:8081/api/agents/memory-stats
# Review token usage
curl http://localhost:8081/api/costs/breakdown
# Check conversation thread status
curl http://localhost:8081/api/conversations?status=active
```
### Performance Optimization
#### Database Tuning
```sql
-- Optimize PostgreSQL for BZZZ MCP workload
ALTER SYSTEM SET shared_buffers = '256MB';
ALTER SYSTEM SET work_mem = '16MB';
ALTER SYSTEM SET maintenance_work_mem = '128MB';
ALTER SYSTEM SET max_connections = 100;
SELECT pg_reload_conf();
```
#### Agent Optimization
```bash
# Optimize agent memory usage
curl -X POST http://localhost:8081/api/agents/cleanup-memory
# Adjust token limits based on usage patterns
curl -X PUT http://localhost:8081/api/config/token-limits \
-H "Content-Type: application/json" \
-d '{"max_tokens": 2000, "context_window": 16000}'
```
## Backup and Recovery
### Database Backup
```bash
# Create database backup
docker exec bzzz-mcp-postgres pg_dump -U bzzz bzzz_mcp | gzip > backup/bzzz-mcp-$(date +%Y%m%d).sql.gz
# Restore from backup
gunzip -c backup/bzzz-mcp-20250107.sql.gz | docker exec -i bzzz-mcp-postgres psql -U bzzz -d bzzz_mcp
```
### Configuration Backup
```bash
# Backup agent configurations
docker exec bzzz-mcp-server tar czf - /var/lib/mcp/config > backup/mcp-config-$(date +%Y%m%d).tar.gz
# Backup conversation data
docker exec bzzz-conversation-manager tar czf - /var/lib/conversations > backup/conversations-$(date +%Y%m%d).tar.gz
```
## Security Considerations
### API Key Security
```bash
# Rotate OpenAI API key monthly
echo "new-api-key" > ~/chorus/business/secrets/openai-api-key-for-bzzz.txt
docker-compose -f deploy/docker-compose.mcp.yml restart mcp-server
# Monitor API key usage
curl -H "Authorization: Bearer $(cat ~/chorus/business/secrets/openai-api-key-for-bzzz.txt)" \
https://api.openai.com/v1/usage
```
### Network Security
```bash
# Configure firewall rules
sudo ufw allow from 192.168.1.0/24 to any port 4001 # P2P port
sudo ufw allow from 192.168.1.0/24 to any port 8080 # BZZZ API
sudo ufw allow from 192.168.1.0/24 to any port 8081 # MCP API
# Enable audit logging
docker-compose -f deploy/docker-compose.mcp.yml \
-f deploy/docker-compose.audit.yml up -d
```
## Maintenance
### Regular Maintenance Tasks
```bash
# Weekly maintenance script
#!/bin/bash
set -e
echo "Starting BZZZ MCP maintenance..."
# Clean up old conversation threads
curl -X POST http://localhost:8081/api/maintenance/cleanup-threads
# Optimize database
docker exec bzzz-mcp-postgres psql -U bzzz -d bzzz_mcp -c "VACUUM ANALYZE;"
# Update cost tracking
curl -X POST http://localhost:8081/api/maintenance/update-costs
# Rotate logs
docker exec bzzz-mcp-server logrotate /etc/logrotate.d/mcp
echo "Maintenance completed successfully"
```
### Performance Monitoring
```bash
# Monitor key performance indicators
curl http://localhost:8081/api/metrics | jq '{
active_agents: .active_agents,
active_threads: .active_threads,
avg_response_time: .avg_response_time,
token_efficiency: .token_efficiency,
cost_per_task: .cost_per_task
}'
```
This deployment guide provides a comprehensive approach to deploying and maintaining the BZZZ MCP integration with GPT-4 agents across the CHORUS cluster. Follow the steps carefully and refer to the troubleshooting section for common issues.

View File

@@ -0,0 +1,324 @@
version: '3.8'
# BZZZ MCP Integration Docker Compose Configuration
# This configuration deploys the complete MCP-enabled BZZZ system with GPT-4 agents
services:
# BZZZ P2P Node with MCP Integration
bzzz-node:
build:
context: ..
dockerfile: Dockerfile
args:
- BUILD_TARGET=mcp-enabled
container_name: bzzz-mcp-node
networks:
- bzzz-network
ports:
- "8080:8080" # BZZZ HTTP API
- "4001:4001" # LibP2P swarm port
environment:
- BZZZ_NODE_ID=${BZZZ_NODE_ID:-bzzz-mcp-1}
- BZZZ_NETWORK_ID=${BZZZ_NETWORK_ID:-bzzz-local}
- BZZZ_P2P_PORT=4001
- BZZZ_HTTP_PORT=8080
- MCP_ENABLED=true
- MCP_SERVER_PORT=8081
volumes:
- bzzz-data:/var/lib/bzzz
- ../business/secrets:/secrets:ro
restart: unless-stopped
depends_on:
- redis
- postgres
# MCP Server for GPT-4 Integration
mcp-server:
build:
context: ../mcp-server
dockerfile: Dockerfile
container_name: bzzz-mcp-server
networks:
- bzzz-network
ports:
- "8081:8081" # MCP HTTP API
- "8082:8082" # WebSocket endpoint
environment:
- NODE_ENV=production
- BZZZ_NODE_URL=http://bzzz-node:8080
- BZZZ_NETWORK_ID=${BZZZ_NETWORK_ID:-bzzz-local}
- OPENAI_API_KEY_FILE=/secrets/openai-api-key-for-bzzz.txt
- OPENAI_MODEL=${OPENAI_MODEL:-gpt-4}
- OPENAI_MAX_TOKENS=${OPENAI_MAX_TOKENS:-4000}
- DAILY_COST_LIMIT=${DAILY_COST_LIMIT:-100.0}
- MONTHLY_COST_LIMIT=${MONTHLY_COST_LIMIT:-1000.0}
- MAX_ACTIVE_THREADS=${MAX_ACTIVE_THREADS:-10}
- MAX_AGENTS=${MAX_AGENTS:-5}
- LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
- ../business/secrets:/secrets:ro
- mcp-logs:/var/log/mcp
- mcp-data:/var/lib/mcp
restart: unless-stopped
depends_on:
- bzzz-node
- postgres
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
interval: 30s
timeout: 10s
retries: 3
# Agent Manager Service
agent-manager:
build:
context: ..
dockerfile: deploy/Dockerfile.agent-manager
container_name: bzzz-agent-manager
networks:
- bzzz-network
environment:
- MCP_SERVER_URL=http://mcp-server:8081
- POSTGRES_URL=postgres://bzzz:${POSTGRES_PASSWORD}@postgres:5432/bzzz_mcp
- REDIS_URL=redis://redis:6379
- AGENT_LIFECYCLE_INTERVAL=30s
- AGENT_HEALTH_CHECK_INTERVAL=60s
- COST_MONITORING_INTERVAL=300s
volumes:
- agent-data:/var/lib/agents
- ../business/secrets:/secrets:ro
restart: unless-stopped
depends_on:
- mcp-server
- postgres
- redis
# Conversation Manager Service
conversation-manager:
build:
context: ..
dockerfile: deploy/Dockerfile.conversation-manager
container_name: bzzz-conversation-manager
networks:
- bzzz-network
environment:
- MCP_SERVER_URL=http://mcp-server:8081
- POSTGRES_URL=postgres://bzzz:${POSTGRES_PASSWORD}@postgres:5432/bzzz_mcp
- REDIS_URL=redis://redis:6379
- THREAD_CLEANUP_INTERVAL=1h
- ESCALATION_CHECK_INTERVAL=5m
- SUMMARY_GENERATION_INTERVAL=15m
volumes:
- conversation-data:/var/lib/conversations
restart: unless-stopped
depends_on:
- mcp-server
- postgres
- redis
# Cost Tracker Service
cost-tracker:
build:
context: ..
dockerfile: deploy/Dockerfile.cost-tracker
container_name: bzzz-cost-tracker
networks:
- bzzz-network
environment:
- MCP_SERVER_URL=http://mcp-server:8081
- POSTGRES_URL=postgres://bzzz:${POSTGRES_PASSWORD}@postgres:5432/bzzz_mcp
- OPENAI_API_KEY_FILE=/secrets/openai-api-key-for-bzzz.txt
- COST_CALCULATION_INTERVAL=5m
- ALERT_WEBHOOK_URL=${ALERT_WEBHOOK_URL}
- SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL}
volumes:
- cost-data:/var/lib/costs
- ../business/secrets:/secrets:ro
restart: unless-stopped
depends_on:
- mcp-server
- postgres
# PostgreSQL Database for MCP data
postgres:
image: postgres:15-alpine
container_name: bzzz-mcp-postgres
networks:
- bzzz-network
environment:
- POSTGRES_DB=bzzz_mcp
- POSTGRES_USER=bzzz
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init.sql
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U bzzz -d bzzz_mcp"]
interval: 10s
timeout: 5s
retries: 5
# Redis for caching and session management
redis:
image: redis:7-alpine
container_name: bzzz-mcp-redis
networks:
- bzzz-network
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
- redis-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
# WHOOSH Integration Service
whoosh-integration:
build:
context: ../../../WHOOSH
dockerfile: Dockerfile
container_name: bzzz-whoosh-integration
networks:
- bzzz-network
- whoosh-network
environment:
- WHOOSH_API_URL=${WHOOSH_API_URL}
- WHOOSH_API_KEY=${WHOOSH_API_KEY}
- MCP_SERVER_URL=http://mcp-server:8081
- INTEGRATION_SYNC_INTERVAL=5m
volumes:
- whoosh-integration-data:/var/lib/whoosh-integration
- ../business/secrets:/secrets:ro
restart: unless-stopped
depends_on:
- mcp-server
# SLURP Integration Service (Context Curation)
slurp-integration:
build:
context: ../../../slurp
dockerfile: Dockerfile
container_name: bzzz-slurp-integration
networks:
- bzzz-network
- slurp-network
environment:
- SLURP_API_URL=${SLURP_API_URL}
- SLURP_API_KEY=${SLURP_API_KEY}
- MCP_SERVER_URL=http://mcp-server:8081
- CONTEXT_SYNC_INTERVAL=2m
- RELEVANCE_THRESHOLD=0.7
volumes:
- slurp-integration-data:/var/lib/slurp-integration
- ../business/secrets:/secrets:ro
restart: unless-stopped
depends_on:
- mcp-server
# Monitoring and Observability
prometheus:
image: prom/prometheus:latest
container_name: bzzz-mcp-prometheus
networks:
- bzzz-network
ports:
- "9090:9090"
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: bzzz-mcp-grafana
networks:
- bzzz-network
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana-data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning
restart: unless-stopped
depends_on:
- prometheus
# Log Aggregation
loki:
image: grafana/loki:latest
container_name: bzzz-mcp-loki
networks:
- bzzz-network
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
volumes:
- loki-data:/loki
restart: unless-stopped
promtail:
image: grafana/promtail:latest
container_name: bzzz-mcp-promtail
networks:
- bzzz-network
volumes:
- ./monitoring/promtail-config.yml:/etc/promtail/config.yml
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
restart: unless-stopped
depends_on:
- loki
networks:
bzzz-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
whoosh-network:
external: true
slurp-network:
external: true
volumes:
bzzz-data:
driver: local
mcp-logs:
driver: local
mcp-data:
driver: local
agent-data:
driver: local
conversation-data:
driver: local
cost-data:
driver: local
postgres-data:
driver: local
redis-data:
driver: local
whoosh-integration-data:
driver: local
slurp-integration-data:
driver: local
prometheus-data:
driver: local
grafana-data:
driver: local
loki-data:
driver: local