milestone 30: overview as a dashboard, explicit health contract, period aggregations

This commit is contained in:
2026-08-22 16:45:15 +02:00
parent 0e83477d80
commit 623667e475
89 changed files with 7222 additions and 4239 deletions
@@ -1,4 +1,4 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import { cleanup, render, screen, waitFor, within } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
@@ -6,6 +6,7 @@ import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import type { QueryDetail } from "@/lib/types";
import { provenance } from "@/features/queries/provenanceFixture";
import { health } from "@/lib/healthFixture";
function detail(id: number, sections: Parameters<typeof provenance>[0] = {}): QueryDetail {
return { id, ...provenance(sections) };
@@ -16,6 +17,7 @@ let responses: Record<string, unknown>;
beforeEach(() => {
responses = {
"/api/version": { version: "0.0.0-test", git_commit: "0000000", zig_version: "0.16.0", uptime_seconds: 1 },
"/api/health": health(),
};
vi.stubGlobal(
"fetch",
@@ -310,3 +312,43 @@ test("a row retention has pruned explains the 404 and keeps the way back to the
domain: "gone",
});
});
/**
* The related-actions region of a query detail. Scoped on purpose: the sidebar
* carries a Pause of its own, and this is the one that answers "this query was
* blocked and should not have been".
*/
function related(): HTMLElement {
return screen.getByRole("region", { name: "Related" });
}
test("a blocked query's Related offers Pause; an allowed one has nothing to pause about", async () => {
responses["/api/queries/50"] = detail(50, {
policy: { action: "block", reason: "blocklist_domain", matched: "ads.example" },
route: { kind: "blocked", upstream: "" },
});
renderDetail(50);
await screen.findByRole("heading", { name: "example.com" });
await waitFor(() => expect(within(related()).getByRole("button", { name: "Pause" })).toBeTruthy());
cleanup();
responses["/api/queries/51"] = detail(51, { policy: { action: "allow", reason: "no_match", matched: "" } });
renderDetail(51);
await screen.findByRole("heading", { name: "example.com" });
expect(within(related()).queryByRole("button", { name: "Pause" })).toBeNull();
});
test("the Pause action stays away while protection is unavailable", async () => {
responses["/api/health"] = health({ protection: { state: "unavailable", until: null } });
responses["/api/queries/52"] = detail(52, {
policy: { action: "block", reason: "blocklist_domain", matched: "ads.example" },
route: { kind: "blocked", upstream: "" },
});
renderDetail(52);
await screen.findByRole("heading", { name: "example.com" });
await waitFor(() => expect(screen.getByText("Diagnostics around this query")).toBeTruthy());
expect(within(related()).queryByRole("button", { name: "Pause" })).toBeNull();
});
@@ -68,7 +68,15 @@ export default function ActivityDetailPage() {
<ProvenanceDetail
provenance={detail}
persistedId={detail.id}
relatedActions={<RelatedActions domain={domain} client={client} ts={time} origin={origin} />}
relatedActions={
<RelatedActions
domain={domain}
client={client}
ts={time}
origin={origin}
blocked={detail.policy.action === "block"}
/>
}
/>
</section>
);
@@ -9,6 +9,7 @@ import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import { health } from "@/lib/healthFixture";
import type { Client, Coverage, QueriesPage, QueryRow } from "@/lib/types";
import { queryRow } from "@/features/queries/provenanceFixture";
@@ -91,6 +92,8 @@ function stubFetch(handler: (url: string) => Response | Promise<Response>) {
fetchMock = vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/version") return Promise.resolve(json(VERSION));
// The shell reads health on every route for the Diagnostics nav badge.
if (url === "/api/health") return Promise.resolve(json(health()));
return Promise.resolve(handler(url));
});
vi.stubGlobal("fetch", fetchMock);
@@ -6,7 +6,7 @@
* which subtree the URL mounts, not by a prop a caller could pass.
*/
import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
import { AuthProvider } from "@/auth/store";
@@ -14,6 +14,7 @@ import { createQueryClient } from "@/lib/queryClient";
import { createAppRouter } from "@/routes";
import type { Client } from "@/lib/types";
import { provenance, queryRow } from "@/features/queries/provenanceFixture";
import { health } from "@/lib/healthFixture";
import { FakeEventSource } from "./fakeEventSource";
function client(ip: string, name: string, learnedName: string): Client {
@@ -50,6 +51,8 @@ function stubFetch(handler: (url: string) => Response | Promise<Response> = () =
const url = String(input);
if (url === "/api/version") return Promise.resolve(json(VERSION));
if (url === "/api/clients") return Promise.resolve(json({ clients: CLIENTS }));
// The shell reads health on every route for the Diagnostics nav badge.
if (url === "/api/health") return Promise.resolve(json(health()));
return Promise.resolve(handler(url));
});
vi.stubGlobal("fetch", fetchMock);
@@ -180,6 +183,8 @@ test("rows stream in as bare IPs while the client list is still loading", async
fetchMock = vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/version") return Promise.resolve(json(VERSION));
// The shell reads health on every route for the Diagnostics nav badge.
if (url === "/api/health") return Promise.resolve(json(health()));
return new Promise<Response>((resolve) => {
if (url !== "/api/clients") {
resolve(json({}));
@@ -397,3 +402,37 @@ test("leaving live closes the stream, and coming back opens exactly one fresh on
expect(sources).toHaveLength(2);
expect(sources[1]!.closed).toBe(false);
});
/**
* The related-actions region of a query detail. Scoped on purpose: the sidebar
* carries a Pause of its own, and this is the one that answers "this query was
* blocked and should not have been".
*/
function related(): HTMLElement {
return screen.getByRole("region", { name: "Related" });
}
test("a streamed blocked row carries the same Pause action as the persisted detail", async () => {
await openLive();
act(() =>
sources[0]!.emit(
"query",
frame(1000, "streamed.example", {
policy: { action: "block", reason: "blocklist_domain", matched: "streamed.example" },
route: { kind: "blocked", upstream: "" },
}),
),
);
fireEvent.click(screen.getByRole("button", { name: "streamed.example" }));
await waitFor(() => expect(within(related()).getByRole("button", { name: "Pause" })).toBeTruthy());
});
test("a streamed row that was allowed offers nothing to pause", async () => {
await openLive();
act(() => sources[0]!.emit("query", frame(1001, "allowed.example", { policy: { action: "allow" } })));
fireEvent.click(screen.getByRole("button", { name: "allowed.example" }));
await screen.findByRole("heading", { level: 1, name: "allowed.example" });
expect(within(related()).queryByRole("button", { name: "Pause" })).toBeNull();
});
@@ -268,6 +268,7 @@ function LiveDetail({ row, origin, onClose }: { row: StreamedRow; origin: Activi
client={summary.client_ip}
ts={summary.ts}
origin={origin}
blocked={row.event.policy.action === "block"}
/>
}
/>
@@ -274,8 +274,13 @@ export default function ProvenanceDetail({ provenance, persistedId, relatedActio
</Section>
</div>
<div {...stylex.props(styles.related)}>
<h2 {...stylex.props(styles.relatedHeading)}>Related</h2>
{/* A named region, because the Pause it may offer is not the only Pause
on screen: the sidebar carries one too, and the two answer different
questions. */}
<section aria-labelledby="related-actions" {...stylex.props(styles.related)}>
<h2 id="related-actions" {...stylex.props(styles.relatedHeading)}>
Related
</h2>
<p {...stylex.props(styles.relatedNote)}>
These read the current configuration, which may no longer be the one that decided this query.
</p>
@@ -294,7 +299,7 @@ export default function ProvenanceDetail({ provenance, persistedId, relatedActio
</p>
)}
<div {...stylex.props(styles.relatedList)}>{relatedActions}</div>
</div>
</section>
</>
);
}
@@ -11,6 +11,7 @@
import { Link } from "@tanstack/react-router";
import * as stylex from "@stylexjs/stylex";
import PauseControl from "@/features/pause/PauseControl";
import { styles as shared } from "@/ui/styles";
import { provenanceRelatedLink } from "./ProvenanceDetail";
import { diagnosticsBounds, relatedBounds } from "./relatedLinks";
@@ -23,9 +24,15 @@ interface Props {
ts: number;
/** The Activity search the reader came from; its bounds win over the defaults. */
origin: Pick<ActivitySearch, "since" | "until">;
/**
* This query was blocked. Pausing is a valid answer to a block the reader
* disagrees with, and to nothing else here — so the control appears for a
* block and not beside an allowed query it could not have caused.
*/
blocked: boolean;
}
export default function RelatedActions({ domain, client, ts, origin }: Props) {
export default function RelatedActions({ domain, client, ts, origin, blocked }: Props) {
const bounds = relatedBounds(ts, origin);
const window = diagnosticsBounds(ts);
return (
@@ -54,6 +61,7 @@ export default function RelatedActions({ domain, client, ts, origin }: Props) {
>
Diagnostics around this query
</Link>
{blocked && <PauseControl />}
</>
);
}