diff --git a/frontend/src/pages/EventsPage.tsx b/frontend/src/pages/EventsPage.tsx index 17345a1..dc82b44 100644 --- a/frontend/src/pages/EventsPage.tsx +++ b/frontend/src/pages/EventsPage.tsx @@ -4,7 +4,6 @@ import { toast } from "sonner"; import { api } from "../api"; import { eventTitle } from "../constants"; -import { DateTime } from "../components/DateTime"; import { EmptyState } from "../components/EmptyState"; import type { JobEvent } from "../types"; @@ -15,6 +14,21 @@ function mergeEvents(current: JobEvent[], incoming: JobEvent[]) { return [...current, ...incoming.filter((event) => !seen.has(event.id))].sort((a, b) => b.id - a.id); } +function eventTone(type: string) { + if (type.includes("fail")) return "danger"; + if (type.includes("success")) return "success"; + if (type.includes("retry") || type.includes("timeout")) return "warning"; + if (type.includes("progress") || type.includes("lease")) return "info"; + if (type.includes("claim")) return "accent"; + return "neutral"; +} + +function formatLogTime(value: string) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "--:--:--"; + return date.toLocaleTimeString(undefined, { hour12: false }); +} + export function EventsPage() { const [events, setEvents] = useState([]); const [nextPageUrl, setNextPageUrl] = useState(null); @@ -89,42 +103,32 @@ export function EventsPage() { -
-
-

Global Event Feed

- Last refresh {lastRefreshAt ? new Date(lastRefreshAt).toLocaleTimeString() : "-"} +
+
+ + job-events.log + refreshed {lastRefreshAt ? new Date(lastRefreshAt).toLocaleTimeString() : "-"}
-
- - - - - - - - - - - - - {newestFirst.map((event) => ( - - - - - - - - - ))} - -
IDEventJobAttemptWorkerCreated
{event.id}{eventTitle(event.type)} - - {event.job.slice(0, 8)} - - {event.attempt}{event.worker_id ?? "-"} - -
+
+ {newestFirst.map((event) => ( +
+ + $ + {eventTitle(event.type)} + + job:{event.job.slice(0, 8)} + + attempt:{event.attempt} + worker:{event.worker_id ?? "system"} + {event.message && {event.message}} +
+ ))} {loading && !newestFirst.length && } {!loading && !newestFirst.length && }
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 1811f7c..87b9db8 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1076,6 +1076,200 @@ label { padding-top: 12px; } +.terminal-panel { + background: #101316; + border: 1px solid #2d3439; + border-radius: 8px; + box-shadow: 0 22px 56px rgb(0 0 0 / 28%); + color: #d8e2e8; + overflow: hidden; +} + +.terminal-header { + align-items: center; + background: #171b1f; + border-bottom: 1px solid #2d3439; + display: grid; + gap: 12px; + grid-template-columns: auto minmax(0, 1fr) auto; + min-height: 44px; + padding: 9px 12px; +} + +.terminal-window-controls { + display: flex; + gap: 6px; +} + +.terminal-window-controls i { + border-radius: 999px; + display: block; + height: 11px; + width: 11px; +} + +.terminal-window-controls i:nth-child(1) { + background: #ff5f57; +} + +.terminal-window-controls i:nth-child(2) { + background: #ffbd2e; +} + +.terminal-window-controls i:nth-child(3) { + background: #28c840; +} + +.terminal-header code { + background: transparent; + border: 0; + color: #d8e2e8; + font-size: 13px; + overflow: hidden; + padding: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.terminal-header span { + color: #83919b; + font-size: 12px; + font-weight: 900; +} + +.terminal-feed { + display: grid; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + gap: 0; + max-height: calc(100vh - 230px); + min-height: 420px; + overflow: auto; + padding: 8px; +} + +.terminal-line { + align-items: baseline; + border-left: 3px solid transparent; + display: grid; + gap: 8px; + grid-template-columns: 80px 16px minmax(130px, 170px) minmax(110px, 150px) minmax(88px, 110px) minmax(150px, 220px) minmax(180px, 1fr); + line-height: 1.55; + min-width: 0; + padding: 7px 8px; +} + +.terminal-line:nth-child(odd) { + background: rgb(255 255 255 / 2.5%); +} + +.terminal-line:hover { + background: rgb(255 255 255 / 6%); +} + +.terminal-line.success { + border-left-color: #42d392; +} + +.terminal-line.danger { + border-left-color: #ff6b6b; +} + +.terminal-line.warning { + border-left-color: #f5b84b; +} + +.terminal-line.info { + border-left-color: #55c8ff; +} + +.terminal-line.accent { + border-left-color: #b388ff; +} + +.terminal-time, +.terminal-token, +.terminal-worker, +.terminal-message { + color: #8d9aa5; + font-size: 12px; + min-width: 0; + overflow-wrap: anywhere; +} + +.terminal-prompt { + color: #63717b; +} + +.terminal-event { + color: #d8e2e8; + font-size: 12px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; +} + +.terminal-line.success .terminal-event, +.terminal-line.success .terminal-prompt { + color: #42d392; +} + +.terminal-line.danger .terminal-event, +.terminal-line.danger .terminal-prompt { + color: #ff6b6b; +} + +.terminal-line.warning .terminal-event, +.terminal-line.warning .terminal-prompt { + color: #f5b84b; +} + +.terminal-line.info .terminal-event, +.terminal-line.info .terminal-prompt { + color: #55c8ff; +} + +.terminal-line.accent .terminal-event, +.terminal-line.accent .terminal-prompt { + color: #b388ff; +} + +.terminal-job { + color: #9ae6ff; + font-size: 12px; + font-weight: 900; + min-width: 0; + overflow-wrap: anywhere; + text-decoration: none; +} + +.terminal-job:hover { + color: #ffffff; +} + +.terminal-message { + color: #c0ccd4; +} + +.terminal-feed .empty-state { + background: rgb(255 255 255 / 4%); + border-color: #2d3439; + color: #8d9aa5; +} + +.terminal-panel .infinite-sentinel { + border-top: 1px solid #2d3439; + color: #8d9aa5; + padding: 12px; +} + +.terminal-panel .secondary-button { + background: #171b1f; + border-color: #2d3439; + color: #d8e2e8; +} + .filter-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -1488,6 +1682,10 @@ code { .filter-grid { grid-template-columns: 1fr 1fr; } + + .terminal-line { + grid-template-columns: 80px 16px minmax(120px, 1fr) minmax(100px, 1fr); + } } @media (max-width: 720px) { @@ -1585,4 +1783,28 @@ code { .dot-chart-row { grid-template-columns: 1fr; } + + .terminal-header { + grid-template-columns: auto minmax(0, 1fr); + } + + .terminal-header span { + grid-column: 2; + } + + .terminal-feed { + max-height: none; + min-height: 360px; + } + + .terminal-line { + grid-template-columns: 64px 14px minmax(0, 1fr); + } + + .terminal-job, + .terminal-token, + .terminal-worker, + .terminal-message { + grid-column: 3; + } }