Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d03f196
feat: Add version checking and update functionality
michelroegl-brunner Oct 7, 2025
4677a91
Update update script
michelroegl-brunner Oct 7, 2025
9d77834
Update update script
michelroegl-brunner Oct 7, 2025
ca9abe8
Update update script
michelroegl-brunner Oct 7, 2025
5870654
Update update script
michelroegl-brunner Oct 7, 2025
d02352b
Update update script
michelroegl-brunner Oct 7, 2025
3ce2139
Update update script
michelroegl-brunner Oct 7, 2025
fc78420
Update update script
michelroegl-brunner Oct 7, 2025
d7e699d
Update update script
michelroegl-brunner Oct 7, 2025
4243543
Update update script
michelroegl-brunner Oct 7, 2025
9fc46df
Update update script
michelroegl-brunner Oct 7, 2025
93ab705
Update update script
michelroegl-brunner Oct 7, 2025
5a03bbb
Update update script
michelroegl-brunner Oct 7, 2025
8a1d9d6
Update update script
michelroegl-brunner Oct 7, 2025
f354977
Update update script
michelroegl-brunner Oct 7, 2025
26aa61d
Update update script
michelroegl-brunner Oct 7, 2025
442faac
Update update script
michelroegl-brunner Oct 7, 2025
e21c593
Update update script
michelroegl-brunner Oct 7, 2025
312211a
Update update script
michelroegl-brunner Oct 7, 2025
72d7462
Update update script
michelroegl-brunner Oct 7, 2025
d6de1a3
Update update script
michelroegl-brunner Oct 7, 2025
b545329
Update update script
michelroegl-brunner Oct 7, 2025
f83c34f
Update update script
michelroegl-brunner Oct 7, 2025
f3fe562
Workflow
michelroegl-brunner Oct 7, 2025
0fc1b25
Workflow
michelroegl-brunner Oct 7, 2025
5028c75
Workflow
michelroegl-brunner Oct 7, 2025
441eb74
Update update script
michelroegl-brunner Oct 7, 2025
328b921
Update update script
michelroegl-brunner Oct 7, 2025
4fe68dd
Update update script
michelroegl-brunner Oct 7, 2025
ffa4770
Update update script
michelroegl-brunner Oct 7, 2025
d67540b
Update update script
michelroegl-brunner Oct 7, 2025
16f6b4b
Update update.sh
michelroegl-brunner Oct 7, 2025
4bd21ca
Update update.sh
michelroegl-brunner Oct 7, 2025
136c2bc
Update update.sh
michelroegl-brunner Oct 7, 2025
bd60f26
Update update.sh
michelroegl-brunner Oct 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.1
0.1.0
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"better-sqlite3": "^9.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.545.0",
"next": "^15.5.3",
"node-pty": "^1.0.0",
"react": "^19.0.0",
Expand Down
233 changes: 233 additions & 0 deletions src/app/_components/VersionDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
'use client';

import { api } from "~/trpc/react";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
import { useState } from "react";

// Loading overlay component
function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-2xl border border-gray-200 dark:border-gray-700 max-w-md mx-4">
<div className="flex flex-col items-center space-y-4">
<div className="relative">
<Loader2 className="h-12 w-12 animate-spin text-blue-600 dark:text-blue-400" />
<div className="absolute inset-0 rounded-full border-2 border-blue-200 dark:border-blue-800 animate-pulse"></div>
</div>
<div className="text-center">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
{isNetworkError ? 'Server Restarting' : 'Updating Application'}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{isNetworkError
? 'The server is restarting after the update...'
: 'Please stand by while we update your application...'
}
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">
{isNetworkError
? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!.'
: 'The server will restart automatically when complete.'
}
</p>
</div>
<div className="flex space-x-1">
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
</div>
</div>
</div>
</div>
);
}

