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