@skyhook-io/radar-app 1.12.3 → 1.13.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.
Files changed (38) hide show
  1. package/package.json +5 -5
  2. package/src/App.tsx +37 -8
  3. package/src/RadarApp.tsx +1 -1
  4. package/src/api/client.ts +38 -6
  5. package/src/api/diagnose.ts +45 -5
  6. package/src/components/cost/ApplicationCostTab.test.ts +45 -0
  7. package/src/components/cost/ApplicationCostTab.tsx +115 -64
  8. package/src/components/cost/CostTrendChart.test.ts +65 -0
  9. package/src/components/cost/CostTrendChart.tsx +107 -17
  10. package/src/components/cost/CostView.test.ts +36 -1
  11. package/src/components/cost/CostView.tsx +133 -51
  12. package/src/components/cost/CurrentAllocationUse.tsx +8 -4
  13. package/src/components/cost/WorkloadCostTab.test.ts +50 -0
  14. package/src/components/cost/WorkloadCostTab.tsx +109 -61
  15. package/src/components/cost/source.test.ts +33 -0
  16. package/src/components/cost/source.ts +100 -0
  17. package/src/components/diagnose/AISettings.tsx +9 -4
  18. package/src/components/diagnose/DiagnoseContext.tsx +216 -48
  19. package/src/components/diagnose/DiagnoseSurface.test.tsx +118 -5
  20. package/src/components/diagnose/DiagnoseSurface.tsx +190 -32
  21. package/src/components/diagnose/Home.tsx +93 -64
  22. package/src/components/diagnose/InvestigationView.tsx +190 -124
  23. package/src/components/diagnose/parts.test.tsx +12 -4
  24. package/src/components/diagnose/parts.tsx +29 -17
  25. package/src/components/execution/BatchExecutionView.render.test.tsx +35 -0
  26. package/src/components/execution/BatchExecutionView.tsx +2 -3
  27. package/src/components/execution/execution-definition.test.ts +19 -0
  28. package/src/components/execution/execution-definition.ts +2 -0
  29. package/src/components/gitops/GitOpsView.tsx +25 -3
  30. package/src/components/home/CostCard.tsx +3 -2
  31. package/src/components/nav/navigation.test.ts +26 -0
  32. package/src/components/nav/navigation.ts +10 -0
  33. package/src/components/rightsizing/RightsizingScanView.tsx +36 -12
  34. package/src/components/rightsizing/copy.test.ts +19 -0
  35. package/src/components/settings/SettingsDialog.tsx +666 -103
  36. package/src/components/settings/settings-state.test.ts +42 -0
  37. package/src/components/settings/settings-state.ts +39 -0
  38. package/src/index.css +11 -1
@@ -20,6 +20,14 @@ import {
20
20
  } from './format'
21
21
  import { CurrentAllocationUse } from './CurrentAllocationUse'
22
22
  import { costUnavailableReasonFromError } from './errors'
23
+ import {
24
+ costFreshnessLabel,
25
+ costIntegrationUnavailableMessage,
26
+ costRateLabels,
27
+ costSourceLabel,
28
+ isCostDiscoveryPending,
29
+ } from './source'
30
+ import { useNavCustomization } from '../../context/NavCustomization'
23
31
 
24
32
  type WorkloadCostState =
25
33
  | 'loading'
@@ -44,8 +52,9 @@ interface WorkloadCostTabProps {
44
52
  }
45
53
 
46
54
  export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps) {
55
+ const settingsAvailable = !useNavCustomization().embedded
47
56
  const [range, setRange] = useState<CostTimeRange>('24h')
48
- const [noPrometheusSince, setNoPrometheusSince] = useState<number | null>(null)
57
+ const [discoverySince, setDiscoverySince] = useState<number | null>(null)
49
58
  const currentQuery = useOpenCostWorkload(kind, namespace, name)
50
59
  const trendQuery = useOpenCostWorkloadTrend(kind, namespace, name, range)
51
60
  const trendMatchesRange = trendQuery.data?.range === range
@@ -62,10 +71,10 @@ export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps)
62
71
  })
