chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
Chart Release / chart (push) Successful in 20s
Server Deploy / deploy (push) Failing after 1m52s

This commit is contained in:
2026-09-10 09:18:55 +00:00
parent b36a696d0e
commit 6ee203f5e9
172 changed files with 860 additions and 856 deletions
+1 -1
View File
@@ -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) }))
+3 -3
View File
@@ -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 && (
+3 -3
View File
@@ -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>
);
})}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 /
+1 -1
View File
@@ -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>
);
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
*/
+1 -1
View File
@@ -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 ?? {});
+3 -3
View File
@@ -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>
+10 -10
View File
@@ -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 }) {
+2 -2
View File
@@ -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.
*
+1 -1
View File
@@ -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.
*/
+1 -1
View File
@@ -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.
*/
+9 -9
View File
@@ -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
+2 -2
View File
@@ -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.`);
},
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -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.
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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 : "";
+2 -2
View File
@@ -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(() => {
+6 -6
View File
@@ -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)) {
+1 -1
View File
@@ -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 && (
+1 -1
View File
@@ -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} />
+1 -1
View File
@@ -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"
+6 -6
View File
@@ -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">
+1 -1
View File
@@ -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.
*/