Update electron builds, fix backend issues

This commit is contained in:
LukeGus
2025-09-28 16:45:24 -05:00
parent cfa7c26c49
commit d1b4345206
16 changed files with 410 additions and 1811 deletions

View File

@@ -9,7 +9,6 @@ COPY package*.json ./
ENV npm_config_target_platform=linux
ENV npm_config_target_arch=x64
ENV npm_config_target_libc=glibc
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm ci --force --ignore-scripts && \
npm cache clean --force
@@ -20,11 +19,7 @@ WORKDIR /app
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV NODE_ENV=production
RUN npm run build
RUN npm run build:types
# Stage 3: Build backend TypeScript
FROM deps AS backend-builder
@@ -35,8 +30,6 @@ COPY . .
ENV npm_config_target_platform=linux
ENV npm_config_target_arch=x64
ENV npm_config_target_libc=glibc
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV NODE_ENV=production
RUN npm rebuild better-sqlite3 --force
@@ -46,14 +39,13 @@ RUN npm run build:backend
FROM node:24-alpine AS production-deps
WORKDIR /app
RUN apk add --no-cache python3 make g++
RUN apk add --no-cache python3 make g++
COPY package*.json ./
ENV npm_config_target_platform=linux
ENV npm_config_target_arch=x64
ENV npm_config_target_libc=glibc
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm ci --only=production --ignore-scripts --force && \
npm rebuild better-sqlite3 bcryptjs --force && \
@@ -90,4 +82,4 @@ EXPOSE ${PORT} 30001 30002 30003 30004 30005
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]
CMD ["/entrypoint.sh"]

View File

@@ -32,11 +32,9 @@ if [ "$ENABLE_SSL" = "true" ]; then
DOMAIN=${SSL_DOMAIN:-localhost}
# Check if certificates already exist and are valid
if [ -f "/app/data/ssl/termix.crt" ] && [ -f "/app/data/ssl/termix.key" ]; then
echo "SSL certificates found, checking validity..."
# Check if certificate is still valid (not expired within 30 days)
if openssl x509 -in /app/data/ssl/termix.crt -checkend 2592000 -noout >/dev/null 2>&1; then
echo "SSL certificates are valid and will be reused for domain: $DOMAIN"
else
@@ -47,7 +45,6 @@ if [ "$ENABLE_SSL" = "true" ]; then
echo "SSL certificates not found, will generate new ones..."
fi
# Generate certificates only if they don't exist or are invalid
if [ ! -f "/app/data/ssl/termix.crt" ] || [ ! -f "/app/data/ssl/termix.key" ]; then
echo "Generating SSL certificates for domain: $DOMAIN"
@@ -101,6 +98,18 @@ echo "Starting backend services..."
cd /app
export NODE_ENV=production
if [ -f "package.json" ]; then
VERSION=$(grep '"version"' package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/')
if [ -n "$VERSION" ]; then
export VERSION
echo "Detected version: $VERSION"
else
echo "Warning: Could not extract version from package.json"
fi
else
echo "Warning: package.json not found"
fi
if command -v su-exec > /dev/null 2>&1; then
su-exec node node dist/backend/backend/starter.js
else

View File

@@ -126,26 +126,31 @@ async function fetchGitHubAPI(endpoint, cacheKey) {
const isHttps = urlObj.protocol === "https:";
const client = isHttps ? https : http;
const req = client.request(
url,
{
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 10000,
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
const requestOptions = {
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 10000,
};
if (isHttps) {
requestOptions.rejectUnauthorized = false;
requestOptions.agent = new https.Agent({
rejectUnauthorized: false,
});
}
const req = client.request(url, requestOptions, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
},
);
});
});
req.on("error", reject);
req.on("timeout", () => {
@@ -295,26 +300,31 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
const isHttps = urlObj.protocol === "https:";
const client = isHttps ? https : http;
const req = client.request(
url,
{
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 5000,
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
const requestOptions = {
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 5000,
};
if (isHttps) {
requestOptions.rejectUnauthorized = false;
requestOptions.agent = new https.Agent({
rejectUnauthorized: false,
});
}
const req = client.request(url, requestOptions, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
text: () => Promise.resolve(data),
json: () => Promise.resolve(JSON.parse(data)),
});
},
);
});
});
req.on("error", reject);
req.on("timeout", () => {

1515
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,9 +10,7 @@
"clean": "npx prettier . --write",
"dev": "vite",
"dev:https": "cross-env VITE_HTTPS=true vite",
"build": "vite build",
"build:types": "tsc -p tsconfig.node.json",
"build:full": "npm run build && npm run build:types",
"build": "vite build && tsc -p tsconfig.node.json",
"build:backend": "tsc -p tsconfig.node.json",
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
"start": "npm run build:backend && node ./dist/backend/backend/starter.js",
@@ -132,4 +130,4 @@
"typescript-eslint": "^8.40.0",
"vite": "^7.1.5"
}
}
}