export function VersionDisplay() {
const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery();
const [isUpdating, setIsUpdating] = useState(false);
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
const [updateStartTime, setUpdateStartTime] = useState<number | null>(null);
const [isNetworkError, setIsNetworkError] = useState(false);

const executeUpdate = api.version.executeUpdate.useMutation({
onSuccess: (result: any) => {
const now = Date.now();
const elapsed = updateStartTime ? now - updateStartTime : 0;


setUpdateResult({ success: result.success, message: result.message });

if (result.success) {
// The script now runs independently, so we show a longer overlay
// and wait for the server to restart
setIsNetworkError(true);
setUpdateResult({ success: true, message: 'Update in progress... Server will restart automatically.' });

// Wait longer for the update to complete and server to restart
setTimeout(() => {
setIsUpdating(false);
setIsNetworkError(false);
// Try to reload after the update completes
setTimeout(() => {
window.location.reload();
}, 10000); // 10 seconds to allow for update completion
}, 5000); // Show overlay for 5 seconds
} else {
// For errors, show for at least 1 second
const remainingTime = Math.max(0, 1000 - elapsed);
setTimeout(() => {
setIsUpdating(false);
}, remainingTime);
}
},
onError: (error) => {
const now = Date.now();
const elapsed = updateStartTime ? now - updateStartTime : 0;

// Check if this is a network error (expected during server restart)
const isNetworkError = error.message.includes('Failed to fetch') ||
error.message.includes('NetworkError') ||
error.message.includes('fetch') ||
error.message.includes('network');

if (isNetworkError && elapsed < 60000) { // If it's a network error within 30 seconds, treat as success
setIsNetworkError(true);
setUpdateResult({ success: true, message: 'Update in progress... Server is restarting.' });

// Wait longer for server to come back up
setTimeout(() => {
setIsUpdating(false);
setIsNetworkError(false);
// Try to reload after a longer delay
setTimeout(() => {
window.location.reload();
}, 5000);
}, 3000);
} else {
// For real errors, show for at least 1 second
setUpdateResult({ success: false, message: error.message });
const remainingTime = Math.max(0, 1000 - elapsed);
setTimeout(() => {
setIsUpdating(false);
}, remainingTime);
}
}
});

const handleUpdate = () => {
setIsUpdating(true);
setUpdateResult(null);
setIsNetworkError(false);
setUpdateStartTime(Date.now());
executeUpdate.mutate();
};

if (isLoading) {
return (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="animate-pulse">
Loading...
</Badge>
</div>
);
}

if (error || !versionStatus?.success) {
return (
<div className="flex items-center gap-2">
<Badge variant="destructive">
v{versionStatus?.currentVersion ?? 'Unknown'}
</Badge>
<span className="text-xs text-muted-foreground">
(Unable to check for updates)
</span>
</div>
);
}

const { currentVersion, isUpToDate, updateAvailable, releaseInfo } = versionStatus;

return (
<>
{/* Loading overlay */}
{isUpdating && <LoadingOverlay isNetworkError={isNetworkError} />}

<div className="flex items-center gap-2">
<Badge variant={isUpToDate ? "default" : "secondary"}>
v{currentVersion}
</Badge>

{updateAvailable && releaseInfo && (
<div className="flex items-center gap-3">
<div className="relative group">
<Badge variant="destructive" className="animate-pulse cursor-help">
Update Available
</Badge>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 mt-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-10">
<div className="text-center">
<div className="font-semibold mb-1">How to update:</div>
<div>Click the button to update</div>
<div>or update manually:</div>
<div>cd $PVESCRIPTLOCAL_DIR</div>
<div>git pull</div>
<div>npm install</div>
<div>npm run build</div>
<div>npm start</div>
</div>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"></div>
</div>
</div>

<Button
onClick={handleUpdate}
disabled={isUpdating}
size="sm"
variant="destructive"
className="text-xs h-6 px-2"
>
{isUpdating ? (
<>
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
Updating...
</>
) : (
<>
<Download className="h-3 w-3 mr-1" />
Update Now
</>
)}
</Button>

<a
href={releaseInfo.htmlUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
title="View latest release"
>
<ExternalLink className="h-3 w-3" />
</a>

{updateResult && (
<div className={`text-xs px-2 py-1 rounded ${
updateResult.success
? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'
}`}>
{updateResult.message}
</div>
)}
</div>
)}

{isUpToDate && (
<span className="text-xs text-green-600 dark:text-green-400">
✓ Up to date
</span>
)}
</div>
</>
);
}
28 changes: 28 additions & 0 deletions src/app/_components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react"
import { cn } from "~/lib/utils"

export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: "default" | "secondary" | "destructive" | "outline"
}

function Badge({ className, variant = "default", ...props }: BadgeProps) {
const variantClasses = {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
}

return (
<div
className={cn(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
variantClasses[variant],
className
)}
{...props}
/>
)
}

export { Badge }
6 changes: 5 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InstalledScriptsTab } from './_components/InstalledScriptsTab';
import { ResyncButton } from './_components/ResyncButton';
import { Terminal } from './_components/Terminal';
import { SettingsButton } from './_components/SettingsButton';
import { VersionDisplay } from './_components/VersionDisplay';
import { Button } from './_components/ui/button';

export default function Home() {
Expand All @@ -30,9 +31,12 @@ export default function Home() {
<h1 className="text-4xl font-bold text-foreground mb-2">
🚀 PVE Scripts Management
</h1>
<p className="text-muted-foreground">
<p className="text-muted-foreground mb-4">
Manage and execute Proxmox helper scripts locally with live output streaming
</p>
<div className="flex justify-center">
<VersionDisplay />
</div>
</div>

{/* Controls */}
Expand Down
2 changes: 2 additions & 0 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { scriptsRouter } from "~/server/api/routers/scripts";
import { installedScriptsRouter } from "~/server/api/routers/installedScripts";
import { serversRouter } from "~/server/api/routers/servers";
import { versionRouter } from "~/server/api/routers/version";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";

/**
Expand All @@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
scripts: scriptsRouter,
installedScripts: installedScriptsRouter,
servers: serversRouter,
version: versionRouter,
});

// export type definition of API
Expand Down
Loading