63
72
 
64
73
  useEffect(() => {
65
- if (state === 'no_prometheus') {
66
- setNoPrometheusSince((prev) => prev ?? Date.now())
74
+ if (isCostDiscoveryPending(state)) {
75
+ setDiscoverySince((prev) => prev ?? Date.now())
67
76
  } else {
68
- setNoPrometheusSince(null)
77
+ setDiscoverySince(null)
69
78
  }
70
79
  }, [state])
71
80
 
@@ -84,22 +93,28 @@ export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps)
84
93
  state === 'query_error' ||
85
94
  state === 'access_denied' ||
86
95
  state === 'not_found' ||
96
+ state === 'no_cost_source' ||
97
+ state === 'source_unavailable' ||
98
+ state === 'authentication_error' ||
99
+ state === 'configuration_mismatch' ||
100
+ state === 'deployment_configuration_error' ||
101
+ state === 'history_unsupported' ||
87
102
  state === 'load_error'
88
103
  ) {
89
- const discoveryAgeMs = noPrometheusSince == null ? 0 : Date.now() - noPrometheusSince
90
- if (state === 'no_prometheus' && discoveryAgeMs < COST_DISCOVERY_GRACE_MS) {
104
+ const discoveryAgeMs = discoverySince == null ? 0 : Date.now() - discoverySince
105
+ if (isCostDiscoveryPending(state) && discoveryAgeMs < COST_DISCOVERY_GRACE_MS) {
91
106
  return (
92
107
  <WorkloadCostDiscovering
93
108
  isFetching={currentQuery.isFetching || trendQuery.isFetching}
94
109
  onRetry={() => {
95
- setNoPrometheusSince(Date.now())
110
+ setDiscoverySince(Date.now())
96
111
  currentQuery.refetch()
97
112
  trendQuery.refetch()
98
113
  }}
99
114
  />
100
115
  )
101
116
  }
102
- return <WorkloadCostUnavailable state={state} />
117
+ return <WorkloadCostUnavailable state={state} settingsAvailable={settingsAvailable} />
103
118
  }
104
119
 
105
120
  const current = currentQuery.data?.current
@@ -119,6 +134,10 @@ export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps)
119
134
  trendLoading || state === 'partial_missing_history',
120
135
  trendCurrency,
121
136
  )
137
+ const source = currentQuery.data?.source ?? trend?.source
138
+ const historyUnsupported = trend?.reason === 'history_unsupported'
139
+ const currentWindow = currentQuery.data?.window
140
+ const rateLabels = costRateLabels(currentWindow)
122
141
 
123
142
  return (
124
143
  <div className="mx-auto w-full max-w-[1600px] space-y-4">
@@ -129,65 +148,76 @@ export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps)
129
148
  <div>
130
149
  <div className="flex items-center gap-1.5">
131
150
  <div className="text-sm font-semibold text-theme-text-primary">
132
- Historical compute cost
151
+ {historyUnsupported ? 'Compute allocation cost' : 'Historical compute cost'}
133
152
  </div>
134
- <MetricInfoTooltip content="Values are based on OpenCost CPU and memory allocation over time, not raw utilization. OpenCost allocation uses the greater of requested or observed resources." />
153
+ <MetricInfoTooltip content="Values are based on CPU and memory allocation, not raw utilization. The cost provider attributes the greater of requested or observed resources." />
135
154
  </div>
136
155
  <div className="text-xs text-theme-text-tertiary">
137
- OpenCost CPU and memory allocation rate ({trendCurrency}/hr) attributed by workload
138
- ownership
156
+ CPU and memory allocation rate ({trendCurrency}/hr) attributed by workload ownership
139
157
  </div>
