Files
ProxmoxVE-Local/src/server/api/routers/scripts.ts
Michel Roegl-Brunner e09c1bbf5d Merge development into main (#42)
* feat: improve button layout and UI organization (#35)

- Reorganize control buttons into a structured container with proper spacing
- Add responsive design for mobile and desktop layouts
- Improve SettingsButton and ResyncButton component structure
- Enhance visual hierarchy with better typography and spacing
- Add background container with shadow and border for better grouping
- Make layout responsive with proper flexbox arrangements

* Add category sidebar and filtering to scripts grid (#36)

* Add category sidebar and filtering to scripts grid

Introduces a CategorySidebar component with icon mapping and category selection. Updates metadata.json to include icons for each category. Enhances ScriptsGrid to support category-based filtering and integrates the sidebar, improving script navigation and discoverability. Also refines ScriptDetailModal layout for better modal presentation.

* Add category metadata to scripts and improve filtering

Introduces category metadata loading and exposes it via new API endpoints. Script cards are now enhanced with category information, allowing for accurate category-based filtering and counting in the ScriptsGrid component. Removes hardcoded category logic and replaces it with dynamic data from metadata.json.

* Add reusable Badge component and refactor badge usage (#37)

Introduces a new Badge component with variants for type, updateable, privileged, status, execution mode, and note. Refactors ScriptCard, ScriptDetailModal, and InstalledScriptsTab to use the new Badge components, improving consistency and maintainability. Also updates DarkModeProvider and layout.tsx for better dark mode handling and fallback.

* Add advanced filtering and sorting to ScriptsGrid (#38)

Introduces a new FilterBar component for ScriptsGrid, enabling filtering by search query, updatable status, script types, and sorting by name or creation date. Updates scripts API to include creation date in card data, improves deduplication and category counting logic, and adds error handling for missing script directories.

* refactore installed scipts tab (#41)

* feat: Add inline editing and manual script entry functionality

- Add inline editing for script names and container IDs in installed scripts table
- Add manual script entry form for pre-installed containers
- Update database and API to support script_name editing
- Improve dark mode hover effects for table rows
- Add form validation and error handling
- Support both local and SSH execution modes for manual entries

* feat: implement installed scripts functionality and clean up test files

- Add installed scripts tab with filtering and execution capabilities
- Update scripts grid with better type safety and error handling
- Remove outdated test files and update test configuration
- Fix TypeScript and ESLint issues in components
- Update .gitattributes for proper line ending handling

* fix: resolve TypeScript error with categoryNames type mismatch

- Fixed categoryNames type from (string | undefined)[] to string[] in scripts router
- Added proper type filtering and assertion in getScriptCardsWithCategories
- Added missing ScriptCard import in scripts router
- Ensures type safety for categoryNames property throughout the application

---------

Co-authored-by: CanbiZ <47820557+MickLesk@users.noreply.github.com>
2025-10-06 16:24:19 +02:00

380 lines
12 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 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 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 {
const script = await githubJsonService.getScriptBySlug(input.slug);
if (!script) {
return {
success: false,
error: 'Script not found',
script: null
};
}
return { success: true, script };
} catch (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') ?? [];
return {
...card,
categories: script?.categories ?? [],
categoryNames: categoryNames,
// Add date_created from script
date_created: script?.date_created,
} 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: []
};
}
}),
// 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'
};
}
})
});