Add 3-step onboarding wizard with IPMI/HTTP test buttons + auto-activate fan curve

This commit is contained in:
devmatrix 2026-02-20 17:37:31 +00:00
parent 009131099a
commit a100327769
7 changed files with 99 additions and 168 deletions

Binary file not shown.

View File

@ -186,5 +186,5 @@
} }
}, },
"theme": "dark", "theme": "dark",
"active_curve": "Balanced" "active_curve": "Performance"
} }

View File

@ -1 +0,0 @@
{"users": {"admin": "ecd71870d1963316a97e3ac3408c9835ad8cf0f3c1bc703527c30265534f75ae"}}

View File

@ -348,22 +348,58 @@ class IPMIControllerService:
# Sensor Selection # Sensor Selection
"primary_sensor": "cpu", # cpu, cpu1, cpu2, inlet, exhaust, pcie, etc. "primary_sensor": "cpu", # cpu, cpu1, cpu2, inlet, exhaust, pcie, etc.
"sensor_preference": "ipmi", # ipmi, http, auto "sensor_preference": "auto", # ipmi, http, auto
# Fan Configuration # Fan Configuration
"fans": {}, # fan_id -> {"name": "Custom Name", "group": "group1"} "fans": {}, # fan_id -> {"name": "Custom Name", "group": "group1"}
"fan_groups": {}, # group_name -> {"fans": ["0x00", "0x01"], "curve": "Default"} "fan_groups": {}, # group_name -> {"fans": ["0x00", "0x01"], "curve": "Default"}
# Fan Curves # Fan Curves
"active_curve": "Balanced",
"fan_curves": { "fan_curves": {
"Default": { "Balanced": {
"points": [ "points": [
{"temp": 30, "speed": 15}, {"temp": 30, "speed": 10},
{"temp": 40, "speed": 25}, {"temp": 35, "speed": 12},
{"temp": 50, "speed": 40}, {"temp": 40, "speed": 15},
{"temp": 60, "speed": 60}, {"temp": 45, "speed": 20},
{"temp": 70, "speed": 80}, {"temp": 50, "speed": 30},
{"temp": 80, "speed": 100}, {"temp": 55, "speed": 40},
{"temp": 60, "speed": 55},
{"temp": 65, "speed": 70},
{"temp": 70, "speed": 85},
{"temp": 75, "speed": 95},
{"temp": 80, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Silent": {
"points": [
{"temp": 30, "speed": 5},
{"temp": 40, "speed": 10},
{"temp": 50, "speed": 15},
{"temp": 55, "speed": 25},
{"temp": 60, "speed": 35},
{"temp": 65, "speed": 50},
{"temp": 70, "speed": 70},
{"temp": 75, "speed": 85},
{"temp": 80, "speed": 100}
],
"sensor_source": "cpu",
"applies_to": "all"
},
"Performance": {
"points": [
{"temp": 30, "speed": 20},
{"temp": 35, "speed": 25},
{"temp": 40, "speed": 35},
{"temp": 45, "speed": 45},
{"temp": 50, "speed": 55},
{"temp": 55, "speed": 70},
{"temp": 60, "speed": 85},
{"temp": 65, "speed": 95},
{"temp": 70, "speed": 100}
], ],
"sensor_source": "cpu", "sensor_source": "cpu",
"applies_to": "all" "applies_to": "all"

View File

@ -1,26 +1,9 @@
INFO: Started server process [29754] INFO: Started server process [33302]
INFO: Waiting for application startup. INFO: Waiting for application startup.
INFO: Application startup complete. INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: 192.168.5.30:55306 - "GET /api/status HTTP/1.1" 401 Unauthorized INFO: 127.0.0.1:44708 - "GET / HTTP/1.1" 200 OK
INFO: 192.168.5.30:55306 - "GET /login HTTP/1.1" 200 OK INFO: 192.168.5.30:64440 - "GET /login HTTP/1.1" 200 OK
INFO: 192.168.5.30:54183 - "GET /login HTTP/1.1" 200 OK INFO: 192.168.5.30:56562 - "GET /favicon.ico HTTP/1.1" 200 OK
/home/devmatrix/projects/fan-controller-v2/web_server.py:149: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). INFO: 192.168.5.30:49401 - "POST /api/setup/test-ipmi HTTP/1.1" 200 OK
self._sessions[token] = (username, datetime.utcnow() + timedelta(days=7)) INFO: 192.168.5.30:49401 - "GET /favicon.ico HTTP/1.1" 200 OK
INFO: 127.0.0.1:36770 - "POST /api/auth/login HTTP/1.1" 200 OK
/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
if datetime.utcnow() > expiry:
2026-02-20 17:23:59,479 - fan_controller - INFO - Loaded config from /home/devmatrix/projects/fan-controller-v2/data/config.json
2026-02-20 17:23:59,481 - fan_controller - INFO - Saved config to /home/devmatrix/projects/fan-controller-v2/data/config.json
2026-02-20 17:23:59,638 - fan_controller - INFO - Connected to IPMI at 192.168.5.191
2026-02-20 17:23:59,638 - fan_controller - INFO - HTTP sensor client initialized for http://192.168.5.200:8888
2026-02-20 17:23:59,639 - fan_controller - INFO - IPMI Controller service started
INFO: 127.0.0.1:36774 - "POST /api/control/auto HTTP/1.1" 200 OK
2026-02-20 17:23:59,796 - fan_controller - INFO - Manual fan control enabled
2026-02-20 17:24:04,639 - fan_controller - INFO - Fan 0xff speed set to 12%
2026-02-20 17:24:04,640 - fan_controller - INFO - All fans set to 12% (Temp 35.0°C)
/home/devmatrix/projects/fan-controller-v2/web_server.py:156: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
if datetime.utcnow() > expiry:
INFO: 127.0.0.1:34428 - "GET /api/status HTTP/1.1" 200 OK
2026-02-20 17:24:19,530 - fan_controller - INFO - Fan 0xff speed set to 13%
2026-02-20 17:24:19,531 - fan_controller - INFO - All fans set to 13% (Temp 37.0°C)

View File

@ -49,6 +49,9 @@ class SetupRequest(BaseModel):
ipmi_username: str ipmi_username: str
ipmi_password: str ipmi_password: str
ipmi_port: int = 623 ipmi_port: int = 623
http_sensor_enabled: Optional[bool] = False
http_sensor_url: Optional[str] = None
http_sensor_timeout: Optional[int] = 10
class IPMIConfig(BaseModel): class IPMIConfig(BaseModel):
host: str host: str
@ -1142,135 +1145,8 @@ LOGIN_HTML = '''<!DOCTYPE html>
</body> </body>
</html>''' </html>'''
SETUP_HTML = '''<!DOCTYPE html> with open('/home/devmatrix/.openclaw/workspace/setup_html.txt', 'r') as f:
<html lang="en"> SETUP_HTML = f.read().strip().replace("SETUP_HTML = '''", "").replace("'''", "")
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/favicon.ico">
<title>Setup - IPMI Controller</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #0f0f1e 0%, #1a1a2e 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
padding: 20px;
}
.setup-box {
background: rgba(255,255,255,0.05);
padding: 40px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.1);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
}
h1 { text-align: center; margin-bottom: 10px; }
.subtitle { text-align: center; color: #888; margin-bottom: 30px; }
.section { margin-bottom: 25px; padding-bottom: 25px; border-bottom: 1px solid rgba(255,255,255,0.1); }
.section:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
h2 { font-size: 1.1rem; color: #64b5f6; margin-bottom: 15px; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 6px; color: #aaa; font-size: 0.9rem; }
input {
width: 100%;
padding: 12px;
background: rgba(0,0,0,0.2);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
color: #fff;
font-size: 1rem;
}
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
button {
width: 100%;
padding: 14px;
background: #4caf50;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
}
button:hover { background: #388e3c; }
.error {
background: rgba(244,67,54,0.2);
border: 1px solid #f44336;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
</style>
</head>
<body>
<div class="setup-box">
<h1>🌡 IPMI Controller</h1>
<p class="subtitle">Initial Setup</p>
<div class="error" id="error"></div>
<form id="setup-form">
<div class="section">
<h2>👤 Admin Account</h2>
<div class="form-row">
<div class="form-group"><label>Username</label><input type="text" id="admin-user" required></div>
<div class="form-group"><label>Password</label><input type="password" id="admin-pass" required minlength="6"></div>
</div>
</div>
<div class="section">
<h2>🖥 IPMI Connection</h2>
<div class="form-group"><label>Host/IP</label><input type="text" id="ipmi-host" placeholder="192.168.1.100" required></div>
<div class="form-row">
<div class="form-group"><label>Username</label><input type="text" id="ipmi-user" value="root" required></div>
<div class="form-group"><label>Password</label><input type="password" id="ipmi-pass" required></div>
</div>
<div class="form-group"><label>Port</label><input type="number" id="ipmi-port" value="623"></div>
</div>
<button type="submit">Complete Setup</button>
</form>
</div>
<script>
document.getElementById('setup-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('error');
errorEl.style.display = 'none';
const data = {
admin_username: document.getElementById('admin-user').value,
admin_password: document.getElementById('admin-pass').value,
ipmi_host: document.getElementById('ipmi-host').value,
ipmi_username: document.getElementById('ipmi-user').value,
ipmi_password: document.getElementById('ipmi-pass').value,
ipmi_port: parseInt(document.getElementById('ipmi-port').value) || 623
};
try {
const res = await fetch('/api/setup', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await res.json();
if (result.success) {
window.location.href = '/';
} else {
errorEl.textContent = result.error || 'Setup failed';
errorEl.style.display = 'block';
}
} catch (e) {
errorEl.textContent = 'Connection error';
errorEl.style.display = 'block';
}
});
</script>
</body>
</html>'''
# FastAPI app # FastAPI app
@asynccontextmanager @asynccontextmanager
@ -1344,6 +1220,35 @@ async def api_change_password(data: ChangePassword, username: str = Depends(get_
return {"success": False, "error": "Current password is incorrect"} return {"success": False, "error": "Current password is incorrect"}
return {"success": True} return {"success": True}
# Setup test endpoints (no auth required for setup)
@app.post("/api/setup/test-ipmi")
async def api_setup_test_ipmi(data: dict):
"""Test IPMI connection during setup."""
from fan_controller import IPMIFanController
try:
controller = IPMIFanController(
host=data.get('host'),
username=data.get('username'),
password=data.get('password'),
port=data.get('port', 623)
)
if controller.test_connection():
return {"success": True}
return {"success": False, "error": "Connection failed"}
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/setup/test-http")
async def api_setup_test_http(data: dict):
"""Test HTTP sensor during setup."""
from fan_controller import HTTPSensorClient
try:
client = HTTPSensorClient(url=data.get('url'), timeout=10)
temps = client.fetch_sensors()
return {"success": True, "sensors": len(temps)}
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/setup") @app.post("/api/setup")
async def api_setup(data: SetupRequest): async def api_setup(data: SetupRequest):
if user_manager.is_setup_complete(): if user_manager.is_setup_complete():
@ -1353,12 +1258,20 @@ async def api_setup(data: SetupRequest):
return {"success": False, "error": "Failed to create user"} return {"success": False, "error": "Failed to create user"}
service = get_service(str(CONFIG_FILE)) service = get_service(str(CONFIG_FILE))
service.update_config( updates = {
ipmi_host=data.ipmi_host, "ipmi_host": data.ipmi_host,
ipmi_username=data.ipmi_username, "ipmi_username": data.ipmi_username,
ipmi_password=data.ipmi_password, "ipmi_password": data.ipmi_password,
ipmi_port=data.ipmi_port "ipmi_port": data.ipmi_port
) }
# Add HTTP config if provided
if hasattr(data, 'http_sensor_enabled') and data.http_sensor_enabled:
updates["http_sensor_enabled"] = True
updates["http_sensor_url"] = getattr(data, 'http_sensor_url', '')
updates["http_sensor_timeout"] = getattr(data, 'http_sensor_timeout', 10)
service.update_config(**updates)
token = user_manager.create_token(data.admin_username) token = user_manager.create_token(data.admin_username)
return {"success": True, "token": token} return {"success": True, "token": token}