@skyhook-io/k8s-ui 1.8.5 → 1.8.7

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 (72) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/ApplicationsView.tsx +2 -0
  3. package/src/components/audit/AuditAlerts.tsx +4 -0
  4. package/src/components/audit/AuditBadgeTooltip.test.tsx +30 -0
  5. package/src/components/audit/AuditBadgeTooltip.tsx +47 -0
  6. package/src/components/audit/AuditFindingsTable.tsx +4 -0
  7. package/src/components/audit/index.ts +1 -0
  8. package/src/components/gitops/GitOpsDetailLayout.tsx +3 -3
  9. package/src/components/gitops/GitOpsStatusBadge.tsx +9 -3
  10. package/src/components/gitops/GitOpsTableView.tsx +3 -1
  11. package/src/components/issues/IssuesView.tsx +9 -36
  12. package/src/components/issues/ResourceIssuesSection.tsx +142 -0
  13. package/src/components/issues/diagnostic.ts +64 -0
  14. package/src/components/issues/index.ts +2 -1
  15. package/src/components/issues/severity.ts +10 -9
  16. package/src/components/issues/types.ts +5 -0
  17. package/src/components/resources/ResourcesView.tsx +38 -2
  18. package/src/components/resources/cron-to-human.test.ts +41 -0
  19. package/src/components/resources/get-pod-phase-display.test.ts +115 -3
  20. package/src/components/resources/get-pod-problems.test.ts +18 -0
  21. package/src/components/resources/health-golden.test.ts +66 -0
  22. package/src/components/resources/renderers/CronJobRenderer.tsx +14 -7
  23. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +4 -3
  24. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +9 -15
  25. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -11
  26. package/src/components/resources/renderers/GatewayRenderer.tsx +11 -35
  27. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +10 -16
  28. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -6
  29. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +24 -28
  30. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +19 -26
  31. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +20 -19
  32. package/src/components/resources/renderers/JobRenderer.tsx +12 -7
  33. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +2 -2
  34. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +9 -11
  35. package/src/components/resources/renderers/NodeRenderer.tsx +17 -8
  36. package/src/components/resources/renderers/PVCRenderer.tsx +7 -7
  37. package/src/components/resources/renderers/PodRenderer.tsx +28 -9
  38. package/src/components/resources/renderers/ServiceRenderer.tsx +23 -9
  39. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +6 -12
  40. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +11 -10
  41. package/src/components/resources/renderers/TraefikMiddlewareRenderer.tsx +185 -0
  42. package/src/components/resources/renderers/TraefikServersTransportRenderer.tsx +84 -0
  43. package/src/components/resources/renderers/TraefikServiceRenderer.tsx +118 -0
  44. package/src/components/resources/renderers/TraefikTLSOptionRenderer.tsx +62 -0
  45. package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -1
  46. package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -3
  47. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +66 -0
  48. package/src/components/resources/renderers/contour-cells.tsx +6 -5
  49. package/src/components/resources/renderers/index.ts +4 -0
  50. package/src/components/resources/renderers/istio-cells.tsx +16 -25
  51. package/src/components/resources/resource-utils-argo.test.ts +23 -0
  52. package/src/components/resources/resource-utils-argo.ts +5 -1
  53. package/src/components/resources/resource-utils-cnpg.ts +1 -1
  54. package/src/components/resources/resource-utils-keda.ts +17 -7
  55. package/src/components/resources/resource-utils.ts +206 -71
  56. package/src/components/shared/ResourceRendererDispatch.tsx +10 -0
  57. package/src/components/timeline/TimelineSwimlanes.tsx +1 -0
  58. package/src/components/timeline/shared.tsx +15 -4
  59. package/src/components/topology/K8sResourceNode.tsx +28 -1
  60. package/src/components/topology/TopologyControls.tsx +41 -9
  61. package/src/components/topology/layout.ts +11 -5
  62. package/src/components/ui/Badge.tsx +66 -3
  63. package/src/components/ui/ClusterName.tsx +12 -4
  64. package/src/components/ui/PaneLoader.tsx +30 -7
  65. package/src/components/ui/drawer-components.test.tsx +35 -0
  66. package/src/components/ui/drawer-components.tsx +13 -1
  67. package/src/components/workload/WorkloadView.tsx +35 -5
  68. package/src/types/core.ts +136 -2
  69. package/src/utils/applications.test.ts +55 -1
  70. package/src/utils/applications.ts +28 -7
  71. package/src/utils/asset-url.ts +13 -0
  72. package/src/utils/badge-colors.ts +7 -0
