@skyhook-io/radar-app 1.8.7 → 1.8.9

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 (56) hide show
  1. package/package.json +4 -4
  2. package/src/App.tsx +69 -61
  3. package/src/RadarApp.tsx +15 -1
  4. package/src/api/apiResources.ts +1 -1
  5. package/src/api/client.argoResourceSync.test.ts +69 -0
  6. package/src/api/client.rightsizing.test.ts +32 -0
  7. package/src/api/client.ts +1222 -244
  8. package/src/api/timelineSource.ts +4 -2
  9. package/src/components/applications/ApplicationsView.tsx +613 -219
  10. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  11. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  12. package/src/components/cost/CostTrendChart.tsx +103 -72
  13. package/src/components/cost/CostView.test.ts +12 -0
  14. package/src/components/cost/CostView.tsx +494 -229
  15. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  16. package/src/components/cost/CostViewTabs.tsx +40 -0
  17. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  18. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  19. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  20. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  21. package/src/components/cost/cloud-console.test.ts +39 -0
  22. package/src/components/cost/cloud-console.ts +81 -0
  23. package/src/components/cost/errors.ts +8 -0
  24. package/src/components/cost/format.test.ts +27 -0
  25. package/src/components/cost/format.ts +46 -0
  26. package/src/components/cost/kinds.ts +5 -0
  27. package/src/components/diagnose/AISettings.tsx +7 -12
  28. package/src/components/diagnose/DiagnoseContext.tsx +1 -0
  29. package/src/components/diagnose/DiagnoseSurface.tsx +3 -0
  30. package/src/components/diagnose/InvestigationView.tsx +16 -3
  31. package/src/components/diagnose/parts.tsx +140 -73
  32. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  33. package/src/components/gitops/GitOpsView.tsx +81 -14
  34. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  35. package/src/components/helm/HelmCompareRoute.tsx +1 -2
  36. package/src/components/helm/ManifestDiffViewer.tsx +1 -31
  37. package/src/components/helm/ValuesDiffPreview.tsx +2 -3
  38. package/src/components/home/CostCard.tsx +21 -36
  39. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  40. package/src/components/resource/RightsizingStrip.tsx +319 -123
  41. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  42. package/src/components/rightsizing/copy.test.ts +56 -0
  43. package/src/components/rightsizing/model.test.ts +227 -0
  44. package/src/components/rightsizing/model.ts +158 -0
  45. package/src/components/rightsizing/presentation.test.ts +104 -0
  46. package/src/components/rightsizing/presentation.ts +94 -0
  47. package/src/components/settings/MyPermissionsDialog.tsx +66 -116
  48. package/src/components/settings/SettingsDialog.tsx +1268 -318
  49. package/src/components/timeline/TimelineList.tsx +35 -8
  50. package/src/components/timeline/TimelineView.tsx +156 -26
  51. package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
  52. package/src/components/workload/WorkloadView.tsx +711 -328
  53. package/src/context/DiagnoseCustomization.tsx +36 -2
  54. package/src/index.css +5 -1
  55. package/src/index.ts +4 -1
  56. package/src/main.tsx +1 -1
@@ -1,60 +1,163 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { useOpenCostSummary, useOpenCostWorkloads, useOpenCostNodes } from '../../api/client'
3
- import type { OpenCostNamespaceCost, OpenCostWorkloadCost, OpenCostNodeCost } from '../../api/client'
4
- import { ArrowLeft, ChevronDown, ChevronRight, DollarSign, HelpCircle, Loader2, Server, X } from 'lucide-react'
5
- import { PaneLoader, FreshnessControl } from '@skyhook-io/k8s-ui'
2
+ import { useLocation } from 'react-router-dom'
3
+ import {
4
+ COST_DISCOVERY_GRACE_MS,
5
+ useClusterInfo,
6
+ useOpenCostSummary,
7
+ useOpenCostWorkloads,
8
+ useOpenCostNodes,
9
+ } from '../../api/client'
10
+ import type {
11
+ OpenCostNamespaceCost,
12
+ OpenCostWorkloadCost,
13
+ OpenCostNodeCost,
14
+ } from '../../api/client'
15
+ import {
16
+ ChevronDown,
17
+ ChevronRight,
18
+ DollarSign,
19
+ ExternalLink,
20
+ HelpCircle,
21
+ Loader2,
22
+ Server,
23
+ X,
24
+ } from 'lucide-react'
25
+ import { PaneLoader, FreshnessControl, PageHeader } from '@skyhook-io/k8s-ui'
6
26
  import { CostTrendChart } from './CostTrendChart'
27
+ import {
28
+ COST_HOURS_PER_MONTH,
29
+ formatCostPerHour,
30
+ formatProjectedDailyRate,
31
+ formatProjectedMonthlyCost,
32
+ formatProjectedMonthlyRate,
33
+ } from './format'
7
34
  import { Tooltip } from '../ui/Tooltip'
8
35
  import { useConnection } from '../../context/ConnectionContext'
36
+ import type { SelectedResource } from '../../types'
37
+ import { kindToPlural, openExternal } from '../../utils/navigation'
38
+ import { clusterCloudConsoleLink, nodeCloudConsoleLink } from './cloud-console'
39
+ import { RightsizingScanView } from '../rightsizing/RightsizingScanView'
40
+ import { CostViewTabs } from './CostViewTabs'
9
41
 
10
42
  interface CostViewProps {
11
43
  onBack: () => void
44
+ onOpenResource?: (resource: SelectedResource) => void
45
+ namespaces: string[]
12
46
  }
13
47
 
