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
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
/**
|
|
* The compact status block at the sidebar's foot (milestone 39): whether
|
|
* filtering is in force, how fast queries are arriving, and how long the
|
|
* process has been up. Three readings the operator glances at from any page,
|
|
* each from data the shell already holds — no request of its own.
|
|
*
|
|
* Queries/min derives from the Overview body the page in front of the reader
|
|
* is drawing, so it appears only while that page is open and its window has
|
|
* landed; on every other page the row is absent rather than stale.
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useMatch } from "@tanstack/react-router";
|
|
import * as stylex from "@stylexjs/stylex";
|
|
import { formatDuration, formatRate } from "@/lib/format";
|
|
import { overviewQuery, versionQuery } from "@/lib/queries";
|
|
import { useProtection } from "@/features/pause/protection";
|
|
import { DEFAULT_PERIOD } from "@/features/overview/period";
|
|
import { styles as shared } from "@/ui/styles";
|
|
import { colors, metrics } from "@/ui/tokens.stylex";
|
|
|
|
const styles = stylex.create({
|
|
block: {
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "0.4375rem",
|
|
marginInline: "1rem",
|
|
marginBottom: "0.75rem",
|
|
borderRadius: metrics.radius,
|
|
borderWidth: 1,
|
|
borderStyle: "solid",
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.surface,
|
|
paddingInline: "0.75rem",
|
|
paddingBlock: "0.875rem",
|
|
fontSize: "0.75rem",
|
|
lineHeight: "1rem",
|
|
color: colors.textMuted,
|
|
},
|
|
line: {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: "0.625rem",
|
|
margin: 0,
|
|
},
|
|
key: {
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: "0.4375rem",
|
|
},
|
|
value: {
|
|
fontWeight: 550,
|
|
color: colors.text,
|
|
},
|
|
dot: {
|
|
flexShrink: 0,
|
|
width: "7px",
|
|
height: "7px",
|
|
borderRadius: "999px",
|
|
},
|
|
dotActive: { backgroundColor: colors.chartGreen },
|
|
dotPaused: { backgroundColor: colors.warnBorderStrong },
|
|
dotUnavailable: { backgroundColor: colors.chartRed },
|
|
dotUnknown: { backgroundColor: colors.borderStrong },
|
|
/** The narrow header's rendering: the dot and the state alone, inline. */
|
|
inline: {
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: "0.4375rem",
|
|
fontSize: "0.75rem",
|
|
lineHeight: "1rem",
|
|
color: colors.textSecondary,
|
|
},
|
|
});
|
|
|
|
const PROTECTION_TEXT = {
|
|
active: "Active",
|
|
paused: "Paused",
|
|
unavailable: "Unavailable",
|
|
unknown: "Unknown",
|
|
} as const;
|
|
|
|
const DOT_STYLE = {
|
|
active: styles.dotActive,
|
|
paused: styles.dotPaused,
|
|
unavailable: styles.dotUnavailable,
|
|
unknown: styles.dotUnknown,
|
|
} as const;
|
|
|
|
/** The dot and the word, for the narrow header where there is room for nothing more. */
|
|
export function ProtectionDot() {
|
|
const { state } = useProtection();
|
|
return (
|
|
<span {...stylex.props(styles.inline)}>
|
|
<span aria-hidden="true" {...stylex.props(styles.dot, DOT_STYLE[state])} />
|
|
<span>Protection {PROTECTION_TEXT[state].toLowerCase()}</span>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Uptime as of now, not as of the fetch: the version answer is a minute old at
|
|
* most, so the seconds it reported are aged by the time since it landed. A
|
|
* restart shows within that minute. Ticks every second, because the first
|
|
* minute reads in seconds.
|
|
*/
|
|
function useUptimeSeconds(): number | null {
|
|
const version = useQuery(versionQuery());
|
|
const [now, setNow] = useState(() => Date.now());
|
|
useEffect(() => {
|
|
const timer = setInterval(() => setNow(Date.now()), 1000);
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
if (version.data === undefined) return null;
|
|
return version.data.uptime_seconds + Math.max(0, now - version.dataUpdatedAt) / 1000;
|
|
}
|
|
|
|
function useQueriesPerMinute(): number | null {
|
|
const match = useMatch({ from: "/shell/overview", shouldThrow: false });
|
|
const period = match?.search.period ?? DEFAULT_PERIOD;
|
|
// Reads the body the page fetched; never fetches one of its own. A failed
|
|
// refetch leaves the last body in the cache, and the page is then showing
|
|
// its error, not that body: the row goes with it.
|
|
const overview = useQuery({ ...overviewQuery(period, match?.search.client), enabled: false });
|
|
if (match === undefined || overview.isError || overview.data === undefined) return null;
|
|
if (overview.data.period !== period) return null;
|
|
const minutes = (overview.data.until - overview.data.since) / 60;
|
|
return minutes > 0 ? overview.data.totals.queries / minutes : null;
|
|
}
|
|
|
|
export default function SidebarStatus() {
|
|
const { state } = useProtection();
|
|
const rate = useQueriesPerMinute();
|
|
const uptime = useUptimeSeconds();
|
|
return (
|
|
<dl aria-label="Status" {...stylex.props(styles.block)}>
|
|
<div {...stylex.props(styles.line)}>
|
|
<dt {...stylex.props(styles.key)}>
|
|
<span aria-hidden="true" {...stylex.props(styles.dot, DOT_STYLE[state])} />
|
|
Protection
|
|
</dt>
|
|
<dd {...stylex.props(styles.value)}>{PROTECTION_TEXT[state]}</dd>
|
|
</div>
|
|
{rate !== null && (
|
|
<div {...stylex.props(styles.line)}>
|
|
<dt>Queries/min</dt>
|
|
<dd {...stylex.props(styles.value, shared.tabularNums)}>{formatRate(rate)}</dd>
|
|
</div>
|
|
)}
|
|
<div {...stylex.props(styles.line)}>
|
|
<dt>Uptime</dt>
|
|
<dd {...stylex.props(styles.value, shared.tabularNums)}>
|
|
{uptime === null ? "—" : formatDuration(uptime)}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
);
|
|
}
|