feat: fix YAML field conversion, admin namespace, streaming logs, and vllm-serve deploy

- Fix Axios keysToSnake converting user values map keys (gpuMem -> gpu_mem)
  - Add skipRecurseKeys to keysToSnake for values/valuesYaml fields
  - Add values_yaml alt json tag and Normalize() in DTOs
  - Check both camelCase/snake_case in enforceNamespaceValues
  - Read both tailLines/tail_lines query param for diagnostics
- Admin users can freely choose namespace in LaunchModal (free-text input)
  - Block only kube-system/kube-public/kube-node-lease for admin
  - Regular users keep existing namespace restrictions
- Add SSE streaming pod logs endpoint (backend + frontend)
  - New PodLogStreamer interface and K8s Follow:true implementation
  - SSE handler with text/event-stream output
  - Frontend DiagnosticsModal: Stream button, auto-scroll, live indicator
- Remove per-card Refresh button from InstanceCard (redundant with page refresh)
- Deploy bge-m3 on vllm-serve 0.6.0 (gpuMem=10000, status=deployed)
This commit is contained in:
Ivan087
2026-05-12 16:50:25 +08:00
parent 7f238a3168
commit 7d9545f827
13 changed files with 475 additions and 61 deletions

View File

@ -76,7 +76,7 @@ import type {
PutRegistriesRegistryIdPathParameters,
} from './generated-orval/api.schemas';
import { customAxiosInstance } from './axios-mutator';
import { AXIOS_INSTANCE, customAxiosInstance } from './axios-mutator';
import {
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation as GeneratedInstanceLastOperationEnum,
@ -247,6 +247,88 @@ export const getInstanceDiagnostics = (
params: options?.tailLines ? { tailLines: options.tailLines } : undefined,
});
/**
* Stream pod logs via SSE from the backend.
* Returns an AbortController to cancel the stream at any time.
*/
export function streamInstanceLogs(
clusterId: string,
instanceId: string,
pod: string,
container: string,
tailLines: number = 200,
onLine: (line: string) => void,
onDone: () => void,
onError: (err: Error) => void,
): AbortController {
const controller = new AbortController();
const baseUrl = AXIOS_INSTANCE.defaults.baseURL ?? "/api/v1";
const authHeader = AXIOS_INSTANCE.defaults.headers.common["Authorization"] as string | undefined;
const params = new URLSearchParams({ pod, container, tailLines: String(tailLines) });
const url = `${baseUrl}/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/logs/stream?${params}`;
const headers: Record<string, string> = { Accept: "text/event-stream" };
if (authHeader) {
headers["Authorization"] = authHeader;
}
fetch(url, { headers, signal: controller.signal })
.then(async (response) => {
if (!response.ok) {
const text = await response.text().catch(() => response.statusText);
onError(new Error(`HTTP ${response.status}: ${text}`));
return;
}
const reader = response.body?.getReader();
if (!reader) {
onError(new Error("ReadableStream not supported"));
return;
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
// Keep the last potentially-incomplete line in the buffer
buffer = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith("data:")) continue;
const data = trimmed.slice(5).trim();
if (data === "[DONE]") {
onDone();
return;
}
if (data.startsWith("[ERROR]")) {
onError(new Error(data.slice(7).trim()));
continue;
}
onLine(data);
}
}
} catch (err: unknown) {
if (err instanceof DOMException && err.name === "AbortError") {
// Stream was intentionally cancelled - not an error
return;
}
onError(err instanceof Error ? err : new Error(String(err)));
}
onDone();
})
.catch((err: unknown) => {
if (err instanceof DOMException && err.name === "AbortError") {
return;
}
onError(err instanceof Error ? err : new Error(String(err)));
});
return controller;
}
export const listRegistries = getRegistries;
export const createRegistry = postRegistries;
export const getRegistry = getRegistriesRegistryId;