@skyhook-io/k8s-ui 1.17.0 → 1.18.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.
Files changed (250) hide show
  1. package/README.md +41 -0
  2. package/package.json +6 -6
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +7 -3
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +142 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -81
  67. package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
  68. package/src/components/gitops/insights/insights-helpers.ts +7 -0
  69. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
  70. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
  71. package/src/components/gitops/tree/layout.test.ts +111 -0
  72. package/src/components/gitops/tree/layout.ts +128 -0
  73. package/src/components/gitops/tree/tree-helpers.test.ts +23 -0
  74. package/src/components/gitops/tree/tree-helpers.ts +12 -2
  75. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  76. package/src/components/issues/IssuesView.tsx +44 -6
  77. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  78. package/src/components/issues/ResourceIssuesSection.tsx +8 -2
  79. package/src/components/issues/diagnostic.ts +2 -0
  80. package/src/components/issues/index.ts +3 -1
  81. package/src/components/issues/issue-timing.ts +12 -1
  82. package/src/components/issues/issues.test.ts +75 -1
  83. package/src/components/issues/severity.ts +30 -2
  84. package/src/components/issues/types.ts +55 -4
  85. package/src/components/logs/LogCore.theme.test.tsx +206 -3
  86. package/src/components/logs/LogCore.tsx +180 -53
  87. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  88. package/src/components/logs/LogsViewer.test.tsx +369 -0
  89. package/src/components/logs/LogsViewer.tsx +91 -13
  90. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  91. package/src/components/logs/StructuredLogLine.tsx +23 -31
  92. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  93. package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
  94. package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
  95. package/src/components/logs/useLogBuffer.test.ts +8 -0
  96. package/src/components/logs/useLogBuffer.ts +48 -18
  97. package/src/components/logs/useLogSearch.ts +3 -1
  98. package/src/components/logs/useLogStream.ts +17 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ColumnPickerList.test.tsx +46 -0
  105. package/src/components/resources/ColumnPickerList.tsx +174 -0
  106. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  107. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  108. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  109. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  110. package/src/components/resources/ResourcesView.kind-view.test.tsx +296 -0
  111. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  112. package/src/components/resources/ResourcesView.tsx +392 -181
  113. package/src/components/resources/column-order.test.ts +104 -0
  114. package/src/components/resources/column-order.ts +64 -0
  115. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  116. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
  117. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
  118. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  119. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  120. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  121. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  122. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  123. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  124. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  127. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  128. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  129. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  130. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  131. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  132. package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
  133. package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
  134. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  135. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  136. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
  137. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  138. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  139. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  140. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  141. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  142. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  143. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  144. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  145. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  146. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  147. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  148. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  149. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  150. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  151. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  152. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  153. package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
  154. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  155. package/src/components/resources/renderers/PodRenderer.tsx +81 -40
  156. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  157. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  158. package/src/components/resources/resource-utils-capi.ts +1 -1
  159. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  160. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  161. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  162. package/src/components/resources/resource-utils-contour.ts +16 -0
  163. package/src/components/resources/resource-utils-istio.ts +12 -8
  164. package/src/components/resources/resource-utils-knative.ts +18 -0
  165. package/src/components/resources/resource-utils.ts +96 -7
  166. package/src/components/resources/rules-column-width.test.ts +1 -1
  167. package/src/components/resources/summarize-scheduler-message.test.ts +31 -0
  168. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  169. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  170. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  171. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  172. package/src/components/shared/DetailShell.test.tsx +80 -0
  173. package/src/components/shared/DetailShell.tsx +85 -10
  174. package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
  175. package/src/components/shared/DrainPlanDialog.tsx +6 -2
  176. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  177. package/src/components/shared/EditableYamlView.tsx +1 -1
  178. package/src/components/shared/ResourceActionsBar.tsx +14 -7
  179. package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
  180. package/src/components/shared/index.ts +2 -0
  181. package/src/components/timeline/DiffViewer.test.tsx +69 -0
  182. package/src/components/timeline/DiffViewer.tsx +113 -4
  183. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  184. package/src/components/timeline/shared.test.tsx +48 -0
  185. package/src/components/timeline/shared.tsx +14 -19
  186. package/src/components/timeline/timeline-filters.ts +1 -0
  187. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  188. package/src/components/topology/K8sResourceNode.tsx +44 -2
  189. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  190. package/src/components/topology/TopologyGraph.tsx +3 -71
  191. package/src/components/topology/TopologySearch.tsx +1 -0
  192. package/src/components/topology/internet-groups.test.ts +35 -0
  193. package/src/components/topology/internet-groups.ts +55 -0
  194. package/src/components/topology/layout.ts +1 -1
  195. package/src/components/ui/Badge.tsx +1 -0
  196. package/src/components/ui/FoldSection.tsx +68 -0
  197. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  198. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  199. package/src/components/ui/RefLink.tsx +20 -0
  200. package/src/components/ui/RowActionMenu.tsx +8 -3
  201. package/src/components/ui/Toast.tsx +15 -4
  202. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  203. package/src/components/ui/drawer-components.test.tsx +110 -1
  204. package/src/components/ui/drawer-components.tsx +62 -9
  205. package/src/components/ui/index.ts +4 -2
  206. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  207. package/src/components/ui/monacoRuntime.ts +14 -19
  208. package/src/components/ui/status-tone.test.ts +10 -1
  209. package/src/components/ui/status-tone.tsx +29 -0
  210. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  211. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  212. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  213. package/src/components/workload/WorkloadView.tsx +193 -71
  214. package/src/index.ts +14 -1
  215. package/src/monaco-deep.d.ts +19 -12
  216. package/src/theme/components.css +13 -0
  217. package/src/types/capacity.ts +11 -2
  218. package/src/types/core.ts +53 -2
  219. package/src/types/gitops-flux-status.test.ts +32 -1
  220. package/src/types/gitops-insights.ts +20 -2
  221. package/src/types/gitops-tree.ts +12 -0
  222. package/src/types/gitops.ts +26 -6
  223. package/src/utils/api-resources.ts +7 -0
  224. package/src/utils/application-history.ts +1 -0
  225. package/src/utils/cnpg-backup.test.ts +26 -0
  226. package/src/utils/cnpg-backup.ts +70 -0
  227. package/src/utils/format.test.ts +9 -0
  228. package/src/utils/format.ts +4 -0
  229. package/src/utils/gitops-owner.ts +3 -2
  230. package/src/utils/grant.test.ts +19 -0
  231. package/src/utils/grant.ts +35 -0
  232. package/src/utils/index.ts +4 -0
  233. package/src/utils/k8s-errors.ts +7 -7
  234. package/src/utils/log-format.test.ts +41 -0
  235. package/src/utils/log-format.ts +71 -15
  236. package/src/utils/log-level.ts +12 -8
  237. package/src/utils/navigation.test.ts +57 -0
  238. package/src/utils/navigation.ts +86 -8
  239. package/src/utils/object-reference-navigation.test.ts +43 -0
  240. package/src/utils/resource-hierarchy.test.ts +37 -0
  241. package/src/utils/resource-hierarchy.ts +15 -7
  242. package/src/utils/resource-icons.ts +2 -0
  243. package/src/utils/skeleton-yaml.test.ts +21 -0
  244. package/src/utils/skeleton-yaml.ts +16 -0
  245. package/src/utils/url-path.test.ts +35 -0
  246. package/src/utils/url-path.ts +21 -0
  247. package/src/utils/validators.test.ts +4 -13
  248. package/src/utils/validators.ts +5 -19
  249. package/src/utils/view-transition.test.ts +35 -1
  250. package/src/utils/view-transition.ts +16 -1
