Created: 2025-10-09 Status: Planning Phase Complete Target Completion: 2025-10-16
This document provides a comprehensive implementation plan for adding 4 integrated configuration management features to AutoBot's backend:
- Environment Variable Priority Enforcement - Already partially implemented, needs enhancement
- Configuration Validation with Startup Warnings - New feature
- API Endpoint for GUI Settings Sync - New feature
- File Watching for Hot Reload - New feature with distributed support
- Current State Analysis
- Feature 1: Environment Variable Enforcement
- Feature 2: Configuration Validation
- Feature 3: Settings Sync API
- Feature 4: Hot Reload System
- Integration Plan
- Testing Strategy
- Deployment Strategy
- Risk Assessment
File: src/unified_config_manager.py
What Works:
- Environment variable override system with
AUTOBOT_*prefix (line 260-317) - Configuration precedence:
config.yaml→settings.json→ environment variables - Comprehensive env var mappings for 30+ settings
- Type conversion (bool, int, string)
- Async file operations infrastructure (line 597-634)
- Callback system placeholder (line 713-729)
What's Missing:
- No validation warnings or conflict detection
- No sync-to-base API endpoint
- No file watching mechanism
- No distributed configuration sync
- No atomic file write operations
- No startup validation integration
File: backend/app_factory.py
Startup Sequence:
# Line 471-487: Configuration loads at 10% progress
config = UnifiedConfigManager()
app.state.config = config
# OPPORTUNITY: Add validation here (after line 487)Dependency Injection:
# backend/dependencies.py line 14-24
def get_config() -> UnifiedConfigManager:
return global_config_managerLocation: unified_config_manager.py line 260-317
def _apply_env_overrides(self) -> None:
"""Apply environment variable overrides using AUTOBOT_ prefix"""
env_overrides = {}
# Environment variable mappings
env_mappings = {
"AUTOBOT_BACKEND_HOST": ["backend", "server_host"],
"AUTOBOT_BACKEND_PORT": ["backend", "server_port"],
# ... 28 more mappings
}
for env_var, config_path in env_mappings.items():
env_value = os.getenv(env_var)
if env_value is not None:
# Convert and applyCurrent: Logs each override individually (line 313) Enhancement: Aggregate and log summary
def _apply_env_overrides(self) -> Dict[str, Any]:
"""Apply environment variable overrides and return summary"""
env_overrides = {}
applied_overrides = []
# ... existing logic ...
if env_overrides:
self._config = self._deep_merge(self._config, env_overrides)
logger.info(f"Applied {len(applied_overrides)} environment variable overrides")
for override in applied_overrides:
logger.debug(f" {override['var']} = {override['value']}")
return {"applied": applied_overrides, "total": len(env_mappings)}Missing Mappings to Add:
# Security settings
"AUTOBOT_SESSION_TIMEOUT": ["security", "session_timeout_minutes"],
"AUTOBOT_ENABLE_ENCRYPTION": ["security", "enable_encryption"],
# Knowledge base
"AUTOBOT_KB_UPDATE_FREQUENCY": ["knowledge_base", "update_frequency_days"],
"AUTOBOT_KB_ENABLED": ["knowledge_base", "enabled"],
# Developer mode
"AUTOBOT_DEVELOPER_MODE": ["ui", "developer_mode"],
"AUTOBOT_DEBUG_LOGGING": ["developer", "debug_logging"],
# Voice interface
"AUTOBOT_VOICE_ENABLED": ["voice_interface", "enabled"],
"AUTOBOT_VOICE_RATE": ["voice_interface", "speech_rate"],- Extract env var mapping to separate method (easier to test and extend)
- Add validation of env var values (before applying)
- Return override summary for integration with validation system
- Add env var documentation in CLAUDE.md
Estimated Effort: 2 hours Priority: Medium (enhancement of existing feature)
Purpose: Detect configuration conflicts and invalid values during startup and reload operations.
class ValidationLevel(Enum):
INFO = "info" # Expected behavior (env var overrides file)
WARNING = "warning" # Potential issue (conflicting values)
ERROR = "error" # Invalid configuration (wrong type/range)
CRITICAL = "critical" # Service will fail (missing required value)Structure Validation:
def _validate_structure(self) -> List[ValidationResult]:
"""Ensure required configuration sections exist"""
required_sections = ["backend", "memory", "ui", "chat", "logging"]
issues = []
for section in required_sections:
if section not in self._config:
issues.append(ValidationResult(
level=ValidationLevel.CRITICAL,
category="structure",
message=f"Missing required section: {section}",
path=section
))
return issuesType Validation:
def _validate_types(self) -> List[ValidationResult]:
"""Validate configuration value types"""
type_specs = {
"backend.server_port": int,
"backend.timeout": int,
"backend.streaming": bool,
"memory.redis.enabled": bool,
"memory.redis.port": int,
# ... more specs
}
issues = []
for path, expected_type in type_specs.items():
value = self.get_nested(path)
if value is not None and not isinstance(value, expected_type):
issues.append(ValidationResult(
level=ValidationLevel.ERROR,
category="type",
message=f"Expected {expected_type.__name__}, got {type(value).__name__}",
path=path,
value=value
))
return issuesRange Validation:
def _validate_ranges(self) -> List[ValidationResult]:
"""Validate numeric ranges"""
range_specs = {
"backend.server_port": (1, 65535),
"backend.timeout": (1, 300),
"memory.redis.port": (1, 65535),
"chat.max_messages": (10, 10000),
"security.session_timeout_minutes": (5, 1440),
}
issues = []
for path, (min_val, max_val) in range_specs.items():
value = self.get_nested(path)
if value is not None and not (min_val <= value <= max_val):
issues.append(ValidationResult(
level=ValidationLevel.ERROR,
category="range",
message=f"Value {value} outside valid range [{min_val}, {max_val}]",
path=path,
value=value
))
return issuesConflict Detection:
def _detect_conflicts(self) -> List[ValidationResult]:
"""Detect conflicts between configuration sources"""
issues = []
# Load raw sources
base_config = self._load_yaml_config()
user_settings = self._load_json_settings()
# Check for complete section replacements
for section in ["backend.cors_origins", "backend.llm.ollama.models"]:
base_value = self._get_nested_from_dict(base_config, section)
user_value = self._get_nested_from_dict(user_settings, section)
if base_value and user_value and len(base_value) != len(user_value):
issues.append(ValidationResult(
level=ValidationLevel.WARNING,
category="conflict",
message=f"settings.json completely replaces {section} from config.yaml "
f"({len(base_value)} items → {len(user_value)} items)",
path=section
))
# Check for env var conflicts
env_overrides = self._get_applied_env_overrides()
for env_var, path in env_overrides.items():
file_value = self._get_nested_from_dict(user_settings or base_config, path)
env_value = os.getenv(env_var)
if file_value != env_value:
issues.append(ValidationResult(
level=ValidationLevel.INFO,
category="override",
message=f"Environment variable {env_var} overrides file value",
path=".".join(path),
details={"file": file_value, "env": env_value}
))
return issuesdef _validate_configuration_consistency(self) -> ValidationReport:
"""
Comprehensive configuration validation.
Returns:
ValidationReport with all detected issues organized by level
"""
all_issues = []
# Run all validation checks
all_issues.extend(self._validate_structure())
all_issues.extend(self._validate_types())
all_issues.extend(self._validate_ranges())
all_issues.extend(self._validate_url_formats())
all_issues.extend(self._validate_file_paths())
all_issues.extend(self._detect_conflicts())
# Organize by level
report = ValidationReport(
timestamp=datetime.now(timezone.utc),
total_issues=len(all_issues),
critical=len([i for i in all_issues if i.level == ValidationLevel.CRITICAL]),
errors=len([i for i in all_issues if i.level == ValidationLevel.ERROR]),
warnings=len([i for i in all_issues if i.level == ValidationLevel.WARNING]),
info=len([i for i in all_issues if i.level == ValidationLevel.INFO]),
issues=all_issues
)
return reportFile: backend/app_factory.py line 482-487
# BEFORE (current)
logger.info("✅ [ 10%] Config: Loading unified configuration...")
config = UnifiedConfigManager()
app.state.config = config
app_state["config"] = config
logger.info("✅ [ 10%] Config: Configuration loaded successfully")
# AFTER (with validation)
logger.info("✅ [ 10%] Config: Loading unified configuration...")
config = UnifiedConfigManager()
app.state.config = config
app_state["config"] = config
logger.info("✅ [ 10%] Config: Configuration loaded successfully")
# NEW: Validate configuration
logger.info("🔍 [ 11%] Config: Validating configuration consistency...")
validation_report = config.validate_configuration_consistency()
app.state.config_validation = validation_report
if validation_report.critical > 0:
logger.error(f"❌ CRITICAL: {validation_report.critical} critical configuration issues detected")
for issue in validation_report.get_critical():
logger.error(f" - {issue.message} (path: {issue.path})")
raise RuntimeError("Critical configuration issues prevent startup")
if validation_report.errors > 0:
logger.warning(f"⚠️ {validation_report.errors} configuration errors detected (non-blocking)")
for issue in validation_report.get_errors():
logger.warning(f" - {issue.message} (path: {issue.path})")
if validation_report.warnings > 0:
logger.info(f"ℹ️ {validation_report.warnings} configuration warnings (informational)")
for issue in validation_report.get_warnings():
logger.debug(f" - {issue.message} (path: {issue.path})")
logger.info(f"✅ [ 12%] Config: Validation complete ({validation_report.total_issues} items)")New File: autobot-backend/api/config_validation.py
from fastapi import APIRouter, Depends
from backend.dependencies import get_config
from src.unified_config_manager import UnifiedConfigManager
router = APIRouter()
@router.get("/api/config/validate")
async def validate_configuration(config: UnifiedConfigManager = Depends(get_config)):
"""
Validate current configuration and return detailed report.
Returns:
ValidationReport with all detected issues
"""
report = config.validate_configuration_consistency()
return {
"status": "healthy" if report.critical == 0 else "unhealthy",
"timestamp": report.timestamp.isoformat(),
"summary": {
"total": report.total_issues,
"critical": report.critical,
"errors": report.errors,
"warnings": report.warnings,
"info": report.info
},
"issues": [
{
"level": issue.level.value,
"category": issue.category,
"message": issue.message,
"path": issue.path,
"value": issue.value,
"details": issue.details
}
for issue in report.issues
]
}Phase 2.1: Core Validation (4 hours)
- Create
ValidationLevelenum - Create
ValidationResultandValidationReportdata classes - Implement
_validate_structure(),_validate_types(),_validate_ranges() - Add
_validate_configuration_consistency()method - Unit tests for validation logic
Phase 2.2: Conflict Detection (3 hours)
- Implement
_detect_conflicts()method - Add raw source loading helpers
- Test conflict detection scenarios
- Document expected vs unexpected conflicts
Phase 2.3: Startup Integration (2 hours)
- Modify
app_factory.pystartup sequence - Add validation report to app state
- Add critical issue blocking logic
- Test startup with various config states
Phase 2.4: API Endpoint (2 hours)
- Create
autobot-backend/api/config_validation.py - Add router to
app_factory.py - Add frontend integration docs
- Test API endpoint
Total Estimated Effort: 11 hours Priority: HIGH (critical for troubleshooting)
Purpose: Allow GUI to persist changes from settings.json back to config.yaml as the new baseline.
Endpoint: POST /api/settings/sync-to-base
import fcntl
import tempfile
from pathlib import Path
from contextlib import contextmanager
@contextmanager
def file_lock(file_path: Path, timeout: float = 5.0):
"""
Context manager for file locking with timeout.
Prevents race conditions during concurrent config file access.
"""
lock_path = file_path.with_suffix(file_path.suffix + '.lock')
lock_file = None
try:
lock_file = open(lock_path, 'w')
# Try to acquire exclusive lock with timeout
start_time = time.time()
while True:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError:
if time.time() - start_time > timeout:
raise TimeoutError(f"Could not acquire lock on {file_path} after {timeout}s")
time.sleep(0.1)
yield lock_file
finally:
if lock_file:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
lock_file.close()
lock_path.unlink(missing_ok=True)
except Exception as e:
logger.warning(f"Error releasing lock: {e}")
def _atomic_write_yaml(self, file_path: Path, data: Dict[str, Any]) -> None:
"""
Write configuration to YAML file atomically.
Uses temp file + rename pattern to ensure atomic write operation.
"""
# Create temp file in same directory (required for atomic rename)
temp_file = file_path.with_suffix('.tmp')
try:
# Write to temp file
with open(temp_file, 'w', encoding='utf-8') as f:
yaml.dump(data, f, default_flow_style=False, indent=2)
# Validate temp file is valid YAML
with open(temp_file, 'r', encoding='utf-8') as f:
yaml.safe_load(f)
# Atomic rename (replaces destination if exists)
temp_file.rename(file_path)
logger.info(f"Successfully wrote configuration to {file_path}")
except Exception as e:
# Clean up temp file on error
temp_file.unlink(missing_ok=True)
raise RuntimeError(f"Failed to write configuration: {e}")def sync_settings_to_base(self) -> Dict[str, Any]:
"""
Sync current settings.json to config.yaml as new baseline.
This makes user customizations the new default configuration.
Returns:
Dict with operation status and details
"""
with file_lock(self.base_config_file, timeout=5.0):
try:
# Load current merged configuration
current_config = self._config.copy()
# Filter out prompts (not stored in config.yaml)
if "prompts" in current_config:
del current_config["prompts"]
# Validate before writing
validation_report = self._validate_configuration_consistency()
if validation_report.critical > 0:
return {
"status": "error",
"message": "Cannot sync: critical configuration issues detected",
"validation": validation_report.to_dict()
}
# Backup current config.yaml
backup_path = self.base_config_file.with_suffix('.yaml.backup')
if self.base_config_file.exists():
import shutil
shutil.copy2(self.base_config_file, backup_path)
# Write atomically
self._atomic_write_yaml(self.base_config_file, current_config)
# Clear cache to force reload
self._sync_cache_timestamp = None
# Remove settings.json (now redundant)
if self.settings_file.exists():
self.settings_file.unlink()
logger.info("Removed settings.json (now baseline)")
return {
"status": "success",
"message": "Settings synced to base configuration",
"backup_created": str(backup_path),
"validation": validation_report.summary()
}
except Exception as e:
logger.error(f"Sync-to-base failed: {e}")
return {
"status": "error",
"message": str(e)
}File: autobot-backend/api/settings_sync.py
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from backend.dependencies import get_config
from src.unified_config_manager import UnifiedConfigManager
import time
from collections import defaultdict
from datetime import datetime, timedelta
router = APIRouter()
# Simple in-memory rate limiter
rate_limit_store = defaultdict(list)
RATE_LIMIT_REQUESTS = 1
RATE_LIMIT_WINDOW = 60 # seconds
def check_rate_limit(request: Request) -> bool:
"""Check if request is within rate limit"""
client_ip = request.client.host
now = time.time()
# Clean old requests
rate_limit_store[client_ip] = [
req_time for req_time in rate_limit_store[client_ip]
if now - req_time < RATE_LIMIT_WINDOW
]
# Check limit
if len(rate_limit_store[client_ip]) >= RATE_LIMIT_REQUESTS:
return False
# Add current request
rate_limit_store[client_ip].append(now)
return True
@router.post("/api/settings/sync-to-base")
async def sync_settings_to_base(
request: Request,
config: UnifiedConfigManager = Depends(get_config)
):
"""
Sync current settings.json to config.yaml as new baseline.
Rate limited to 1 request per minute to prevent accidental overwrites.
Returns:
Operation status with backup file path
"""
# Rate limiting
if not check_rate_limit(request):
raise HTTPException(
status_code=429,
detail="Rate limit exceeded. Only 1 sync operation per minute allowed."
)
# Perform sync
result = config.sync_settings_to_base()
if result["status"] == "error":
raise HTTPException(status_code=500, detail=result["message"])
return JSONResponse(content=result)
@router.get("/api/settings/sync-status")
async def get_sync_status(config: UnifiedConfigManager = Depends(get_config)):
"""
Get information about sync-to-base operation readiness.
Returns:
Status information including if settings.json exists and validation state
"""
settings_exists = config.settings_file.exists()
validation_report = config.validate_configuration_consistency()
return {
"settings_file_exists": settings_exists,
"can_sync": settings_exists and validation_report.critical == 0,
"validation_summary": validation_report.summary(),
"backup_exists": config.base_config_file.with_suffix('.yaml.backup').exists()
}Phase 3.1: Atomic Write (3 hours)
- Implement
file_lock()context manager - Implement
_atomic_write_yaml()method - Add error handling and cleanup
- Unit tests for atomic operations
Phase 3.2: Sync Method (3 hours)
- Implement
sync_settings_to_base()method - Add backup creation logic
- Add validation integration
- Add settings.json cleanup
- Unit tests for sync logic
Phase 3.3: API Endpoint (2 hours)
- Create
autobot-backend/api/settings_sync.py - Implement rate limiting
- Add sync status endpoint
- Add router to
app_factory.py
Phase 3.4: Documentation (1 hour)
- Document API endpoints
- Add usage examples
- Document backup/restore procedure
Total Estimated Effort: 9 hours Priority: MEDIUM (useful for GUI integration)
Purpose: Automatically detect configuration file changes and reload across all VMs in distributed architecture.
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import asyncio
class ConfigFileHandler(FileSystemEventHandler):
"""
File system event handler for configuration files.
Implements debouncing to handle rapid successive changes.
"""
def __init__(self, config_manager, debounce_seconds: float = 0.5):
self.config_manager = config_manager
self.debounce_seconds = debounce_seconds
self._pending_reload = None
self._reload_lock = asyncio.Lock()
def on_modified(self, event):
"""Handle file modification events"""
if event.is_directory:
return
# Only watch config files
if event.src_path not in [
str(self.config_manager.base_config_file),
str(self.config_manager.settings_file)
]:
return
logger.info(f"Configuration file modified: {event.src_path}")
# Cancel pending reload
if self._pending_reload:
self._pending_reload.cancel()
# Schedule debounced reload
loop = asyncio.get_event_loop()
self._pending_reload = loop.call_later(
self.debounce_seconds,
lambda: asyncio.create_task(self._perform_reload(event.src_path))
)
async def _perform_reload(self, file_path: str):
"""
Perform the actual configuration reload.
Acquires lock to prevent concurrent reloads.
"""
async with self._reload_lock:
try:
logger.info(f"Reloading configuration from {file_path}...")
# Reload configuration
self.config_manager.reload()
# Validate new configuration
validation_report = self.config_manager.validate_configuration_consistency()
if validation_report.critical > 0:
logger.error("Configuration reload resulted in critical issues")
for issue in validation_report.get_critical():
logger.error(f" - {issue.message}")
# Reload from backup?
return
# Notify callbacks
await self.config_manager._notify_callbacks("main", self.config_manager.to_dict())
# Publish reload notification to Redis (for other VMs)
await self._publish_reload_notification(file_path)
logger.info("Configuration reloaded successfully")
except Exception as e:
logger.error(f"Configuration reload failed: {e}")
async def _publish_reload_notification(self, file_path: str):
"""
Publish reload notification to Redis pub/sub.
Other VMs subscribe to this channel and reload their local configs.
"""
try:
from src.utils.async_redis_manager import AsyncRedisManager
redis_manager = AsyncRedisManager()
await redis_manager.publish_message(
channel="config:reload",
message={
"timestamp": datetime.now(timezone.utc).isoformat(),
"source_vm": os.getenv("VM_HOSTNAME", "unknown"),
"file_path": file_path,
"action": "reload"
}
)
logger.info("Published reload notification to Redis")
except Exception as e:
logger.warning(f"Failed to publish reload notification: {e}")async def start_file_watcher(self) -> None:
"""
Start watching configuration files for changes.
Initializes watchdog observer and Redis pub/sub subscriber.
"""
if hasattr(self, '_file_watcher_observer'):
logger.warning("File watcher already running")
return
try:
# Start watchdog observer
event_handler = ConfigFileHandler(self)
observer = Observer()
observer.schedule(
event_handler,
str(self.config_dir),
recursive=False
)
observer.start()
self._file_watcher_observer = observer
self._file_watcher_handler = event_handler
logger.info(f"File watcher started for {self.config_dir}")
# Start Redis subscriber (for distributed reload)
await self._start_redis_subscriber()
except Exception as e:
logger.error(f"Failed to start file watcher: {e}")
raise
async def stop_file_watcher(self) -> None:
"""Stop file watching and cleanup resources"""
if hasattr(self, '_file_watcher_observer'):
self._file_watcher_observer.stop()
self._file_watcher_observer.join(timeout=5.0)
delattr(self, '_file_watcher_observer')
logger.info("File watcher stopped")
await self._stop_redis_subscriber()async def _start_redis_subscriber(self) -> None:
"""
Start Redis pub/sub subscriber for distributed reload notifications.
Listens on 'config:reload' channel for reload messages from other VMs.
"""
try:
from src.utils.async_redis_manager import AsyncRedisManager
self._redis_manager = AsyncRedisManager()
async def reload_handler(message: Dict[str, Any]):
"""Handle reload notification from other VMs"""
source_vm = message.get("source_vm")
current_vm = os.getenv("VM_HOSTNAME", "unknown")
# Ignore our own messages
if source_vm == current_vm:
return
logger.info(f"Received reload notification from {source_vm}")
# Reload local configuration
try:
self.reload()
await self._notify_callbacks("main", self.to_dict())
logger.info("Configuration reloaded from remote notification")
except Exception as e:
logger.error(f"Failed to reload from notification: {e}")
# Subscribe to reload channel
self._redis_subscriber_task = asyncio.create_task(
self._redis_manager.subscribe_channel("config:reload", reload_handler)
)
logger.info("Redis subscriber started for distributed config reload")
except Exception as e:
logger.warning(f"Failed to start Redis subscriber (graceful degradation): {e}")
async def _stop_redis_subscriber(self) -> None:
"""Stop Redis pub/sub subscriber"""
if hasattr(self, '_redis_subscriber_task'):
self._redis_subscriber_task.cancel()
try:
await self._redis_subscriber_task
except asyncio.CancelledError:
pass
delattr(self, '_redis_subscriber_task')
logger.info("Redis subscriber stopped")File: backend/app_factory.py - add to background initialization (line 547-616)
# In background_init() function, after Memory Graph initialization (~line 586)
# Initialize Configuration Hot Reload - NON-CRITICAL
logger.info("✅ [ 95%] Config Hot Reload: Initializing file watcher...")
try:
await app.state.config.start_file_watcher()
logger.info("✅ [ 95%] Config Hot Reload: File watcher active")
except Exception as reload_error:
logger.warning(f"Config hot reload initialization failed: {reload_error}")
logger.warning("Configuration will require manual reload")
logger.info("✅ [100%] PHASE 2 COMPLETE: All background services initialized")When Redis is unavailable:
async def _publish_reload_notification(self, file_path: str):
"""Publish with graceful fallback"""
try:
# Try Redis pub/sub
await self._redis_publish(...)
except Exception as e:
logger.warning(f"Redis unavailable, reload is local-only: {e}")
# Continue without distributed reloadWhen watchdog is unavailable:
async def start_file_watcher(self) -> None:
"""Start with fallback to polling"""
try:
# Try watchdog
observer = Observer()
...
except ImportError:
logger.warning("watchdog not available, using polling fallback")
await self._start_polling_watcher()Phase 4.1: Local File Watching (4 hours)
- Add watchdog dependency
- Implement
ConfigFileHandlerclass - Implement debouncing logic
- Add
start_file_watcher()andstop_file_watcher()methods - Unit tests for file watching
Phase 4.2: Reload Logic (3 hours)
- Implement
_perform_reload()method - Add validation integration
- Add callback notification
- Add error handling and rollback
- Test reload scenarios
Phase 4.3: Distributed Sync (4 hours)
- Implement Redis pub/sub publisher
- Implement Redis pub/sub subscriber
- Add VM identification logic
- Add message filtering (ignore own messages)
- Test cross-VM reload
Phase 4.4: Graceful Degradation (2 hours)
- Add fallback for Redis unavailable
- Add fallback for watchdog unavailable
- Add polling implementation
- Test degraded modes
Phase 4.5: Integration (2 hours)
- Add to app_factory.py background init
- Add cleanup to shutdown sequence
- Integration testing
- Documentation
Total Estimated Effort: 15 hours Priority: HIGH (significant operational improvement)
- Expand env var coverage
- Improve logging
- Document new variables
- Deliverable: Enhanced environment variable system
- Implement validation framework
- Add startup integration
- Create API endpoint
- Deliverable: Validation system with startup warnings
- Implement atomic writes
- Create sync-to-base endpoint
- Add rate limiting
- Deliverable: Settings sync API
- Implement file watching
- Add distributed reload
- Integrate with startup
- Deliverable: Hot reload with distributed support
- Test all features together
- Verify distributed behavior
- Performance testing
- Deliverable: Fully integrated system
- Update CLAUDE.md
- Create API documentation
- Deployment guide
- Deliverable: Production-ready feature set
File: tests/unit/test_config_validation.py
def test_validate_structure():
"""Test structure validation detects missing sections"""
def test_validate_types():
"""Test type validation catches type mismatches"""
def test_validate_ranges():
"""Test range validation enforces limits"""
def test_detect_conflicts():
"""Test conflict detection identifies source conflicts"""File: tests/unit/test_settings_sync.py
def test_atomic_write():
"""Test atomic write creates temp file and renames"""
def test_file_locking():
"""Test file locking prevents concurrent writes"""
def test_sync_to_base():
"""Test sync-to-base copies settings to config"""File: tests/unit/test_hot_reload.py
def test_file_watcher():
"""Test file watcher detects changes"""
def test_debouncing():
"""Test debouncing batches rapid changes"""
def test_reload_notification():
"""Test Redis pub/sub notification"""File: tests/integration/test_config_management.py
async def test_startup_validation():
"""Test validation runs during app startup"""
async def test_config_reload():
"""Test configuration reload updates app state"""
async def test_distributed_reload():
"""Test reload propagates across VMs"""File: tests/performance/test_config_performance.py
def test_validation_performance():
"""Ensure validation completes in < 100ms"""
def test_reload_performance():
"""Ensure reload completes in < 500ms"""
def test_concurrent_access():
"""Test file locking under concurrent access"""Add to requirements.txt:
watchdog>=3.0.0 # File system event monitoring
Document in CLAUDE.md:
# Configuration Management
AUTOBOT_CONFIG_AUTO_RELOAD=true # Enable hot reload
AUTOBOT_CONFIG_VALIDATION_ON_STARTUP=true # Validate on startup
AUTOBOT_CONFIG_DISTRIBUTED_RELOAD=true # Use Redis for distributed reload
VM_HOSTNAME=vm1-frontend # VM identifier for distributed reload-
Install Dependencies:
pip install -r requirements.txt
-
Update Configuration:
- No breaking changes to existing config files
- New features opt-in via environment variables
-
Deploy to VMs:
# Use sync-to-vm script to deploy updated code ./scripts/utilities/sync-to-vm.sh all src/ /home/autobot/src/ ./scripts/utilities/sync-to-vm.sh all backend/ /home/autobot/backend/ -
Verify Deployment:
# Check validation endpoint curl https://<backend-ip>:8443/api/config/validate # Check sync status curl https://<backend-ip>:8443/api/settings/sync-status
-
Enable Hot Reload:
# Set environment variable on all VMs export AUTOBOT_CONFIG_AUTO_RELOAD=true # Restart backend sudo systemctl restart autobot-backend
Risk: Configuration file corruption during atomic write Mitigation: Backup before write, validate before rename, rollback on error Impact: Service disruption
Risk: Redis unavailable during distributed reload Mitigation: Graceful degradation to local-only reload Impact: Reduced functionality
Risk: File locking timeout on slow filesystem Mitigation: Configurable timeout, async operations Impact: Failed sync operations
Risk: Validation false positives blocking startup Mitigation: Separate critical vs warning levels, non-blocking warnings Impact: Delayed startup
Risk: Rate limiting too restrictive Mitigation: Configurable rate limits, clear error messages Impact: User frustration
Risk: File watcher memory leak Mitigation: Proper cleanup in shutdown, watchdog validated Impact: Memory consumption
Risk: Debouncing delay too long Mitigation: Configurable debounce time (default 500ms) Impact: Slow reload response
- All 40+ configuration paths support env var override
- Override summary logged on startup
- Documentation updated in CLAUDE.md
- Validation completes in < 100ms
- Critical issues block startup
- Warnings logged but non-blocking
- API endpoint returns detailed report
- Atomic writes prevent corruption
- File locking prevents race conditions
- Rate limiting prevents accidents
- Backup created before sync
- File changes detected in < 1 second
- Reload completes in < 500ms
- Distributed reload propagates to all VMs
- Graceful degradation when Redis unavailable
- No breaking changes to existing deployments
- All tests passing (unit, integration, performance)
- Documentation complete
- Deployed to all 6 machines (main + 5 VMs)
This implementation plan provides a comprehensive approach to adding advanced configuration management features to AutoBot's backend. The phased approach ensures incremental delivery of value while managing complexity.
Estimated Total Effort: 37 hours (approximately 1 week with proper focus)
Key Benefits:
- Better troubleshooting with validation warnings
- Reduced configuration errors via conflict detection
- Improved GUI integration with sync-to-base API
- Operational excellence with hot reload
Next Steps:
- Review and approve this plan
- Begin Phase 1 implementation (Environment Variables)
- Iterate through phases with testing at each step
- Deploy and verify in production environment