chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
This commit is contained in:
@@ -221,7 +221,7 @@ export function SidebarContent({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const { user, instance, isAdmin } = useAuth();
|
||||
|
||||
// A group whose every item is admin-only disappears entirely for a member,
|
||||
// heading and rule included — an empty labelled section reads as something
|
||||
// heading and rule included - an empty labelled section reads as something
|
||||
// that failed to load.
|
||||
const visibleGroups = navGroups
|
||||
.map((group) => ({ ...group, items: group.items.filter((item) => !item.adminOnly || isAdmin) }))
|
||||
|
||||
@@ -34,7 +34,7 @@ function rolesAtOrBelow(role: Role): Role[] {
|
||||
* The whole API Keys page body, header included.
|
||||
*
|
||||
* It is a page rather than a card on /settings because any member may mint and
|
||||
* revoke their own keys — the API has never required owner or admin for that —
|
||||
* revoke their own keys - the API has never required owner or admin for that -
|
||||
* while /settings is owner|admin throughout. The instance-wide lifetime cap
|
||||
* stays on /settings, being policy rather than one person's credentials.
|
||||
*/
|
||||
@@ -138,7 +138,7 @@ export function ApiKeysPanel() {
|
||||
|
||||
function closeCreate() {
|
||||
// The plaintext is gone once this closes, so only invalidate having
|
||||
// shown it — closing before a result exists is a plain cancel.
|
||||
// shown it - closing before a result exists is a plain cancel.
|
||||
if (result) invalidate();
|
||||
setCreateOpen(false);
|
||||
resetCreateError();
|
||||
@@ -169,7 +169,7 @@ export function ApiKeysPanel() {
|
||||
</div>
|
||||
|
||||
{/* Owner and admin can see everyone's keys, so the scope of the list
|
||||
is a filter rather than a preference — the same pill treatment
|
||||
is a filter rather than a preference - the same pill treatment
|
||||
the vulnerabilities page uses for its state filter, so a person
|
||||
who has learned one has learned both. */}
|
||||
{isAdmin && (
|
||||
|
||||
@@ -131,7 +131,7 @@ export function CreateKeyDialog({
|
||||
{result ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded border border-warning/30 bg-warning/10 px-3 py-2 text-sm text-warning">
|
||||
This is the only time the key is shown. Copy it now — Vantage stores only a hash and cannot show it again.
|
||||
This is the only time the key is shown. Copy it now - Vantage stores only a hash and cannot show it again.
|
||||
</div>
|
||||
|
||||
{/* Below sm the button drops beneath the value: Copy has to
|
||||
@@ -198,7 +198,7 @@ export function CreateKeyDialog({
|
||||
className="space-y-5"
|
||||
>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<Field label="Name" hint="What will use this key — the CI pipeline, the script, the cluster.">
|
||||
<Field label="Name" hint="What will use this key - the CI pipeline, the script, the cluster.">
|
||||
<input required value={name} onChange={(e) => setName(e.target.value)} className={inputClass} />
|
||||
</Field>
|
||||
|
||||
@@ -238,7 +238,7 @@ export function CreateKeyDialog({
|
||||
const disabled = capDays > 0 && (o.days === null || o.days > capDays);
|
||||
return (
|
||||
<option key={o.label} value={o.days === null ? "never" : String(o.days)} disabled={disabled}>
|
||||
{o.days === null ? o.label : `${o.label} — ${expiryDate(o.days)}`}
|
||||
{o.days === null ? o.label : `${o.label} - ${expiryDate(o.days)}`}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function roleVariant(role: Role) {
|
||||
|
||||
/*
|
||||
* A grid rather than the shared <Table>: the identity column stacks four
|
||||
* things — name, hint, holder, role — and Td assumes one value per cell.
|
||||
* things - name, hint, holder, role - and Td assumes one value per cell.
|
||||
*
|
||||
* Below lg the grid collapses to a stacked record and each cell grows its own
|
||||
* label from data-label. A date sitting under a chip list with no headings is
|
||||
|
||||
@@ -3,7 +3,7 @@ import { keyLifetime } from "@/lib/keyLifetime";
|
||||
|
||||
/*
|
||||
* Four counts above the list, answering "is anything wrong here" before a
|
||||
* single row is read. All four are derived from the tokens already in hand —
|
||||
* single row is read. All four are derived from the tokens already in hand -
|
||||
* no second request, and no endpoint that could disagree with the list.
|
||||
*
|
||||
* The counts describe the list as filtered, so this sits below the My keys /
|
||||
|
||||
@@ -35,7 +35,7 @@ export function LifetimeBar({ token, capDays }: { token: ApiToken; capDays: numb
|
||||
<div className={`h-full ${FILL[state]}`} style={{ width: `${remainingPct}%` }} />
|
||||
</div>
|
||||
<span className={`font-mono text-xs tabular-nums ${TEXT[state]}`}>{label}</span>
|
||||
{outsidePolicy && <p className="text-xs text-warning">Outside the current policy — rotate when convenient.</p>}
|
||||
{outsidePolicy && <p className="text-xs text-warning">Outside the current policy - rotate when convenient.</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function summariseScopes(scopes: string[]): { resource: string; access: s
|
||||
}
|
||||
|
||||
/**
|
||||
* The chip splits in two — resource, then a tinted access half — so the read
|
||||
* The chip splits in two - resource, then a tinted access half - so the read
|
||||
* and write halves of a grant are told apart without reading either word.
|
||||
*
|
||||
* `wrap` is false in the ledger, where the list scrolls in its own track on a
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Nine bordered cards each holding two checkboxes made the grant look like nine
|
||||
* decisions. It is one decision with a shape, and a matrix is the shape.
|
||||
*
|
||||
* Resources come from GET /api/tokens/scopes and are never hardcoded here —
|
||||
* Resources come from GET /api/tokens/scopes and are never hardcoded here -
|
||||
* the endpoint is the source of truth and the vocabulary grows.
|
||||
*/
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export function TagRestriction({
|
||||
}
|
||||
|
||||
/** The same restriction rendered for a key that already exists. Unrestricted
|
||||
* renders nothing at all — most keys are, and a chip on every row for the
|
||||
* renders nothing at all - most keys are, and a chip on every row for the
|
||||
* common case is noise rather than information. */
|
||||
export function TagChips({ selector }: { selector?: Record<string, string> | null }) {
|
||||
const pairs = Object.entries(selector ?? {});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { formatDuration } from "@/components/monitors/MonitorVisuals";
|
||||
/*
|
||||
* The form is grouped the way the detail page reads it back: what is checked,
|
||||
* how often, and who hears about it. The section names are the same words on
|
||||
* both screens — someone editing "Fails after" should recognise the number
|
||||
* both screens - someone editing "Fails after" should recognise the number
|
||||
* they saw under "Fails after".
|
||||
*/
|
||||
|
||||
@@ -109,7 +109,7 @@ export function MonitorForm({
|
||||
|
||||
const { data: servers } = useQuery({ queryKey: ["servers"], queryFn: () => api.listServers() });
|
||||
/* The group is free text, so the existing groups are offered as suggestions
|
||||
rather than a fixed list — grouping is a label people invent, and a
|
||||
rather than a fixed list - grouping is a label people invent, and a
|
||||
select would mean adding one before it could be used. */
|
||||
const { data: allMonitors } = useQuery({ queryKey: ["monitors"], queryFn: () => api.listMonitors() });
|
||||
const knownGroups = Array.from(new Set((allMonitors ?? []).map((m) => m.group).filter((g): g is string => !!g))).sort();
|
||||
@@ -309,7 +309,7 @@ export function MonitorForm({
|
||||
|
||||
<p className="rounded-lg bg-well px-4 py-3 font-mono text-[11.5px] leading-relaxed text-text-secondary">
|
||||
Checked every {intervalSec} s from {runnerName}. Reported down after {retries}{" "}
|
||||
{retries === 1 ? "failure" : "consecutive failures"} — roughly {downAfter}.
|
||||
{retries === 1 ? "failure" : "consecutive failures"} - roughly {downAfter}.
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const chipTone: Record<DisplayStatus, string> = {
|
||||
paused: "text-text-tertiary border-border",
|
||||
};
|
||||
|
||||
/** Shape is the primary signal — round for up, triangle for down, and so on. */
|
||||
/** Shape is the primary signal - round for up, triangle for down, and so on. */
|
||||
function StatusMark({ status }: { status: DisplayStatus }) {
|
||||
if (status === "up") return <span className="block h-[7px] w-[7px] rounded-full bg-success" />;
|
||||
if (status === "down") return <span className="block h-0 w-0 border-x-[4px] border-b-[7px] border-x-transparent border-b-danger" />;
|
||||
@@ -72,12 +72,12 @@ export function avgLatency(rollups: Rollup[]): number | null {
|
||||
}
|
||||
|
||||
export function formatPct(pct: number | null): string {
|
||||
if (pct === null) return "—";
|
||||
if (pct === null) return "-";
|
||||
return pct >= 99.995 ? "100" : pct.toFixed(2);
|
||||
}
|
||||
|
||||
export function formatMs(ms: number | null): string {
|
||||
if (ms === null) return "—";
|
||||
if (ms === null) return "-";
|
||||
if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
|
||||
return `${Math.round(ms)} ms`;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ export interface Slot {
|
||||
checks: number;
|
||||
latency: number | null;
|
||||
/**
|
||||
* An incident overlapped this slot — the monitor was actually down for
|
||||
* An incident overlapped this slot - the monitor was actually down for
|
||||
* some of it. A failed check on its own is not this: `retries` exists so a
|
||||
* transient failure never opens an incident, and painting one red taught
|
||||
* operators the check had gone offline when nothing had. `pct` still
|
||||
@@ -129,8 +129,8 @@ export interface Slot {
|
||||
|
||||
/**
|
||||
* Bucket rollups into the last `hours` whole hours, ending with the hour in
|
||||
* progress. Rollups are sparse — an hour the scheduler never reached has no
|
||||
* document at all — so the slots are built from the clock and filled in, not
|
||||
* progress. Rollups are sparse - an hour the scheduler never reached has no
|
||||
* document at all - so the slots are built from the clock and filled in, not
|
||||
* read off the array. A missing hour has to read as a gap rather than
|
||||
* silently shifting the rest of the tape sideways.
|
||||
*/
|
||||
@@ -163,7 +163,7 @@ export function buildSlots(rollups: Rollup[], hours = 48): Slot[] {
|
||||
|
||||
/**
|
||||
* The same tape, bucketed from individual check results rather than hourly
|
||||
* rollups — this is what the sub-hour ranges are drawn from, because an hourly
|
||||
* rollups - this is what the sub-hour ranges are drawn from, because an hourly
|
||||
* rollup cannot say anything about a window shorter than an hour.
|
||||
*
|
||||
* Buckets are built from the clock like buildSlots, for the same reason: a
|
||||
@@ -200,7 +200,7 @@ export function buildSampleSlots(samples: MonitorSample[], windowMs: number, buc
|
||||
/**
|
||||
* Mark the slots an incident ran through. Incidents are the record of what the
|
||||
* monitor decided after `retries`, so this is the only thing that may paint a
|
||||
* slot as down — the raw check results say only that a check failed, which is
|
||||
* slot as down - the raw check results say only that a check failed, which is
|
||||
* a different and much more common event.
|
||||
*
|
||||
* An unresolved incident runs to now. Returns new slots; the input is left
|
||||
@@ -240,7 +240,7 @@ function slotHeight(s: Slot): number {
|
||||
return 55 + (s.pct - 80) * 2.2;
|
||||
}
|
||||
|
||||
/** One line of plain text for a slot — the tape's tooltip and the chart's
|
||||
/** One line of plain text for a slot - the tape's tooltip and the chart's
|
||||
* accessible name for a bar, so both read the same hour the same way. */
|
||||
export function slotLabel(s: Slot): string {
|
||||
const when = s.at.toLocaleString(undefined, { weekday: "short", hour: "2-digit", minute: "2-digit" });
|
||||
@@ -251,7 +251,7 @@ export function slotLabel(s: Slot): string {
|
||||
|
||||
/**
|
||||
* The tape: 48 hours of history as one strip. This is the spine of both
|
||||
* screens — a row shows what has been happening, not only what is true this
|
||||
* screens - a row shows what has been happening, not only what is true this
|
||||
* second.
|
||||
*/
|
||||
export function Tape({ slots, height = "h-9", live = true }: { slots: Slot[]; height?: string; live?: boolean }) {
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Button } from "@/components/ui";
|
||||
/*
|
||||
* Every action this page can take, behind one control.
|
||||
*
|
||||
* The header used to carry up to six buttons — a Connect per console protocol,
|
||||
* OS updates, Generate key, Remove — and which of them appeared depended on the
|
||||
* The header used to carry up to six buttons - a Connect per console protocol,
|
||||
* OS updates, Generate key, Remove - and which of them appeared depended on the
|
||||
* server, so the row an operator reached for moved between machines. One button
|
||||
* in one place is worth more than a shortcut that is sometimes there.
|
||||
*
|
||||
|
||||
@@ -15,7 +15,7 @@ import { clsx } from "clsx";
|
||||
*
|
||||
* Counts live on the labels because that is the only way an operator learns
|
||||
* there is something wrong on a tab they are not looking at. A count with a
|
||||
* tone is still labelled by its tab name, so tone is never the whole message —
|
||||
* tone is still labelled by its tab name, so tone is never the whole message -
|
||||
* and in the select, where tone cannot survive, the count still does.
|
||||
*/
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { api } from "@/lib/api";
|
||||
import { Button, useToast } from "@/components/ui";
|
||||
|
||||
/*
|
||||
* A tag is key:value, so the chip shows both halves with the key dimmed — the
|
||||
* A tag is key:value, so the chip shows both halves with the key dimmed - the
|
||||
* value is the part people scan for, the key is what disambiguates it.
|
||||
*/
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { formatBytes, relativeAge } from "./format";
|
||||
* part of the page that does not move when the tab changes.
|
||||
*
|
||||
* A meter is a hairline, not a bar: four of them across the top would otherwise
|
||||
* out-shout the hostname, and the number beside each is the value being read —
|
||||
* out-shout the hostname, and the number beside each is the value being read -
|
||||
* the meter only says how close to full it is.
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,7 @@ function Meter({ pct }: { pct: number }) {
|
||||
/** The phone strip: four readings on one line, no sub-lines and no byte
|
||||
* totals. A percentage is the reading; "70.9 GB / 100.0 GB · NTFS" is the
|
||||
* working, and the working belongs on the Overview tab, which is one scroll
|
||||
* away. Memory is shown as a percentage here for the same reason — it is the
|
||||
* away. Memory is shown as a percentage here for the same reason - it is the
|
||||
* only form that fits a quarter of a phone without wrapping. */
|
||||
function CompactVital({ label, value, pct }: { label: string; value: string; pct?: number }) {
|
||||
return (
|
||||
@@ -43,7 +43,7 @@ function CompactVital({ label, value, pct }: { label: string; value: string; pct
|
||||
|
||||
function Vital({ label, value, pct, sub }: { label: string; value: string; pct?: number; sub?: string }) {
|
||||
return (
|
||||
// sm and up only — the phone gets CompactVital instead.
|
||||
// sm and up only - the phone gets CompactVital instead.
|
||||
<div className="min-w-0 bg-surface px-4 py-3">
|
||||
<div className="flex items-baseline justify-between gap-3">
|
||||
<span className="truncate font-mono text-[0.62rem] uppercase tracking-[0.16em] text-text-secondary">{label}</span>
|
||||
@@ -56,7 +56,7 @@ function Vital({ label, value, pct, sub }: { label: string; value: string; pct?:
|
||||
}
|
||||
|
||||
/** The partition an operator means by "the disk": the root filesystem, or the
|
||||
* fullest one if there is no root — a Windows agent reports no `/`. */
|
||||
* fullest one if there is no root - a Windows agent reports no `/`. */
|
||||
function primaryPartition(inv: Inventory) {
|
||||
const parts = inv.partitions ?? [];
|
||||
if (parts.length === 0) return undefined;
|
||||
@@ -64,7 +64,7 @@ function primaryPartition(inv: Inventory) {
|
||||
}
|
||||
|
||||
/** "10.8 / 16.0 GB" rather than "10.8 GB / 16.0 GB" when both halves carry the
|
||||
* same unit — the repeated unit is what pushes the memory cell onto a second
|
||||
* same unit - the repeated unit is what pushes the memory cell onto a second
|
||||
* line on a phone, and it says nothing the right-hand half does not. */
|
||||
function pair(used: number, total: number) {
|
||||
const u = formatBytes(used);
|
||||
@@ -124,16 +124,16 @@ export function VitalsRail({ server, agentUpToDate }: { server: Server; agentUpT
|
||||
/>
|
||||
<Vital
|
||||
label={disk ? `Disk ${disk.mountpoint}` : "Disk"}
|
||||
value={disk ? `${diskPct.toFixed(0)}%` : "—"}
|
||||
value={disk ? `${diskPct.toFixed(0)}%` : "-"}
|
||||
pct={disk ? diskPct : undefined}
|
||||
sub={disk ? `${pair(disk.used_bytes, disk.total_bytes)}${disk.fstype ? ` · ${disk.fstype}` : ""}` : "no partitions reported"}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Vital label="CPU" value="—" sub="no metrics reported" />
|
||||
<Vital label="Memory" value="—" sub="no metrics reported" />
|
||||
<Vital label="Disk" value="—" sub="no metrics reported" />
|
||||
<Vital label="CPU" value="-" sub="no metrics reported" />
|
||||
<Vital label="Memory" value="-" sub="no metrics reported" />
|
||||
<Vital label="Disk" value="-" sub="no metrics reported" />
|
||||
</>
|
||||
)}
|
||||
<Vital label="Last seen" value={relativeAge(server.last_seen)} pct={server.status === "active" ? 100 : 0} sub={agentSub} />
|
||||
|
||||
@@ -178,8 +178,8 @@ export function MaintenanceTab({
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<span className="font-mono text-text-primary">{server.hostname}</span> and its history — keys,
|
||||
inventory, workflow runs and findings — are removed from Vantage.
|
||||
<span className="font-mono text-text-primary">{server.hostname}</span> and its history - keys,
|
||||
inventory, workflow runs and findings - are removed from Vantage.
|
||||
</p>
|
||||
<p>
|
||||
The agent stays installed on the machine and keeps trying to connect until you uninstall it
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { TabId } from "../ServerTabs";
|
||||
|
||||
/*
|
||||
* Overview answers one question: is anything wrong with this machine, and where
|
||||
* do I go about it. The detail lives on the other tabs — everything here either
|
||||
* do I go about it. The detail lives on the other tabs - everything here either
|
||||
* states a fact about the host or points at the tab that can act on it.
|
||||
*/
|
||||
|
||||
@@ -108,7 +108,7 @@ function AttentionPanel({ items, onGoTo }: { items: Attention[]; onGoTo: (tab: T
|
||||
{items.map((item) => (
|
||||
<li key={item.title} className="flex flex-wrap items-center justify-between gap-3 px-6 py-3.5">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
{/* The dot is recognition, never the message — the title says
|
||||
{/* The dot is recognition, never the message - the title says
|
||||
what is wrong on its own. */}
|
||||
<span className={clsx("mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full", item.tone === "danger" ? "bg-danger" : "bg-warning")} />
|
||||
<div className="min-w-0">
|
||||
|
||||
@@ -132,8 +132,8 @@ function ProviderRow({ p }: { p: AuthProvider }) {
|
||||
onSuccess: (_data, patch) => {
|
||||
setSecret("");
|
||||
invalidate();
|
||||
// The lockout guard answers 409 here — the last way in cannot be
|
||||
// switched off — so the outcome of this toggle is worth stating
|
||||
// The lockout guard answers 409 here - the last way in cannot be
|
||||
// switched off - so the outcome of this toggle is worth stating
|
||||
// rather than leaving to a checkbox that may have sprung back.
|
||||
toast.success(patch.client_secret ? `Client secret updated for ${p.name}.` : patch.enabled ? `${p.name} enabled.` : `${p.name} disabled.`);
|
||||
},
|
||||
|
||||
@@ -85,7 +85,7 @@ export function MembersCard() {
|
||||
});
|
||||
|
||||
// Removal failures are shown inside the confirm dialog that raised them, so
|
||||
// only the inline role change lands here — otherwise the same sentence
|
||||
// only the inline role change lands here - otherwise the same sentence
|
||||
// appears twice on screen.
|
||||
const actionError = roleError as Error | null;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const LABEL: Record<string, string> = {
|
||||
no_data: "Unknown",
|
||||
};
|
||||
|
||||
// A component under maintenance is drawn as maintenance, never as down — but
|
||||
// A component under maintenance is drawn as maintenance, never as down - but
|
||||
// its uptime figure (below) is left untouched. The window changes how a
|
||||
// component is drawn, never what the numbers say; see
|
||||
// applyMaintenanceRepaint in statussnapshot.go.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PublicDay } from "@/lib/api";
|
||||
|
||||
// The no-data tail (a component created less recently than 90 days ago) reads
|
||||
// as grey rather than as uptime — see uptimeFromDays in
|
||||
// as grey rather than as uptime - see uptimeFromDays in
|
||||
// server/internal/services/statussnapshot.go, which skips these days rather
|
||||
// than counting them as zero. Painting them the same as "up" here would undo
|
||||
// that on the one screen a reader actually looks at.
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function IncidentCard({ incident }: { incident: PublicIncident })
|
||||
<p className="mt-1 text-xs text-text-secondary">
|
||||
{new Date(incident.started_at).toLocaleString()}
|
||||
{incident.resolved_at
|
||||
? ` — resolved ${new Date(incident.resolved_at).toLocaleString()}`
|
||||
? ` - resolved ${new Date(incident.resolved_at).toLocaleString()}`
|
||||
: ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export function EmptyState({
|
||||
/*
|
||||
* `loading` matters rather than being decoration: the empty-state button is
|
||||
* usually the one that creates the first of something, and without it a
|
||||
* double click creates two. A link action takes neither — there is no
|
||||
* double click creates two. A link action takes neither - there is no
|
||||
* pending state to show for a navigation.
|
||||
*/
|
||||
action?:
|
||||
@@ -194,7 +194,7 @@ export function friendlyMessage(error: unknown): string {
|
||||
// The backend writes its 4xx messages for an operator and they are usually
|
||||
// the most specific thing available ("default steps cannot be edited",
|
||||
// "vulnerability scanning is not licensed"). Keep them; only replace the
|
||||
// ones that are a status code wearing a coat — "HTTP 409", or the bare
|
||||
// ones that are a status code wearing a coat - "HTTP 409", or the bare
|
||||
// reason phrase fetch() falls back to when the body was empty.
|
||||
const useful = raw && !/^HTTP \d{3}$/.test(raw) && !STATUS_TEXT.has(raw) ? raw : "";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Modal } from "./Modal";
|
||||
* the delete then fails, and it offers the same two buttons whether the action
|
||||
* removes one key or an entire secret group.
|
||||
*
|
||||
* `requireTyped` is for the cases with no undo — deleting a secret group, a
|
||||
* `requireTyped` is for the cases with no undo - deleting a secret group, a
|
||||
* step used by every workflow. Typing the name is not friction for its own
|
||||
* sake: it is what stops a muscle-memory Enter from destroying something whose
|
||||
* name the operator never actually read.
|
||||
@@ -43,7 +43,7 @@ export function ConfirmDialog({
|
||||
const [typed, setTyped] = useState("");
|
||||
const inputId = useId();
|
||||
|
||||
// A reopened dialog must not carry the previous attempt's typing — nor may
|
||||
// A reopened dialog must not carry the previous attempt's typing - nor may
|
||||
// a row reused for a different item stay armed with the name it matched
|
||||
// before, which is why `requireTyped` is a dependency and not just `open`.
|
||||
useEffect(() => {
|
||||
|
||||
@@ -13,8 +13,8 @@ const stack: symbol[] = [];
|
||||
|
||||
/*
|
||||
* The scroll lock is refcounted rather than saved and restored per dialog.
|
||||
* Per-instance save/restore breaks when the outer dialog unmounts first — which
|
||||
* a dialog that navigates away on success does — since the outer's cleanup then
|
||||
* Per-instance save/restore breaks when the outer dialog unmounts first - which
|
||||
* a dialog that navigates away on success does - since the outer's cleanup then
|
||||
* releases the lock while the inner one is still on screen.
|
||||
*/
|
||||
let lockCount = 0;
|
||||
@@ -27,7 +27,7 @@ let hidden: HTMLElement[] = [];
|
||||
* the aria-hidden sweep below skips it. Exported because the toast layer needs
|
||||
* the same exemption: a confirmation raised by a dialog is raised *before* that
|
||||
* dialog closes, so a toast rendered inside the app tree would be inserted into
|
||||
* a hidden subtree and never announced — and un-hiding a live region later does
|
||||
* a hidden subtree and never announced - and un-hiding a live region later does
|
||||
* not replay what it missed.
|
||||
*/
|
||||
export const DIALOG_LAYER_ATTR = "data-vantage-dialog";
|
||||
@@ -47,7 +47,7 @@ function lockScroll() {
|
||||
/*
|
||||
* aria-modal is a claim, not a mechanism. Portalled to the body, the
|
||||
* app tree is a plain sibling of the dialog, so a screen reader's
|
||||
* virtual cursor happily browses the page underneath — which is the
|
||||
* virtual cursor happily browses the page underneath - which is the
|
||||
* exact thing the overlay exists to prevent. Hiding the siblings is
|
||||
* what makes the claim true.
|
||||
*/
|
||||
@@ -105,7 +105,7 @@ export function Modal({
|
||||
|
||||
/*
|
||||
* onClose is an inline arrow at every call site, so its identity changes on
|
||||
* each render of the parent — and a parent re-renders on every react-query
|
||||
* each render of the parent - and a parent re-renders on every react-query
|
||||
* poll and every mutation state flip. Holding it in a ref is what keeps the
|
||||
* effect below keyed on `open` alone: depending on the handler tore the
|
||||
* whole thing down and rebuilt it mid-interaction, which yanked focus out
|
||||
@@ -154,7 +154,7 @@ export function Modal({
|
||||
const last = items[items.length - 1];
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
|
||||
// Focus can be outside the panel entirely — on <body> after a
|
||||
// Focus can be outside the panel entirely - on <body> after a
|
||||
// control unmounted, or on the page behind. Pull it back rather
|
||||
// than letting Tab continue out into content the overlay covers.
|
||||
if (!active || !panel.contains(active)) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DIALOG_LAYER_ATTR } from "./Modal";
|
||||
|
||||
/*
|
||||
* Mutations succeeded silently. Copying an install one-liner, generating a key,
|
||||
* restarting a container, rotating the ESO token — all of them changed
|
||||
* restarting a container, rotating the ESO token - all of them changed
|
||||
* something and said nothing, so the only way to know it worked was to watch
|
||||
* for the list to redraw. Failures were worse: each page wired its own
|
||||
* `onError: setError` into its own inline div, so an error raised by a modal
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ageHours, relativeTime } from "./SeverityVisuals";
|
||||
* A fleet scanned against a three-week-old database must say so where its
|
||||
* findings are read. Quietly reporting "0 open" against stale data is the same
|
||||
* class of lie as reporting zero findings for a distribution we hold no feed
|
||||
* for — it looks exactly like good news.
|
||||
* for - it looks exactly like good news.
|
||||
*/
|
||||
|
||||
// Past this the banner stops being informational and starts being a warning.
|
||||
|
||||
@@ -37,7 +37,7 @@ export function PackageRow({ group, serverName, canAct, onAccept, onUnaccept, on
|
||||
|
||||
return (
|
||||
// A row inside the page's one bordered container, not a card of its
|
||||
// own — the same stack idiom as the monitors and workflows lists.
|
||||
// own - the same stack idiom as the monitors and workflows lists.
|
||||
<div className="border-t border-border-soft first:border-t-0">
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function ServerVulnerabilities({ serverId }: { serverId: string }) {
|
||||
<p className="text-sm text-warning">
|
||||
{pkg.os.family} {pkg.os.version_id} has no advisory feed, so this server cannot be scanned.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-text-tertiary">This is not the same as having no vulnerabilities — it means we cannot answer the question for this distribution.</p>
|
||||
<p className="mt-1 text-xs text-text-tertiary">This is not the same as having no vulnerabilities - it means we cannot answer the question for this distribution.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -62,7 +62,7 @@ export function VulnAlertRulesCard() {
|
||||
<div>
|
||||
<h2 className="text-base font-bold tracking-[-0.02em] text-text-primary">Vulnerability alerts</h2>
|
||||
<p className="mt-0.5 text-sm text-text-secondary">
|
||||
One digest per rule per scan, summarising what newly opened — never one message per finding.
|
||||
One digest per rule per scan, summarising what newly opened - never one message per finding.
|
||||
</p>
|
||||
</div>
|
||||
{!adding && (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useMemo, useState } from "react";
|
||||
*
|
||||
* The panes are divs rather than <select multiple>. A native multi-select draws
|
||||
* its selected rows with the platform's own highlight colour, which cannot be
|
||||
* restyled reliably across browsers — on a dark ground it renders as a pale
|
||||
* restyled reliably across browsers - on a dark ground it renders as a pale
|
||||
* band that belongs to no palette. Rebuilding the widget is the only way to
|
||||
* keep it inside the token system.
|
||||
*/
|
||||
|
||||
@@ -145,7 +145,7 @@ export function EditWorkflowModal({ open, workflow, onSaved, onClose }: { open:
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{tagRows.length === 0 && <p className="text-xs text-text-secondary">No tag selector — only the named servers will run.</p>}
|
||||
{tagRows.length === 0 && <p className="text-xs text-text-secondary">No tag selector - only the named servers will run.</p>}
|
||||
</div>
|
||||
|
||||
{tagRows.length < 20 && (
|
||||
@@ -159,7 +159,7 @@ export function EditWorkflowModal({ open, workflow, onSaved, onClose }: { open:
|
||||
)}
|
||||
</div>
|
||||
{/* The schedule saves through its own endpoint, so it sits above
|
||||
the footer rather than under it — the footer's Save covers the
|
||||
the footer rather than under it - the footer's Save covers the
|
||||
name and targets only, and the two are labelled accordingly. */}
|
||||
<ScheduleCard workflow={workflow} />
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export function ScheduleCard({ workflow }: { workflow: Workflow }) {
|
||||
|
||||
{workflow.last_skipped && (
|
||||
<p className="rounded-lg border border-warning/30 bg-warning/10 px-4 py-3 text-xs text-warning">
|
||||
Skipped {new Date(workflow.last_skipped.due).toLocaleString()} —{" "}
|
||||
Skipped {new Date(workflow.last_skipped.due).toLocaleString()} -{" "}
|
||||
{workflow.last_skipped.reason === "already_running"
|
||||
? "previous run still active"
|
||||
: workflow.last_skipped.reason === "missed"
|
||||
|
||||
@@ -38,7 +38,7 @@ function group(list: Workload[]) {
|
||||
}
|
||||
|
||||
/* The agent has acted by the time the call returns, so these read as done
|
||||
* rather than as sent — unlike an OS update, which it only accepts. */
|
||||
* rather than as sent - unlike an OS update, which it only accepts. */
|
||||
const ACTION_PAST: Record<WorkloadAction, string> = {
|
||||
start: "Started",
|
||||
stop: "Stopped",
|
||||
@@ -57,7 +57,7 @@ export function WorkloadList({ serverId, canControl, isWindows }: { serverId: st
|
||||
|
||||
const refresh = useMutation({
|
||||
mutationFn: () => workloads.refresh(serverId),
|
||||
// The refresh returns no data — the agent reports through the normal
|
||||
// The refresh returns no data - the agent reports through the normal
|
||||
// path, so the only correct move is to refetch the stored document.
|
||||
onSuccess: () => setTimeout(() => qc.invalidateQueries({ queryKey: ["workloads", serverId] }), 1500),
|
||||
// Silent when it works: this fires on every panel open, and a toast
|
||||
@@ -71,8 +71,8 @@ export function WorkloadList({ serverId, canControl, isWindows }: { serverId: st
|
||||
qc.invalidateQueries({ queryKey: ["workloads", serverId] });
|
||||
toast.success(`${ACTION_PAST[action]} ${w.name}.`);
|
||||
},
|
||||
// Toasted rather than banner-ed. The 409 for a protected workload — the
|
||||
// agent refusing to stop itself — is the one an operator most needs to
|
||||
// Toasted rather than banner-ed. The 409 for a protected workload - the
|
||||
// agent refusing to stop itself - is the one an operator most needs to
|
||||
// read, and it arrives from a button that may be scrolled well away
|
||||
// from where the banner sat.
|
||||
onError: toast.error,
|
||||
@@ -132,8 +132,8 @@ export function WorkloadList({ serverId, canControl, isWindows }: { serverId: st
|
||||
{/* One wire field, two honest words for it: the agent
|
||||
reports Windows services under the same `unit` kind
|
||||
systemd units use, and only the UI knows which host
|
||||
this is. On Windows there is no "not in use" case —
|
||||
every Windows host has a service controller — so a
|
||||
this is. On Windows there is no "not in use" case -
|
||||
every Windows host has a service controller - so a
|
||||
failure is the only thing worth saying. */}
|
||||
{data.systemd_error ? (
|
||||
<p className="text-warning">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Badge, Button } from "@/components/ui";
|
||||
import type { Workload, WorkloadAction } from "@/lib/api";
|
||||
|
||||
/*
|
||||
* Container and unit states are kept in their own vocabularies on purpose — a
|
||||
* Container and unit states are kept in their own vocabularies on purpose - a
|
||||
* failed unit and an exited container mean different things. Colour never
|
||||
* carries the state on its own: Badge already pairs a dot with the word.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user