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