@@ -0,0 +1,82 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { expect, it, vi } from 'vitest'
5
+ import { IssueRow } from './IssuesView'
6
+ import type { Issue } from './types'
7
+
8
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
9
+
10
+ const condition =
11
+ 'If victoria-metrics-operator was uninstalled on purpose (and nothing else should finalize this object), remove the finalizer'
12
+ const kubectl = `kubectl patch 'vmagents.operator.victoriametrics.com' 'stuck' -n 'team' --type=json -p '[]' --dry-run=server`
13
+
14
+ const issue: Issue = {
15
+ id: 'vmagent-stuck',
16
+ severity: 'warning',
17
+ source: 'problem',
18
+ category: 'termination_stuck',
19
+ category_group: 'control_plane',
20
+ grouping_scope: 'unknown',
21
+ group: 'operator.victoriametrics.com',
22
+ kind: 'VMAgent',
23
+ namespace: 'team',
24
+ name: 'stuck',
25
+ reason: 'Terminating stuck',
26
+ cause: 'Deletion has been pending for 2h.',
27
+ action:
28
+ 'No recognized controller found for finalizer "apps.victoriametrics.com/finalizer". If victoria-metrics-operator should be running, restore it so it can resume cleanup.',
29
+ conditional_cleanup: {
30
+ condition,
31
+ agent_guidance: 'Not the next step.',
32
+ caution: 'Removing a finalizer skips the cleanup its controller would have done.',
33
+ removals: [
34
+ {
35
+ finalizer: 'apps.victoriametrics.com/finalizer',
36
+ patch_resource: { kind: 'VMAgent', name: 'stuck', dry_run: true },
37
+ kubectl,
38
+ },
39
+ ],
40
+ },
41
+ }
42
+
43
+ it('keeps a conditional cleanup closed behind its condition, apart from the next step', async () => {
44
+ const element = document.createElement('div')
45
+ const root = createRoot(element)
46
+ await act(async () => {
47
+ root.render(<IssueRow issue={issue} open onToggle={() => {}} />)
48
+ })
49
+
50
+ const toggle = [...element.querySelectorAll('button')].find((button) => button.textContent?.includes(condition))
51
+ expect(toggle, 'the toggle is labeled with the condition').toBeTruthy()
52
+ expect(toggle!.getAttribute('aria-expanded')).toBe('false')
53
+ const panel = [...element.querySelectorAll<HTMLElement>('[id]')].find((node) => node.id === toggle!.getAttribute('aria-controls'))!
54
+ expect(panel.style.gridTemplateRows).toBe('0fr')
55
+ expect(panel.firstElementChild!.hasAttribute('inert')).toBe(true)
56
+ expect(panel.textContent).toContain(kubectl)
57
+
58
+ const nextStep = [...element.querySelectorAll('section')].find((section) => section.querySelector('h4')?.textContent === 'Next step')
59
+ expect(nextStep?.textContent).toContain('restore it')
60
+ expect(nextStep?.textContent).not.toContain('kubectl')
61
+ expect(nextStep?.textContent).not.toContain('patch_resource')
62
+ expect(nextStep?.contains(toggle!)).toBe(false)
63
+
64
+ await act(async () => {
65
+ toggle!.click()
66
+ })
67
+ expect(toggle!.getAttribute('aria-expanded')).toBe('true')
68
+ expect(panel.style.gridTemplateRows).toBe('1fr')
69
+ expect(panel.firstElementChild!.hasAttribute('inert')).toBe(false)
70
+
71
+ await act(async () => root.unmount())
72
+ })
73
+
74
+ it('renders no cleanup section when the issue has none', async () => {
75
+ const element = document.createElement('div')
76
+ const root = createRoot(element)
77
+ await act(async () => {
78
+ root.render(<IssueRow issue={{ ...issue, conditional_cleanup: undefined }} open onToggle={() => {}} />)
79
+ })
80
+ expect(element.textContent).not.toContain('Conditional cleanup')
81
+ await act(async () => root.unmount())
82
+ })
@@ -1,6 +1,7 @@
1
+ import { summarizeSchedulerMessage } from '../resources/resource-utils'
1
2
  import { useMemo, useState, type ComponentType, type ReactNode } from 'react';
