milestone 32: task-shaped configuration, file mode as a rendering, config status api
Gates / frontend (push) Successful in 1m22s
Gates / test (push) Successful in 1m54s
Gates / test-aarch64 (push) Successful in 7m57s
Gates / package (push) Successful in 5m29s
Gates / container (push) Successful in 10s
CI / gates (push) Successful in 32m51s

This commit is contained in:
2026-08-22 22:42:50 +02:00
parent 025edbb093
commit 7e0df5fd94
89 changed files with 6101 additions and 3750 deletions
+79 -54
View File
@@ -25,6 +25,7 @@ import {
blocklistsQuery,
clientPrefixesQuery,
clientsQuery,
configStatusQuery,
diagnosticQuery,
diagnosticsInfiniteQuery,
forwardZonesQuery,
@@ -43,6 +44,13 @@ import {
upstreamsQuery,
} from "@/lib/queries";
import { DEFAULT_PERIOD, parsePeriod } from "@/features/overview/period";
import {
validateGroupId,
validateProtectionSearch,
validateResolutionSearch,
type ProtectionSearch,
type ResolutionSearch,
} from "@/features/configuration/search";
import type { Period } from "@/lib/types";
import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
@@ -253,6 +261,11 @@ const activityTestRoute = createRoute({
const clientsRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/clients",
// `group` is the filter Protection's client count links into. Validated here
// so a hand-typed id cannot reach the page as anything but a row id.
validateSearch: (search: Record<string, unknown>): { group?: number } => ({
group: validateGroupId(search["group"]),
}),
loader: ({ context }) =>
Promise.all([
context.queryClient.ensureQueryData(clientsQuery()),
@@ -262,51 +275,22 @@ const clientsRoute = createRoute({
component: lazyRouteComponent(() => import("@/features/clients/ClientsPage")),
});
const groupsRoute = createRoute({
/**
* One client, keyed by the row id the client API already identifies it with.
*
* No endpoint answers for a single client, so the list is the source. It is
* awaited, as the other detail routes await theirs: a cold deep link has
* nothing to render until it lands, and holding the navigation for one request
* beats a page that flashes "no such client" before the rows arrive. The
* rejection is swallowed for the same reason theirs are — the page states a
* missing row and a failed fetch itself, where the whole-page error component
* would call both a request failure.
*/
const clientDetailRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/groups",
loader: ({ context }) =>
Promise.all([
context.queryClient.ensureQueryData(groupsQuery()),
context.queryClient.ensureQueryData(blocklistsQuery()),
]),
component: lazyRouteComponent(() => import("@/features/groups/GroupsPage")),
});
const blocklistsRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/blocklists",
loader: ({ context }) => context.queryClient.ensureQueryData(blocklistsQuery()),
component: lazyRouteComponent(() => import("@/features/blocklists/BlocklistsPage")),
});
const rulesRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/rules",
loader: ({ context }) =>
Promise.all([
context.queryClient.ensureQueryData(rulesQuery()),
context.queryClient.ensureQueryData(groupsQuery()),
]),
component: lazyRouteComponent(() => import("@/features/rules/RulesPage")),
});
const localDnsRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/local-dns",
loader: ({ context }) =>
Promise.all([
context.queryClient.ensureQueryData(localRecordsQuery()),
context.queryClient.ensureQueryData(forwardZonesQuery()),
]),
component: lazyRouteComponent(() => import("@/features/local/LocalDnsPage")),
});
const upstreamsRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/upstreams",
loader: ({ context }) => context.queryClient.ensureQueryData(upstreamsQuery()),
component: lazyRouteComponent(() => import("@/features/upstreams/UpstreamsPage")),
path: "/clients/$id",
loader: ({ context }) => context.queryClient.ensureQueryData(clientsQuery()).catch(() => undefined),
component: lazyRouteComponent(() => import("@/features/clients/ClientDetailPage")),
});
/**
@@ -366,11 +350,54 @@ const diagnosticDetailRoute = createRoute({
component: lazyRouteComponent(() => import("@/features/diagnostics/DiagnosticDetailPage")),
});
const settingsRoute = createRoute({
/**
* The three task-shaped configuration pages. There is no `/configuration`
* landing route: a bare `/configuration` is not a question anyone has, and a
* route that only redirects is a second name for a page.
*
* Every loader here is started and awaited nowhere, the pattern the rest of the
* app uses: each panel owns its loading and error surface, so awaiting would
* trade that for one blocking navigation on the slowest request. The rejections
* are caught only to keep them from going unhandled.
*
* `configStatusQuery` starts with all of them. Every configuration page renders
* nothing — neither form nor definition list — until it answers, so it is on the
* critical path of all three.
*/
function startConfiguration(queryClient: QueryClient, queries: readonly unknown[]): void {
const start = (promise: Promise<unknown>) => void promise.catch(() => {});
start(queryClient.ensureQueryData(configStatusQuery()));
for (const options of queries) {
start(queryClient.ensureQueryData(options as Parameters<QueryClient["ensureQueryData"]>[0]));
}
}
const protectionRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/settings",
loader: ({ context }) => context.queryClient.ensureQueryData(settingsQuery()),
component: lazyRouteComponent(() => import("@/features/settings/SettingsPage")),
path: "/configuration/protection",
validateSearch: validateProtectionSearch,
loaderDeps: ({ search }): ProtectionSearch => ({ tab: search.tab, group: search.group }),
// Clients rides along because the selected group's detail counts them.
loader: ({ context }) =>
startConfiguration(context.queryClient, [groupsQuery(), blocklistsQuery(), rulesQuery(), clientsQuery()]),
component: lazyRouteComponent(() => import("@/features/configuration/ProtectionPage")),
});
const resolutionRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/configuration/resolution",
validateSearch: validateResolutionSearch,
loaderDeps: ({ search }): ResolutionSearch => ({ tab: search.tab }),
loader: ({ context }) =>
startConfiguration(context.queryClient, [upstreamsQuery(), localRecordsQuery(), forwardZonesQuery()]),
component: lazyRouteComponent(() => import("@/features/configuration/ResolutionPage")),
});
const systemRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/configuration/system",
loader: ({ context }) => startConfiguration(context.queryClient, [settingsQuery()]),
component: lazyRouteComponent(() => import("@/features/configuration/SystemPage")),
});
const routeTree = rootRoute.addChildren([
@@ -382,14 +409,12 @@ const routeTree = rootRoute.addChildren([
activityDetailRoute,
activityTestRoute,
clientsRoute,
groupsRoute,
blocklistsRoute,
rulesRoute,
localDnsRoute,
upstreamsRoute,
clientDetailRoute,
diagnosticsRoute,
diagnosticDetailRoute,
settingsRoute,
protectionRoute,
resolutionRoute,
systemRoute,
]),
]);