@@ -0,0 +1,62 @@
1
+ import { Lock } from 'lucide-react'
2
+ import { Section, PropertyList, Property } from '../../ui/drawer-components'
3
+ import { Badge } from '../../ui/Badge'
4
+
5
+ interface TraefikTLSOptionRendererProps {
6
+ data: any
7
+ }
8
+
9
+ export function TraefikTLSOptionRenderer({ data }: TraefikTLSOptionRendererProps) {
10
+ const spec = data.spec || {}
11
+ const ciphers: string[] = spec.cipherSuites || []
12
+ const curves: string[] = spec.curvePreferences || []
13
+ const alpn: string[] = spec.alpnProtocols || []
14
+ const clientAuth = spec.clientAuth
15
+
16
+ return (
17
+ <>
18
+ <Section title="TLS Option" icon={Lock} defaultExpanded>
19
+ <PropertyList>
20
+ <Property label="Min Version" value={spec.minVersion || 'default (VersionTLS12)'} />
21
+ {spec.maxVersion && <Property label="Max Version" value={spec.maxVersion} />}
22
+ {spec.sniStrict !== undefined && <Property label="SNI Strict" value={String(spec.sniStrict)} />}
23
+ {spec.preferServerCipherSuites !== undefined && (
24
+ <Property label="Prefer Server Ciphers" value={String(spec.preferServerCipherSuites)} />
25
+ )}
26
+ {alpn.length > 0 && <Property label="ALPN Protocols" value={alpn.join(', ')} />}
27
+ </PropertyList>
28
+ </Section>
29
+
30
+ {ciphers.length > 0 && (
31
+ <Section title={`Cipher Suites (${ciphers.length})`}>
32
+ <div className="flex flex-wrap gap-1">
33
+ {ciphers.map((c, i) => (
34
+ <Badge key={i} tone="structural" size="sm" className="font-mono">{c}</Badge>
35
+ ))}
36
+ </div>
37
+ </Section>
38
+ )}
39
+
40
+ {curves.length > 0 && (
41
+ <Section title={`Curve Preferences (${curves.length})`}>
42
+ <div className="flex flex-wrap gap-1">
43
+ {curves.map((c, i) => (
44
+ <Badge key={i} tone="structural" size="sm" className="font-mono">{c}</Badge>
45
+ ))}
46
+ </div>
47
+ </Section>
48
+ )}
49
+
50
+ {clientAuth && (
51
+ <Section title="Client Authentication" defaultExpanded>
52
+ <PropertyList>
53
+ {clientAuth.clientAuthType && <Property label="Type" value={clientAuth.clientAuthType} />}
54
+ {Array.isArray(clientAuth.secretNames) && clientAuth.secretNames.length > 0 && (
55
+ <Property label="CA Secrets" value={clientAuth.secretNames.join(', ')} />
56
+ )}
57
+ </PropertyList>
58
+ </Section>
59
+ )}
60
+ </>
61
+ )
62
+ }
@@ -48,7 +48,7 @@ function getPhaseBadgeClass(phase: string): string {
48
48
  case 'Succeeded':
49
49
  return 'status-healthy'
50
50
  case 'Running':
51
- return 'status-degraded'
51
+ return 'status-neutral'
52
52
  case 'Failed':
53
53
  case 'Error':
54
54
  return 'status-unhealthy'
@@ -1,7 +1,7 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { Server, ExternalLink, Scale, Minus, Plus, Loader2, Shield } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink, ResourceRefBadge } from '../../ui/drawer-components'
4
+ import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
5
5
  import { DialogPortal } from '../../ui/DialogPortal'
6
6
  import { Tooltip } from '../../ui/Tooltip'