View File

@@ -218,14 +218,46 @@ app.get("/version", authenticateJWT, async (req, res) => {
let localVersion = process.env.VERSION;
if (!localVersion) {
try {
const packagePath = path.resolve(process.cwd(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
localVersion = packageJson.version;
} catch (error) {
databaseLogger.error("Failed to read version from package.json", error, {
operation: "version_check",
});
const versionSources = [
() => {
try {
const packagePath = path.resolve(process.cwd(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
return packageJson.version;
} catch {
return null;
}
},
() => {
try {
const packagePath = path.resolve("/app", "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
return packageJson.version;
} catch {
return null;
}
},
() => {
try {
const packagePath = path.resolve(__dirname, "../../../package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
return packageJson.version;
} catch {
return null;
}
}
];
for (const getVersion of versionSources) {
try {
const foundVersion = getVersion();
if (foundVersion && foundVersion !== "unknown") {
localVersion = foundVersion;
break;
}
} catch (error) {
continue;
}
}
}

View File

@@ -126,15 +126,18 @@ async function initializeCompleteDatabase(): Promise<void> {
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
is_oidc INTEGER NOT NULL DEFAULT 0,
client_id TEXT NOT NULL,
client_secret TEXT NOT NULL,
issuer_url TEXT NOT NULL,
authorization_url TEXT NOT NULL,
token_url TEXT NOT NULL,
redirect_uri TEXT,
identifier_path TEXT NOT NULL,
name_path TEXT NOT NULL,
scopes TEXT NOT NULL
oidc_identifier TEXT,
client_id TEXT,
client_secret TEXT,
issuer_url TEXT,
authorization_url TEXT,
token_url TEXT,
identifier_path TEXT,
name_path TEXT,
scopes TEXT DEFAULT 'openid email profile',
totp_secret TEXT,
totp_enabled INTEGER NOT NULL DEFAULT 0,
totp_backup_codes TEXT
);
CREATE TABLE IF NOT EXISTS settings (

View File

@@ -1,5 +1,6 @@
import dotenv from "dotenv";
import { promises as fs } from "fs";
import { readFileSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { AutoSSLSetup } from "./utils/auto-ssl-setup.js";
@@ -23,18 +24,52 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
} catch {}
let version = "unknown";
try {
const __filename = fileURLToPath(import.meta.url);
const packageJsonPath = path.join(
path.dirname(__filename),
"../../../package.json",
);
const packageJson = JSON.parse(
await fs.readFile(packageJsonPath, "utf-8"),
);
version = packageJson.version || "unknown";
} catch (error) {
version = process.env.VERSION || "unknown";
const versionSources = [
() => process.env.VERSION,
() => {
try {
const packageJsonPath = path.join(process.cwd(), "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return packageJson.version;
} catch {
return null;
}
},
() => {
try {
const __filename = fileURLToPath(import.meta.url);
const packageJsonPath = path.join(
path.dirname(__filename),
"../../../package.json",
);
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return packageJson.version;
} catch {
return null;
}
},
() => {
try {
const packageJsonPath = path.join("/app", "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return packageJson.version;
} catch {
return null;
}
}
];
for (const getVersion of versionSources) {
try {
const foundVersion = getVersion();
if (foundVersion && foundVersion !== "unknown") {
version = foundVersion;
break;
}
} catch (error) {
continue;
}
}
versionLogger.info(`Termix Backend starting - Version: ${version}`, {
operation: "startup",
@@ -50,79 +85,6 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
const dbModule = await import("./database/db/index.js");
await dbModule.initializeDatabase();
if (process.env.NODE_ENV === "production") {
const securityIssues: string[] = [];
if (!process.env.JWT_SECRET) {
systemLogger.warn(
"JWT_SECRET not set - using auto-generated keys (consider setting for production)",
{
operation: "security_warning",
note: "Auto-generated keys are secure but not persistent across deployments",
},
);
} else if (process.env.JWT_SECRET.length < 64) {
securityIssues.push(
"JWT_SECRET should be at least 64 characters in production",
);
}
if (!process.env.DATABASE_KEY) {
systemLogger.warn(
"DATABASE_KEY not set - using auto-generated keys (consider setting for production)",
{
operation: "security_warning",
note: "Auto-generated keys are secure but not persistent across deployments",
},
);
} else if (process.env.DATABASE_KEY.length < 64) {
securityIssues.push(
"DATABASE_KEY should be at least 64 characters in production",
);
}
if (!process.env.INTERNAL_AUTH_TOKEN) {
systemLogger.warn(
"INTERNAL_AUTH_TOKEN not set - using auto-generated token (consider setting for production)",
{
operation: "security_warning",
note: "Auto-generated tokens are secure but not persistent across deployments",
},
);
} else if (process.env.INTERNAL_AUTH_TOKEN.length < 32) {
securityIssues.push(
"INTERNAL_AUTH_TOKEN should be at least 32 characters in production",
);
}
if (process.env.DB_FILE_ENCRYPTION === "false") {
securityIssues.push(
"Database file encryption should be enabled in production",
);
}
systemLogger.warn(
"Production deployment detected - ensure CORS is properly configured",
{
operation: "security_checks",
warning: "Verify frontend domain whitelist",
},
);
if (securityIssues.length > 0) {
systemLogger.error("SECURITY ISSUES DETECTED IN PRODUCTION:", {
operation: "security_checks_failed",
issues: securityIssues,
});
for (const issue of securityIssues) {
systemLogger.error(`- ${issue}`, { operation: "security_issue" });
}
systemLogger.error("Fix these issues before running in production!", {
operation: "security_checks_failed",
});
process.exit(1);
}
}
const authManager = AuthManager.getInstance();
await authManager.initialize();

View File

@@ -29,28 +29,17 @@ export class AutoSSLSetup {
try {
if (await this.isSSLConfigured()) {
await this.logCertificateInfo();
await this.setupEnvironmentVariables();
return;
}
// In Docker, certificates might be generated by entrypoint script
// Check if they exist but weren't detected by isSSLConfigured
try {
await fs.access(this.CERT_FILE);
await fs.access(this.KEY_FILE);
systemLogger.info("SSL certificates found from entrypoint script", {
operation: "ssl_cert_found_entrypoint",
cert_path: this.CERT_FILE,
key_path: this.KEY_FILE,
});
await this.logCertificateInfo();
await this.setupEnvironmentVariables();
return;
} catch {
// Certificates don't exist, generate them
await this.generateSSLCertificates();
await this.setupEnvironmentVariables();
}
@@ -169,8 +158,6 @@ IP.2 = ::1
key_path: this.KEY_FILE,
valid_days: 365,
});
await this.logCertificateInfo();
} catch (error) {
throw new Error(
`SSL certificate generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
@@ -178,49 +165,6 @@ IP.2 = ::1
}
}
private static async logCertificateInfo(): Promise<void> {
try {
const subject = execSync(
`openssl x509 -in "${this.CERT_FILE}" -noout -subject`,
{ stdio: "pipe" },
)
.toString()
.trim();
const issuer = execSync(
`openssl x509 -in "${this.CERT_FILE}" -noout -issuer`,
{ stdio: "pipe" },
)
.toString()
.trim();
const notAfter = execSync(
`openssl x509 -in "${this.CERT_FILE}" -noout -enddate`,
{ stdio: "pipe" },
)
.toString()
.trim();
const notBefore = execSync(
`openssl x509 -in "${this.CERT_FILE}" -noout -startdate`,
{ stdio: "pipe" },
)
.toString()
.trim();
systemLogger.info("SSL Certificate Information:", {
operation: "ssl_cert_info",
subject: subject.replace("subject=", ""),
issuer: issuer.replace("issuer=", ""),
valid_from: notBefore.replace("notBefore=", ""),
valid_until: notAfter.replace("notAfter=", ""),
note: "Certificate will auto-renew 30 days before expiration",
});
} catch (error) {
systemLogger.warn("Could not retrieve certificate information", {
operation: "ssl_cert_info_error",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
private static async setupEnvironmentVariables(): Promise<void> {
const certPath = this.CERT_FILE;
const keyPath = this.KEY_FILE;

View File

@@ -1,23 +1,47 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
import { Toaster as Sonner, type ToasterProps, toast } from "sonner";
import { useRef } from "react";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
const { theme = "system" } = useTheme();
const lastToastRef = useRef<{ text: string; timestamp: number } | null>(null);
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
const originalToast = toast;
const rateLimitedToast = (message: string, options?: any) => {
const now = Date.now();
const lastToast = lastToastRef.current;
if (lastToast && lastToast.text === message && (now - lastToast.timestamp) < 1000) {
return;
}
lastToastRef.current = { text: message, timestamp: now };
return originalToast(message, options);
};
Object.assign(toast, {
success: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'success' }),
error: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'error' }),
warning: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'warning' }),
info: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'info' }),
message: rateLimitedToast,
});
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };
export { Toaster };

View File

@@ -0,0 +1,180 @@
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button.tsx";
import { VersionAlert } from "@/components/ui/version-alert.tsx";
import { RefreshCw, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { checkElectronUpdate, isElectron } from "@/ui/main-axios.ts";
interface VersionCheckModalProps {
onDismiss: () => void;
onContinue: () => void;
}
export function VersionCheckModal({ onDismiss, onContinue }: VersionCheckModalProps) {
const { t } = useTranslation();
const [versionInfo, setVersionInfo] = useState<any>(null);
const [versionChecking, setVersionChecking] = useState(false);
const [versionDismissed, setVersionDismissed] = useState(false);
useEffect(() => {
if (isElectron()) {
checkForUpdates();
} else {
onContinue();
}
}, []);
const checkForUpdates = async () => {
setVersionChecking(true);
try {
const updateInfo = await checkElectronUpdate();
setVersionInfo(updateInfo);
} catch (error) {
console.error("Failed to check for updates:", error);
setVersionInfo({ success: false, error: "Check failed" });
} finally {
setVersionChecking(false);
}
};
const handleVersionDismiss = () => {
setVersionDismissed(true);
};
const handleDownloadUpdate = () => {
if (versionInfo?.latest_release?.html_url) {
window.open(versionInfo.latest_release.html_url, "_blank");
}
};
const handleContinue = () => {
onContinue();
};
if (!isElectron()) {
return null;
}
if (versionChecking && !versionInfo) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-dark-bg border border-dark-border rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-center mb-4">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
<p className="text-center text-muted-foreground">
{t("versionCheck.checkingUpdates")}
</p>
</div>
</div>
);
}
if (!versionInfo || versionInfo.status === "up_to_date" || versionDismissed) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-dark-bg border border-dark-border rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">
{t("versionCheck.checkUpdates")}
</h2>
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
{versionInfo && !versionDismissed && (
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDismiss={handleVersionDismiss}
onDownload={handleDownloadUpdate}
showDismiss={true}
/>
</div>
)}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={versionChecking}
className="flex-1"
>
{versionChecking ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
{t("versionCheck.refresh")}
</Button>
<Button
onClick={handleContinue}
className="flex-1"
>
{t("common.continue")}
</Button>
</div>
</div>
</div>
);
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-dark-bg border border-dark-border rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">
{t("versionCheck.updateRequired")}
</h2>
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDismiss={handleVersionDismiss}
onDownload={handleDownloadUpdate}
showDismiss={true}
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={versionChecking}
className="flex-1"
>
{versionChecking ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
{t("versionCheck.refresh")}
</Button>
<Button
onClick={handleContinue}
className="flex-1"
>
{t("common.continue")}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -226,7 +226,10 @@
"downloadUpdate": "Download Update",
"dismiss": "Dismiss",
"checking": "Checking for updates...",
"checkUpdates": "Check for Updates"
"checkUpdates": "Check for Updates",
"checkingUpdates": "Checking for updates...",
"refresh": "Refresh",
"updateRequired": "Update Required"
},
"common": {
"close": "Close",
@@ -1207,7 +1210,7 @@
"databaseConnected": "Database connected successfully",
"databaseConnectionFailed": "Failed to connect to the database server",
"checkServerConnection": "Please check your server connection and try again",
"resetCodeSent": "Reset code sent to your email",
"resetCodeSent": "Reset code sent to Docker logs",
"codeVerified": "Code verified successfully",
"passwordResetSuccess": "Password reset successfully",
"loginSuccess": "Login successful",

View File

@@ -224,7 +224,10 @@
"downloadUpdate": "下载更新",
"dismiss": "忽略",
"checking": "正在检查更新...",
"checkUpdates": "检查更新"
"checkUpdates": "检查更新",
"checkingUpdates": "正在检查更新...",
"refresh": "刷新",
"updateRequired": "需要更新"
},
"common": {
"close": "关闭",
@@ -1184,7 +1187,7 @@
"databaseConnected": "数据库连接成功",
"databaseConnectionFailed": "无法连接到数据库服务器",
"checkServerConnection": "请检查您的服务器连接并重试",
"resetCodeSent": "重置代码已发送到您的邮箱",
"resetCodeSent": "重置代码已发送到 Docker 日志",
"codeVerified": "代码验证成功",
"passwordResetSuccess": "密码重置成功",
"loginSuccess": "登录成功",

View File

@@ -11,6 +11,7 @@ import { TopNavbar } from "@/ui/Desktop/Navigation/TopNavbar.tsx";
import { AdminSettings } from "@/ui/Desktop/Admin/AdminSettings.tsx";
import { UserProfile } from "@/ui/Desktop/User/UserProfile.tsx";
import { Toaster } from "@/components/ui/sonner.tsx";
import { VersionCheckModal } from "@/components/ui/version-check-modal.tsx";
import { getUserInfo, getCookie } from "@/ui/main-axios.ts";
function AppContent() {
@@ -22,6 +23,7 @@ function AppContent() {
const [username, setUsername] = useState<string | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
const [authLoading, setAuthLoading] = useState(true);
const [showVersionCheck, setShowVersionCheck] = useState(true);
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
const { currentTab, tabs } = useTabs();
@@ -94,6 +96,13 @@ function AppContent() {
return (
<div>
{showVersionCheck && (
<VersionCheckModal
onDismiss={() => setShowVersionCheck(false)}
onContinue={() => setShowVersionCheck(false)}
/>
)}
{!isAuthenticated && !authLoading && (
<div>
<div

View File

@@ -3,16 +3,14 @@ import { Button } from "@/components/ui/button.tsx";
import { Input } from "@/components/ui/input.tsx";
import { Label } from "@/components/ui/label.tsx";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
import { VersionAlert } from "@/components/ui/version-alert.tsx";
import { useTranslation } from "react-i18next";
import {
getServerConfig,
saveServerConfig,
testServerConnection,
checkElectronUpdate,
type ServerConfig,
} from "@/ui/main-axios.ts";
import { CheckCircle, XCircle, Server, Wifi, RefreshCw } from "lucide-react";
import { CheckCircle, XCircle, Server, Wifi } from "lucide-react";
interface ServerConfigProps {
onServerConfigured: (serverUrl: string) => void;
@@ -33,13 +31,9 @@ export function ServerConfig({
const [connectionStatus, setConnectionStatus] = useState<
"unknown" | "success" | "error"
>("unknown");
const [versionInfo, setVersionInfo] = useState<any>(null);
const [versionChecking, setVersionChecking] = useState(false);
const [versionDismissed, setVersionDismissed] = useState(false);
useEffect(() => {
loadServerConfig();
checkForUpdates();
}, []);
const loadServerConfig = async () => {
@@ -52,29 +46,6 @@ export function ServerConfig({
} catch (error) {}
};
const checkForUpdates = async () => {
setVersionChecking(true);
try {
const updateInfo = await checkElectronUpdate();
setVersionInfo(updateInfo);
} catch (error) {
console.error("Failed to check for updates:", error);
setVersionInfo({ success: false, error: "Check failed" });
} finally {
setVersionChecking(false);
}
};
const handleVersionDismiss = () => {
setVersionDismissed(true);
};
const handleDownloadUpdate = () => {
if (versionInfo?.latest_release?.html_url) {
window.open(versionInfo.latest_release.html_url, "_blank");
}
};
const handleTestConnection = async () => {
if (!serverUrl.trim()) {
setError(t("serverConfig.enterServerUrl"));
@@ -224,36 +195,6 @@ export function ServerConfig({
</Alert>
)}
{/* Version Check Section */}
{versionInfo && !versionDismissed && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">
{t("versionCheck.checkUpdates")}
</h3>
<Button
variant="ghost"
size="sm"
onClick={checkForUpdates}
disabled={versionChecking}
className="h-6 px-2"
>
{versionChecking ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
</Button>
</div>
<VersionAlert
updateInfo={versionInfo}
onDismiss={handleVersionDismiss}
onDownload={handleDownloadUpdate}
showDismiss={true}
/>
</div>
)}
<div className="flex space-x-2">
{onCancel && !isFirstTime && (
<Button

View File

@@ -526,8 +526,12 @@ function handleApiError(error: unknown, operation: string): never {
`Auth failed: ${method} ${url} - ${message}`,
errorContext,
);
const isLoginEndpoint = url?.includes('/users/login');
const errorMessage = isLoginEndpoint ? message : "Authentication required. Please log in again.";
throw new ApiError(
"Authentication required. Please log in again.",
errorMessage,
401,
"AUTH_REQUIRED",
);