@skyhook-io/radar-app 1.8.13 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,13 +27,14 @@ import {
27
27
  type Diagnosis,
28
28
  type DiagnoseStep,
29
29
  type AgentInfo,
30
+ type ExecutionProfile,
30
31
  type RunSummary,
31
32
  } from "../../api/diagnose";
32
33
  import { StatusDot } from "@skyhook-io/k8s-ui";
33
34
  import { Markdown } from "../ui/Markdown";
34
35
 
35
- // Segmented two-or-more-way selector — shared shape for the agent picker and the
36
- // isolation toggle.
36
+ // Segmented two-or-more-way selector — shared shape for the agent and execution
37
+ // profile pickers.
37
38
  function Segmented<T extends string | boolean>({
38
39
  label,
39
40
  options,
@@ -228,15 +229,15 @@ function SelectMenu({
228
229
  );
229
230
  }
230
231
 
231
- // AgentControls is the full AI-diagnosis config block (agent, isolation, model,
232
+ // AgentControls is the full AI-diagnosis config block (agent, execution profile, model,
232
233
  // effort) — pure + prop-driven. It lives in Settings, not the investigation panel,
233
234
  // since these are set-once preferences rather than per-run knobs.
234
235
  export function AgentControls({
235
236
  agents,
236
237
  selectedAgent,
237
238
  onSelectAgent,
238
- isolated,
239
- onSetIsolated,
239
+ profile,
240
+ onSetProfile,
240
241
  model,
241
242
  onSetModel,
242
243
  effort,
@@ -245,8 +246,8 @@ export function AgentControls({
245
246
  agents: AgentInfo[];
246
247
  selectedAgent: string;
247
248
  onSelectAgent: (name: string) => void;
248
- isolated: boolean;
249
- onSetIsolated: (v: boolean) => void;
249
+ profile: ExecutionProfile;
250
+ onSetProfile: (v: ExecutionProfile) => void;
250
251
  model: string;
251
252
  onSetModel: (v: string) => void;
252
253
  effort: string;
@@ -255,6 +256,17 @@ export function AgentControls({
255
256
  const isCodex = selectedAgent === "codex";
256
257
  const isClaude = selectedAgent === "claude";
257
258
  const isCursor = selectedAgent === "cursor-agent";
259
+ const selectedAgentInfo = agents.find((a) => a.name === selectedAgent);
260
+ const selectedAgentLabel =
261
+ selectedAgentInfo?.label || selectedAgent || "agent";
262
+ const profiles = selectedAgentInfo?.profiles ?? [];
263
+ const shownProfile = profiles.includes(profile)
264
+ ? profile
265
+ : (profiles[0] ?? profile);
266
+ const profileLabels: Record<ExecutionProfile, string> = {
267
+ safeguarded: "Radar safeguards",
268
+ "full-local": `Your ${selectedAgentLabel} setup`,
269
+ };
258
270
  return (
259
271
  <div className="space-y-3">
260
272
  {agents.length >= 2 && (
@@ -268,29 +280,66 @@ export function AgentControls({
268
280
  }))}
269
281
  />
270
282
  )}
271
- {isCodex && (
283
+ {profiles.length > 0 && (
272
284
  <div>
273
- <Segmented<boolean>
274
- label="Environment"
275
- value={isolated}
276
- onChange={onSetIsolated}
277
- options={[
278
- { value: true, label: "Isolated (recommended)" },
279
- { value: false, label: "My setup" },
280
- ]}
281
- />
282
- {isolated ? (
283
- <p className="mt-1.5 text-[11px] leading-snug text-theme-text-tertiary">
284
- Runs Codex on its own — no access to your other MCP servers,
285
- guidelines, or project files.
286
- </p>
285
+ {profiles.length > 1 ? (
286
+ <>
287
+ <Segmented<ExecutionProfile>
288
+ label="How Radar runs it"
289
+ value={shownProfile}
290
+ onChange={onSetProfile}
291
+ options={profiles.map((value) => ({
292
+ value,
293
+ label: profileLabels[value],
294
+ }))}
295
+ />
296
+ {shownProfile === "safeguarded" ? (
297
+ <p className="mt-1.5 text-[11px] leading-snug text-theme-text-tertiary">
298
+ {isClaude
299
+ ? "Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
300
+ : isCodex
301
+ ? "Radar excludes your Codex configuration and other MCP servers. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
302
+ : "Radar uses this agent’s safeguarded execution profile. Review the agent’s documented restrictions before continuing."}
303
+ </p>
304
+ ) : (
305
+ <div className="mt-1.5 flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
306
+ <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
307
+ <span>
308
+ Uses your agent&apos;s normal configuration and other
309
+ configured tools and MCP servers. Radar cannot constrain that
310
+ external tooling; it may access local files or the network
311
+ and may be able to change your cluster.{" "}
312
+ {isClaude
313
+ ? "Claude uses the permissions from your setup; Radar does not override them."
314
+ : "Radar still enables the agent CLI’s own sandbox, but that sandbox does not constrain external MCP servers."}{" "}
315
+ Choose this only when you need that setup.
316
+ </span>
317
+ </div>
318
+ )}
319
+ </>
320
+ ) : shownProfile === "safeguarded" ? (
321
+ <div className="flex items-start gap-1.5 rounded border border-theme-border bg-theme-base p-2 text-[11px] leading-snug text-theme-text-secondary">
322
+ <ShieldCheck className="mt-0.5 h-3 w-3 shrink-0 text-accent" />
323
+ <span>
324
+ Radar always runs this agent with safeguards.
325
+ {isClaude
326
+ ? " Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
327
+ : isCodex
328
+ ? " Your Codex configuration and other MCP servers are excluded. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
329
+ : " Review the agent’s documented restrictions before continuing."}
330
+ </span>
331
+ </div>
287
332
  ) : (
288
- <div className="mt-1.5 flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
333
+ <div className="flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
289
334
  <AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
290
335
  <span>
291
- Runs Codex with your full setup your own MCP servers (which may
292
- be write- or network-capable) and guidelines, and it can read
293
- local files. Only choose this if you rely on that config.
336
+ Radar must use this agent&apos;s normal setup. Radar cannot
337
+ constrain its external tools or MCP servers; they may access
338
+ local files or the network and may be able to change your
339
+ cluster. Radar still enables the agent CLI&apos;s own sandbox,
340
+ but that sandbox does not constrain external MCP servers.
341
+ {isCursor &&
342
+ " Cursor always loads your global MCP servers, so Radar cannot exclude them."}
294
343
  </span>
295
344
  </div>
296
345
  )}
@@ -304,7 +353,7 @@ export function AgentControls({
304
353
  onChange={onSetModel}
305
354
  hint="Aliases always resolve to the latest of that tier."
306
355
  />
307
- ) : (
356
+ ) : isCodex || isCursor ? (
308
357
  <TextField
309
358
  label="Model"
310
359
  value={model}
@@ -317,11 +366,19 @@ export function AgentControls({
317
366
  hint={
318
367
  isCursor
319
368
  ? "Leave empty for your Cursor default, or enter a model slug Cursor supports."
320
- : !isolated
321
- ? "“My setup uses your own Codex config's model; set a slug here to override it."
369
+ : shownProfile === "full-local"
370
+ ? "Your Codex setup uses its configured model; set a slug here to override it."
322
371
  : "Leave empty for Codex's default, or enter a model your Codex version supports."
323
372
  }
324
373
  />
374
+ ) : (
375
+ <TextField
376
+ label="Model"
377
+ value={model}
378
+ placeholder="Default"
379
+ onChange={onSetModel}
380
+ hint="Leave empty for the agent's default, or enter a model identifier it supports."
381
+ />
325
382
  )}
326
383
  {isCodex && (
327
384
  <SelectMenu
@@ -591,10 +648,12 @@ function ConsentCardShell({
591
648
  bullets,
592
649
  settingsLabel,
593
650
  approveLabel = "Approve & investigate",
651
+ warning = false,
594
652
  onOpenSettings,
595
653
  onApprove,
596
654
  onCancel,
597
655
  }: DiagnoseConsentCopy & {
656
+ warning?: boolean;
598
657
  onOpenSettings?: () => void;
599
658
  onApprove: () => void;
600
659
  onCancel: () => void;
@@ -606,9 +665,19 @@ function ConsentCardShell({
606
665
  ? "Change the agent and how it runs in Settings"
607
666
  : settingsLabel;
608
667
  return (
609
- <div className="rounded-lg border border-theme-border bg-theme-elevated p-4">
668
+ <div
669
+ className={
670
+ warning
671
+ ? "rounded-lg border border-amber-500/40 bg-amber-500/10 p-4"
672
+ : "rounded-lg border border-theme-border bg-theme-elevated p-4"
673
+ }
674
+ >
610
675
  <div className="mb-2 flex items-center gap-2">
611
- <ShieldCheck className="h-4 w-4 text-accent" />
676
+ {warning ? (
677
+ <AlertTriangle className="h-4 w-4 text-amber-500" />
678
+ ) : (
679
+ <ShieldCheck className="h-4 w-4 text-accent" />
680
+ )}
612
681
  <div className="text-sm font-medium text-theme-text-primary">
613
682
  {title}
614
683
  </div>
@@ -660,7 +729,7 @@ function ConsentCardShell({
660
729
  export function ConsentCard({
661
730
  agentName,
662
731
  agent,
663
- isolated = true,
732
+ profile,
664
733
  copy,
665
734
  onOpenSettings,
666
735
  onApprove,
@@ -668,7 +737,7 @@ export function ConsentCard({
668
737
  }: {
669
738
  agentName: string;
670
739
  agent?: string;
671
- isolated?: boolean;
740
+ profile: ExecutionProfile;
672
741
  copy?: DiagnoseConsentCopy;
673
742
  onOpenSettings?: () => void;
674
743
  onApprove: () => void;
@@ -679,17 +748,19 @@ export function ConsentCard({
679
748
  // Tier 1: a host (e.g. radar-hub-web) supplied its own copy — use it verbatim.
680
749
  if (copy) return <ConsentCardShell {...copy} {...chrome} />;
681
750
 
682
- // Tier 2: OSS BYO-local default. Cursor can't be isolated (no flag suppresses
683
- // its global MCP servers), so it gets its own honest framing rather than the
684
- // isolated/my-setup pair.
685
- const isCursor = agent === "cursor-agent";
686
751
  return (
687
752
  <ConsentCardShell
688
753
  {...chrome}
754
+ warning={profile === "full-local"}
755
+ approveLabel={
756
+ profile === "full-local"
757
+ ? "Continue with my agent setup"
758
+ : "Approve & investigate"
759
+ }
689
760
  title={
690
- isolated && !isCursor
691
- ? "Run a read-only AI investigation?"
692
- : "Run an AI investigation?"
761
+ profile === "safeguarded"
762
+ ? "Run an AI investigation with Radar safeguards?"
763
+ : `Run using your ${agentName} setup?`
693
764
  }
694
765
  body={
695
766
  <>
@@ -698,48 +769,68 @@ export function ConsentCard({
698
769
  your own {agentName}
699
770
  </span>{" "}
700
771
  on your machine — no Radar cloud, no API key, no account. Radar sends
701
- this resource&apos;s spec, recent events, and pod logs to it (and on to
702
- its model provider under your account, not to Radar). Transcripts are
703
- kept in your local Radar history on this machine until cleared.
704
- {isolated && !isCursor && (
772
+ this resource&apos;s spec, recent events, and pod logs to it (and on
773
+ to its model provider under your account, not to Radar). Transcripts
774
+ are kept in your local Radar history on this machine until cleared.
775
+ {profile === "safeguarded" && (
705
776
  <>
706
777
  {" "}
707
- Through Radar the agent can only{" "}
708
- <span className="font-medium">read</span> it cannot change your
709
- cluster.
778
+ Radar&apos;s investigation tools can only{" "}
779
+ <span className="font-medium">read</span> your cluster.
710
780
  </>
711
781
  )}
712
782
  </>
713
783
  }
714
- bullets={[
715
- isCursor ? (
716
- <>
717
- Through Radar the agent only{" "}
718
- <span className="font-medium">reads</span> your cluster. But Cursor
719
- also loads your own global MCP servers and Radar can&apos;t exclude
720
- them (unlike Claude or Codex), so if any of those can make changes,
721
- Cursor could use them.
722
- </>
723
- ) : isolated ? (
724
- <>
725
- Isolated: only Radar&apos;s read-only investigation tools — your
726
- other CLI config and MCP servers are excluded.
727
- {agent === "codex" && (
784
+ bullets={
785
+ profile === "safeguarded"
786
+ ? [
787
+ agent === "claude" ? (
788
+ <>
789
+ Radar safeguards disable Claude&apos;s built-in tools and limit
790
+ MCP access to Radar&apos;s read-only investigation tools. Your
791
+ Claude settings, hooks, and CLAUDE.md instructions still apply
792
+ and are outside Radar&apos;s control.
793
+ </>
794
+ ) : agent === "codex" ? (
795
+ <>
796
+ Radar safeguards exclude your Codex configuration and other MCP
797
+ servers. Codex&apos;s sandboxed shell can still read files on
798
+ this machine; it cannot write or reach the network.
799
+ </>
800
+ ) : (
801
+ <>
802
+ Radar uses this agent&apos;s safeguarded execution profile.
803
+ Review the agent&apos;s documented restrictions before
804
+ continuing.
805
+ </>
806
+ ),
807
+ ]
808
+ : [
728
809
  <>
729
- {" "}
730
- Codex&apos;s sandboxed shell can still <em>read</em> files on
731
- your machine (it cannot write or reach the network).
732
- </>
733
- )}
734
- </>
735
- ) : (
736
- <>
737
- &ldquo;My setup&rdquo;: the agent also runs with your own CLI config
738
- + MCP servers and can read local files. Only Radar&apos;s own tools
739
- are read-only.
740
- </>
741
- ),
742
- ]}
810
+ Radar cannot constrain the agent&apos;s other configured tools or
811
+ MCP servers. They may access local files or the network and may
812
+ be able to change your cluster.
813
+ </>,
814
+ agent === "claude" ? (
815
+ <>
816
+ Claude uses the permissions from your setup; Radar does not
817
+ override them.
818
+ </>
819
+ ) : (
820
+ <>
821
+ Radar still enables the agent CLI&apos;s own sandbox, but that
822
+ sandbox does not constrain external MCP servers.
823
+ {agent === "cursor-agent" && (
824
+ <>
825
+ {" "}
826
+ Cursor always loads your global MCP servers; Radar cannot
827
+ exclude them.
828
+ </>
829
+ )}
830
+ </>
831
+ ),
832
+ ]
833
+ }
743
834
  />
744
835
  );
745
836
  }
@@ -141,7 +141,7 @@ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
141
141
  params: [
142
142
  { arg: 'namespace', desc: 'filter to a specific namespace' },
143
143
  { arg: 'category', desc: 'Security, Reliability, or Efficiency' },
144
- { arg: 'severity', desc: 'danger or warning' },
144
+ { arg: 'severity', desc: 'posture priority: critical, high, medium, or low (built-ins use high or medium)' },
145
145
  { arg: 'limit', desc: 'max findings (default 30, max 100)' },
146
146
  ],
147
147
  },
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'
4
4
  import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, useCapabilities, useNamespaceCapabilities, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
5
5
  import { isBadgeWorthy } from '../../utils/auditBadges'
6
6
  import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
7
- import { apiUrl, getAuthHeaders, getCredentialsMode, getBasename } from '../../api/config'
7
+ import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../../api/config'
8
8
  import { useAPIResources } from '../../api/apiResources'
9
9
  import { useConnection } from '../../context/ConnectionContext'
10
10
  import { initNavigationMap } from '@skyhook-io/k8s-ui'
@@ -198,7 +198,13 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
198
198
  const podCountAllowsBulkMetrics = countsData != null && podCountKnown && !podCountUnavailable && (podCount ?? 0) <= LARGE_RESOURCE_LIST_LIMIT
199
199
  const selectedKindName = selectedKind?.name.toLowerCase() ?? ''
200
200
  const topPodMetricsEnabled = selectedKindName === 'pods' && podCountAllowsBulkMetrics
201
- const topNodeMetricsEnabled = selectedKindName === 'nodes' && namespaces.length === 0 && podCountAllowsBulkMetrics
201
+ // Node metrics back the Nodes table and, for the Pods table, the pod-vs-node
202
+ // context line in the CPU/Memory tooltip (a pod can be fine against its own
203
+ // limit yet at risk from a saturated node). Nodes are cluster-wide, so the
204
+ // pods case is not gated on the namespace filter.
205
+ const topNodeMetricsEnabled =
206
+ ((selectedKindName === 'nodes' && namespaces.length === 0) || selectedKindName === 'pods') &&
207
+ podCountAllowsBulkMetrics
202
208
  const largeListGuard = selectedKind && largeListBlocked
203
209
  ? {
204
210
  kind: selectedKind.name,
@@ -295,13 +301,8 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
295
301
  // any host that mounts RadarApp under a non-empty basename (Radar Cloud).
296
302
  // Strip the basename here so react-router can re-apply it cleanly.
297
303
  const handleNavigate = useMemo(() => {
298
- const base = getBasename()
299
304
  return (path: string, options?: { replace?: boolean }) => {
300
- let p = path
301
- if (base && (p === base || p.startsWith(base + '/') || p.startsWith(base + '?'))) {
302
- p = p.slice(base.length) || '/'
303
- }
304
- navigate(p, { replace: options?.replace })
305
+ navigate(stripBasename(path), { replace: options?.replace })
305
306
  }
306
307
  }, [navigate])
307
308
 
@@ -60,6 +60,7 @@ interface ConfigResponse {
60
60
  interface SettingsDialogProps {
61
61
  open: boolean
62
62
  onClose: () => void
63
+ initialSection?: SettingsSectionId
63
64
  }
64
65
 
65
66
  // The settings surface splits into three honest apply buckets:
@@ -68,7 +69,7 @@ interface SettingsDialogProps {
68
69
  // • Live integrations (Prometheus, Argo CD) — their own Apply/Connect endpoints
69
70
  // re-point the running server; effect immediately, NOT part of footer dirty.
70
71
  // • AI diagnose — client-side prefs, self-saving, editable by everyone.
71
- type SectionId =
72
+ export type SettingsSectionId =
72
73
  | 'overview' | 'perms' | 'connection' | 'prometheus' | 'argocd' | 'ai' | 'advanced'
73
74
 
74
75
  // Only STARTUP fields count toward footer dirty. Integration fields (prometheusUrl,
@@ -89,7 +90,11 @@ function normalizeStartup(c: Config) {
89
90
  }
90
91
  }
91
92
 
92
- export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
93
+ export function SettingsDialog({
94
+ open,
95
+ onClose,
96
+ initialSection = 'overview',
97
+ }: SettingsDialogProps) {
93
98
  const dialogRef = useRef<HTMLDivElement>(null)
94
99
  const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
95
100
  const { data: versionInfo } = useVersionCheck()
@@ -107,7 +112,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
107
112
  const [saving, setSaving] = useState(false)
108
113
  const [saveMessage, setSaveMessage] = useState<string | null>(null)
109
114
  const [loadError, setLoadError] = useState<string | null>(null)
110
- const [section, setSection] = useState<SectionId>('overview')
115
+ const [section, setSection] = useState<SettingsSectionId>('overview')
111
116
  const [confirmingClose, setConfirmingClose] = useState(false)
112
117
 
113
118
  // AI Diagnosis prefs are client-side (localStorage) and now SELF-SAVING: the
@@ -117,14 +122,14 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
117
122
  const aiAvailable = diag.available && diag.agents.length > 0
118
123
  const [aiDraft, setAiDraft] = useState<AIDraft>({
119
124
  agent: diag.selectedAgent,
120
- isolated: diag.isolated,
125
+ profile: diag.profile,
121
126
  model: diag.model,
122
127
  effort: diag.effort,
123
128
  })
124
129
  const [aiSaved, setAiSaved] = useState(false)
125
130
  const aiDirty =
126
131
  aiDraft.agent !== diag.selectedAgent ||
127
- aiDraft.isolated !== diag.isolated ||
132
+ aiDraft.profile !== diag.profile ||
128
133
  aiDraft.model !== diag.model ||
129
134
  aiDraft.effort !== diag.effort
130
135
 
@@ -158,16 +163,10 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
158
163
  setAiSaved(false)
159
164
  setAiDraft({
160
165
  agent: diag.selectedAgent,
161
- isolated: diag.isolated,
166
+ profile: diag.profile,
162
167
  model: diag.model,
163
168
  effort: diag.effort,
164
169
  })
165
- // Overview is the landing section — a status-at-a-glance of what Radar is
166
- // connected to (cluster, integrations, MCP, AI), useful to owners and
167
- // viewers alike, rather than dropping owners on a config form or everyone
168
- // on a permissions dump.
169
- setSection('overview')
170
-
171
170
  fetch(apiUrl('/config'), { credentials: getCredentialsMode(), headers: getAuthHeaders() })
172
171
  .then((res) => {
173
172
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
@@ -185,6 +184,19 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
185
184
  // eslint-disable-next-line react-hooks/exhaustive-deps
186
185
  }, [open])
187
186
 
187
+ useEffect(() => {
188
+ if (open) setSection(initialSection)
189
+ }, [open, initialSection])
190
+
191
+ useEffect(() => {
192
+ if (!open || diag.agents.length === 0) return
193
+ setAiDraft((current) => {
194
+ const profiles = diag.agents.find((agent) => agent.name === current.agent)?.profiles ?? []
195
+ if (profiles.length === 0 || profiles.includes(current.profile)) return current
196
+ return { ...current, profile: profiles[0] }
197
+ })
198
+ }, [open, diag.agents])
199
+
188
200
  const updateConfigField = useCallback(<K extends keyof Config>(field: K, value: Config[K]) => {
189
201
  setEditedConfig((prev) => ({ ...prev, [field]: value }))
190
202
  setSaveMessage(null)
@@ -234,7 +246,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
234
246
  // agent first, then restore the draft's model/effort.
235
247
  const saveAi = useCallback(() => {
236
248
  diag.setSelectedAgent(aiDraft.agent)
237
- diag.setIsolated(aiDraft.isolated)
249
+ diag.setProfile(aiDraft.profile)
238
250
  diag.setModel(aiDraft.model)
239
251
  diag.setEffort(aiDraft.effort)
240
252
  setAiSaved(true)
@@ -335,7 +347,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
335
347
  // Fixed height so the dialog doesn't jump when switching tabs — short
336
348
  // tabs leave breathing room, tall ones scroll inside the content pane.
337
349
  // max-h keeps it on-screen on short viewports.
338
- 'sm:rounded-xl sm:max-w-4xl sm:mx-4 sm:h-[620px] sm:max-h-[85vh]',
350
+ 'sm:rounded-xl sm:max-w-4xl sm:mx-4 sm:h-[660px] sm:max-h-[85vh]',
339
351
  TRANSITION_PANEL,
340
352
  isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
341
353
  )}
@@ -667,7 +679,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
667
679
  // -- Sidebar primitives -------------------------------------------------------
668
680
 
669
681
  interface NavItemDef {
670
- id: SectionId
682
+ id: SettingsSectionId
671
683
  label: string
672
684
  icon: LucideIcon
673
685
  ownerOnly: boolean
@@ -748,8 +760,8 @@ function SectionPane({
748
760
  locked,
749
761
  children,
750
762
  }: {
751
- id: SectionId
752
- active: SectionId
763
+ id: SettingsSectionId
764
+ active: SettingsSectionId
753
765
  title: string
754
766
  caption?: string
755
767
  live?: boolean
@@ -802,7 +814,7 @@ function LockWall() {
802
814
  type OverviewTone = 'ok' | 'warn' | 'off' | 'unknown'
803
815
 
804
816
  interface OverviewRow {
805
- id: SectionId
817
+ id: SettingsSectionId
806
818
  icon: LucideIcon
807
819
  label: string
808
820
  tone: OverviewTone
@@ -817,7 +829,7 @@ interface OverviewRow {
817
829
  // probe, so we don't want it firing when Settings opens on another section);
818
830
  // cluster and Prometheus status are shared app-wide caches, so they're read
819
831
  // unconditionally.
820
- function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s: SectionId) => void }) {
832
+ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s: SettingsSectionId) => void }) {
821
833
  const { data: cluster } = useClusterInfo()
822
834
  const { data: prom } = usePrometheusStatus()
823
835
  const { data: argo } = useArgoStatus(active)
@@ -13,10 +13,11 @@ import {
13
13
  type ScrubberRange,
14
14
  type TimelineLiveState,
15
15
  } from '@skyhook-io/k8s-ui'
16
- import type {
17
- TimelineSource,
18
- TimelineOverviewBucket,
19
- TimelineOverviewResult,
16
+ import {
17
+ RETAINED_CLOCK_SKEW_SLACK_MS,
18
+ type TimelineSource,
19
+ type TimelineOverviewBucket,
20
+ type TimelineOverviewResult,
20
21
  } from '../../api/timelineSource'
21
22
  import { getApiBase } from '../../api/config'
22
23
 
@@ -25,8 +26,11 @@ const DAY_MS = 24 * HOUR_MS
25
26
  const EMPTY_BUCKETS: TimelineOverviewBucket[] = []
26
27
  const MAX_STRIP_BARS = 512
27
28
 
28
- // Per-request guard on the retained events endpoint: never brush wider than 7d.
29
- const MAX_SELECTION_MS = 7 * DAY_MS
29
+ // Absolute per-request ceiling on the retained events endpoint, matching the
30
+ // hub's own timelineEventsMaxRange. The effective cap is the smaller of this
31
+ // and the embedder's declared retention depth (maxRangeDays), so a host that
32
+ // advertises 30d of retention can load all 30d in one view.
33
+ const MAX_SELECTION_MS = 31 * DAY_MS
30
34
 
31
35
  // Group the server's hour buckets into fixed display buckets aligned to the
32
36
  // display size, summing counts. The host owns this so the pure scrubber only
@@ -83,8 +87,8 @@ export function buildPresets(maxRangeDays: number): ScrubberPreset[] {
83
87
  { label: '24h', ms: DAY_MS },
84
88
  { label: '7d', ms: 7 * DAY_MS },
85
89
  ]
86
- // 30d is a domain-context preset it clamps to the 7d per-request cap, but
87
- // signals the deeper retained window is available.
90
+ // 30d loads the full retained window in one request (bounded by the hub's
91
+ // per-request cap); shown only when the retention depth reaches it.
88
92
  if (maxRangeDays >= 30) presets.push({ label: '30d', ms: 30 * DAY_MS })
89
93
  return presets
90
94
  }
@@ -176,17 +180,21 @@ export function RetainedTimelineScrubber({ source, selection, onSelectionChange,
176
180
  const availableFromMs = overview.data?.availableFromMs
177
181
 
178
182
  const domain = useMemo<ScrubberRange>(() => {
179
- // Clamp the domain floor to the queryable window. availableFromMs can point
183
+ // Clamp the domain floor to the LOADED window. availableFromMs can point
180
184
  // at ancient synthesized-historical event times (resource creation dates on
181
- // long-lived clusters), which would stretch the strip over years of
182
- // unreachable nothing the UI can never brush past maxRangeDays anyway.
183
- const floor = now - maxRangeDays * DAY_MS
185
+ // long-lived clusters), and a host may declare maxRangeDays deeper than the
186
+ // ring the client actually loads (MAX_SELECTION_MS) either would stretch
187
+ // the strip over regions that render empty despite overview density. The
188
+ // ring's window slides forward by the clock-skew slack, so the floor does
189
+ // too — without it the oldest slack-width sliver is brushable but never
190
+ // loadable.
191
+ const floor = now - Math.min(maxRangeDays * DAY_MS, MAX_SELECTION_MS) + RETAINED_CLOCK_SKEW_SLACK_MS
184
192
  const fromMs = availableFromMs != null ? Math.max(availableFromMs, floor) : floor
185
193
  return { fromMs: Math.min(fromMs, now - HOUR_MS), toMs: now }
186
194
  }, [availableFromMs, now, maxRangeDays])
187
195
 
188
196
  const domainWidth = domain.toMs - domain.fromMs
189
- const maxSelectionMs = Math.min(MAX_SELECTION_MS, domainWidth)
197
+ const maxSelectionMs = Math.min(maxRangeDays * DAY_MS, MAX_SELECTION_MS, domainWidth)
190
198
 
191
199
  // The histogram spans the QUERY RANGE (selection) directly —
192
200
  // no ×8 framing, no minimap. The query is the view, so a narrow window is never