feat(ui): E31 S4a — rail rows are links, the channel page opens on unwatched, users and audit tidied

- Nav rail page rows are real links (/?page=<id>): a middle-click or
  Ctrl-click opens the page in a new tab; a plain click stays in-app.
- A fresh channel page shows the unwatched videos; a picked watch state
  is a remembered preference (channelShow).
- Users: the role, suspend and delete move into the row's one menu
  (the "…", right-click, Shift+F10); locked ones keep their reason.
- Audit log: back-to-back successful runs of one job fold into one row
  that opens to the earlier ones; a failed run reads in the danger colour.
- DESIGN.md decision log: the E31 picks.
This commit is contained in:
2026-10-01 22:51:38 +02:00
parent ab146211fa
commit 87d71e3762
23 changed files with 542 additions and 98 deletions
@@ -6,7 +6,7 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { Bell } from "lucide-react";
import { NavRailAction, NavRailItem } from "./NavRail";
@@ -24,10 +24,11 @@ describe("NavRailItem", () => {
showLabel={false}
active
badge={3}
href="/?page=notifications"
onClick={() => {}}
/>,
);
const row = screen.getByRole("button", { name: "Notifications (3)" });
const row = screen.getByRole("link", { name: "Notifications (3)" });
expect(row.getAttribute("aria-current")).toBe("page");
expect(row.className).toContain("bg-accent/15");
expect(row.textContent).not.toContain("Alerts");
@@ -40,10 +41,11 @@ describe("NavRailItem", () => {
label="Notifications"
shortLabel="Alerts"
showLabel
href="/?page=notifications"
onClick={() => {}}
/>,
);
const row = screen.getByRole("button", { name: "Alerts" });
const row = screen.getByRole("link", { name: "Alerts" });
expect(row.getAttribute("aria-current")).toBeNull();
});
@@ -57,10 +59,51 @@ describe("NavRailItem", () => {
shortLabel="Alerts"
showLabel
badge={20}
href="/?page=notifications"
onClick={() => {}}
/>,
);
expect(screen.getByRole("button", { name: "Alerts (20)" })).toBeTruthy();
expect(screen.getByRole("link", { name: "Alerts (20)" })).toBeTruthy();
});
// E31 (the owner): a middle-click on a rail row opens the page in a new tab. The row is a real link
// to the page, so the browser's own gestures work; only a plain left click navigates in-app.
it("is a link to its page, navigating in-app only on a plain left click", () => {
const onClick = vi.fn();
render(
<NavRailItem
icon={Bell}
label="Notifications"
shortLabel="Alerts"
showLabel={false}
href="/?page=notifications"
onClick={onClick}
/>,
);
const row = screen.getByRole("link", { name: "Notifications" });
expect(row.getAttribute("href")).toBe("/?page=notifications");
// Whether the row left the click to the browser, read after the app's own handler ran; then the
// click is stopped here, as jsdom cannot follow a link.
const leftToBrowser: boolean[] = [];
const record = (e: Event) => {
leftToBrowser.push(!e.defaultPrevented);
e.preventDefault();
};
window.addEventListener("click", record);
try {
fireEvent.click(row, { button: 0 });
expect(onClick).toHaveBeenCalledTimes(1);
for (const init of [
{ button: 1 },
{ button: 0, ctrlKey: true },
{ button: 0, metaKey: true },
])
fireEvent.click(row, init);
} finally {
window.removeEventListener("click", record);
}
expect(leftToBrowser).toEqual([false, true, true, true]);
expect(onClick).toHaveBeenCalledTimes(1);
});
});