159 lines
7.0 KiB
Python
159 lines
7.0 KiB
Python
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: "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() |