Fix env not loading after restart, update translsations, fix export DB nginx conf

This commit is contained in:
LukeGus
2025-09-26 09:13:41 -05:00
parent 62bc684fff
commit 9b12515676
12 changed files with 125 additions and 78 deletions

View File

@@ -90,6 +90,24 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location ~ ^/database(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/encryption(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/ { location /ssh/ {
proxy_pass http://127.0.0.1:30001; proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -76,6 +76,24 @@ http {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
location ~ ^/database(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/encryption(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /ssh/ { location /ssh/ {
proxy_pass http://127.0.0.1:30001; proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -17,8 +17,6 @@ import { DataCrypto } from "../utils/data-crypto.js";
import { DatabaseFileEncryption } from "../utils/database-file-encryption.js"; import { DatabaseFileEncryption } from "../utils/database-file-encryption.js";
import { DatabaseMigration } from "../utils/database-migration.js"; import { DatabaseMigration } from "../utils/database-migration.js";
import { UserDataExport } from "../utils/user-data-export.js"; import { UserDataExport } from "../utils/user-data-export.js";
import { UserDataImport } from "../utils/user-data-import.js";
import https from "https";
import { AutoSSLSetup } from "../utils/auto-ssl-setup.js"; import { AutoSSLSetup } from "../utils/auto-ssl-setup.js";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { users, sshData, sshCredentials, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts, sshCredentialUsage, settings } from "./db/schema.js"; import { users, sshData, sshCredentials, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts, sshCredentialUsage, settings } from "./db/schema.js";
@@ -447,7 +445,10 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
} }
// Create temporary SQLite database // Create temporary SQLite database
const tempDir = path.join(os.tmpdir(), 'termix-exports'); // Use app data directory for temp files in Docker environments
const tempDir = process.env.NODE_ENV === 'production'
? path.join(process.env.DATA_DIR || './db/data', '.temp', 'exports')
: path.join(os.tmpdir(), 'termix-exports');
if (!fs.existsSync(tempDir)) { if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true }); fs.mkdirSync(tempDir, { recursive: true });
} }

View File

@@ -931,7 +931,17 @@
"operationCompletedSuccessfully": "{{operation}} {{count}} items successfully", "operationCompletedSuccessfully": "{{operation}} {{count}} items successfully",
"operationCompleted": "{{operation}} {{count}} items", "operationCompleted": "{{operation}} {{count}} items",
"downloadFileSuccess": "File {{name}} downloaded successfully", "downloadFileSuccess": "File {{name}} downloaded successfully",
"downloadFileFailed": "Download failed" "downloadFileFailed": "Download failed",
"moveTo": "Move to {{name}}",
"diffCompareWith": "Diff compare with {{name}}",
"dragOutsideToDownload": "Drag outside window to download ({{count}} files)",
"newFolderDefault": "NewFolder",
"newFileDefault": "NewFile.txt",
"successfullyMovedItems": "Successfully moved {{count}} items to {{target}}",
"move": "Move",
"searchInFile": "Search in file (Ctrl+F)",
"showKeyboardShortcuts": "Show keyboard shortcuts",
"startWritingMarkdown": "Start writing your markdown content..."
}, },
"tunnels": { "tunnels": {
"title": "SSH Tunnels", "title": "SSH Tunnels",

View File

@@ -945,7 +945,17 @@
"operationCompletedSuccessfully": "已{{operation}} {{count}} 个项目", "operationCompletedSuccessfully": "已{{operation}} {{count}} 个项目",
"operationCompleted": "已{{operation}} {{count}} 个项目", "operationCompleted": "已{{operation}} {{count}} 个项目",
"downloadFileSuccess": "文件 {{name}} 下载成功", "downloadFileSuccess": "文件 {{name}} 下载成功",
"downloadFileFailed": "下载失败" "downloadFileFailed": "下载失败",
"moveTo": "移动到 {{name}}",
"diffCompareWith": "与 {{name}} 对比",
"dragOutsideToDownload": "拖拽到窗口外下载 ({{count}} 个文件)",
"newFolderDefault": "新文件夹",
"newFileDefault": "新文件.txt",
"successfullyMovedItems": "成功移动 {{count}} 个项目到 {{target}}",
"move": "移动",
"searchInFile": "在文件中搜索 (Ctrl+F)",
"showKeyboardShortcuts": "显示键盘快捷键",
"startWritingMarkdown": "开始编写您的 markdown 内容..."
}, },
"tunnels": { "tunnels": {
"title": "SSH 隧道", "title": "SSH 隧道",

View File

@@ -389,6 +389,11 @@ export function AdminSettings({
); );
setImportFile(null); setImportFile(null);
setImportPassword(""); setImportPassword("");
// Refresh the page to show imported data
setTimeout(() => {
window.location.reload();
}, 1500);
} else { } else {
toast.error( toast.error(
`${t("admin.databaseImportFailed")}: ${result.summary?.errors?.join(", ") || "Unknown error"}`, `${t("admin.databaseImportFailed")}: ${result.summary?.errors?.join(", ") || "Unknown error"}`,

View File

@@ -657,7 +657,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
// Linus-style creation: pure intent, no side effects // Linus-style creation: pure intent, no side effects
function handleCreateNewFolder() { function handleCreateNewFolder() {
const defaultName = generateUniqueName("NewFolder", "directory"); const defaultName = generateUniqueName(t("fileManager.newFolderDefault"), "directory");
const newCreateIntent = { const newCreateIntent = {
id: Date.now().toString(), id: Date.now().toString(),
type: 'directory' as const, type: 'directory' as const,
@@ -670,7 +670,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
} }
function handleCreateNewFile() { function handleCreateNewFile() {
const defaultName = generateUniqueName("NewFile.txt", "file"); const defaultName = generateUniqueName(t("fileManager.newFileDefault"), "file");
const newCreateIntent = { const newCreateIntent = {
id: Date.now().toString(), id: Date.now().toString(),
type: 'file' as const, type: 'file' as const,

View File

@@ -1026,13 +1026,13 @@ export function FileManagerGrid({
/> />
<button <button
onClick={confirmEditingPath} onClick={confirmEditingPath}
className="px-2 py-1 bg-primary text-primary-foreground rounded text-xs hover:bg-primary/80" className="px-2 py-1 bg-primary text-primary-foreground rounded text-sm hover:bg-primary/80"
> >
{t("fileManager.confirm")} {t("fileManager.confirm")}
</button> </button>
<button <button
onClick={cancelEditingPath} onClick={cancelEditingPath}
className="px-2 py-1 bg-secondary text-secondary-foreground rounded text-xs hover:bg-secondary/80" className="px-2 py-1 bg-secondary text-secondary-foreground rounded text-sm hover:bg-secondary/80"
> >
{t("fileManager.cancel")} {t("fileManager.cancel")}
</button> </button>
@@ -1061,7 +1061,7 @@ export function FileManagerGrid({
))} ))}
<button <button
onClick={startEditingPath} onClick={startEditingPath}
className="ml-2 p-1 rounded hover:bg-dark-hover opacity-60 hover:opacity-100" className="ml-2 p-1 rounded hover:bg-dark-hover opacity-60 hover:opacity-100 flex items-center justify-center"
title={t("fileManager.editPath")} title={t("fileManager.editPath")}
> >
<Edit className="w-3 h-3" /> <Edit className="w-3 h-3" />
@@ -1147,7 +1147,7 @@ export function FileManagerGrid({
data-file-path={file.path} data-file-path={file.path}
draggable={true} draggable={true}
className={cn( className={cn(
"group p-3 rounded-lg cursor-pointer transition-all", "group p-3 rounded-lg cursor-pointer",
"hover:bg-accent hover:text-accent-foreground border-2 border-transparent", "hover:bg-accent hover:text-accent-foreground border-2 border-transparent",
isSelected && "bg-primary/20 border-primary", isSelected && "bg-primary/20 border-primary",
dragState.target?.path === file.path && dragState.target?.path === file.path &&
@@ -1240,7 +1240,7 @@ export function FileManagerGrid({
data-file-path={file.path} data-file-path={file.path}
draggable={true} draggable={true}
className={cn( className={cn(
"flex items-center gap-3 p-2 rounded cursor-pointer transition-all", "flex items-center gap-3 p-2 rounded cursor-pointer",
"hover:bg-accent hover:text-accent-foreground", "hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-primary/20", isSelected && "bg-primary/20",
dragState.target?.path === file.path && dragState.target?.path === file.path &&
@@ -1378,14 +1378,14 @@ export function FileManagerGrid({
<> <>
<Move className="w-4 h-4 text-blue-500" /> <Move className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
Move to {dragState.target.name} {t("fileManager.moveTo", { name: dragState.target.name })}
</span> </span>
</> </>
) : ( ) : (
<> <>
<GitCompare className="w-4 h-4 text-purple-500" /> <GitCompare className="w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
Diff compare with {dragState.target.name} {t("fileManager.diffCompareWith", { name: dragState.target.name })}
</span> </span>
</> </>
) )
@@ -1393,7 +1393,7 @@ export function FileManagerGrid({
<> <>
<Download className="w-4 h-4 text-green-500" /> <Download className="w-4 h-4 text-green-500" />
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
Drag outside window to download ({files.length} files) {t("fileManager.dragOutsideToDownload", { count: files.length })}
</span> </span>
</> </>
); );
@@ -1436,7 +1436,7 @@ function CreateIntentGridItem({
}; };
return ( return (
<div className="group p-3 rounded-lg border-2 border-dashed border-primary bg-primary/10 transition-all"> <div className="group p-3 rounded-lg border-2 border-dashed border-primary bg-primary/10">
<div className="flex flex-col items-center text-center"> <div className="flex flex-col items-center text-center">
<div className="mb-2"> <div className="mb-2">
{intent.type === 'directory' ? ( {intent.type === 'directory' ? (
@@ -1490,7 +1490,7 @@ function CreateIntentListItem({
}; };
return ( return (
<div className="flex items-center gap-3 p-2 rounded border-2 border-dashed border-primary bg-primary/10 transition-all"> <div className="flex items-center gap-3 p-2 rounded border-2 border-dashed border-primary bg-primary/10">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{intent.type === 'directory' ? ( {intent.type === 'directory' ? (
<Folder className="w-6 h-6 text-primary" /> <Folder className="w-6 h-6 text-primary" />

View File

@@ -472,7 +472,7 @@ export function FileViewer({
} }
}} }}
className="flex items-center gap-2" className="flex items-center gap-2"
title="Search in file (Ctrl+F)" title={t("fileManager.searchInFile")}
> >
<Search className="w-4 h-4" /> <Search className="w-4 h-4" />
</Button> </Button>
@@ -484,7 +484,7 @@ export function FileViewer({
size="sm" size="sm"
onClick={() => setShowKeyboardShortcuts(!showKeyboardShortcuts)} onClick={() => setShowKeyboardShortcuts(!showKeyboardShortcuts)}
className="flex items-center gap-2" className="flex items-center gap-2"
title="Show keyboard shortcuts" title={t("fileManager.showKeyboardShortcuts")}
> >
<Keyboard className="w-4 h-4" /> <Keyboard className="w-4 h-4" />
</Button> </Button>
@@ -928,7 +928,7 @@ export function FileViewer({
onContentChange?.(e.target.value); onContentChange?.(e.target.value);
}} }}
className="w-full h-full resize-none border-0 bg-transparent text-foreground font-mono text-sm leading-relaxed focus:outline-none focus:ring-0" className="w-full h-full resize-none border-0 bg-transparent text-foreground font-mono text-sm leading-relaxed focus:outline-none focus:ring-0"
placeholder="Start writing your markdown content..." placeholder={t("fileManager.startWritingMarkdown")}
/> />
</div> </div>
</div> </div>

View File

@@ -845,7 +845,6 @@ export function HomepageAuth({
{resetStep === "verify" && ( {resetStep === "verify" && (
<> <>
o
<div className="text-center text-muted-foreground mb-4"> <div className="text-center text-muted-foreground mb-4">
<p> <p>
{t("auth.enterResetCode")}{" "} {t("auth.enterResetCode")}{" "}

View File

@@ -6,6 +6,7 @@ import { Label } from "@/components/ui/label.tsx";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { LanguageSwitcher } from "@/ui/Desktop/User/LanguageSwitcher.tsx"; import { LanguageSwitcher } from "@/ui/Desktop/User/LanguageSwitcher.tsx";
import { toast } from "sonner";
import { import {
registerUser, registerUser,
loginUser, loginUser,
@@ -92,6 +93,12 @@ export function HomepageAuth({
}); });
}, []); }, []);
useEffect(() => {
if (!registrationAllowed && !internalLoggedIn) {
toast.warning(t("messages.registrationDisabled"));
}
}, [registrationAllowed, internalLoggedIn, t]);
useEffect(() => { useEffect(() => {
getOIDCConfig() getOIDCConfig()
.then((response) => { .then((response) => {
@@ -116,6 +123,7 @@ export function HomepageAuth({
if (res.setup_required) { if (res.setup_required) {
setFirstUser(true); setFirstUser(true);
setTab("signup"); setTab("signup");
toast.info(t("auth.firstUserMessage"));
} else { } else {
setFirstUser(false); setFirstUser(false);
} }
@@ -124,7 +132,7 @@ export function HomepageAuth({
.catch(() => { .catch(() => {
setDbError(t("errors.databaseConnection")); setDbError(t("errors.databaseConnection"));
}); });
}, [setDbError]); }, [setDbError, t]);
async function handleSubmit(e: React.FormEvent) { async function handleSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
@@ -132,7 +140,7 @@ export function HomepageAuth({
setLoading(true); setLoading(true);
if (!localUsername.trim()) { if (!localUsername.trim()) {
setError(t("errors.requiredField")); toast.error(t("errors.requiredField"));
setLoading(false); setLoading(false);
return; return;
} }
@@ -143,12 +151,12 @@ export function HomepageAuth({
res = await loginUser(localUsername, password); res = await loginUser(localUsername, password);
} else { } else {
if (password !== signupConfirmPassword) { if (password !== signupConfirmPassword) {
setError(t("errors.passwordMismatch")); toast.error(t("errors.passwordMismatch"));
setLoading(false); setLoading(false);
return; return;
} }
if (password.length < 6) { if (password.length < 6) {
setError(t("errors.minLength", { min: 6 })); toast.error(t("errors.minLength", { min: 6 }));
setLoading(false); setLoading(false);
return; return;
} }
@@ -185,12 +193,15 @@ export function HomepageAuth({
setInternalLoggedIn(true); setInternalLoggedIn(true);
if (tab === "signup") { if (tab === "signup") {
setSignupConfirmPassword(""); setSignupConfirmPassword("");
toast.success(t("messages.registrationSuccess"));
} else {
toast.success(t("messages.loginSuccess"));
} }
setTotpRequired(false); setTotpRequired(false);
setTotpCode(""); setTotpCode("");
setTotpTempToken(""); setTotpTempToken("");
} catch (err: any) { } catch (err: any) {
setError( toast.error(
err?.response?.data?.error || err?.message || t("errors.unknownError"), err?.response?.data?.error || err?.message || t("errors.unknownError"),
); );
setInternalLoggedIn(false); setInternalLoggedIn(false);
@@ -215,9 +226,9 @@ export function HomepageAuth({
try { try {
const result = await initiatePasswordReset(localUsername); const result = await initiatePasswordReset(localUsername);
setResetStep("verify"); setResetStep("verify");
setError(null); toast.success(t("messages.resetCodeSent"));
} catch (err: any) { } catch (err: any) {
setError( toast.error(
err?.response?.data?.error || err?.response?.data?.error ||
err?.message || err?.message ||
t("errors.failedPasswordReset"), t("errors.failedPasswordReset"),
@@ -234,9 +245,9 @@ export function HomepageAuth({
const response = await verifyPasswordResetCode(localUsername, resetCode); const response = await verifyPasswordResetCode(localUsername, resetCode);
setTempToken(response.tempToken); setTempToken(response.tempToken);
setResetStep("newPassword"); setResetStep("newPassword");
setError(null); toast.success(t("messages.codeVerified"));
} catch (err: any) { } catch (err: any) {
setError(err?.response?.data?.error || t("errors.failedVerifyCode")); toast.error(err?.response?.data?.error || t("errors.failedVerifyCode"));
} finally { } finally {
setResetLoading(false); setResetLoading(false);
} }
@@ -247,13 +258,13 @@ export function HomepageAuth({
setResetLoading(true); setResetLoading(true);
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
setError(t("errors.passwordMismatch")); toast.error(t("errors.passwordMismatch"));
setResetLoading(false); setResetLoading(false);
return; return;
} }
if (newPassword.length < 6) { if (newPassword.length < 6) {
setError(t("errors.minLength", { min: 6 })); toast.error(t("errors.minLength", { min: 6 }));
setResetLoading(false); setResetLoading(false);
return; return;
} }
@@ -269,8 +280,9 @@ export function HomepageAuth({
setError(null); setError(null);
setResetSuccess(true); setResetSuccess(true);
toast.success(t("messages.passwordResetSuccess"));
} catch (err: any) { } catch (err: any) {
setError(err?.response?.data?.error || t("errors.failedCompleteReset")); toast.error(err?.response?.data?.error || t("errors.failedCompleteReset"));
} finally { } finally {
setResetLoading(false); setResetLoading(false);
} }
@@ -295,7 +307,7 @@ export function HomepageAuth({
async function handleTOTPVerification() { async function handleTOTPVerification() {
if (totpCode.length !== 6) { if (totpCode.length !== 6) {
setError(t("auth.enterCode")); toast.error(t("auth.enterCode"));
return; return;
} }
@@ -327,8 +339,9 @@ export function HomepageAuth({
setTotpRequired(false); setTotpRequired(false);
setTotpCode(""); setTotpCode("");
setTotpTempToken(""); setTotpTempToken("");
toast.success(t("messages.loginSuccess"));
} catch (err: any) { } catch (err: any) {
setError( toast.error(
err?.response?.data?.error || err?.response?.data?.error ||
err?.message || err?.message ||
t("errors.invalidTotpCode"), t("errors.invalidTotpCode"),
@@ -351,7 +364,7 @@ export function HomepageAuth({
window.location.replace(authUrl); window.location.replace(authUrl);
} catch (err: any) { } catch (err: any) {
setError( toast.error(
err?.response?.data?.error || err?.response?.data?.error ||
err?.message || err?.message ||
t("errors.failedOidcLogin"), t("errors.failedOidcLogin"),
@@ -367,7 +380,7 @@ export function HomepageAuth({
const error = urlParams.get("error"); const error = urlParams.get("error");
if (error) { if (error) {
setError(`${t("errors.oidcAuthFailed")}: ${error}`); toast.error(`${t("errors.oidcAuthFailed")}: ${error}`);
setOidcLoading(false); setOidcLoading(false);
window.history.replaceState({}, document.title, window.location.pathname); window.history.replaceState({}, document.title, window.location.pathname);
return; return;
@@ -399,7 +412,7 @@ export function HomepageAuth({
); );
}) })
.catch((err) => { .catch((err) => {
setError(t("errors.failedUserInfo")); toast.error(t("errors.failedUserInfo"));
setInternalLoggedIn(false); setInternalLoggedIn(false);
setLoggedIn(false); setLoggedIn(false);
setIsAdmin(false); setIsAdmin(false);
@@ -451,31 +464,6 @@ export function HomepageAuth({
<AlertDescription>{dbError}</AlertDescription> <AlertDescription>{dbError}</AlertDescription>
</Alert> </Alert>
)} )}
{firstUser && !dbError && !internalLoggedIn && (
<Alert variant="default" className="mb-4">
<AlertTitle>{t("auth.firstUser")}</AlertTitle>
<AlertDescription className="inline">
{t("auth.firstUserMessage")}{" "}
<a
href="https://github.com/LukeGus/Termix/issues/new"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline hover:text-blue-800 inline"
>
GitHub Issue
</a>
.
</AlertDescription>
</Alert>
)}
{!registrationAllowed && !internalLoggedIn && (
<Alert variant="destructive" className="mb-4">
<AlertTitle>{t("auth.registerTitle")}</AlertTitle>
<AlertDescription>
{t("messages.registrationDisabled")}
</AlertDescription>
</Alert>
)}
{totpRequired && ( {totpRequired && (
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<div className="mb-6 text-center"> <div className="mb-6 text-center">
@@ -709,14 +697,11 @@ export function HomepageAuth({
{resetSuccess && ( {resetSuccess && (
<> <>
<Alert className="mb-4"> <div className="text-center p-4 bg-green-500/10 rounded-lg border border-green-500/20 mb-4">
<AlertTitle> <p className="text-green-400 text-sm">
{t("auth.passwordResetSuccess")}
</AlertTitle>
<AlertDescription>
{t("auth.passwordResetSuccessDesc")} {t("auth.passwordResetSuccessDesc")}
</AlertDescription> </p>
</Alert> </div>
<Button <Button
type="button" type="button"
className="w-full h-11 text-base font-semibold" className="w-full h-11 text-base font-semibold"
@@ -881,12 +866,6 @@ export function HomepageAuth({
</div> </div>
</> </>
)} )}
{error && (
<Alert variant="destructive" className="mt-4">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
</div> </div>
); );
} }

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, type FC } from "react";
import { Terminal } from "@/ui/Mobile/Apps/Terminal/Terminal.tsx"; import { Terminal } from "@/ui/Mobile/Apps/Terminal/Terminal.tsx";
import { TerminalKeyboard } from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx"; import { TerminalKeyboard } from "@/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx";
import { BottomNavbar } from "@/ui/Mobile/Navigation/BottomNavbar.tsx"; import { BottomNavbar } from "@/ui/Mobile/Navigation/BottomNavbar.tsx";
@@ -10,6 +10,7 @@ import {
import { getUserInfo, getCookie } from "@/ui/main-axios.ts"; import { getUserInfo, getCookie } from "@/ui/main-axios.ts";
import { HomepageAuth } from "@/ui/Mobile/Homepage/HomepageAuth.tsx"; import { HomepageAuth } from "@/ui/Mobile/Homepage/HomepageAuth.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Toaster } from "@/components/ui/sonner.tsx";
const AppContent: FC = () => { const AppContent: FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -159,7 +160,6 @@ const AppContent: FC = () => {
ref={tab.terminalRef} ref={tab.terminalRef}
hostConfig={tab.hostConfig} hostConfig={tab.hostConfig}
isVisible={tab.id === currentTab} isVisible={tab.id === currentTab}
onClose={() => removeTab(tab.id)}
/> />
</div> </div>
))} ))}
@@ -207,6 +207,13 @@ const AppContent: FC = () => {
/> />
</div> </div>
</div> </div>
<Toaster
position="bottom-center"
richColors={false}
closeButton
duration={5000}
offset={20}
/>
</div> </div>
); );
}; };