140
158
  </div>
141
159
  </div>
142
- <CostTimeRangeSelector value={range} onChange={setRange} />
160
+ {!historyUnsupported && <CostTimeRangeSelector value={range} onChange={setRange} />}
143
161
  </div>
144
162
 
145
- <div className="grid gap-4 p-4 lg:grid-cols-[220px_minmax(0,1fr)]">
146
- <div className="space-y-4">
163
+ {historyUnsupported ? (
164
+ <div className="grid gap-4 p-4 sm:grid-cols-2">
147
165
  <MetricBlock
148
- label={`Spend over ${range}`}
149
- value={windowSpendValue}
150
- subvalue={
151
- state === 'partial_missing_history' ? 'Historical data unavailable' : undefined
152
- }
166
+ label={rateLabels.hourly}
167
+ value={hasCurrent ? formatCostPerHour(hourly, currentCurrency) : '—'}
168
+ subvalue="CPU and memory allocation"
153
169
  />
154
170
  <MetricBlock
155
171
  label="Projected monthly"
156
172
  value={hasCurrent ? formatProjectedMonthlyCost(hourly, currentCurrency) : '—'}
157
- subvalue={
158
- hasCurrent
159
- ? `${formatCostPerHour(hourly, currentCurrency)} current rate`
160
- : 'Current allocation unavailable'
161
- }
173
+ subvalue={`${rateLabels.rate} × 730 hours`}
162
174
  />
163
175
  </div>
164
- <div className="min-w-0">
165
- {trendLoading ? (
166
- <div className="flex h-[240px] items-center justify-center rounded-md border border-dashed border-theme-border bg-theme-base/60 text-sm text-theme-text-tertiary">
167
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
168
- Loading historical cost…
169
- </div>
170
- ) : hasTrend ? (
171
- <StackedAreaChart
172
- series={[{ namespace: 'Allocation rate', dataPoints: points }]}
173
- currency={trendCurrency}
176
+ ) : (
177
+ <div className="grid gap-4 p-4 lg:grid-cols-[220px_minmax(0,1fr)]">
178
+ <div className="space-y-4">
179
+ <MetricBlock
180
+ label={`Spend over ${range}`}
181
+ value={windowSpendValue}
182
+ subvalue={
183
+ state === 'partial_missing_history' ? 'Historical data unavailable' : undefined
184
+ }
174
185
  />
175
- ) : (
176
- <div className="flex h-[240px] items-center justify-center rounded-md border border-dashed border-theme-border bg-theme-base/60 text-sm text-theme-text-tertiary">
177
- No historical workload owner cost points for this range.
178
- </div>
179
- )}
186
+ <MetricBlock
187
+ label="Projected monthly"
188
+ value={hasCurrent ? formatProjectedMonthlyCost(hourly, currentCurrency) : '—'}
189
+ subvalue={
190
+ hasCurrent
191
+ ? `${formatCostPerHour(hourly, currentCurrency)} · ${rateLabels.rate}`
192
+ : 'Current allocation unavailable'
193
+ }
194
+ />
195
+ </div>
196
+ <div className="min-w-0">
197
+ {trendLoading ? (
198
+ <div className="flex h-[240px] items-center justify-center rounded-md border border-dashed border-theme-border bg-theme-base/60 text-sm text-theme-text-tertiary">
199
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
200
+ Loading historical cost…
201
+ </div>
202
+ ) : hasTrend ? (
203
+ <StackedAreaChart
204
+ series={[{ namespace: 'Allocation rate', dataPoints: points }]}
205
+ currency={trendCurrency}
206
+ />
207
+ ) : (
208
+ <div className="flex h-[240px] items-center justify-center rounded-md border border-dashed border-theme-border bg-theme-base/60 text-sm text-theme-text-tertiary">
209
+ No historical workload owner cost points for this range.
210
+ </div>
211
+ )}
212
+ </div>
180
213
  </div>
181
- </div>
214
+ )}
182
215
  </section>
