Refactor nullish checks and add type safety

Replaces many uses of logical OR (||) with nullish coalescing (??) for more accurate handling of undefined/null values. Adds explicit type annotations and interfaces to improve type safety, especially in API routes and server-side code. Updates SSH connection test handling and config parsing in installedScripts router for better reliability. Minor fixes to deduplication logic, cookie handling, and error reporting.
This commit is contained in:
CanbiZ
2025-11-28 12:10:15 +01:00
parent d40aeb6c82
commit 48cf86a449
12 changed files with 227 additions and 132 deletions

View File

@@ -1,6 +1,13 @@
'use client';
"use client";
import { createContext, useContext, useEffect, useState, useCallback, type ReactNode } from 'react';
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
interface AuthContextType {
isAuthenticated: boolean;
@@ -27,10 +34,13 @@ export function AuthProvider({ children }: AuthProviderProps) {
const checkAuthInternal = async (retryCount = 0) => {
try {
// First check if setup is completed
const setupResponse = await fetch('/api/settings/auth-credentials');
const setupResponse = await fetch("/api/settings/auth-credentials");
if (setupResponse.ok) {
const setupData = await setupResponse.json() as { setupCompleted: boolean; enabled: boolean };
const setupData = (await setupResponse.json()) as {
setupCompleted: boolean;
enabled: boolean;
};
// If setup is not completed or auth is disabled, don't verify
if (!setupData.setupCompleted || !setupData.enabled) {
setIsAuthenticated(false);
@@ -42,12 +52,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
}
// Only verify authentication if setup is completed and auth is enabled
const response = await fetch('/api/auth/verify', {
credentials: 'include', // Ensure cookies are sent
const response = await fetch("/api/auth/verify", {
credentials: "include", // Ensure cookies are sent
});
if (response.ok) {
const data = await response.json() as {
username: string;
const data = (await response.json()) as {
username: string;
expirationTime?: number | null;
timeUntilExpiration?: number | null;
};
@@ -58,7 +68,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
setIsAuthenticated(false);
setUsername(null);
setExpirationTime(null);
// Retry logic for failed auth checks (max 2 retries)
if (retryCount < 2) {
setTimeout(() => {
@@ -68,11 +78,11 @@ export function AuthProvider({ children }: AuthProviderProps) {
}
}
} catch (error) {
console.error('Error checking auth:', error);
console.error("Error checking auth:", error);
setIsAuthenticated(false);
setUsername(null);
setExpirationTime(null);
// Retry logic for network errors (max 2 retries)
if (retryCount < 2) {
setTimeout(() => {
@@ -87,24 +97,28 @@ export function AuthProvider({ children }: AuthProviderProps) {
const checkAuth = useCallback(() => {
return checkAuthInternal(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const login = async (username: string, password: string): Promise<boolean> => {
const login = async (
username: string,
password: string,
): Promise<boolean> => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
credentials: 'include', // Ensure cookies are received
credentials: "include", // Ensure cookies are received
});
if (response.ok) {
const data = await response.json() as { username: string };
const data = (await response.json()) as { username: string };
setIsAuthenticated(true);
setUsername(data.username);
// Check auth again to get expiration time
// Add a small delay to ensure the httpOnly cookie is available
await new Promise<void>((resolve) => {
@@ -115,18 +129,19 @@ export function AuthProvider({ children }: AuthProviderProps) {
return true;
} else {
const errorData = await response.json();
console.error('Login failed:', errorData.error);
console.error("Login failed:", errorData.error);
return false;
}
} catch (error) {
console.error('Login error:', error);
console.error("Login error:", error);
return false;
}
};
const logout = () => {
// Clear the auth cookie by setting it to expire
document.cookie = 'auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
document.cookie =
"auth-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
setIsAuthenticated(false);
setUsername(null);
setExpirationTime(null);
@@ -156,7 +171,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

View File

@@ -89,7 +89,7 @@ export function BackupsTab() {
setShouldPollRestore(false);
// Check if restore was successful or failed
const lastLog =
restoreLogsData.logs[restoreLogsData.logs.length - 1] || "";
restoreLogsData.logs[restoreLogsData.logs.length - 1] ?? "";
if (lastLog.includes("Restore completed successfully")) {
setRestoreSuccess(true);
setRestoreError(null);
@@ -118,9 +118,9 @@ export function BackupsTab() {
const progressMessages =
restoreProgress.length > 0
? restoreProgress
: result.progress?.map((p) => p.message) || [
: (result.progress?.map((p) => p.message) ?? [
"Restore completed successfully",
];
]);
setRestoreProgress(progressMessages);
setRestoreSuccess(true);
setRestoreError(null);
@@ -128,9 +128,9 @@ export function BackupsTab() {
setSelectedBackup(null);
// Keep success message visible - user can dismiss manually
} else {
setRestoreError(result.error || "Restore failed");
setRestoreError(result.error ?? "Restore failed");
setRestoreProgress(
result.progress?.map((p) => p.message) || restoreProgress,
result.progress?.map((p) => p.message) ?? restoreProgress,
);
setRestoreSuccess(false);
setRestoreConfirmOpen(false);
@@ -141,7 +141,7 @@ export function BackupsTab() {
onError: (error) => {
// Stop polling on error
setShouldPollRestore(false);
setRestoreError(error.message || "Restore failed");
setRestoreError(error.message ?? "Restore failed");
setRestoreConfirmOpen(false);
setSelectedBackup(null);
setRestoreProgress([]);
@@ -158,11 +158,12 @@ export function BackupsTab() {
useEffect(() => {
if (!hasAutoDiscovered && !isLoading && backupsData) {
// Only auto-discover if there are no backups yet
if (!backupsData.backups || backupsData.backups.length === 0) {
handleDiscoverBackups();
if (!backupsData.backups?.length) {
void handleDiscoverBackups();
}
setHasAutoDiscovered(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasAutoDiscovered, isLoading, backupsData]);
const handleDiscoverBackups = () => {
@@ -436,7 +437,7 @@ export function BackupsTab() {
{backupsData && !backupsData.success && (
<div className="bg-destructive/10 border-destructive rounded-lg border p-4">
<p className="text-destructive">
Error loading backups: {backupsData.error || "Unknown error"}
Error loading backups: {backupsData.error ?? "Unknown error"}
</p>
</div>
)}

View File

@@ -1785,7 +1785,7 @@ export function GeneralSettingsModal({
) {
return;
}
setDeletingRepoId(repo.id);
setDeletingRepoId(Number(repo.id));
setMessage(null);
try {
const result =

View File

@@ -216,11 +216,16 @@ export function TextViewer({
</Button>
</div>
)}
{/* Boolean logic intentionally uses || for truthiness checks - eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
{((selectedVersion === "default" &&
(scriptContent.mainScript || scriptContent.installScript)) ||
Boolean(
scriptContent.mainScript ?? scriptContent.installScript,
)) ||
(selectedVersion === "alpine" &&
(scriptContent.alpineMainScript ||
scriptContent.alpineInstallScript))) && (
Boolean(
scriptContent.alpineMainScript ??
scriptContent.alpineInstallScript,
))) && (
<div className="flex space-x-2">
<Button
variant={activeTab === "main" ? "outline" : "ghost"}

View File

@@ -3,6 +3,14 @@ import { NextResponse } from 'next/server';
import { getDatabase } from '../../../../../server/database-prisma';
import { getSSHService } from '../../../../../server/ssh-service';
interface ServerData {
id: number;
name: string;
ip: string;
ssh_key_path?: string | null;
key_generated?: boolean;
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
@@ -18,7 +26,7 @@ export async function GET(
}
const db = getDatabase();
const server = await db.getServerById(id);
const server = await db.getServerById(id) as ServerData | null;
if (!server) {
return NextResponse.json(
@@ -28,14 +36,14 @@ export async function GET(
}
// Only allow viewing public key if it was generated by the system
if (!(server as any).key_generated) {
if (!server.key_generated) {
return NextResponse.json(
{ error: 'Public key not available for user-provided keys' },
{ status: 403 }
);
}
if (!(server as any).ssh_key_path) {
if (!server.ssh_key_path) {
return NextResponse.json(
{ error: 'SSH key path not found' },
{ status: 404 }
@@ -43,13 +51,13 @@ export async function GET(
}
const sshService = getSSHService();
const publicKey = sshService.getPublicKey((server as any).ssh_key_path as string);
const publicKey = sshService.getPublicKey(server.ssh_key_path);
return NextResponse.json({
success: true,
publicKey,
serverName: (server as any).name,
serverIp: (server as any).ip
serverName: server.name,
serverIp: server.ip
});
} catch (error) {
console.error('Error retrieving public key:', error);

View File

@@ -12,7 +12,7 @@ export const POST = withApiLogging(async function POST(_request: NextRequest) {
// Get the next available server ID for key file naming
const serverId = await db.getNextServerId();
const keyPair = await sshService.generateKeyPair(serverId);
const keyPair = await sshService.generateKeyPair(Number(serverId));
return NextResponse.json({
success: true,

View File

@@ -4,9 +4,25 @@ import fs from 'fs';
import path from 'path';
import { isValidCron } from 'cron-validator';
interface AutoSyncSettings {
autoSyncEnabled: boolean;
syncIntervalType: string;
syncIntervalPredefined?: string;
syncIntervalCron?: string;
autoDownloadNew: boolean;
autoUpdateExisting: boolean;
notificationEnabled: boolean;
appriseUrls?: string[] | string;
lastAutoSync?: string;
lastAutoSyncError?: string;
lastAutoSyncErrorTime?: string;
testNotification?: boolean;
triggerManualSync?: boolean;
}
export async function POST(request: NextRequest) {
try {
const settings = await request.json();
const settings = await request.json() as AutoSyncSettings;
if (!settings || typeof settings !== 'object') {
return NextResponse.json(
@@ -54,7 +70,7 @@ export async function POST(request: NextRequest) {
// Validate predefined interval
if (settings.syncIntervalType === 'predefined') {
const validIntervals = ['15min', '30min', '1hour', '6hours', '12hours', '24hours'];
if (!validIntervals.includes(settings.syncIntervalPredefined)) {
if (!settings.syncIntervalPredefined || !validIntervals.includes(settings.syncIntervalPredefined)) {
return NextResponse.json(
{ error: 'Invalid predefined interval' },
{ status: 400 }
@@ -67,7 +83,7 @@ export async function POST(request: NextRequest) {
if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string' || settings.syncIntervalCron.trim() === '') {
// Fallback to predefined if custom is selected but no cron expression
settings.syncIntervalType = 'predefined';
settings.syncIntervalPredefined = settings.syncIntervalPredefined || '1hour';
settings.syncIntervalPredefined = settings.syncIntervalPredefined ?? '1hour';
settings.syncIntervalCron = '';
} else if (!isValidCron(settings.syncIntervalCron, { seconds: false })) {
return NextResponse.json(
@@ -109,7 +125,7 @@ export async function POST(request: NextRequest) {
);
}
}
} catch (parseError) {
} catch {
return NextResponse.json(
{ error: 'Invalid JSON format for Apprise URLs' },
{ status: 400 }
@@ -130,15 +146,15 @@ export async function POST(request: NextRequest) {
const autoSyncSettings = {
'AUTO_SYNC_ENABLED': settings.autoSyncEnabled ? 'true' : 'false',
'SYNC_INTERVAL_TYPE': settings.syncIntervalType,
'SYNC_INTERVAL_PREDEFINED': settings.syncIntervalPredefined || '',
'SYNC_INTERVAL_CRON': settings.syncIntervalCron || '',
'SYNC_INTERVAL_PREDEFINED': settings.syncIntervalPredefined ?? '',
'SYNC_INTERVAL_CRON': settings.syncIntervalCron ?? '',
'AUTO_DOWNLOAD_NEW': settings.autoDownloadNew ? 'true' : 'false',
'AUTO_UPDATE_EXISTING': settings.autoUpdateExisting ? 'true' : 'false',
'NOTIFICATION_ENABLED': settings.notificationEnabled ? 'true' : 'false',
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls || '[]'),
'LAST_AUTO_SYNC': settings.lastAutoSync || '',
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError || '',
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime || ''
'APPRISE_URLS': Array.isArray(settings.appriseUrls) ? JSON.stringify(settings.appriseUrls) : (settings.appriseUrls ?? '[]'),
'LAST_AUTO_SYNC': settings.lastAutoSync ?? '',
'LAST_AUTO_SYNC_ERROR': settings.lastAutoSyncError ?? '',
'LAST_AUTO_SYNC_ERROR_TIME': settings.lastAutoSyncErrorTime ?? ''
};
// Update or add each setting
@@ -231,21 +247,21 @@ export async function GET() {
autoSyncEnabled: getEnvValue(envContent, 'AUTO_SYNC_ENABLED') === 'true',
syncIntervalType: getEnvValue(envContent, 'SYNC_INTERVAL_TYPE') || 'predefined',
syncIntervalPredefined: getEnvValue(envContent, 'SYNC_INTERVAL_PREDEFINED') || '1hour',
syncIntervalCron: getEnvValue(envContent, 'SYNC_INTERVAL_CRON') || '',
syncIntervalCron: getEnvValue(envContent, 'SYNC_INTERVAL_CRON') ?? '',
autoDownloadNew: getEnvValue(envContent, 'AUTO_DOWNLOAD_NEW') === 'true',
autoUpdateExisting: getEnvValue(envContent, 'AUTO_UPDATE_EXISTING') === 'true',
notificationEnabled: getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true',
appriseUrls: (() => {
try {
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
return JSON.parse(urlsValue);
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') ?? '[]';
return JSON.parse(urlsValue) as string[];
} catch {
return [];
}
})(),
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') || '',
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') || null,
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') || null
lastAutoSync: getEnvValue(envContent, 'LAST_AUTO_SYNC') ?? '',
lastAutoSyncError: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR') ?? null,
lastAutoSyncErrorTime: getEnvValue(envContent, 'LAST_AUTO_SYNC_ERROR_TIME') ?? null
};
return NextResponse.json({ settings });
@@ -275,8 +291,8 @@ async function handleTestNotification() {
const notificationEnabled = getEnvValue(envContent, 'NOTIFICATION_ENABLED') === 'true';
const appriseUrls = (() => {
try {
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') || '[]';
return JSON.parse(urlsValue);
const urlsValue = getEnvValue(envContent, 'APPRISE_URLS') ?? '[]';
return JSON.parse(urlsValue) as string[];
} catch {
return [];
}
@@ -289,7 +305,7 @@ async function handleTestNotification() {
);
}
if (!appriseUrls || appriseUrls.length === 0) {
if (!appriseUrls?.length) {
return NextResponse.json(
{ error: 'No Apprise URLs configured' },
{ status: 400 }
@@ -347,9 +363,9 @@ async function handleManualSync() {
// Trigger manual sync using the auto-sync service
const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js');
const autoSyncService = new AutoSyncService();
const result = await autoSyncService.executeAutoSync() as any;
const result = await autoSyncService.executeAutoSync() as { success: boolean; message?: string } | null;
if (result && result.success) {
if (result?.success) {
return NextResponse.json({
success: true,
message: 'Manual sync completed successfully',
@@ -357,7 +373,7 @@ async function handleManualSync() {
});
} else {
return NextResponse.json(
{ error: result.message },
{ error: result?.message ?? 'Unknown error' },
{ status: 500 }
);
}
@@ -376,7 +392,7 @@ function getEnvValue(envContent: string, key: string): string {
const regex = new RegExp(`^${key}="(.+)"$`, 'm');
let match = regex.exec(envContent);
if (match && match[1]) {
if (match?.[1]) {
let value = match[1];
// Remove extra quotes that might be around JSON values
if (value.startsWith('"') && value.endsWith('"')) {
@@ -388,7 +404,7 @@ function getEnvValue(envContent: string, key: string): string {
// Try to match without quotes (fallback)
const regexNoQuotes = new RegExp(`^${key}=([^\\s]*)$`, 'm');
match = regexNoQuotes.exec(envContent);
if (match && match[1]) {
if (match?.[1]) {
return match[1];
}

View File

@@ -126,18 +126,26 @@ export default function Home() {
.replace(/^-+|-+$/g, "");
// First deduplicate GitHub scripts using Map by slug
const scriptMap = new Map<string, any>();
interface ScriptCard {
name?: string;
slug?: string;
install_basenames?: string[];
}
const scriptMap = new Map<string, ScriptCard>();
scriptCardsData.cards?.forEach((script) => {
if (script?.name && script?.slug) {
if (!scriptMap.has(script.slug)) {
scriptMap.set(script.slug, script);
scriptMap.set(script.slug, script as ScriptCard);
}
}
});
const deduplicatedGithubScripts = Array.from(scriptMap.values());
const localScripts = localScriptsData.scripts ?? [];
const localScripts = (localScriptsData.scripts ?? []) as Array<{
name?: string;
slug?: string;
}>;
// Count scripts that are both in deduplicated GitHub data and have local versions
// Use the same matching logic as DownloadedScriptsTab and ScriptsGrid
@@ -154,26 +162,27 @@ export default function Home() {
return true;
}
// Also try normalized slug matching (handles filename-based slugs vs JSON slugs)
if (normalizeId(local.slug) === normalizeId(script.slug)) {
if (
normalizeId(local.slug ?? undefined) ===
normalizeId(script.slug ?? undefined)
) {
return true;
}
}
// Secondary: Check install basenames (for edge cases where install script names differ from slugs)
const normalizedLocal = normalizeId(local.name);
const scriptWithBasenames = script as {
install_basenames?: string[];
};
const normalizedLocal = normalizeId(local.name ?? undefined);
const matchesInstallBasename =
scriptWithBasenames.install_basenames?.some(
(base: string) => normalizeId(base) === normalizedLocal,
script.install_basenames?.some(
(base) => normalizeId(String(base)) === normalizedLocal,
) ?? false;
if (matchesInstallBasename) return true;
// Tertiary: Normalized filename to normalized slug matching
if (
script.slug &&
normalizeId(local.name) === normalizeId(script.slug)
normalizeId(local.name ?? undefined) ===
normalizeId(script.slug ?? undefined)
) {
return true;
}

View File

@@ -147,7 +147,7 @@ export function getAuthConfig(): {
const sessionDurationRegex = /^AUTH_SESSION_DURATION_DAYS=(.*)$/m;
const sessionDurationMatch = sessionDurationRegex.exec(envContent);
const sessionDurationDays = sessionDurationMatch
? parseInt(sessionDurationMatch[1]?.trim() || String(DEFAULT_JWT_EXPIRY_DAYS), 10) || DEFAULT_JWT_EXPIRY_DAYS
? parseInt(sessionDurationMatch[1]?.trim() ?? String(DEFAULT_JWT_EXPIRY_DAYS), 10) || DEFAULT_JWT_EXPIRY_DAYS
: DEFAULT_JWT_EXPIRY_DAYS;
const hasCredentials = !!(username && passwordHash);

View File

@@ -38,7 +38,7 @@ export const backupsRouter = createTRPCRouter({
if (backups.length === 0) continue;
// Get hostname from first backup (all backups for same container should have same hostname)
const hostname = backups[0]?.hostname || '';
const hostname = backups[0]?.hostname ?? '';
result.push({
container_id: containerId,

View File

@@ -5,10 +5,47 @@ import { createHash } from "crypto";
import type { Server } from "~/types/server";
import { getStorageService } from "~/server/services/storageService";
// Type for SSH connection test result
interface SSHConnectionResult {
success: boolean;
error?: string;
}
// Type for parsed LXC config
interface ParsedLXCConfig {
arch?: string;
cores?: number;
memory?: number;
hostname?: string;
swap?: number;
onboot?: number;
ostype?: string;
unprivileged?: number;
tags?: string;
net_name?: string;
net_bridge?: string;
net_hwaddr?: string;
net_ip?: string;
net_ip_type?: string;
net_gateway?: string;
net_type?: string;
net_vlan?: number;
feature_keyctl?: number;
feature_nesting?: number;
feature_fuse?: number;
feature_mount?: string;
rootfs_storage?: string;
rootfs_size?: string;
advanced_config?: string;
config_hash?: string;
synced_at?: Date;
[key: string]: unknown;
}
// Helper function to parse raw LXC config into structured data
function parseRawConfig(rawConfig: string): any {
function parseRawConfig(rawConfig: string): ParsedLXCConfig {
const lines = rawConfig.split('\n');
const config: any = { advanced: [] };
const config: ParsedLXCConfig & { advanced: string[] } = { advanced: [] };
for (const line of lines) {
const trimmed = line.trim();
@@ -80,17 +117,18 @@ function parseRawConfig(rawConfig: string): any {
}
config.advanced_config = config.advanced.join('\n');
delete config.advanced; // Remove the advanced array since we only need advanced_config
return config;
// Remove the advanced array after copying to advanced_config
const { advanced: _, ...configWithoutAdvanced } = config;
return configWithoutAdvanced;
}
// Helper function to reconstruct config from structured data
function reconstructConfig(parsed: any): string {
function reconstructConfig(parsed: ParsedLXCConfig): string {
const lines: string[] = [];
// Add standard fields in order
if (parsed.arch) lines.push(`arch: ${parsed.arch}`);
if (parsed.cores) lines.push(`cores: ${parsed.cores}`);
if (parsed.arch) lines.push(`arch: ${String(parsed.arch)}`);
if (parsed.cores) lines.push(`cores: ${String(parsed.cores)}`);
// Build features line
if (parsed.feature_keyctl !== undefined || parsed.feature_nesting !== undefined || parsed.feature_fuse !== undefined) {
@@ -666,12 +704,12 @@ export const installedScriptsRouter = createTRPCRouter({
// Test SSH connection first
const connectionTest = await sshService.testSSHConnection(server as Server);
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!(connectionTest as any).success) {
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
detectedContainers: []
};
}
@@ -744,8 +782,8 @@ export const installedScriptsRouter = createTRPCRouter({
containerId,
hostname,
configPath,
serverId: Number((server as any).id),
serverName: (server as any).name,
serverId: Number(server.id),
serverName: String(server.name),
parsedConfig: {
...parsedConfig,
config_hash: configHash,
@@ -914,9 +952,9 @@ export const installedScriptsRouter = createTRPCRouter({
}
// Test SSH connection
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
console.warn(`cleanupOrphanedScripts: SSH connection failed for server ${String((server as any).name)}, skipping ${serverScripts.length} scripts`);
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
console.warn(`cleanupOrphanedScripts: SSH connection failed for server ${String(String(server.name))}, skipping ${serverScripts.length} scripts`);
continue;
}
@@ -926,7 +964,7 @@ export const installedScriptsRouter = createTRPCRouter({
const existingContainerIds = await new Promise<Set<string>>((resolve, reject) => {
const timeout = setTimeout(() => {
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String((server as any).name)}`);
console.warn(`cleanupOrphanedScripts: timeout while getting container list from server ${String(String(server.name))}`);
resolve(new Set()); // Treat timeout as no containers found
}, 20000);
@@ -937,7 +975,7 @@ export const installedScriptsRouter = createTRPCRouter({
listOutput += data;
},
(error: string) => {
console.error(`cleanupOrphanedScripts: error getting container list from server ${String((server as any).name)}:`, error);
console.error(`cleanupOrphanedScripts: error getting container list from server ${String(String(server.name))}:`, error);
clearTimeout(timeout);
resolve(new Set()); // Treat error as no containers found
},
@@ -1010,7 +1048,7 @@ export const installedScriptsRouter = createTRPCRouter({
// If container is not in pct list AND config file doesn't exist, it's orphaned
if (!configExists) {
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (container ${containerId}) from server ${String((server as any).name)}`);
console.log(`cleanupOrphanedScripts: Removing orphaned script ${String(scriptData.script_name)} (container ${containerId}) from server ${String(String(server.name))}`);
await db.deleteInstalledScript(Number(scriptData.id));
deletedScripts.push(String(scriptData.script_name));
} else {
@@ -1019,7 +1057,7 @@ export const installedScriptsRouter = createTRPCRouter({
}
}
} catch (error) {
console.error(`cleanupOrphanedScripts: Error checking script ${String((scriptData as any).script_name)}:`, error);
console.error(`cleanupOrphanedScripts: Error checking script ${String(scriptData.script_name)}:`, error);
}
}
} catch (error) {
@@ -1075,8 +1113,8 @@ export const installedScriptsRouter = createTRPCRouter({
// Test SSH connection
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
continue;
}
@@ -1099,7 +1137,7 @@ export const installedScriptsRouter = createTRPCRouter({
listOutput += data;
},
(error: string) => {
console.error(`pct list error on server ${(server as any).name}:`, error);
console.error(`pct list error on server ${String(server.name)}:`, error);
reject(new Error(error));
},
(_exitCode: number) => {
@@ -1134,7 +1172,7 @@ export const installedScriptsRouter = createTRPCRouter({
}
}
} catch (error) {
console.error(`Error processing server ${(server as any).name}:`, error);
console.error(`Error processing server ${String(server.name)}:`, error);
}
}
@@ -1198,11 +1236,11 @@ export const installedScriptsRouter = createTRPCRouter({
// Test SSH connection first
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
status: 'unknown' as const
};
}
@@ -1297,11 +1335,11 @@ export const installedScriptsRouter = createTRPCRouter({
// Test SSH connection first
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
};
}
@@ -1388,11 +1426,11 @@ export const installedScriptsRouter = createTRPCRouter({
// Test SSH connection first
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
};
}
@@ -1551,7 +1589,7 @@ export const installedScriptsRouter = createTRPCRouter({
};
}
console.log('🖥️ Server found:', { id: (server as any).id, name: (server as any).name, ip: (server as any).ip });
console.log('🖥️ Server found:', { id: Number(server.id), name: String(server.name), ip: String(server.ip) });
// Import SSH services
const { default: SSHService } = await import('~/server/ssh-service');
@@ -1562,12 +1600,12 @@ export const installedScriptsRouter = createTRPCRouter({
// Test SSH connection first
console.log('🔌 Testing SSH connection...');
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
console.log('❌ SSH connection failed:', (connectionTest as any).error);
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
console.log('❌ SSH connection failed:', connectionTest.error);
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
};
}
@@ -1666,11 +1704,11 @@ export const installedScriptsRouter = createTRPCRouter({
console.log('✅ Successfully updated database');
return {
success: true,
message: `Successfully detected IP: ${detectedIp}:${detectedPort} for LXC ${scriptData.container_id} on ${(server as any).name}`,
message: `Successfully detected IP: ${detectedIp}:${detectedPort} for LXC ${scriptData.container_id} on ${String(server.name)}`,
detectedIp,
detectedPort: detectedPort,
containerId: scriptData.container_id,
serverName: (server as any).name
serverName: String(server.name)
};
} catch (error) {
console.error('Error in autoDetectWebUI:', error);
@@ -1739,11 +1777,11 @@ export const installedScriptsRouter = createTRPCRouter({
const sshExecutionService = new SSHExecutionService();
// Test SSH connection
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
};
}
@@ -1857,11 +1895,11 @@ export const installedScriptsRouter = createTRPCRouter({
const sshExecutionService = new SSHExecutionService();
// Test SSH connection
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`
};
}
@@ -2068,11 +2106,11 @@ EOFCONFIG`;
const sshExecutionService = getSSHExecutionService();
// Test SSH connection first
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
storages: [],
cached: false
};
@@ -2170,11 +2208,11 @@ EOFCONFIG`;
const sshService = new SSHService();
// Test SSH connection first
const connectionTest = await sshService.testSSHConnection(server as Server);
if (!(connectionTest as any).success) {
const connectionTest = await sshService.testSSHConnection(server as Server) as SSHConnectionResult;
if (!connectionTest.success) {
return {
success: false,
error: `SSH connection failed: ${(connectionTest as any).error ?? 'Unknown error'}`,
error: `SSH connection failed: ${connectionTest.error ?? 'Unknown error'}`,
executionId: null
};
}
@@ -2199,3 +2237,6 @@ EOFCONFIG`;
}
})
});

View File

@@ -4,7 +4,7 @@ const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
export const prisma: PrismaClient = globalForPrisma.prisma ?? new PrismaClient({
log: ['warn', 'error']
});