Technician Debug Web Server
Build, push image, and notify Watchtower / build-image (push) Successful in 42s
Build, push image, and notify Watchtower / notify (push) Successful in 13s

This commit is contained in:
2026-08-19 17:04:33 +02:00
parent 022c45edee
commit bb5327fb93
3 changed files with 167 additions and 1 deletions
+4 -1
View File
@@ -1,9 +1,12 @@
paho-mqtt>=1.6,<3
pyserial>=3.5,<4
zeroconf>=0.131.0
fastapi>=0.100.0,<1.0.0
uvicorn>=0.20.0,<1.0.0
python-multipart>=0.0.6,<1.0.0
# picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2
# OpenCV
# sudo apt install -y python3-opencv
# sudo apt install -y opencv-data
zeroconf>=0.131.0
# sudo apt install pigpio python3-pigpio
# sudo systemctl enable pigpiod --now
+159
View File
@@ -0,0 +1,159 @@
import asyncio
from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
import uvicorn
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SmartWave Technician Portal</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #121212; color: #e0e0e0; height: 100vh; overflow: hidden; }
.container { display: flex; height: 100vh; }
.logs-section { flex: 1; display: flex; flex-direction: column; border-right: 1px solid #333; padding: 15px; }
.logs-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.logs-header h2 { font-size: 1.1rem; color: #4fc3f7; }
.logs-header button { background: #333; color: #fff; border: 1px solid #555; padding: 4px 10px; border-radius: 4px; cursor: pointer; }
pre#log-output { flex: 1; background: #000; color: #00ff66; padding: 12px; border-radius: 6px; overflow-y: auto; font-size: 0.85rem; line-height: 1.4; white-space: pre-wrap; word-break: break-all; }
.control-section { width: 500px; padding: 20px; background: #1a1a1a; display: flex; flex-direction: column; gap: 15px; }
.control-section h2 { font-size: 1.1rem; color: #ffb74d; margin-bottom: 5px; }
.form-group { display: flex; flex-direction: column; gap: 6px; }
label { font-size: 0.85rem; color: #aaa; }
input[type="text"] { background: #2a2a2a; border: 1px solid #444; color: #fff; padding: 10px; border-radius: 4px; font-size: 0.9rem; }
input[type="text"]:focus { outline: none; border-color: #ffb74d; }
button.btn-submit { background: #ffb74d; color: #121212; border: none; padding: 12px; font-weight: bold; border-radius: 4px; cursor: pointer; transition: background 0.2s; }
button.btn-submit:hover { background: #ffa726; }
#status-msg { font-size: 0.85rem; padding: 8px; border-radius: 4px; display: none; }
.success { background: #1b5e20; color: #a5d6a7; }
.error { background: #b71c1c; color: #ef9a9a; }
</style>
</head>
<body>
<div class="container">
<div class="logs-section">
<div class="logs-header">
<h2>SmartWave Orchestrator System Logs</h2>
<button onclick="fetchLogs()">Refresh</button>
</div>
<pre id="log-output">Loading logs...</pre>
</div>
<div class="control-section">
<h2>Telemetry Control</h2>
<div class="form-group">
<label for="endpoint">Telemetry Endpoint URL:</label>
<input type="text" id="endpoint" value="https://smartwave.matthiasg.dev/telemetry">
</div>
<button class="btn-submit" onclick="triggerTelemetry()">Send Telemetry</button>
<div id="status-msg"></div>
</div>
</div>
<script>
async function fetchLogs() {
try {
const res = await fetch('/api/logs');
const logs = await res.json();
const logContainer = document.getElementById('log-output');
logContainer.textContent = logs.map(entry => {
const ts = entry.timestamp ? entry.timestamp.replace('T', ' ').split('.')[0] : 'RAW';
const pid = entry.pid ? `[PID:${entry.pid}]` : '';
return `${ts} ${pid} ${entry.message}`;
}).join('\\n');
logContainer.scrollTop = logContainer.scrollHeight;
} catch (err) {
console.error("Failed to fetch logs", err);
}
}
async function triggerTelemetry() {
const endpoint = document.getElementById('endpoint').value;
const statusDiv = document.getElementById('status-msg');
statusDiv.style.display = 'block';
statusDiv.className = '';
statusDiv.textContent = 'Sending telemetry...';
try {
const formData = new FormData();
formData.append('endpoint', endpoint);
const res = await fetch('/api/telemetry', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok && data.status === 'ok') {
statusDiv.className = 'success';
statusDiv.textContent = 'Telemetry sent successfully!';
} else {
throw new Error(data.detail || 'Failed');
}
} catch (err) {
statusDiv.className = 'error';
statusDiv.textContent = 'Error sending telemetry: ' + err.message;
}
}
fetchLogs();
setInterval(fetchLogs, 3000);
</script>
</body>
</html>
"""
def create_app(get_logs_cb, telemetry_cb, is_tech_mode_cb) -> FastAPI:
"""Factory creating FastAPI app with injected handler callbacks."""
app = FastAPI(title="Technician Portal")
# Attach callbacks to app.state
app.state.get_systemd_logs = get_logs_cb
app.state.handle_telemetry_request = telemetry_cb
app.state.is_tech_mode = is_tech_mode_cb
# Middleware to block traffic when TECHNICIAN_MODE is False
@app.middleware("http")
async def enforce_technician_mode(request: Request, call_next):
if not request.app.state.is_tech_mode():
return JSONResponse(
status_code=503,
content={"detail": "Technician mode is disabled."}
)
return await call_next(request)
@app.get("/", response_class=HTMLResponse)
async def serve_dashboard():
return HTML_TEMPLATE
@app.get("/api/logs")
async def api_get_logs(request: Request):
try:
logs = await request.app.state.get_systemd_logs(lines=200, service_name="smartwave")
return JSONResponse(content=logs)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/telemetry")
async def api_trigger_telemetry(request: Request, endpoint: str = Form(...)):
try:
await request.app.state.handle_telemetry_request(endpoint=endpoint, do_timeout=True)
return {"status": "ok", "message": f"Telemetry request sent to {endpoint}"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Telemetry trigger failed: {str(e)}")
return app
async def start_technician_web_server(get_logs_cb, telemetry_cb, is_tech_mode_cb, host="192.168.50.1", port=8080):
"""Asynchronous server launcher meant to run within main.py's event loop."""
app = create_app(get_logs_cb, telemetry_cb, is_tech_mode_cb)
config = uvicorn.Config(app, host=host, port=port, log_level="info")
server = uvicorn.Server(config)
await server.serve()
+4
View File
@@ -3,6 +3,10 @@ DEBUG=True
DEBUG_DANGEROUS_AREA=False
DEBUG_TEMPERATURE_ALERT=False
# Technitian Web Server
TECHNICIAN_WEB_SERVER_HOST = "192.168.50.1"
TECHNICIAN_WEB_SERVER_PORT = 8080
# LoRa
LORA_HEARTBEAT_INTERVAL = 30