feat: use the api url from device config (#161)

This commit is contained in:
Aveline 2025-02-17 11:34:38 +01:00 committed by GitHub
parent 806792203f
commit f3b4dbce49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 99 additions and 32 deletions

View File

@ -2,3 +2,5 @@ VITE_SIGNAL_API=http://localhost:3000
VITE_CLOUD_APP=http://localhost:5173 VITE_CLOUD_APP=http://localhost:5173
VITE_CLOUD_API=http://localhost:3000 VITE_CLOUD_API=http://localhost:3000
VITE_JETKVM_HEAD=

View File

@ -2,3 +2,5 @@ VITE_SIGNAL_API= # Uses the KVM device's IP address as the signal API endpoint
VITE_CLOUD_APP=https://app.jetkvm.com VITE_CLOUD_APP=https://app.jetkvm.com
VITE_CLOUD_API=https://api.jetkvm.com VITE_CLOUD_API=https://api.jetkvm.com
VITE_JETKVM_HEAD=<script src="/device/ui-config.js"></script>

View File

@ -2,3 +2,5 @@ VITE_SIGNAL_API=https://api.jetkvm.com
VITE_CLOUD_APP=https://app.jetkvm.com VITE_CLOUD_APP=https://app.jetkvm.com
VITE_CLOUD_API=https://api.jetkvm.com VITE_CLOUD_API=https://api.jetkvm.com
VITE_JETKVM_HEAD=

View File

@ -28,6 +28,7 @@
<title>JetKVM</title> <title>JetKVM</title>
<link rel="stylesheet" href="/fonts/fonts.css" /> <link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png" />
%VITE_JETKVM_HEAD%
<script> <script>
// Initial theme setup // Initial theme setup
document.documentElement.classList.toggle( document.documentElement.classList.toggle(

View File

@ -6,6 +6,7 @@ import { useLocation, useNavigation, useSearchParams } from "react-router-dom";
import Fieldset from "@components/Fieldset"; import Fieldset from "@components/Fieldset";
import GridBackground from "@components/GridBackground"; import GridBackground from "@components/GridBackground";
import StepCounter from "@components/StepCounter"; import StepCounter from "@components/StepCounter";
import { CLOUD_API } from "@/ui.config";
type AuthLayoutProps = { type AuthLayoutProps = {
title: string; title: string;
@ -62,7 +63,7 @@ export default function AuthLayout({
<Fieldset className="space-y-12"> <Fieldset className="space-y-12">
<div className="max-w-sm mx-auto space-y-4"> <div className="max-w-sm mx-auto space-y-4">
<form <form
action={`${import.meta.env.VITE_CLOUD_API}/oidc/google`} action={`${CLOUD_API}/oidc/google`}
method="POST" method="POST"
> >
{/*This could be the KVM ID*/} {/*This could be the KVM ID*/}

View File

@ -14,6 +14,7 @@ import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
import api from "../api"; import api from "../api";
import { isOnDevice } from "../main"; import { isOnDevice } from "../main";
import { Button, LinkButton } from "./Button"; import { Button, LinkButton } from "./Button";
import { CLOUD_API, SIGNAL_API } from "@/ui.config";
interface NavbarProps { interface NavbarProps {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -37,8 +38,8 @@ export default function DashboardNavbar({
const navigate = useNavigate(); const navigate = useNavigate();
const onLogout = useCallback(async () => { const onLogout = useCallback(async () => {
const logoutUrl = isOnDevice const logoutUrl = isOnDevice
? `${import.meta.env.VITE_SIGNAL_API}/auth/logout` ? `${SIGNAL_API}/auth/logout`
: `${import.meta.env.VITE_CLOUD_API}/logout`; : `${CLOUD_API}/logout`;
const res = await api.POST(logoutUrl); const res = await api.POST(logoutUrl);
if (!res.ok) return; if (!res.ok) return;

View File

@ -35,6 +35,7 @@ import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import notifications from "../notifications"; import notifications from "../notifications";
import Fieldset from "./Fieldset"; import Fieldset from "./Fieldset";
import { isOnDevice } from "../main"; import { isOnDevice } from "../main";
import { SIGNAL_API } from "@/ui.config";
export default function MountMediaModal({ export default function MountMediaModal({
open, open,
@ -1119,7 +1120,7 @@ function UploadFileView({
alreadyUploadedBytes: number, alreadyUploadedBytes: number,
dataChannel: string, dataChannel: string,
) { ) {
const uploadUrl = `${import.meta.env.VITE_SIGNAL_API}/storage/upload?uploadId=${dataChannel}`; const uploadUrl = `${SIGNAL_API}/storage/upload?uploadId=${dataChannel}`;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("POST", uploadUrl, true); xhr.open("POST", uploadUrl, true);

View File

@ -26,6 +26,7 @@ import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
import { LocalDevice } from "@routes/devices.$id"; import { LocalDevice } from "@routes/devices.$id";
import { useRevalidator } from "react-router-dom"; import { useRevalidator } from "react-router-dom";
import { ShieldCheckIcon } from "@heroicons/react/20/solid"; import { ShieldCheckIcon } from "@heroicons/react/20/solid";
import { CLOUD_APP, SIGNAL_API } from "@/ui.config";
export function SettingsItem({ export function SettingsItem({
title, title,
@ -366,7 +367,7 @@ export default function SettingsSidebar() {
const getDevice = useCallback(async () => { const getDevice = useCallback(async () => {
try { try {
const status = await api const status = await api
.GET(`${import.meta.env.VITE_SIGNAL_API}/device`) .GET(`${SIGNAL_API}/device`)
.then(res => res.json() as Promise<LocalDevice>); .then(res => res.json() as Promise<LocalDevice>);
setLocalDevice(status); setLocalDevice(status);
} catch (error) { } catch (error) {
@ -677,7 +678,7 @@ export default function SettingsSidebar() {
<div> <div>
<LinkButton <LinkButton
to={ to={
import.meta.env.VITE_CLOUD_APP + CLOUD_APP +
"/signup?deviceId=" + "/signup?deviceId=" +
deviceId + deviceId +
`&returnTo=${location.href}adopt` `&returnTo=${location.href}adopt`

View File

@ -27,12 +27,13 @@ import LoginLocalRoute from "./routes/login-local";
import WelcomeLocalModeRoute from "./routes/welcome-local.mode"; import WelcomeLocalModeRoute from "./routes/welcome-local.mode";
import WelcomeRoute from "./routes/welcome-local"; import WelcomeRoute from "./routes/welcome-local";
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password"; import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
import { CLOUD_API } from "./ui.config";
export const isOnDevice = import.meta.env.MODE === "device"; export const isOnDevice = import.meta.env.MODE === "device";
export const isInCloud = !isOnDevice; export const isInCloud = !isOnDevice;
export async function checkAuth() { export async function checkAuth() {
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/me`, { const res = await fetch(`${CLOUD_API}/me`, {
mode: "cors", mode: "cors",
credentials: "include", credentials: "include",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@ -1,5 +1,6 @@
import { LoaderFunctionArgs, redirect } from "react-router-dom"; import { LoaderFunctionArgs, redirect } from "react-router-dom";
import api from "../api"; import api from "../api";
import { CLOUD_API, CLOUD_APP, SIGNAL_API } from "@/ui.config";
const loader = async ({ request }: LoaderFunctionArgs) => { const loader = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
@ -11,17 +12,17 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
const clientId = searchParams.get("clientId"); const clientId = searchParams.get("clientId");
const res = await api.POST( const res = await api.POST(
`${import.meta.env.VITE_SIGNAL_API}/cloud/register`, `${SIGNAL_API}/cloud/register`,
{ {
token: tempToken, token: tempToken,
cloudApi: import.meta.env.VITE_CLOUD_API, cloudApi: CLOUD_API,
oidcGoogle, oidcGoogle,
clientId, clientId,
}, },
); );
if (!res.ok) throw new Error("Failed to register device"); if (!res.ok) throw new Error("Failed to register device");
return redirect(import.meta.env.VITE_CLOUD_APP + `/devices/${deviceId}/setup`); return redirect(CLOUD_APP + `/devices/${deviceId}/setup`);
}; };
export default function AdoptRoute() { export default function AdoptRoute() {

View File

@ -14,6 +14,7 @@ import { User } from "@/hooks/stores";
import { checkAuth } from "@/main"; import { checkAuth } from "@/main";
import Fieldset from "@components/Fieldset"; import Fieldset from "@components/Fieldset";
import { ChevronLeftIcon } from "@heroicons/react/16/solid"; import { ChevronLeftIcon } from "@heroicons/react/16/solid";
import { CLOUD_API } from "@/ui.config";
interface LoaderData { interface LoaderData {
device: { id: string; name: string; user: { googleId: string } }; device: { id: string; name: string; user: { googleId: string } };
@ -24,7 +25,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
const { deviceId } = Object.fromEntries(await request.formData()); const { deviceId } = Object.fromEntries(await request.formData());
try { try {
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${deviceId}`, { const res = await fetch(`${CLOUD_API}/devices/${deviceId}`, {
method: "DELETE", method: "DELETE",
credentials: "include", credentials: "include",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -46,7 +47,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
const { id } = params; const { id } = params;
try { try {
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, { const res = await fetch(`${CLOUD_API}/devices/${id}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",

View File

@ -16,6 +16,7 @@ import { User } from "@/hooks/stores";
import { checkAuth } from "@/main"; import { checkAuth } from "@/main";
import Fieldset from "@components/Fieldset"; import Fieldset from "@components/Fieldset";
import api from "../api"; import api from "../api";
import { CLOUD_API } from "@/ui.config";
interface LoaderData { interface LoaderData {
device: { id: string; name: string; user: { googleId: string } }; device: { id: string; name: string; user: { googleId: string } };
@ -31,7 +32,7 @@ const action = async ({ params, request }: ActionFunctionArgs) => {
} }
try { try {
const res = await api.PUT(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, { const res = await api.PUT(`${CLOUD_API}/devices/${id}`, {
name, name,
}); });
if (!res.ok) { if (!res.ok) {
@ -49,7 +50,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
const { id } = params; const { id } = params;
try { try {
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, { const res = await fetch(`${CLOUD_API}/devices/${id}`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",

View File

@ -16,10 +16,11 @@ import { InputFieldWithLabel } from "@components/InputField";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { checkAuth } from "@/main"; import { checkAuth } from "@/main";
import api from "../api"; import api from "../api";
import { CLOUD_API } from "@/ui.config";
const loader = async ({ params }: LoaderFunctionArgs) => { const loader = async ({ params }: LoaderFunctionArgs) => {
await checkAuth(); await checkAuth();
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${params.id}`, { const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
method: "GET", method: "GET",
mode: "cors", mode: "cors",
credentials: "include", credentials: "include",
@ -35,7 +36,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
const action = async ({ request }: ActionFunctionArgs) => { const action = async ({ request }: ActionFunctionArgs) => {
// Handle form submission // Handle form submission
const { name, id, returnTo } = Object.fromEntries(await request.formData()); const { name, id, returnTo } = Object.fromEntries(await request.formData());
const res = await api.PUT(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, { name }); const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
if (res.ok) { if (res.ok) {
return redirect(returnTo?.toString() ?? `/devices/${id}`); return redirect(returnTo?.toString() ?? `/devices/${id}`);

View File

@ -36,6 +36,7 @@ import { DeviceStatus } from "./welcome-local";
import FocusTrap from "focus-trap-react"; import FocusTrap from "focus-trap-react";
import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal"; import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal";
import TerminalWrapper from "../components/Terminal"; import TerminalWrapper from "../components/Terminal";
import { CLOUD_API, SIGNAL_API } from "@/ui.config";
interface LocalLoaderResp { interface LocalLoaderResp {
authMode: "password" | "noPassword" | null; authMode: "password" | "noPassword" | null;
@ -56,12 +57,12 @@ export interface LocalDevice {
const deviceLoader = async () => { const deviceLoader = async () => {
const res = await api const res = await api
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`) .GET(`${SIGNAL_API}/device/status`)
.then(res => res.json() as Promise<DeviceStatus>); .then(res => res.json() as Promise<DeviceStatus>);
if (!res.isSetup) return redirect("/welcome"); if (!res.isSetup) return redirect("/welcome");
const deviceRes = await api.GET(`${import.meta.env.VITE_SIGNAL_API}/device`); const deviceRes = await api.GET(`${SIGNAL_API}/device`);
if (deviceRes.status === 401) return redirect("/login-local"); if (deviceRes.status === 401) return redirect("/login-local");
if (deviceRes.ok) { if (deviceRes.ok) {
const device = (await deviceRes.json()) as LocalDevice; const device = (await deviceRes.json()) as LocalDevice;
@ -74,11 +75,11 @@ const deviceLoader = async () => {
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => { const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
const user = await checkAuth(); const user = await checkAuth();
const iceResp = await api.POST(`${import.meta.env.VITE_CLOUD_API}/webrtc/ice_config`); const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`);
const iceConfig = await iceResp.json(); const iceConfig = await iceResp.json();
const deviceResp = await api.GET( const deviceResp = await api.GET(
`${import.meta.env.VITE_CLOUD_API}/devices/${params.id}`, `${CLOUD_API}/devices/${params.id}`,
); );
if (!deviceResp.ok) { if (!deviceResp.ok) {
@ -142,7 +143,7 @@ export default function KvmIdRoute() {
try { try {
const sd = btoa(JSON.stringify(pc.localDescription)); const sd = btoa(JSON.stringify(pc.localDescription));
const res = await api.POST(`${import.meta.env.VITE_SIGNAL_API}/webrtc/session`, { const res = await api.POST(`${SIGNAL_API}/webrtc/session`, {
sd, sd,
// When on device, we don't need to specify the device id, as it's already known // When on device, we don't need to specify the device id, as it's already known
...(isOnDevice ? {} : { id: params.id }), ...(isOnDevice ? {} : { id: params.id }),
@ -317,7 +318,7 @@ export default function KvmIdRoute() {
} }
// Fire and forget // Fire and forget
api.POST(`${import.meta.env.VITE_CLOUD_API}/webrtc/turn_activity`, { api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
bytesReceived: bytesReceivedDelta, bytesReceived: bytesReceivedDelta,
bytesSent: bytesSentDelta, bytesSent: bytesSentDelta,
}); });

View File

@ -9,6 +9,7 @@ import { User } from "@/hooks/stores";
import EmptyCard from "@components/EmptyCard"; import EmptyCard from "@components/EmptyCard";
import { LuMonitorSmartphone } from "react-icons/lu"; import { LuMonitorSmartphone } from "react-icons/lu";
import { ArrowRightIcon } from "@heroicons/react/16/solid"; import { ArrowRightIcon } from "@heroicons/react/16/solid";
import { CLOUD_API } from "@/ui.config";
interface LoaderData { interface LoaderData {
devices: { id: string; name: string; online: boolean; lastSeen: string }[]; devices: { id: string; name: string; online: boolean; lastSeen: string }[];
@ -19,7 +20,7 @@ export const loader = async () => {
const user = await checkAuth(); const user = await checkAuth();
try { try {
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices`, { const res = await fetch(`${CLOUD_API}/devices`, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
mode: "cors", mode: "cors",

View File

@ -12,15 +12,16 @@ import LogoWhiteIcon from "@/assets/logo-white.svg";
import api from "../api"; import api from "../api";
import { DeviceStatus } from "./welcome-local"; import { DeviceStatus } from "./welcome-local";
import ExtLink from "../components/ExtLink"; import ExtLink from "../components/ExtLink";
import { SIGNAL_API } from "@/ui.config";
const loader = async () => { const loader = async () => {
const res = await api const res = await api
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`) .GET(`${SIGNAL_API}/device/status`)
.then(res => res.json() as Promise<DeviceStatus>); .then(res => res.json() as Promise<DeviceStatus>);
if (!res.isSetup) return redirect("/welcome"); if (!res.isSetup) return redirect("/welcome");
const deviceRes = await api.GET(`${import.meta.env.VITE_SIGNAL_API}/device`); const deviceRes = await api.GET(`${SIGNAL_API}/device`);
if (deviceRes.ok) return redirect("/"); if (deviceRes.ok) return redirect("/");
return null; return null;
}; };
@ -31,7 +32,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
try { try {
const response = await api.POST( const response = await api.POST(
`${import.meta.env.VITE_SIGNAL_API}/auth/login-local`, `${SIGNAL_API}/auth/login-local`,
{ {
password, password,
}, },

View File

@ -9,10 +9,11 @@ import LogoWhiteIcon from "@/assets/logo-white.svg";
import { cx } from "../cva.config"; import { cx } from "../cva.config";
import api from "../api"; import api from "../api";
import { DeviceStatus } from "./welcome-local"; import { DeviceStatus } from "./welcome-local";
import { SIGNAL_API } from "@/ui.config";
const loader = async () => { const loader = async () => {
const res = await api const res = await api
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`) .GET(`${SIGNAL_API}/device/status`)
.then(res => res.json() as Promise<DeviceStatus>); .then(res => res.json() as Promise<DeviceStatus>);
if (res.isSetup) return redirect("/login-local"); if (res.isSetup) return redirect("/login-local");
@ -30,7 +31,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
if (localAuthMode === "noPassword") { if (localAuthMode === "noPassword") {
try { try {
await api.POST(`${import.meta.env.VITE_SIGNAL_API}/device/setup`, { await api.POST(`${SIGNAL_API}/device/setup`, {
localAuthMode, localAuthMode,
}); });
return redirect("/"); return redirect("/");

View File

@ -10,10 +10,11 @@ import LogoBlueIcon from "@/assets/logo-blue.png";
import LogoWhiteIcon from "@/assets/logo-white.svg"; import LogoWhiteIcon from "@/assets/logo-white.svg";
import api from "../api"; import api from "../api";
import { DeviceStatus } from "./welcome-local"; import { DeviceStatus } from "./welcome-local";
import { SIGNAL_API } from "@/ui.config";
const loader = async () => { const loader = async () => {
const res = await api const res = await api
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`) .GET(`${SIGNAL_API}/device/status`)
.then(res => res.json() as Promise<DeviceStatus>); .then(res => res.json() as Promise<DeviceStatus>);
if (res.isSetup) return redirect("/login-local"); if (res.isSetup) return redirect("/login-local");
@ -30,7 +31,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
} }
try { try {
const response = await api.POST(`${import.meta.env.VITE_SIGNAL_API}/device/setup`, { const response = await api.POST(`${SIGNAL_API}/device/setup`, {
localAuthMode: "password", localAuthMode: "password",
password, password,
}); });

View File

@ -9,6 +9,7 @@ import LogoMark from "@/assets/logo-mark.png";
import { cx } from "cva"; import { cx } from "cva";
import api from "../api"; import api from "../api";
import { redirect } from "react-router-dom"; import { redirect } from "react-router-dom";
import { SIGNAL_API } from "@/ui.config";
export interface DeviceStatus { export interface DeviceStatus {
isSetup: boolean; isSetup: boolean;
@ -16,7 +17,7 @@ export interface DeviceStatus {
const loader = async () => { const loader = async () => {
const res = await api const res = await api
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`) .GET(`${SIGNAL_API}/device/status`)
.then(res => res.json() as Promise<DeviceStatus>); .then(res => res.json() as Promise<DeviceStatus>);
if (res.isSetup) return redirect("/login-local"); if (res.isSetup) return redirect("/login-local");

23
ui/src/ui.config.ts Normal file
View File

@ -0,0 +1,23 @@
interface JetKVMConfig {
CLOUD_API?: string;
CLOUD_APP?: string;
DEVICE_VERSION?: string;
}
declare global {
interface Window { JETKVM_CONFIG?: JetKVMConfig; }
}
const getAppURL = (api_url?: string) => {
if (!api_url) {
return;
}
const url = new URL(api_url);
url.host = url.host.replace(/api\./, "app.");
// remove the ending slash
return url.toString().replace(/\/$/, "");
}
export const CLOUD_API = window.JETKVM_CONFIG?.CLOUD_API || import.meta.env.VITE_CLOUD_API;
export const CLOUD_APP = window.JETKVM_CONFIG?.CLOUD_APP || getAppURL(CLOUD_API) || import.meta.env.VITE_CLOUD_APP;
export const SIGNAL_API = import.meta.env.VITE_SIGNAL_API;

22
web.go
View File

@ -2,6 +2,8 @@ package kvm
import ( import (
"embed" "embed"
"encoding/json"
"fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -77,6 +79,9 @@ func setupRouter() *gin.Engine {
// We use this to determine if the device is setup // We use this to determine if the device is setup
r.GET("/device/status", handleDeviceStatus) r.GET("/device/status", handleDeviceStatus)
// We use this to provide the UI with the device configuration
r.GET("/device/ui-config.js", handleDeviceUIConfig)
// We use this to setup the device in the welcome page // We use this to setup the device in the welcome page
r.POST("/device/setup", handleSetup) r.POST("/device/setup", handleSetup)
@ -361,6 +366,23 @@ func handleDeviceStatus(c *gin.Context) {
c.JSON(http.StatusOK, response) c.JSON(http.StatusOK, response)
} }
func handleDeviceUIConfig(c *gin.Context) {
LoadConfig()
config, _ := json.Marshal(gin.H{
"CLOUD_API": config.CloudURL,
"DEVICE_VERSION": builtAppVersion,
})
if config == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
return
}
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
}
func handleSetup(c *gin.Context) { func handleSetup(c *gin.Context) {
LoadConfig() LoadConfig()