export const COURSEWARE_RUNTIME_CONFIG_PATH = "/courseware-runtime.json"; export const LAB1_DEFAULT_NETRON_URL = "http://127.0.0.1:8338"; export const LAB2_DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"; export const LAB2_DEFAULT_OLLAMA_MODELS = [ { label: "Gemma 4 E2B Q4", value: "batiai/gemma4-e2b:q4", }, { label: "Gemma 4 E2B Q6", value: "batiai/gemma4-e2b:q6", }, ] as const; export const LAB3_DEFAULT_TERMINAL_PATH = "/wetty"; export const COURSEWARE_SERVICE_IDS = [ "open-webui", "promptfoo", "chunkviz", "embedding-atlas", "unsloth", "ssh", ] as const; export type CoursewareRuntimeModelOption = { label: string; value: string; }; export type CoursewareServiceId = (typeof COURSEWARE_SERVICE_IDS)[number]; export type CoursewareRuntimeServices = Partial< Record >; export type ResolvedCoursewareRuntimeServices = Record< CoursewareServiceId, string >; export const COURSEWARE_SERVICE_DEFAULTS: ResolvedCoursewareRuntimeServices = { "chunkviz": "http://127.0.0.1:3000", "embedding-atlas": "http://127.0.0.1:5055", "open-webui": "http://127.0.0.1:8080", "promptfoo": "http://127.0.0.1:15500", "ssh": "ssh://127.0.0.1:22", "unsloth": "http://127.0.0.1:8888", }; export type CoursewareRuntimeConfig = { lab1NetronUrl?: string; lab2OllamaModels?: CoursewareRuntimeModelOption[]; lab2OllamaUrl?: string; lab3TerminalUrl?: string; services?: CoursewareRuntimeServices; }; export type ResolvedCoursewareRuntimeConfig = { lab1NetronUrl: string; lab2OllamaModels: CoursewareRuntimeModelOption[]; lab2OllamaUrl: string; lab3TerminalUrl: string; services: ResolvedCoursewareRuntimeServices; }; const loopbackHosts = new Set(["127.0.0.1", "localhost", "::1"]); function rewriteLoopbackHost(urlValue: string, currentHostname?: string) { try { const url = new URL(urlValue); if (!currentHostname || !loopbackHosts.has(url.hostname)) { return url.toString(); } url.hostname = currentHostname; return url.toString(); } catch { return urlValue; } } function getCurrentHostname() { if (typeof window === "undefined") { return undefined; } const hostname = window.location.hostname?.trim(); return hostname || undefined; } export function isCoursewareServiceId( value: string, ): value is CoursewareServiceId { return (COURSEWARE_SERVICE_IDS as readonly string[]).includes(value); } export function getCoursewareServiceBaseUrl( serviceId: CoursewareServiceId, envValue?: string, currentHostname = getCurrentHostname(), ) { const trimmedValue = envValue?.trim(); const baseUrl = trimmedValue || COURSEWARE_SERVICE_DEFAULTS[serviceId]; return rewriteLoopbackHost(baseUrl, currentHostname); } export function getCoursewareServices( envValue?: CoursewareRuntimeServices, currentHostname = getCurrentHostname(), ): ResolvedCoursewareRuntimeServices { const services = {} as ResolvedCoursewareRuntimeServices; for (const serviceId of COURSEWARE_SERVICE_IDS) { services[serviceId] = getCoursewareServiceBaseUrl( serviceId, envValue?.[serviceId], currentHostname, ); } return services; } function appendUrlPath(urlValue: string, pathSuffix: string) { const trimmedSuffix = pathSuffix.trim(); if (!trimmedSuffix) { return urlValue; } try { const url = new URL(urlValue); const normalizedSuffix = trimmedSuffix.startsWith("/") ? trimmedSuffix : `/${trimmedSuffix}`; const basePath = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, ""); url.pathname = `${basePath}${normalizedSuffix}`; return url.toString(); } catch { return urlValue; } } export function resolveCoursewareServiceBaseUrl( runtimeConfig: Pick, serviceId: CoursewareServiceId, ) { return runtimeConfig.services[serviceId]; } export function resolveCoursewareServiceUrl( runtimeConfig: Pick, serviceId: CoursewareServiceId, pathSuffix?: string, ) { const baseUrl = resolveCoursewareServiceBaseUrl(runtimeConfig, serviceId); if (!pathSuffix?.trim()) { return baseUrl; } return appendUrlPath(baseUrl, pathSuffix); } export function resolveCoursewareServiceAddress( runtimeConfig: Pick, serviceId: CoursewareServiceId, ) { const baseUrl = resolveCoursewareServiceBaseUrl(runtimeConfig, serviceId); try { const url = new URL(baseUrl); return url.port ? `${url.hostname}:${url.port}` : url.hostname; } catch { return baseUrl; } } export function getLab1NetronUrl( envValue?: string, currentHostname = getCurrentHostname(), ) { const trimmedValue = envValue?.trim(); if (!trimmedValue) { return rewriteLoopbackHost(LAB1_DEFAULT_NETRON_URL, currentHostname); } return rewriteLoopbackHost(trimmedValue, currentHostname); } export function getLab2OllamaUrl( envValue?: string, currentHostname = getCurrentHostname(), ) { const trimmedValue = envValue?.trim(); if (!trimmedValue) { return rewriteLoopbackHost(LAB2_DEFAULT_OLLAMA_URL, currentHostname); } return rewriteLoopbackHost(trimmedValue, currentHostname); } export function getLab2OllamaModels( envValue?: CoursewareRuntimeModelOption[], ) { if (!Array.isArray(envValue) || envValue.length === 0) { return LAB2_DEFAULT_OLLAMA_MODELS.map((model) => ({ ...model })); } const normalizedModels = envValue .map((model) => { const label = model?.label?.trim(); const value = model?.value?.trim(); if (!label || !value) { return null; } return { label, value } satisfies CoursewareRuntimeModelOption; }) .filter( (model): model is CoursewareRuntimeModelOption => model !== null, ); if (normalizedModels.length === 0) { return LAB2_DEFAULT_OLLAMA_MODELS.map((model) => ({ ...model })); } return normalizedModels; } export function getLab3TerminalPath( envValue?: string, currentHostname = getCurrentHostname(), ) { const trimmedValue = envValue?.trim(); if (!trimmedValue) { return LAB3_DEFAULT_TERMINAL_PATH; } if (/^https?:\/\//i.test(trimmedValue)) { return rewriteLoopbackHost(trimmedValue, currentHostname); } return trimmedValue.startsWith("/") ? trimmedValue : `/${trimmedValue}`; } export function normalizeCoursewareRuntimeConfig( config?: CoursewareRuntimeConfig, currentHostname = getCurrentHostname(), ): ResolvedCoursewareRuntimeConfig { return { lab1NetronUrl: getLab1NetronUrl(config?.lab1NetronUrl, currentHostname), lab2OllamaModels: getLab2OllamaModels(config?.lab2OllamaModels), lab2OllamaUrl: getLab2OllamaUrl(config?.lab2OllamaUrl, currentHostname), lab3TerminalUrl: getLab3TerminalPath( config?.lab3TerminalUrl, currentHostname, ), services: getCoursewareServices(config?.services, currentHostname), }; } export async function fetchCoursewareRuntimeConfig() { const response = await fetch(COURSEWARE_RUNTIME_CONFIG_PATH, { cache: "no-store", }); if (!response.ok) { throw new Error(`Runtime config request failed: ${response.status}`); } const config = (await response.json()) as CoursewareRuntimeConfig; return normalizeCoursewareRuntimeConfig(config); }