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
+2 -2
View File
@@ -24,7 +24,7 @@ function formatDate(dateStr: string) {
/*
* One treatment for every event, whatever it is: the subsystem as a chip, then
* what happened, in the same weight and size on every row. Colour carries the
* severity of the act and nothing else, and never carries it alone the
* severity of the act and nothing else, and never carries it alone - the
* sentence beside it says the same thing in words.
*/
function EventCell({ type }: { type: string }) {
@@ -131,7 +131,7 @@ export default function AuditPage() {
) : (
<EmptyState
title="No audit events recorded yet."
description="Every mutating action a key assigned, a workflow run, a member added is written here as it happens."
description="Every mutating action - a key assigned, a workflow run, a member added - is written here as it happens."
/>
)
}
+1 -1
View File
@@ -287,7 +287,7 @@ export default function KeyDetailPage() {
{/*
* Typed, like the server and monitor deletes. Deleting a key revokes it
* from every server at once, and for a generated key the stored private
* half goes with it there is no copy anywhere else, so this is the one
* half goes with it - there is no copy anywhere else, so this is the one
* delete on the fleet side that cannot be undone by re-uploading what
* the operator already has.
*/}
+7 -7
View File
@@ -99,7 +99,7 @@ function niceCeiling(ms: number): number {
/*
* The bar readout. A native title attribute arrives a second late, cannot show
* the latency alongside the uptime, and is invisible to keyboard users so the
* the latency alongside the uptime, and is invisible to keyboard users - so the
* hovered hour gets a real popover, anchored to its own bar.
*/
function SlotPopover({ slot, index, count }: { slot: Slot; index: number; count: number }) {
@@ -151,7 +151,7 @@ function History({ slots, note }: { slots: Slot[]; note?: string }) {
/*
* The trace breaks over hours with no checks rather than drawing a
* straight line across them a line implies data that was never
* straight line across them - a line implies data that was never
* collected.
*/
const segments: string[] = [];
@@ -334,7 +334,7 @@ export default function MonitorDetailPage() {
refetchInterval: 60_000,
});
/* Only fetched for the ranges that read it the 24h and 48h views are
/* Only fetched for the ranges that read it - the 24h and 48h views are
served by the rollups the page already holds. The 1h view refreshes on
the check interval's order rather than the rollup's: at one minute per
bar, a 60s poll is the difference between live and a bar behind. */
@@ -411,7 +411,7 @@ export default function MonitorDetailPage() {
const all: Rollup[] = rollups ?? [];
/* Marked with the incidents, so a bar is red only where the monitor was
actually down a failed check the retry policy absorbed stays amber. */
actually down - a failed check the retry policy absorbed stays amber. */
const slots = markIncidents(
range.source === "rollups"
? buildSlots(all, Math.round(range.minutes / 60))
@@ -474,7 +474,7 @@ export default function MonitorDetailPage() {
{/*
* Typed, like the server and secret-group deletes. Uptime history
* and incidents go with the monitor and there is nothing to restore
* them from, and this button sits inches from "Pause checks" the
* them from, and this button sits inches from "Pause checks" - the
* reversible action someone reaching for it usually wanted.
*/}
<ConfirmDialog
@@ -526,7 +526,7 @@ export default function MonitorDetailPage() {
<Figure label="Uptime 30d" value={formatPct(pct30d)} unit={pct30d !== null ? "%" : undefined} />
<Figure
label="Response now"
value={status === "down" ? "" : formatMs(latency)}
value={status === "down" ? "-" : formatMs(latency)}
tone={status === "down" ? "text-danger" : ""}
/>
<Figure
@@ -593,7 +593,7 @@ export default function MonitorDetailPage() {
<span className={`block h-2 w-2 rounded-full ${statusStripe[status]}`} />
<p className="text-[13px] text-text-secondary">
{status === "paused"
? "Paused no checks are running."
? "Paused - no checks are running."
: status === "down"
? `Failing for ${monitor.state.fails} consecutive ${monitor.state.fails === 1 ? "check" : "checks"}.`
: `Checked ${relativeTime(monitor.state.last_check_at)}.`}
+2 -2
View File
@@ -30,7 +30,7 @@ import {
* tens of checks, not thousands, and the alternative is a list endpoint that
* embeds history for every row whether or not anyone looks at it.
*
* Groups are display only a label on the monitor, nothing schedules or
* Groups are display only - a label on the monitor, nothing schedules or
* alerts by it. A fleet that never sets one sees the flat list it had before,
* with no "Ungrouped" heading over the whole page.
*/
@@ -179,7 +179,7 @@ function GroupHeader({
function MonitorRow({ monitor, rollups, incidents }: MonitorRowData) {
const status = displayStatus(monitor);
/* Incidents, not raw check results, decide which hours read as down see
/* Incidents, not raw check results, decide which hours read as down - see
markIncidents. */
const slots = markIncidents(buildSlots(rollups), incidents);
const pct = uptimePct(rollups.slice(-24));
+3 -3
View File
@@ -199,7 +199,7 @@ function SecretRow({ group, secret }: { group: string; secret: Secret }) {
title="Delete key"
confirmLabel="Delete key"
// Typed, like the group delete above it. A secret cannot be
// read back before it is deleted only revealed so there
// read back before it is deleted - only revealed - so there
// is no way to put it back afterwards from anything Vantage
// holds.
requireTyped={secret.key}
@@ -216,7 +216,7 @@ function SecretRow({ group, secret }: { group: string; secret: Secret }) {
<span className="font-mono text-text-primary">{secret.key}</span> will be removed from the{" "}
<span className="font-mono text-text-primary">{group}</span> group.
</p>
<p>Anything reading this key a workflow step, an API consumer, an external sync starts failing at its next run.</p>
<p>Anything reading this key - a workflow step, an API consumer, an external sync - starts failing at its next run.</p>
</>
}
/>
@@ -331,7 +331,7 @@ export default function SecretGroupPage() {
title="Delete secret group"
confirmLabel="Delete group"
// No undo, and the blast radius is every consumer of the group
// rather than one key so the name has to be typed.
// rather than one key - so the name has to be typed.
requireTyped={group}
loading={deleting}
error={deleteError ? friendlyMessage(deleteError) : null}
+1 -1
View File
@@ -343,7 +343,7 @@ export default function ServerConsolePage() {
</div>
)}
{/* The viewport is always mounted Guacamole attaches its display
{/* The viewport is always mounted - Guacamole attaches its display
element to it on connect, so it cannot be conditionally rendered.
Anything the operator needs to be told is layered over it
instead, which is what a bare black rectangle never did. */}
+3 -3
View File
@@ -23,8 +23,8 @@ import { MaintenanceTab } from "@/components/servers/tabs/MaintenanceTab";
/*
* One server, as a faceplate over five tabs.
*
* The page used to stack every panel it had agent updater, inventory,
* details, vulnerabilities, workloads, keys so the answer to "is this machine
* The page used to stack every panel it had - agent updater, inventory,
* details, vulnerabilities, workloads, keys - so the answer to "is this machine
* healthy" was several screens below the answer to "which agent build is on
* it". Identity, status and the four live readings now stay pinned; everything
* else is a tab, and the tab labels carry counts so a problem on a tab nobody
@@ -65,7 +65,7 @@ export default function ServerDetailPage() {
const activeTab: TabId = tabParam && TAB_IDS.includes(tabParam) ? tabParam : "overview";
/** The tab lives in the URL so an alert, a bookmark or a browser Back can
* name one. replace, not push five tabs of history between two pages is
* name one. replace, not push - five tabs of history between two pages is
* a Back button that does not go back. */
function selectTab(tab: TabId) {
const next = new URLSearchParams(searchParams.toString());
+2 -2
View File
@@ -73,7 +73,7 @@ const DOT_SHORT: Record<DotStatus, string> = {
* The dot alone was the whole control: four meanings carried by hue, with the
* distinction living in a `title` a touch user never sees and a screen reader
* is not obliged to announce. This is the one rule the design system states
* outright state never reads by colour alone so the label is now part of
* outright - state never reads by colour alone - so the label is now part of
* the component rather than something each page remembers to add.
*/
function StatusDot({ status }: { status: DotStatus }) {
@@ -104,7 +104,7 @@ function formatLastSeen(dateStr: string): string {
}
// useSearchParams opts this page into client-side bailout, which the App
// Router only permits inside a Suspense boundary hence the wrapper at the
// Router only permits inside a Suspense boundary - hence the wrapper at the
// bottom of this file rather than a bare default export.
function ServersPageBody() {
const searchParams = useSearchParams();
@@ -90,7 +90,7 @@ function ChannelForm({ initial, onDone }: { initial?: NotificationChannel; onDon
className={inputClass}
type={field === "password" ? "password" : "text"}
value={unchanged ? "" : value}
placeholder={unchanged ? "unchanged type to replace" : undefined}
placeholder={unchanged ? "unchanged - type to replace" : undefined}
onChange={(e) => setConfig({ ...config, [field]: e.target.value })}
/>
</div>
@@ -182,7 +182,7 @@ function ChannelRow({ ch }: { ch: NotificationChannel }) {
{/*
* Not typed, unlike a server or a secret: a channel is a name and a URL
* and can be recreated in a minute. What it does need is any confirmation
* at all Delete sat inches from Test and Disable and fired on one
* at all - Delete sat inches from Test and Disable and fired on one
* click, silently detaching every monitor that alerted through it.
*/}
<ConfirmDialog
+3 -3
View File
@@ -229,7 +229,7 @@ export default function SettingsPage() {
if (!settings) return;
// Same in-progress form state the main Save button submits
// (handleSubmit below), not the stale loaded `settings`
// object otherwise an unsaved edit to the offline
// object - otherwise an unsaved edit to the offline
// threshold or retention days is silently reverted the
// moment this toggle is flipped.
save({ ...currentPayload(), local_login_enabled: v });
@@ -298,7 +298,7 @@ export default function SettingsPage() {
</SectionCard>
{/* The cap lives here rather than on /tokens because it is
instance policy, not one person's credentials which is
instance policy, not one person's credentials - which is
also what lets that page be reachable at every role. */}
<SectionCard
title="API keys"
@@ -307,7 +307,7 @@ export default function SettingsPage() {
>
<Field
label="Maximum API key lifetime (days)"
hint="0 means no cap, and keys may be created with no expiry. Changing this affects new keys only existing keys keep working and are flagged for rotation."
hint="0 means no cap, and keys may be created with no expiry. Changing this affects new keys only - existing keys keep working and are flagged for rotation."
>
<input type="number" min={0} value={apiTokenMaxDays} onChange={(e) => setApiTokenMaxDays(Number(e.target.value))} className={numberInputClass} />
</Field>
+9 -9
View File
@@ -27,7 +27,7 @@ import { relativeTime } from "@/components/monitors/MonitorVisuals";
/*
* Details and Components are edited as one local draft and saved together by
* the single "Save changes" button in the header, matching artboard 2 of the
* mockup. Incidents are their own timeline and mutate immediately opening
* mockup. Incidents are their own timeline and mutate immediately - opening
* one, posting an update or editing a maintenance window has no "unsaved"
* state to lose, so there is nothing to batch.
*/
@@ -82,7 +82,7 @@ function unnamedSectionIndex(draft: Draft): number {
}
/*
* A blank display_name does NOT fall back to the monitor's name on the server
* A blank display_name does NOT fall back to the monitor's name on the server -
* it publishes the raw monitor UUID, deliberately, because publishing an
* internal name has to be a decision rather than a default. So the editor
* refuses to save one instead of letting an operator add five monitors and
@@ -102,7 +102,7 @@ function unnamedComponent(draft: Draft): { section: number; entry: number } | nu
* An incident's affected components are the PAGE's components, not the fleet's
* monitors: naming a monitor the page never listed publishes a machine the page
* deliberately does not, which is the leak assembleSnapshot exists to prevent,
* reached from the authoring side. services.checkAffectedOnPages refuses it
* reached from the authoring side. services.checkAffectedOnPages refuses it -
* this is what stops an operator getting that far.
*
* It reads the SAVED page rather than the draft. A component added in the
@@ -110,7 +110,7 @@ function unnamedComponent(draft: Draft): { section: number; entry: number } | nu
* incident the server refuses.
*
* The label is the per-page display name, which is the name the reader will see
* the monitor's own name is internal and may differ.
* - the monitor's own name is internal and may differ.
*/
interface PageComponent {
monitorId: string;
@@ -185,7 +185,7 @@ function DetailsPanel({ draft, setDraft, pageId }: { draft: Draft; setDraft: (d:
<div>
<label className="mb-1.5 block text-sm font-medium text-text-secondary">Page address</label>
<input type="text" value={pageId} disabled className={`${inputClass} font-mono opacity-60`} />
<p className="mt-1 text-xs text-text-tertiary">Fixed once created the link is already out there.</p>
<p className="mt-1 text-xs text-text-tertiary">Fixed once created - the link is already out there.</p>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-text-secondary">Description</label>
@@ -326,7 +326,7 @@ function ComponentsPanel({
<h2 className="text-base font-bold tracking-[-0.02em] text-text-primary">Components</h2>
<p className="mt-0.5 text-sm text-text-secondary">
Monitors grouped for the public page. Grouping here is separate from the groups on Monitors.
Each component needs a public name monitor names are never published for you.
Each component needs a public name - monitor names are never published for you.
</p>
</div>
<Button
@@ -556,7 +556,7 @@ function IncidentFormModal({
className="h-4 w-4 accent-accent"
/>
<span className="text-sm text-text-primary">{c.label}</span>
{c.stale && <span className="text-xs text-warning">No longer on this page uncheck to save</span>}
{c.stale && <span className="text-xs text-warning">No longer on this page - uncheck to save</span>}
</label>
))}
</div>
@@ -694,7 +694,7 @@ function DeleteIncidentButton({ pageId, incident }: { pageId: string; incident:
page it was published to, along with its updates.
</p>
<p>
To leave the record standing but close it out, mark it resolved instead the public page
To leave the record standing but close it out, mark it resolved instead - the public page
files a resolved incident under its history.
</p>
</>
@@ -864,7 +864,7 @@ export default function StatusPageEditorPage() {
/*
* Delete is the only correction for a typo'd page address: the address is
* immutable by design, because it is a URL handed to customers. Typed, like
* the monitor and secret-group deletes, and for the same reason the page,
* the monitor and secret-group deletes, and for the same reason - the page,
* its components and its authored incidents go together and there is
* nothing to restore them from.
*/
+1 -1
View File
@@ -107,7 +107,7 @@ function CreateStatusPageModal({ onClose }: { onClose: () => void }) {
className="w-full rounded border border-border bg-surface-2 px-3 py-2 font-mono text-sm text-text-primary focus:border-accent/50 focus:outline-none focus:ring-1 focus:ring-accent/30"
/>
<p className="mt-1 text-xs text-text-tertiary">
{`Becomes ${publicUrl(pageId || "<address>")}. Fixed once created lowercase letters, numbers and hyphens, 3-40 characters.`}
{`Becomes ${publicUrl(pageId || "<address>")}. Fixed once created - lowercase letters, numbers and hyphens, 3-40 characters.`}
</p>
{touched && pageId.length > 0 && !idValid && <p className="mt-1 text-xs text-danger">Not a valid page address.</p>}
</div>
+1 -1
View File
@@ -191,7 +191,7 @@ export default function StepsPage() {
<Td label="Outputs">
<span className="flex flex-wrap gap-1">
{(s.declared_outputs ?? []).length === 0 ? (
<span className="text-text-tertiary"></span>
<span className="text-text-tertiary">-</span>
) : (
(s.declared_outputs ?? []).map((o) => (
<span key={o} className="rounded-sm border border-signal/35 px-1.5 py-0.5 font-mono text-[10px] text-signal">
+1 -1
View File
@@ -4,7 +4,7 @@ import { ApiKeysPanel } from "@/components/apikeys/ApiKeysPanel";
/**
* Reachable at every role, unlike /settings. Any member may mint and revoke
* their own API keys the API has never required owner or admin for that
* their own API keys - the API has never required owner or admin for that -
* and owner and admin additionally see every key in the instance.
*/
export default function ApiKeysPage() {
+1 -1
View File
@@ -216,7 +216,7 @@ export default function VulnerabilitiesPage() {
</p>
{/* Named explicitly, because "no findings" under a filter
the reader has forgotten setting reads as a clean
fleet the one claim this page must never make by
fleet - the one claim this page must never make by
accident. */}
<p className="mx-auto mt-2 max-w-[52ch] text-sm text-text-secondary">
{hasFix !== undefined
+1 -1
View File
@@ -349,7 +349,7 @@ export default function WorkflowBuilder() {
<span className="text-text-secondary">{v}</span>
</span>
))}
{Object.keys(targetTags).length === 0 && <p className="text-xs text-text-secondary">No tag selector only the named servers will run.</p>}
{Object.keys(targetTags).length === 0 && <p className="text-xs text-text-secondary">No tag selector - only the named servers will run.</p>}
</div>
<p className="mt-3 font-mono text-xs text-text-secondary" title={matched.map((s) => s.hostname).join("\n")}>
+2 -2
View File
@@ -93,8 +93,8 @@ export default function WorkloadsPage() {
<Badge variant="neutral">{h.workload.kind}</Badge>
</Td>
<Td>{h.workload.state}</Td>
<Td className="font-mono text-xs">{h.workload.image ?? ""}</Td>
<Td>{h.workload.stack ?? ""}</Td>
<Td className="font-mono text-xs">{h.workload.image ?? "-"}</Td>
<Td>{h.workload.stack ?? "-"}</Td>
</Tr>
))}
</Tbody>
+3 -3
View File
@@ -5,8 +5,8 @@
/* ==========================================================================
Vantage control plane design tokens.
These are site/app/globals.css's **dark** token values same names, same
colours so the marketing site, the HQ console and this app are one visual
These are site/app/globals.css's **dark** token values - same names, same
colours - so the marketing site, the HQ console and this app are one visual
system. Change them in all three apps in the same commit; nothing enforces
the match automatically.
@@ -47,7 +47,7 @@
site/ brightens the accent with a filter, which a Tailwind colour token
cannot do, so the two hovers are named values. --well is the floor the
ground sits on install one-liners, key blobs and run logs, the places
ground sits on - install one-liners, key blobs and run logs, the places
showing machine output rather than interface. */
--accent-hover-rgb: 127 178 240; /* #7fb2f0, the accent at site/'s hover brightness */
--down-hover-rgb: 200 90 69; /* #c85a45 */
+1 -1
View File
@@ -3,7 +3,7 @@ import { NextResponse } from "next/server";
// Liveness and readiness for the Next server itself. Deliberately at /healthz
// and not /api/healthz: next.config.ts rewrites the whole of /api to the Go
// server, so a probe there would report the backend's health instead of this
// process's and would keep passing while this pod was wedged.
// process's - and would keep passing while this pod was wedged.
//
// It answers without touching the backend on purpose. web is stateless; a
// backend outage must not take every web replica out of its Service as well,
+3 -3
View File
@@ -16,7 +16,7 @@ type FetchResult = { kind: "ok"; snapshot: StatusSnapshot } | { kind: "not-found
* next.config.ts): /public is routed to it by the reverse proxy, exactly as
* /api and /auth are for the browser. So the SSR fetch goes back through that
* proxy at <slug>.vantage.<tld>, which is a per-tenant address by
* construction and X-Forwarded-Host below is still what selects the tenant,
* construction - and X-Forwarded-Host below is still what selects the tenant,
* because the hop from this process cannot set Host.
*/
function apiBase(proto: string, host: string): string {
@@ -25,7 +25,7 @@ function apiBase(proto: string, host: string): string {
async function fetchSnapshot(base: string, host: string, forwardedFor: string, pageId: string): Promise<FetchResult> {
// The instance is resolved server-side from the visitor's host, so it has
// to be forwarded explicitly this is a server-to-server call and its own
// to be forwarded explicitly - this is a server-to-server call and its own
// Host names the Go service.
//
// It goes in X-Forwarded-Host and NOT in Host: `Host` is a forbidden header
@@ -36,7 +36,7 @@ async function fetchSnapshot(base: string, host: string, forwardedFor: string, p
// Likewise the visitor's own address. Without it the Go server sees a
// request from this pod with no XFF and rate limits every visitor of every
// page into one 120/min bucket tripped by exactly the traffic an outage
// page into one 120/min bucket - tripped by exactly the traffic an outage
// produces. Appending rather than replacing keeps the chain in front of us
// intact.
if (forwardedFor) outbound["X-Forwarded-For"] = forwardedFor;