2026-06-25 00:28:23 +00:00
|
|
|
import type {
|
|
|
|
|
PresentationData,
|
|
|
|
|
Agent,
|
|
|
|
|
Approval,
|
2026-06-27 01:44:26 +02:00
|
|
|
CdcChange,
|
|
|
|
|
CdcStats,
|
2026-06-27 02:09:38 +02:00
|
|
|
DataflowGraph,
|
2026-06-25 00:28:23 +00:00
|
|
|
FeedEntry,
|
|
|
|
|
GpuStatus,
|
2026-06-27 02:09:38 +02:00
|
|
|
Movement,
|
|
|
|
|
PiiDataset,
|
2026-06-27 19:37:50 +00:00
|
|
|
SparkRun,
|
|
|
|
|
SparkLive,
|
|
|
|
|
StreamingStatus,
|
2026-06-25 00:28:23 +00:00
|
|
|
StatusData,
|
|
|
|
|
TerminalLine,
|
|
|
|
|
WorkloadData,
|
|
|
|
|
} from '../types'
|
|
|
|
|
|
|
|
|
|
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 })
|
|
|
|
|
if (!r.ok) return null
|
|
|
|
|
return (await r.json()) as T
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(timer)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchAgents() {
|
|
|
|
|
const j = await fetchJson<{ agents?: Agent[] }>('/api/agents', 8000)
|
|
|
|
|
return j?.agents || []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchStatus() {
|
|
|
|
|
return (await fetchJson<StatusData>('/api/status', 8000)) as StatusData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchFeed() {
|
|
|
|
|
const j = await fetchJson<{ entries?: FeedEntry[] }>('/api/feed', 8000)
|
|
|
|
|
return j?.entries || []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchApprovals() {
|
|
|
|
|
const j = await fetchJson<{ approvals?: Approval[] }>('/api/approvals', 8000)
|
|
|
|
|
return j?.approvals || []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchApprovalHistory(status: string = 'pending') {
|
|
|
|
|
const j = await fetchJson<{
|
|
|
|
|
approvals?: Approval[]
|
|
|
|
|
stats?: { pending: number; approved: number; denied: number; total: number }
|
|
|
|
|
}>(`/api/approvals?status=${encodeURIComponent(status)}&limit=200`, 8000)
|
|
|
|
|
return { approvals: j?.approvals || [], stats: j?.stats }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchGpu(): Promise<GpuStatus | null> {
|
|
|
|
|
return fetchJson<GpuStatus>('/api/gpu', 8000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchTerminals(): Promise<Record<string, TerminalLine[]>> {
|
|
|
|
|
const j = await fetchJson<{ terminals?: Record<string, TerminalLine[]> }>('/api/terminals', 8000)
|
|
|
|
|
return j?.terminals || {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchWorkload(): Promise<WorkloadData | null> {
|
|
|
|
|
return fetchJson<WorkloadData>('/api/workload?fast=true', 25000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchNodeDetail(nodeId: string) {
|
|
|
|
|
const j = await fetchJson<Record<string, unknown>>(`/api/nodes/${nodeId}`, 15000)
|
|
|
|
|
return j || { error: 'timeout' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function probeNode(nodeId: string) {
|
|
|
|
|
return fetch(`/api/nodes/${nodeId}/probe`, { method: 'POST' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function askNode(nodeId: string, message: string) {
|
|
|
|
|
return fetch(`/api/nodes/${nodeId}/ask`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ message }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function sendPrompt(message: string, agentId?: string) {
|
|
|
|
|
return fetch('/api/prompt', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ message, agent_id: agentId || undefined }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchPresentation(): Promise<PresentationData | null> {
|
|
|
|
|
return fetchJson<PresentationData>('/api/presentation', 60000)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 01:44:26 +02:00
|
|
|
export async function fetchChanges(opts: { source?: string; op?: string; limit?: number } = {}) {
|
|
|
|
|
const p = new URLSearchParams()
|
|
|
|
|
if (opts.source) p.set('source', opts.source)
|
|
|
|
|
if (opts.op) p.set('op', opts.op)
|
|
|
|
|
p.set('limit', String(opts.limit ?? 150))
|
|
|
|
|
const j = await fetchJson<{ changes?: CdcChange[]; connected?: boolean; consumed?: number }>(
|
|
|
|
|
`/api/changes?${p.toString()}`, 8000,
|
|
|
|
|
)
|
|
|
|
|
return { changes: j?.changes || [], connected: !!j?.connected, consumed: j?.consumed || 0 }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchChangeStats(minutes = 15): Promise<CdcStats | null> {
|
|
|
|
|
return fetchJson<CdcStats>(`/api/changes/stats?minutes=${minutes}`, 8000)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 13:31:11 +00:00
|
|
|
export type ConnectorState = { name: string; state?: string; failed?: number[] }
|
|
|
|
|
export type ResyncResult = { ok: boolean; restarted?: string[]; healthy?: number; total?: number; after?: ConnectorState[] }
|
|
|
|
|
|
|
|
|
|
export async function resyncSources(force = true): Promise<ResyncResult | null> {
|
|
|
|
|
const r = await fetch('/api/pipeline/streaming/resync', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ force }),
|
|
|
|
|
})
|
|
|
|
|
return r.ok ? ((await r.json()) as ResyncResult) : null
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 01:44:26 +02:00
|
|
|
export async function fetchAgentOpsStatus() {
|
|
|
|
|
return fetchJson<Record<string, unknown>>('/api/agent-ops/status', 8000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function toggleAgentOps(enabled?: boolean) {
|
|
|
|
|
return fetch('/api/agent-ops/toggle', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(enabled === undefined ? {} : { enabled }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function runAgentOpOnce(source?: string, op?: string) {
|
|
|
|
|
return fetch('/api/agent-ops/run-once', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ source, op }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 02:09:38 +02:00
|
|
|
export async function fetchDataflow(refresh = false): Promise<DataflowGraph | null> {
|
|
|
|
|
return fetchJson<DataflowGraph>(`/api/dataflow${refresh ? '?refresh=true' : ''}`, 25000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function runDataflowMovement(movementId: string, conf?: Record<string, unknown>) {
|
|
|
|
|
return fetch(`/api/dataflow/${movementId}/run`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(conf ? { conf } : {}),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchMovements(): Promise<Movement[]> {
|
|
|
|
|
const j = await fetchJson<{ movements?: Movement[] }>('/api/movements', 8000)
|
|
|
|
|
return j?.movements || []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchPii(refresh = false): Promise<{ datasets: PiiDataset[]; summary: Record<string, number> } | null> {
|
|
|
|
|
return fetchJson<{ datasets: PiiDataset[]; summary: Record<string, number> }>(
|
|
|
|
|
`/api/pii${refresh ? '?refresh=true' : ''}`, 15000,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 11:36:36 +02:00
|
|
|
export function setPiiMask(key: string, column: string, masked: boolean) {
|
|
|
|
|
return fetch('/api/pii/policy', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ key, column, masked }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 02:09:38 +02:00
|
|
|
export function toggleEtlAgent(enabled?: boolean) {
|
|
|
|
|
return fetch('/api/agent-ops/etl/toggle', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(enabled === undefined ? {} : { enabled }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 00:28:23 +00:00
|
|
|
export async function decideApproval(id: string, approved: boolean, decidedBy: string, note: string) {
|
|
|
|
|
return fetch(`/api/approvals/${id}/decide`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ approved, decided_by: decidedBy, note }),
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-06-27 19:37:50 +00:00
|
|
|
|
|
|
|
|
export async function fetchStreamingStatus(refresh = false): Promise<StreamingStatus | null> {
|
|
|
|
|
return fetchJson<StreamingStatus>(`/api/pipeline/streaming/status${refresh ? '?refresh=true' : ''}`, 20000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function triggerStreamingJob(jobId: string, conf?: Record<string, unknown>) {
|
|
|
|
|
return fetch(`/api/pipeline/streaming/jobs/${jobId}/trigger`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ conf: conf ?? {} }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function restartKafkaConnector(name: string) {
|
|
|
|
|
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/restart`, { method: 'POST' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function pauseKafkaConnector(name: string) {
|
|
|
|
|
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/pause`, { method: 'POST' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resumeKafkaConnector(name: string) {
|
|
|
|
|
return fetch(`/api/pipeline/streaming/kafka/connectors/${encodeURIComponent(name)}/resume`, { method: 'POST' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function triggerStreamingPipeline(pipelineId: string) {
|
|
|
|
|
return fetch(`/api/pipeline/streaming/pipeline/${encodeURIComponent(pipelineId)}`, { method: 'POST' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function exportHdfsToKafka(body?: { path?: string; topic?: string; limit?: number }) {
|
|
|
|
|
return fetch('/api/pipeline/streaming/hdfs/to-kafka', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body ?? {}),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function setStreamingFlow(action: 'pause' | 'resume' | 'stop') {
|
|
|
|
|
return fetch(`/api/pipeline/streaming/flow/${action}`, { method: 'POST' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Spark Workbench ──────────────────────────────────────────────
|
|
|
|
|
export async function fetchSparkCatalogs(): Promise<string[]> {
|
|
|
|
|
const j = await fetchJson<{ catalogs?: string[] }>('/api/spark/catalogs', 15000)
|
|
|
|
|
return j?.catalogs ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSparkSchemas(catalog: string): Promise<string[]> {
|
|
|
|
|
const j = await fetchJson<{ schemas?: string[] }>(`/api/spark/schemas?catalog=${encodeURIComponent(catalog)}`, 15000)
|
|
|
|
|
return j?.schemas ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSparkTables(catalog: string, schema: string): Promise<{ name: string; fqn: string }[]> {
|
|
|
|
|
const j = await fetchJson<{ tables?: { name: string; fqn: string }[] }>(
|
|
|
|
|
`/api/spark/tables?catalog=${encodeURIComponent(catalog)}&schema=${encodeURIComponent(schema)}`, 15000)
|
|
|
|
|
return j?.tables ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSparkColumns(table: string): Promise<{ name: string; type: string }[]> {
|
|
|
|
|
const j = await fetchJson<{ columns?: { name: string; type: string }[] }>(
|
|
|
|
|
`/api/spark/columns?table=${encodeURIComponent(table)}`, 15000)
|
|
|
|
|
return j?.columns ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createSparkRun(body: Record<string, unknown>): Promise<{ ok: boolean; run_id?: string; sql?: string; error?: string; target?: string | null }> {
|
|
|
|
|
const r = await fetch('/api/spark/run', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
})
|
|
|
|
|
return r.json()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSparkRun(runId: string) {
|
|
|
|
|
return fetchJson<{ ok: boolean; run?: SparkRun }>(`/api/spark/run/${runId}`, 15000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function cancelSparkRun(runId: string) {
|
|
|
|
|
return fetch(`/api/spark/run/${runId}/cancel`, { method: 'POST' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSparkLive() {
|
|
|
|
|
return fetchJson<SparkLive>('/api/spark/live', 15000)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function fetchSparkRuns(): Promise<SparkRun[]> {
|
|
|
|
|
const j = await fetchJson<{ runs?: SparkRun[] }>('/api/spark/runs', 15000)
|
|
|
|
|
return j?.runs ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function toggleCustodianOffload(enabled?: boolean) {
|
|
|
|
|
return fetch('/api/agent-ops/custodian/toggle', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(enabled === undefined ? {} : { enabled }),
|
|
|
|
|
})
|
|
|
|
|
}
|