7
7
  import { Badge, type BadgeSeverity } from '../../ui/Badge'
@@ -165,8 +165,11 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
165
165
  }
166
166
  }, [spec.replicas, scaledTo])
167
167
 
168
- // Check for problems and progress
169
- const problems = getWorkloadProblems(status, spec, kind)
168
+ // Check for problems and progress. Suppressed when the dedicated Operational
169
+ // Issues section is shown — it carries the workload's own issues plus its pods'
170
+ // (richer, with cause/action), so the workload-status problems would duplicate.
171
+ const operationalIssuesShown = useOperationalIssuesShown()
172
+ const problems = operationalIssuesShown ? [] : getWorkloadProblems(status, spec, kind)
170
173
  const hasProblems = problems.length > 0
171
174
  const progressMessage = getWorkloadProgress(status, spec, kind)
172
175
 
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { readdirSync, readFileSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ // Ratchet guard for the badge token system (see DESIGN.md "Badges").
6
+ //
7
+ // Hand-rolled chip color strings (`bg-<color>-N/NN text-<color>-N`) bypass the
8
+ // theme and overload hues across intents. The migration to <Badge> is
9
+ // incremental: this test pins the set of renderer files that STILL contain
10
+ // hand-rolled chips. The set may SHRINK (clean a file → remove it here) but must
11
+ // never GROW — a new renderer must use <Badge>/tokens, not literal colors.
12
+ //
13
+ // When this fails because you cleaned a file: delete it from BASELINE.
14
+ // When it fails because you added a chip: use <Badge severity|kind|protocol|tone>
15
+ // instead (DESIGN.md has the decision tree).
16
+
17
+ const CHIP_RE =
18
+ /bg-(red|green|blue|yellow|orange|amber|purple|cyan|pink|indigo|emerald|teal|violet|rose|lime|sky)-[0-9]+\/[0-9]+ text-(red|green|blue|yellow|orange|amber|purple|cyan|pink|indigo|emerald|teal|violet|rose|lime|sky)-[0-9]+/
19
+
20
+ // Renderer files that still contain hand-rolled chips, pending migration.
21
+ // Ratchet: this list only shrinks. Do not add to it.
22
+ const BASELINE = new Set<string>([
23
+ 'AlertRenderer.tsx', 'ArgoApplicationRenderer.tsx', 'CertificateRenderer.tsx',
24
+ 'CertificateRequestRenderer.tsx', 'ChallengeRenderer.tsx', 'CiliumNetworkPolicyRenderer.tsx',
25
+ 'ClusterExternalSecretRenderer.tsx', 'ClusterIssuerRenderer.tsx', 'ClusterNetworkPolicyRenderer.tsx',
26
+ 'cnpg-cells.tsx', 'CNPGClusterRenderer.tsx', 'EventRenderer.tsx', 'GatewayRenderer.tsx',
27
+ 'GenericRenderer.tsx', 'IngressClassRenderer.tsx', 'IstioPeerAuthenticationRenderer.tsx',
28
+ 'IstioServiceEntryRenderer.tsx', 'KarpenterNodeClaimRenderer.tsx', 'knative-cells.tsx',
29
+ 'KnativeEventingRenderer.tsx', 'KnativeRevisionRenderer.tsx', 'kyverno-cells.tsx',
30
+ 'KyvernoPolicyReportRenderer.tsx', 'NetworkPolicyRenderer.tsx', 'NodeRenderer.tsx',
31
+ 'OrderRenderer.tsx', 'PodRenderer.tsx', 'RoleRenderer.tsx', 'RolloutRenderer.tsx',
32
+ 'SealedSecretRenderer.tsx', 'SecretRenderer.tsx', 'SecretStoreRenderer.tsx',
33
+ 'VeleroBackupRenderer.tsx', 'VeleroRestoreRenderer.tsx', 'VeleroScheduleRenderer.tsx',
34
+ 'VPARenderer.tsx', 'VulnerabilityReportRenderer.tsx', 'WebhookConfigRenderer.tsx',
35
+ ])
36
+
37
+ const dir = fileURLToPath(new URL('.', import.meta.url))
38
+
39
+ function filesWithHandRolledChips(): string[] {
40
+ return readdirSync(dir)
41
+ .filter((f) => f.endsWith('.tsx') && !f.endsWith('.test.tsx'))
42
+ .filter((f) => CHIP_RE.test(readFileSync(`${dir}/${f}`, 'utf8')))
43
+ .sort()
44
+ }
45
+
46
+ describe('badge token system — no hand-rolled chip colors', () => {
47
+ const offenders = filesWithHandRolledChips()
48
+
49
+ it('does not introduce hand-rolled chips in new/migrated files (ratchet only shrinks)', () => {
50
+ const newOffenders = offenders.filter((f) => !BASELINE.has(f))
51
+ expect(
52
+ newOffenders,
53
+ `These renderers use hand-rolled chip colors. Use <Badge severity|kind|protocol|tone> ` +
54
+ `(see DESIGN.md "Badges"), not literal bg-/text- color strings:\n ${newOffenders.join('\n ')}`,
55
+ ).toEqual([])
56
+ })
57
+
58
+ it('baseline has no stale entries (remove files you have already cleaned)', () => {
59
+ const stale = [...BASELINE].filter((f) => !offenders.includes(f)).sort()
60
+ expect(
61
+ stale,
62
+ `These files are in BASELINE but no longer contain hand-rolled chips — ` +
63
+ `delete them from BASELINE in this test:\n ${stale.join('\n ')}`,
64
+ ).toEqual([])
65
+ })
66
+ })
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Shield } from 'lucide-react'
4
4
  import { Tooltip } from '../../ui/Tooltip'