183
216
 
184
- {state === 'partial_missing_history' && (
217
+ {state === 'partial_missing_history' && !historyUnsupported && (
185
218
  <div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-base px-3 py-2 text-sm text-theme-text-secondary">
186
219
  <AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-theme-text-tertiary" />
187
- <span>
188
- Current cost is available, but historical workload owner metrics are not available for
189
- this range.
190
- </span>
220
+ <span>Current cost is available, but historical workload owner metrics are not available for this range.</span>
191
221
  </div>
192
222
  )}
193
223
  {state === 'partial_missing_current' && (
@@ -200,13 +230,13 @@ export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps)
200
230
  )}
201
231
 
202
232
  <div className="grid gap-4 md:grid-cols-2">
203
- <MetricTile label="Replicas" value={hasCurrent ? String(current?.replicas ?? 0) : '—'} />
233
+ <MetricTile label="Live replicas" value={hasCurrent ? String(current?.replicas ?? 0) : '—'} />
204
234
  <MetricTile
205
235
  label="Projected daily"
206
236
  value={hasCurrent ? formatProjectedDailyRate(hourly, currentCurrency) : '—'}
207
237
  subvalue={
208
238
  hasCurrent
209
- ? `${formatCostPerHour(hourly, currentCurrency)} current hourly rate`
239
+ ? `${formatCostPerHour(hourly, currentCurrency)} · ${rateLabels.rate}`
210
240
  : 'Current allocation unavailable'
211
241
  }
212
242
  />
@@ -222,15 +252,19 @@ export function WorkloadCostTab({ kind, namespace, name }: WorkloadCostTabProps)
222
252
  memoryAllocationUse={current?.memoryAllocationUse ?? 0}
223
253
  cpuUsageAvailable={current?.cpuUsageAvailable ?? false}
224
254
  memoryUsageAvailable={current?.memoryUsageAvailable ?? false}
255
+ window={source === 'kubecost' ? currentWindow : undefined}
225
256
  />
226
257
 
227
258
  <div className="text-xs text-theme-text-tertiary">
228
- Powered by OpenCost via Prometheus.{' '}
259
+ {costSourceLabel(source)} &middot; {costFreshnessLabel(source, source === 'kubecost' ? currentWindow : '1h', currentQuery.data?.dataThrough)}.{' '}
229
260
  {currentCurrency !== DEFAULT_COST_CURRENCY && (
230
261
  <>Labeled {currentCurrency}; no conversion. </>
231
262
  )}
232
- Historical spend uses the selected range; projected monthly values multiply the current
233
- hourly allocation. Storage/PVC attribution remains at namespace and cluster level.
263
+ {historyUnsupported
264
+ ? 'Historical workload charts are not available for Kubecost yet. '
265
+ : 'Historical spend uses the selected range. '}
266
+ Projected monthly values multiply the {rateLabels.rate}. Storage/PVC attribution
267
+ remains at namespace and cluster level.
234
268
  </div>
235
269
  </div>
236
270
  )