2
- import { AlertOctagon, AlertTriangle, ArrowRight, CircleCheck, Clock, ExternalLink, Layers, Terminal, Workflow } from 'lucide-react';
3
- import { CardBody, CardSection, ClusterName, EmptyState, KIND_CHIP_CLASS, TerminalBlock } from '../ui';
3
+ import { AlertOctagon, AlertTriangle, ArrowRight, CircleCheck, Clock, Eraser, ExternalLink, Layers, Terminal, Workflow } from 'lucide-react';
4
+ import { CardBody, CardSection, ClusterName, Disclosure, EmptyState, KIND_CHIP_CLASS, TerminalBlock } from '../ui';
4
5
  import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse';
5
6
  import { Tooltip } from '../ui/Tooltip';
6
7
  import { formatCompactAge, formatRelativeAgeTime } from '../../utils/format';
@@ -13,7 +14,7 @@ import {
13
14
  ISSUE_SEVERITY_RAIL_CLASS,
14
15
  ISSUE_SEVERITY_SOLID_CLASS,
15
16
  ISSUE_SEVERITY_TEXT_CLASS,
16
- categoryLabel,
17
+ issueTitle,
17
18
  groupBadgeClass,
18
19
  groupLabel,
19
20
  } from './severity';
@@ -24,6 +25,7 @@ import {
24
25
  subjectRef,
25
26
  type Issue,
26
27
  type IssueAffected,
28
+ type IssueConditionalCleanup,
27
29
  type IssueResourceRef,
28
30
  type IssueSeverity,
29
31
  } from './types';
@@ -168,6 +170,7 @@ export interface IssueRowProps {
168
170
  resourceHref?: (ref: IssueResourceRef) => string;
169
171
  onResourceClick?: (ref: IssueResourceRef) => void;
170
172
  as?: 'li' | 'div';
173
+ compact?: boolean;
171
174
  className?: string;
172
175
  dimmed?: boolean;
173
176
  /** Suppress the "Subject" deep-link in the expanded body — set by hosts that
@@ -195,6 +198,7 @@ export function IssueRow({
195
198
  resourceHref,
196
199
  onResourceClick,
197
200
  as = 'li',
201
+ compact = false,
198
202
  className,
199
203
  dimmed,
200
204
  hideSubject,
@@ -208,6 +212,7 @@ export function IssueRow({
208
212
  const cluster = clusterLabel?.(issue);
209
213
  const affected = affectedSummary(issue.affected);
210
214
  const { headline } = issueMessageParts(issue);
215
+ const schedulingCause = compact && issue.reason === 'Unschedulable' ? summarizeSchedulerMessage(issue.cause || issue.message || headline || '', { plain: true }) : undefined;
211
216
  const { panelId, buttonProps } = useDisclosure(open);
212
217
  const Container = as;
213
218
  const severity = normalizeIssueSeverity(issue.severity);
@@ -289,7 +294,7 @@ export function IssueRow({
289
294
 
290
295
  <div className="flex min-w-0 flex-1 flex-col gap-1">
291
296
  <div className="flex min-w-0 items-baseline gap-2">
292
- <span className="min-w-0 truncate text-sm font-medium text-theme-text-primary">{categoryLabel(issue.category)}</span>
297
+ <span className="min-w-0 truncate text-sm font-medium text-theme-text-primary">{issueTitle(issue)}</span>
293
298
  <span className={`shrink-0 self-center ${groupBadgeClass(issue.category_group)}`}>{groupLabel(issue.category_group)}</span>
294
299
  {renderBadges?.(slotCtx)}
295
300
  {/* The detector reason rides the title row while COLLAPSED so the
@@ -297,7 +302,7 @@ export function IssueRow({
297
302
  cause lives in the WHAT'S WRONG section below, so it fades out
298
303
  here (stays mounted — it's the flex-1 filler, so unmounting
299
304
  wouldn't reflow anything, but fading avoids the pop). */}
