@skyhook-io/k8s-ui 1.8.15 → 1.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.8.15",
3
+ "version": "1.9.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -278,6 +278,34 @@ describe("ApplicationDetail shell", () => {
278
278
  expect(html).not.toContain('Recording coverage is incomplete')
279
279
  })
280
280
 
281
+ it('reports ring truncation with its own copy, only in retained mode', () => {
282
+ const base = {
283
+ selectedView: 'history' as const,
284
+ onSelectView: () => {},
285
+ selectedWorkloadKey: null,
286
+ onSelectWorkload: () => {},
287
+ historyRange: '7d' as const,
288
+ historyRangeOptions: [{ value: '7d' as const, label: '7 days' }],
289
+ historyRingTruncated: true,
290
+ historyItems: [{
291
+ id: 'ring-change',
292
+ category: 'change' as const,
293
+ title: 'Deployment updated',
294
+ timestamp: '2026-07-13T09:00:00.000Z',
295
+ }],
296
+ }
297
+ const retained = renderDetail({ ...base, historyMode: 'retained' })
298
+ expect(retained).toContain('the retained window exceeded')
299
+ // Distinct cause, distinct copy — not the per-query 10k note.
300
+ expect(retained).not.toContain('Showing the 10,000 most recent events')
301
+
302
+ const local = renderDetail({ ...base, historyMode: 'local' })
303
+ expect(local).not.toContain('the retained window exceeded')
304
+ // Local can be ring-truncated too (the binary's 10k ring) - same warning,
305
+ // source-appropriate copy.
306
+ expect(local).toContain('ring is full, so the oldest activity is not loaded')
307
+ })
308
+
281
309
  it('shows a single honest error state when runtime history fails without source history', () => {
282
310
  const html = renderDetail({
283
311
  selectedView: 'history',
@@ -238,6 +238,9 @@ export type ApplicationDetailProps = {
238
238
  }>;
239
239
  historyCoverageRecordCount?: number;
240
240
  historyRuntimeLimited?: boolean;
241
+ // The retained ring itself was row-capped: the OLDEST part of the retention
242
+ // window is not loaded, independent of the per-query event limit.
243
+ historyRingTruncated?: boolean;
241
244
  onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
242
245
  onOpenTimeline?: (timestamp?: string) => void;
243
246
  onOpenSource?: (source: AppSourceRef) => void;
@@ -330,6 +333,7 @@ export function ApplicationDetail({
330
333
  historyRangeOptions,
331
334
  historyCoverageRecordCount,
332
335
  historyRuntimeLimited,
336
+ historyRingTruncated,
333
337
  onHistoryRangeChange,
334
338
  onOpenTimeline,
335
339
  onOpenSource,
@@ -750,6 +754,7 @@ export function ApplicationDetail({
750
754
  historyRangeOptions={historyRangeOptions}
751
755
  historyCoverageRecordCount={historyCoverageRecordCount}
752
756
  historyRuntimeLimited={historyRuntimeLimited}
757
+ historyRingTruncated={historyRingTruncated}
753
758
  onHistoryRangeChange={onHistoryRangeChange}
754
759
  onOpenTimeline={onOpenTimeline}
755
760
  onOpenSource={onOpenSource}
@@ -797,6 +802,7 @@ function ApplicationWorkspace({
797
802
  historyRangeOptions,
798
803
  historyCoverageRecordCount,
799
804
  historyRuntimeLimited,
805
+ historyRingTruncated,
800
806
  onHistoryRangeChange,
801
807
  onOpenTimeline,
802
808
  onOpenSource,
@@ -848,6 +854,7 @@ function ApplicationWorkspace({
848
854
  }>;
849
855
  historyCoverageRecordCount?: number;
850
856
  historyRuntimeLimited?: boolean;
857
+ historyRingTruncated?: boolean;
851
858
  onHistoryRangeChange?: (range: ApplicationHistoryRange) => void;
852
859
  onOpenTimeline?: (timestamp?: string) => void;
853
860
  onOpenSource?: (source: AppSourceRef) => void;
@@ -916,6 +923,7 @@ function ApplicationWorkspace({
916
923
  rangeOptions={historyRangeOptions}
917
924
  coverageRecordCount={historyCoverageRecordCount}
918
925
  runtimeLimited={historyRuntimeLimited}
926
+ ringTruncated={historyRingTruncated}
919
927
  workloads={workloads}
920
928
  onSelectWorkload={onSelectWorkload}
921
929
  onNavigateToResource={onNavigateToResource}
@@ -2146,6 +2154,7 @@ function ApplicationHistoryView({
2146
2154
  rangeOptions,
2147
2155
  coverageRecordCount,
2148
2156
  runtimeLimited,
2157
+ ringTruncated,
2149
2158
  workloads,
2150
2159
  onSelectWorkload,
2151
2160
  onNavigateToResource,
@@ -2163,6 +2172,7 @@ function ApplicationHistoryView({
2163
2172
  rangeOptions?: Array<{ value: ApplicationHistoryRange; label: string }>;
2164
2173
  coverageRecordCount?: number;
2165
2174
  runtimeLimited?: boolean;
2175
+ ringTruncated?: boolean;
2166
2176
  workloads: AppWorkload[];
2167
2177
  onSelectWorkload: (workload: AppWorkload) => void;
2168
2178
  onNavigateToResource?: (resource: ResourceRef) => void;
@@ -2372,6 +2382,13 @@ function ApplicationHistoryView({
2372
2382
  application activity in this range may not be included.
2373
2383
  </div>
2374
2384
  )}
2385
+ {ringTruncated && (
2386
+ <div>
2387
+ {mode === "retained"
2388
+ ? "Event history may be incomplete: the retained window exceeded its size limit, so the oldest activity is not loaded."
2389
+ : "Event history may be incomplete: the event store's ring is full, so the oldest activity is not loaded."}
2390
+ </div>
2391
+ )}
2375
2392
  </div>
2376
2393
  </section>
2377
2394
  </div>
@@ -1,7 +1,8 @@
1
1
  import { useState } from 'react'
2
2
  import { ClipboardCheck, ChevronRight, ShieldAlert, AlertTriangle, ArrowRight } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { SEVERITY_TEXT, BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
4
+ import { BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
5
+ import { SEVERITY_TEXT_CLASS } from '../checks/severity'
5
6
 
6
7
  export interface AuditFinding {
7
8
  kind: string
@@ -13,6 +14,7 @@ export interface AuditFinding {
13
14
  name: string
14
15
  checkID: string
15
16
  category: string
17
+ /** Raw detector compatibility value: danger renders as High, warning as Medium. */
16
18
  severity: string
17
19
  message: string
18
20
  /** Cluster context — set when findings come from multiple clusters
@@ -37,8 +39,8 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
37
39
 
38
40
  if (findings.length === 0) return null
39
41
 
40
- const dangers = findings.filter(f => f.severity === 'danger').length
41
- const warnings = findings.filter(f => f.severity === 'warning').length
42
+ const highCount = findings.filter(f => f.severity === 'danger').length
43
+ const mediumCount = findings.filter(f => f.severity === 'warning').length
42
44
 
43
45
  return (
44
46
  <section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
@@ -51,11 +53,11 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
51
53
  <ClipboardCheck className="w-4 h-4 text-theme-text-secondary" />
52
54
  <span className="text-sm font-semibold text-theme-text-primary">Audit Findings</span>
53
55
  <div className="flex items-center gap-2 ml-1">
54
- {dangers > 0 && (
55
- <span className={clsx('text-xs font-medium tabular-nums', SEVERITY_TEXT.error)}>{dangers} critical</span>
56
+ {highCount > 0 && (
57
+ <span className={clsx('text-xs font-medium tabular-nums', SEVERITY_TEXT_CLASS.high)}>{highCount} high</span>
56
58
  )}
57
- {warnings > 0 && (
58
- <span className={clsx('text-xs font-medium tabular-nums', SEVERITY_TEXT.warning)}>{warnings} warning</span>
59
+ {mediumCount > 0 && (
60
+ <span className={clsx('text-xs font-medium tabular-nums', SEVERITY_TEXT_CLASS.medium)}>{mediumCount} medium</span>
59
61
  )}
60
62
  </div>
61
63
  </button>
@@ -67,13 +69,13 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
67
69
  <div className="overflow-hidden">
68
70
  <div className="flex flex-col gap-0.5 pt-3">
69
71
  {findings.map((f, i) => {
70
- const isDanger = f.severity === 'danger'
72
+ const isHigh = f.severity === 'danger'
71
73
  return (
72
74
  <div key={`${f.checkID}-${i}`} className="flex items-start gap-2 py-1">
73
- {isDanger ? (
74
- <ShieldAlert className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT.error)} />
75
+ {isHigh ? (
76
+ <ShieldAlert className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT_CLASS.high)} />
75
77
  ) : (
76
- <AlertTriangle className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT.warning)} />
78
+ <AlertTriangle className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT_CLASS.medium)} />
77
79
  )}
78
80
  <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1">
79
81
  <span className="text-xs text-theme-text-secondary">{f.message}</span>
@@ -23,6 +23,12 @@ describe('AuditBadgeTooltip', () => {
23
23
  expect(html).not.toContain('A fourth finding')
24
24
  })
25
25
 
26
+ it('sorts High first before applying the cap', () => {
27
+ const html = renderToString(<AuditBadgeTooltip messages={msgs} max={1} />)
28
+ expect(html).toContain('Ingress references missing Service')
29
+ expect(html).not.toContain('Service selector matches no pods')
30
+ })
31
+
26
32
  it('shows the click hint by default and omits it when disabled', () => {
27
33
  expect(renderToString(<AuditBadgeTooltip messages={msgs.slice(0, 1)} />)).toContain('Click to open')
28
34
  expect(renderToString(<AuditBadgeTooltip messages={msgs.slice(0, 1)} clickHint={false} />)).not.toContain('Click to open')
@@ -1,8 +1,9 @@
1
1
  import { ShieldAlert, AlertTriangle } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
- import { SEVERITY_TEXT } from '../../utils/badge-colors'
3
+ import { SEVERITY_TEXT_CLASS } from '../checks/severity'
4
4
 
5
5
  export interface AuditBadgeMessage {
6
+ /** Raw detector compatibility value: danger renders as High, warning as Medium. */
6
7
  severity: string
7
8
  message: string
8
9
  }
@@ -17,21 +18,23 @@ interface AuditBadgeTooltipProps {
17
18
 
18
19
  /**
19
20
  * Inline tooltip body for an audit badge: lists the actual finding messages
20
- * (danger-first) so the operator reads WHAT is wrong on hover, instead of a
21
+ * (High-first) so the operator reads WHAT is wrong on hover, instead of a
21
22
  * content-free "N findings". Shared by the resource-list and topology-node
22
23
  * badges so the two can't drift.
23
24
  */
24
25
  export function AuditBadgeTooltip({ messages, max = 3, clickHint = true }: AuditBadgeTooltipProps) {
25
- const shown = messages.slice(0, max)
26
+ const shown = [...messages]
27
+ .sort((a, b) => Number(b.severity === 'danger') - Number(a.severity === 'danger'))
28
+ .slice(0, max)
26
29
  const overflow = messages.length - shown.length
27
30
  return (
28
31
  <div className="flex flex-col gap-1 text-left">
29
32
  {shown.map((m, i) => {
30
- const isDanger = m.severity === 'danger'
31
- const Icon = isDanger ? ShieldAlert : AlertTriangle
33
+ const isHigh = m.severity === 'danger'
34
+ const Icon = isHigh ? ShieldAlert : AlertTriangle
32
35
  return (
33
36
  <div key={i} className="flex items-start gap-1.5">
34
- <Icon className={clsx('w-3 h-3 shrink-0 mt-0.5', isDanger ? SEVERITY_TEXT.error : SEVERITY_TEXT.warning)} />
37
+ <Icon className={clsx('w-3 h-3 shrink-0 mt-0.5', isHigh ? SEVERITY_TEXT_CLASS.high : SEVERITY_TEXT_CLASS.medium)} />
35
38
  <span>{m.message}</span>
36
39
  </div>
37
40
  )
@@ -1,11 +1,14 @@
1
1
  import { ClipboardCheck, ArrowRight, Check } from 'lucide-react'
2
2
  import { clsx } from 'clsx'
3
3
  import { SEVERITY_TEXT, SEVERITY_DOT } from '../../utils/badge-colors'
4
+ import { SEVERITY_FILL_CLASS, SEVERITY_TEXT_CLASS } from '../checks/severity'
4
5
 
5
6
  export interface AuditCardData {
6
7
  passing: number
8
+ /** Raw compatibility counts; warning renders as Medium and danger as High. */
7
9
  warning: number
8
10
  danger: number
11
+ /** Nested warning/danger counts follow the same Medium/High mapping. */
9
12
  categories: Record<string, { passing: number; warning: number; danger: number }>
10
13
  }
11
14
 
@@ -14,20 +17,19 @@ interface AuditCardProps {
14
17
  onNavigate: () => void
15
18
  }
16
19
 
17
- type SeverityLevel = 'success' | 'warning' | 'error'
20
+ type SeverityLevel = 'success' | 'high' | 'medium'
18
21
 
19
22
  function getSeverityLevel(data: AuditCardData): SeverityLevel {
20
23
  if (data.warning + data.danger === 0) return 'success'
21
- const dangerRatio = data.danger / (data.warning + data.danger)
22
- if (dangerRatio > 0.2) return 'error'
23
- return 'warning'
24
+ const highRatio = data.danger / (data.warning + data.danger)
25
+ if (highRatio > 0.2) return 'high'
26
+ return 'medium'
24
27
  }
25
28
 
26
- // Card-specific accent backgrounds (light opacity variants, work in both themes)
27
29
  const ACCENT_BG: Record<SeverityLevel, string> = {
28
30
  success: 'bg-green-500/10',
29
- warning: 'bg-yellow-500/10',
30
- error: 'bg-red-500/10',
31
+ high: 'bg-orange-500/10',
32
+ medium: 'bg-yellow-500/10',
31
33
  }
32
34
 
33
35
  export function AuditCard({ data, onNavigate }: AuditCardProps) {
@@ -35,7 +37,7 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
35
37
  const issueCount = data.warning + data.danger
36
38
  const allPassing = issueCount === 0
37
39
  const level = getSeverityLevel(data)
38
- const accentColor = SEVERITY_TEXT[level]
40
+ const accentColor = level === 'success' ? SEVERITY_TEXT.success : SEVERITY_TEXT_CLASS[level]
39
41
  const accentBg = ACCENT_BG[level]
40
42
 
41
43
  return (
@@ -77,10 +79,10 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
77
79
  <div className="flex-1 h-3 rounded-full overflow-hidden bg-theme-hover flex">
78
80
  <div className={clsx('h-full', SEVERITY_DOT.success)} style={{ width: `${(data.passing / total) * 100}%` }} />
79
81
  {data.warning > 0 && (
80
- <div className={clsx('h-full', SEVERITY_DOT.warning)} style={{ width: `${(data.warning / total) * 100}%` }} />
82
+ <div className={clsx('h-full', SEVERITY_FILL_CLASS.medium)} style={{ width: `${(data.warning / total) * 100}%` }} />
81
83
  )}
82
84
  {data.danger > 0 && (
83
- <div className={clsx('h-full', SEVERITY_DOT.error)} style={{ width: `${(data.danger / total) * 100}%` }} />
85
+ <div className={clsx('h-full', SEVERITY_FILL_CLASS.high)} style={{ width: `${(data.danger / total) * 100}%` }} />
84
86
  )}
85
87
  </div>
86
88
  </div>
@@ -90,7 +92,7 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
90
92
  <div className="grid grid-cols-1 gap-y-2 mt-4 w-full">
91
93
  {Object.entries(data.categories).map(([category, counts]) => {
92
94
  const catIssues = counts.warning + counts.danger
93
- const dotColor = counts.danger > 0 ? SEVERITY_DOT.error : counts.warning > 0 ? SEVERITY_DOT.warning : SEVERITY_DOT.success
95
+ const dotColor = counts.danger > 0 ? SEVERITY_FILL_CLASS.high : counts.warning > 0 ? SEVERITY_FILL_CLASS.medium : SEVERITY_DOT.success
94
96
  return (
95
97
  <div key={category} className="flex items-center gap-2">
96
98
  <span className={clsx('w-2 h-2 rounded-full shrink-0', dotColor)} />
@@ -98,10 +100,10 @@ export function AuditCard({ data, onNavigate }: AuditCardProps) {
98
100
  {catIssues > 0 ? (
99
101
  <div className="flex items-center gap-2">
100
102
  {counts.danger > 0 && (
101
- <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.error)}>{counts.danger} critical</span>
103
+ <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT_CLASS.high)}>{counts.danger} high</span>
102
104
  )}
103
105
  {counts.warning > 0 && (
104
- <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.warning)}>{counts.warning} warning</span>
106
+ <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT_CLASS.medium)}>{counts.warning} medium</span>
105
107
  )}
106
108
  </div>
107
109
  ) : (
@@ -2,19 +2,21 @@ import { useState, useMemo, useRef, useEffect, type Dispatch, type SetStateActio
2
2
  import { ShieldAlert, AlertTriangle, ChevronRight, CheckCircle2, ExternalLink, MoreHorizontal, EyeOff, Layers } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import type { AuditFinding } from './AuditAlerts'
5
- import { SEVERITY_TEXT, BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
5
+ import { BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
6
6
  import { EmptyState } from '../ui/EmptyState'
7
7
  import { SearchBox } from '../ui/SearchBox'
8
8
  import { FilterPill } from '../ui/FilterPill'
9
9
  import { pluralize } from '../../utils/pluralize'
10
+ import { SEVERITY_TEXT_CLASS } from '../checks/severity'
10
11
 
11
12
  const CATEGORIES = ['Security', 'Reliability', 'Efficiency'] as const
12
- const SEVERITIES = ['danger', 'warning'] as const
13
+ const RAW_SEVERITIES = ['danger', 'warning'] as const
13
14
 
14
15
  export interface ResourceGroup {
15
16
  kind: string
16
17
  namespace: string
17
18
  name: string
19
+ /** Raw compatibility counts; warning renders as Medium and danger as High. */
18
20
  warning: number
19
21
  danger: number
20
22
  findings: AuditFinding[]
@@ -94,8 +96,8 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
94
96
  return findings ?? []
95
97
  }, [groups, findings])
96
98
 
97
- const totalDangerCount = allFindings.filter(f => f.severity === 'danger').length
98
- const totalWarningCount = allFindings.filter(f => f.severity === 'warning').length
99
+ const totalHighCount = allFindings.filter(f => f.severity === 'danger').length
100
+ const totalMediumCount = allFindings.filter(f => f.severity === 'warning').length
99
101
 
100
102
  // Derive available frameworks from checks metadata
101
103
  const frameworks = useMemo(() => {
@@ -166,8 +168,8 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
166
168
  const filteredAllFindings = filteredGroups
167
169
  ? filteredGroups.flatMap(g => g.findings)
168
170
  : filteredFindings ?? []
169
- const dangerCount = hasActiveFilters ? filteredAllFindings.filter(f => f.severity === 'danger').length : totalDangerCount
170
- const warningCount = hasActiveFilters ? filteredAllFindings.filter(f => f.severity === 'warning').length : totalWarningCount
171
+ const highCount = hasActiveFilters ? filteredAllFindings.filter(f => f.severity === 'danger').length : totalHighCount
172
+ const mediumCount = hasActiveFilters ? filteredAllFindings.filter(f => f.severity === 'warning').length : totalMediumCount
171
173
 
172
174
  // Group resources by namespace when enabled
173
175
  const namespacedGroups = useMemo(() => {
@@ -181,9 +183,9 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
181
183
  }
182
184
  // Sort namespaces: most severe first
183
185
  return Array.from(nsMap.entries()).sort((a, b) => {
184
- const aDanger = a[1].reduce((n, g) => n + g.danger, 0)
185
- const bDanger = b[1].reduce((n, g) => n + g.danger, 0)
186
- if (aDanger !== bDanger) return bDanger - aDanger
186
+ const aHigh = a[1].reduce((n, g) => n + g.danger, 0)
187
+ const bHigh = b[1].reduce((n, g) => n + g.danger, 0)
188
+ if (aHigh !== bHigh) return bHigh - aHigh
187
189
  return a[0].localeCompare(b[0])
188
190
  })
189
191
  }, [groupByNS, filteredGroups])
@@ -231,8 +233,8 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
231
233
  <div className="flex flex-col gap-2 px-4 py-3 border-b border-theme-border bg-theme-base rounded-xl shrink-0">
232
234
  {/* Row 1: Counts + Search + View toggle */}
233
235
  <div className="flex items-center gap-4">
234
- <SummaryBadge label="Critical" count={dangerCount} color={SEVERITY_TEXT.error} />
235
- <SummaryBadge label="Warning" count={warningCount} color={SEVERITY_TEXT.warning} />
236
+ <SummaryBadge label="High" count={highCount} color={SEVERITY_TEXT_CLASS.high} />
237
+ <SummaryBadge label="Medium" count={mediumCount} color={SEVERITY_TEXT_CLASS.medium} />
236
238
 
237
239
  <SearchBox value={searchTerm} onChange={setSearchTerm} scope="audit" shortcutId="audit-search" className="w-64" />
238
240
 
@@ -264,12 +266,12 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
264
266
  <FilterPill key={cat} label={cat} active={categoryFilter.has(cat)} onClick={() => toggleInSet(setCategoryFilter, cat)} />
265
267
  ))}
266
268
  <span className="w-px h-5 bg-theme-border mx-2" />
267
- {SEVERITIES.map(sev => (
269
+ {RAW_SEVERITIES.map(sev => (
268
270
  <FilterPill
269
271
  key={sev}
270
- label={sev === 'danger' ? 'Critical' : 'Warning'}
272
+ label={sev === 'danger' ? 'High' : 'Medium'}
271
273
  active={severityFilter.has(sev)}
272
- tone={sev === 'danger' ? 'danger' : 'warn'}
274
+ tone={sev === 'danger' ? 'high' : 'medium'}
273
275
  onClick={() => toggleInSet(setSeverityFilter, sev)}
274
276
  />
275
277
  ))}
@@ -318,8 +320,8 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
318
320
  <div className="flex flex-col gap-1">
319
321
  {namespacedGroups.map(([ns, nsGroups]) => {
320
322
  const nsExpanded = expandedNS.has(ns)
321
- const nsDanger = nsGroups.reduce((n, g) => n + g.danger, 0)
322
- const nsWarning = nsGroups.reduce((n, g) => n + g.warning, 0)
323
+ const nsHigh = nsGroups.reduce((n, g) => n + g.danger, 0)
324
+ const nsMedium = nsGroups.reduce((n, g) => n + g.warning, 0)
323
325
  return (
324
326
  <div key={ns}>
325
327
  <div
@@ -338,8 +340,8 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
338
340
  <span className="text-xs text-theme-text-tertiary">{pluralize(nsGroups.length, 'resource')}</span>
339
341
  <span className="flex-1" />
340
342
  <div className="flex items-center gap-3 shrink-0">
341
- {nsDanger > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.error)}>{nsDanger} critical</span>}
342
- {nsWarning > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.warning)}>{nsWarning} warning</span>}
343
+ {nsHigh > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT_CLASS.high)}>{nsHigh} high</span>}
344
+ {nsMedium > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT_CLASS.medium)}>{nsMedium} medium</span>}
343
345
  </div>
344
346
  {onHideNamespace && ns !== '(cluster-scoped)' && (
345
347
  <ContextMenu items={[{ label: `Hide ${ns} namespace`, onClick: () => onHideNamespace(ns) }]} />
@@ -392,7 +394,7 @@ function FindingDetail({ finding, meta, onHideCheck, onHideCategory }: {
392
394
  onHideCheck?: (checkID: string, title: string) => void
393
395
  onHideCategory?: (category: string) => void
394
396
  }) {
395
- const isDanger = finding.severity === 'danger'
397
+ const isHigh = finding.severity === 'danger'
396
398
  const menuItems: ContextMenuItem[] = []
397
399
  if (onHideCheck) {
398
400
  menuItems.push({ label: `Hide "${meta?.title || finding.checkID}" check`, onClick: () => onHideCheck(finding.checkID, meta?.title || finding.checkID) })
@@ -404,10 +406,10 @@ function FindingDetail({ finding, meta, onHideCheck, onHideCategory }: {
404
406
  return (
405
407
  <div className="flex flex-col gap-0.5 px-3 py-2 rounded group/finding">
406
408
  <div className="flex items-center gap-3">
407
- {isDanger ? (
408
- <ShieldAlert className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT.error)} />
409
+ {isHigh ? (
410
+ <ShieldAlert className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT_CLASS.high)} />
409
411
  ) : (
410
- <AlertTriangle className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT.warning)} />
412
+ <AlertTriangle className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT_CLASS.medium)} />
411
413
  )}
412
414
  <span className="text-sm text-theme-text-primary flex-1 min-w-0">{finding.message}</span>
413
415
  <span className={clsx('badge-sm text-[10px]', BP_CATEGORY_BADGE[finding.category] || DEFAULT_BADGE_COLOR)}>
@@ -438,7 +440,7 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
438
440
  }) {
439
441
  const key = `${g.kind}/${g.namespace}/${g.name}`
440
442
  const isExpanded = expanded.has(key)
441
- const hasDanger = g.danger > 0
443
+ const hasHigh = g.danger > 0
442
444
 
443
445
  return (
444
446
  <div>
@@ -454,10 +456,10 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
454
456
  className="group flex items-center gap-3 w-full px-4 py-2.5 rounded-lg hover:bg-theme-hover/50 transition-colors text-left cursor-pointer focus-visible:ring-2 focus-visible:ring-theme-text-primary/20 focus-visible:outline-none"
455
457
  >
456
458
  <ChevronRight className={clsx('w-3.5 h-3.5 text-theme-text-tertiary shrink-0 transition-transform duration-200', isExpanded && 'rotate-90')} />
457
- {hasDanger ? (
458
- <ShieldAlert className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT.error)} />
459
+ {hasHigh ? (
460
+ <ShieldAlert className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT_CLASS.high)} />
459
461
  ) : (
460
- <AlertTriangle className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT.warning)} />
462
+ <AlertTriangle className={clsx('w-4 h-4 shrink-0', SEVERITY_TEXT_CLASS.medium)} />
461
463
  )}
462
464
  <span className="text-xs text-theme-text-tertiary shrink-0">{g.kind}</span>
463
465
  {onResourceClick ? (
@@ -476,8 +478,8 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
476
478
  )}
477
479
  <span className="flex-1" />
478
480
  <div className="flex items-center gap-3 shrink-0">
479
- {g.danger > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.error)}>{g.danger} critical</span>}
480
- {g.warning > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.warning)}>{g.warning} warning</span>}
481
+ {g.danger > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT_CLASS.high)}>{g.danger} high</span>}
482
+ {g.warning > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT_CLASS.medium)}>{g.warning} medium</span>}
481
483
  </div>
482
484
  {showNamespace && onHideNamespace && g.namespace && (
483
485
  <ContextMenu items={[{ label: `Hide ${g.namespace} namespace`, onClick: () => onHideNamespace(g.namespace) }]} />
@@ -500,12 +502,12 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
500
502
  }
501
503
 
502
504
  function FlatFindingRow({ finding, onResourceClick, showCluster, onClusterClick }: { finding: AuditFinding; onResourceClick?: (kind: string, namespace: string, name: string) => void; showCluster?: boolean; onClusterClick?: (clusterId: string) => void }) {
503
- const isDanger = finding.severity === 'danger'
504
- const severityColor = isDanger ? SEVERITY_TEXT.error : SEVERITY_TEXT.warning
505
+ const isHigh = finding.severity === 'danger'
506
+ const severityColor = isHigh ? SEVERITY_TEXT_CLASS.high : SEVERITY_TEXT_CLASS.medium
505
507
 
506
508
  return (
507
509
  <div className="flex items-center gap-3 px-4 py-2.5 rounded-lg hover:bg-theme-hover/50 transition-colors">
508
- {isDanger ? (
510
+ {isHigh ? (
509
511
  <ShieldAlert className={clsx('w-4 h-4 shrink-0', severityColor)} />
510
512
  ) : (
511
513
  <AlertTriangle className={clsx('w-4 h-4 shrink-0', severityColor)} />
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { AuditAlerts, type AuditFinding } from './AuditAlerts'
4
+ import { AuditCard } from './AuditCard'
5
+ import { AuditFindingsTable } from './AuditFindingsTable'
6
+
7
+ const findings: AuditFinding[] = [
8
+ {
9
+ kind: 'Deployment',
10
+ namespace: 'prod',
11
+ name: 'api',
12
+ checkID: 'privileged',
13
+ category: 'Security',
14
+ severity: 'danger',
15
+ message: 'Container is privileged',
16
+ },
17
+ {
18
+ kind: 'Deployment',
19
+ namespace: 'prod',
20
+ name: 'api',
21
+ checkID: 'cpuRequestMissing',
22
+ category: 'Efficiency',
23
+ severity: 'warning',
24
+ message: 'CPU request is missing',
25
+ },
26
+ ]
27
+
28
+ describe('legacy audit severity presentation', () => {
29
+ it('labels raw summary counts as High and Medium', () => {
30
+ const html = renderToString(
31
+ <AuditCard
32
+ data={{
33
+ passing: 3,
34
+ danger: 1,
35
+ warning: 2,
36
+ categories: {
37
+ Security: { passing: 1, danger: 1, warning: 0 },
38
+ Efficiency: { passing: 2, danger: 0, warning: 2 },
39
+ },
40
+ }}
41
+ onNavigate={() => {}}
42
+ />,
43
+ )
44
+ expect(html).toMatch(/1(?:<!-- -->)? high/)
45
+ expect(html).toMatch(/2(?:<!-- -->)? medium/)
46
+ expect(html).toContain('bg-orange-500/10')
47
+ expect(html).not.toContain('critical')
48
+ })
49
+
50
+ it('uses High and Medium in resource audit summaries', () => {
51
+ const html = renderToString(<AuditAlerts findings={findings} />)
52
+ expect(html).toMatch(/1(?:<!-- -->)? high/)
53
+ expect(html).toMatch(/1(?:<!-- -->)? medium/)
54
+ expect(html).not.toContain('critical')
55
+ })
56
+
57
+ it('uses High and Medium in audit totals and filters', () => {
58
+ const html = renderToString(<AuditFindingsTable findings={findings} />)
59
+ expect(html).toContain('High')
60
+ expect(html).toContain('Medium')
61
+ expect(html).not.toContain('Critical')
62
+ expect(html).not.toContain('Warning')
63
+ })
64
+ })
@@ -7,12 +7,12 @@
7
7
  //
8
8
  // Mirrors Radar OSS's resource-key convention (radar/pkg/audit.ResourceKey).
9
9
 
10
- /** Canonical Checks severity ladder distinct from the raw detector severity
11
- * (danger/warning) so operational criticality and compliance risk stay
12
- * separate axes. */
10
+ /** Canonical user-facing Checks severity ladder. Live issues remain a separate
11
+ * operational-health axis even where the same severity word appears. */
13
12
  export type CheckSeverity = 'critical' | 'high' | 'medium' | 'low';
14
13
 
15
- /** Raw detector severity Radar emits. */
14
+ /** Raw detector values retained by existing audit transport/public-prop
15
+ * compatibility contracts. Checks presentation maps them before display. */
16
16
  export type RadarSeverity = 'danger' | 'warning';
17
17
 
18
18
  /** Ordered worst→least, for rendering severity filters/sorts consistently. */
@@ -128,6 +128,10 @@ export interface IssueRecentChange {
128
128
  timestamp: string;
129
129
  change_category?: 'spec_config' | 'lifecycle' | 'runtime_status' | string;
130
130
  rank_reason?: string;
131
+ /** Ranking hint for workload runtime configuration (including the image) or directly consumed ConfigMap data; not a causal claim. */
132
+ application_configuration_change?: boolean;
133
+ /** Set only on top-level recent_changes in an eligible, unfiltered issues response with complete linkage evidence. */
134
+ not_linked_to_returned_issues?: boolean;
131
135
  fields?: IssueRecentChangeField[];
132
136
  /** Workloads that mount/reference this ConfigMap directly ("Deployment/flagd").
133
137
  * Direct spec references only — runtime consumers via an intermediary