Compare commits

...

5 Commits

Author SHA1 Message Date
github-actions[bot]
5283169a98 chore: add VERSION v0.2.1 2025-10-07 14:28:29 +00:00
Michel Roegl-Brunner
6c2868f8b9 chore: replace emojis with Lucide icons (#68)
- Replace all emojis with Lucide React icons for better accessibility and consistency
- Update page header: rocket emoji → Rocket icon
- Update tab navigation: package, hard drive, folder open icons
- Update terminal controls: Play, Square, Trash2, X icons
- Update filter bar: Package, Monitor, Wrench, Server, FileText, Calendar icons
- Update version display: Check icon for up-to-date status
- Replace emoji prefixes in terminal and error messages with text labels
- Remove unused icon imports
2025-10-07 16:27:28 +02:00
Michel Roegl-Brunner
c2705430a3 Enhance README with an illustrative image
Added an image to the README for better visualization.
2025-10-07 16:18:39 +02:00
Michel Roegl-Brunner
fc4c6efa8c Change release drafter to use patch versioning
Updated release drafter configuration to use patch versioning.
2025-10-07 16:17:54 +02:00
github-actions[bot]
8039d5aa96 chore: add VERSION v0.2.0 (#67)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-07 14:14:55 +00:00
9 changed files with 101 additions and 72 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -1 +1 @@
0.1.0 0.2.1

View File

@@ -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

View File

@@ -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);
}, },
}); });

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>