feat: include device hostname in monitoring page and enhance event view toggle functionality
All checks were successful
CI / test (push) Successful in 7s
All checks were successful
CI / test (push) Successful in 7s
This commit is contained in:
@@ -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:
|
||||
|
||||
81
src/ui.py
81
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 """<!doctype html>
|
||||
<html lang="en">
|
||||
<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">
|
||||
<header class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
<img id="latestVisual" alt="Latest visual update" class="max-h-[24vh] w-full object-contain rounded" />
|
||||
</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>
|
||||
</section>
|
||||
@@ -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(() => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
""".replace("__MONITOR_HOST__", host_suffix)
|
||||
|
||||
Reference in New Issue
Block a user