Files
ProxmoxVE-Local/src/server/api/routers/scripts.ts
Michel Roegl-Brunner 7b4daf8754 Fix autosync continuing to run after being disabled
- Add defensive check in cron job execution to stop if autosync is disabled
- Ensure isRunning flag is set to false when stopping autosync
- Add logging to show when autosync is disabled and not scheduling
- Fix both API route and TRPC router to properly stop service
- Prevent multiple cron jobs from running simultaneously

This fixes the issue where autosync would continue running even after
being disabled in the GUI, causing rate limit errors and unwanted syncs.
2025-10-24 22:20:13 +02:00

592 lines
19 KiB
TypeScript

import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { scriptManager } from "~/server/lib/scripts";
import { githubJsonService } from "~/server/services/githubJsonService";
import { localScriptsService } from "~/server/services/localScripts";
import { scriptDownloaderService } from "~/server/services/scriptDownloader";
import { AutoSyncService } from "~/server/services/autoSyncService";
import type { ScriptCard } from "~/types/script";
export const scriptsRouter = createTRPCRouter({
// Get all available scripts
getScripts: publicProcedure
.query(async () => {
const scripts = await scriptManager.getScripts();
return {
scripts,
directoryInfo: scriptManager.getScriptsDirectoryInfo()
};
}),
// Get CT scripts (for local scripts tab)
getCtScripts: publicProcedure
.query(async () => {
const scripts = await scriptManager.getCtScripts();
return {
scripts,
directoryInfo: scriptManager.getScriptsDirectoryInfo()
};
}),
// Get all downloaded scripts from all directories
getAllDownloadedScripts: publicProcedure
.query(async () => {
const scripts = await scriptManager.getAllDownloadedScripts();
return {
scripts,
directoryInfo: scriptManager.getScriptsDirectoryInfo()
};
}),
// Get script content for viewing
getScriptContent: publicProcedure
.input(z.object({ path: z.string() }))
.query(async ({ input }) => {
try {
const { readFile } = await import('fs/promises');
const { join } = await import('path');
const { env } = await import('~/env');
const scriptsDir = join(process.cwd(), env.SCRIPTS_DIRECTORY);
const fullPath = join(scriptsDir, input.path);
// Security check: ensure the path is within the scripts directory
if (!fullPath.startsWith(scriptsDir)) {
throw new Error('Invalid script path');
}
const content = await readFile(fullPath, 'utf-8');
return { success: true, content };
} catch (error) {
console.error('Error reading script content:', error);
return { success: false, error: 'Failed to read script content' };
}
}),
// Validate script path
validateScript: publicProcedure
.input(z.object({ scriptPath: z.string() }))
.query(async ({ input }) => {
const validation = scriptManager.validateScriptPath(input.scriptPath);
return validation;
}),
// Get directory information
getDirectoryInfo: publicProcedure
.query(async () => {
return scriptManager.getScriptsDirectoryInfo();
}),
// Local script routes (using scripts/json directory)
// Get all script cards from local directory
getScriptCards: publicProcedure
.query(async () => {
try {
const cards = await localScriptsService.getScriptCards();
return { success: true, cards };
} catch (error) {
console.error('Error in getScriptCards:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch script cards',
cards: []
};
}
}),
// Get all scripts from GitHub (1 API call + raw downloads)
getAllScripts: publicProcedure
.query(async () => {
try {
const scripts = await githubJsonService.getAllScripts();
return { success: true, scripts };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch scripts',
scripts: []
};
}
}),
// Get script by slug from GitHub (1 API call + raw downloads)
getScriptBySlug: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ input }) => {
try {
console.log('getScriptBySlug called with slug:', input.slug);
console.log('githubJsonService methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(githubJsonService)));
console.log('githubJsonService.getScriptBySlug type:', typeof githubJsonService.getScriptBySlug);
if (typeof githubJsonService.getScriptBySlug !== 'function') {
return {
success: false,
error: 'getScriptBySlug method is not available on githubJsonService',
script: null
};
}
const script = await githubJsonService.getScriptBySlug(input.slug);
if (!script) {
return {
success: false,
error: 'Script not found',
script: null
};
}
return { success: true, script };
} catch (error) {
console.error('Error in getScriptBySlug:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch script',
script: null
};
}
}),
// Get metadata (categories and other metadata)
getMetadata: publicProcedure
.query(async () => {
try {
const metadata = await localScriptsService.getMetadata();
return { success: true, metadata };
} catch (error) {
console.error('Error in getMetadata:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch metadata',
metadata: null
};
}
}),
// Get script cards with category information
getScriptCardsWithCategories: publicProcedure
.query(async () => {
try {
const [cards, metadata] = await Promise.all([
localScriptsService.getScriptCards(),
localScriptsService.getMetadata()
]);
// Get all scripts to access their categories
const scripts = await localScriptsService.getAllScripts();
// Create category ID to name mapping
const categoryMap: Record<number, string> = {};
if (metadata?.categories) {
metadata.categories.forEach((cat: any) => {
categoryMap[cat.id] = cat.name;
});
}
// Enhance cards with category information and additional script data
const cardsWithCategories = cards.map(card => {
const script = scripts.find(s => s.slug === card.slug);
const categoryNames: string[] = script?.categories?.map(id => categoryMap[id]).filter((name): name is string => typeof name === 'string') ?? [];
// Extract OS and version from first install method
const firstInstallMethod = script?.install_methods?.[0];
const os = firstInstallMethod?.resources?.os;
const version = firstInstallMethod?.resources?.version;
// Extract install basenames for robust local matching (e.g., execute.sh -> execute)
const install_basenames = (script?.install_methods ?? [])
.map(m => m?.script)
.filter((p): p is string => typeof p === 'string')
.map(p => {
const parts = p.split('/');
const file = parts[parts.length - 1] ?? '';
return file.replace(/\.(sh|bash|py|js|ts)$/i, '');
});
return {
...card,
categories: script?.categories ?? [],
categoryNames: categoryNames,
// Add date_created from script
date_created: script?.date_created,
// Add OS and version from install methods
os: os,
version: version,
// Add interface port
interface_port: script?.interface_port,
install_basenames,
} as ScriptCard;
});
return { success: true, cards: cardsWithCategories, metadata };
} catch (error) {
console.error('Error in getScriptCardsWithCategories:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch script cards with categories',
cards: [],
metadata: null
};
}
}),
// Resync scripts from GitHub (1 API call + raw downloads)
resyncScripts: publicProcedure
.mutation(async () => {
try {
// Sync JSON files using 1 API call + raw downloads
const result = await githubJsonService.syncJsonFiles();
return {
success: result.success,
message: result.message,
count: result.count
};
} catch (error) {
console.error('Error in resyncScripts:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to resync scripts. Make sure REPO_URL is set.',
count: 0
};
}
}),
// Load script files from GitHub
loadScript: publicProcedure
.input(z.object({ slug: z.string() }))
.mutation(async ({ input }) => {
try {
// Get the script details
const script = await localScriptsService.getScriptBySlug(input.slug);
if (!script) {
return {
success: false,
error: 'Script not found',
files: []
};
}
// Load the script files
const result = await scriptDownloaderService.loadScript(script);
return result;
} catch (error) {
console.error('Error in loadScript:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to load script',
files: []
};
}
}),
// Load multiple scripts from GitHub
loadMultipleScripts: publicProcedure
.input(z.object({ slugs: z.array(z.string()) }))
.mutation(async ({ input }) => {
try {
const successful = [];
const failed = [];
for (const slug of input.slugs) {
try {
// Get the script details
const script = await localScriptsService.getScriptBySlug(slug);
if (!script) {
failed.push({ slug, error: 'Script not found' });
continue;
}
// Load the script files
const result = await scriptDownloaderService.loadScript(script);
if (result.success) {
successful.push({ slug, files: result.files });
} else {
const error = 'error' in result ? result.error : 'Failed to load script';
failed.push({ slug, error });
}
} catch (error) {
failed.push({
slug,
error: error instanceof Error ? error.message : 'Failed to load script'
});
}
}
return {
success: true,
message: `Downloaded ${successful.length} scripts successfully, ${failed.length} failed`,
successful,
failed,
total: input.slugs.length
};
} catch (error) {
console.error('Error in loadMultipleScripts:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to load multiple scripts',
successful: [],
failed: [],
total: 0
};
}
}),
// Check if script files exist locally
checkScriptFiles: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ input }) => {
try {
const script = await localScriptsService.getScriptBySlug(input.slug);
if (!script) {
return {
success: false,
error: 'Script not found',
ctExists: false,
installExists: false,
files: []
};
}
const result = await scriptDownloaderService.checkScriptExists(script);
return {
success: true,
...result
};
} catch (error) {
console.error('Error in checkScriptFiles:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to check script files',
ctExists: false,
installExists: false,
files: []
};
}
}),
// Compare local and remote script content
compareScriptContent: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ input }) => {
try {
const script = await localScriptsService.getScriptBySlug(input.slug);
if (!script) {
return {
success: false,
error: 'Script not found',
hasDifferences: false,
differences: []
};
}
const result = await scriptDownloaderService.compareScriptContent(script);
return {
success: true,
...result
};
} catch (error) {
console.error('Error in compareScriptContent:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to compare script content',
hasDifferences: false,
differences: []
};
}
}),
// Get diff content for a specific script file
getScriptDiff: publicProcedure
.input(z.object({ slug: z.string(), filePath: z.string() }))
.query(async ({ input }) => {
try {
const script = await localScriptsService.getScriptBySlug(input.slug);
if (!script) {
return {
success: false,
error: 'Script not found',
diff: null
};
}
const result = await scriptDownloaderService.getScriptDiff(script, input.filePath);
return {
success: true,
...result
};
} catch (error) {
console.error('Error in getScriptDiff:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get script diff',
diff: null
};
}
}),
// Check if running on Proxmox VE host
checkProxmoxVE: publicProcedure
.query(async () => {
try {
const { spawn } = await import('child_process');
return new Promise((resolve) => {
const child = spawn('command', ['-v', 'pveversion'], {
stdio: ['pipe', 'pipe', 'pipe'],
shell: true
});
child.on('close', (code) => {
// If command exits with code 0, pveversion command exists
if (code === 0) {
resolve({
success: true,
isProxmoxVE: true,
message: 'Running on Proxmox VE host'
});
} else {
resolve({
success: true,
isProxmoxVE: false,
message: 'Not running on Proxmox VE host'
});
}
});
child.on('error', (error) => {
resolve({
success: false,
isProxmoxVE: false,
error: error.message,
message: 'Failed to check Proxmox VE status'
});
});
});
} catch (error) {
console.error('Error in checkProxmoxVE:', error);
return {
success: false,
isProxmoxVE: false,
error: error instanceof Error ? error.message : 'Failed to check Proxmox VE status',
message: 'Failed to check Proxmox VE status'
};
}
}),
// Auto-sync settings and operations
getAutoSyncSettings: publicProcedure
.query(async () => {
try {
const autoSyncService = new AutoSyncService();
const settings = autoSyncService.loadSettings();
return { success: true, settings };
} catch (error) {
console.error('Error getting auto-sync settings:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get auto-sync settings',
settings: null
};
}
}),
saveAutoSyncSettings: publicProcedure
.input(z.object({
autoSyncEnabled: z.boolean(),
syncIntervalType: z.enum(['predefined', 'custom']),
syncIntervalPredefined: z.string().optional(),
syncIntervalCron: z.string().optional(),
autoDownloadNew: z.boolean(),
autoUpdateExisting: z.boolean(),
notificationEnabled: z.boolean(),
appriseUrls: z.array(z.string()).optional()
}))
.mutation(async ({ input }) => {
try {
// Use the global auto-sync service instance
const { getAutoSyncService, setAutoSyncService } = await import('~/server/lib/autoSyncInit');
let autoSyncService = getAutoSyncService();
// If no global instance exists, create one
if (!autoSyncService) {
const { AutoSyncService } = await import('~/server/services/autoSyncService');
autoSyncService = new AutoSyncService();
setAutoSyncService(autoSyncService);
}
// Save settings to both .env file and service instance
autoSyncService.saveSettings(input);
// Reschedule auto-sync if enabled
if (input.autoSyncEnabled) {
autoSyncService.scheduleAutoSync();
console.log('Auto-sync rescheduled with new settings');
} else {
autoSyncService.stopAutoSync();
// Ensure the service is completely stopped and won't restart
autoSyncService.isRunning = false;
console.log('Auto-sync stopped');
}
return { success: true, message: 'Auto-sync settings saved successfully' };
} catch (error) {
console.error('Error saving auto-sync settings:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to save auto-sync settings'
};
}
}),
testNotification: publicProcedure
.mutation(async () => {
try {
const autoSyncService = new AutoSyncService();
const result = await autoSyncService.testNotification();
return result;
} catch (error) {
console.error('Error testing notification:', error);
return {
success: false,
message: error instanceof Error ? error.message : 'Failed to test notification'
};
}
}),
triggerManualAutoSync: publicProcedure
.mutation(async () => {
try {
const autoSyncService = new AutoSyncService();
const result = await autoSyncService.executeAutoSync();
return {
success: true,
message: 'Manual auto-sync completed successfully',
result
};
} catch (error) {
console.error('Error in manual auto-sync:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to execute manual auto-sync',
result: null
};
}
}),
getAutoSyncStatus: publicProcedure
.query(async () => {
try {
const autoSyncService = new AutoSyncService();
const status = autoSyncService.getStatus();
return { success: true, status };
} catch (error) {
console.error('Error getting auto-sync status:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get auto-sync status',
status: null
};
}
})
});