feat: Authentik login + switchable GPU prod target
Add OIDC auth for Command Center and runtime GPU endpoint selection pointed at atc-gpu-prod (10.0.10.106), matching what is currently deployed.
This commit is contained in:
Generated
+2868
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { useAuth } from './hooks/useAuth'
|
||||
import { LoginView } from './components/features/LoginView'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { useClock } from './hooks/useClock'
|
||||
import { useCommandCenter } from './hooks/useCommandCenter'
|
||||
@@ -27,6 +29,7 @@ import { resolveInfraNode } from './lib/infraCatalog'
|
||||
import { cn } from './lib/utils'
|
||||
|
||||
export default function App() {
|
||||
const auth = useAuth()
|
||||
const clock = useClock()
|
||||
const cc = useCommandCenter()
|
||||
const [gpuChatActive, setGpuChatActive] = useState(false)
|
||||
@@ -53,6 +56,19 @@ export default function App() {
|
||||
return 'Lab'
|
||||
})()
|
||||
|
||||
if (auth.status === 'loading') {
|
||||
return (
|
||||
<div className="flex h-full min-h-screen items-center justify-center bg-surface text-sm text-foreground-muted">
|
||||
Checking session…
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (auth.status === 'anon') {
|
||||
return <LoginView />
|
||||
}
|
||||
|
||||
const userLabel = auth.user?.name || auth.user?.preferred_username || auth.user?.email || 'Signed in'
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden bg-surface">
|
||||
<TopBar
|
||||
@@ -62,6 +78,8 @@ export default function App() {
|
||||
agents={cc.agents}
|
||||
approvals={cc.approvals}
|
||||
onApprovalsClick={openApprovals}
|
||||
userLabel={userLabel}
|
||||
onLogout={() => { window.location.href = '/auth/logout' }}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Activity, ChevronDown, ChevronUp, Cpu, ExternalLink, Zap } from 'lucide-react'
|
||||
import { fetchGpu } from '../../lib/api'
|
||||
import type { GpuDevice, GpuStatus } from '../../types'
|
||||
import { Activity, ChevronDown, ChevronUp, Cpu, ExternalLink, Settings2, Zap } from 'lucide-react'
|
||||
import { fetchGpu, fetchGpuConfig, resetGpuConfig, saveGpuConfig, testGpuConfig } from '../../lib/api'
|
||||
import type { GpuConfigPayload, GpuConfigTestResult, GpuDevice, GpuStatus } from '../../types'
|
||||
import type { GpuLiveMetrics } from '../../hooks/useLiveMetrics'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
@@ -53,6 +53,13 @@ export function GpuMatrixPanel({ gpu, live, boost = false, onSelectGpu }: Props)
|
||||
const [localGpu, setLocalGpu] = useState<GpuStatus | null>(gpu)
|
||||
const [lastPoll, setLastPoll] = useState<Date | null>(null)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [gpuConfig, setGpuConfig] = useState<GpuConfigPayload | null>(null)
|
||||
const [selectedPreset, setSelectedPreset] = useState('gpu-prod')
|
||||
const [customHost, setCustomHost] = useState('')
|
||||
const [configBusy, setConfigBusy] = useState(false)
|
||||
const [testResult, setTestResult] = useState<GpuConfigTestResult | null>(null)
|
||||
const [configMsg, setConfigMsg] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalGpu(gpu)
|
||||
@@ -62,6 +69,19 @@ export function GpuMatrixPanel({ gpu, live, boost = false, onSelectGpu }: Props)
|
||||
if (boost) setExpanded(true)
|
||||
}, [boost])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSettings) return
|
||||
fetchGpuConfig().then((cfg) => {
|
||||
if (!cfg) return
|
||||
setGpuConfig(cfg)
|
||||
const active = cfg.active
|
||||
setSelectedPreset(active.preset_id === 'env' ? 'gpu-prod' : active.preset_id)
|
||||
if (active.preset_id === 'custom' || active.source === 'override') {
|
||||
setCustomHost(active.host)
|
||||
}
|
||||
})
|
||||
}, [showSettings])
|
||||
|
||||
useEffect(() => {
|
||||
const poll = async () => {
|
||||
const g = await fetchGpu()
|
||||
@@ -76,6 +96,49 @@ export function GpuMatrixPanel({ gpu, live, boost = false, onSelectGpu }: Props)
|
||||
return () => clearInterval(iv)
|
||||
}, [boost])
|
||||
|
||||
const handleTestTarget = async () => {
|
||||
setConfigBusy(true)
|
||||
setTestResult(null)
|
||||
setConfigMsg(null)
|
||||
const body =
|
||||
selectedPreset === 'custom'
|
||||
? { preset_id: 'custom', host: customHost.trim() }
|
||||
: { preset_id: selectedPreset }
|
||||
const result = await testGpuConfig(body)
|
||||
setTestResult(result)
|
||||
setConfigBusy(false)
|
||||
}
|
||||
|
||||
const handleSaveTarget = async () => {
|
||||
setConfigBusy(true)
|
||||
setConfigMsg(null)
|
||||
const body =
|
||||
selectedPreset === 'custom'
|
||||
? { preset_id: 'custom', host: customHost.trim() }
|
||||
: { preset_id: selectedPreset }
|
||||
const res = await saveGpuConfig(body)
|
||||
setConfigBusy(false)
|
||||
if (res?.active) {
|
||||
setConfigMsg(`Saved → ${res.active.label}`)
|
||||
const g = await fetchGpu()
|
||||
if (g) setLocalGpu(g)
|
||||
} else {
|
||||
setConfigMsg(res?.detail || 'Save failed')
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetTarget = async () => {
|
||||
setConfigBusy(true)
|
||||
await resetGpuConfig()
|
||||
setSelectedPreset('gpu-prod')
|
||||
setCustomHost('')
|
||||
setTestResult(null)
|
||||
setConfigMsg('Reset to environment default')
|
||||
const g = await fetchGpu()
|
||||
if (g) setLocalGpu(g)
|
||||
setConfigBusy(false)
|
||||
}
|
||||
|
||||
const g = localGpu
|
||||
const devices = g?.gpus || []
|
||||
const inferenceOn = g?.ok && g.inference_active
|
||||
@@ -102,7 +165,31 @@ export function GpuMatrixPanel({ gpu, live, boost = false, onSelectGpu }: Props)
|
||||
<h2 className="flex items-center gap-1.5 text-[9px] font-semibold uppercase tracking-widest text-foreground-faint">
|
||||
<Cpu className="h-3 w-3" /> GPU Matrix
|
||||
</h2>
|
||||
<p className="mt-1 text-[9px] text-foreground-faint">GPU Lab offline</p>
|
||||
<p className="mt-1 text-[9px] text-foreground-faint">
|
||||
GPU Lab offline{g?.host ? ` · ${g.ip || g.host}` : ''}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSettings((v) => !v)}
|
||||
className="mt-1 flex items-center gap-1 text-[8px] text-docker hover:underline"
|
||||
>
|
||||
<Settings2 className="h-2.5 w-2.5" /> GPU target
|
||||
</button>
|
||||
{showSettings && gpuConfig && (
|
||||
<GpuTargetSettings
|
||||
gpuConfig={gpuConfig}
|
||||
selectedPreset={selectedPreset}
|
||||
customHost={customHost}
|
||||
configBusy={configBusy}
|
||||
testResult={testResult}
|
||||
configMsg={configMsg}
|
||||
onPresetChange={setSelectedPreset}
|
||||
onCustomHostChange={setCustomHost}
|
||||
onTest={handleTestTarget}
|
||||
onSave={handleSaveTarget}
|
||||
onReset={handleResetTarget}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -130,6 +217,17 @@ export function GpuMatrixPanel({ gpu, live, boost = false, onSelectGpu }: Props)
|
||||
)}
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSettings((v) => !v)}
|
||||
className={cn(
|
||||
'rounded p-1 hover:bg-surface-overlay',
|
||||
showSettings ? 'text-docker' : 'text-foreground-muted hover:text-foreground',
|
||||
)}
|
||||
title="GPU target settings"
|
||||
>
|
||||
<Settings2 className="h-3 w-3" />
|
||||
</button>
|
||||
{g.ui_url && (
|
||||
<a
|
||||
href={g.ui_url}
|
||||
@@ -207,11 +305,121 @@ export function GpuMatrixPanel({ gpu, live, boost = false, onSelectGpu }: Props)
|
||||
</div>
|
||||
|
||||
<p className="font-mono text-[7px] text-foreground-faint">
|
||||
{g.gpu_count ?? devices.length}× V100 · {g.host} · poll {boost ? '1s' : '3s'}
|
||||
{g.gpu_count ?? devices.length}× V100 · {g.ip || g.host}
|
||||
{g.config_label && g.config_source === 'override' ? ` · ${g.config_label}` : ''}
|
||||
{' · poll '}{boost ? '1s' : '3s'}
|
||||
{lastPoll && ` · ${lastPoll.toLocaleTimeString()}`}
|
||||
</p>
|
||||
|
||||
{showSettings && gpuConfig && (
|
||||
<GpuTargetSettings
|
||||
gpuConfig={gpuConfig}
|
||||
selectedPreset={selectedPreset}
|
||||
customHost={customHost}
|
||||
configBusy={configBusy}
|
||||
testResult={testResult}
|
||||
configMsg={configMsg}
|
||||
onPresetChange={setSelectedPreset}
|
||||
onCustomHostChange={setCustomHost}
|
||||
onTest={handleTestTarget}
|
||||
onSave={handleSaveTarget}
|
||||
onReset={handleResetTarget}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
type GpuTargetSettingsProps = {
|
||||
gpuConfig: GpuConfigPayload
|
||||
selectedPreset: string
|
||||
customHost: string
|
||||
configBusy: boolean
|
||||
testResult: GpuConfigTestResult | null
|
||||
configMsg: string | null
|
||||
onPresetChange: (id: string) => void
|
||||
onCustomHostChange: (host: string) => void
|
||||
onTest: () => void
|
||||
onSave: () => void
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
function GpuTargetSettings({
|
||||
gpuConfig,
|
||||
selectedPreset,
|
||||
customHost,
|
||||
configBusy,
|
||||
testResult,
|
||||
configMsg,
|
||||
onPresetChange,
|
||||
onCustomHostChange,
|
||||
onTest,
|
||||
onSave,
|
||||
onReset,
|
||||
}: GpuTargetSettingsProps) {
|
||||
return (
|
||||
<div className="mt-1.5 rounded border border-border/80 bg-surface-overlay/40 p-2 space-y-1.5">
|
||||
<p className="text-[8px] font-semibold uppercase tracking-wider text-foreground-faint">GPU Target</p>
|
||||
<select
|
||||
value={selectedPreset}
|
||||
onChange={(e) => onPresetChange(e.target.value)}
|
||||
className="w-full rounded border border-border bg-surface px-1.5 py-1 font-mono text-[9px] text-foreground"
|
||||
>
|
||||
{gpuConfig.presets.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.label} ({p.host})
|
||||
</option>
|
||||
))}
|
||||
<option value="custom">Custom IP…</option>
|
||||
</select>
|
||||
{selectedPreset === 'custom' && (
|
||||
<input
|
||||
type="text"
|
||||
value={customHost}
|
||||
onChange={(e) => onCustomHostChange(e.target.value)}
|
||||
placeholder="10.0.x.x"
|
||||
className="w-full rounded border border-border bg-surface px-1.5 py-1 font-mono text-[9px] text-foreground"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<button
|
||||
type="button"
|
||||
disabled={configBusy}
|
||||
onClick={onTest}
|
||||
className="rounded border border-border px-2 py-0.5 font-mono text-[8px] text-foreground-muted hover:bg-surface-overlay disabled:opacity-50"
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={configBusy}
|
||||
onClick={onSave}
|
||||
className="rounded border border-docker/40 bg-docker/10 px-2 py-0.5 font-mono text-[8px] text-docker hover:bg-docker/20 disabled:opacity-50"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={configBusy}
|
||||
onClick={onReset}
|
||||
className="rounded border border-border px-2 py-0.5 font-mono text-[8px] text-foreground-faint hover:bg-surface-overlay disabled:opacity-50"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
{testResult && (
|
||||
<p className={cn('font-mono text-[8px]', testResult.ok ? 'text-success' : 'text-warning')}>
|
||||
{testResult.ok
|
||||
? `OK · ${testResult.gpu_count} GPU(s) · ${testResult.active_model || 'no model'}`
|
||||
: `Failed · ${testResult.errors.join('; ') || 'unreachable'}`}
|
||||
</p>
|
||||
)}
|
||||
{configMsg && <p className="font-mono text-[8px] text-foreground-muted">{configMsg}</p>}
|
||||
<p className="font-mono text-[7px] text-foreground-faint">
|
||||
Active: {gpuConfig.active.label} ({gpuConfig.active.host})
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Box, LogIn } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export function LoginView() {
|
||||
const error = useMemo(() => {
|
||||
try {
|
||||
const p = new URLSearchParams(window.location.search)
|
||||
const err = p.get('error')
|
||||
if (!err) return null
|
||||
if (err === 'login_failed') return 'Sign-in failed — try again.'
|
||||
return err
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-screen flex-col items-center justify-center bg-surface px-4">
|
||||
<div className="w-full max-w-md border border-border bg-surface-raised/90 p-8 shadow-panel backdrop-blur-sm">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-docker to-blue-600 shadow-docker">
|
||||
<Box className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase tracking-wider text-foreground-muted">ATC Lab</p>
|
||||
<h1 className="text-lg font-semibold text-foreground">Data & AI Command Center</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-6 text-sm text-foreground-muted">
|
||||
Sign in with Authentik to open the ops glass. Lab environment — not an official Dell product.
|
||||
</p>
|
||||
<a
|
||||
href="/auth/login"
|
||||
className="inline-flex w-full items-center justify-center gap-2 rounded-md bg-docker px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90"
|
||||
>
|
||||
<LogIn className="h-4 w-4" />
|
||||
Continue with Authentik
|
||||
</a>
|
||||
{error ? (
|
||||
<p className="mt-4 text-sm text-warning" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Activity, Bot, Box, Clock, ShieldAlert } from 'lucide-react'
|
||||
import { Activity, Bot, Box, Clock, LogOut, ShieldAlert } from 'lucide-react'
|
||||
import type { Agent, Approval, StatusData, WorkloadData } from '../../types'
|
||||
import { Badge } from '../ui/Badge'
|
||||
import { ThemeToggle } from './ThemeToggle'
|
||||
@@ -11,9 +11,11 @@ type Props = {
|
||||
agents: Agent[]
|
||||
approvals: Approval[]
|
||||
onApprovalsClick: () => void
|
||||
userLabel?: string
|
||||
onLogout?: () => void
|
||||
}
|
||||
|
||||
export function TopBar({ clock, workload, agents, approvals, onApprovalsClick }: Props) {
|
||||
export function TopBar({ clock, workload, agents, approvals, onApprovalsClick, userLabel, onLogout }: Props) {
|
||||
const pipelineOk = workload?.totals?.pipeline_active ?? false
|
||||
const running = workload?.totals?.apps_running ?? 0
|
||||
const total = workload?.totals?.apps_total ?? 0
|
||||
@@ -53,6 +55,22 @@ export function TopBar({ clock, workload, agents, approvals, onApprovalsClick }:
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{userLabel ? (
|
||||
<span className="hidden max-w-[10rem] truncate text-[11px] text-foreground-muted sm:inline" title={userLabel}>
|
||||
{userLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{onLogout ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLogout}
|
||||
className="inline-flex items-center gap-1 rounded border border-border px-1.5 py-0.5 text-[10px] text-foreground-muted hover:bg-surface hover:text-foreground"
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut className="h-3 w-3" />
|
||||
Logout
|
||||
</button>
|
||||
) : null}
|
||||
<ThemeToggle />
|
||||
<div className="flex items-center gap-1.5 font-mono text-[10px] text-foreground-muted">
|
||||
<Clock className="h-3 w-3" />
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export type AuthUser = {
|
||||
user?: string
|
||||
email?: string
|
||||
name?: string
|
||||
preferred_username?: string
|
||||
auth_enabled?: boolean
|
||||
}
|
||||
|
||||
type AuthState =
|
||||
| { status: 'loading'; user: null }
|
||||
| { status: 'anon'; user: null }
|
||||
| { status: 'authed'; user: AuthUser }
|
||||
|
||||
export function useAuth(): AuthState & { refresh: () => Promise<void> } {
|
||||
const [state, setState] = useState<AuthState>({ status: 'loading', user: null })
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const r = await fetch('/api/auth/me', { credentials: 'same-origin' })
|
||||
if (r.status === 401) {
|
||||
setState({ status: 'anon', user: null })
|
||||
return
|
||||
}
|
||||
if (!r.ok) {
|
||||
setState({ status: 'anon', user: null })
|
||||
return
|
||||
}
|
||||
const me = (await r.json()) as AuthUser
|
||||
setState({ status: 'authed', user: me })
|
||||
} catch {
|
||||
setState({ status: 'anon', user: null })
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
void refresh()
|
||||
}, [refresh])
|
||||
|
||||
return { ...state, refresh }
|
||||
}
|
||||
+58
-8
@@ -6,6 +6,8 @@ import type {
|
||||
CdcStats,
|
||||
DataflowGraph,
|
||||
FeedEntry,
|
||||
GpuConfigPayload,
|
||||
GpuConfigTestResult,
|
||||
GpuStatus,
|
||||
Movement,
|
||||
PiiDataset,
|
||||
@@ -21,7 +23,7 @@ async function fetchJson<T>(url: string, timeoutMs = 10000): Promise<T | null> {
|
||||
const ctrl = new AbortController()
|
||||
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
||||
try {
|
||||
const r = await fetch(url, { signal: ctrl.signal })
|
||||
const r = await fetch(url, { signal: ctrl.signal, credentials: 'same-origin' })
|
||||
if (!r.ok) return null
|
||||
return (await r.json()) as T
|
||||
} catch {
|
||||
@@ -62,6 +64,54 @@ export async function fetchGpu(): Promise<GpuStatus | null> {
|
||||
return fetchJson<GpuStatus>('/api/gpu', 8000)
|
||||
}
|
||||
|
||||
export async function fetchGpuConfig(): Promise<GpuConfigPayload | null> {
|
||||
return fetchJson<GpuConfigPayload>('/api/gpu/config', 8000)
|
||||
}
|
||||
|
||||
export async function saveGpuConfig(body: {
|
||||
preset_id?: string
|
||||
host?: string
|
||||
gpu_ui_port?: number
|
||||
llm_port?: number
|
||||
}) {
|
||||
const r = await fetch('/api/gpu/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
return r.json()
|
||||
}
|
||||
|
||||
export async function testGpuConfig(body: {
|
||||
preset_id?: string
|
||||
host?: string
|
||||
gpu_ui_port?: number
|
||||
llm_port?: number
|
||||
}): Promise<GpuConfigTestResult | null> {
|
||||
const ctrl = new AbortController()
|
||||
const timer = setTimeout(() => ctrl.abort(), 12000)
|
||||
try {
|
||||
const r = await fetch('/api/gpu/config/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: ctrl.signal,
|
||||
})
|
||||
if (!r.ok) return null
|
||||
return (await r.json()) as GpuConfigTestResult
|
||||
} catch {
|
||||
return null
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetGpuConfig() {
|
||||
const r = await fetch('/api/gpu/config', { method: 'DELETE' , credentials: 'same-origin' })
|
||||
return r.json()
|
||||
}
|
||||
|
||||
|
||||
export async function fetchTerminals(): Promise<Record<string, TerminalLine[]>> {
|
||||
const j = await fetchJson<{ terminals?: Record<string, TerminalLine[]> }>('/api/terminals', 8000)
|
||||
return j?.terminals || {}
|
||||
@@ -77,7 +127,7 @@ export async function fetchNodeDetail(nodeId: string) {
|
||||
}
|
||||
|
||||
export function probeNode(nodeId: string) {
|
||||
return fetch(`/api/nodes/${nodeId}/probe`, { method: 'POST' })
|
||||
return fetch(`/api/nodes/${nodeId}/probe`, { method: 'POST' , credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
export function askNode(nodeId: string, message: string) {
|
||||
@@ -207,19 +257,19 @@ export function triggerStreamingJob(jobId: string, conf?: Record<string, unknown
|
||||
}
|
||||
|
||||
export function restartKafkaConnector(name: string) {
|
||||
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/restart`, { method: 'POST' })
|
||||
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/restart`, { method: 'POST' , credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
export function pauseKafkaConnector(name: string) {
|
||||
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/pause`, { method: 'POST' })
|
||||
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/pause`, { method: 'POST' , credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
export function resumeKafkaConnector(name: string) {
|
||||
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/resume`, { method: 'POST' })
|
||||
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/resume`, { method: 'POST' , credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
export function triggerStreamingPipeline(pipelineId: string) {
|
||||
return fetch(`/api/pipeline/streaming/pipeline/${encodeURIComponent(pipelineId)}`, { method: 'POST' })
|
||||
return fetch(`/api/pipeline/streaming/pipeline/${encodeURIComponent(pipelineId)}`, { method: 'POST' , credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
export function exportHdfsToKafka(body?: { path?: string; topic?: string; limit?: number }) {
|
||||
@@ -231,7 +281,7 @@ export function exportHdfsToKafka(body?: { path?: string; topic?: string; limit?
|
||||
}
|
||||
|
||||
export function setStreamingFlow(action: 'pause' | 'resume' | 'stop') {
|
||||
return fetch(`/api/pipeline/streaming/flow/${action}`, { method: 'POST' })
|
||||
return fetch(`/api/pipeline/streaming/flow/${action}`, { method: 'POST' , credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
// ── Spark Workbench ──────────────────────────────────────────────
|
||||
@@ -271,7 +321,7 @@ export async function fetchSparkRun(runId: string) {
|
||||
}
|
||||
|
||||
export function cancelSparkRun(runId: string) {
|
||||
return fetch(`/api/spark/run/${runId}/cancel`, { method: 'POST' })
|
||||
return fetch(`/api/spark/run/${runId}/cancel`, { method: 'POST' , credentials: 'same-origin' })
|
||||
}
|
||||
|
||||
export async function fetchSparkLive() {
|
||||
|
||||
@@ -158,17 +158,17 @@ export const INFRA_CATALOG: InfraNode[] = [
|
||||
{
|
||||
id: 'gpu',
|
||||
label: 'GPU Lab',
|
||||
vm: 'atc-gpu-dev',
|
||||
ip: '10.0.20.106',
|
||||
vm: 'atc-gpu-prod',
|
||||
ip: '10.0.10.106',
|
||||
zone: 'gpu',
|
||||
agentId: 'infra-sentinel',
|
||||
icon: Sparkles,
|
||||
accent: '#3fb950',
|
||||
description: 'vLLM inference — Llama 3 70B on 4× V100',
|
||||
ssh: 'ssh root@10.0.20.106',
|
||||
ssh: 'ssh root@10.0.10.106',
|
||||
apps: [
|
||||
{ label: 'GPU Lab UI', url: 'http://10.0.20.106:9000', port: '9000' },
|
||||
{ label: 'vLLM API', url: 'http://10.0.20.106:8001/v1', port: '8001' },
|
||||
{ label: 'GPU Lab UI', url: 'http://10.0.10.106:9000', port: '9000' },
|
||||
{ label: 'vLLM API', url: 'http://10.0.10.106:8001/v1', port: '8001' },
|
||||
],
|
||||
topoIds: ['llm', 'cons-ml'],
|
||||
},
|
||||
|
||||
@@ -217,15 +217,63 @@ export type GpuDevice = {
|
||||
export type GpuStatus = {
|
||||
ok: boolean
|
||||
host: string
|
||||
ip?: string
|
||||
ui_url: string
|
||||
inference_active?: boolean
|
||||
active_model?: string | null
|
||||
vllm_url?: string | null
|
||||
gpu_count?: number
|
||||
gpus?: GpuDevice[]
|
||||
config_source?: string
|
||||
preset_id?: string
|
||||
config_label?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type GpuPreset = {
|
||||
id: string
|
||||
label: string
|
||||
vm: string
|
||||
vmid: number
|
||||
host: string
|
||||
gpu_ui_port: number
|
||||
llm_port: number
|
||||
description: string
|
||||
}
|
||||
|
||||
export type GpuTargetConfig = {
|
||||
source: string
|
||||
preset_id: string
|
||||
label: string
|
||||
host: string
|
||||
gpu_url: string
|
||||
gpu_ui_url: string
|
||||
llm_url: string
|
||||
env_gpu_url?: string
|
||||
env_llm_url?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export type GpuConfigPayload = {
|
||||
active: GpuTargetConfig
|
||||
presets: GpuPreset[]
|
||||
defaults: GpuTargetConfig
|
||||
}
|
||||
|
||||
export type GpuConfigTestResult = {
|
||||
ok: boolean
|
||||
host: string
|
||||
gpu_url: string
|
||||
llm_url: string
|
||||
metrics_ok: boolean
|
||||
llm_ok: boolean
|
||||
gpu_count: number
|
||||
inference_active: boolean
|
||||
active_model: string | null
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
|
||||
export type AgentState = 'idle' | 'walk' | 'fetch' | 'return'
|
||||
|
||||
export type AgentAnim = {
|
||||
|
||||
Reference in New Issue
Block a user