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
263 lines
7.3 KiB
TypeScript
263 lines
7.3 KiB
TypeScript
import { useState } from "react";
|
|
import { ArrowLeft } from "@phosphor-icons/react/dist/icons/ArrowLeft";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
|
import * as stylex from "@stylexjs/stylex";
|
|
import InlineError from "@/lib/InlineError";
|
|
import { formatCount, formatDuration, formatTime } from "@/lib/format";
|
|
import { diagnosticPurgeMutation, diagnosticQuery } from "@/lib/queries";
|
|
import ConfirmDialog from "@/ui/ConfirmDialog";
|
|
import Card from "@/ui/Card";
|
|
import { styles as shared } from "@/ui/styles";
|
|
import { colors, metrics } from "@/ui/tokens.stylex";
|
|
import SeverityBadge from "./SeverityBadge";
|
|
import { componentLabel, copyFor } from "./eventCopy";
|
|
|
|
const styles = stylex.create({
|
|
back: {
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: "0.25rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
color: colors.primaryOnSurface,
|
|
textDecorationLine: "none",
|
|
},
|
|
backIcon: {
|
|
display: "inline-flex",
|
|
},
|
|
headingRow: {
|
|
marginTop: "0.5rem",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
flexWrap: "wrap",
|
|
gap: "0.5rem",
|
|
},
|
|
heading: {
|
|
fontSize: "1.5rem",
|
|
lineHeight: "2rem",
|
|
fontWeight: 600,
|
|
textWrap: "balance",
|
|
},
|
|
purgeAction: {
|
|
marginInlineStart: "auto",
|
|
},
|
|
subject: {
|
|
marginTop: "0.25rem",
|
|
color: colors.textSecondary,
|
|
wordBreak: "break-all",
|
|
},
|
|
panel: {
|
|
marginTop: "1rem",
|
|
maxWidth: "48rem",
|
|
},
|
|
facts: {
|
|
display: "grid",
|
|
gap: "0.5rem 1rem",
|
|
gridTemplateColumns: {
|
|
default: "auto",
|
|
"@media (min-width: 640px)": "max-content 1fr",
|
|
},
|
|
margin: 0,
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
},
|
|
term: {
|
|
color: colors.textMuted,
|
|
},
|
|
value: {
|
|
margin: 0,
|
|
},
|
|
sectionHeading: {
|
|
marginTop: "1.5rem",
|
|
fontSize: "1.4rem",
|
|
lineHeight: 1.2,
|
|
fontWeight: 650,
|
|
letterSpacing: "-0.015em",
|
|
},
|
|
prose: {
|
|
marginTop: "0.5rem",
|
|
maxWidth: "48rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.5rem",
|
|
},
|
|
detail: {
|
|
marginTop: "0.5rem",
|
|
maxWidth: "48rem",
|
|
overflowX: "auto",
|
|
borderRadius: metrics.radius,
|
|
borderWidth: 1,
|
|
borderStyle: "solid",
|
|
borderColor: colors.border,
|
|
padding: "0.75rem",
|
|
fontSize: "0.8125rem",
|
|
lineHeight: "1.25rem",
|
|
whiteSpace: "pre-wrap",
|
|
wordBreak: "break-all",
|
|
},
|
|
links: {
|
|
marginTop: "1rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
},
|
|
link: {
|
|
color: colors.primaryOnSurface,
|
|
},
|
|
loading: {
|
|
// The heading row, the subject and the seven-row facts panel, which is
|
|
// what stands above the fold once the event lands.
|
|
minHeight: "20rem",
|
|
marginTop: "1rem",
|
|
color: colors.textMuted,
|
|
},
|
|
});
|
|
|
|
function BackLink() {
|
|
return (
|
|
<Link to="/diagnostics" {...stylex.props(styles.back, shared.focusRing)}>
|
|
<span aria-hidden="true" {...stylex.props(styles.backIcon)}>
|
|
<ArrowLeft size={12} />
|
|
</span>
|
|
All diagnostics
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export default function DiagnosticDetailPage() {
|
|
const { id } = useParams({ from: "/shell/diagnostics/$id" });
|
|
const eventId = Number(id);
|
|
const { data, error, isPending, refetch } = useQuery(diagnosticQuery(eventId));
|
|
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const purge = useMutation(diagnosticPurgeMutation(queryClient));
|
|
const [confirming, setConfirming] = useState(false);
|
|
|
|
function confirmPurge() {
|
|
setConfirming(false);
|
|
// The row this page is about is gone, so staying here would show the
|
|
// 404 the purge itself caused.
|
|
purge.mutate(eventId, { onSuccess: () => void navigate({ to: "/diagnostics" }) });
|
|
}
|
|
|
|
if (isPending) {
|
|
return (
|
|
<p {...stylex.props(styles.loading, shared.pulse)} role="status">
|
|
Loading event…
|
|
</p>
|
|
);
|
|
}
|
|
if (data === undefined) {
|
|
return (
|
|
<section>
|
|
<BackLink />
|
|
<InlineError error={error} onRetry={() => void refetch()} />
|
|
</section>
|
|
);
|
|
}
|
|
|
|
const copy = copyFor(data.code);
|
|
const resolvedAt = data.resolved_at;
|
|
const span = (resolvedAt ?? Math.floor(Date.now() / 1000)) - data.first_seen;
|
|
|
|
return (
|
|
<section>
|
|
<BackLink />
|
|
<div {...stylex.props(styles.headingRow)}>
|
|
<h1 {...stylex.props(styles.heading)}>{copy.title}</h1>
|
|
<SeverityBadge severity={data.severity} />
|
|
{/* Only history can be purged: an open episode is the current state of the box. */}
|
|
{resolvedAt !== null && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setConfirming(true)}
|
|
disabled={purge.isPending}
|
|
{...stylex.props(styles.purgeAction, shared.dangerLinkButton, shared.focusRing)}
|
|
>
|
|
Purge
|
|
</button>
|
|
)}
|
|
</div>
|
|
<p {...stylex.props(styles.subject)}>{data.subject}</p>
|
|
<InlineError error={purge.error} />
|
|
|
|
<Card
|
|
title="Details"
|
|
description="How long this has been failing, how often it recurred, and whether it has cleared."
|
|
style={styles.panel}
|
|
>
|
|
<dl {...stylex.props(styles.facts)}>
|
|
<dt {...stylex.props(styles.term)}>State</dt>
|
|
<dd {...stylex.props(styles.value)}>
|
|
{resolvedAt === null
|
|
? `Active for ${formatDuration(span)}`
|
|
: `Resolved after ${formatDuration(span)}`}
|
|
</dd>
|
|
<dt {...stylex.props(styles.term)}>First seen</dt>
|
|
<dd {...stylex.props(styles.value)}>{formatTime(data.first_seen)}</dd>
|
|
<dt {...stylex.props(styles.term)}>Last seen</dt>
|
|
<dd {...stylex.props(styles.value)}>{formatTime(data.last_seen)}</dd>
|
|
<dt {...stylex.props(styles.term)}>Occurrences</dt>
|
|
<dd {...stylex.props(styles.value, shared.tabularNums)}>{formatCount(data.occurrences)}</dd>
|
|
<dt {...stylex.props(styles.term)}>Resolved</dt>
|
|
<dd {...stylex.props(styles.value)}>
|
|
{data.resolved_at === null ? "Not yet — still failing" : formatTime(data.resolved_at)}
|
|
</dd>
|
|
<dt {...stylex.props(styles.term)}>Component</dt>
|
|
<dd {...stylex.props(styles.value)}>{componentLabel(data.component)}</dd>
|
|
<dt {...stylex.props(styles.term)}>Code</dt>
|
|
<dd {...stylex.props(styles.value, shared.mono)}>{data.code}</dd>
|
|
</dl>
|
|
</Card>
|
|
|
|
<h2 {...stylex.props(styles.sectionHeading)}>Impact</h2>
|
|
<p {...stylex.props(styles.prose)}>{copy.impact}</p>
|
|
|
|
<h2 {...stylex.props(styles.sectionHeading)}>What to do</h2>
|
|
<p {...stylex.props(styles.prose)}>{copy.remediation}</p>
|
|
|
|
<h2 {...stylex.props(styles.sectionHeading)}>Last error</h2>
|
|
{data.detail === "" ? (
|
|
<p {...stylex.props(styles.prose)}>The server recorded no error text for this event.</p>
|
|
) : (
|
|
<pre {...stylex.props(styles.detail, shared.mono)}>{data.detail}</pre>
|
|
)}
|
|
|
|
{copy.link !== undefined && (
|
|
<p {...stylex.props(styles.links)}>
|
|
{copy.link.to === "/configuration/protection" ? (
|
|
<Link
|
|
to="/configuration/protection"
|
|
search={{ tab: "sources" }}
|
|
{...stylex.props(styles.link, shared.focusRing)}
|
|
>
|
|
Go to {copy.link.label}
|
|
</Link>
|
|
) : copy.link.to === "/configuration/resolution" ? (
|
|
<Link
|
|
to="/configuration/resolution"
|
|
search={{ tab: "upstreams" }}
|
|
{...stylex.props(styles.link, shared.focusRing)}
|
|
>
|
|
Go to {copy.link.label}
|
|
</Link>
|
|
) : (
|
|
<Link to={copy.link.to} {...stylex.props(styles.link, shared.focusRing)}>
|
|
Go to {copy.link.label}
|
|
</Link>
|
|
)}
|
|
</p>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
isOpen={confirming}
|
|
title="Purge event"
|
|
message="Purge this resolved event? Its history is gone for good."
|
|
confirmLabel="Purge"
|
|
onConfirm={confirmPurge}
|
|
onCancel={() => setConfirming(false)}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|