Skip to content

Commit 4f3a857

Browse files
QoL features
1 parent 7c1c2f7 commit 4f3a857

File tree

6 files changed

+276
-94
lines changed

6 files changed

+276
-94
lines changed

src/app/__tests__/page.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ vi.mock('~/trpc/react', () => ({
3131
data: null,
3232
})),
3333
},
34+
checkProxmoxVE: {
35+
useQuery: vi.fn(() => ({
36+
data: { success: true, isProxmoxVE: true },
37+
isLoading: false,
38+
error: null,
39+
})),
40+
},
3441
fullUpdateRepo: {
3542
useMutation: vi.fn(() => ({
3643
mutate: vi.fn(),
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { api } from '~/trpc/react';
5+
6+
interface ProxmoxCheckProps {
7+
children: React.ReactNode;
8+
}
9+
10+
export function ProxmoxCheck({ children }: ProxmoxCheckProps) {
11+
const [isChecking, setIsChecking] = useState(true);
12+
const [isProxmoxVE, setIsProxmoxVE] = useState<boolean | null>(null);
13+
const [error, setError] = useState<string | null>(null);
14+
15+
const { data: proxmoxData, isLoading } = api.scripts.checkProxmoxVE.useQuery();
16+
17+
useEffect(() => {
18+
if (proxmoxData && typeof proxmoxData === 'object' && 'success' in proxmoxData) {
19+
setIsChecking(false);
20+
if (proxmoxData.success) {
21+
const isProxmox = 'isProxmoxVE' in proxmoxData ? proxmoxData.isProxmoxVE as boolean : false;
22+
setIsProxmoxVE(isProxmox);
23+
if (!isProxmox) {
24+
setError('This application can only run on a Proxmox VE Host');
25+
}
26+
} else {
27+
const errorMsg = 'error' in proxmoxData ? proxmoxData.error as string : 'Failed to check Proxmox VE status';
28+
setError(errorMsg);
29+
setIsProxmoxVE(false);
30+
}
31+
}
32+
}, [proxmoxData]);
33+
34+
// Show loading state
35+
if (isChecking || isLoading) {
36+
return (
37+
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
38+
<div className="text-center">
39+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
40+
<p className="text-gray-600">Checking system requirements...</p>
41+
</div>
42+
</div>
43+
);
44+
}
45+
46+
// Show error if not running on Proxmox VE
47+
if (!isProxmoxVE || error) {
48+
return (
49+
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
50+
<div className="max-w-md mx-auto text-center">
51+
<div className="bg-red-50 border border-red-200 rounded-lg p-8">
52+
<div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full">
53+
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
54+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 19.5c-.77.833.192 2.5 1.732 2.5z" />
55+
</svg>
56+
</div>
57+
<h1 className="text-2xl font-bold text-red-800 mb-2">
58+
System Requirements Not Met
59+
</h1>
60+
<p className="text-red-700 mb-4">
61+
{error ?? 'This application can only run on a Proxmox VE Host'}
62+
</p>
63+
<div className="text-sm text-red-600 bg-red-100 rounded-lg p-4">
64+
<p className="font-medium mb-2">To use this application, you need:</p>
65+
<ul className="text-left space-y-1">
66+
<li>• A Proxmox VE host system</li>
67+
<li>• The <code className="bg-red-200 px-1 rounded">pveversion</code> command must be available</li>
68+
<li>• Proper permissions to execute system commands</li>
69+
</ul>
70+
</div>
71+
<button
72+
onClick={() => window.location.reload()}
73+
className="mt-6 px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
74+
>
75+
Retry Check
76+
</button>
77+
</div>
78+
</div>
79+
</div>
80+
);
81+
}
82+
83+
// If running on Proxmox VE, render the children
84+
return <>{children}</>;
85+
}

src/app/_components/ScriptDetailModal.tsx

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
8787
const scriptPath = `scripts/${scriptMethod.script}`;
8888
const scriptName = script.name;
8989
onInstallScript(scriptPath, scriptName);
90+
91+
// Scroll to top of the page to see the terminal
92+
window.scrollTo({ top: 0, behavior: 'smooth' });
93+
9094
onClose(); // Close the modal when starting installation
9195
}
9296
};
9397

94-
const handleShowDiff = (filePath: string) => {
95-
setSelectedDiffFile(filePath);
96-
setDiffViewerOpen(true);
97-
};
9898

9999
const handleViewScript = () => {
100100
setTextViewerOpen(true);
@@ -279,53 +279,46 @@ export function ScriptDetailModal({ script, isOpen, onClose, onInstallScript }:
279279
</div>
280280
)}
281281

282-
{scriptFilesData?.success && !scriptFilesLoading && (
283-
<div className="mx-6 mb-4 p-3 rounded-lg bg-gray-50 text-sm">
284-
<div className="flex items-center space-x-4">
285-
<div className="flex items-center space-x-2">
286-
<div className={`w-2 h-2 rounded-full ${scriptFilesData.ctExists ? 'bg-green-500' : 'bg-gray-300'}`}></div>
287-
<span>CT Script: {scriptFilesData.ctExists ? 'Available' : 'Not loaded'}</span>
288-
</div>
289-
<div className="flex items-center space-x-2">
290-
<div className={`w-2 h-2 rounded-full ${scriptFilesData.installExists ? 'bg-green-500' : 'bg-gray-300'}`}></div>
291-
<span>Install Script: {scriptFilesData.installExists ? 'Available' : 'Not loaded'}</span>
292-
</div>
293-
{scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && comparisonData?.success && !comparisonLoading && (
282+
{scriptFilesData?.success && !scriptFilesLoading && (() => {
283+
// Determine script type from the first install method
284+
const firstScript = script?.install_methods?.[0]?.script;
285+
let scriptType = 'Script';
286+
if (firstScript?.startsWith('ct/')) {
287+
scriptType = 'CT Script';
288+
} else if (firstScript?.startsWith('tools/')) {
289+
scriptType = 'Tools Script';
290+
} else if (firstScript?.startsWith('vm/')) {
291+
scriptType = 'VM Script';
292+
} else if (firstScript?.startsWith('vw/')) {
293+
scriptType = 'VW Script';
294+
}
295+
296+
return (
297+
<div className="mx-6 mb-4 p-3 rounded-lg bg-gray-50 text-sm">
298+
<div className="flex items-center space-x-4">
294299
<div className="flex items-center space-x-2">
295-
<div className={`w-2 h-2 rounded-full ${comparisonData.hasDifferences ? 'bg-orange-500' : 'bg-green-500'}`}></div>
296-
<span>Status: {comparisonData.hasDifferences ? 'Update available' : 'Up to date'}</span>
297-
</div>
298-
)}
299-
</div>
300-
{scriptFilesData.files.length > 0 && (
301-
<div className="mt-2 text-xs text-gray-600">
302-
Files: {scriptFilesData.files.join(', ')}
303-
</div>
304-
)}
305-
{scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) &&
306-
comparisonData?.success && comparisonData.hasDifferences && comparisonData.differences.length > 0 && (
307-
<div className="mt-2">
308-
<div className="text-xs text-orange-600 mb-2">
309-
Differences in: {comparisonData.differences.join(', ')}
300+
<div className={`w-2 h-2 rounded-full ${scriptFilesData.ctExists ? 'bg-green-500' : 'bg-gray-300'}`}></div>
301+
<span>{scriptType}: {scriptFilesData.ctExists ? 'Available' : 'Not loaded'}</span>
310302
</div>
311-
<div className="flex flex-wrap gap-2">
312-
{comparisonData.differences.map((filePath, index) => (
313-
<button
314-
key={index}
315-
onClick={() => handleShowDiff(filePath)}
316-
className="px-2 py-1 text-xs bg-orange-100 text-orange-700 rounded hover:bg-orange-200 transition-colors flex items-center space-x-1"
317-
>
318-
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
319-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
320-
</svg>
321-
<span>Show Diff: {filePath}</span>
322-
</button>
323-
))}
303+
<div className="flex items-center space-x-2">
304+
<div className={`w-2 h-2 rounded-full ${scriptFilesData.installExists ? 'bg-green-500' : 'bg-gray-300'}`}></div>
305+
<span>Install Script: {scriptFilesData.installExists ? 'Available' : 'Not loaded'}</span>
324306
</div>
307+
{scriptFilesData?.success && (scriptFilesData.ctExists || scriptFilesData.installExists) && comparisonData?.success && !comparisonLoading && (
308+
<div className="flex items-center space-x-2">
309+
<div className={`w-2 h-2 rounded-full ${comparisonData.hasDifferences ? 'bg-orange-500' : 'bg-green-500'}`}></div>
310+
<span>Status: {comparisonData.hasDifferences ? 'Update available' : 'Up to date'}</span>
311+
</div>
312+
)}
325313
</div>
326-
)}
327-
</div>
328-
)}
314+
{scriptFilesData.files.length > 0 && (
315+
<div className="mt-2 text-xs text-gray-600">
316+
Files: {scriptFilesData.files.join(', ')}
317+
</div>
318+
)}
319+
</div>
320+
);
321+
})()}
329322

330323
{/* Content */}
331324
<div className="p-6 space-y-6">

src/app/page.tsx

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ScriptsGrid } from './_components/ScriptsGrid';
66
import { ResyncButton } from './_components/ResyncButton';
77
import { RepoStatusButton } from './_components/RepoStatusButton';
88
import { Terminal } from './_components/Terminal';
9+
import { ProxmoxCheck } from './_components/ProxmoxCheck';
910

