Added Nginx Reverse Proxy

This commit is contained in:
2026-04-26 11:53:53 -06:00
parent 562be3fd1f
commit 75138cdbc8
3 changed files with 249 additions and 0 deletions
+204
View File
@@ -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<string, string | string[] | undefined>;
};
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<ResolvedCoursewareRuntimeConfig>(() =>
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 (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<h1 className="text-2xl font-semibold text-[#004E78]">
Unknown tool
</h1>
<p className="mt-2 text-slate-700">
The tool <code>{serviceId}</code> is not configured.
</p>
<div className="mt-6">
<Link href="/apps" className="text-[#004E78] hover:text-[#F89C27]">
Back to tools
</Link>
</div>
</main>
);
}
const resolvedUrl = descriptor.resolveUrl(runtime);
const shouldEmbed = embed && descriptor.allowIframe;
if (!isRuntimeResolved) {
return (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<h1 className="text-2xl font-semibold text-[#004E78]">
Loading {descriptor.label}
</h1>
</main>
);
}
if (!resolvedUrl) {
return (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<h1 className="text-2xl font-semibold text-[#004E78]">
{descriptor.label} is unavailable
</h1>
<p className="mt-2 text-slate-700">
The runtime config did not provide a URL for this service.
</p>
<div className="mt-6">
<Link href="/apps" className="text-[#004E78] hover:text-[#F89C27]">
Back to tools
</Link>
</div>
</main>
);
}
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 (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<h1 className="text-2xl font-semibold text-[#004E78]">
Opening {descriptor.label}
</h1>
<p className="mt-3 text-slate-700">
If you are not redirected automatically, open{" "}
<a
className="text-[#004E78] underline hover:text-[#F89C27]"
href={resolvedUrl}
>
{resolvedUrl}
</a>
.
</p>
<div className="mt-6">
<Link href="/apps" className="text-[#004E78] hover:text-[#F89C27]">
Back to tools
</Link>
</div>
</main>
);
}
return (
<div className="h-[calc(100vh-0px)] w-full">
<iframe
title={descriptor.label}
src={resolvedUrl}
className="h-screen w-full border-0"
allow="clipboard-read; clipboard-write"
/>
</div>
);
}
+42
View File
@@ -0,0 +1,42 @@
import Link from "next/link";
const tools = [
{ id: "open-webui", label: "Open WebUI" },
{ id: "netron", label: "Netron" },
{ id: "chunkviz", label: "ChunkViz" },
{ id: "embedding-atlas", label: "Embedding Atlas" },
{ id: "unsloth", label: "Unsloth Studio" },
{ id: "promptfoo", label: "Promptfoo" },
{ id: "wetty", label: "Lab 3 Terminal" },
] as const;
export default function AppsIndexPage() {
return (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<section className="rounded-xl border border-[#f6d5a5] bg-white p-8 shadow-sm">
<h1 className="mb-3 text-3xl font-bold text-[#004E78]">Lab Tools</h1>
<p className="max-w-3xl text-slate-700">
Launch the courseware services full-screen under the same URL.
</p>
</section>
<section className="mt-8 grid gap-4 md:grid-cols-2">
{tools.map((tool) => (
<Link
key={tool.id}
href={tool.id === "wetty" ? "/apps/wetty" : `/apps/${tool.id}`}
className="block rounded-lg border border-slate-200 bg-white p-5 transition hover:border-[#F89C27] hover:shadow-sm"
>
<h2 className="text-lg font-semibold text-[#004E78]">
{tool.label}
</h2>
<p className="mt-2 text-sm text-slate-600">
Open full-screen
</p>
</Link>
))}
</section>
</main>
);
}
+3
View File
@@ -16,6 +16,9 @@ export function SiteHeader() {
<Link href="/labs" className="hover:text-[#F89C27]">
Labs
</Link>
<Link href="/apps" className="hover:text-[#F89C27]">
Tools
</Link>
<Link
href="https://discord.gg/Ma9UZNBxvh"
className="rounded-md border border-[#F89C27] px-3 py-1.5 text-[#004E78] hover:bg-[#F89C27] hover:text-white"