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;
+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.
*/
+4 -4
View File
@@ -180,7 +180,7 @@ export type ChannelType = "webhook" | "smtp" | "discord" | "slack" | "telegram";
* written verbatim.
*
* Mirrors `models.RedactedSecret` and `models.channelSecretKeys` in
* `server/internal/models/channel.go` change both in the same commit, the
* `server/internal/models/channel.go` - change both in the same commit, the
* same hazard as the mirrored token blocks.
*/
export const REDACTED_SECRET = "••••••••";
@@ -304,7 +304,7 @@ export type ApiToken = {
user_id: string;
user_email?: string;
/** Restricts the token to servers carrying every pair. Absent or empty is
* the whole fleet the asymmetry is deliberate, see services.MatchesSelector. */
* the whole fleet - the asymmetry is deliberate, see services.MatchesSelector. */
tag_selector?: Record<string, string> | null;
};
@@ -1147,7 +1147,7 @@ export interface VulnFinding {
cve_id: string;
package_name: string;
installed_version: string;
/** Absent means no vendor fix is published a real state, not missing data. */
/** Absent means no vendor fix is published - a real state, not missing data. */
fixed_in?: string;
severity: Severity;
cvss_score?: number;
@@ -1385,7 +1385,7 @@ export interface Skip {
// Public status page types
//
// These mirror services.StatusSnapshot and friends in
// server/internal/services/statussnapshot.go field for field that Go file
// server/internal/services/statussnapshot.go field for field - that Go file
// is the contract. They back the anonymous /status/[pageId] page, which is
// deliberately outside the (app) route group and never calls `request()`
// (no session, no auth). Task 10 adds the authoring types and api client
+3 -3
View File
@@ -4,7 +4,7 @@
* The page used to hold a map of eleven event types to labels and a second map
* of seven to colours. The server emits forty-seven. Everything unmapped fell
* through to the raw string, so one row read "Key Assigned" in green and the
* next "workflow.schedule_updated" in grey the same kind of fact in two
* next "workflow.schedule_updated" in grey - the same kind of fact in two
* different formats, which made the column look like it carried a meaning it
* did not.
*
@@ -50,7 +50,7 @@ const CATEGORY_LABELS = new Map(AUDIT_CATEGORIES.map((c) => [c.value, c.label]))
* deleting a workflow are the same weight of act.
*
* Stems match with or without their past tense, because the two spellings both
* occur `auth_provider.delete` beside `key.deleted`, `workload.stop` beside
* occur - `auth_provider.delete` beside `key.deleted`, `workload.stop` beside
* `workflow.schedule_disabled`. Matching only the past tense left half the
* destructive events drawn in the same grey as a settings change.
*
@@ -69,7 +69,7 @@ const TONE_RULES: [RegExp, AuditTone][] = [
/*
* Only where the derived text is wrong or reads clumsily. Anything absent is
* derived, which is the point this list should stay short.
* derived, which is the point - this list should stay short.
*/
const OVERRIDES: Record<string, string> = {
"key.generation_dispatched": "Key generation requested",
+2 -2
View File
@@ -48,7 +48,7 @@ const STATUS_TEXT: Record<number, string> = {
522: "The session timed out.",
523: "The session was closed.",
768: "The server rejected the connection request.",
769: "Authentication failed check the credentials or SSH key.",
769: "Authentication failed - check the credentials or SSH key.",
771: "Access to this connection was refused.",
776: "The session was closed after a period of inactivity.",
781: "The connection was closed because the client fell behind.",
@@ -110,7 +110,7 @@ export function openConsole(
// Guacamole.Client.State: 0 IDLE, 1 CONNECTING, 2 WAITING, 3 CONNECTED,
// 4 DISCONNECTING, 5 DISCONNECTED. CONNECTING and WAITING are one state to an
// operator both mean "not usable yet".
// operator - both mean "not usable yet".
client.onstatechange = (state: number) => {
if (closing) return;
if (state === 1 || state === 2) handlers.onState?.("connecting");
+1 -1
View File
@@ -30,7 +30,7 @@ function shortDate(d: Date) {
/**
* `capDays` is the instance's current `api_token_max_days`, 0 when unset. It is
* never applied retroactively a token issued before the cap tightened keeps
* never applied retroactively - a token issued before the cap tightened keeps
* working, and `outsidePolicy` is a prompt to rotate rather than a failure of
* any kind. Copy built on this flag must not imply the key has stopped working.
*/
+1 -1
View File
@@ -7,7 +7,7 @@ export const queryClient = new QueryClient({
queries: {
staleTime: 30_000,
retry: 1,
// Several pages poll on an interval the fleet list every 30s, run logs
// Several pages poll on an interval - the fleet list every 30s, run logs
// faster than that. A hidden tab was still doing all of it, so a console
// left open overnight in a background tab kept refetching the fleet and
// its inventory blobs until the session expired. The default here rather
+1 -1
View File
@@ -10,7 +10,7 @@ import { Server } from "@/lib/api";
* change in the same commit as it.
*
* It lives here rather than in a component because it had already been written
* twice once in the designer and once, wrongly, on the workflows list, where
* twice - once in the designer and once, wrongly, on the workflows list, where
* the count ignored tags entirely and a tag-only workflow reported zero targets.
*/
+2 -2
View File
@@ -4,7 +4,7 @@ import type { NextConfig } from "next";
* This app proxies nothing.
*
* /api, /auth, /install, /update and /public are the Go server's, and routing
* them there is the reverse proxy's job the same proxy that already
* them there is the reverse proxy's job - the same proxy that already
* terminates TLS in front of this process. Next used to rewrite them itself
* from an API_URL naming the control plane, which meant every deployment had
* two possible request paths for the same URL and one environment variable
@@ -23,7 +23,7 @@ const nextConfig: NextConfig = {
{
// The API keys page briefly lived at /api-keys, which every
// raw-prefix proxy in front of this app captures with its /api
// rule nginx's `location /api` matches /api-keys, so the
// rule - nginx's `location /api` matches /api-keys, so the
// request reached the Go server and 404'd. The page is at
// /tokens now precisely because that cannot happen to it.
source: "/api-keys",
+6 -6
View File
@@ -1,14 +1,14 @@
import type { Config } from "tailwindcss";
/*
* Tokens are shared with site/ and adminsite/ same names, same values, in
* Tokens are shared with site/ and adminsite/ - same names, same values, in
* app/globals.css. Nothing here may hold a hex value: if a colour needs to
* change it changes in globals.css, in all three apps, in one commit.
*
* The colours keep this app's own names rather than site/'s. site/ says ink
* and rule because it is a document; this is a console, so it says
* text-primary and border, and every screen already reads that way. Same
* colours, honest names on both sides the aliasing adminsite/ does for
* colours, honest names on both sides - the aliasing adminsite/ does for
* licence state, one level further.
*
* `<alpha-value>` is what makes bg-danger/10 and border-accent/50 compile.
@@ -39,16 +39,16 @@ const config: Config = {
danger: withAlpha("--down-rgb"),
"danger-hover": withAlpha("--down-hover-rgb"),
// The floor beneath the ground: install one-liners, key blobs,
// run logs surfaces showing machine output, not interface.
// run logs - surfaces showing machine output, not interface.
well: withAlpha("--well-rgb"),
// Attention that is not yet failure an unsaved workflow, a
// Attention that is not yet failure - an unsaved workflow, a
// step about to run. Same amber as warning, because it is the
// same signal at a different distance.
signal: withAlpha("--pend-rgb"),
"signal-ink": withAlpha("--accent-ink-rgb"),
// Shell identity on step chips. Reusing the semantic pair is
// safe here because the chip is labelled "bash" or "pwsh" in
// text the colour is recognition, never the whole message.
// text - the colour is recognition, never the whole message.
bash: withAlpha("--up-rgb"),
pwsh: withAlpha("--accent-rgb"),
},
@@ -61,7 +61,7 @@ const config: Config = {
* steps are collapsed rather than the ~140 rounded-lg/md/xl classes
* across the app being rewritten: every one of them meant "a panel
* corner", and this is where that decision now lives. rounded-full
* is untouched status dots and pills still need it.
* is untouched - status dots and pills still need it.
*/
borderRadius: {
sm: "3px",