14
- export function CostView({ onBack }: CostViewProps) {
15
- const { data, isLoading, dataUpdatedAt, refetch } = useOpenCostSummary()
48
+ const SYSTEM_COST_NAMESPACES = new Set(['kube-system', 'kube-public', 'kube-node-lease'])
49
+
50
+ export function CostView(props: CostViewProps) {
51
+ const { pathname } = useLocation()
52
+ if (pathname.startsWith('/cost/rightsizing')) {
53
+ return <RightsizingScanView namespaces={props.namespaces} />
54
+ }
55
+ return <CostOverview {...props} />
56
+ }
57
+
58
+ function CostOverview({ onBack, onOpenResource }: CostViewProps) {
59
+ const { data, isLoading, isFetching, dataUpdatedAt, refetch } = useOpenCostSummary()
16
60
  const { data: nodeData } = useOpenCostNodes()
61
+ const { data: clusterInfo } = useClusterInfo()
17
62
  const { connection } = useConnection()
18
63
  const [showHelp, setShowHelp] = useState(false)
64
+ const [noPrometheusSince, setNoPrometheusSince] = useState<number | null>(null)
65
+
66
+ useEffect(() => {
67
+ if (data?.available === false && data.reason === 'no_prometheus') {
68
+ setNoPrometheusSince((prev) => prev ?? Date.now())
69
+ } else {
70
+ setNoPrometheusSince(null)
71
+ }
72
+ }, [data?.available, data?.reason])
19
73
 
20
74
  if (isLoading) {
21
- return <PaneLoader label="Loading cost data…" className="flex-1" />
75
+ return (
76
+ <CostOverviewState>
77
+ <PaneLoader label="Loading cost data…" className="min-h-64" />
78
+ </CostOverviewState>
79
+ )
22
80
  }
23
81
 
24
82
  if (!data || !data.available) {
25
83
  const reason = data?.reason
26
- const message = reason === 'no_prometheus'
27
- ? 'Prometheus not found OpenCost requires Prometheus or VictoriaMetrics'
28
- : reason === 'no_metrics'
29
- ? 'OpenCost metrics not found — Prometheus is available but no cost metrics were detected'
30
- : reason === 'query_error'
31
- ? 'Cost data temporarily unavailable Prometheus was found but queries failed'
32
- : 'OpenCost not detected install OpenCost for cost visibility'
84
+ const discoveryAgeMs = noPrometheusSince == null ? 0 : Date.now() - noPrometheusSince
85
+ if (reason === 'no_prometheus' && discoveryAgeMs < COST_DISCOVERY_GRACE_MS) {
86
+ return (
87
+ <CostOverviewState>
88
+ <div className="flex min-h-64 items-center justify-center">
89
+ <div className="flex max-w-md flex-col items-center gap-3 text-center text-theme-text-secondary">
90
+ <Loader2 className="w-8 h-8 animate-spin text-theme-text-tertiary/60" />
91
+ <div>
92
+ <p className="text-sm font-medium text-theme-text-primary">
93
+ Looking for Prometheus cost data…
94
+ </p>
95
+ <p className="mt-1 text-xs text-theme-text-tertiary">
96
+ First discovery can take a few seconds while Radar checks cluster services and
97
+ opens a local port-forward.
98
+ </p>
99
+ </div>
100
+ <button
101
+ onClick={() => {
102
+ setNoPrometheusSince(Date.now())
103
+ refetch()
104
+ }}
105
+ disabled={isFetching}
106
+ className="text-xs text-accent-text hover:text-theme-text-primary disabled:cursor-not-allowed disabled:text-theme-text-disabled transition-colors"
107
+ >
108
+ {isFetching ? 'Checking…' : 'Check again'}
109
+ </button>
110
+ </div>
111
+ </div>
112
+ </CostOverviewState>
113
+ )
114
+ }
115
+ const message =
116
+ reason === 'no_prometheus'
117
+ ? 'Prometheus not found — OpenCost requires Prometheus or VictoriaMetrics'
118
+ : reason === 'no_metrics'
119
+ ? 'OpenCost metrics not found — Prometheus is available but no cost metrics were detected'
120
+ : reason === 'query_error'
121
+ ? 'Cost data temporarily unavailable — Prometheus was found but queries failed'
122
+ : 'OpenCost not detected — install OpenCost for cost visibility'
33
123
 
34
124
  return (
35
- <div className="flex-1 flex items-center justify-center">
36
- <div className="flex flex-col items-center gap-3 text-theme-text-secondary">
37
- <DollarSign className="w-8 h-8 text-theme-text-tertiary/40" />
38
- <p className="text-sm">{message}</p>
39
- <button
40
- onClick={onBack}
41
- className="text-xs text-skyhook-400 hover:text-skyhook-300 transition-colors"
42
- >
43
- Back to Dashboard
44
- </button>
125
+ <CostOverviewState>
126
+ <div className="flex min-h-64 items-center justify-center">
127
+ <div className="flex flex-col items-center gap-3 text-theme-text-secondary">
128
+ <DollarSign className="w-8 h-8 text-theme-text-tertiary/40" />
129
+ <p className="text-sm">{message}</p>
130
+ <button
131
+ onClick={onBack}
132
+ className="text-xs text-skyhook-400 hover:text-skyhook-300 transition-colors"
133
+ >
134
+ Back to Dashboard
135
+ </button>
136
+ {reason === 'no_prometheus' && (
137
+ <button
138
+ onClick={() => {
139
+ setNoPrometheusSince(Date.now())
140
+ refetch()
141
+ }}
142
+ disabled={isFetching}
143
+ className="text-xs text-accent-text hover:text-theme-text-primary disabled:cursor-not-allowed disabled:text-theme-text-disabled transition-colors"
144
+ >
145
+ {isFetching ? 'Checking…' : 'Check again'}
146
+ </button>
147
+ )}
148
+ </div>
45
149
  </div>
46
- </div>
150
+ </CostOverviewState>
47
151
  )
48
152
  }
49
153
 
50
154
  const hourlyCost = data.totalHourlyCost ?? 0
51
- const monthlyCost = hourlyCost * 730
52
155
  const namespaces = data.namespaces ?? []
53
156
  const totalCpu = namespaces.reduce((sum, ns) => sum + ns.cpuCost, 0)
54
157
  const totalMem = namespaces.reduce((sum, ns) => sum + ns.memoryCost, 0)
55
158
  const totalStorage = data.totalStorageCost ?? 0
56
159
  const hasStorage = totalStorage > 0
57
- const hasEfficiency = (data.clusterEfficiency ?? 0) > 0
160
+ const hasSystemNamespaces = namespaces.some((ns) => isSystemCostNamespace(ns.name))
58
161
 
59
162
  // Compute split percentages (CPU + Memory + optional Storage)
60
163
  const allocTotal = totalCpu + totalMem + totalStorage
@@ -62,22 +165,15 @@ export function CostView({ onBack }: CostViewProps) {
62
165
  const memPct = allocTotal > 0 ? (totalMem / allocTotal) * 100 : 50
63
166
  const storagePct = allocTotal > 0 ? (totalStorage / allocTotal) * 100 : 0
64
167
 
65
- const nodes = nodeData?.available ? nodeData.nodes ?? [] : []
168
+ const nodes = nodeData?.available ? (nodeData.nodes ?? []) : []
169
+ const clusterConsoleLink = clusterCloudConsoleLink(clusterInfo?.context)
66
170
 
67
171
  return (
68
172
  <div className="flex-1 overflow-y-auto">
69
- <div className="max-w-[1100px] mx-auto px-6 py-6 space-y-6">
173
+ <div className="mx-auto w-full max-w-[1920px] px-6 py-6 space-y-6">
70
174
  {/* Header */}
71
175
  <div className="flex items-center justify-between">
72
176
  <div className="flex items-center gap-3">
73
- <button
74
- onClick={onBack}
75
- className="flex items-center gap-1.5 text-sm text-theme-text-secondary hover:text-theme-text-primary transition-colors"
76
- >
77
- <ArrowLeft className="w-4 h-4" />
78
- Dashboard
79
- </button>
80
- <div className="w-px h-5 bg-theme-border" />
81
177
  <div className="flex items-center gap-2">
82
178
  <DollarSign className="w-5 h-5 text-indigo-500" />
83
179
  <h1 className="text-lg font-semibold text-theme-text-primary">Cost Insights</h1>
@@ -90,9 +186,22 @@ export function CostView({ onBack }: CostViewProps) {
90
186
  <HelpCircle className="w-3.5 h-3.5" />
91
187
  How this works
92
188
  </button>
189
+ {clusterConsoleLink && (
190
+ <Tooltip content={clusterConsoleLink.label}>
191
+ <button
192
+ type="button"
193
+ onClick={() => openExternal(clusterConsoleLink.url)}
194
+ className="flex items-center gap-1 text-xs text-theme-text-tertiary hover:text-accent-text transition-colors duration-150"
195
+ aria-label={clusterConsoleLink.label}
196
+ >
197
+ <ExternalLink className="w-3.5 h-3.5" />
198
+ Cloud console
199
+ </button>
200
+ </Tooltip>
201
+ )}
93
202
  </div>
94
203
  <div className="flex items-center gap-4">
95
- {/* Tracks the headline $/hr summary (the primary query); its load
204
+ {/* Tracks the headline monthly summary (the primary query); its load
96
205
  time is the representative freshness signal for the view. */}
97
206
  <FreshnessControl
98
207
  mode="auto"
@@ -100,64 +209,61 @@ export function CostView({ onBack }: CostViewProps) {
100
209
  onRefresh={() => refetch()}
101
210
  connectionState={connection.state}
102
211
  />
103
- {hasEfficiency && (
104
- <div className="flex flex-col items-end gap-0.5">
105
- <div className="flex items-center gap-2 text-sm">
106
- <span className={efficiencyColor(data.clusterEfficiency ?? 0)}>
107
- {(data.clusterEfficiency ?? 0).toFixed(0)}% efficient
108
- </span>
109
- </div>
110
- <span className="text-[10px] text-theme-text-tertiary">
111
- ~{formatCost((data.totalIdleCost ?? 0) * 730)}/mo unused capacity
212
+ <div className="flex flex-col items-end">
213
+ <div className="flex items-baseline gap-1">
214
+ <span className="text-2xl font-bold text-theme-text-primary tabular-nums">
215
+ {formatProjectedMonthlyCost(hourlyCost)}
112
216
  </span>
217
+ <span className="text-xs text-theme-text-tertiary">/mo</span>
113
218
  </div>
114
- )}
115
- <div className="flex flex-col items-end">
116
- <div className="flex items-baseline gap-3">
117
- <div className="flex items-baseline gap-1">
118
- <span className="text-2xl font-bold text-theme-text-primary tabular-nums">
119
- {formatCost(hourlyCost)}
120
- </span>
121
- <span className="text-xs text-theme-text-tertiary">/hr</span>
122
- </div>
123
- <div className="flex items-baseline gap-1 text-theme-text-secondary">
124
- <span className="text-sm font-medium tabular-nums">~{formatCost(monthlyCost)}</span>
125
- <span className="text-[10px] text-theme-text-tertiary">/mo</span>
126
- </div>
219
+ <div className="mt-0.5 flex items-baseline gap-2 text-theme-text-secondary">
220
+ <span className="text-sm font-medium tabular-nums">
221
+ {formatProjectedDailyRate(hourlyCost)}
222
+ </span>
223
+ <span className="text-[10px] text-theme-text-quaternary">·</span>
224
+ <span className="text-sm font-medium tabular-nums">
225
+ {formatCostPerHour(hourlyCost)}
226
+ </span>
127
227
  </div>
128
- <span className="text-[10px] text-theme-text-quaternary">based on last 1h average</span>
228
+ <span className="text-[10px] text-theme-text-quaternary">
229
+ projected from last 1h average
230
+ </span>
129
231
  </div>
130
232
  </div>
131
233
  </div>
132
234
 
235
+ <CostViewTabs />
236
+
133
237
  {/* CPU vs Memory (vs Storage) split bar */}
134
238
  <div className="rounded-lg border border-theme-border bg-theme-surface/50 p-4">
135
239
  <div className="flex items-center justify-between mb-2">
136
- <span className="text-xs font-medium text-theme-text-secondary">Cluster Resource Cost</span>
240
+ <span className="text-xs font-medium text-theme-text-secondary">
241
+ Cluster Resource Cost
242
+ </span>
137
243
  <div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
138
244
  <span className="flex items-center gap-1.5">
139
- <span className="w-2.5 h-2.5 rounded-sm bg-blue-500" />
140
- CPU {formatCost(totalCpu)}/hr
245
+ <span className="w-2.5 h-2.5 rounded-sm bg-accent" />
246
+ CPU {formatProjectedMonthlyRate(totalCpu)}
141
247
  </span>
142
248
  <span className="flex items-center gap-1.5">
143
- <span className="w-2.5 h-2.5 rounded-sm bg-purple-500" />
144
- Memory {formatCost(totalMem)}/hr
249
+ <span className="w-2.5 h-2.5 rounded-sm bg-amber-500" />
250
+ Memory {formatProjectedMonthlyRate(totalMem)}
145
251
  </span>
146
252
  {hasStorage && (
147
253
  <span className="flex items-center gap-1.5">
148
254
  <span className="w-2.5 h-2.5 rounded-sm bg-teal-500" />
149
- Storage {formatCost(totalStorage)}/hr
255
+ Storage {formatProjectedMonthlyRate(totalStorage)}
150
256
  </span>
151
257
  )}
152
258
  </div>
153
259
  </div>
154
260
  <div className="h-3 rounded-full overflow-hidden bg-theme-hover flex">
155
261
  <div
156
- className="h-full bg-blue-500 transition-all duration-300"
262
+ className="h-full bg-accent transition-all duration-300"
157
263
  style={{ width: `${cpuPct}%` }}
158
264
  />
159
265
  <div
160
- className="h-full bg-purple-500 transition-all duration-300"
266
+ className="h-full bg-amber-500 transition-all duration-300"
161
267
  style={{ width: `${memPct}%` }}
162
268
  />
163
269
  {hasStorage && (
@@ -177,42 +283,61 @@ export function CostView({ onBack }: CostViewProps) {
177
283
  <div className="px-4 py-3 border-b border-theme-border">
178
284
  <div className="flex items-center justify-between">
179
285
  <div>
180
- <span className="text-sm font-semibold text-theme-text-primary">Namespace Breakdown</span>
181
- <span className="text-[10px] text-theme-text-quaternary ml-2">current hourly rates</span>
286
+ <span className="text-sm font-semibold text-theme-text-primary">
287
+ Namespace Breakdown
288
+ </span>
289
+ <span className="text-[10px] text-theme-text-quaternary ml-2">
290
+ projected monthly from current rate
291
+ </span>
182
292
  </div>
183
- <span className="text-xs text-theme-text-tertiary">{namespaces.length} namespaces</span>
293
+ <span className="text-xs text-theme-text-tertiary">
294
+ {namespaces.length} namespaces
295
+ </span>
184
296
  </div>
185
297
  </div>
298
+ {hasSystemNamespaces && <SystemNamespacesCostNote />}
186
299
 
187
300
  {/* Table header */}
188
- <div className="grid grid-cols-[minmax(180px,1fr)_90px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
301
+ <div className="grid grid-cols-[minmax(180px,1fr)_110px_90px_minmax(160px,1fr)_150px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
189
302
  <span>Namespace</span>
190
- <span className="text-right">Hourly</span>
191
- <Tooltip content="Projected from current hourly rate — not historical spend" wrapperClassName="!block text-right">
192
- <span className="cursor-help">Monthly*</span>
193
- </Tooltip>
194
- <Tooltip content="% of reserved resources actually being used, weighted by cost" wrapperClassName="!block text-right">
195
- <span className="cursor-help">Efficiency</span>
303
+ <Tooltip
304
+ content="Projected from current hourly rate — not historical spend"
305
+ wrapperClassName="!block text-right"
306
+ >
307
+ <span className="cursor-help">Projected/mo*</span>
196
308
  </Tooltip>
309
+ <span className="text-right">Hourly</span>
197
310
  <span>CPU / Memory</span>
198
- <span className="text-right">Cost Split</span>
311
+ <Tooltip
312
+ content="Projected monthly CPU and memory allocation from the current hourly rate"
313
+ wrapperClassName="!block text-right"
314
+ >
315
+ <span className="cursor-help">CPU / Memory/mo*</span>
316
+ </Tooltip>
199
317
  </div>
200
318
 
201
319
  {/* Namespace rows */}
202
320
  <div className="divide-y divide-theme-border/50">
203
321
  {namespaces.map((ns) => (
204
- <NamespaceCostRow key={ns.name} ns={ns} maxCost={namespaces[0]?.hourlyCost ?? 0} hasStorage={hasStorage} />
322
+ <NamespaceCostRow
323
+ key={ns.name}
324
+ ns={ns}
325
+ maxCost={namespaces[0]?.hourlyCost ?? 0}
326
+ hasStorage={hasStorage}
327
+ onOpenResource={onOpenResource}
328
+ />
205
329
  ))}
206
330
  </div>
207
331
  </div>
208
332
 
209
333
  {/* Node cost table */}
210
- {nodes.length > 0 && <NodeCostTable nodes={nodes} />}
334
+ {nodes.length > 0 && <NodeCostTable nodes={nodes} onOpenResource={onOpenResource} />}
211
335
 
212
336
  {/* Footer */}
213
337
  <div className="flex items-center justify-between text-xs text-theme-text-tertiary pb-4">
214
338
  <span>
215
- {data.currency ?? 'USD'} &middot; costs based on last 1h average &middot; *monthly estimates assume 730 hrs/mo
339
+ {data.currency ?? 'USD'} &middot; current rates based on last 1h average &middot;
340
+ *monthly projections assume {COST_HOURS_PER_MONTH} hrs/mo
216
341
  </span>
217
342
  <span className="text-indigo-500 font-medium">Powered by OpenCost</span>
218
343
  </div>
@@ -224,21 +349,43 @@ export function CostView({ onBack }: CostViewProps) {
224
349
  )
225
350
  }
226
351
 
227
- function NamespaceCostRow({ ns, maxCost, hasStorage }: { ns: OpenCostNamespaceCost; maxCost: number; hasStorage: boolean }) {
352
+ function CostOverviewState({ children }: { children: React.ReactNode }) {
353
+ return (
354
+ <div className="flex-1 overflow-y-auto">
355
+ <div className="mx-auto flex w-full max-w-[1920px] flex-col gap-4 px-6 py-6">
356
+ <PageHeader
357
+ icon={DollarSign}
358
+ title="Cost Insights"
359
+ description="Understand current allocation and find CPU and memory requests worth tuning."
360
+ />
361
+ <CostViewTabs />
362
+ {children}
363
+ </div>
364
+ </div>
365
+ )
366
+ }
367
+
368
+ function NamespaceCostRow({
369
+ ns,
370
+ maxCost,
371
+ hasStorage,
372
+ onOpenResource,
373
+ }: {
374
+ ns: OpenCostNamespaceCost
375
+ maxCost: number
376
+ hasStorage: boolean
377
+ onOpenResource?: (resource: SelectedResource) => void
378
+ }) {
228
379
  const [expanded, setExpanded] = useState(false)
229
- const monthlyCost = ns.hourlyCost * 730
230
380
  const allocTotal = ns.cpuCost + ns.memoryCost + (ns.storageCost ?? 0)
231
381
  const cpuPct = allocTotal > 0 ? (ns.cpuCost / allocTotal) * 100 : 50
232
382
  const memPct = allocTotal > 0 ? (ns.memoryCost / allocTotal) * 100 : 50
233
383
  const barWidth = maxCost > 0 ? (ns.hourlyCost / maxCost) * 100 : 0
234
- const eff = ns.efficiency ?? 0
235
- const hasEff = eff > 0
236
-
237
384
  return (
238
385
  <div>
239
386
  <button
240
387
  onClick={() => setExpanded(!expanded)}
241
- className="w-full grid grid-cols-[minmax(180px,1fr)_90px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2.5 text-left hover:bg-theme-hover/50 transition-colors group"
388
+ className="w-full grid grid-cols-[minmax(180px,1fr)_110px_90px_minmax(160px,1fr)_150px] gap-2 px-4 py-2.5 text-left hover:bg-theme-hover/50 transition-colors group"
242
389
  >
243
390
  <span className="flex items-center gap-1.5 min-w-0">
244
391
  {expanded ? (
@@ -246,46 +393,89 @@ function NamespaceCostRow({ ns, maxCost, hasStorage }: { ns: OpenCostNamespaceCo
246
393
  ) : (
247
394
  <ChevronRight className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
248
395
  )}
249
- <span className="text-sm text-theme-text-primary truncate font-medium">{ns.name}</span>
250
- </span>
251
- <span className="text-sm text-theme-text-primary tabular-nums text-right">{formatCost(ns.hourlyCost)}</span>
252
- <span className="text-sm text-theme-text-secondary tabular-nums text-right">~{formatCost(monthlyCost)}</span>
253
- <span className="text-right flex items-center justify-end gap-1">
254
- {hasEff ? (
255
- <>
256
- <span className={`text-xs font-medium tabular-nums ${efficiencyColor(eff)}`}>
257
- {eff.toFixed(0)}%
396
+ <Tooltip content={`Namespace ${ns.name}`} wrapperClassName="min-w-0">
397
+ <span className="block truncate text-sm text-theme-text-primary font-medium">
398
+ {ns.name}
399
+ </span>
400
+ </Tooltip>
401
+ {isSystemCostNamespace(ns.name) && (
402
+ <Tooltip
403
+ content="Usually platform overhead from Kubernetes and cluster add-ons. Review enabled add-ons before treating this as an application optimization target."
404
+ wrapperClassName="shrink-0"
405
+ >
406
+ <span className="rounded bg-theme-elevated px-1.5 py-0.5 text-[10px] font-medium text-theme-text-tertiary">
407
+ system
258
408
  </span>
259
-
260
- </>
261
- ) : (
262
- <span className="text-xs text-theme-text-quaternary">-</span>
409
+ </Tooltip>
263
410
  )}
264
411
  </span>
412
+ <span className="text-sm font-medium text-theme-text-primary tabular-nums text-right">
413
+ {formatProjectedMonthlyCost(ns.hourlyCost)}
414
+ </span>
415
+ <span className="text-sm text-theme-text-secondary tabular-nums text-right">
416
+ {formatCostPerHour(ns.hourlyCost)}
417
+ </span>
265
418
  <span className="flex items-center gap-2">
266
- <div className="flex-1 h-2 rounded-full overflow-hidden bg-theme-hover flex" style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}>
267
- <div className="h-full bg-blue-500/70" style={{ width: `${cpuPct}%` }} />
268
- <div className="h-full bg-purple-500/70" style={{ width: `${memPct}%` }} />
419
+ <div
420
+ className="flex-1 h-2 rounded-full overflow-hidden bg-theme-hover flex"
421
+ style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}
422
+ >
423
+ <div className="h-full bg-accent" style={{ width: `${cpuPct}%` }} />
424
+ <div className="h-full bg-amber-500" style={{ width: `${memPct}%` }} />
269
425
  {hasStorage && (ns.storageCost ?? 0) > 0 && (
270
- <div className="h-full bg-teal-500/70" style={{ width: `${100 - cpuPct - memPct}%` }} />
426
+ <div className="h-full bg-teal-500" style={{ width: `${100 - cpuPct - memPct}%` }} />
271
427
  )}
272
428
  </div>
273
429
  </span>
274
430
  <span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
275
- {formatCost(ns.cpuCost)} / {formatCost(ns.memoryCost)}
276
- {hasStorage && (ns.storageCost ?? 0) > 0 && ` / ${formatCost(ns.storageCost ?? 0)}`}
431
+ {formatProjectedMonthlyCost(ns.cpuCost)} / {formatProjectedMonthlyCost(ns.memoryCost)}
432
+ {hasStorage &&
433
+ (ns.storageCost ?? 0) > 0 &&
434
+ ` / ${formatProjectedMonthlyCost(ns.storageCost ?? 0)}`}
277
435
  </span>
278
436
  </button>
279
437
 
280
438
  {/* Expanded workload rows */}
281
- {expanded && (
282
- <WorkloadRows namespace={ns.name} />
439
+ {expanded && isSystemCostNamespace(ns.name) && (
440
+ <SystemNamespaceCostNote namespace={ns.name} />
283
441
  )}
442
+ {expanded && <WorkloadRows namespace={ns.name} onOpenResource={onOpenResource} />}
284
443
  </div>
285
444
  )
286
445
  }
287
446
 
288
- function WorkloadRows({ namespace }: { namespace: string }) {
447
+ function isSystemCostNamespace(namespace: string): boolean {
448
+ return SYSTEM_COST_NAMESPACES.has(namespace)
449
+ }
450
+
451
+ function SystemNamespaceCostNote({ namespace }: { namespace: string }) {
452
+ return (
453
+ <div className="border-t border-theme-border/30 bg-theme-elevated/30 px-10 py-2 text-xs text-theme-text-tertiary">
454
+ <span className="font-medium text-theme-text-secondary">{namespace}</span> is usually baseline
455
+ cluster overhead: Kubernetes components, cloud-provider agents, and installed add-ons.
456
+ Optimize by reviewing add-ons, logging, monitoring, or node count rather than deleting system
457
+ workloads directly.
458
+ </div>
459
+ )
460
+ }
461
+
462
+ function SystemNamespacesCostNote() {
463
+ return (
464
+ <div className="border-b border-theme-border/50 bg-theme-elevated/30 px-4 py-2 text-xs text-theme-text-tertiary">
465
+ Rows marked <span className="font-medium text-theme-text-secondary">system</span> are usually
466
+ baseline Kubernetes, cloud-provider, or add-on overhead. Optimize by reviewing enabled
467
+ add-ons, telemetry, or node count before treating them as application optimization targets.
468
+ </div>
469
+ )
470
+ }
471
+
472
+ function WorkloadRows({
473
+ namespace,
474
+ onOpenResource,
475
+ }: {
476
+ namespace: string
477
+ onOpenResource?: (resource: SelectedResource) => void
478
+ }) {
289
479
  const { data, isLoading } = useOpenCostWorkloads(namespace)
290
480
 
291
481
  if (isLoading) {
@@ -309,57 +499,94 @@ function WorkloadRows({ namespace }: { namespace: string }) {
309
499
  return (
310
500
  <div className="bg-theme-elevated/30 border-t border-theme-border/30">
311
501
  {workloads.map((wl) => (
312
- <WorkloadCostRow key={`${wl.kind}-${wl.name}`} wl={wl} maxCost={workloads[0]?.hourlyCost ?? 0} />
502
+ <WorkloadCostRow
503
+ key={`${wl.kind}-${wl.name}`}
504
+ wl={wl}
505
+ namespace={namespace}
506
+ maxCost={workloads[0]?.hourlyCost ?? 0}
507
+ onOpenResource={onOpenResource}
508
+ />
313
509
  ))}
314
510
  </div>
315
511
  )
316
512
  }
317
513
 
318
- function WorkloadCostRow({ wl, maxCost }: { wl: OpenCostWorkloadCost; maxCost: number }) {
319
- const monthlyCost = wl.hourlyCost * 730
514
+ function WorkloadCostRow({
515
+ wl,
516
+ namespace,
517
+ maxCost,
518
+ onOpenResource,
519
+ }: {
520
+ wl: OpenCostWorkloadCost
521
+ namespace: string
522
+ maxCost: number
523
+ onOpenResource?: (resource: SelectedResource) => void
524
+ }) {
320
525
  const cpuPct = wl.hourlyCost > 0 ? (wl.cpuCost / wl.hourlyCost) * 100 : 50
321
526
  const barWidth = maxCost > 0 ? (wl.hourlyCost / maxCost) * 100 : 0
322
- const eff = wl.efficiency ?? 0
323
- const hasEff = eff > 0
324
- const kindLabel = wl.kind === 'standalone' ? 'pod' : wl.kind
325
-
326
- return (
327
- <div className="grid grid-cols-[minmax(180px,1fr)_90px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2 text-left">
527
+ const kindLabel = displayCostWorkloadKind(wl.kind)
528
+ const resource = costWorkloadResource(wl, namespace)
529
+ const canOpen = Boolean(resource && onOpenResource)
530
+ const content = (
531
+ <>
328
532
  <span className="flex items-center gap-1.5 min-w-0 pl-5">
329
- <span className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded shrink-0">{kindLabel}</span>
330
- <span className="text-xs text-theme-text-secondary truncate">{wl.name}</span>
533
+ <span className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded shrink-0">
534
+ {kindLabel}
535
+ </span>
536
+ <Tooltip content={`${kindLabel} ${namespace}/${wl.name}`} wrapperClassName="min-w-0">
537
+ <span className="block truncate text-xs text-theme-text-secondary">{wl.name}</span>
538
+ </Tooltip>
331
539
  {wl.replicas > 1 && (
332
540
  <span className="text-[10px] text-theme-text-tertiary shrink-0">{wl.replicas}x</span>
333
541
  )}
334
542
  </span>
335
- <span className="text-xs text-theme-text-secondary tabular-nums text-right">{formatCost(wl.hourlyCost)}</span>
336
- <span className="text-xs text-theme-text-tertiary tabular-nums text-right">~{formatCost(monthlyCost)}</span>
337
- <span className="text-right flex items-center justify-end gap-1">
338
- {hasEff ? (
339
- <>
340
- <span className={`text-[10px] font-medium tabular-nums ${efficiencyColor(eff)}`}>
341
- {eff.toFixed(0)}%
342
- </span>
343
- {eff < 25 && <span className="text-[9px] text-red-400">low</span>}
344
- </>
345
- ) : (
346
- <span className="text-[10px] text-theme-text-quaternary">-</span>
347
- )}
543
+ <span className="text-xs font-medium text-theme-text-secondary tabular-nums text-right">
544
+ {formatProjectedMonthlyCost(wl.hourlyCost)}
545
+ </span>
546
+ <span className="text-xs text-theme-text-tertiary tabular-nums text-right">
547
+ {formatCostPerHour(wl.hourlyCost)}
348
548
  </span>
349
549
  <span className="flex items-center gap-2">
350
- <div className="flex-1 h-1.5 rounded-full overflow-hidden bg-theme-hover flex" style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}>
351
- <div className="h-full bg-blue-500/50" style={{ width: `${cpuPct}%` }} />
352
- <div className="h-full bg-purple-500/50" style={{ width: `${100 - cpuPct}%` }} />
550
+ <div
551
+ className="flex-1 h-1.5 rounded-full overflow-hidden bg-theme-hover flex"
552
+ style={{ maxWidth: `${Math.max(barWidth, 3)}%` }}
553
+ >
554
+ <div className="h-full bg-accent" style={{ width: `${cpuPct}%` }} />
555
+ <div className="h-full bg-amber-500" style={{ width: `${100 - cpuPct}%` }} />
353
556
  </div>
354
557
  </span>
355
558
  <span className="text-[10px] text-theme-text-tertiary tabular-nums text-right">
356
- {formatCost(wl.cpuCost)} / {formatCost(wl.memoryCost)}
559
+ {formatProjectedMonthlyCost(wl.cpuCost)} / {formatProjectedMonthlyCost(wl.memoryCost)}
357
560
  </span>
358
- </div>
561
+ </>
359
562
  )
563
+
564
+ const rowClass =
565
+ 'grid grid-cols-[minmax(180px,1fr)_110px_90px_minmax(160px,1fr)_150px] gap-2 px-4 py-2 text-left'
566
+
567
+ if (canOpen && resource) {
568
+ return (
569
+ <button
570
+ type="button"
571
+ onClick={() => onOpenResource?.(resource)}
572
+ className={`${rowClass} w-full transition-colors hover:bg-theme-hover/60 focus:outline-none focus:ring-2 focus:ring-accent focus:ring-inset`}
573
+ aria-label={`Open ${kindLabel} ${wl.name}`}
574
+ >
575
+ {content}
576
+ </button>
577
+ )
578
+ }
579
+
580
+ return <div className={rowClass}>{content}</div>
360
581
  }
361
582
 
362
- function NodeCostTable({ nodes }: { nodes: OpenCostNodeCost[] }) {
583
+ function NodeCostTable({
584
+ nodes,
585
+ onOpenResource,
586
+ }: {
587
+ nodes: OpenCostNodeCost[]
588
+ onOpenResource?: (resource: SelectedResource) => void
589
+ }) {
363
590
  return (
364
591
  <div className="rounded-lg border border-theme-border bg-theme-surface/50">
365
592
  <div className="px-4 py-3 border-b border-theme-border">
@@ -379,49 +606,127 @@ function NodeCostTable({ nodes }: { nodes: OpenCostNodeCost[] }) {
379
606
  </div>
380
607
 
381
608
  {/* Table header */}
382
- <div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_90px_100px_140px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
609
+ <div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_110px_90px_150px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
383
610
  <span>Node</span>
384
611
  <span>Instance Type</span>
612
+ <Tooltip
613
+ content="Projected from current hourly rate — not historical spend"
614
+ wrapperClassName="!block text-right"
615
+ >
616
+ <span className="cursor-help">Projected/mo*</span>
617
+ </Tooltip>
385
618
  <span className="text-right">Hourly</span>
386
- <Tooltip content="Projected from current hourly rate — not historical spend" wrapperClassName="!block text-right">
387
- <span className="cursor-help">Monthly*</span>
619
+ <Tooltip
620
+ content="Projected monthly CPU and memory portions of this node's current price"
621
+ wrapperClassName="!block text-right"
622
+ >
623
+ <span className="cursor-help">CPU / Memory/mo*</span>
388
624
  </Tooltip>
389
- <span className="text-right">CPU / Memory</span>
390
625
  </div>
391
626
 
392
627
  {/* Node rows */}
393
628
  <div className="divide-y divide-theme-border/50">
394
629
  {nodes.map((node) => (
395
- <NodeCostRow key={node.name} node={node} />
630
+ <NodeCostRow key={node.name} node={node} onOpenResource={onOpenResource} />
396
631
  ))}
397
632
  </div>
398
633
  </div>
399
634
  )
400
635
  }
401
636
 
402
- function NodeCostRow({ node }: { node: OpenCostNodeCost }) {
403
- const monthlyCost = node.hourlyCost * 730
637
+ function NodeCostRow({
638
+ node,
639
+ onOpenResource,
640
+ }: {
641
+ node: OpenCostNodeCost
642
+ onOpenResource?: (resource: SelectedResource) => void
643
+ }) {
644
+ const cloudLink = nodeCloudConsoleLink(node.providerID)
645
+ const openNode = () => onOpenResource?.({ kind: 'nodes', namespace: '', name: node.name })
404
646
 
405
647
  return (
406
- <div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_90px_100px_140px] gap-2 px-4 py-2.5">
407
- <Tooltip content={node.name} wrapperClassName="!block min-w-0">
408
- <span className="text-sm text-theme-text-primary truncate font-medium block">
409
- {node.name}
648
+ <div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_110px_90px_150px] gap-2 px-4 py-2.5">
649
+ <span className="flex min-w-0 items-center gap-1.5">
650
+ <Tooltip content={`Open Node ${node.name}`} wrapperClassName="!block min-w-0">
651
+ {onOpenResource ? (
652
+ <button
653
+ type="button"
654
+ onClick={openNode}
655
+ className="block min-w-0 truncate text-left text-sm font-medium text-theme-text-primary transition-colors hover:text-accent-text focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-1 focus:ring-offset-theme-base"
656
+ >
657
+ {node.name}
658
+ </button>
659
+ ) : (
660
+ <span className="text-sm text-theme-text-primary truncate font-medium block">
661
+ {node.name}
662
+ </span>
663
+ )}
664
+ </Tooltip>
665
+ {cloudLink && (
666
+ <Tooltip content={cloudLink.label}>
667
+ <button
668
+ type="button"
669
+ onClick={() => openExternal(cloudLink.url)}
670
+ className="shrink-0 rounded p-0.5 text-theme-text-tertiary transition-colors hover:bg-theme-hover hover:text-accent-text focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-1 focus:ring-offset-theme-base"
671
+ aria-label={`${cloudLink.label}: ${node.name}`}
672
+ >
673
+ <ExternalLink className="h-3.5 w-3.5" />
674
+ </button>
675
+ </Tooltip>
676
+ )}
410
677
  </span>
411
- </Tooltip>
412
678
  <span className="text-xs text-theme-text-secondary truncate">
413
679
  {node.instanceType || '-'}
414
680
  {node.region && <span className="text-theme-text-quaternary ml-1.5">({node.region})</span>}
415
681
  </span>
416
- <span className="text-sm text-theme-text-primary tabular-nums text-right">{formatCost(node.hourlyCost)}</span>
417
- <span className="text-sm text-theme-text-secondary tabular-nums text-right">~{formatCost(monthlyCost)}</span>
682
+ <span className="text-sm font-medium text-theme-text-primary tabular-nums text-right">
683
+ {formatProjectedMonthlyCost(node.hourlyCost)}
684
+ </span>
685
+ <span className="text-sm text-theme-text-secondary tabular-nums text-right">
686
+ {formatCostPerHour(node.hourlyCost)}
687
+ </span>
418
688
  <span className="text-[11px] text-theme-text-tertiary tabular-nums text-right">
419
- {formatCost(node.cpuCost)} / {formatCost(node.memoryCost)}
689
+ {formatProjectedMonthlyCost(node.cpuCost)} / {formatProjectedMonthlyCost(node.memoryCost)}
420
690
  </span>
421
691
  </div>
422
692
  )
423
693
  }
424
694
 
695
+ function displayCostWorkloadKind(kind: string): string {
696
+ if (kind === 'standalone') return 'pod'
697
+ if (kind === 'staticpod') return 'static pod'
698
+ return kind
699
+ }
700
+
701
+ function costWorkloadResource(
702
+ wl: OpenCostWorkloadCost,
703
+ namespace: string,
704
+ ): SelectedResource | null {
705
+ const kind = resourceKindForCostWorkload(wl.kind)
706
+ if (!kind || !wl.name) return null
707
+ return {
708
+ kind: kindToPlural(kind),
709
+ namespace: kind === 'Node' ? '' : namespace,
710
+ name: wl.name,
711
+ group: apiGroupForCostWorkload(kind),
712
+ }
713
+ }
714
+
715
+ export function resourceKindForCostWorkload(kind: string): string | null {
716
+ if (kind === 'staticpod') return 'Pod'
717
+ if (kind === 'standalone') return null
718
+ if (kind === 'Deployment' || kind === 'StatefulSet' || kind === 'DaemonSet') return kind
719
+ if (kind === 'Job' || kind === 'CronJob') return kind
720
+ if (kind === 'Node') return kind
721
+ return null
722
+ }
723
+
724
+ function apiGroupForCostWorkload(kind: string): string | undefined {
725
+ if (kind === 'Deployment' || kind === 'StatefulSet' || kind === 'DaemonSet') return 'apps'
726
+ if (kind === 'Job' || kind === 'CronJob') return 'batch'
727
+ return undefined
728
+ }
729
+
425
730
  // --- Help dialog ---
426
731
 
427
732
  function CostHelpDialog({ onClose }: { onClose: () => void }) {
@@ -441,7 +746,9 @@ function CostHelpDialog({ onClose }: { onClose: () => void }) {
441
746
  <div className="flex items-center justify-between p-4 border-b border-theme-border sticky top-0 bg-theme-surface rounded-t-lg">
442
747
  <div className="flex items-center gap-2">
443
748
  <HelpCircle className="w-5 h-5 text-indigo-500" />
444
- <h2 className="text-base font-semibold text-theme-text-primary">Understanding Cost Data</h2>
749
+ <h2 className="text-base font-semibold text-theme-text-primary">
750
+ Understanding Cost Data
751
+ </h2>
445
752
  </div>
446
753
  <button
447
754
  onClick={onClose}
@@ -454,77 +761,59 @@ function CostHelpDialog({ onClose }: { onClose: () => void }) {
454
761
  <div className="p-4 space-y-5 text-sm text-theme-text-secondary">
455
762
  {/* Where costs come from */}
456
763
  <section>
457
- <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">Where do these costs come from?</h3>
764
+ <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">
765
+ Where do these costs come from?
766
+ </h3>
458
767
  <p>
459
- Cost data comes from <strong>OpenCost</strong>, an open-source tool that combines your cloud provider's
460
- pricing (how much each node costs per hour) with Kubernetes resource allocation data. This gives you
461
- a dollar value for each workload running on your cluster.
768
+ Cost data comes from <strong>OpenCost</strong>, an open-source tool that combines your
769
+ cloud provider's pricing (how much each node costs per hour) with Kubernetes resource
770
+ allocation data. This gives you a dollar value for each workload running on your
771
+ cluster.
462
772
  </p>
463
773
  </section>
464
774
 
465
775
  {/* What costs represent */}
466
776
  <section>
467
- <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">What does "hourly cost" mean?</h3>
777
+ <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">
778
+ What do monthly, daily, and hourly cost mean?
779
+ </h3>
468
780
  <p>
469
- Each workload <strong>requests</strong> a certain amount of CPU and memory when it's deployed.
470
- These requests reserve capacity on a node that reserved capacity has a cost based on
471
- the node's cloud pricing, whether the workload actually uses it or not.
781
+ OpenCost assigns CPU and memory allocation using the greater of a workload's request
782
+ or observed use, then applies the node's cloud pricing. Allocation cost is therefore
783
+ useful for attribution, but it is not a direct measurement of request headroom.
472
784
  </p>
473
785
  <p className="mt-1.5">
474
- The hourly cost shown here is based on what your workloads have <strong>reserved</strong> (requested),
475
- not what they're actually consuming. Monthly estimates simply multiply the current hourly rate by 730 hours.
786
+ Projected monthly and daily numbers multiply the current hourly allocation rate. They
787
+ are useful for budget impact, but they are not a historical invoice total. Historical
788
+ spend on application and workload tabs uses the selected range.
476
789
  </p>
477
790
  </section>
478
791
 
479
- {/* Efficiency */}
480
- <section>
481
- <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">What is efficiency?</h3>
482
- <p>
483
- Efficiency compares what you're <strong>actually using</strong> versus what you've <strong>reserved</strong>,
484
- weighted by cost. If a namespace reserves $1/hr of resources but only uses $0.40 worth, it's 40% efficient —
485
- the other $0.60/hr is idle capacity you're paying for but not using.
486
- </p>
487
- <p className="mt-2 text-theme-text-tertiary text-xs">
488
- Some over-provisioning is normal and healthy — it gives your workloads room to handle
489
- traffic spikes without running out of resources. Don't aim for 100%.
490
- </p>
491
- <div className="mt-2 space-y-1">
492
- <div className="flex items-center gap-2">
493
- <span className="w-2 h-2 rounded-full bg-emerald-400" />
494
- <span><strong>50%+</strong> — well-utilized</span>
495
- </div>
496
- <div className="flex items-center gap-2">
497
- <span className="w-2 h-2 rounded-full bg-amber-400" />
498
- <span><strong>25–50%</strong> — typical for most clusters, some room to optimize</span>
499
- </div>
500
- <div className="flex items-center gap-2">
501
- <span className="w-2 h-2 rounded-full bg-red-400" />
502
- <span><strong>Below 25%</strong> — worth investigating, may be significantly over-provisioned</span>
503
- </div>
504
- </div>
505
- </section>
506
-
507
792
  {/* Time context */}
508
793
  <section>
509
- <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">How fresh is this data?</h3>
794
+ <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">
795
+ How fresh is this data?
796
+ </h3>
510
797
  <p>
511
- Cost rates, efficiency, and breakdowns are <strong>snapshots based on the last 1 hour</strong> of data.
512
- They update automatically every minute. The trend chart is the only historical view — it shows how
513
- total cost has changed over the selected time range (6 hours, 24 hours, or 7 days).
798
+ Cost rates and breakdowns are <strong>snapshots based on the last 1 hour</strong> of
799
+ data. They update automatically every minute. The trend chart shows historical hourly
800
+ allocation rate over the selected time range (6 hours, 24 hours, or 7 days).
514
801
  </p>
515
802
  <p className="mt-1.5">
516
- Because costs are based on a 1-hour window, short-lived spikes or dips may not be reflected.
517
- The trend chart gives you the longer-term picture.
803
+ Because costs are based on a 1-hour window, short-lived spikes or dips may not be
804
+ reflected. The trend chart gives you the longer-term rate picture.
518
805
  </p>
519
806
  </section>
520
807
 
521
808
  {/* Node costs */}
522
809
  <section>
523
- <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">What are node costs?</h3>
810
+ <h3 className="text-sm font-semibold text-theme-text-primary mb-1.5">
811
+ What are node costs?
812
+ </h3>
524
813
  <p>
525
- Node costs show the hourly price of each machine in your cluster, based on instance type and
526
- cloud pricing. This is the total capacity cost — the namespace and workload breakdowns above
527
- show how that capacity is allocated across your workloads.
814
+ Node costs show the hourly price of each machine in your cluster, based on instance
815
+ type and cloud pricing. This is the total capacity cost — the namespace and workload
816
+ breakdowns above show how that capacity is allocated across your workloads.
528
817
  </p>
529
818
  </section>
530
819
  </div>
@@ -532,27 +821,3 @@ function CostHelpDialog({ onClose }: { onClose: () => void }) {
532
821
  </div>
533
822
  )
534
823
  }
535
-
536
- // --- Utilities ---
537
-
538
- function efficiencyColor(efficiency: number): string {
539
- if (efficiency >= 50) return 'text-emerald-400'
540
- if (efficiency >= 25) return 'text-amber-400'
541
- return 'text-red-400'
542
- }
543
-
544
- function formatCost(value: number): string {
545
- if (value >= 1000) {
546
- return `$${(value / 1000).toFixed(1)}k`
547
- }
548
- if (value >= 1) {
549
- return `$${value.toFixed(2)}`
550
- }
551
- if (value >= 0.01) {
552
- return `$${value.toFixed(3)}`
553
- }
554
- if (value > 0) {
555
- return `$${value.toFixed(4)}`
556
- }
557
- return '$0.00'
558
- }