diff --git a/web/src/features/live/LiveLogPage.test.tsx b/web/src/features/live/LiveLogPage.test.tsx index 485a667..cc0294f 100644 --- a/web/src/features/live/LiveLogPage.test.tsx +++ b/web/src/features/live/LiveLogPage.test.tsx @@ -50,8 +50,14 @@ test("streams rows, flags blocked ones, and freezes the display", () => { expect(screen.getByText("Blocked")).toBeTruthy(); expect(screen.getByText("blocklist:stevenblack")).toBeTruthy(); expect(screen.getByText("AAAA")).toBeTruthy(); + // StyleX compiles to opaque class names, so the check is structural: a blocked + // row carries every class a plain row does, plus the ones the flag adds. const blockedRow = screen.getByText("ads.example").closest("tr"); - expect(blockedRow?.className).toContain("bg-red-50"); + const plainRow = screen.getByText("ok.example").closest("tr"); + const blockedClasses = new Set(blockedRow?.className.split(" ")); + const plainClasses = plainRow?.className.split(" ") ?? []; + expect(plainClasses.every((name) => blockedClasses.has(name))).toBe(true); + expect(blockedClasses.size).toBeGreaterThan(plainClasses.length); const freeze = screen.getByRole("button", { name: "Freeze" }); fireEvent.click(freeze); diff --git a/web/src/features/live/LiveLogPage.tsx b/web/src/features/live/LiveLogPage.tsx index 4c0598b..e1d0b7b 100644 --- a/web/src/features/live/LiveLogPage.tsx +++ b/web/src/features/live/LiveLogPage.tsx @@ -1,20 +1,164 @@ +import * as stylex from "@stylexjs/stylex"; import { QueryCells, QueryTableHead } from "@/features/queries/QueryLogPage"; import { RING_CAPACITY } from "./ringBuffer"; import { useLiveQueries, type EventSourceFactory, type StreamStatus } from "./useLiveQueries"; -import { buttonClass, retryButtonClass, tableWrapClass } from "@/ui/classes"; +import { styles as shared } from "@/ui/styles"; +import { colors } from "@/ui/tokens.stylex"; -const toolbarButtonClass = `${buttonClass} font-medium`; +const DARK = "@media (prefers-color-scheme: dark)"; + +const styles = stylex.create({ + toolbar: { + display: "flex", + flexWrap: "wrap", + alignItems: "center", + gap: "0.75rem", + }, + heading: { + fontSize: "1.5rem", + lineHeight: "2rem", + fontWeight: 600, + }, + toolbarButton: { + fontWeight: 500, + }, + pill: { + borderRadius: "9999px", + paddingInline: "0.625rem", + paddingBlock: "0.125rem", + fontSize: "0.75rem", + lineHeight: "1rem", + fontWeight: 500, + }, + /** Four stream states need four tints; only two of them map onto a token role. */ + pillConnecting: { + backgroundColor: { default: "oklch(96.7% 0.001 286.375)", [DARK]: "oklch(27.4% 0.006 286.033)" }, + color: { default: "oklch(37% 0.013 285.805)", [DARK]: "oklch(87.1% 0.006 286.286)" }, + }, + pillOpen: { + backgroundColor: { default: "oklch(96.2% 0.044 156.743)", [DARK]: "oklch(39.3% 0.095 152.535)" }, + color: { default: "oklch(44.8% 0.119 151.328)", [DARK]: "oklch(92.5% 0.084 155.995)" }, + }, + pillRetrying: { + backgroundColor: { default: "oklch(96.2% 0.059 95.617)", [DARK]: "oklch(41.4% 0.112 45.904)" }, + color: { default: "oklch(47.3% 0.137 46.201)", [DARK]: "oklch(92.4% 0.12 95.746)" }, + }, + pillCapped: { + backgroundColor: { default: "oklch(93.6% 0.032 17.717)", [DARK]: "oklch(39.6% 0.141 25.723)" }, + color: { default: "oklch(44.4% 0.177 26.899)", [DARK]: "oklch(88.5% 0.062 18.334)" }, + }, + note: { + marginTop: "0.5rem", + fontSize: "0.875rem", + lineHeight: "1.25rem", + color: colors.textMuted, + }, + /** Informational, neither a warning nor a failure, so the blue ramp stands alone. */ + resumed: { + marginTop: "0.75rem", + display: "flex", + alignItems: "center", + gap: "0.75rem", + borderRadius: "0.25rem", + borderWidth: 1, + borderStyle: "solid", + borderColor: { default: "oklch(80.9% 0.105 251.813)", [DARK]: "oklch(37.9% 0.146 265.522)" }, + backgroundColor: { default: "oklch(97% 0.014 254.604)", [DARK]: "oklch(28.2% 0.091 267.935)" }, + color: { default: "oklch(42.4% 0.199 265.638)", [DARK]: "oklch(88.2% 0.059 254.128)" }, + paddingInline: "0.75rem", + paddingBlock: "0.5rem", + fontSize: "0.875rem", + lineHeight: "1.25rem", + }, + dismiss: { + borderStyle: "none", + backgroundColor: "transparent", + padding: 0, + color: "inherit", + fontSize: "inherit", + fontWeight: 500, + textDecorationLine: "underline", + }, + failureNote: { + marginTop: "0.75rem", + fontSize: "0.875rem", + lineHeight: "1.25rem", + color: colors.dangerText, + }, + cappedBox: { + marginTop: "1rem", + borderRadius: "0.25rem", + borderWidth: 1, + borderStyle: "solid", + borderColor: colors.dangerBorder, + backgroundColor: colors.dangerSurface, + padding: "1rem", + }, + cappedHeading: { + fontWeight: 600, + color: colors.dangerText, + }, + cappedDetail: { + marginTop: "0.25rem", + fontSize: "0.875rem", + lineHeight: "1.25rem", + color: colors.dangerText, + }, + empty: { + marginTop: "1.5rem", + color: colors.textMuted, + }, + tableWrap: { + marginTop: "1rem", + overflowX: "auto", + borderRadius: "0.25rem", + borderWidth: 1, + borderStyle: "solid", + borderColor: colors.border, + }, + table: { + width: "100%", + fontSize: "0.875rem", + lineHeight: "1.25rem", + }, + /** `divide-y`: a hairline between rows, so the first row carries none. */ + row: { + borderTopWidth: { default: 1, ":first-child": 0 }, + borderTopStyle: "solid", + borderTopColor: colors.border, + }, + rowBlocked: { + backgroundColor: { + default: "oklch(97.1% 0.013 17.38)", + [DARK]: "oklch(25.8% 0.092 26.042 / 0.4)", + }, + }, + footnote: { + marginTop: "0.75rem", + fontSize: "0.875rem", + lineHeight: "1.25rem", + color: colors.textMuted, + }, +}); + +const PILL_LABELS: Record = { + connecting: "Connecting…", + open: "Live", + retrying: "Reconnecting…", + capped: "Disconnected", +}; + +function pillStyle(status: StreamStatus) { + if (status === "open") return styles.pillOpen; + if (status === "retrying") return styles.pillRetrying; + if (status === "capped") return styles.pillCapped; + return styles.pillConnecting; +} function StatusPill({ status }: { status: StreamStatus }) { - const styles: Record = { - connecting: ["Connecting…", "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"], - open: ["Live", "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"], - retrying: ["Reconnecting…", "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200"], - capped: ["Disconnected", "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"], - }; - const [label, tone] = styles[status]; + const label = PILL_LABELS[status]; return ( - + {label} ); @@ -27,58 +171,56 @@ export default function LiveLogPage({ createEventSource }: { createEventSource?: return (
-
-

Live

+
+

Live

{live.frozen && ( -

+

Display frozen — new queries keep buffering ({live.liveCount} in buffer, newest {RING_CAPACITY}{" "} kept).

)} {live.missed !== null && ( -
+
Stream resumed —{" "} {live.missed === 0 ? "no queries missed" : `${live.missed} missed queries recovered`}. -
)} {live.resyncFailed && ( -

+

Stream resumed, but re-syncing the gap failed — some queries may be missing here.

)} {live.status === "capped" && ( -
-

Live stream unavailable

-

+

+

Live stream unavailable

+

The connection failed repeatedly — possibly too many live viewers (the server caps streams per address), or the server is unreachable.

-
@@ -86,25 +228,25 @@ export default function LiveLogPage({ createEventSource }: { createEventSource?: {live.rows.length === 0 ? ( live.status !== "capped" && ( -

+

{live.status === "open" ? "Waiting for queries…" : "No queries received yet."}

) ) : ( <> -
- +
+
- + {live.rows.map((row) => ( - + ))}
-

+

Showing {live.rows.length} {live.rows.length === 1 ? "query" : "queries"} (newest first, last{" "} {RING_CAPACITY} kept).