@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.
- package/package.json +7 -7
- package/src/App.tsx +34 -13
- package/src/api/client.ts +21 -6
- package/src/api/config.test.ts +47 -0
- package/src/api/config.ts +15 -0
- package/src/api/diagnose.ts +10 -15
- package/src/api/timelineSource.test.ts +464 -21
- package/src/api/timelineSource.ts +521 -184
- package/src/components/applications/ApplicationsView.tsx +2 -1
- package/src/components/diagnose/AISettings.tsx +21 -7
- package/src/components/diagnose/DiagnoseContext.tsx +82 -53
- package/src/components/diagnose/DiagnoseSurface.tsx +13 -8
- package/src/components/diagnose/parts.test.tsx +125 -0
- package/src/components/diagnose/parts.tsx +166 -75
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/resources/ResourcesView.tsx +9 -8
- package/src/components/settings/SettingsDialog.tsx +31 -19
- package/src/components/timeline/RetainedTimelineScrubber.tsx +21 -13
- package/src/components/timeline/TimelineList.tsx +72 -49
- package/src/components/timeline/TimelineView.tsx +83 -19
- package/src/context/DiagnoseCustomization.tsx +1 -1
- package/src/utils/auditBadges.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"yaml": "^2.9.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@skyhook-io/k8s-ui": ">=1.
|
|
44
|
+
"@skyhook-io/k8s-ui": ">=1.10.0",
|
|
45
45
|
"@tanstack/react-query": ">=5",
|
|
46
46
|
"@xyflow/react": ">=12.0.0",
|
|
47
47
|
"clsx": ">=2",
|
|
@@ -54,15 +54,15 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@eslint/js": "^10.0.1",
|
|
57
|
-
"@playwright/test": "^1.
|
|
57
|
+
"@playwright/test": "^1.62.0",
|
|
58
58
|
"@skyhook-io/k8s-ui": "*",
|
|
59
59
|
"@tailwindcss/typography": "^0.5.20",
|
|
60
|
-
"@tailwindcss/vite": "^4.3.
|
|
61
|
-
"@tanstack/react-query": "^5.101.
|
|
60
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
61
|
+
"@tanstack/react-query": "^5.101.4",
|
|
62
62
|
"@types/node": "^26.1.1",
|
|
63
63
|
"@types/react": "^19.2.17",
|
|
64
64
|
"@types/react-dom": "^19.2.3",
|
|
65
|
-
"@vitejs/plugin-react": "^6.0.
|
|
65
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
66
66
|
"@xyflow/react": "^12.11.2",
|
|
67
67
|
"clsx": "^2.1.1",
|
|
68
68
|
"elkjs": "^0.11.1",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"react-dom": "^19.2.7",
|
|
78
78
|
"react-router-dom": "^7.18.1",
|
|
79
79
|
"tailwind-merge": "^3.6.0",
|
|
80
|
-
"tailwindcss": "^4.3.
|
|
80
|
+
"tailwindcss": "^4.3.3",
|
|
81
81
|
"typescript": "^6.0.2",
|
|
82
82
|
"typescript-eslint": "^8.62.0",
|
|
83
83
|
"vite": "^8.1.5"
|
package/src/App.tsx
CHANGED
|
@@ -50,7 +50,7 @@ import { DiagnosticsOverlay } from './components/ui/DiagnosticsOverlay'
|
|
|
50
50
|
import { useEventSource } from './hooks/useEventSource'
|
|
51
51
|
import { debugNamespaceLog, useNamespaces, useNamespaceScope, useSetActiveNamespace, useSwitchContext, useAuthMe, useAudit } from './api/client'
|
|
52
52
|
import { buildAuditSeverityMap } from './utils/auditBadges'
|
|
53
|
-
import { routePath, apiUrl, getAuthHeaders, getCredentialsMode } from './api/config'
|
|
53
|
+
import { routePath, apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from './api/config'
|
|
54
54
|
import { KeyboardShortcutProvider, useRegisterShortcut, useRegisterShortcuts, useSuppressBaseShortcuts } from './hooks/useKeyboardShortcuts'
|
|
55
55
|
import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
|
|
56
56
|
import { useDocumentTitle } from './hooks/useDocumentTitle'
|
|
@@ -60,7 +60,7 @@ import { Network, List, Clock, Package, Sun, Moon, Activity, Home, Star, Search,
|
|
|
60
60
|
import { useTheme } from './context/ThemeContext'
|
|
61
61
|
import { Tooltip } from './components/ui/Tooltip'
|
|
62
62
|
import { LargeClusterNamespacePicker } from './components/shared/LargeClusterNamespacePicker'
|
|
63
|
-
import { SettingsDialog } from './components/settings/SettingsDialog'
|
|
63
|
+
import { SettingsDialog, type SettingsSectionId } from './components/settings/SettingsDialog'
|
|
64
64
|
import type { APIResource, TopologyNode, GroupingMode, MainView, SelectedResource, SelectedHelmRelease, NodeKind, TopologyMode, Topology, K8sEvent } from './types'
|
|
65
65
|
import { kindToPlural, pluralToKind, openExternal, apiVersionToGroup, relatedResourcePath, searchHitToSelectedResource } from './utils/navigation'
|
|
66
66
|
import { type OmnibarHandle } from './components/ui/Omnibar'
|
|
@@ -343,12 +343,19 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
343
343
|
// Auth check — detect if auth is enabled but user is not authenticated
|
|
344
344
|
const { data: authMe, isPending: authMePending } = useAuthMe()
|
|
345
345
|
|
|
346
|
-
// Restore navigation path after session-expiry re-auth redirect
|
|
346
|
+
// Restore navigation path after session-expiry re-auth redirect.
|
|
347
|
+
// The stored value is basename-relative, but strip defensively anyway:
|
|
348
|
+
// sessionStorage outlives app upgrades, so a value written by an older
|
|
349
|
+
// version may still carry the basename — and navigate() re-applies the
|
|
350
|
+
// basename, which would double it (/c/abc/c/abc/...). Trade-off: if a host
|
|
351
|
+
// mounts the app at a basename that exactly equals an internal route (e.g.
|
|
352
|
+
// /topology), this second strip eats the route and re-auth lands on home —
|
|
353
|
+
// accepted, since a wrong-but-valid landing beats a doubled URL.
|
|
347
354
|
useEffect(() => {
|
|
348
355
|
const returnPath = sessionStorage.getItem('radar_return_path')
|
|
349
356
|
if (returnPath) {
|
|
350
357
|
sessionStorage.removeItem('radar_return_path')
|
|
351
|
-
navigate(returnPath, { replace: true })
|
|
358
|
+
navigate(stripBasename(returnPath), { replace: true })
|
|
352
359
|
}
|
|
353
360
|
}, [navigate])
|
|
354
361
|
|
|
@@ -507,13 +514,23 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
507
514
|
|
|
508
515
|
// Settings dialog state
|
|
509
516
|
const [showSettings, setShowSettings] = useState(false)
|
|
517
|
+
const [settingsSection, setSettingsSection] = useState<SettingsSectionId>('overview')
|
|
518
|
+
const openSettings = useCallback((section: SettingsSectionId = 'overview') => {
|
|
519
|
+
setSettingsSection(section)
|
|
520
|
+
setShowSettings(true)
|
|
521
|
+
}, [])
|
|
510
522
|
|
|
511
523
|
// Listen for "open-settings" DOM event (used by MCPSetupDialog etc.)
|
|
512
524
|
useEffect(() => {
|
|
513
|
-
const handler = () =>
|
|
525
|
+
const handler = (event: Event) => {
|
|
526
|
+
const section =
|
|
527
|
+
(event as CustomEvent<{ section?: SettingsSectionId }>).detail?.section ??
|
|
528
|
+
'overview'
|
|
529
|
+
openSettings(section)
|
|
530
|
+
}
|
|
514
531
|
window.addEventListener('radar:open-settings', handler)
|
|
515
532
|
return () => window.removeEventListener('radar:open-settings', handler)
|
|
516
|
-
}, [])
|
|
533
|
+
}, [openSettings])
|
|
517
534
|
|
|
518
535
|
// Listen for "open-local-terminal" DOM event — the AI surface is portaled above
|
|
519
536
|
// the DockProvider, so it can't call useOpenLocalTerminal directly; it dispatches
|
|
@@ -853,7 +870,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
853
870
|
description: 'Open settings',
|
|
854
871
|
category: 'General' as const,
|
|
855
872
|
scope: 'global' as const,
|
|
856
|
-
handler: () =>
|
|
873
|
+
handler: () => openSettings(),
|
|
857
874
|
}]
|
|
858
875
|
: []),
|
|
859
876
|
])
|
|
@@ -967,6 +984,10 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
967
984
|
if (tl.timer === null) {
|
|
968
985
|
tl.timer = window.setTimeout(() => {
|
|
969
986
|
queryClient.invalidateQueries({ queryKey: ['changes'] })
|
|
987
|
+
// The ring-and-delta timeline path: an invalidation costs one ~KB
|
|
988
|
+
// cursor delta, so SSE keeps the timeline fresh within seconds and
|
|
989
|
+
// the hook's 10s poll remains the no-SSE fallback.
|
|
990
|
+
queryClient.invalidateQueries({ queryKey: ['timeline-ring'] })
|
|
970
991
|
timelineInvalidationRef.current = { timer: null }
|
|
971
992
|
}, 5000)
|
|
972
993
|
}
|
|
@@ -1512,10 +1533,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1512
1533
|
}, [displayedTopology, visibleKinds, namespaces, topologyMode])
|
|
1513
1534
|
|
|
1514
1535
|
// Cluster Audit findings, joined onto topology nodes by the audit key the
|
|
1515
|
-
// backend stamps on each node (data.auditKey).
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1518
|
-
// when there are findings to attach — no overhead on clusters with none.
|
|
1536
|
+
// backend stamps on each node (data.auditKey). Only badge-worthy findings
|
|
1537
|
+
// reach the graph; the raw auditDanger/auditWarning property names remain at
|
|
1538
|
+
// this compatibility boundary while the node presents them as High/Medium.
|
|
1519
1539
|
const audit = useAudit(namespaces)
|
|
1520
1540
|
const auditSeverityMap = useMemo(
|
|
1521
1541
|
() => buildAuditSeverityMap(audit.data?.findings, audit.data?.checks),
|
|
@@ -1596,7 +1616,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1596
1616
|
pinned={navRailEffectivePinned}
|
|
1597
1617
|
onTogglePinned={toggleNavRailPinned}
|
|
1598
1618
|
showPinToggle={!railForcedSlim}
|
|
1599
|
-
onOpenSettings={() =>
|
|
1619
|
+
onOpenSettings={() => openSettings()}
|
|
1600
1620
|
accountSlot={<UserMenu variant="rail" pinned={navRailEffectivePinned} />}
|
|
1601
1621
|
/>
|
|
1602
1622
|
)}
|
|
@@ -2183,7 +2203,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2183
2203
|
navigateToResource(resource)
|
|
2184
2204
|
}}
|
|
2185
2205
|
onClearNamespaces={clearAllNamespaces}
|
|
2186
|
-
onOpenSettings={() =>
|
|
2206
|
+
onOpenSettings={() => openSettings()}
|
|
2187
2207
|
/>
|
|
2188
2208
|
)}
|
|
2189
2209
|
|
|
@@ -2414,6 +2434,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2414
2434
|
{/* Settings dialog — My permissions is rendered inline in its own section */}
|
|
2415
2435
|
<SettingsDialog
|
|
2416
2436
|
open={showSettings}
|
|
2437
|
+
initialSection={settingsSection}
|
|
2417
2438
|
onClose={() => setShowSettings(false)}
|
|
2418
2439
|
/>
|
|
2419
2440
|
|
package/src/api/client.ts
CHANGED
|
@@ -42,7 +42,7 @@ import type {
|
|
|
42
42
|
ArgoRevisionMetadata,
|
|
43
43
|
} from '../types'
|
|
44
44
|
import type { GitOpsOperationResponse } from '../types/gitops'
|
|
45
|
-
import { getApiBase, getAuthHeaders, getCredentialsMode, getBasename, routePath } from './config'
|
|
45
|
+
import { getApiBase, getAuthHeaders, getCredentialsMode, getBasename, routePath, stripBasename } from './config'
|
|
46
46
|
import { pluralToKind } from '../utils/navigation'
|
|
47
47
|
|
|
48
48
|
// Auto-refresh cadences (ms) — named constants for each polled hook's
|
|
@@ -74,11 +74,13 @@ export function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<
|
|
|
74
74
|
const authPrefix = `${getBasename()}/auth`
|
|
75
75
|
if (response.status === 401 && !window.location.pathname.startsWith(authPrefix)) {
|
|
76
76
|
// Save current location so user returns to where they were after re-auth.
|
|
77
|
+
// Stored basename-relative: the restore path replays it through React
|
|
78
|
+
// Router's navigate(), which re-applies the basename itself.
|
|
77
79
|
// Editor draft is auto-saved by EditableYamlView via sessionStorage.
|
|
78
80
|
try {
|
|
79
81
|
sessionStorage.setItem(
|
|
80
82
|
'radar_return_path',
|
|
81
|
-
window.location.pathname + window.location.search,
|
|
83
|
+
stripBasename(window.location.pathname) + window.location.search,
|
|
82
84
|
)
|
|
83
85
|
} catch {
|
|
84
86
|
/* best-effort */
|
|
@@ -2026,15 +2028,28 @@ export function useNodeMetricsHistory(nodeName: string) {
|
|
|
2026
2028
|
}
|
|
2027
2029
|
|
|
2028
2030
|
// Top metrics types (bulk, for resource table view)
|
|
2031
|
+
export interface ContainerResourceMetrics {
|
|
2032
|
+
name: string
|
|
2033
|
+
cpu: number // nanocores (usage)
|
|
2034
|
+
cpuRequest: number // nanocores
|
|
2035
|
+
cpuLimit: number // nanocores
|
|
2036
|
+
memory: number // bytes (usage)
|
|
2037
|
+
memoryRequest: number // bytes
|
|
2038
|
+
memoryLimit: number // bytes
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2029
2041
|
export interface TopPodMetrics {
|
|
2030
2042
|
namespace: string
|
|
2031
2043
|
name: string
|
|
2032
2044
|
cpu: number // nanocores (usage)
|
|
2033
2045
|
memory: number // bytes (usage)
|
|
2034
|
-
cpuRequest: number // nanocores (sum across containers)
|
|
2035
|
-
cpuLimit: number // nanocores (sum across containers)
|
|
2036
|
-
memoryRequest: number // bytes (sum across containers)
|
|
2037
|
-
memoryLimit: number // bytes (sum across containers)
|
|
2046
|
+
cpuRequest: number // nanocores (sum across running containers)
|
|
2047
|
+
cpuLimit: number // nanocores (sum across running containers)
|
|
2048
|
+
memoryRequest: number // bytes (sum across running containers)
|
|
2049
|
+
memoryLimit: number // bytes (sum across running containers)
|
|
2050
|
+
// Per-container breakdown; present only for pods with more than one running
|
|
2051
|
+
// container (regular + native sidecars). Absent for single-container pods.
|
|
2052
|
+
containers?: ContainerResourceMetrics[]
|
|
2038
2053
|
}
|
|
2039
2054
|
|
|
2040
2055
|
export interface TopNodeMetrics {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { setBasename, stripBasename, routePath } from './config'
|
|
4
|
+
|
|
5
|
+
// setBasename mutates module state — restore standalone default after each test.
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
setBasename('')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
describe('stripBasename', () => {
|
|
11
|
+
it('passes paths through unchanged when no basename is configured', () => {
|
|
12
|
+
expect(stripBasename('/resources/pods')).toBe('/resources/pods')
|
|
13
|
+
expect(stripBasename('/')).toBe('/')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('strips a configured basename prefix', () => {
|
|
17
|
+
setBasename('/c/abc')
|
|
18
|
+
expect(stripBasename('/c/abc/resources/pods')).toBe('/resources/pods')
|
|
19
|
+
expect(stripBasename('/c/abc/timeline?range=1h')).toBe('/timeline?range=1h')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('maps the bare basename to root', () => {
|
|
23
|
+
setBasename('/c/abc')
|
|
24
|
+
expect(stripBasename('/c/abc')).toBe('/')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('strips when the basename is followed directly by a query string', () => {
|
|
28
|
+
setBasename('/c/abc')
|
|
29
|
+
expect(stripBasename('/c/abc?tab=events')).toBe('?tab=events')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('leaves basename-relative paths unchanged (idempotent)', () => {
|
|
33
|
+
setBasename('/c/abc')
|
|
34
|
+
expect(stripBasename('/resources/pods')).toBe('/resources/pods')
|
|
35
|
+
expect(stripBasename(stripBasename('/c/abc/resources/pods'))).toBe('/resources/pods')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('does not strip a path that merely shares a string prefix', () => {
|
|
39
|
+
setBasename('/c/abc')
|
|
40
|
+
expect(stripBasename('/c/abcdef/resources')).toBe('/c/abcdef/resources')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('inverts routePath', () => {
|
|
44
|
+
setBasename('/c/abc')
|
|
45
|
+
expect(stripBasename(routePath('/auth/login'))).toBe('/auth/login')
|
|
46
|
+
})
|
|
47
|
+
})
|
package/src/api/config.ts
CHANGED
|
@@ -64,6 +64,21 @@ export function routePath(path: string): string {
|
|
|
64
64
|
return prefix + path;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Inverse of `routePath`: strips the configured basename from a window-level
|
|
69
|
+
* path so it can be handed to React Router's navigate(), which re-applies the
|
|
70
|
+
* basename. Passing an already-prefixed path to navigate() doubles the prefix
|
|
71
|
+
* (e.g. /c/abc/c/abc/...). Basename-relative paths pass through unchanged.
|
|
72
|
+
*/
|
|
73
|
+
export function stripBasename(path: string): string {
|
|
74
|
+
const prefix = basename;
|
|
75
|
+
if (!prefix) return path;
|
|
76
|
+
if (path === prefix || path.startsWith(prefix + '/') || path.startsWith(prefix + '?')) {
|
|
77
|
+
return path.slice(prefix.length) || '/';
|
|
78
|
+
}
|
|
79
|
+
return path;
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
/**
|
|
68
83
|
* Builds a WebSocket URL for a given API path.
|
|
69
84
|
*
|
package/src/api/diagnose.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface AgentInfo {
|
|
|
10
10
|
version: string;
|
|
11
11
|
present: boolean;
|
|
12
12
|
supported: boolean;
|
|
13
|
+
profiles?: ExecutionProfile[];
|
|
14
|
+
consentSurfaces?: Partial<Record<ExecutionProfile, string>>;
|
|
13
15
|
hosted?: boolean;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -18,9 +20,11 @@ export interface AgentsResponse {
|
|
|
18
20
|
enabled: boolean;
|
|
19
21
|
// Machine-scoped consent per disclosure surface, recorded server-side
|
|
20
22
|
// (~/.radar) — one acknowledgment covers the web panel and the CLI.
|
|
21
|
-
consented?:
|
|
23
|
+
consented?: Record<string, boolean>;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export type ExecutionProfile = "safeguarded" | "full-local";
|
|
27
|
+
|
|
24
28
|
export interface DiagnoseStep {
|
|
25
29
|
id: string;
|
|
26
30
|
tool: string;
|
|
@@ -64,14 +68,7 @@ export interface ResourceHealthSignal {
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
export interface DiagnoseStreamEvent {
|
|
67
|
-
type:
|
|
68
|
-
| "turn"
|
|
69
|
-
| "phase"
|
|
70
|
-
| "step"
|
|
71
|
-
| "thinking"
|
|
72
|
-
| "done"
|
|
73
|
-
| "error"
|
|
74
|
-
| "closed";
|
|
71
|
+
type: "turn" | "phase" | "step" | "thinking" | "done" | "error" | "closed";
|
|
75
72
|
phase?: string;
|
|
76
73
|
step?: DiagnoseStep;
|
|
77
74
|
token?: string;
|
|
@@ -91,7 +88,7 @@ export interface RunSummary {
|
|
|
91
88
|
name: string;
|
|
92
89
|
context: string;
|
|
93
90
|
agent?: string; // backend CLI that drove this run ("claude"/"codex")
|
|
94
|
-
|
|
91
|
+
profile?: ExecutionProfile;
|
|
95
92
|
model?: string;
|
|
96
93
|
effort?: string;
|
|
97
94
|
managedBy?: string; // GitOps/Helm owner of the target ("Argo CD"/"Flux"/"Helm"), for the Apply warning
|
|
@@ -145,7 +142,7 @@ export async function createRun(
|
|
|
145
142
|
},
|
|
146
143
|
opts?: {
|
|
147
144
|
agent?: string;
|
|
148
|
-
|
|
145
|
+
profile?: ExecutionProfile;
|
|
149
146
|
model?: string;
|
|
150
147
|
effort?: string;
|
|
151
148
|
},
|
|
@@ -179,10 +176,8 @@ export async function listRuns(signal?: AbortSignal): Promise<RunsResponse> {
|
|
|
179
176
|
return { runs: d.runs ?? [], historyDegraded: !!d.historyDegraded };
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
// recordConsent acknowledges the current disclosure for
|
|
183
|
-
export async function recordConsent(
|
|
184
|
-
surface: "standard" | "cursor",
|
|
185
|
-
): Promise<void> {
|
|
179
|
+
// recordConsent acknowledges the current disclosure for an execution profile, server-side.
|
|
180
|
+
export async function recordConsent(surface: string): Promise<void> {
|
|
186
181
|
const res = await fetch(`${getApiBase()}/diagnose/consent`, {
|
|
187
182
|
method: "POST",
|
|
188
183
|
credentials: getCredentialsMode(),
|