Add 3-step onboarding wizard with IPMI/HTTP test buttons + auto-activate fan curve
This commit is contained in:
parent
009131099a
commit
a100327769
Binary file not shown.
Binary file not shown.
|
|
@ -186,5 +186,5 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"theme": "dark",
|
"theme": "dark",
|
||||||
"active_curve": "Balanced"
|
"active_curve": "Performance"
|
||||||
}
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"users": {"admin": "ecd71870d1963316a97e3ac3408c9835ad8cf0f3c1bc703527c30265534f75ae"}}
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
29
server.log
29
server.log
|
|
@ -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)
|
|
||||||
|
|
|
||||||
183
web_server.py
183
web_server.py
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue