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

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

View File

@@ -1 +1 @@
0.1.0
0.2.1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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