5
+ import { Badge, type BadgeSeverity } from '../../ui/Badge'
5
6
  import {
6
7
  getHTTPProxyFQDN,
7
8
  getHTTPProxyRouteCount,
@@ -36,11 +37,11 @@ export function HTTPProxyCell({ resource, column }: { resource: any; column: str
36
37
  }
37
38
  case 'status': {
38
39
  const { label } = getHTTPProxyStatus(resource)
39
- const color = label === 'Valid' ? 'text-green-500'
40
- : label === 'Invalid' ? 'text-red-500'
41
- : label === 'Orphaned' ? 'text-yellow-500'
42
- : 'text-theme-text-secondary'
43
- return <span className={`text-sm ${color}`}>{label}</span>
40
+ const severity: BadgeSeverity = label === 'Valid' ? 'success'
41
+ : label === 'Invalid' ? 'error'
42
+ : label === 'Orphaned' ? 'warning'
43
+ : 'neutral'
44
+ return <Badge severity={severity}>{label}</Badge>
44
45
  }
45
46
  default:
46
47
  return <span className="text-sm text-theme-text-tertiary">-</span>
@@ -114,6 +114,10 @@ export * from './WorkflowRenderer'
114
114
  export * from './WorkflowTemplateRenderer'
115
115
  export * from './WorkloadRenderer'
116
116
  export * from './TraefikIngressRouteRenderer'
117
+ export * from './TraefikMiddlewareRenderer'
118
+ export * from './TraefikServiceRenderer'
119
+ export * from './TraefikTLSOptionRenderer'
120
+ export * from './TraefikServersTransportRenderer'
117
121
  export * from './traefik-cells'
118
122
  export * from './ContourHTTPProxyRenderer'
119
123
  export * from './contour-cells'
@@ -1,7 +1,7 @@
1
1
  // Istio cell components for ResourcesView table
2
2
 
3
3
  import { clsx } from 'clsx'
4
- import { BADGE_INACTIVE } from '../../../utils/badge-colors'
4
+ import { Badge, type BadgeSeverity } from '../../ui/Badge'
5
5
  import {
6
6
  getVirtualServiceStatus,
7
7
  getVirtualServiceHosts,
@@ -26,17 +26,17 @@ import {
26
26
  getAuthorizationPolicyRuleCount,
27
27
  } from '../resource-utils-istio'
28
28
 
29
- const modeColors: Record<string, string> = {
30
- STRICT: 'bg-green-500/20 text-green-400',
31
- PERMISSIVE: 'bg-yellow-500/20 text-yellow-400',
32
- DISABLE: 'bg-red-500/20 text-red-400',
29
+ const modeSeverity: Record<string, BadgeSeverity> = {
30
+ STRICT: 'success',
31
+ PERMISSIVE: 'warning',
32
+ DISABLE: 'error',
33
33
  }
34
34
 
35
- const actionColors: Record<string, string> = {
36
- ALLOW: 'bg-green-500/20 text-green-400',
37
- DENY: 'bg-red-500/20 text-red-400',
38
- CUSTOM: 'bg-blue-500/20 text-blue-400',
39
- AUDIT: 'bg-yellow-500/20 text-yellow-400',
35
+ const actionSeverity: Record<string, BadgeSeverity> = {
36
+ ALLOW: 'success',
37
+ DENY: 'error',
38
+ CUSTOM: 'info',
39
+ AUDIT: 'warning',
40
40
  }
41
41
 
42
42
  export function VirtualServiceCell({ resource, column }: { resource: any; column: string }) {
@@ -133,12 +133,9 @@ export function ServiceEntryCell({ resource, column }: { resource: any; column:
133
133
  case 'location': {
134
134
  const location = getServiceEntryLocation(resource)
135
135
  return (
136
- <span className={clsx(
137
- 'badge',
138
- location === 'MESH_EXTERNAL' ? 'bg-orange-500/20 text-orange-400' : 'bg-blue-500/20 text-blue-400'
139
- )}>
136
+ <Badge tone={location === 'MESH_EXTERNAL' ? 'accent2' : 'accent1'}>
140
137
  {location === 'MESH_EXTERNAL' ? 'External' : 'Internal'}
141
- </span>
138
+ </Badge>
142
139
  )
143
140
  }
144
141
  case 'ports': {
@@ -163,12 +160,9 @@ export function PeerAuthenticationCell({ resource, column }: { resource: any; co
163
160
  case 'mode': {
164
161
  const mode = getPeerAuthenticationMode(resource)
165
162
  return (
166
- <span className={clsx(
167
- 'badge',
168
- modeColors[mode] || BADGE_INACTIVE
169
- )}>
163
+ <Badge severity={modeSeverity[mode] ?? 'neutral'}>
170
164
  {mode}
171
- </span>
165
+ </Badge>
172
166
  )
173
167
  }
174
168
  case 'selector': {
@@ -193,12 +187,9 @@ export function AuthorizationPolicyCell({ resource, column }: { resource: any; c
193
187
  case 'action': {
194
188
  const action = getAuthorizationPolicyAction(resource)
195
189
  return (
196
- <span className={clsx(
197
- 'badge',
198
- actionColors[action] || BADGE_INACTIVE
199
- )}>
190
+ <Badge severity={actionSeverity[action] ?? 'neutral'}>
200
191
  {action}
201
- </span>
192
+ </Badge>
202
193
  )
203
194
  }
204
195
  case 'rules': {
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getArgoApplicationStatus } from './resource-utils-argo'
3
+
4
+ describe('getArgoApplicationStatus', () => {
5
+ // A Suspended Argo app is intentionally paused — neutral (sky), matching the
6
+ // backend rollup (mapArgoHealth) + the GitOps badge, so it doesn't read amber
7
+ // on the resource table while reading Idle in Applications.
8
+ it('maps health Suspended to neutral (sky), not degraded', () => {
9
+ const badge = getArgoApplicationStatus({ status: { health: { status: 'Suspended' }, sync: { status: 'Synced' } } })
10
+ expect(badge.level).toBe('neutral')
11
+ expect(badge.text).toBe('Suspended')
12
+ })
13
+
14
+ it('still maps a healthy synced app to healthy', () => {
15
+ const badge = getArgoApplicationStatus({ status: { health: { status: 'Healthy' }, sync: { status: 'Synced' } } })
16
+ expect(badge.level).toBe('healthy')
17
+ })
18
+
19
+ it('still maps a degraded app to unhealthy', () => {
20
+ const badge = getArgoApplicationStatus({ status: { health: { status: 'Degraded' }, sync: { status: 'Synced' } } })
21
+ expect(badge.level).toBe('unhealthy')
22
+ })
23
+ })
@@ -22,7 +22,11 @@ export function getArgoApplicationStatus(app: any): StatusBadge {
22
22
  const annotations = app.metadata?.annotations
23
23
  const suspendedByRadar = annotations?.['radarhq.io/suspended-prune'] || annotations?.['skyhook.io/suspended-prune']
24
24
  if (health === 'Suspended' || (!hasAutomatedSync && suspendedByRadar)) {
25
- return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
25
+ // Suspended = an operator deliberately paused this app — intentional, not a
26
+ // degradation. Neutral (sky), matching the backend rollup (mapArgoHealth) so a
27
+ // suspended app reads the same Idle tone in Applications, the resource table,
28
+ // and GitOps instead of amber in some surfaces and sky in others.
29
+ return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
26
30
  }
27
31
 
28
32
  // Operation in progress
@@ -283,7 +283,7 @@ export function getCNPGScheduledBackupStatus(resource: any): StatusBadge {
283
283
  const isSuspended = resource.spec?.suspend === true
284
284
 
285
285
  if (isSuspended) {
286
- return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
286
+ return { text: 'Suspended', color: healthColors.neutral, level: 'neutral' }
287
287
  }
288
288
 
289
289
  // If we have a last schedule time, it's active
@@ -29,7 +29,9 @@ export function getScaledObjectStatus(resource: any): StatusBadge {
29
29
  conditions.some((c: any) => c.type === 'Paused' && c.status === 'True')
30
30
 
31
31
  if (isPaused) {
32
- return { text: 'Paused', color: healthColors.degraded, level: 'degraded' }
32
+ // Paused = operator deliberately froze autoscaling — intentional, sky/neutral
33
+ // (like Idle), not amber.
34
+ return { text: 'Paused', color: healthColors.neutral, level: 'neutral' }
33
35
  }
34
36
 
35
37
  // Check Fallback condition
@@ -50,7 +52,9 @@ export function getScaledObjectStatus(resource: any): StatusBadge {
50
52
  return { text: 'Active', color: healthColors.healthy, level: 'healthy' }
51
53
  }
52
54
  if (activeCond?.status === 'False') {
53
- return { text: 'Idle', color: healthColors.degraded, level: 'degraded' }
55
+ // Idle is the normal resting state of a scaler with no triggers firing
56
+ // (like a CronJob waiting for its next run), not a fault.
57
+ return { text: 'Idle', color: healthColors.neutral, level: 'neutral' }
54
58
  }
55
59
 
56
60
  if (readyCond?.status === 'True') {
@@ -126,20 +130,26 @@ export function getScaledJobStatus(resource: any): StatusBadge {
126
130
  const conditions = resource.status?.conditions || []
127
131
 
128
132
  const readyCond = conditions.find((c: any) => c.type === 'Ready')
129
- if (readyCond?.status === 'True') {
130
- return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
133
+ // A non-operational scaler (Ready=False) is unhealthy and must take precedence
134
+ // over the Idle branch — otherwise a broken-and-idle ScaledJob hides as benign.
135
+ if (readyCond?.status === 'False') {
136
+ return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
131
137
  }
132
138
 
139
+ // Check Active BEFORE falling back to Ready=True: an operational scaler with no
140
+ // jobs running (Active=False) is intentionally idle → sky, not the green of a
141
+ // busy one. (Ready=True first would make Idle unreachable.) Mirrors
142
+ // getScaledObjectStatus.
133
143
  const activeCond = conditions.find((c: any) => c.type === 'Active')
134
144
  if (activeCond?.status === 'True') {
135
145
  return { text: 'Active', color: healthColors.healthy, level: 'healthy' }
136
146
  }
137
147
  if (activeCond?.status === 'False') {
138
- return { text: 'Idle', color: healthColors.degraded, level: 'degraded' }
148
+ return { text: 'Idle', color: healthColors.neutral, level: 'neutral' }
139
149
  }
140
150
 
141
- if (readyCond?.status === 'False') {
142
- return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
151
+ if (readyCond?.status === 'True') {
152
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
143
153
  }
144
154
 
145
155
  return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }