/* eslint-disable react-refresh/only-export-components */
import { prepareClientCacheVersion } from "@/lib/client-cache-version";
import { StrictMode, Suspense, lazy, useState, useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";
import "./ui/index.css";
import { ThemeProvider } from "@/components/theme-provider";
import "./ui/i18n/i18n";
import { isElectron } from "@/lib/electron";
import { Toaster } from "@/components/sonner";
import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth";
import { getUserInfo, getCurrentToken, appReadyPromise } from "@/main-axios";
import { applyAccentColor, applyFontSize } from "@/lib/theme";
import { installElectronWheelZoomGuard } from "@/lib/electron-wheel-zoom";
import type { FontSizeId } from "@/types/ui-types";
import { useServiceWorker } from "@/hooks/use-service-worker";
import { useTranslation } from "react-i18next";
const AppShell = lazy(() =>
import("@/AppShell").then((m) => ({ default: m.AppShell })),
);
// Full-screen apps opened via query params (e.g. from external links or Electron)
const TerminalApp = lazy(() =>
import("@/features/terminal/TerminalApp").then((m) => ({
default: m.default,
})),
);
const FileManagerApp = lazy(() =>
import("@/features/file-manager/FileManagerApp").then((m) => ({
default: m.default,
})),
);
const TunnelApp = lazy(() =>
import("@/features/tunnel/TunnelApp").then((m) => ({ default: m.default })),
);
const HostMetricsApp = lazy(() =>
import("@/features/host-metrics/HostMetricsApp").then((m) => ({
default: m.default,
})),
);
const DockerApp = lazy(() =>
import("@/features/docker/DockerApp").then((m) => ({ default: m.default })),
);
const GuacamoleApp = lazy(() =>
import("@/features/guacamole/GuacamoleApp").then((m) => ({
default: m.default,
})),
);
// --- tmux-monitor ---
const TmuxMonitorApp = lazy(() =>
import("@/features/tmux-monitor/TmuxMonitorApp").then((m) => ({
default: m.default,
})),
);
const HomepageApp = lazy(() =>
import("@/features/homepage/HomepageApp").then((m) => ({
default: m.default,
})),
);
const ElectronVersionCheck = lazy(() =>
import("@/user/ElectronVersionCheck").then((module) => ({
default: module.ElectronVersionCheck,
})),
);
type Phase =
| "verifying"
| "idle-auth"
| "fading-in"
| "idle-app"
| "fading-out";
function FullscreenApp() {
const searchParams = new URLSearchParams(window.location.search);
const view = searchParams.get("view");
const hostId = searchParams.get("hostId");
const tmuxSession = searchParams.get("tmuxSession");
const path = searchParams.get("path");
switch (view) {
case "terminal":
return (
);
case "file-manager":
return (
);
case "tunnel":
return ;
case "host-metrics":
case "server-stats":
return ;
case "docker":
return ;
case "rdp":
case "vnc":
case "telnet":
return (
);
case "tmux-monitor": // --- tmux-monitor ---
case "tmux_monitor": // tab type spelling, so copied links also resolve
return ;
case "homepage":
return ;
default:
return null;
}
}
function FullscreenAppGate() {
const { t } = useTranslation();
const [ready, setReady] = useState(false);
const [authFailed, setAuthFailed] = useState(false);
useEffect(() => {
let cancelled = false;
appReadyPromise
.then(() => getUserInfo())
.then(async () => {
if (isElectron()) {
try {
const token = await getCurrentToken();
if (token) localStorage.setItem("jwt", token);
} catch {
// WebSocket connections can still fall back to cookie auth.
}
}
if (!cancelled) setReady(true);
})
.catch(() => {
if (!cancelled) setAuthFailed(true);
});
return () => {
cancelled = true;
};
}, []);
if (authFailed) {
return ;
}
if (!ready) {
return (
);
}
return ;
}
function App() {
const stored = getStoredAuth();
const [phase, setPhase] = useState(
stored?.loggedIn ? "verifying" : "idle-auth",
);
const [authUsername, setAuthUsername] = useState(stored?.username ?? "");
const timerRef = useRef | null>(null);
// Track whether fading-in came from a fresh login (vs. session verification on page load).
// When session-verified, Auth must not mount during the transition — it would trigger
// silent OIDC redirect and cause an infinite refresh loop.
const fadingInFromLoginRef = useRef(false);
useEffect(() => {
const savedAccent = localStorage.getItem("termix-accent");
if (savedAccent) applyAccentColor(savedAccent);
const savedSize = localStorage.getItem(
"termix-font-size",
) as FontSizeId | null;
applyFontSize(savedSize ?? "lg");
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, []);
// Verify stored session against the server before rendering AppShell.
// Wait for API instances to be initialized with correct embedded/server config first.
// In Electron, also repopulate localStorage["jwt"] so WebSocket connections can auth
// after a session restore (the token is only written to localStorage during a fresh login).
useEffect(() => {
if (phase !== "verifying") return;
appReadyPromise
.then(() => getUserInfo())
.then(async () => {
if (isElectron()) {
try {
const token = await getCurrentToken();
if (token) localStorage.setItem("jwt", token);
} catch {
// Non-fatal: WebSocket connections will fall back to cookie auth
}
}
fadingInFromLoginRef.current = false;
setPhase("fading-in");
timerRef.current = setTimeout(() => setPhase("idle-app"), 450);
})
.catch(() => {
clearStoredAuth();
setPhase("idle-auth");
});
}, [phase]);
function handleLogin(u: string) {
setAuthUsername(u);
fadingInFromLoginRef.current = true;
setPhase("fading-in");
timerRef.current = setTimeout(() => setPhase("idle-app"), 450);
if (isElectron()) {
window.electronAPI?.startC2SAutoStartTunnels?.().catch(() => {});
}
}
function handleLogout() {
clearStoredAuth();
setPhase("fading-out");
timerRef.current = setTimeout(() => {
setAuthUsername("");
setPhase("idle-auth");
}, 450);
}
function handleChangeServer() {
localStorage.setItem("termix_show_server_config", "true");
handleLogout();
}
const showApp =
phase === "idle-app" || phase === "fading-in" || phase === "fading-out";
const showAuth =
phase === "idle-auth" ||
(phase === "fading-in" && fadingInFromLoginRef.current) ||
phase === "fading-out";
const appOpacity = phase === "idle-app" ? 1 : 0;
const authOpacity = phase === "idle-auth" ? 1 : 0;
const { t } = useTranslation();
const isTransitioning = phase === "fading-in" || phase === "fading-out";
if (phase === "verifying") {
return (
);
}
return (
<>
{isTransitioning && (
)}
{showApp && (
)}
{showAuth && (
)}
>
);
}
function RootApp() {
const [showVersionCheck, setShowVersionCheck] = useState(true);
useServiceWorker();
const searchParams = new URLSearchParams(window.location.search);
const isFullscreen = searchParams.has("view");
if (isFullscreen) {
return (
);
}
if (isElectron() && showVersionCheck) {
return (
setShowVersionCheck(false)} />
);
}
return ;
}
installElectronWheelZoomGuard();
prepareClientCacheVersion().finally(() => {
createRoot(document.getElementById("root")!).render(
,
);
});