From 142593083f1d0ad0443ccadeedbad7daab2fd36f Mon Sep 17 00:00:00 2001
From: npeter83
Date: Sat, 19 Sep 2026 02:02:45 +0200
Subject: [PATCH] feat(diagnostics): walk the log lines from the keyboard; name
the console "Server log"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both log viewers put their lines in a NavGrid: j/k (and the arrows) step
from line to line, and Enter opens one exactly as a click does (the line's
button is its `data-nav-open`). A focused line is named by its code and
message.
The console's nav entry is "Szervernapló" / "Server log", its page title:
"Diagnosztika" was already the Settings tab that holds the user's own log,
and two places with one name read as one place.
---
.../src/components/AdminLogs.dom.test.tsx | 11 ++++
frontend/src/components/AdminLogs.tsx | 44 ++++++++-------
frontend/src/components/LogLine.tsx | 11 +++-
frontend/src/components/MyLogViewer.tsx | 56 ++++++++++---------
frontend/src/i18n/locales/en/header.json | 2 +-
frontend/src/i18n/locales/hu/header.json | 2 +-
6 files changed, 77 insertions(+), 49 deletions(-)
diff --git a/frontend/src/components/AdminLogs.dom.test.tsx b/frontend/src/components/AdminLogs.dom.test.tsx
index 773ec9e7..f4646334 100644
--- a/frontend/src/components/AdminLogs.dom.test.tsx
+++ b/frontend/src/components/AdminLogs.dom.test.tsx
@@ -142,6 +142,17 @@ describe("AdminLogs", () => {
).toBe("true");
});
+ it("the keyboard can walk the lines and open one (NavGrid items)", async () => {
+ logsMock.mockResolvedValue({ items: [row(2), row(1)], accounts: {}, next_cursor: null });
+ renderConsole();
+ await waitFor(() => expect(lines()).toHaveLength(2));
+
+ const items = lines().map((b) => b.parentElement!);
+ expect(items.map((el) => el.getAttribute("data-nav-index"))).toEqual(["0", "1"]);
+ expect(lines().every((b) => b.hasAttribute("data-nav-open"))).toBe(true);
+ expect(screen.getByTestId("admin-logs-lines").querySelector("[data-nav-list]")).toBeTruthy();
+ });
+
it("a failed read is an error with a retry, not an empty log", async () => {
logsMock.mockRejectedValue(new Error("502"));
renderConsole();
diff --git a/frontend/src/components/AdminLogs.tsx b/frontend/src/components/AdminLogs.tsx
index 9289de67..1d8b9992 100644
--- a/frontend/src/components/AdminLogs.tsx
+++ b/frontend/src/components/AdminLogs.tsx
@@ -24,13 +24,14 @@ import { qk } from "../lib/queryKeys";
import { useDebounced } from "../lib/useDebounced";
import { useUrlMirror } from "../lib/useUrlMirror";
import LogLine from "./LogLine";
+import NavGrid from "./ui/NavGrid";
import { Button } from "./ui/Button";
import FilterChipGroup from "./ui/FilterChipGroup";
import FilterSelect from "./ui/FilterSelect";
import { Section } from "./ui/form";
import { LoadingState, StateMessage } from "./ui/QueryState";
-// Admin → Diagnosztika: the server's stored log (`log_events`, E22.6). One list, newest first; a line
+// Admin → Szervernapló: the server's stored log (`log_events`, E22.6). One list, newest first; a line
// opens in place to its full record, and its request id / job id narrow the list to that request or
// job. Filters live in the URL, so a filtered view is a link. "Élő" re-reads the newest page every
// few seconds while the tab is visible — the newest page only: live is watching the top, so paging
@@ -287,25 +288,28 @@ export default function AdminLogs() {
{t("adminLogs.empty")}
) : (
- rows.map((row) => (
- setOpen(open === row.id ? null : row.id)}
- >
- pivot({ requestId, jobId: null })}
- onJob={(jobId) => pivot({ requestId: null, jobId })}
- />
-
- ))
+
+ {rows.map((row, index) => (
+ setOpen(open === row.id ? null : row.id)}
+ >
+ pivot({ requestId, jobId: null })}
+ onJob={(jobId) => pivot({ requestId: null, jobId })}
+ />
+
+ ))}
+
)}
diff --git a/frontend/src/components/LogLine.tsx b/frontend/src/components/LogLine.tsx
index 7dc63fed..bb9ea5cc 100644
--- a/frontend/src/components/LogLine.tsx
+++ b/frontend/src/components/LogLine.tsx
@@ -1,10 +1,12 @@
import type { ReactNode } from "react";
import type { Level } from "../lib/log";
+import { navItemAttrs } from "../lib/listNav";
// 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.
+// Inside a NavGrid the keyboard walks the lines (`j`/`k`) and Enter opens one, as a click does.
const LEVEL_TAG: Record = {
TRACE: { text: "TRACE", className: "text-muted opacity-80" },
@@ -30,6 +32,7 @@ export default function LogLine({
open,
onToggle,
title,
+ navIndex,
children,
}: {
ts: string;
@@ -40,14 +43,20 @@ export default function LogLine({
onToggle: () => void;
/** The row button's tooltip — the admin console puts the full date there. */
title?: string;
+ /** The line's place in its NavGrid. */
+ navIndex: number;
/** The detail shown while the line is open. */
children: ReactNode;
}) {
const tag = LEVEL_TAG[level];
return (
-