1011
export default function Home() {
1112
const [runningScript, setRunningScript] = useState<{ path: string; name: string } | null>(null);
@@ -19,44 +20,46 @@ export default function Home() {
1920
};
2021

2122
return (
22-
<main className="min-h-screen bg-gray-100">
23-
<div className="container mx-auto px-4 py-8">
24-
{/* Header */}
25-
<div className="text-center mb-8">
26-
<h1 className="text-4xl font-bold text-gray-800 mb-2">
27-
🚀 PVE Scripts Management
28-
</h1>
29-
<p className="text-gray-600">
30-
Manage and execute Proxmox helper scripts locally with live output streaming
31-
</p>
32-
</div>
33-
34-
{/* Repository Status and Update */}
35-
<div className="mb-8">
36-
<RepoStatusButton />
37-
</div>
23+
<ProxmoxCheck>
24+
<main className="min-h-screen bg-gray-100">
25+
<div className="container mx-auto px-4 py-8">
26+
{/* Header */}
27+
<div className="text-center mb-8">
28+
<h1 className="text-4xl font-bold text-gray-800 mb-2">
29+
🚀 PVE Scripts Management
30+
</h1>
31+
<p className="text-gray-600">
32+
Manage and execute Proxmox helper scripts locally with live output streaming
33+
</p>
34+
</div>
3835

39-
{/* Resync Button */}
40-
<div className="mb-8">
41-
<div className="flex items-center justify-between mb-6">
42-
<div></div>
43-
<ResyncButton />
36+
{/* Repository Status and Update */}
37+
<div className="mb-8">
38+
<RepoStatusButton />
4439
</div>
45-
</div>
4640

47-
{/* Running Script Terminal */}
48-
{runningScript && (
41+
{/* Resync Button */}
4942
<div className="mb-8">
50-
<Terminal
51-
scriptPath={runningScript.path}
52-
onClose={handleCloseTerminal}
53-
/>
43+
<div className="flex items-center justify-between mb-6">
44+
<div></div>
45+
<ResyncButton />
46+
</div>
5447
</div>
55-
)}
5648

57-
{/* Scripts List */}
58-
<ScriptsGrid onInstallScript={handleRunScript} />
59-
</div>
60-
</main>
49+
{/* Running Script Terminal */}
50+
{runningScript && (
51+
<div className="mb-8">
52+
<Terminal
53+
scriptPath={runningScript.path}
54+
onClose={handleCloseTerminal}
55+
/>
56+
</div>
57+
)}
58+
59+
{/* Scripts List */}
60+
<ScriptsGrid onInstallScript={handleRunScript} />
61+
</div>
62+
</main>
63+
</ProxmoxCheck>
6164
);
6265
}

