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 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:
|
||||||
|
|||||||
77
src/ui.py
77
src/ui.py
@@ -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>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="font-semibold text-sm">Live Events</h3>
|
<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 ?? "-");
|
||||||
|
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 || {})}`;
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user