Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5283169a98 | ||
|
|
6c2868f8b9 | ||
|
|
c2705430a3 | ||
|
|
fc4c6efa8c | ||
|
|
8039d5aa96 |
4
.github/release-drafter.yml
vendored
4
.github/release-drafter.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# Template for release drafts
|
||||
name-template: 'v$NEXT_MINOR_VERSION' # You can switch to $NEXT_MINOR_VERSION or $NEXT_MAJOR_VERSION
|
||||
tag-template: 'v$NEXT_MINOR_VERSION'
|
||||
name-template: 'v$NEXT_PATCH_VERSION' # You can switch to $NEXT_MINOR_VERSION or $NEXT_MAJOR_VERSION
|
||||
tag-template: 'v$NEXT_PATCH_VERSION'
|
||||
|
||||
# Exclude PRs with this label from release notes
|
||||
exclude-labels:
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
A modern web-based management interface for Proxmox VE (PVE) helper scripts. This tool provides a user-friendly way to discover, download, and execute community-sourced Proxmox scripts locally with real-time terminal output streaming. No more need for curl -> bash calls, it all happens in your enviroment.
|
||||
|
||||
|
||||
<img width="1725" height="1088" alt="image" src="https://github.com/user-attachments/assets/75323765-7375-4346-a41e-08d219275248" />
|
||||
|
||||
|
||||
|
||||
## 🎯 Deployment Options
|
||||
|
||||
This application can be deployed in multiple ways to suit different environments:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Package, Monitor, Wrench, Server, FileText, Calendar } from "lucide-react";
|
||||
|
||||
export interface FilterState {
|
||||
searchQuery: string;
|
||||
@@ -20,10 +21,10 @@ interface FilterBarProps {
|
||||
}
|
||||
|
||||
const SCRIPT_TYPES = [
|
||||
{ value: "ct", label: "LXC Container", icon: "📦" },
|
||||
{ value: "vm", label: "Virtual Machine", icon: "💻" },
|
||||
{ value: "addon", label: "Add-on", icon: "🔧" },
|
||||
{ value: "pve", label: "PVE Host", icon: "🖥️" },
|
||||
{ value: "ct", label: "LXC Container", Icon: Package },
|
||||
{ value: "vm", label: "Virtual Machine", Icon: Monitor },
|
||||
{ value: "addon", label: "Add-on", Icon: Wrench },
|
||||
{ value: "pve", label: "PVE Host", Icon: Server },
|
||||
];
|
||||
|
||||
export function FilterBar({
|
||||
@@ -183,38 +184,41 @@ export function FilterBar({
|
||||
{isTypeDropdownOpen && (
|
||||
<div className="absolute top-full left-0 z-10 mt-1 w-48 rounded-lg border border-border bg-card shadow-lg">
|
||||
<div className="p-2">
|
||||
{SCRIPT_TYPES.map((type) => (
|
||||
<label
|
||||
key={type.value}
|
||||
className="flex cursor-pointer items-center space-x-3 rounded-md px-3 py-2 hover:bg-accent"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.selectedTypes.includes(type.value)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
updateFilters({
|
||||
selectedTypes: [
|
||||
...filters.selectedTypes,
|
||||
type.value,
|
||||
],
|
||||
});
|
||||
} else {
|
||||
updateFilters({
|
||||
selectedTypes: filters.selectedTypes.filter(
|
||||
(t) => t !== type.value,
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="rounded border-input text-primary focus:ring-primary"
|
||||
/>
|
||||
<span className="text-lg">{type.icon}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{type.label}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{SCRIPT_TYPES.map((type) => {
|
||||
const IconComponent = type.Icon;
|
||||
return (
|
||||
<label
|
||||
key={type.value}
|
||||
className="flex cursor-pointer items-center space-x-3 rounded-md px-3 py-2 hover:bg-accent"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.selectedTypes.includes(type.value)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
updateFilters({
|
||||
selectedTypes: [
|
||||
...filters.selectedTypes,
|
||||
type.value,
|
||||
],
|
||||
});
|
||||
} else {
|
||||
updateFilters({
|
||||
selectedTypes: filters.selectedTypes.filter(
|
||||
(t) => t !== type.value,
|
||||
),
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="rounded border-input text-primary focus:ring-primary"
|
||||
/>
|
||||
<IconComponent className="h-4 w-4" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{type.label}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="border-t border-border p-2">
|
||||
<Button
|
||||
@@ -236,16 +240,25 @@ export function FilterBar({
|
||||
{/* Sort Options */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Sort By Dropdown */}
|
||||
<select
|
||||
value={filters.sortBy}
|
||||
onChange={(e) =>
|
||||
updateFilters({ sortBy: e.target.value as "name" | "created" })
|
||||
}
|
||||
className="rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-primary focus:outline-none"
|
||||
>
|
||||
<option value="name">📝 By Name</option>
|
||||
<option value="created">📅 By Created Date</option>
|
||||
</select>
|
||||
<div className="relative inline-flex items-center">
|
||||
<select
|
||||
value={filters.sortBy}
|
||||
onChange={(e) =>
|
||||
updateFilters({ sortBy: e.target.value as "name" | "created" })
|
||||
}
|
||||
className="rounded-lg border border-input bg-background pl-9 pr-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-primary focus:outline-none appearance-none"
|
||||
>
|
||||
<option value="name">By Name</option>
|
||||
<option value="created">By Created Date</option>
|
||||
</select>
|
||||
<div className="absolute left-2 pointer-events-none">
|
||||
{filters.sortBy === "name" ? (
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort Order Button */}
|
||||
<Button
|
||||
|
||||
@@ -63,20 +63,20 @@ export function ScriptDetailModal({
|
||||
if (data.success) {
|
||||
const message =
|
||||
"message" in data ? data.message : "Script loaded successfully";
|
||||
setLoadMessage(`✅ ${message}`);
|
||||
setLoadMessage(`[SUCCESS] ${message}`);
|
||||
// Refetch script files status and comparison data to update the UI
|
||||
void refetchScriptFiles();
|
||||
void refetchComparison();
|
||||
} else {
|
||||
const error = "error" in data ? data.error : "Failed to load script";
|
||||
setLoadMessage(`❌ ${error}`);
|
||||
setLoadMessage(`[ERROR] ${error}`);
|
||||
}
|
||||
// Clear message after 5 seconds
|
||||
setTimeout(() => setLoadMessage(null), 5000);
|
||||
},
|
||||
onError: (error) => {
|
||||
setIsLoading(false);
|
||||
setLoadMessage(`❌ Error: ${error.message}`);
|
||||
setLoadMessage(`[ERROR] ${error.message}`);
|
||||
setTimeout(() => setLoadMessage(null), 5000);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { Button } from './ui/button';
|
||||
import { Play, Square, Trash2, X } from 'lucide-react';
|
||||
|
||||
interface TerminalProps {
|
||||
scriptPath: string;
|
||||
@@ -215,7 +216,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
|
||||
switch (message.type) {
|
||||
case 'start':
|
||||
xtermRef.current.writeln(`${prefix}🚀 ${message.data}`);
|
||||
xtermRef.current.writeln(`${prefix}[START] ${message.data}`);
|
||||
setIsRunning(true);
|
||||
break;
|
||||
case 'output':
|
||||
@@ -232,14 +233,14 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
xtermRef.current.write(message.data);
|
||||
} else if (message.data.includes('exit code') && message.data.includes('clear')) {
|
||||
// This is a script error, show it with error prefix
|
||||
xtermRef.current.writeln(`${prefix}❌ ${message.data}`);
|
||||
xtermRef.current.writeln(`${prefix}[ERROR] ${message.data}`);
|
||||
} else {
|
||||
// This is a real error, show it with error prefix
|
||||
xtermRef.current.writeln(`${prefix}❌ ${message.data}`);
|
||||
xtermRef.current.writeln(`${prefix}[ERROR] ${message.data}`);
|
||||
}
|
||||
break;
|
||||
case 'end':
|
||||
xtermRef.current.writeln(`${prefix}✅ ${message.data}`);
|
||||
xtermRef.current.writeln(`${prefix}[SUCCESS] ${message.data}`);
|
||||
setIsRunning(false);
|
||||
break;
|
||||
}
|
||||
@@ -337,7 +338,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
size="sm"
|
||||
className={isConnected && !isRunning ? 'bg-green-600 hover:bg-green-700' : 'bg-muted text-muted-foreground cursor-not-allowed'}
|
||||
>
|
||||
▶️ Start
|
||||
<Play className="h-4 w-4 mr-1" />
|
||||
Start
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -347,7 +349,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
size="sm"
|
||||
className={isRunning ? 'bg-red-600 hover:bg-red-700' : 'bg-muted text-muted-foreground cursor-not-allowed'}
|
||||
>
|
||||
⏹️ Stop
|
||||
<Square className="h-4 w-4 mr-1" />
|
||||
Stop
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -356,7 +359,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
size="sm"
|
||||
className="bg-secondary text-secondary-foreground hover:bg-secondary/80"
|
||||
>
|
||||
🗑️ Clear
|
||||
<Trash2 className="h-4 w-4 mr-1" />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -366,7 +370,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
size="sm"
|
||||
className="bg-gray-600 text-white hover:bg-gray-700"
|
||||
>
|
||||
✕ Close
|
||||
<X className="h-4 w-4 mr-1" />
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { api } from "~/trpc/react";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
|
||||
import { ExternalLink, Download, RefreshCw, Loader2, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
// Loading overlay component
|
||||
@@ -223,8 +223,9 @@ export function VersionDisplay() {
|
||||
)}
|
||||
|
||||
{isUpToDate && (
|
||||
<span className="text-xs text-green-600 dark:text-green-400">
|
||||
✓ Up to date
|
||||
<span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
|
||||
<Check className="h-3 w-3" />
|
||||
Up to date
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ const handler = (req: NextRequest) =>
|
||||
env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
`[ERROR] tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Terminal } from './_components/Terminal';
|
||||
import { SettingsButton } from './_components/SettingsButton';
|
||||
import { VersionDisplay } from './_components/VersionDisplay';
|
||||
import { Button } from './_components/ui/button';
|
||||
import { Rocket, Package, HardDrive, FolderOpen } from 'lucide-react';
|
||||
|
||||
export default function Home() {
|
||||
const [runningScript, setRunningScript] = useState<{ path: string; name: string; mode?: 'local' | 'ssh'; server?: any } | null>(null);
|
||||
@@ -28,8 +29,9 @@ export default function Home() {
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-foreground mb-2">
|
||||
🚀 PVE Scripts Management
|
||||
<h1 className="text-4xl font-bold text-foreground mb-2 flex items-center justify-center gap-3">
|
||||
<Rocket className="h-9 w-9" />
|
||||
PVE Scripts Management
|
||||
</h1>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Manage and execute Proxmox helper scripts locally with live output streaming
|
||||
@@ -59,34 +61,37 @@ export default function Home() {
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('scripts')}
|
||||
className={`px-3 py-1 text-sm ${
|
||||
className={`px-3 py-1 text-sm flex items-center gap-2 ${
|
||||
activeTab === 'scripts'
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent hover:text-accent-foreground'
|
||||
}`}>
|
||||
📦 Available Scripts
|
||||
<Package className="h-4 w-4" />
|
||||
Available Scripts
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('downloaded')}
|
||||
className={`px-3 py-1 text-sm ${
|
||||
className={`px-3 py-1 text-sm flex items-center gap-2 ${
|
||||
activeTab === 'downloaded'
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent hover:text-accent-foreground'
|
||||
}`}>
|
||||
💾 Downloaded Scripts
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Downloaded Scripts
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="null"
|
||||
onClick={() => setActiveTab('installed')}
|
||||
className={`px-3 py-1 text-sm ${
|
||||
className={`px-3 py-1 text-sm flex items-center gap-2 ${
|
||||
activeTab === 'installed'
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'hover:bg-accent hover:text-accent-foreground'
|
||||
}`}>
|
||||
🗂️ Installed Scripts
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
Installed Scripts
|
||||
</Button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user