Create complete web configuration pages following Bob The Skull's aesthetic and patterns. Use when creating new config pages, adding settings pages, or extending the web configuration interface.
Creates complete configuration pages following Bob The Skull's established aesthetic, structure, and backend patterns.
All config pages follow this pattern:
1. HTML Template - web/templates/config_<name>.html
2. Flask Routes - web/monitor_server.py (GET page, GET settings, POST save)
3. Backend Methods - web/config_manager.py (get_<name>_settings, save_<name>_settings)
4. Dashboard Card - web/templates/config_dashboard.html
5. Test Script - test_<name>_config.py (optional but recommended)
{% extends "base_config.html" %}
{% block title %}[Page Title] - Bob The Skull Configuration{% endblock %}
{% block config_content %}
<div class="config-header">
<h1>[PAGE TITLE]</h1>
<p class="config-subtitle">[Brief description of what this page configures]</p>
</div>
<!-- Loading indicator -->
<div id="loading" class="loading">
<p>β³ LOADING SETTINGS...</p>
</div>
<!-- Settings form -->
<div id="settings-content" style="display: none;">
<form id="[name]Form">
<!-- Section 1 -->
<div class="settings-section">
<h2>[SECTION NAME]</h2>
<p style="color: #00aa00; font-size: 0.85em; margin-bottom: 15px;">
[Section description]
</p>
<!-- Text/Number Input -->
<div class="form-group">
<label for="param_name">[Display Name]</label>
<input type="[text|number]" id="param_name" name="param_name"
min="X" max="Y" step="Z" required>
<span class="form-help">[Help text explaining the parameter]</span>
</div>
<!-- Checkbox -->
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="feature_enabled" name="feature_enabled">
<label for="feature_enabled">[Feature Name]</label>
</div>
<span class="form-help">[Help text]</span>
</div>
<!-- Dropdown -->
<div class="form-group">
<label for="option_select">[Option Name]</label>
<select id="option_select" name="option_select">
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</select>
<span class="form-help">[Help text]</span>
</div>
</div>
<!-- Save buttons -->
<div class="button-group">
<button type="submit" class="btn btn-primary" id="saveBtn">SAVE SETTINGS</button>
<a href="/config" class="btn btn-secondary">BACK TO DASHBOARD</a>
</div>
</form>
<!-- Status messages -->
<div id="status-container"></div>
</div>
<script>
let currentSettings = null;
// Load settings on page load
document.addEventListener('DOMContentLoaded', async () => {
await loadSettings();
});
async function loadSettings() {
try {
const response = await fetch('/config/[name]/settings');
const data = await response.json();
if (data.error) {
showStatus('Failed to load settings: ' + data.error, 'error');
return;
}
currentSettings = data;
populateForm(data);
// Hide loading, show content
document.getElementById('loading').style.display = 'none';
document.getElementById('settings-content').style.display = 'block';
} catch (error) {
showStatus('Failed to load settings: ' + error.message, 'error');
}
}
function populateForm(settings) {
// Section 1
document.getElementById('param_name').value = settings.section1.param_name;
document.getElementById('feature_enabled').checked = settings.section1.feature_enabled;
document.getElementById('option_select').value = settings.section1.option_select;
// Section 2
// ... more fields
}
// Handle form submission
document.getElementById('[name]Form').addEventListener('submit', async (e) => {
e.preventDefault();
const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
saveBtn.textContent = 'SAVING...';
const settings = {
section1: {
param_name: document.getElementById('param_name').value,
feature_enabled: document.getElementById('feature_enabled').checked,
option_select: document.getElementById('option_select').value
},
section2: {
// ... more fields
}
};
try {
showStatus('Saving settings...', 'success');
const response = await fetch('/config/[name]/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
});
const result = await response.json();
if (result.success) {
showStatus(result.message, 'success');
currentSettings = settings;
} else {
showStatus('Save failed: ' + result.message, 'error');
}
} catch (error) {
showStatus('Save failed: ' + error.message, 'error');
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'SAVE SETTINGS';
}
});
function showStatus(message, type) {
const container = document.getElementById('status-container');
const statusDiv = document.createElement('div');
statusDiv.className = `status-message status-${type}`;
statusDiv.textContent = message;
container.innerHTML = '';
container.appendChild(statusDiv);
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
</script>
{% endblock %}
Add these routes to MonitorServer class:
@self.app.route('/config/[name]')
def config_[name]():
"""[Name] configuration page"""
return render_template('config_[name].html')
@self.app.route('/config/[name]/settings')
def get_[name]_settings():
"""Get current [name] settings"""
settings = self.config_manager.get_[name]_settings()
return jsonify(settings)
@self.app.route('/config/[name]/save', methods=['POST'])
def save_[name]_settings():
"""Save [name] settings"""
settings = request.get_json()
result = self.config_manager.save_[name]_settings(settings)
return jsonify(result)
def get_[name]_settings(self) -> Dict:
"""
Get current [name] settings from BobConfig
Returns:
Dictionary containing all configurable [name] settings
"""
try:
return {
'section1': {
'param1': self.config.PARAM1,
'param2': self.config.PARAM2
},
'section2': {
'param3': self.config.PARAM3
}
}
except Exception as e:
logger.error(f"Error getting [name] settings: {e}", exc_info=True)
return {'error': str(e)}
def save_[name]_settings(self, settings: Dict) -> Dict:
"""
Save [name] settings to BobConfig.py
Args:
settings: Dictionary of settings to save (can be partial update)
Returns:
Dictionary with success status and message
"""
try:
config_path = Path(__file__).parent.parent / 'BobConfig.py'
if not config_path.exists():
return {'success': False, 'message': f'Config file not found: {config_path}'}
# Read current config file
with open(config_path, 'r', encoding='utf-8') as f:
content = f.read()
# Track changes
changes_made = []
# Section 1 settings
if 'section1' in settings:
sec1 = settings['section1']
if 'param1' in sec1:
content = re.sub(
r'PARAM1:\s*int\s*=\s*\d+',
f'PARAM1: int = {sec1["param1"]}',
content
)
changes_made.append(f"PARAM1 = {sec1['param1']}")
if 'param2' in sec1:
content = re.sub(
r'PARAM2:\s*bool\s*=\s*(True|False)',
f'PARAM2: bool = {sec1["param2"]}',
content
)
changes_made.append(f"PARAM2 = {sec1['param2']}")
# Section 2 settings
if 'section2' in settings:
sec2 = settings['section2']
if 'param3' in sec2:
content = re.sub(
r'PARAM3:\s*float\s*=\s*[\d.]+',
f'PARAM3: float = {float(sec2["param3"])}',
content
)
changes_made.append(f"PARAM3 = {sec2['param3']}")
if not changes_made:
return {'success': False, 'message': 'No valid settings to update'}
# Write updated config
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"Updated [name] settings: {', '.join(changes_made)}")
return {
'success': True,
'message': f'Settings saved successfully. Updated: {", ".join(changes_made)}'
}
except Exception as e:
logger.error(f"Error saving [name] settings: {e}", exc_info=True)
return {'success': False, 'message': f'Error: {str(e)}'}
Add card to dashboard (around line 100-200):
<!-- [Name] Configuration -->
<a href="/config/[name]" class="config-card">
<div class="config-icon">[EMOJI]</div>
<h3>[Name] Settings</h3>
<p>[Brief description]</p>
<div class="config-status">
<span class="badge badge-success">β Available</span>
</div>
</a>
"""
Test [Name] Configuration
Quick test to verify [name] routes are registered
"""
from BobConfig import BobConfig
from web.monitor_server import MonitorServer
from local_event_bus import LocalEventBus
import time
def test_[name]_settings():
print("=" * 60)
print("[Name] Configuration - Test")
print("=" * 60)
# Initialize components
print("\n1. Initializing MonitorServer...")
config = BobConfig()
event_bus = LocalEventBus()
monitor = MonitorServer(event_bus=event_bus, config=config)
print(" [PASS] MonitorServer initialized")
# List all registered routes
print("\n2. Checking registered routes...")
required_routes = ['/config/[name]', '/config/[name]/settings', '/config/[name]/save']
all_routes = [rule.rule for rule in monitor.app.url_map.iter_rules()]
for route in required_routes:
if route in all_routes:
print(f" [FOUND] {route}")
else:
print(f" [MISSING] {route}")
return False
# Start server in background
print("\n3. Starting web server...")
import threading
server_thread = threading.Thread(target=lambda: monitor.start(), daemon=True)
server_thread.start()
time.sleep(2)
# Test with requests
print("\n4. Testing routes...")
import requests
try:
response = requests.get('http://localhost:5000/config/[name]/settings', timeout=5)
print(f" GET /config/[name]/settings: {response.status_code}")
if response.status_code == 200:
settings = response.json()
print(f" Response keys: {list(settings.keys())}")
for section in settings:
print(f" - {section}: {list(settings[section].keys())}")
except Exception as e:
print(f" Error: {e}")
return False
print("\n" + "="*60)
print("[PASS] All [name] configuration tests passed!")
print("="*60)
print("\nServer is running. Open in browser:")
print(" - Config Dashboard: http://localhost:5000/config")
print(" - [Name] Settings: http://localhost:5000/config/[name]")
print("\nPress Ctrl+C to stop...")
print("="*60)
# Keep server running
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n\nStopping...")
return True
if __name__ == "__main__":
test_[name]_settings()
#1a1a1a (dark gray)#00ff00 (terminal green)#00aa00 (darker green)#ff4444 (red)#00ff00 (green)#ffaa00 (orange)input[type="text"],
input[type="number"],
select {
background: #2a2a2a;
border: 1px solid #00ff00;
color: #00ff00;
padding: 8px;
font-family: 'Courier New', monospace;
}
button.btn-primary {
background: #00aa00;
color: #000;
border: 2px solid #00ff00;
padding: 10px 20px;
font-weight: bold;
}
<div class="status-message status-success">
β Settings saved successfully
</div>
<div class="status-message status-error">
β Error saving settings
</div>
config_audio.html) - Device selection, volumesconfig_vision.html) - Camera, face detection, performanceconfig_system.html) - Wake word, STT, LLM, TTS modelsconfig_performance.html) - Timeouts, state machineconfig_eyes.html) - Hardware interface, discoveryconfig_apikeys.html) - API key managementInput Validation:
if (value < min || value > max) {
showStatus('Value must be between ' + min + ' and ' + max, 'error');
return;
}
Confirmation for destructive actions:
if (!confirm('Are you sure you want to reset all settings?')) {
return;
}
Loading states:
saveBtn.disabled = true;
saveBtn.textContent = 'SAVING...';
// ... do save ...
saveBtn.disabled = false;
saveBtn.textContent = 'SAVE SETTINGS';