@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 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.13",
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.16.0",
26
- "@checkstack/frontend-api": "0.11.0",
27
- "@checkstack/sdk": "0.109.1",
28
- "@checkstack/script-packages-common": "0.3.4",
29
- "@checkstack/signal-frontend": "0.2.5",
30
- "@checkstack/ui": "1.16.2",
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.2"
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-muted px-1 text-[10px] text-muted-foreground"
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: "Configuration",
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: "Configuration",
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
- function mb(bytes: number): string {
64
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
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
- <Card>
340
- <CardHeader className="flex flex-row items-center justify-between">
341
- <CardTitle className="text-base">Install state</CardTitle>
342
- <Button
343
- type="button"
344
- size="sm"
345
- onClick={handleInstall}
346
- disabled={installMutation.isPending || installState?.status === "installing"}
347
- >
348
- <Download className="h-4 w-4" />
349
- {installState?.status === "installing" ? "Installing…" : "Install now"}
350
- </Button>
351
- </CardHeader>
352
- <CardContent className="space-y-2 text-sm">
353
- <div className="flex items-center gap-2">
354
- <span className="text-muted-foreground">Status:</span>
355
- <Badge variant={installState?.status === "error" ? "destructive" : "secondary"}>
356
- {installState?.status ?? "unknown"}
357
- </Badge>
358
- {installState?.totalSizeBytes !== undefined && (
359
- <Badge variant={overWarn ? "destructive" : "secondary"}>
360
- {mb(installState.totalSizeBytes)}
361
- </Badge>
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
- {installState?.errorMessage && (
365
- <p className="text-destructive text-xs">{installState.errorMessage}</p>
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
- <Card>
377
- <CardHeader className="flex flex-row items-center justify-between">
378
- <CardTitle className="text-base flex items-center gap-2">
379
- {auditAdvisories.length > 0 ? (
380
- <ShieldAlert className="h-4 w-4 text-destructive" />
381
- ) : (
382
- <ShieldCheck className="h-4 w-4 text-emerald-600" />
383
- )}
384
- Vulnerability audit
385
- {auditAdvisories.length > 0 && (
386
- <Badge variant="destructive">{auditAdvisories.length}</Badge>
387
- )}
388
- </CardTitle>
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
- </CardHeader>
405
- <CardContent className="space-y-3 text-sm">
406
- <p className="text-xs text-muted-foreground">
407
- Runs <code>bun audit</code> against the installed package tree once
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
- <Badge variant="destructive">
422
- {auditState.counts.critical} critical
423
- </Badge>
434
+ <StatusPill
435
+ tone="down"
436
+ label={`${auditState.counts.critical} critical`}
437
+ />
424
438
  )}
425
439
  {auditState.counts.high > 0 && (
426
- <Badge variant="destructive">
427
- {auditState.counts.high} high
428
- </Badge>
440
+ <StatusPill
441
+ tone="down"
442
+ label={`${auditState.counts.high} high`}
443
+ />
429
444
  )}
430
445
  {auditState.counts.moderate > 0 && (
431
- <Badge variant="secondary">
432
- {auditState.counts.moderate} moderate
433
- </Badge>
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
- </div>
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-destructive text-xs">
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
- <ul className="divide-y divide-border rounded-md border border-border">
475
+ <SurfaceList>
454
476
  {auditAdvisories.map((a) => (
455
- <li
456
- key={`${a.packageName} ${a.advisoryId}`}
457
- className="flex items-center justify-between gap-3 px-3 py-2"
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 truncate">
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
- (a.title || a.advisoryId)
496
+ a.title || a.advisoryId
478
497
  )}
479
498
  </span>
480
499
  </span>
481
- <Badge variant={severityVariant(a.severity)}>
482
- {a.severity}
483
- </Badge>
484
- </li>
500
+ <StatusPill
501
+ tone={severityTone(a.severity)}
502
+ label={a.severity}
503
+ />
504
+ </SurfaceListRow>
485
505
  ))}
486
- </ul>
506
+ </SurfaceList>
487
507
  )}
488
- </CardContent>
489
- </Card>
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
- <ul className="divide-y divide-border rounded-md border border-border">
580
+ <SurfaceList>
561
581
  {packages.map((pkg) => (
562
- <li
563
- key={pkg.name}
564
- className="flex items-center justify-between gap-3 px-3 py-2"
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-3">
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
- </li>
604
+ </SurfaceListRow>
588
605
  ))}
589
- </ul>
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-destructive">
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-emerald-600">
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), {mb(blobGc.lastBytesReclaimed)}{" "}
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: {mb(blobGc.totalBytesReclaimed)}
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
- <ul className="divide-y divide-border rounded-md border border-border">
914
+ <SurfaceList>
897
915
  {satellites.map((s) => (
898
- <li
899
- key={s.satelliteId}
900
- className="flex items-center justify-between px-3 py-2 text-sm"
901
- >
902
- <span className="font-mono">{s.satelliteId}</span>
903
- <Badge
904
- variant={
905
- s.status === "error" ? "destructive" : "secondary"
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
- </ul>
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
+ }