feat: include device hostname in monitoring page and enhance event view toggle functionality
All checks were successful
CI / test (push) Successful in 7s

This commit is contained in:
Space-Banane
2026-05-27 21:29:33 +02:00
parent 595375e1a7
commit 52e09ce3b0
2 changed files with 77 additions and 8 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import secrets import secrets
import socket
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -89,6 +90,7 @@ def create_app(config: AppConfig | None = None) -> FastAPI:
app.state.db = db app.state.db = db
app.state.ws_hub = ws_hub app.state.ws_hub = ws_hub
app.state.manager = manager app.state.manager = manager
device_hostname = socket.gethostname()
def _extract_token( def _extract_token(
authorization: str | None, authorization: str | None,
@@ -196,7 +198,7 @@ def create_app(config: AppConfig | None = None) -> FastAPI:
if not app_config.disable_ui: if not app_config.disable_ui:
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
def ui_root() -> str: def ui_root() -> str:
return monitoring_page_html() return monitoring_page_html(device_hostname=device_hostname)
@app.websocket("/ws") @app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket, token: str = Query(default="")) -> None: async def ws_endpoint(websocket: WebSocket, token: str = Query(default="")) -> None:

View File

@@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
from html import escape
def monitoring_page_html() -> str: def monitoring_page_html(device_hostname: str = "") -> str:
host_suffix = f" ({escape(device_hostname)})" if device_hostname else ""
return """<!doctype html> return """<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -14,7 +16,7 @@ def monitoring_page_html() -> str:
<div class="max-w-7xl mx-auto p-4 md:p-8 space-y-6"> <div class="max-w-7xl mx-auto p-4 md:p-8 space-y-6">
<header class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> <header class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div> <div>
<h1 class="text-2xl md:text-3xl font-bold tracking-tight">ScreenJob Monitor</h1> <h1 class="text-2xl md:text-3xl font-bold tracking-tight">ScreenJob Monitor<span class="text-slate-400 text-base md:text-lg font-medium">__MONITOR_HOST__</span></h1>
<p class="text-slate-400 text-sm">Read-only monitoring for active and historical tasks.</p> <p class="text-slate-400 text-sm">Read-only monitoring for active and historical tasks.</p>
</div> </div>
<div class="flex flex-col md:flex-row gap-2 md:items-center"> <div class="flex flex-col md:flex-row gap-2 md:items-center">
@@ -41,7 +43,14 @@ def monitoring_page_html() -> str:
<div class="bg-slate-950 border border-slate-800 rounded p-2"> <div class="bg-slate-950 border border-slate-800 rounded p-2">
<img id="latestVisual" alt="Latest visual update" class="max-h-[24vh] w-full object-contain rounded" /> <img id="latestVisual" alt="Latest visual update" class="max-h-[24vh] w-full object-contain rounded" />
</div> </div>
<h3 class="font-semibold text-sm">Live Events</h3> <div class="flex items-center justify-between">
<h3 class="font-semibold text-sm">Live Events</h3>
<label for="eventsViewToggle" class="flex items-center gap-2 text-xs text-slate-300 cursor-pointer select-none">
<span>Raw</span>
<input id="eventsViewToggle" type="checkbox" class="accent-cyan-400 h-4 w-4" />
<span>Beautiful</span>
</label>
</div>
<div id="events" class="bg-slate-950 border border-slate-800 rounded p-3 text-xs overflow-auto max-h-[36vh] space-y-1"></div> <div id="events" class="bg-slate-950 border border-slate-800 rounded p-3 text-xs overflow-auto max-h-[36vh] space-y-1"></div>
</div> </div>
</section> </section>
@@ -56,13 +65,15 @@ def monitoring_page_html() -> str:
const eventsEl = document.getElementById("events"); const eventsEl = document.getElementById("events");
const statsEl = document.getElementById("stats"); const statsEl = document.getElementById("stats");
const latestVisualEl = document.getElementById("latestVisual"); const latestVisualEl = document.getElementById("latestVisual");
const eventsViewToggle = document.getElementById("eventsViewToggle");
const state = { const state = {
token: localStorage.getItem("screenjob_token") || "", token: localStorage.getItem("screenjob_token") || "",
jobs: [], jobs: [],
selectedJobId: null, selectedJobId: null,
ws: null, ws: null,
wsReconnectTimer: null wsReconnectTimer: null,
eventsViewMode: localStorage.getItem("screenjob_events_view_mode") === "beautiful" ? "beautiful" : "raw"
}; };
const manuallyClosedSockets = new WeakSet(); const manuallyClosedSockets = new WeakSet();
tokenInput.value = state.token; tokenInput.value = state.token;
@@ -123,10 +134,56 @@ def monitoring_page_html() -> str:
function pushEventLine(obj) { function pushEventLine(obj) {
if (!obj || !obj.job_id || !obj.event_type) return; if (!obj || !obj.job_id || !obj.event_type) return;
const line = document.createElement("div"); const line = document.createElement("div");
line.className = "border-b border-slate-800 pb-1";
const ts = obj.ts || "-"; const ts = obj.ts || "-";
const step = (obj.step ?? "-"); const step = (obj.step ?? "-");
line.textContent = `[${ts}] ${obj.job_id} step=${step} ${obj.event_type} ${JSON.stringify(obj.payload || {})}`; if (state.eventsViewMode === "raw") {
line.className = "border-b border-slate-800 pb-1";
line.textContent = `[${ts}] ${obj.job_id} step=${step} ${obj.event_type} ${JSON.stringify(obj.payload || {})}`;
} else {
const typeColors = {
info: "bg-sky-900/50 text-sky-200 border border-sky-800",
warning: "bg-amber-900/40 text-amber-200 border border-amber-800",
error: "bg-rose-900/40 text-rose-200 border border-rose-800",
visual_update: "bg-emerald-900/40 text-emerald-200 border border-emerald-800",
tool_call: "bg-violet-900/40 text-violet-200 border border-violet-800",
tool_result: "bg-indigo-900/40 text-indigo-200 border border-indigo-800"
};
const dt = new Date(ts);
const tsText = Number.isNaN(dt.getTime()) ? ts : dt.toLocaleString();
const payload = obj.payload || {};
line.className = "rounded-lg border border-slate-800 bg-slate-900/80 p-2 space-y-2";
const header = document.createElement("div");
header.className = "flex flex-wrap items-center gap-2";
const typePill = document.createElement("span");
typePill.className = `px-2 py-0.5 rounded text-[10px] font-semibold ${typeColors[obj.event_type] || "bg-slate-800 text-slate-200 border border-slate-700"}`;
typePill.textContent = obj.event_type;
const stepPill = document.createElement("span");
stepPill.className = "px-2 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 border border-slate-700";
stepPill.textContent = `step ${step}`;
const tsSpan = document.createElement("span");
tsSpan.className = "text-[10px] text-slate-400";
tsSpan.textContent = tsText;
header.appendChild(typePill);
header.appendChild(stepPill);
header.appendChild(tsSpan);
const jobLine = document.createElement("div");
jobLine.className = "text-[11px] text-slate-300 font-medium";
jobLine.textContent = obj.job_id;
const body = document.createElement("pre");
body.className = "bg-slate-950 border border-slate-800 rounded p-2 text-[11px] text-slate-200 overflow-auto";
body.textContent = JSON.stringify(payload, null, 2);
line.appendChild(header);
line.appendChild(jobLine);
line.appendChild(body);
}
eventsEl.prepend(line); eventsEl.prepend(line);
while (eventsEl.childNodes.length > 400) { while (eventsEl.childNodes.length > 400) {
eventsEl.removeChild(eventsEl.lastChild); eventsEl.removeChild(eventsEl.lastChild);
@@ -231,10 +288,20 @@ def monitoring_page_html() -> str:
connectWs(); connectWs();
} }
function syncEventsViewToggle() {
eventsViewToggle.checked = state.eventsViewMode === "beautiful";
}
saveTokenBtn.addEventListener("click", () => connect().catch((err) => alert(err.message))); saveTokenBtn.addEventListener("click", () => connect().catch((err) => alert(err.message)));
refreshBtn.addEventListener("click", () => fullRefresh().catch((err) => alert(err.message))); refreshBtn.addEventListener("click", () => fullRefresh().catch((err) => alert(err.message)));
eventsViewToggle.addEventListener("change", () => {
state.eventsViewMode = eventsViewToggle.checked ? "beautiful" : "raw";
localStorage.setItem("screenjob_events_view_mode", state.eventsViewMode);
refreshJobDetail().catch((err) => alert(err.message));
});
syncEventsViewToggle();
if (state.token) connect().catch(() => {}); if (state.token) connect().catch(() => {});
</script> </script>
</body> </body>
</html> </html>
""" """.replace("__MONITOR_HOST__", host_suffix)