300
- {issue.reason ? (
305
+ {issue.reason && !schedulingCause ? (
301
306
  <span
302
307
  aria-hidden={open || undefined}
303
308
  className={`min-w-0 flex-1 truncate text-xs text-theme-text-tertiary transition-opacity duration-200 ${open ? 'opacity-0' : 'opacity-100'}`}
@@ -307,6 +312,7 @@ export function IssueRow({
307
312
  </span>
308
313
  ) : null}
309
314
  </div>
315
+ {schedulingCause && <div className="break-words text-xs text-theme-text-secondary">{schedulingCause}</div>}
310
316
  <div className="flex min-w-0 items-center gap-1.5 text-xs text-theme-text-tertiary">
311
317
  <span className={KIND_CHIP_CLASS}>{issue.kind}</span>
312
318
  <span className="min-w-0 truncate font-medium text-theme-text-secondary">
@@ -429,9 +435,12 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
429
435
  const partialUnknown = issue.onset_coverage?.unknown ?? 0;
430
436
  const partialOnset = Boolean(issue.first_seen && partialUnknown > 0);
431
437
  const timing = issueTimingForDisplay(issue);
438
+ // A message Radar wrote itself is not raw output, so it never lands in the
439
+ // terminal block; its cause carries the same facts.
440
+ const messageIsRaw = issue.message_source !== 'radar';
432
441
  const rawError = [
433
442
  ...new Set(
434
- [issue.raw_message ?? (issue.cause ? issue.message : undefined), detail].filter(
443
+ [issue.raw_message ?? (issue.cause && messageIsRaw ? issue.message : undefined), detail].filter(
435
444
  (s): s is string => !!s && s !== shownText,
436
445
  ),
437
446
  ),
@@ -544,6 +553,8 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
544
553
  </CardSection>
545
554
  ) : null}
546
555
 
556
+ {issue.conditional_cleanup ? <ConditionalCleanup cleanup={issue.conditional_cleanup} /> : null}
557
+
547
558
  {rawError ? (
548
559
  <CardSection icon={Terminal} label="Raw error">
549
560
  <TerminalBlock>{rawError}</TerminalBlock>
@@ -553,6 +564,33 @@ function Diagnosis({ issue, source }: { issue: Issue; source?: IssueDiagnosisSou
553
564
  );
554
565
  }
555
566
 
567
+ // A step that is right only under its condition. It stays closed behind the
568
+ // condition so the row's next step remains the default path, and it is not
569
+ // toned as a fix.
570
+ function ConditionalCleanup({ cleanup }: { cleanup: IssueConditionalCleanup }) {
571
+ return (
572
+ <CardSection icon={Eraser} label="Conditional cleanup">
573
+ <Disclosure
574
+ summary={<span className="text-sm leading-relaxed">{cleanup.condition}</span>}
575
+ summaryClassName="items-baseline text-theme-text-primary hover:text-theme-text-secondary"
576
+ >
577
+ <div className="flex flex-col gap-2 pt-2">
578
+ {cleanup.caution ? <p className="text-xs leading-relaxed text-theme-text-secondary">{cleanup.caution}</p> : null}
579
+ {cleanup.removals.map((removal) => (
580
+ <div key={removal.finalizer} className="flex flex-col gap-1.5">
581
+ <p className="text-xs text-theme-text-tertiary">
582
+ Finalizer <code className="inline-code">{removal.finalizer}</code>
583
+ </p>
584
+ <TerminalBlock label="patch_resource · dry run">{JSON.stringify(removal.patch_resource)}</TerminalBlock>
585
+ <TerminalBlock label="kubectl · server dry run">{removal.kubectl}</TerminalBlock>
586
+ </div>
587
+ ))}
588
+ </div>
589
+ </Disclosure>
590
+ </CardSection>
591
+ );
592
+ }
593
+
556
594
  function changeContextText(change: NonNullable<Issue['change_context']>): string {
557
595
  const parts = [change.when ? `Changed ${change.when} ago` : 'Changed', change.what ? change.what.replace(/_/g, ' ') : null, change.evidence].filter(Boolean);
558
596
  return parts.join(' · ');
@@ -106,3 +106,31 @@ it('keeps pod/template evidence in expanded neutral context with navigable witne
106
106
  expect(expanded).not.toContain('confidence')
107
107
  expect(expanded).not.toContain('Caused by')
108
108
  })
109
+
110
+ it('wraps a compact Pod scheduling cause separately and retains the full message in details', () => {
111
+ const raw = '0/2 nodes are available: 2 Too many pods. preemption: 0/2 nodes are available: 2 No preemption victims found for incoming pod.'
112
+ const pod: Issue = { ...issue, kind: 'Pod', reason: 'Unschedulable', cause: raw, message: raw }
113
+ const html = renderToString(<IssueRow issue={pod} compact open onToggle={() => {}} />)
114
+ expect(html).toContain('break-words text-xs text-theme-text-secondary')
115
+ expect(html).toContain('both nodes have reached their Pod limit')
116
+ expect(html).toContain(raw)
117
+ const producerMessage = '2 node(s) insufficient pods (0/2 nodes available)'
118
+ const reported = renderToString(<IssueRow issue={{ ...pod, cause: undefined, message: producerMessage }} compact open onToggle={() => {}} />)
119
+ expect(reported).toContain('both nodes have reached their Pod limit')
120
+ expect(reported).toContain(producerMessage)
121
+ const regular = renderToString(<IssueRow issue={pod} open={false} onToggle={() => {}} />)
122
+ expect(regular).not.toContain('both nodes have reached their Pod limit')
123
+ })
124
+
125
+ it.each(['Cluster', 'Deployment'])('wraps compact scheduling causes aggregated under %s', (kind) => {
126
+ const raw = '2 node(s) insufficient pods (0/2 nodes available)'
127
+ const aggregated: Issue = { ...issue, kind, reason: 'Unschedulable', cause: raw, message: raw }
128
+ const collapsed = renderToString(<IssueRow issue={aggregated} compact open={false} onToggle={() => {}} />)
129
+ expect(collapsed).toContain('<div class="break-words text-xs text-theme-text-secondary">both nodes have reached their Pod limit</div>')
130
+ expect(collapsed).not.toContain(raw)
131
+ const expanded = renderToString(<IssueRow issue={aggregated} compact open onToggle={() => {}} />)
132
+ expect(expanded).toContain(raw)
133
+ const regular = renderToString(<IssueRow issue={aggregated} open={false} onToggle={() => {}} />)
134
+ expect(regular).toContain('Unschedulable')
135
+ expect(regular).not.toContain('both nodes have reached their Pod limit')
136
+ })
@@ -1,14 +1,18 @@
1
- import { useMemo, useState } from 'react'
1
+ import { useMemo, useState, type ReactNode } from 'react'
2
2
  import { AlertTriangle } from 'lucide-react'
3
3
  import { kindToPluralWithGroup } from '../../utils/navigation'
4
- import { IssueRow } from './IssuesView'
4
+ import { IssueRow, type IssueRowSlotContext } from './IssuesView'
5
5
  import { compareIssues, subjectRef, type Issue, type IssueResourceRef } from './types'
6
6
 
7
7
  export function ResourceIssuesSection({
8
8
  issues,
9
9
  onResourceClick,
10
10
  subjectResource,
11
+ compact,
12
+ renderDetailSection,
11
13
  }: {
14
+ compact?: boolean
15
+ renderDetailSection?: (ctx: IssueRowSlotContext) => ReactNode
12
16
  issues: Issue[] | undefined
13
17
  /** When provided, related resources in a causal link become clickable. */
14
18
  onResourceClick?: (ref: IssueResourceRef) => void
@@ -36,6 +40,8 @@ export function ResourceIssuesSection({
36
40
  <IssueRow
37
41
  key={key}
38
42
  issue={issue}
43
+ compact={compact}
44
+ renderDetailSection={renderDetailSection}
39
45
  open={openId === key}
40
46
  onToggle={() => setOpenId((cur) => (cur === key ? null : key))}
41
47
  onResourceClick={onResourceClick}
@@ -52,6 +52,8 @@ export function diagnosticFactLabel(type: string): string {
52
52
  return 'Dependent pods';
53
53
  case 'admission_webhook_backend':
54
54
  return 'Admission dependency';
55
+ case 'network_policy_isolates':
56
+ return 'Affected resources';
55
57
  default:
56
58
  return type.replace(/_/g, ' ');
57
59
  }
@@ -16,7 +16,7 @@ export {
16
16
  issueSortAnchor,
17
17
  compareIssueSortAnchors,
18
18
  } from './types';
19
- export type { Issue, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagnosticContext, IssueDiagnosticFact, IssueDiagnosticConfidence, IssueDiagnosticIssueRef, IssueDiagnosticRole, IssueIncidentParent, IssueChangeContext, IssueRecentChange, IssueRecentChangeField } from './types';
19
+ export type { Issue, IssueConditionalCleanup, IssueFinalizerRemoval, IssueSeverity, IssueAffected, IssueResourceRef, IssueDiagnosticContext, IssueDiagnosticFact, IssueDiagnosticConfidence, IssueDiagnosticIssueRef, IssueDiagnosticRole, IssueIncidentParent, IssueChangeContext, IssueRecentChange, IssueRecentChangeField, IssueRunningPreviousValue, IssueWithheld } from './types';
20
20
  export {
21
21
  ISSUE_SEVERITY_LABEL,
22
22
  ISSUE_SEVERITY_BADGE_CLASS,
@@ -24,5 +24,7 @@ export {
24
24
  ISSUE_SEVERITY_FILL_CLASS,
25
25
  ISSUE_SEVERITY_RAIL_CLASS,
26
26
  categoryLabel,
27
+ issueTitle,
28
+ issueReasonTitle,
27
29
  groupLabel,
28
30
  } from './severity';
@@ -1,7 +1,7 @@
1
1
  import { isDeploymentLikeWorkloadKind } from '../../types';
2
2
  import type { Issue } from './types';
3
3
 
4
- export type IssueTimingDisplayKind = 'creation' | 'regression';
4
+ export type IssueTimingDisplayKind = 'creation' | 'regression' | 'deletion';
5
5
 
6
6
  export interface IssueTimingDisplay {
7
7
  kind: IssueTimingDisplayKind;
@@ -77,6 +77,17 @@ export function issueTiming(issue: Issue): IssueTimingDisplay | null {
77
77
  const grouped = (coverage?.known ?? 0) + (coverage?.unknown ?? 0) > 1;
78
78
  const uncertaintyMeta = onsetUncertaintyMeta(issue);
79
79
  const uncertaintyTooltip = onsetUncertaintyTooltip(issue);
80
+ // A stuck deletion is timed from its deletionTimestamp. The creation-vs-healthy
81
+ // classification only says whether the object was deleted soon after it was
82
+ // created, which is not what the age beside this chip measures.
83
+ if (issue.issue_timing_basis === 'deletion' && issue.issue_timing) {
84
+ return {
85
+ kind: 'deletion',
86
+ chip: 'since deletion',
87
+ meta: 'pending since deletion was requested',
88
+ tooltip: 'Timed from when deletion was requested; the object is still present.',
89
+ };
90
+ }
80
91
  switch (issue.issue_timing) {
81
92
  case 'started_after_resource_was_healthy': {
82
93
  if (issue.issue_timing_basis === 'owner_condition') {
@@ -2,7 +2,7 @@ import { afterEach, describe, it, expect, vi } from 'vitest'
2
2
  import { createElement } from 'react'
3
3
  import { renderToString } from 'react-dom/server'
4
4
  import { compareIssues, issueSortAnchor, subjectRef, memberRef, normalizeImagePullMessage, issueMessageParts, type Issue } from './types'
5
- import { categoryLabel, groupBadgeClass, groupLabel } from './severity'
5
+ import { categoryLabel, groupBadgeClass, groupLabel, issueTitle } from './severity'
6
6
  import { IssueRow } from './IssuesView'
7
7
  import { issueFirstSeenTitle, issueResourceCreatedTitle, issueTiming } from './issue-timing'
8
8
 
@@ -83,6 +83,8 @@ describe('category/group label fallbacks', () => {
83
83
  it('returns the mapped label, else humanizes (server-added category needs no frontend deploy)', () => {
84
84
  expect(categoryLabel('crashloop')).toBe('Crash loop')
85
85
  expect(categoryLabel('some_new_future_category')).toBe('Some new future category')
86
+ expect(issueTitle({ category: 'backup_failed', reason: 'CNPGWALArchivingFailing' })).toBe('WAL archiving failing')
87
+ expect(issueTitle({ category: 'backup_failed', reason: 'BackupFailed' })).toBe('Backup failed')
86
88
  })
87
89
  it('humanizes an unmapped group', () => {
88
90
  expect(groupLabel('runtime')).toBe('Runtime')
@@ -137,6 +139,33 @@ describe('IssueRow', () => {
137
139
  expect(html).toContain('after healthy')
138
140
  })
139
141
 
142
+ it('labels a stuck deletion by how long deletion has been pending', () => {
143
+ vi.useFakeTimers()
144
+ vi.setSystemTime(new Date('2026-06-30T12:00:00Z'))
145
+
146
+ const html = renderToString(createElement(IssueRow, {
147
+ issue: mk({
148
+ id: 'terminating',
149
+ kind: 'VMAgent',
150
+ group: 'operator.victoriametrics.com',
151
+ category: 'termination_stuck',
152
+ category_group: 'control_plane',
153
+ reason: 'Terminating stuck',
154
+ first_seen: '2026-06-30T11:30:00Z',
155
+ issue_timing: 'started_at_resource_creation',
156
+ issue_timing_basis: 'deletion',
157
+ }),
158
+ open: true,
159
+ onToggle: () => undefined,
160
+ }))
161
+
162
+ expect(html).toContain('30m')
163
+ expect(html).toContain('since deletion')
164
+ expect(html).toContain('pending since deletion was requested')
165
+ expect(html).not.toContain('since creation')
166
+ expect(html).not.toContain('first reconciliation')
167
+ })
168
+
140
169
  it('uses the category-group chip class in the queue row', () => {
141
170
  const html = renderToString(createElement(IssueRow, {
142
171
  issue: mk({ category_group: 'control_plane', kind: 'Node' }),
@@ -394,6 +423,25 @@ describe('issueTiming', () => {
394
423
  })
395
424
  })
396
425
 
426
+ it('times stuck deletions from the deletion request, however soon after creation it came', () => {
427
+ for (const timing of ['started_at_resource_creation', 'started_after_resource_was_healthy'] as const) {
428
+ for (const kind of ['Pod', 'VMAgent']) {
429
+ const display = issueTiming(mk({
430
+ kind,
431
+ category: 'termination_stuck',
432
+ issue_timing: timing,
433
+ issue_timing_basis: 'deletion',
434
+ }))
435
+ expect(display).toMatchObject({
436
+ kind: 'deletion',
437
+ chip: 'since deletion',
438
+ meta: 'pending since deletion was requested',
439
+ })
440
+ expect(`${display?.chip} ${display?.meta} ${display?.tooltip}`).not.toMatch(/creation|healthy|deploy/i)
441
+ }
442
+ }
443
+ })
444
+
397
445
  it('describes direct after-healthy timing without coupling it to first_seen', () => {
398
446
  const display = issueTiming(mk({
399
447
  first_seen: '2026-06-30T10:00:00Z',
@@ -489,4 +537,30 @@ describe('IssueRow diagnosis raw messages', () => {
489
537
  expect(html).toContain('app path does not exist')
490
538
  expect(html).toContain('rpc error: code = Unknown desc = app path does not exist')
491
539
  })
540
+
541
+ it("doesn't present Radar's own summary as a raw error", () => {
542
+ const base = {
543
+ category: 'network_policy_denied',
544
+ category_group: 'networking',
545
+ severity: 'warning' as const,
546
+ reason: 'NetworkPolicy denies all ingress',
547
+ message: 'Would block all inbound traffic to the 2 pods behind Service recommendation',
548
+ cause: 'NetworkPolicy deny-all leaves the 2 pods behind Service recommendation with no allowed inbound traffic.',
549
+ }
550
+ const radar = renderToString(createElement(IssueRow, {
551
+ issue: mk({
552
+ ...base,
553
+ message_source: 'radar',
554
+ diagnostic_context: { role: 'candidate', facts: [{ type: 'network_policy_isolates', confidence: 'high', refs: [{ kind: 'Service', namespace: 'shop', name: 'recommendation' }] }] },
555
+ }),
556
+ open: true, onToggle: () => undefined, as: 'div',
557
+ }))
558
+ expect(radar).not.toContain('Raw error')
559
+ expect(radar).toContain('leaves the 2 pods behind Service recommendation')
560
+ expect(radar).toContain('Affected resources')
561
+
562
+ // Without the marker the message is still treated as detector output.
563
+ const unmarked = renderToString(createElement(IssueRow, { issue: mk(base), open: true, onToggle: () => undefined, as: 'div' }))
564
+ expect(unmarked).toContain('Raw error')
565
+ })
492
566
  })
@@ -70,13 +70,14 @@ const CATEGORY_LABEL: Record<string, string> = {
70
70
  pdb_blocks_evictions: 'Evictions blocked',
71
71
  secret_sync_failed: 'Secret sync failed',
72
72
  service_no_endpoints: 'No endpoints',
73
+ service_mixed_backends: 'Mixed Service backends',
73
74
  ingress_backend_missing: 'Ingress backend missing',
74
75
  ingress_class_missing: 'Ingress class missing',
75
76
  load_balancer_pending: 'Load balancer pending',
76
77
  gateway_not_ready: 'Gateway not ready',
77
78
  gateway_route_invalid: 'Gateway route invalid',
78
79
  dns_failure: 'DNS failure',
79
- network_policy_block: 'Network policy block',
80
+ network_policy_denied: 'Traffic denied by NetworkPolicy',
80
81
  pvc_pending: 'PVC pending',
81
82
  pvc_lost: 'PVC lost',
82
83
  pv_failed: 'PV failed',
@@ -87,7 +88,7 @@ const CATEGORY_LABEL: Record<string, string> = {
87
88
  backup_target_unavailable: 'Backup target unavailable',
88
89
  backup_stalled: 'Backup not progressing',
89
90
  job_failed: 'Job failed',
90
- cronjob_failed: 'CronJob failed',
91
+ cronjob_failed: 'CronJob needs attention',
91
92
  rollout_stalled: 'Rollout stalled',
92
93
  hpa_limited_or_failed: 'Autoscaling limited',
93
94
  rbac_forbidden: 'Permission denied',
@@ -136,6 +137,33 @@ export function categoryLabel(category: string): string {
136
137
  return CATEGORY_LABEL[category] ?? humanize(category);
137
138
  }
138
139
 
140
+ // Reasons filed under a category whose label names a different operation:
141
+ // CNPG archiving and an unanswered scheduled run sit under backup_failed.
142
+ const REASON_TITLE: Record<string, string> = {
143
+ InvalidSchedule: 'Scheduling error reported',
144
+ UnparseableSchedule: 'Scheduling error reported',
145
+ UnParseableCronJobSchedule: 'Scheduling error reported',
146
+ UnknownTimeZone: 'Time zone error reported',
147
+ MissSchedule: 'Missed schedule reported',
148
+ TooManyMissedTimes: 'Missed schedules reported',
149
+ CNPGWALArchivingFailing: "WAL archiving failing",
150
+ CNPGLastBackupFailed: "Latest backup failed",
151
+ CNPGScheduleDestinationMissing: "Backup schedule has no destination",
152
+ CNPGInstanceReadinessMismatch: "Instance Pods contradict Cluster readiness",
153
+ CNPGPrimaryLabelMismatch: "Primary Pod label contradicts Cluster status",
154
+ CNPGScheduledRunNoBackup: "No successful backup since a scheduled run",
155
+ };
156
+
157
+ /** The title a reason has wherever it is shown, when its category's label would name the wrong operation. */
158
+ export function issueReasonTitle(reason: string | undefined): string | undefined {
159
+ return reason ? REASON_TITLE[reason] : undefined;
160
+ }
161
+
162
+ /** An issue row's title: its category, unless the reason names it better. */
163
+ export function issueTitle(issue: { category: string; reason?: string }): string {
164
+ return issueReasonTitle(issue.reason) || categoryLabel(issue.category);
165
+ }
166
+
139
167
  export function groupLabel(group: string): string {
140
168
  return GROUP_LABEL[group] ?? humanize(group);
141
169
  }
@@ -118,6 +118,23 @@ export interface IssueRecentChangeField {
118
118
  newValue?: unknown;
119
119
  }
120
120
 
121
+ /** Names the caller's access kept out of a list, counted but never named. */
122
+ export interface IssueWithheld {
123
+ count: number;
124
+ /** A readreason code; always "user_forbidden" today. */
125
+ reason: string;
126
+ }
127
+
128
+ export interface IssueRunningPreviousValue {
129
+ /** Owners the caller may read, capped at 5. */
130
+ workloads: string[] | null;
131
+ /** Owners left out because the caller may not read them. */
132
+ withheld?: IssueWithheld;
133
+ pods: number;
134
+ keys: string[];
135
+ note: string;
136
+ }
137
+
121
138
  export interface IssueRecentChange {
122
139
  source?: string;
123
140
  kind: string;
@@ -130,15 +147,39 @@ export interface IssueRecentChange {
130
147
  timestamp: string;
131
148
  change_category?: 'spec_config' | 'lifecycle' | 'runtime_status' | string;
132
149
  rank_reason?: string;
133
- /** Ranking hint for workload runtime configuration (including the image) or directly consumed ConfigMap data; not a causal claim. */
150
+ /** Ranking hint for workload runtime configuration (including the image), directly referenced ConfigMap data, or Secret writes with likely-stale env readers; not a causal claim. */
134
151
  application_configuration_change?: boolean;
135
152
  /** Set only on top-level recent_changes in an eligible, unfiltered issues response with complete linkage evidence. */
136
153
  not_linked_to_returned_issues?: boolean;
137
154
  fields?: IssueRecentChangeField[];
138
- /** Workloads that mount/reference this ConfigMap directly ("Deployment/flagd").
139
- * Direct spec references only — runtime consumers via an intermediary
140
- * service are not captured. */
155
+ /** Workloads or Pods this ConfigMap or Secret is referenced by ("Deployment/flagd"), capped at 5.
156
+ * Direct spec references only; references do not establish runtime use. */
141
157
  consumed_by?: string[];
158
+ /** Referencing workloads or Pods left out of consumed_by because the caller may not read them. */
159
+ consumed_by_withheld?: IssueWithheld;
160
+ running_previous_value?: IssueRunningPreviousValue;
161
+ updates_in_window?: number;
162
+ first_update_at?: string;
163
+ }
164
+
165
+ /** One finalizer removal: a JSON Patch that tests the object UID and the
166
+ * finalizer at its index before removing it, previewed as dry runs. */
167
+ export interface IssueFinalizerRemoval {
168
+ finalizer: string;
169
+ /** patch_resource tool input, with dry_run=true. */
170
+ patch_resource: Record<string, unknown>;
171
+ /** The same patch as a kubectl server-side dry run. */
172
+ kubectl: string;
173
+ }
174
+
175
+ export interface IssueConditionalCleanup {
176
+ /** Lead-in the reader must confirm, e.g. "If victoria-metrics-operator was
177
+ * uninstalled on purpose (…), remove the finalizer". */
178
+ condition: string;
179
+ /** For agents: when the cleanup may be proposed. */
180
+ agent_guidance: string;
181
+ caution: string;
182
+ removals: IssueFinalizerRemoval[];
142
183
  }
143
184
 
144
185
  /** "No tracked non-status changes in the window" — the claim is scoped by
@@ -181,6 +222,10 @@ export interface Issue {
181
222
  reason: string;
182
223
  message?: string;
183
224
  raw_message?: string;
225
+ /** 'radar' when `message` is Radar's own summary rather than text from
226
+ * Kubernetes or a controller. Such a message is never shown as raw output;
227
+ * the server writes a `cause` that stands without it. */
228
+ message_source?: 'radar';
184
229
  /** Parsed domain diagnosis: plain-English cause, suggested next step, and
185
230
  * an optional structured one-click fix.
186
231
  * Server-emitted (omitempty); empty for issues without a parser. */
@@ -188,6 +233,9 @@ export interface Issue {
188
233
  action?: string;
189
234
  remediation_kind?: string;
190
235
  remediation_target?: string;
236
+ /** A cleanup that is right only when its condition holds — never part of
237
+ * `action`. Shown collapsed, labeled with the condition. */
238
+ conditional_cleanup?: IssueConditionalCleanup;
191
239
  /** Controller-operation retry count (e.g. Argo's "(retried N times)") —
192
240
  * distinct from restart_count (pod restarts). stuck = not expected to
193
241
  * self-recover. */
@@ -260,6 +308,9 @@ export interface Issue {
260
308
  * NEITHER correlation field was not checked — absence must not be read as
261
309
  * "no changes". MCP-only, like correlated_changes. */
262
310
  no_recent_changes?: IssueNoRecentChanges;
311
+ /** The subject's change history in the window was larger than Radar reads,
312
+ * so correlated_changes are leads, not its full history. MCP-only. */
313
+ correlated_changes_incomplete?: boolean;
263
314
  }
264
315
 
265
316
  /** subjectRef builds a deep-linkable ref for an issue's subject — the row's