admin: overview redesign, device scope, one formatting contract (milestone 39)
Gates / frontend (push) Successful in 1m57s
Gates / test (push) Successful in 2m34s
Gates / test-aarch64 (push) Successful in 8m9s
Gates / package (push) Successful in 7m14s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 18m17s
Release / guard (push) Successful in 33s
Gates / test-aarch64 (push) Successful in 7m22s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m35s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m16s
Gates / package (push) Successful in 44s
Release / publish (push) Successful in 10m4s
Gates / frontend (push) Successful in 1m57s
Gates / test (push) Successful in 2m34s
Gates / test-aarch64 (push) Successful in 8m9s
Gates / package (push) Successful in 7m14s
Gates / container (push) Successful in 17s
CI / gates (push) Successful in 18m17s
Release / guard (push) Successful in 33s
Gates / test-aarch64 (push) Successful in 7m22s
Gates / container (push) Successful in 11s
Release / gates (push) Successful in 10m35s
Gates / frontend (push) Successful in 2m8s
Gates / test (push) Successful in 2m16s
Gates / package (push) Successful in 44s
Release / publish (push) Successful in 10m4s
The Overview page takes the decided visual language (specs/ui-visual-redesign.md): four centred totals with their Activity links, a smoothed area chart of total and blocked queries with point hover and a tooltip centred beside the point, a stacked client chart in eight distinct hues plus one Other band that is always a series, and a card row with the cache hit rate, the query types as a single-hue ramp ring, and the upstream breakdown. The count axis grows its margin with the widest grouped tick and draws whole-number ticks only. GET /api/overview takes a client parameter; the scoped read uses idx_query_log_ts and the cache keeps scoped slots. The device selector beside the period selector is URL state, so a scoped view is a link, and the tile links carry the scope into Activity. The route reduces a pasted IPv6 scope to the RFC 5952 spelling the logger stores, mapped addresses included, and drops anything that is not an address. A failed device list says so under the selector with a retry. All measured quantities go through admin/src/lib/format.ts: grouped counts, two-decimal percentages, one-decimal rates, durations as the two largest nonzero units. Identifiers, configured values and preset labels render as written; the module header states that scope. A sweep test refuses toFixed, toLocaleString, Intl.NumberFormat and padStart anywhere else. Chrome: one 4px radius from the metrics constants, shared Card with a prominent title and a one-line description on every panel, the settings form sections on the same card with a floated legend, the sidebar grouped into Monitoring and System with a status block (protection, queries per minute on Overview, uptime), keyboard-focusable table scroll wrappers, and the accent darkened to 5.43:1 on its wash. Not built: the spec's ranked-list primitive, which has no consumer and no API rows. Codex reviewed sessions B to D over five rounds (thirty-three findings fixed, thirteen rejected as non-quantities); the owner skipped a sixth round. Claude-Session: https://claude.ai/code/session_01VTgx3a1zz1R78o4K55kkwR
This commit is contained in:
@@ -9,30 +9,31 @@ import { healthQuery, versionQuery } from "@/lib/queries";
|
||||
import PauseControl from "@/features/pause/PauseControl";
|
||||
import { diagnosticsBadge } from "./diagnosticsBadge";
|
||||
import ConfigStatusNotices from "./ConfigStatusNotices";
|
||||
import SidebarStatus, { ProtectionDot } from "./SidebarStatus";
|
||||
import AuthorityLine from "@/features/configuration/AuthorityLine";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors, metrics } from "@/ui/tokens.stylex";
|
||||
import { colors, layers, metrics } from "@/ui/tokens.stylex";
|
||||
|
||||
/** The one breakpoint the shell has: below it the sidebar becomes a drawer. */
|
||||
const WIDE = "@media (min-width: 768px)";
|
||||
const DARK = "@media (prefers-color-scheme: dark)";
|
||||
const WIDE = "@media (min-width: 801px)";
|
||||
|
||||
/**
|
||||
* The four operational surfaces, then the configuration group. Configuration
|
||||
* is a labelled group rather than a collapsible tree: three items do not earn
|
||||
* a disclosure, and a tree would hide the authority line under it.
|
||||
* Two labelled groups (ui-visual-redesign.md): MONITORING is what the resolver
|
||||
* is doing, SYSTEM is what it is told to do and how the box is faring. The
|
||||
* three configuration pages sit flat in SYSTEM — "Configuration" is not one
|
||||
* page — with the authority line under them.
|
||||
*/
|
||||
const NAV_ITEMS = [
|
||||
const MONITORING_ITEMS = [
|
||||
{ to: "/overview", label: "Overview" },
|
||||
{ to: "/activity", label: "Activity" },
|
||||
{ to: "/clients", label: "Clients" },
|
||||
{ to: "/diagnostics", label: "Diagnostics" },
|
||||
] as const;
|
||||
|
||||
const CONFIGURATION_ITEMS = [
|
||||
const SYSTEM_ITEMS = [
|
||||
{ to: "/configuration/protection", label: "Protection" },
|
||||
{ to: "/configuration/resolution", label: "Resolution" },
|
||||
{ to: "/configuration/system", label: "System" },
|
||||
{ to: "/diagnostics", label: "Diagnostics" },
|
||||
] as const;
|
||||
|
||||
const styles = stylex.create({
|
||||
@@ -45,7 +46,8 @@ const styles = stylex.create({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
borderRadius: "0.25rem",
|
||||
minHeight: metrics.hitTarget,
|
||||
borderRadius: metrics.radius,
|
||||
paddingInline: "0.75rem",
|
||||
paddingBlock: "0.375rem",
|
||||
// Carried by every item, active or not: a weight that changes on
|
||||
@@ -57,16 +59,17 @@ const styles = stylex.create({
|
||||
flex: 1,
|
||||
},
|
||||
navGroup: {
|
||||
marginTop: "1rem",
|
||||
marginTop: "1.125rem",
|
||||
},
|
||||
navGroupLabel: {
|
||||
display: "block",
|
||||
paddingInline: "0.75rem",
|
||||
paddingBlock: "0.25rem",
|
||||
fontSize: "0.75rem",
|
||||
paddingBottom: "0.375rem",
|
||||
fontSize: "0.6875rem",
|
||||
lineHeight: "1rem",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
letterSpacing: "0.09em",
|
||||
color: colors.textMuted,
|
||||
},
|
||||
/**
|
||||
@@ -90,12 +93,12 @@ const styles = stylex.create({
|
||||
/** The control sits with the footer, not in the scrolling nav list above it. */
|
||||
sidebarFooter: {
|
||||
paddingInline: "1rem",
|
||||
paddingTop: "0.75rem",
|
||||
paddingBlock: "0.75rem",
|
||||
},
|
||||
/** The current page reads as a filled chip, heavier than the hover fill. */
|
||||
/** The current page is a soft accent-tinted block, in the accent's own colour. */
|
||||
navActive: {
|
||||
backgroundColor: { default: "oklch(92% 0.004 286.32)", [DARK]: "oklch(27.4% 0.006 286.033)" },
|
||||
color: colors.text,
|
||||
backgroundColor: colors.primarySurface,
|
||||
color: colors.primaryOnSurface,
|
||||
},
|
||||
navIdle: {
|
||||
backgroundColor: { default: "transparent", ":hover": colors.surfaceHover },
|
||||
@@ -140,16 +143,18 @@ const styles = stylex.create({
|
||||
flexDirection: { default: null, [WIDE]: "column" },
|
||||
minHeight: { default: null, [WIDE]: 0 },
|
||||
overflow: { default: null, [WIDE]: "hidden" },
|
||||
backgroundColor: colors.surfaceRaised,
|
||||
borderRightWidth: 1,
|
||||
borderRightStyle: "solid",
|
||||
borderRightColor: colors.border,
|
||||
},
|
||||
brand: {
|
||||
paddingInline: "1rem",
|
||||
paddingBlock: "1rem",
|
||||
fontSize: "1.125rem",
|
||||
lineHeight: "1.75rem",
|
||||
fontWeight: 600,
|
||||
paddingBlock: "1.25rem",
|
||||
fontSize: "1rem",
|
||||
lineHeight: "1.5rem",
|
||||
fontWeight: 650,
|
||||
letterSpacing: "-0.015em",
|
||||
},
|
||||
sidebarNav: {
|
||||
flex: 1,
|
||||
@@ -163,11 +168,19 @@ const styles = stylex.create({
|
||||
minWidth: { default: null, [WIDE]: 0 },
|
||||
flexDirection: "column",
|
||||
},
|
||||
/** Narrow only: on WIDE the sidebar carries everything this row held. */
|
||||
/**
|
||||
* Narrow only: on WIDE the sidebar carries everything this row held. Sticky,
|
||||
* so the brand, the protection state and the menu stay in reach down a long
|
||||
* page.
|
||||
*/
|
||||
header: {
|
||||
display: { default: "flex", [WIDE]: "none" },
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: layers.tooltip,
|
||||
alignItems: "center",
|
||||
gap: "0.75rem",
|
||||
backgroundColor: colors.surfaceRaised,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomStyle: "solid",
|
||||
borderBottomColor: colors.border,
|
||||
@@ -178,9 +191,10 @@ const styles = stylex.create({
|
||||
display: { default: null, [WIDE]: "none" },
|
||||
},
|
||||
narrowBrand: {
|
||||
fontSize: "1.125rem",
|
||||
lineHeight: "1.75rem",
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
lineHeight: "1.5rem",
|
||||
fontWeight: 650,
|
||||
letterSpacing: "-0.015em",
|
||||
display: { default: null, [WIDE]: "none" },
|
||||
},
|
||||
headerRight: {
|
||||
@@ -245,31 +259,37 @@ function NavItem({
|
||||
function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const health = useQuery(healthQuery());
|
||||
const badge = diagnosticsBadge(health.data, health.isError);
|
||||
const groupHeadingId = useId();
|
||||
const monitoringId = useId();
|
||||
const systemId = useId();
|
||||
return (
|
||||
<>
|
||||
<ul {...stylex.props(styles.navList)}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavItem
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
label={item.label}
|
||||
onNavigate={onNavigate}
|
||||
badge={item.to === "/diagnostics" ? badge : undefined}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<div {...stylex.props(styles.navGroup)}>
|
||||
<div>
|
||||
{/* A span, not a heading: the sidebar label is not a section of the
|
||||
page, and an h2 here lands in the middle of the page's own outline. */}
|
||||
<span id={groupHeadingId} {...stylex.props(styles.navGroupLabel)}>
|
||||
Configuration
|
||||
<span id={monitoringId} {...stylex.props(styles.navGroupLabel)}>
|
||||
Monitoring
|
||||
</span>
|
||||
<ul aria-labelledby={groupHeadingId} {...stylex.props(styles.navList)}>
|
||||
{CONFIGURATION_ITEMS.map((item) => (
|
||||
<ul aria-labelledby={monitoringId} {...stylex.props(styles.navList)}>
|
||||
{MONITORING_ITEMS.map((item) => (
|
||||
<NavItem key={item.to} to={item.to} label={item.label} onNavigate={onNavigate} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div {...stylex.props(styles.navGroup)}>
|
||||
<span id={systemId} {...stylex.props(styles.navGroupLabel)}>
|
||||
System
|
||||
</span>
|
||||
<ul aria-labelledby={systemId} {...stylex.props(styles.navList)}>
|
||||
{SYSTEM_ITEMS.map((item) => (
|
||||
<NavItem
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
label={item.label}
|
||||
onNavigate={onNavigate}
|
||||
badge={item.to === "/diagnostics" ? badge : undefined}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<AuthorityLine />
|
||||
</div>
|
||||
</>
|
||||
@@ -279,7 +299,8 @@ function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
|
||||
/**
|
||||
* The sidebar's foot, in both renderings. Pause is a runtime action on the whole
|
||||
* resolver rather than on the page in front of the reader, which is why it sits
|
||||
* with the version label instead of in the header of every route.
|
||||
* with the status block and the version label instead of in the header of every
|
||||
* route.
|
||||
*/
|
||||
function SidebarFooter() {
|
||||
return (
|
||||
@@ -287,6 +308,7 @@ function SidebarFooter() {
|
||||
<div {...stylex.props(styles.sidebarFooter)}>
|
||||
<PauseControl />
|
||||
</div>
|
||||
<SidebarStatus />
|
||||
<VersionFooter />
|
||||
</>
|
||||
);
|
||||
@@ -353,6 +375,7 @@ export default function AppShell() {
|
||||
</Button>
|
||||
<span {...stylex.props(styles.narrowBrand)}>nxdns</span>
|
||||
<div {...stylex.props(styles.headerRight)}>
|
||||
<ProtectionDot />
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user