src/server/api/routers/scripts.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,5 +283,55 @@ export const scriptsRouter = createTRPCRouter({
283283
diff: null
284284
};
285285
}
286+
}),
287+
288+
// Check if running on Proxmox VE host
289+
checkProxmoxVE: publicProcedure
290+
.query(async () => {
291+
try {
292+
const { spawn } = await import('child_process');
293+
294+
return new Promise((resolve) => {
295+
const child = spawn('command', ['-v', 'pveversion'], {
296+
stdio: ['pipe', 'pipe', 'pipe'],
297+
shell: true
298+
});
299+
300+
301+
child.on('close', (code) => {
302+
// If command exits with code 0, pveversion command exists
303+
if (code === 0) {
304+
resolve({
305+
success: true,
306+
isProxmoxVE: true,
307+
message: 'Running on Proxmox VE host'
308+
});
309+
} else {
310+
resolve({
311+
success: true,
312+
isProxmoxVE: false,
313+
message: 'Not running on Proxmox VE host'
314+
});
315+
}
316+
});
317+
318+
child.on('error', (error) => {
319+
resolve({
320+
success: false,
321+
isProxmoxVE: false,
322+
error: error.message,
323+
message: 'Failed to check Proxmox VE status'
324+
});
325+
});
326+
});
327+
} catch (error) {
328+
console.error('Error in checkProxmoxVE:', error);
329+
return {
330+
success: false,
331+
isProxmoxVE: false,
332+
error: error instanceof Error ? error.message : 'Failed to check Proxmox VE status',
333+
message: 'Failed to check Proxmox VE status'
334+
};
335+
}
286336
})
287337
});

0 commit comments

Comments
 (0)