@checkstack/script-packages-frontend 0.3.13 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/package.json +8 -8
- package/src/components/PackageVersionCombobox.tsx +1 -1
- package/src/components/StatusPill.tsx +25 -0
- package/src/components/SummaryPanel.tsx +84 -0
- package/src/components/SurfaceList.tsx +40 -0
- package/src/index.tsx +2 -2
- package/src/pages/ScriptPackagesSettingsPage.tsx +143 -129
- package/src/pages/status-display.test.ts +89 -0
- package/src/pages/status-display.ts +114 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
# @checkstack/script-packages-frontend
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8cad340: Make data-dense tables mobile-friendly and align status colors with semantic tokens.
|
|
8
|
+
|
|
9
|
+
- Migrated the remaining data-dense tables to the `ResponsiveTable` + `MobileCardList` dual-layout: catalog (Systems/Groups/Environments), incident config, maintenance config + system history, announcement management, notification delivery attempts, plugin manager (installed plugins + events), satellite list, automation list, healthcheck runs, OAuth applications, and the queue runtime panel. On viewports below `sm` these now render stacked cards surfacing the high-priority fields instead of an overflowing table. Genuinely narrow or runtime-diagnostic panels (cache runtime, healthcheck history, anomaly mute list) were intentionally left as plain tables.
|
|
10
|
+
- Swapped hardcoded semantic status colors for design tokens (`text-warning`, `text-success`, `text-destructive`, `text-muted-foreground`) in GitOps provenance status, healthcheck editor warnings, dependency canvas node status, automation run-step status, queue runtime tone map, and script-packages settings. Chart-series literals, syntax/terminal palettes, and intentional brand accents (tips lightbulb, SLO streak flame ramp) were left untouched.
|
|
11
|
+
- Extracted pure display/validation logic into sibling `.logic.ts` modules (SLO display + editor, maintenance editor + config summary, dependency display, incident sort + validation, gitops kind-registry YAML) so it can be unit-tested in isolation. These extractions are behavior-preserving.
|
|
12
|
+
|
|
13
|
+
- 8cad340: Standardize number/byte/relative-time formatting and unify loading/empty/error
|
|
14
|
+
states across the queue, cache, script-packages, and pluginmanager admin
|
|
15
|
+
surfaces.
|
|
16
|
+
|
|
17
|
+
Byte sizes now route through the shared `formatBytes` helper from
|
|
18
|
+
`@checkstack/ui` with a single binary (KiB/MiB/GiB) convention, so cache,
|
|
19
|
+
script-packages, and pluginmanager no longer disagree on units. Number counts
|
|
20
|
+
use the shared `formatNumber`, and the queue runtime panel's job "ago" times use
|
|
21
|
+
the shared `formatRelativeTime` (date-fns) instead of hand-rolled math.
|
|
22
|
+
|
|
23
|
+
The queue runtime panel's job listing now renders failed loads via the shared
|
|
24
|
+
`QueryErrorState` (with a Retry button) and empty listings via the shared
|
|
25
|
+
`EmptyState`, replacing bare inline text. Error-bearing toasts on plugin
|
|
26
|
+
install/uninstall now use the canonical `toastError`/`toastSuccess` helpers.
|
|
27
|
+
|
|
28
|
+
These are presentation-only changes; the underlying values and the labels /
|
|
29
|
+
column headers / empty-state copy that the panels render are unchanged.
|
|
30
|
+
|
|
31
|
+
- 8cad340: Improve sidebar navigation and information architecture:
|
|
32
|
+
|
|
33
|
+
- Split the overloaded "Configuration" group into focused sections: "Settings"
|
|
34
|
+
(Auth Settings, Teams, Secrets, Notification Settings), "Platform" (Plugins,
|
|
35
|
+
GitOps, Integrations, Infrastructure), and "Developer" (Script Packages,
|
|
36
|
+
Script Sandbox).
|
|
37
|
+
- Unify nav active-state on a single shared `isNavRouteActive` helper so the
|
|
38
|
+
sidebar rail and the shared `NavItem` both prefix-match section roots
|
|
39
|
+
(child/detail routes now highlight the parent entry consistently).
|
|
40
|
+
- Mark the external Docs entry with an external-link icon so it is clear which
|
|
41
|
+
entries leave the app.
|
|
42
|
+
- Add an "Expand all" affordance to recover from a fully-collapsed sidebar.
|
|
43
|
+
- Flatten single-entry groups (e.g. Automation) into top-level items, skipping
|
|
44
|
+
the redundant group header.
|
|
45
|
+
- Add an in-drawer search entry to the mobile navigation (opens the Cmd+K
|
|
46
|
+
palette) and auto-expand the group containing the active route when the
|
|
47
|
+
drawer opens.
|
|
48
|
+
|
|
49
|
+
### Patch Changes
|
|
50
|
+
|
|
51
|
+
- 8cad340: Design-system rework: a premium, consistent UI language across the platform.
|
|
52
|
+
|
|
53
|
+
Foundation (`@checkstack/ui` + the shared Tailwind preset):
|
|
54
|
+
|
|
55
|
+
- A token system wired into the shared preset so it generates app-wide: a
|
|
56
|
+
surface elevation ramp (`surface` / `surface-2` / `surface-inset`), the
|
|
57
|
+
aurora gradient stops, a colorblind-safe `status` triad, and `grid-line`.
|
|
58
|
+
- A density model (`comfortable` / `compact`) via `--d-*` vars + `DensityProvider`
|
|
59
|
+
/ `useDensity`, with a user-menu density toggle, plus the polished
|
|
60
|
+
skeleton / empty / error state set.
|
|
61
|
+
- Honest, token-driven chart primitives (`TimeSeriesChart`, `Sparkline`,
|
|
62
|
+
`RadialGauge` / aurora hero, `RequestWaterfall`, `UptimeRibbon`).
|
|
63
|
+
- A signature aurora moment per page: `PageHeader` paints its icon strokes with
|
|
64
|
+
the aurora gradient and adds a hairline; `Card` gains soft layered depth.
|
|
65
|
+
|
|
66
|
+
Shell + surfaces:
|
|
67
|
+
|
|
68
|
+
- The app shell adopts the elevation ramp (header `surface-2`, sidebar
|
|
69
|
+
`surface`, content on the ambient base).
|
|
70
|
+
- The system-health dashboard, health-check latency / single-run views, and the
|
|
71
|
+
SLO dashboard are reskinned onto the primitives (aurora confidence gauge,
|
|
72
|
+
honest p50/p95 latency, request waterfall, number-led status cards).
|
|
73
|
+
|
|
74
|
+
App-wide adoption + premium rework:
|
|
75
|
+
|
|
76
|
+
- Every plugin frontend adopts the tokens, status triad, density, and elevation.
|
|
77
|
+
- The highest-impact surfaces in each plugin are then redesigned to a premium
|
|
78
|
+
bar: real depth, number-led hierarchy, multi-encoded status (pill + dot +
|
|
79
|
+
accent stripe), and refined list/table density. Several plugins extract pure
|
|
80
|
+
tone/label/format logic into unit-tested modules.
|
|
81
|
+
|
|
82
|
+
Alerts:
|
|
83
|
+
|
|
84
|
+
- Every alert/callout is unified onto a single premium `Alert` (depth surface +
|
|
85
|
+
status-accent stripe + toned icon chip, variant-driven).
|
|
86
|
+
|
|
87
|
+
BREAKING CHANGE: the duplicate `InfoBanner` component (and its sub-components)
|
|
88
|
+
is removed; use `Alert` instead - it is a drop-in replacement with the same
|
|
89
|
+
variants and composable parts.
|
|
90
|
+
|
|
91
|
+
- Updated dependencies [8cad340]
|
|
92
|
+
- Updated dependencies [8cad340]
|
|
93
|
+
- Updated dependencies [8cad340]
|
|
94
|
+
- Updated dependencies [8cad340]
|
|
95
|
+
- Updated dependencies [8cad340]
|
|
96
|
+
- Updated dependencies [8cad340]
|
|
97
|
+
- Updated dependencies [8cad340]
|
|
98
|
+
- Updated dependencies [8cad340]
|
|
99
|
+
- Updated dependencies [8cad340]
|
|
100
|
+
- Updated dependencies [8cad340]
|
|
101
|
+
- Updated dependencies [8cad340]
|
|
102
|
+
- Updated dependencies [8cad340]
|
|
103
|
+
- Updated dependencies [8cad340]
|
|
104
|
+
- Updated dependencies [8cad340]
|
|
105
|
+
- Updated dependencies [8cad340]
|
|
106
|
+
- Updated dependencies [8cad340]
|
|
107
|
+
- Updated dependencies [8cad340]
|
|
108
|
+
- Updated dependencies [8cad340]
|
|
109
|
+
- @checkstack/ui@1.17.0
|
|
110
|
+
- @checkstack/common@0.17.0
|
|
111
|
+
- @checkstack/frontend-api@0.11.1
|
|
112
|
+
- @checkstack/sdk@0.112.1
|
|
113
|
+
- @checkstack/script-packages-common@0.3.5
|
|
114
|
+
- @checkstack/signal-frontend@0.2.6
|
|
115
|
+
|
|
3
116
|
## 0.3.13
|
|
4
117
|
|
|
5
118
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/script-packages-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,12 +22,12 @@
|
|
|
22
22
|
"test": "bun test"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@checkstack/common": "0.
|
|
26
|
-
"@checkstack/frontend-api": "0.11.
|
|
27
|
-
"@checkstack/sdk": "0.
|
|
28
|
-
"@checkstack/script-packages-common": "0.3.
|
|
29
|
-
"@checkstack/signal-frontend": "0.2.
|
|
30
|
-
"@checkstack/ui": "1.
|
|
25
|
+
"@checkstack/common": "0.17.0",
|
|
26
|
+
"@checkstack/frontend-api": "0.11.1",
|
|
27
|
+
"@checkstack/sdk": "0.112.1",
|
|
28
|
+
"@checkstack/script-packages-common": "0.3.5",
|
|
29
|
+
"@checkstack/signal-frontend": "0.2.6",
|
|
30
|
+
"@checkstack/ui": "1.17.0",
|
|
31
31
|
"lucide-react": "^1.17.0",
|
|
32
32
|
"react": "19.2.7",
|
|
33
33
|
"react-router-dom": "^7.16.0"
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"@types/react": "^19.0.0",
|
|
37
37
|
"typescript": "^5.7.2",
|
|
38
38
|
"@checkstack/tsconfig": "0.0.7",
|
|
39
|
-
"@checkstack/scripts": "0.6.
|
|
39
|
+
"@checkstack/scripts": "0.6.3"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -149,7 +149,7 @@ export const PackageVersionCombobox: React.FC<PackageVersionComboboxProps> = ({
|
|
|
149
149
|
{tags.map((tag) => (
|
|
150
150
|
<span
|
|
151
151
|
key={tag}
|
|
152
|
-
className="flex shrink-0 items-center gap-0.5 rounded bg-
|
|
152
|
+
className="flex shrink-0 items-center gap-0.5 rounded bg-surface-inset px-1 text-[10px] text-muted-foreground"
|
|
153
153
|
>
|
|
154
154
|
<Tag className="h-2.5 w-2.5" />
|
|
155
155
|
{tag}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@checkstack/ui";
|
|
3
|
+
import { toneStyles, type StatusTone } from "../pages/status-display";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A colorblind-safe status pill from the design-system triad: a leading dot
|
|
7
|
+
* plus a text label so status is multi-encoded, never hue-alone.
|
|
8
|
+
*/
|
|
9
|
+
export const StatusPill: React.FC<{ tone: StatusTone; label: string }> = ({
|
|
10
|
+
tone,
|
|
11
|
+
label,
|
|
12
|
+
}) => {
|
|
13
|
+
const style = toneStyles[tone];
|
|
14
|
+
return (
|
|
15
|
+
<span
|
|
16
|
+
className={cn(
|
|
17
|
+
"inline-flex shrink-0 items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium",
|
|
18
|
+
style.pill,
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
<span className={cn("size-1.5 rounded-full", style.dot)} aria-hidden />
|
|
22
|
+
{label}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@checkstack/ui";
|
|
3
|
+
import { toneStyles, type StatusTone } from "../pages/status-display";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A premium number-led summary panel: the slo-card depth treatment (gradient
|
|
7
|
+
* surface, layered shadow, soft border, density tokens) with a left accent
|
|
8
|
+
* stripe driven by the status triad. Leads with a single hero figure, then a
|
|
9
|
+
* caption, then a right-aligned cluster (status pill + action) and optional
|
|
10
|
+
* metadata / footer rows.
|
|
11
|
+
*
|
|
12
|
+
* Visual only - it owns no domain logic; callers pass already-formatted text.
|
|
13
|
+
*/
|
|
14
|
+
export interface SummaryPanelProps {
|
|
15
|
+
/** Status triad tone driving the left accent stripe. */
|
|
16
|
+
tone: StatusTone;
|
|
17
|
+
/** Optional leading glyph next to the title. */
|
|
18
|
+
icon?: React.ReactNode;
|
|
19
|
+
/** Section title, e.g. "Install state". */
|
|
20
|
+
title: string;
|
|
21
|
+
/** The single most important figure, rendered as the hero. */
|
|
22
|
+
hero: React.ReactNode;
|
|
23
|
+
/** Small caption beneath the hero, e.g. "installed size". */
|
|
24
|
+
caption: string;
|
|
25
|
+
/** Recolors the hero figure (e.g. status-warn when over a threshold). */
|
|
26
|
+
heroToneClass?: string;
|
|
27
|
+
/** Top-right cluster: status pill, action button, etc. */
|
|
28
|
+
trailing?: React.ReactNode;
|
|
29
|
+
/** Secondary metadata row (e.g. per-severity pills, last-run badge). */
|
|
30
|
+
meta?: React.ReactNode;
|
|
31
|
+
/** Footer content (explanatory copy, threshold notes, lists). */
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SummaryPanel: React.FC<SummaryPanelProps> = ({
|
|
36
|
+
tone,
|
|
37
|
+
icon,
|
|
38
|
+
title,
|
|
39
|
+
hero,
|
|
40
|
+
caption,
|
|
41
|
+
heroToneClass,
|
|
42
|
+
trailing,
|
|
43
|
+
meta,
|
|
44
|
+
children,
|
|
45
|
+
}) => {
|
|
46
|
+
const accent = toneStyles[tone].accent;
|
|
47
|
+
return (
|
|
48
|
+
<div className="group relative overflow-hidden rounded-[var(--d-card-r)] border border-border/70 bg-gradient-to-b from-surface-2 to-surface p-[var(--d-pad)] shadow-[0_1px_2px_hsl(var(--foreground)/0.04),0_10px_30px_-14px_hsl(var(--foreground)/0.12)] transition-all duration-200 hover:border-border">
|
|
49
|
+
{/* Status accent stripe: status by position + hue, not color alone. */}
|
|
50
|
+
<span
|
|
51
|
+
className={cn("absolute inset-y-0 left-0 w-1", accent)}
|
|
52
|
+
aria-hidden
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<div className="flex items-start justify-between gap-3 pl-2">
|
|
56
|
+
<h3 className="flex min-w-0 items-center gap-2 text-sm font-semibold text-foreground">
|
|
57
|
+
{icon}
|
|
58
|
+
{title}
|
|
59
|
+
</h3>
|
|
60
|
+
{trailing && (
|
|
61
|
+
<div className="flex shrink-0 items-center gap-2">{trailing}</div>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div className="mt-4 pl-2">
|
|
66
|
+
<p
|
|
67
|
+
className={cn(
|
|
68
|
+
"text-3xl font-bold leading-none tabular-nums",
|
|
69
|
+
heroToneClass ?? "text-foreground",
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{hero}
|
|
73
|
+
</p>
|
|
74
|
+
<p className="mt-1 text-xs text-muted-foreground">{caption}</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{meta && (
|
|
78
|
+
<div className="mt-3 flex flex-wrap items-center gap-2 pl-2">{meta}</div>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{children && <div className="mt-3 pl-2">{children}</div>}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "@checkstack/ui";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A premium elevated list surface: one rounded gradient container with a soft
|
|
6
|
+
* border and low-contrast dividers, so a set of rows reads as a single
|
|
7
|
+
* cohesive surface rather than bare divider lines. Rows are supplied as
|
|
8
|
+
* children and should use `SurfaceListRow` for consistent hover + density.
|
|
9
|
+
*/
|
|
10
|
+
export const SurfaceList: React.FC<{
|
|
11
|
+
className?: string;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}> = ({ className, children }) => (
|
|
14
|
+
<ul
|
|
15
|
+
className={cn(
|
|
16
|
+
"divide-y divide-border/60 overflow-hidden rounded-[var(--d-card-r)] border border-border/70 bg-gradient-to-b from-surface-2 to-surface",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</ul>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A single row inside a `SurfaceList`: density-token padding, a refined hover
|
|
26
|
+
* affordance, and a comfortable leading/trailing rhythm.
|
|
27
|
+
*/
|
|
28
|
+
export const SurfaceListRow: React.FC<{
|
|
29
|
+
className?: string;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
}> = ({ className, children }) => (
|
|
32
|
+
<li
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex items-center justify-between gap-3 px-[var(--d-pad)] py-2.5 transition-colors hover:bg-surface-inset",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</li>
|
|
40
|
+
);
|
package/src/index.tsx
CHANGED
|
@@ -34,7 +34,7 @@ export default createFrontendPlugin({
|
|
|
34
34
|
title: "Script packages",
|
|
35
35
|
accessRule: scriptPackagesAccess.manage,
|
|
36
36
|
nav: {
|
|
37
|
-
group: "
|
|
37
|
+
group: "Developer",
|
|
38
38
|
icon: Package,
|
|
39
39
|
label: "Script Packages",
|
|
40
40
|
},
|
|
@@ -48,7 +48,7 @@ export default createFrontendPlugin({
|
|
|
48
48
|
title: "Script sandbox",
|
|
49
49
|
accessRule: scriptSandboxAccess.manage,
|
|
50
50
|
nav: {
|
|
51
|
-
group: "
|
|
51
|
+
group: "Developer",
|
|
52
52
|
icon: ShieldCheck,
|
|
53
53
|
label: "Script Sandbox",
|
|
54
54
|
},
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
scriptPackagesAccess,
|
|
21
21
|
PackageVersionSchema,
|
|
22
22
|
SCRIPT_PACKAGES_AUDIT_COMPLETED_SIGNAL,
|
|
23
|
-
type AuditSeverity,
|
|
24
23
|
} from "@checkstack/script-packages-common";
|
|
25
24
|
import { PackageNameCombobox } from "../components/PackageNameCombobox";
|
|
26
25
|
import { PackageVersionCombobox } from "../components/PackageVersionCombobox";
|
|
@@ -28,6 +27,9 @@ import {
|
|
|
28
27
|
applyLatestDistTag,
|
|
29
28
|
versionFromHit,
|
|
30
29
|
} from "../components/version-autofill";
|
|
30
|
+
import { StatusPill } from "../components/StatusPill";
|
|
31
|
+
import { SummaryPanel } from "../components/SummaryPanel";
|
|
32
|
+
import { SurfaceList, SurfaceListRow } from "../components/SurfaceList";
|
|
31
33
|
import {
|
|
32
34
|
PageLayout,
|
|
33
35
|
Card,
|
|
@@ -57,27 +59,22 @@ import {
|
|
|
57
59
|
usePerformance,
|
|
58
60
|
useInitOnceForKey,
|
|
59
61
|
cn,
|
|
62
|
+
formatBytes,
|
|
60
63
|
} from "@checkstack/ui";
|
|
61
64
|
import { extractErrorMessage } from "@checkstack/common";
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
import {
|
|
66
|
+
auditPostureTone,
|
|
67
|
+
installStatusTone,
|
|
68
|
+
satelliteStatusTone,
|
|
69
|
+
severityTone,
|
|
70
|
+
toneStyles,
|
|
71
|
+
} from "./status-display";
|
|
66
72
|
|
|
67
73
|
/** MB (UI unit) → bytes (the size-cap schema unit). */
|
|
68
74
|
function mbToBytes(value: number): number {
|
|
69
75
|
return Math.round(value * 1024 * 1024);
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
/** Badge variant for an advisory severity. */
|
|
73
|
-
function severityVariant(
|
|
74
|
-
severity: AuditSeverity,
|
|
75
|
-
): "destructive" | "secondary" {
|
|
76
|
-
return severity === "critical" || severity === "high"
|
|
77
|
-
? "destructive"
|
|
78
|
-
: "secondary";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
78
|
const SettingsContent: React.FC = () => {
|
|
82
79
|
const client = usePluginClient(ScriptPackagesApi);
|
|
83
80
|
const accessApi = useApi(accessApiRef);
|
|
@@ -319,6 +316,10 @@ const SettingsContent: React.FC = () => {
|
|
|
319
316
|
const audit = auditQuery.data;
|
|
320
317
|
const auditAdvisories = audit?.advisories ?? [];
|
|
321
318
|
const auditState = audit?.state;
|
|
319
|
+
const auditTone = auditPostureTone({
|
|
320
|
+
total: auditAdvisories.length,
|
|
321
|
+
counts: auditState?.counts,
|
|
322
|
+
});
|
|
322
323
|
const availableBackends = backendsQuery.data?.backends ?? [];
|
|
323
324
|
const migrating = storage?.migrationStatus === "migrating";
|
|
324
325
|
const migrationTargets = availableBackends.filter(
|
|
@@ -336,56 +337,73 @@ const SettingsContent: React.FC = () => {
|
|
|
336
337
|
)}
|
|
337
338
|
|
|
338
339
|
{/* Install state */}
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
340
|
+
<SummaryPanel
|
|
341
|
+
tone={installStatusTone(installState?.status)}
|
|
342
|
+
title="Install state"
|
|
343
|
+
hero={
|
|
344
|
+
installState?.totalSizeBytes === undefined
|
|
345
|
+
? "—"
|
|
346
|
+
: formatBytes(installState.totalSizeBytes)
|
|
347
|
+
}
|
|
348
|
+
heroToneClass={overWarn ? "text-status-warn" : undefined}
|
|
349
|
+
caption="installed size"
|
|
350
|
+
trailing={
|
|
351
|
+
<>
|
|
352
|
+
<StatusPill
|
|
353
|
+
tone={installStatusTone(installState?.status)}
|
|
354
|
+
label={installState?.status ?? "unknown"}
|
|
355
|
+
/>
|
|
356
|
+
<Button
|
|
357
|
+
type="button"
|
|
358
|
+
size="sm"
|
|
359
|
+
onClick={handleInstall}
|
|
360
|
+
disabled={
|
|
361
|
+
installMutation.isPending ||
|
|
362
|
+
installState?.status === "installing"
|
|
363
|
+
}
|
|
364
|
+
>
|
|
365
|
+
<Download className="h-4 w-4" />
|
|
366
|
+
{installState?.status === "installing"
|
|
367
|
+
? "Installing…"
|
|
368
|
+
: "Install now"}
|
|
369
|
+
</Button>
|
|
370
|
+
</>
|
|
371
|
+
}
|
|
372
|
+
>
|
|
373
|
+
{(installState?.errorMessage || (overWarn && sizeCap)) && (
|
|
374
|
+
<div className="space-y-1">
|
|
375
|
+
{installState?.errorMessage && (
|
|
376
|
+
<p className="text-xs text-status-down">
|
|
377
|
+
{installState.errorMessage}
|
|
378
|
+
</p>
|
|
379
|
+
)}
|
|
380
|
+
{overWarn && sizeCap && (
|
|
381
|
+
<p className="text-xs text-muted-foreground">
|
|
382
|
+
Resolved size exceeds the {formatBytes(sizeCap.warnBytes)}{" "}
|
|
383
|
+
warning threshold.
|
|
384
|
+
</p>
|
|
362
385
|
)}
|
|
363
386
|
</div>
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
)}
|
|
367
|
-
{overWarn && sizeCap && (
|
|
368
|
-
<p className="text-xs text-amber-600">
|
|
369
|
-
Resolved size exceeds the {mb(sizeCap.warnBytes)} warning threshold.
|
|
370
|
-
</p>
|
|
371
|
-
)}
|
|
372
|
-
</CardContent>
|
|
373
|
-
</Card>
|
|
387
|
+
)}
|
|
388
|
+
</SummaryPanel>
|
|
374
389
|
|
|
375
390
|
{/* Vulnerability audit */}
|
|
376
|
-
<
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
391
|
+
<SummaryPanel
|
|
392
|
+
tone={auditTone}
|
|
393
|
+
icon={
|
|
394
|
+
auditAdvisories.length > 0 ? (
|
|
395
|
+
<ShieldAlert className="h-4 w-4 text-status-down" />
|
|
396
|
+
) : (
|
|
397
|
+
<ShieldCheck className="h-4 w-4 text-status-ok" />
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
title="Vulnerability audit"
|
|
401
|
+
hero={String(auditAdvisories.length)}
|
|
402
|
+
heroToneClass={
|
|
403
|
+
auditAdvisories.length === 0 ? toneStyles.ok.text : undefined
|
|
404
|
+
}
|
|
405
|
+
caption={auditAdvisories.length === 0 ? "clean" : "open advisories"}
|
|
406
|
+
trailing={
|
|
389
407
|
<Button
|
|
390
408
|
type="button"
|
|
391
409
|
size="sm"
|
|
@@ -401,15 +419,10 @@ const SettingsContent: React.FC = () => {
|
|
|
401
419
|
/>
|
|
402
420
|
{auditMutation.isPending ? "Auditing…" : "Audit now"}
|
|
403
421
|
</Button>
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
a day and notifies managers when a new vulnerability appears.
|
|
409
|
-
Findings below cover every severity.
|
|
410
|
-
</p>
|
|
411
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
412
|
-
<span className="text-muted-foreground">Last run:</span>
|
|
422
|
+
}
|
|
423
|
+
meta={
|
|
424
|
+
<>
|
|
425
|
+
<span className="text-xs text-muted-foreground">Last run:</span>
|
|
413
426
|
<Badge variant="secondary">
|
|
414
427
|
{auditState?.lastRunAt
|
|
415
428
|
? new Date(auditState.lastRunAt).toLocaleString()
|
|
@@ -418,30 +431,39 @@ const SettingsContent: React.FC = () => {
|
|
|
418
431
|
{auditState && auditState.total > 0 && (
|
|
419
432
|
<>
|
|
420
433
|
{auditState.counts.critical > 0 && (
|
|
421
|
-
<
|
|
422
|
-
|
|
423
|
-
|
|
434
|
+
<StatusPill
|
|
435
|
+
tone="down"
|
|
436
|
+
label={`${auditState.counts.critical} critical`}
|
|
437
|
+
/>
|
|
424
438
|
)}
|
|
425
439
|
{auditState.counts.high > 0 && (
|
|
426
|
-
<
|
|
427
|
-
|
|
428
|
-
|
|
440
|
+
<StatusPill
|
|
441
|
+
tone="down"
|
|
442
|
+
label={`${auditState.counts.high} high`}
|
|
443
|
+
/>
|
|
429
444
|
)}
|
|
430
445
|
{auditState.counts.moderate > 0 && (
|
|
431
|
-
<
|
|
432
|
-
|
|
433
|
-
|
|
446
|
+
<StatusPill
|
|
447
|
+
tone="warn"
|
|
448
|
+
label={`${auditState.counts.moderate} moderate`}
|
|
449
|
+
/>
|
|
434
450
|
)}
|
|
435
451
|
{auditState.counts.low > 0 && (
|
|
436
|
-
<Badge variant="secondary">
|
|
437
|
-
{auditState.counts.low} low
|
|
438
|
-
</Badge>
|
|
452
|
+
<Badge variant="secondary">{auditState.counts.low} low</Badge>
|
|
439
453
|
)}
|
|
440
454
|
</>
|
|
441
455
|
)}
|
|
442
|
-
|
|
456
|
+
</>
|
|
457
|
+
}
|
|
458
|
+
>
|
|
459
|
+
<div className="space-y-3 text-sm">
|
|
460
|
+
<p className="text-xs text-muted-foreground">
|
|
461
|
+
Runs <code>bun audit</code> against the installed package tree once a
|
|
462
|
+
day and notifies managers when a new vulnerability appears. Findings
|
|
463
|
+
below cover every severity.
|
|
464
|
+
</p>
|
|
443
465
|
{auditState?.errorMessage && (
|
|
444
|
-
<p className="text-
|
|
466
|
+
<p className="text-xs text-status-down">
|
|
445
467
|
Last audit failed: {auditState.errorMessage}
|
|
446
468
|
</p>
|
|
447
469
|
)}
|
|
@@ -450,20 +472,17 @@ const SettingsContent: React.FC = () => {
|
|
|
450
472
|
No known vulnerabilities in the installed tree.
|
|
451
473
|
</p>
|
|
452
474
|
) : (
|
|
453
|
-
<
|
|
475
|
+
<SurfaceList>
|
|
454
476
|
{auditAdvisories.map((a) => (
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
>
|
|
459
|
-
<span className="flex flex-col gap-0.5 min-w-0">
|
|
460
|
-
<span className="font-mono text-sm truncate">
|
|
477
|
+
<SurfaceListRow key={`${a.packageName} ${a.advisoryId}`}>
|
|
478
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
479
|
+
<span className="truncate font-mono text-sm">
|
|
461
480
|
{a.packageName}{" "}
|
|
462
481
|
<span className="text-muted-foreground">
|
|
463
482
|
{a.vulnerableVersions}
|
|
464
483
|
</span>
|
|
465
484
|
</span>
|
|
466
|
-
<span className="text-xs text-muted-foreground
|
|
485
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
467
486
|
{a.url ? (
|
|
468
487
|
<a
|
|
469
488
|
href={a.url}
|
|
@@ -474,19 +493,20 @@ const SettingsContent: React.FC = () => {
|
|
|
474
493
|
{a.title || a.advisoryId}
|
|
475
494
|
</a>
|
|
476
495
|
) : (
|
|
477
|
-
|
|
496
|
+
a.title || a.advisoryId
|
|
478
497
|
)}
|
|
479
498
|
</span>
|
|
480
499
|
</span>
|
|
481
|
-
<
|
|
482
|
-
{a.severity}
|
|
483
|
-
|
|
484
|
-
|
|
500
|
+
<StatusPill
|
|
501
|
+
tone={severityTone(a.severity)}
|
|
502
|
+
label={a.severity}
|
|
503
|
+
/>
|
|
504
|
+
</SurfaceListRow>
|
|
485
505
|
))}
|
|
486
|
-
</
|
|
506
|
+
</SurfaceList>
|
|
487
507
|
)}
|
|
488
|
-
</
|
|
489
|
-
</
|
|
508
|
+
</div>
|
|
509
|
+
</SummaryPanel>
|
|
490
510
|
|
|
491
511
|
{/* Allowlist */}
|
|
492
512
|
<Card>
|
|
@@ -557,17 +577,14 @@ const SettingsContent: React.FC = () => {
|
|
|
557
577
|
No packages yet. Add pinned, lightweight pure-JS packages.
|
|
558
578
|
</p>
|
|
559
579
|
) : (
|
|
560
|
-
<
|
|
580
|
+
<SurfaceList>
|
|
561
581
|
{packages.map((pkg) => (
|
|
562
|
-
<
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
>
|
|
566
|
-
<span className="flex items-center gap-2 font-mono text-sm">
|
|
567
|
-
<Package className="h-3.5 w-3.5 text-muted-foreground" />
|
|
582
|
+
<SurfaceListRow key={pkg.name}>
|
|
583
|
+
<span className="flex min-w-0 items-center gap-2 truncate font-mono text-sm">
|
|
584
|
+
<Package className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
568
585
|
{pkg.name}@{pkg.version}
|
|
569
586
|
</span>
|
|
570
|
-
<div className="flex items-center gap-
|
|
587
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
571
588
|
<Toggle
|
|
572
589
|
checked={pkg.enabled}
|
|
573
590
|
onCheckedChange={(enabled) =>
|
|
@@ -584,9 +601,9 @@ const SettingsContent: React.FC = () => {
|
|
|
584
601
|
<Trash2 className="h-4 w-4" />
|
|
585
602
|
</Button>
|
|
586
603
|
</div>
|
|
587
|
-
</
|
|
604
|
+
</SurfaceListRow>
|
|
588
605
|
))}
|
|
589
|
-
</
|
|
606
|
+
</SurfaceList>
|
|
590
607
|
)}
|
|
591
608
|
</CardContent>
|
|
592
609
|
</Card>
|
|
@@ -737,12 +754,12 @@ const SettingsContent: React.FC = () => {
|
|
|
737
754
|
|
|
738
755
|
{storage?.migrationStatus === "error" &&
|
|
739
756
|
storage.migrationError && (
|
|
740
|
-
<p className="text-xs text-
|
|
757
|
+
<p className="text-xs text-status-down">
|
|
741
758
|
Migration failed: {storage.migrationError}
|
|
742
759
|
</p>
|
|
743
760
|
)}
|
|
744
761
|
{storage?.migrationStatus === "completed" && (
|
|
745
|
-
<p className="text-xs text-
|
|
762
|
+
<p className="text-xs text-status-ok">
|
|
746
763
|
Migration complete. Active backend is now{" "}
|
|
747
764
|
{storage.activeBackend}.
|
|
748
765
|
</p>
|
|
@@ -864,14 +881,15 @@ const SettingsContent: React.FC = () => {
|
|
|
864
881
|
</Badge>
|
|
865
882
|
{blobGc && blobGc.lastRunAt && (
|
|
866
883
|
<Badge variant="secondary">
|
|
867
|
-
{blobGc.lastDeleted} blob(s),
|
|
868
|
-
reclaimed
|
|
884
|
+
{blobGc.lastDeleted} blob(s),{" "}
|
|
885
|
+
{formatBytes(blobGc.lastBytesReclaimed)} reclaimed
|
|
869
886
|
</Badge>
|
|
870
887
|
)}
|
|
871
888
|
</div>
|
|
872
889
|
{blobGc && blobGc.totalBytesReclaimed > 0 && (
|
|
873
890
|
<div className="text-xs text-muted-foreground">
|
|
874
|
-
Total reclaimed to date:
|
|
891
|
+
Total reclaimed to date:{" "}
|
|
892
|
+
{formatBytes(blobGc.totalBytesReclaimed)}
|
|
875
893
|
</div>
|
|
876
894
|
)}
|
|
877
895
|
<Button
|
|
@@ -893,23 +911,19 @@ const SettingsContent: React.FC = () => {
|
|
|
893
911
|
Satellite sync
|
|
894
912
|
</AccordionTrigger>
|
|
895
913
|
<AccordionContent>
|
|
896
|
-
<
|
|
914
|
+
<SurfaceList>
|
|
897
915
|
{satellites.map((s) => (
|
|
898
|
-
<
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
<
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
>
|
|
908
|
-
{s.status}
|
|
909
|
-
</Badge>
|
|
910
|
-
</li>
|
|
916
|
+
<SurfaceListRow key={s.satelliteId}>
|
|
917
|
+
<span className="truncate font-mono text-sm">
|
|
918
|
+
{s.satelliteId}
|
|
919
|
+
</span>
|
|
920
|
+
<StatusPill
|
|
921
|
+
tone={satelliteStatusTone(s.status)}
|
|
922
|
+
label={s.status}
|
|
923
|
+
/>
|
|
924
|
+
</SurfaceListRow>
|
|
911
925
|
))}
|
|
912
|
-
</
|
|
926
|
+
</SurfaceList>
|
|
913
927
|
</AccordionContent>
|
|
914
928
|
</AccordionItem>
|
|
915
929
|
)}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
auditPostureTone,
|
|
4
|
+
installStatusTone,
|
|
5
|
+
satelliteStatusTone,
|
|
6
|
+
severityTone,
|
|
7
|
+
} from "./status-display";
|
|
8
|
+
|
|
9
|
+
describe("installStatusTone", () => {
|
|
10
|
+
it("maps known install statuses to triad tones", () => {
|
|
11
|
+
expect(installStatusTone("ready")).toBe("ok");
|
|
12
|
+
expect(installStatusTone("installing")).toBe("warn");
|
|
13
|
+
expect(installStatusTone("error")).toBe("down");
|
|
14
|
+
expect(installStatusTone("idle")).toBe("unknown");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("falls back to unknown for undefined or unrecognized values", () => {
|
|
18
|
+
expect(installStatusTone(undefined)).toBe("unknown");
|
|
19
|
+
expect(installStatusTone("bogus")).toBe("unknown");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("satelliteStatusTone", () => {
|
|
24
|
+
it("maps known satellite sync statuses to triad tones", () => {
|
|
25
|
+
expect(satelliteStatusTone("ready")).toBe("ok");
|
|
26
|
+
expect(satelliteStatusTone("syncing")).toBe("warn");
|
|
27
|
+
expect(satelliteStatusTone("pending")).toBe("warn");
|
|
28
|
+
expect(satelliteStatusTone("error")).toBe("down");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("falls back to unknown for unrecognized values", () => {
|
|
32
|
+
expect(satelliteStatusTone("bogus")).toBe("unknown");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("severityTone", () => {
|
|
37
|
+
it("maps advisory severities to triad tones", () => {
|
|
38
|
+
expect(severityTone("critical")).toBe("down");
|
|
39
|
+
expect(severityTone("high")).toBe("down");
|
|
40
|
+
expect(severityTone("moderate")).toBe("warn");
|
|
41
|
+
expect(severityTone("low")).toBe("ok");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("auditPostureTone", () => {
|
|
46
|
+
it("is ok when there are no open advisories", () => {
|
|
47
|
+
expect(auditPostureTone({ total: 0 })).toBe("ok");
|
|
48
|
+
expect(
|
|
49
|
+
auditPostureTone({
|
|
50
|
+
total: 0,
|
|
51
|
+
counts: { critical: 0, high: 0, moderate: 0, low: 0 },
|
|
52
|
+
}),
|
|
53
|
+
).toBe("ok");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("is down when any critical or high advisory is present", () => {
|
|
57
|
+
expect(
|
|
58
|
+
auditPostureTone({
|
|
59
|
+
total: 1,
|
|
60
|
+
counts: { critical: 1, high: 0, moderate: 0, low: 0 },
|
|
61
|
+
}),
|
|
62
|
+
).toBe("down");
|
|
63
|
+
expect(
|
|
64
|
+
auditPostureTone({
|
|
65
|
+
total: 2,
|
|
66
|
+
counts: { critical: 0, high: 2, moderate: 0, low: 0 },
|
|
67
|
+
}),
|
|
68
|
+
).toBe("down");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("is warn when only moderate or low advisories remain", () => {
|
|
72
|
+
expect(
|
|
73
|
+
auditPostureTone({
|
|
74
|
+
total: 3,
|
|
75
|
+
counts: { critical: 0, high: 0, moderate: 1, low: 2 },
|
|
76
|
+
}),
|
|
77
|
+
).toBe("warn");
|
|
78
|
+
expect(
|
|
79
|
+
auditPostureTone({
|
|
80
|
+
total: 1,
|
|
81
|
+
counts: { critical: 0, high: 0, moderate: 0, low: 1 },
|
|
82
|
+
}),
|
|
83
|
+
).toBe("warn");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("falls back to down when advisories exist without a breakdown", () => {
|
|
87
|
+
expect(auditPostureTone({ total: 5 })).toBe("down");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { AuditSeverity } from "@checkstack/script-packages-common";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Colorblind-safe status tones from the shared design-system triad. Maps the
|
|
5
|
+
* plugin's domain status values (install state, satellite sync, advisory
|
|
6
|
+
* severity) onto `ok` / `warn` / `down` / `unknown` so every status readout is
|
|
7
|
+
* multi-encoded (hue + dot + label), never color-alone.
|
|
8
|
+
*/
|
|
9
|
+
export type StatusTone = "ok" | "warn" | "down" | "unknown";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Per-tone class sets for a status pill, its leading dot, a card's left accent
|
|
13
|
+
* stripe, and the foreground color for a number-led hero figure.
|
|
14
|
+
*/
|
|
15
|
+
export interface ToneStyle {
|
|
16
|
+
pill: string;
|
|
17
|
+
dot: string;
|
|
18
|
+
accent: string;
|
|
19
|
+
text: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Full literal class strings (not interpolated) so Tailwind's JIT keeps them.
|
|
24
|
+
*/
|
|
25
|
+
export const toneStyles: Record<StatusTone, ToneStyle> = {
|
|
26
|
+
ok: {
|
|
27
|
+
pill: "bg-status-ok/10 text-status-ok",
|
|
28
|
+
dot: "bg-status-ok",
|
|
29
|
+
accent: "bg-status-ok",
|
|
30
|
+
text: "text-status-ok",
|
|
31
|
+
},
|
|
32
|
+
warn: {
|
|
33
|
+
pill: "bg-status-warn/10 text-status-warn",
|
|
34
|
+
dot: "bg-status-warn",
|
|
35
|
+
accent: "bg-status-warn",
|
|
36
|
+
text: "text-status-warn",
|
|
37
|
+
},
|
|
38
|
+
down: {
|
|
39
|
+
pill: "bg-status-down/10 text-status-down",
|
|
40
|
+
dot: "bg-status-down",
|
|
41
|
+
accent: "bg-status-down",
|
|
42
|
+
text: "text-status-down",
|
|
43
|
+
},
|
|
44
|
+
unknown: {
|
|
45
|
+
pill: "bg-status-unknown/10 text-status-unknown",
|
|
46
|
+
dot: "bg-status-unknown",
|
|
47
|
+
accent: "bg-status-unknown",
|
|
48
|
+
text: "text-status-unknown",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Install-state status values mapped to a triad tone. */
|
|
53
|
+
const INSTALL_STATUS_TONES: Record<string, StatusTone> = {
|
|
54
|
+
ready: "ok",
|
|
55
|
+
installing: "warn",
|
|
56
|
+
error: "down",
|
|
57
|
+
idle: "unknown",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Tone for an install-state status value (the backend's `status` string). */
|
|
61
|
+
export function installStatusTone(status: string | undefined): StatusTone {
|
|
62
|
+
return (status && INSTALL_STATUS_TONES[status]) || "unknown";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Satellite sync status values mapped to a triad tone. */
|
|
66
|
+
const SATELLITE_STATUS_TONES: Record<string, StatusTone> = {
|
|
67
|
+
ready: "ok",
|
|
68
|
+
syncing: "warn",
|
|
69
|
+
pending: "warn",
|
|
70
|
+
error: "down",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Tone for a satellite sync status value. */
|
|
74
|
+
export function satelliteStatusTone(status: string): StatusTone {
|
|
75
|
+
return SATELLITE_STATUS_TONES[status] || "unknown";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Advisory severity values mapped to a triad tone. */
|
|
79
|
+
const SEVERITY_TONES: Record<AuditSeverity, StatusTone> = {
|
|
80
|
+
critical: "down",
|
|
81
|
+
high: "down",
|
|
82
|
+
moderate: "warn",
|
|
83
|
+
low: "ok",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Tone for an advisory severity. */
|
|
87
|
+
export function severityTone(severity: AuditSeverity): StatusTone {
|
|
88
|
+
return SEVERITY_TONES[severity];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Per-severity advisory counts used to derive overall audit posture. */
|
|
92
|
+
export interface AuditCounts {
|
|
93
|
+
critical: number;
|
|
94
|
+
high: number;
|
|
95
|
+
moderate: number;
|
|
96
|
+
low: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Overall vulnerability-audit posture as a triad tone:
|
|
101
|
+
* `ok` when there are no open advisories, `down` when any critical/high
|
|
102
|
+
* advisory is present, and `warn` when only moderate/low advisories remain.
|
|
103
|
+
*/
|
|
104
|
+
export function auditPostureTone(params: {
|
|
105
|
+
total: number;
|
|
106
|
+
counts?: AuditCounts;
|
|
107
|
+
}): StatusTone {
|
|
108
|
+
const { total, counts } = params;
|
|
109
|
+
if (total <= 0) return "ok";
|
|
110
|
+
if (counts && (counts.critical > 0 || counts.high > 0)) return "down";
|
|
111
|
+
if (counts && (counts.moderate > 0 || counts.low > 0)) return "warn";
|
|
112
|
+
// Advisories exist but we have no per-severity breakdown - treat as down.
|
|
113
|
+
return "down";
|
|
114
|
+
}
|