Admin → Diagnostics reads the server's stored log: one list, newest first, where a line opens in place to its full record (time, source and logger, account, request and job ids, version, message, attributes, stack). A request or job id narrows the list to that request or job with every other filter off, shown as a removable chip. Filters: level group, source, account, event, period and a free-text search; all of them live in the URL (`l*` keys, which setPage clears like the Plex and Channels keys), so a filtered view is a link and Back restores it. "Live" re-reads the newest page every few seconds while the tab is visible; paging into older rows turns it off. Copy puts the loaded lines on the clipboard as JSON lines. The facet counts now honour every filter but their own, as the feed's do: a request pivot narrows them, and a picked level does not zero its siblings. A value in use stays offered even when the counts drop it. "Saját napló" and the console draw a line with one shared component, `LogLine`, whose phone layout gives the message its own full-width line (sharing the code's line left it a few characters). The level groups moved to lib/log.ts for both. The console's own reads are logged at DEBUG on success, like the healthcheck: at INFO its live tail filled the log with itself.
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import type { ReactNode } from "react";
|
|
|
|
import type { Level } from "../lib/log";
|
|
|
|
// One log line as both log viewers draw it — "Saját napló" (this tab's ring) and the admin
|
|
// diagnostics console (the server's stored log): time, level, event code, message, and the line opens
|
|
// in place to whatever detail its screen has. One component, so a level reads the same on both.
|
|
|
|
const LEVEL_TAG: Record<Level, { text: string; className: string }> = {
|
|
TRACE: { text: "TRACE", className: "text-muted opacity-80" },
|
|
DEBUG: { text: "DEBUG", className: "text-muted" },
|
|
INFO: { text: "INFO", className: "text-info" },
|
|
WARNING: { text: "WARN", className: "text-warn" },
|
|
ERROR: { text: "ERROR", className: "text-danger" },
|
|
CRITICAL: { text: "FATAL", className: "text-danger" },
|
|
};
|
|
|
|
/** `HH:MM:SS.mmm` in local time — a log is read to the millisecond. */
|
|
function logClock(ts: string): string {
|
|
const d = new Date(ts);
|
|
const pad = (n: number, w = 2) => String(n).padStart(w, "0");
|
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
|
|
}
|
|
|
|
export default function LogLine({
|
|
ts,
|
|
level,
|
|
event,
|
|
msg,
|
|
open,
|
|
onToggle,
|
|
title,
|
|
children,
|
|
}: {
|
|
ts: string;
|
|
level: Level;
|
|
event: string;
|
|
msg: string;
|
|
open: boolean;
|
|
onToggle: () => void;
|
|
/** The row button's tooltip — the admin console puts the full date there. */
|
|
title?: string;
|
|
/** The detail shown while the line is open. */
|
|
children: ReactNode;
|
|
}) {
|
|
const tag = LEVEL_TAG[level];
|
|
return (
|
|
<div className="border-b border-border/50 last:border-b-0">
|
|
<button
|
|
type="button"
|
|
aria-expanded={open}
|
|
onClick={onToggle}
|
|
title={title}
|
|
className={`grid w-full grid-cols-[6.5em_3.5em_minmax(0,1fr)] items-baseline sm:grid-cols-[6.5em_3.5em_minmax(0,10em)_minmax(0,1fr)] gap-x-2 px-3 py-1 text-left hover:bg-card ${
|
|
open ? "bg-accent/10" : ""
|
|
}`}
|
|
>
|
|
<span className="tabular-nums text-muted">{logClock(ts)}</span>
|
|
<span className={`font-semibold ${tag.className}`}>{tag.text}</span>
|
|
<span className="truncate">{event}</span>
|
|
{/* A phone has no room for a fourth column: the message takes a line of its own under the
|
|
code, full width — sharing the code's line left it a few characters. */}
|
|
<span className="col-span-3 truncate sm:col-span-1">{msg}</span>
|
|
</button>
|
|
{open && <div className="bg-accent/5 px-3 py-2 text-muted">{children}</div>}
|
|
</div>
|
|
);
|
|
}
|