diff --git a/src/server.py b/src/server.py index dda0d22..599267a 100644 --- a/src/server.py +++ b/src/server.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import secrets +import socket from contextlib import asynccontextmanager from pathlib import Path from typing import Any @@ -89,6 +90,7 @@ def create_app(config: AppConfig | None = None) -> FastAPI: app.state.db = db app.state.ws_hub = ws_hub app.state.manager = manager + device_hostname = socket.gethostname() def _extract_token( authorization: str | None, @@ -196,7 +198,7 @@ def create_app(config: AppConfig | None = None) -> FastAPI: if not app_config.disable_ui: @app.get("/", response_class=HTMLResponse) def ui_root() -> str: - return monitoring_page_html() + return monitoring_page_html(device_hostname=device_hostname) @app.websocket("/ws") async def ws_endpoint(websocket: WebSocket, token: str = Query(default="")) -> None: diff --git a/src/ui.py b/src/ui.py index df93730..a4bc53f 100644 --- a/src/ui.py +++ b/src/ui.py @@ -1,7 +1,9 @@ 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 """ @@ -14,7 +16,7 @@ def monitoring_page_html() -> str:
-

ScreenJob Monitor

+

ScreenJob Monitor__MONITOR_HOST__

Read-only monitoring for active and historical tasks.

@@ -41,7 +43,14 @@ def monitoring_page_html() -> str:
Latest visual update
-

Live Events

+
+

Live Events

+ +
@@ -56,13 +65,15 @@ def monitoring_page_html() -> str: const eventsEl = document.getElementById("events"); const statsEl = document.getElementById("stats"); const latestVisualEl = document.getElementById("latestVisual"); + const eventsViewToggle = document.getElementById("eventsViewToggle"); const state = { token: localStorage.getItem("screenjob_token") || "", jobs: [], selectedJobId: null, ws: null, - wsReconnectTimer: null + wsReconnectTimer: null, + eventsViewMode: localStorage.getItem("screenjob_events_view_mode") === "beautiful" ? "beautiful" : "raw" }; const manuallyClosedSockets = new WeakSet(); tokenInput.value = state.token; @@ -123,10 +134,56 @@ def monitoring_page_html() -> str: function pushEventLine(obj) { if (!obj || !obj.job_id || !obj.event_type) return; const line = document.createElement("div"); - line.className = "border-b border-slate-800 pb-1"; const ts = obj.ts || "-"; 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); while (eventsEl.childNodes.length > 400) { eventsEl.removeChild(eventsEl.lastChild); @@ -231,10 +288,20 @@ def monitoring_page_html() -> str: connectWs(); } + function syncEventsViewToggle() { + eventsViewToggle.checked = state.eventsViewMode === "beautiful"; + } + saveTokenBtn.addEventListener("click", () => connect().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(() => {}); -""" +""".replace("__MONITOR_HOST__", host_suffix)