@@ -260,14 +294,19 @@ export function getWorkloadCostState(
260
294
  if (trendHasData) return 'partial_missing_current'
261
295
  const reason =
262
296
  current?.reason ??
263
- trend?.reason ??
264
297
  costUnavailableReasonFromError(queryStatus.currentError) ??
298
+ trend?.reason ??
265
299
  costUnavailableReasonFromError(queryStatus.trendError)
266
300
  if (
267
301
  reason === 'no_prometheus' ||
302
+ reason === 'no_cost_source' ||
268
303
  reason === 'query_error' ||
269
304
  reason === 'access_denied' ||
270
- reason === 'not_found'
305
+ reason === 'not_found' ||
306
+ reason === 'source_unavailable' ||
307
+ reason === 'authentication_error' ||
308
+ reason === 'configuration_mismatch' ||
309
+ reason === 'deployment_configuration_error'
271
310
  )
272
311
  return reason
273
312
  if (queryError) return 'load_error'
@@ -289,11 +328,11 @@ function WorkloadCostDiscovering({
289
328
  <Loader2 className="h-8 w-8 animate-spin text-theme-text-tertiary/60" />
290
329
  <div>
291
330
  <p className="text-sm font-medium text-theme-text-primary">
292
- Looking for Prometheus cost data…
331
+ Looking for cost data…
293
332
  </p>
294
333
  <p className="mt-1 text-xs text-theme-text-tertiary">
295
- First discovery can take a few seconds while Radar checks cluster services and opens a
296
- local port-forward.
334
+ Radar is checking OpenCost metrics in a PromQL-compatible backend and a local Kubecost
335
+ 3 Aggregator. First discovery can take a few seconds.
297
336
  </p>
298
337
  </div>
299
338
  <button
@@ -308,19 +347,28 @@ function WorkloadCostDiscovering({
308
347
  )
309
348
  }
310
349
 
311
- function WorkloadCostUnavailable({ state }: { state: CostUnavailableReason | 'load_error' }) {
350
+ function WorkloadCostUnavailable({
351
+ state,
352
+ settingsAvailable,
353
+ }: {
354
+ state: CostUnavailableReason | 'load_error'
355
+ settingsAvailable: boolean
356
+ }) {
312
357
  const message =
313
- state === 'no_prometheus'
314
- ? 'Prometheus not found. OpenCost workload cost requires Prometheus or VictoriaMetrics.'
358
+ costIntegrationUnavailableMessage(state, settingsAvailable) ??
359
+ (state === 'no_prometheus'
360
+ ? 'No compatible metrics backend was found. OpenCost workload cost requires OpenCost metrics in a PromQL-compatible backend.'
315
361
  : state === 'query_error'
316
- ? 'Cost data is temporarily unavailable. Prometheus was found, but workload cost queries failed.'
362
+ ? 'Cost data is temporarily unavailable. A metrics backend was found, but workload cost queries failed.'
363
+ : state === 'history_unsupported'
364
+ ? 'Historical workload cost is not available for Kubecost yet.'
317
365
  : state === 'access_denied'
318
366
  ? 'You do not have access to view cost for this workload.'
319
367
  : state === 'not_found'
320
368
  ? 'This workload no longer exists.'
321
369
  : state === 'load_error'
322
370
  ? 'Could not load workload cost data. Check access to this workload and try again.'
323
- : 'OpenCost workload metrics were not found for this workload.'
371
+ : 'No workload cost data was returned for this workload by the active cost source.')
324
372
 
325
373
  return (
326
374
  <div className="flex h-full min-h-[320px] items-center justify-center">
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { costDataThroughLabel, costFreshnessLabel, costIntegrationUnavailableMessage, costRateLabels, costSourceLabel, isCostDiscoveryPending } from './source'
3
+
4
+ describe('cost source presentation', () => {
5
+ it('distinguishes Prometheus windows from Kubecost ETL freshness', () => {
6
+ expect(costSourceLabel('prometheus')).toBe('OpenCost via Prometheus')
7
+ expect(costSourceLabel('kubecost')).toBe('Kubecost Aggregator')
8
+ expect(costFreshnessLabel('prometheus', '1h')).toBe('last 1h average')
9
+ expect(costFreshnessLabel('kubecost', '1h', '2026-08-26T13:58:00Z')).toContain('1-hour allocation average')
10
+ expect(costFreshnessLabel('kubecost', '1d', '2026-08-26T13:58:00Z')).toContain('1-day allocation average')
11
+ expect(costDataThroughLabel('2026-08-26T13:58:00Z')).toContain('2026')
12
+ expect(costDataThroughLabel('invalid')).toBe('')
13
+ })
14
+
15
+ it('labels fallback allocation windows without calling a daily average current', () => {
16
+ expect(costRateLabels('1h').hourly).toBe('Hourly (1-hour average)')
17
+ expect(costRateLabels('1d').rate).toBe('1-day average hourly rate')
18
+ expect(costRateLabels().allocationTitle).toBe('Current allocation and use')
19
+ })
20
+
21
+ it('keeps both absent-source reasons inside the discovery grace period', () => {
22
+ expect(isCostDiscoveryPending('no_prometheus')).toBe(true)
23
+ expect(isCostDiscoveryPending('no_cost_source')).toBe(true)
24
+ expect(isCostDiscoveryPending('source_unavailable')).toBe(false)
25
+ })
26
+
27
+ it('does not point embedded users at standalone Settings', () => {
28
+ expect(costIntegrationUnavailableMessage('no_cost_source', true)).toContain('Settings → Metrics')
29
+ expect(costIntegrationUnavailableMessage('no_cost_source', false)).toContain('host application')
30
+ expect(costIntegrationUnavailableMessage('source_unavailable', false)).not.toContain('Settings')
31
+ expect(costIntegrationUnavailableMessage('authentication_error', false)).not.toContain('Settings')
32
+ })
33
+ })
@@ -0,0 +1,100 @@
1
+ import type { CostDataSource, CostUnavailableReason } from '../../api/client'
2
+
3
+ export function costSourceLabel(source?: CostDataSource): string {
4
+ return source === 'kubecost' ? 'Kubecost Aggregator' : 'OpenCost via Prometheus'
5
+ }
6
+
7
+ export function isCostDiscoveryPending(reason?: string): boolean {
8
+ return reason === 'no_prometheus' || reason === 'no_cost_source'
9
+ }
10
+
11
+ export function costConfigurationAction(reason?: CostUnavailableReason): {
12
+ section: 'prometheus' | 'cost'
13
+ label: string
14
+ } {
15
+ return reason === 'no_prometheus'
16
+ ? { section: 'prometheus', label: 'Configure metrics' }
17
+ : { section: 'cost', label: 'Configure cost source' }
18
+ }
19
+
20
+ export function costRateLabels(window?: string): {
21
+ hourly: string
22
+ rate: string
23
+ allocationTitle: string
24
+ allocationPeriod: string
25
+ } {
26
+ if (window === '1d') {
27
+ return {
28
+ hourly: 'Hourly (1-day average)',
29
+ rate: '1-day average hourly rate',
30
+ allocationTitle: '1-day average allocation and use',
31
+ allocationPeriod: 'Allocation and observed use: 1-day average',
32
+ }
33
+ }
34
+ if (window === '1h') {
35
+ return {
36
+ hourly: 'Hourly (1-hour average)',
37
+ rate: '1-hour average hourly rate',
38
+ allocationTitle: '1-hour average allocation and use',
39
+ allocationPeriod: 'Allocation and observed use: 1-hour average',
40
+ }
41
+ }
42
+ return {
43
+ hourly: 'Current hourly',
44
+ rate: 'current hourly rate',
45
+ allocationTitle: 'Current allocation and use',
46
+ allocationPeriod: 'Allocation and CPU use: 1h average · Memory use: current',
47
+ }
48
+ }
49
+
50
+ export function costFreshnessLabel(
51
+ source?: CostDataSource,
52
+ window?: string,
53
+ dataThrough?: string,
54
+ ): string {
55
+ if (source !== 'kubecost') return window ? `last ${window} average` : 'current allocation average'
56
+ const average = window === '1d'
57
+ ? '1-day allocation average'
58
+ : window === '1h'
59
+ ? '1-hour allocation average'
60
+ : 'current allocation average'
61
+ if (!dataThrough) return `latest Kubecost ${average}`
62
+ const timestamp = costDataThroughLabel(dataThrough)
63
+ if (!timestamp) return `latest Kubecost ${average}`
64
+ return `Kubecost ${average} · data through ${timestamp}`
65
+ }
66
+
67
+ export function costDataThroughLabel(dataThrough?: string): string {
68
+ if (!dataThrough) return ''
69
+ const timestamp = new Date(dataThrough)
70
+ if (Number.isNaN(timestamp.getTime())) return ''
71
+ return timestamp.toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' })
72
+ }
73
+
74
+ export function costIntegrationUnavailableMessage(
75
+ reason?: CostUnavailableReason | 'load_error',
76
+ settingsAvailable = true,
77
+ ): string | undefined {
78
+ switch (reason) {
79
+ case 'no_cost_source':
80
+ return settingsAvailable
81
+ ? 'No compatible cost source was found. Connect OpenCost metrics in Settings → Metrics or configure Kubecost in Settings → Cost.'
82
+ : 'No compatible cost source is configured for this cluster. Configure OpenCost metrics or Kubecost in the host application or Radar deployment.'
83
+ case 'source_unavailable':
84
+ return settingsAvailable
85
+ ? 'Kubecost Aggregator is unavailable. Check the URL, network path, and cluster ID in Settings → Cost.'
86
+ : 'Kubecost Aggregator is unavailable. Update this cluster’s cost-source configuration in the host application or Radar deployment.'
87
+ case 'deployment_configuration_error':
88
+ return 'Cost collection is misconfigured by this Radar deployment. Update its environment variables or Helm cost values, then restart Radar.'
89
+ case 'authentication_error':
90
+ return settingsAvailable
91
+ ? 'Kubecost rejected the configured API key. Update it in Settings → Cost.'
92
+ : 'Kubecost rejected the configured API key. Update this cluster’s credential in the host application or Radar deployment.'
93
+ case 'configuration_mismatch':
94
+ return settingsAvailable
95
+ ? 'Saved Kubecost settings are not valid for this cluster. Update the cluster ID or local API key in Settings → Cost.'
96
+ : 'Kubecost settings are not valid for this cluster. Update the cluster ID or credential in the host application or Radar deployment.'
97
+ default:
98
+ return undefined
99
+ }
100
+ }
@@ -49,7 +49,7 @@ function ClearHistoryRow({
49
49
  <div className="mt-3 flex items-center justify-between gap-2 border-t border-theme-border/60 pt-3">
50
50
  <p className="text-[11px] leading-snug text-theme-text-tertiary">
51
51
  {hosted ? (
52
- `Investigation transcripts are stored by ${agentLabel} so history survives restarts.`
52
+ `${agentLabel} stores private, organization-shared, and automatic investigation transcripts so history survives restarts.`
53
53
  ) : (
54
54
  <>
55
55
  Investigation transcripts are kept on this machine (
@@ -67,6 +67,12 @@ function ClearHistoryRow({
67
67
  Couldn&apos;t clear history.
68
68
  </span>
69
69
  )}
70
+ {hosted && confirming && state === "idle" && (
71
+ <span className="ml-1 font-medium text-red-400">
72
+ This permanently deletes every member&apos;s investigations for this
73
+ cluster — private, organization-shared, and automatic.
74
+ </span>
75
+ )}
70
76
  </p>
71
77
  {confirming ? (
72
78
  <div className="flex shrink-0 items-center gap-1.5">
@@ -133,9 +139,8 @@ export function AISettingsSection({
133
139
  selectedAgent={draft.agent}
134
140
  // Model + effort are agent-specific; reset them when the agent changes.
135
141
  onSelectAgent={(a) => {
136
- const nextProfile = agents.find(
137
- (agent) => agent.name === a,
138
- )?.profiles?.[0];
142
+ const nextProfile = agents.find((agent) => agent.name === a)
143
+ ?.profiles?.[0];
139
144
  onChange({
140
145
  agent: a,
141
146
  profile: nextProfile ?? draft.profile,