From 75138cdbc82ae2edec5e48508b168ed9cf41521b Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Sun, 26 Apr 2026 11:53:53 -0600 Subject: [PATCH] Added Nginx Reverse Proxy --- src/app/apps/[service]/page.tsx | 204 ++++++++++++++++++++++++++++++++ src/app/apps/page.tsx | 42 +++++++ src/components/SiteHeader.tsx | 3 + 3 files changed, 249 insertions(+) create mode 100644 src/app/apps/[service]/page.tsx create mode 100644 src/app/apps/page.tsx diff --git a/src/app/apps/[service]/page.tsx b/src/app/apps/[service]/page.tsx new file mode 100644 index 0000000..929362f --- /dev/null +++ b/src/app/apps/[service]/page.tsx @@ -0,0 +1,204 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import Link from "next/link"; +import { + fetchCoursewareRuntimeConfig, + normalizeCoursewareRuntimeConfig, + type ResolvedCoursewareRuntimeConfig, +} from "~/lib/courseware-runtime"; + +type PageProps = { + params: { service: string }; + searchParams?: Record; +}; + +type ServiceDescriptor = { + label: string; + resolveUrl: (runtime: ResolvedCoursewareRuntimeConfig) => string | null; + allowIframe: boolean; +}; + +function coerceSearchParam( + value: string | string[] | undefined, +): string | undefined { + if (Array.isArray(value)) return value[0]; + return value; +} + +function resolveDescriptor(serviceId: string): ServiceDescriptor | null { + switch (serviceId) { + case "open-webui": + return { + label: "Open WebUI", + resolveUrl: (runtime) => runtime.services["open-webui"], + allowIframe: false, + }; + case "promptfoo": + return { + label: "Promptfoo", + resolveUrl: (runtime) => runtime.services.promptfoo, + allowIframe: false, + }; + case "chunkviz": + return { + label: "ChunkViz", + resolveUrl: (runtime) => runtime.services.chunkviz, + allowIframe: false, + }; + case "embedding-atlas": + return { + label: "Embedding Atlas", + resolveUrl: (runtime) => runtime.services["embedding-atlas"], + allowIframe: false, + }; + case "unsloth": + return { + label: "Unsloth Studio", + resolveUrl: (runtime) => runtime.services.unsloth, + allowIframe: false, + }; + case "wetty": + return { + label: "Lab 3 Terminal", + resolveUrl: (runtime) => runtime.lab3TerminalUrl, + allowIframe: true, + }; + case "netron": + return { + label: "Netron", + resolveUrl: (runtime) => runtime.lab1NetronUrl, + allowIframe: false, + }; + default: + return null; + } +} + +export default function ServiceAppPage({ params, searchParams }: PageProps) { + const serviceId = params.service?.trim() ?? ""; + const descriptor = useMemo( + () => resolveDescriptor(serviceId), + [serviceId], + ); + const embedParam = coerceSearchParam(searchParams?.embed); + const embed = embedParam === "1" || embedParam === "true"; + + const [runtime, setRuntime] = useState(() => + normalizeCoursewareRuntimeConfig(), + ); + const [isRuntimeResolved, setIsRuntimeResolved] = useState(false); + + useEffect(() => { + let isCancelled = false; + void fetchCoursewareRuntimeConfig() + .then((nextRuntime) => { + if (isCancelled) return; + setRuntime(nextRuntime); + }) + .catch(() => { + if (isCancelled) return; + setRuntime(normalizeCoursewareRuntimeConfig()); + }) + .finally(() => { + if (isCancelled) return; + setIsRuntimeResolved(true); + }); + + return () => { + isCancelled = true; + }; + }, []); + + if (!descriptor) { + return ( +
+

+ Unknown tool +

+

+ The tool {serviceId} is not configured. +

+
+ + Back to tools + +
+
+ ); + } + + const resolvedUrl = descriptor.resolveUrl(runtime); + const shouldEmbed = embed && descriptor.allowIframe; + + if (!isRuntimeResolved) { + return ( +
+

+ Loading {descriptor.label}… +

+
+ ); + } + + if (!resolvedUrl) { + return ( +
+

+ {descriptor.label} is unavailable +

+

+ The runtime config did not provide a URL for this service. +

+
+ + Back to tools + +
+
+ ); + } + + if (!shouldEmbed) { + // Full-screen navigation: replace the current page so the URL remains a single origin path. + // (Embedding is reserved for services that explicitly allow iframes, like WeTTY.) + if (typeof window !== "undefined") { + window.location.replace(resolvedUrl); + } + + return ( +
+

+ Opening {descriptor.label}… +

+

+ If you are not redirected automatically, open{" "} + + {resolvedUrl} + + . +

+
+ + Back to tools + +
+
+ ); + } + + return ( +
+