Skip to content

Commit 24430ee

Browse files
Add web-based update system with detached process management (#65)
* feat: Add version checking and update functionality - Add version display component with GitHub release comparison - Implement update.sh script execution via API - Add hover tooltip with update instructions - Create shadcn/ui style Badge component - Add version router with getCurrentVersion, getLatestRelease, and executeUpdate endpoints - Update homepage header to show version and update status - Add Update Now button with loading states and result feedback - Support automatic page refresh after successful update * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Update update script * Workflow * Workflow * Workflow * Update update script * Update update script * Update update script * Update update script * Update update script * Update update.sh * Update update.sh * Update update.sh * Update update.sh
1 parent 0b1ce29 commit 24430ee

File tree

9 files changed

+1323
-2
lines changed

9 files changed

+1323
-2
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.1
1+
0.1.0

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"better-sqlite3": "^12.4.1",
3737
"class-variance-authority": "^0.7.1",
3838
"clsx": "^2.1.1",
39+
"lucide-react": "^0.545.0",
3940
"next": "^15.5.3",
4041
"node-pty": "^1.0.0",
4142
"react": "^19.0.0",
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
'use client';
2+
3+
import { api } from "~/trpc/react";
4+
import { Badge } from "./ui/badge";
5+
import { Button } from "./ui/button";
6+
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
7+
import { useState } from "react";
8+
9+
// Loading overlay component
10+
function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }) {
11+
return (
12+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
13+
<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">
14+
<div className="flex flex-col items-center space-y-4">
15+
<div className="relative">
16+
<Loader2 className="h-12 w-12 animate-spin text-blue-600 dark:text-blue-400" />
17+
<div className="absolute inset-0 rounded-full border-2 border-blue-200 dark:border-blue-800 animate-pulse"></div>
18+
</div>
19+
<div className="text-center">
20+
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
21+
{isNetworkError ? 'Server Restarting' : 'Updating Application'}
22+
</h3>
23+
<p className="text-sm text-gray-600 dark:text-gray-400">
24+
{isNetworkError
25+
? 'The server is restarting after the update...'
26+
: 'Please stand by while we update your application...'
27+
}
28+
</p>
29+
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">
30+
{isNetworkError
31+
? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!.'
32+
: 'The server will restart automatically when complete.'
33+
}
34+
</p>
35+
</div>
36+
<div className="flex space-x-1">
37+
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce"></div>
38+
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
39+
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
);
45+
}
46+
47+
export function VersionDisplay() {
48+
const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery();
49+
const [isUpdating, setIsUpdating] = useState(false);
50+
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
51+
const [updateStartTime, setUpdateStartTime] = useState<number | null>(null);
52+
const [isNetworkError, setIsNetworkError] = useState(false);
53+
54+
const executeUpdate = api.version.executeUpdate.useMutation({
55+
onSuccess: (result: any) => {
56+
const now = Date.now();
57+
const elapsed = updateStartTime ? now - updateStartTime : 0;
58+
59+
60+
setUpdateResult({ success: result.success, message: result.message });
61+
62+
if (result.success) {
63+
// The script now runs independently, so we show a longer overlay
64+
// and wait for the server to restart
65+
setIsNetworkError(true);
66+
setUpdateResult({ success: true, message: 'Update in progress... Server will restart automatically.' });
67+
68+
// Wait longer for the update to complete and server to restart
69+
setTimeout(() => {
70+
setIsUpdating(false);
71+
setIsNetworkError(false);
72+
// Try to reload after the update completes
73+
setTimeout(() => {
74+
window.location.reload();
75+
}, 10000); // 10 seconds to allow for update completion
76+
}, 5000); // Show overlay for 5 seconds
77+
} else {
78+
// For errors, show for at least 1 second
79+
const remainingTime = Math.max(0, 1000 - elapsed);
80+
setTimeout(() => {
81+
setIsUpdating(false);
82+
}, remainingTime);
83+
}
84+
},
85+
onError: (error) => {
86+
const now = Date.now();
87+
const elapsed = updateStartTime ? now - updateStartTime : 0;
88+
89+
// Check if this is a network error (expected during server restart)
90+
const isNetworkError = error.message.includes('Failed to fetch') ||
91+
error.message.includes('NetworkError') ||
92+
error.message.includes('fetch') ||
93+
error.message.includes('network');
94+
95+
if (isNetworkError && elapsed < 60000) { // If it's a network error within 30 seconds, treat as success
96+
setIsNetworkError(true);
97+
setUpdateResult({ success: true, message: 'Update in progress... Server is restarting.' });
98+
99+
// Wait longer for server to come back up
100+
setTimeout(() => {
101+
setIsUpdating(false);
102+
setIsNetworkError(false);
103+
// Try to reload after a longer delay
104+
setTimeout(() => {
105+
window.location.reload();
106+
}, 5000);
107+
}, 3000);
108+
} else {
109+
// For real errors, show for at least 1 second
110+
setUpdateResult({ success: false, message: error.message });
111+
const remainingTime = Math.max(0, 1000 - elapsed);
112+
setTimeout(() => {
113+
setIsUpdating(false);
114+
}, remainingTime);
115+
}
116+
}
117+
});
118+
119+
const handleUpdate = () => {
120+
setIsUpdating(true);
121+
setUpdateResult(null);
122+
setIsNetworkError(false);
123+
setUpdateStartTime(Date.now());
124+
executeUpdate.mutate();
125+
};
126+
127+
if (isLoading) {
128+
return (
129+
<div className="flex items-center gap-2">
130+
<Badge variant="secondary" className="animate-pulse">
131+
Loading...
132+
</Badge>
133+
</div>
134+
);
135+
}
136+
137+
if (error || !versionStatus?.success) {
138+
return (
139+
<div className="flex items-center gap-2">
140+
<Badge variant="destructive">
141+
v{versionStatus?.currentVersion ?? 'Unknown'}
142+
</Badge>
143+
<span className="text-xs text-muted-foreground">
144+
(Unable to check for updates)
145+
</span>
146+
</div>
147+
);
148+
}
149+
150+
const { currentVersion, isUpToDate, updateAvailable, releaseInfo } = versionStatus;
151+
152+
return (
153+
<>
154+
{/* Loading overlay */}
155+
{isUpdating && <LoadingOverlay isNetworkError={isNetworkError} />}
156+
157+
<div className="flex items-center gap-2">
158+
<Badge variant={isUpToDate ? "default" : "secondary"}>
159+
v{currentVersion}
160+
</Badge>
161+
162+
{updateAvailable && releaseInfo && (
163+
<div className="flex items-center gap-3">
164+
<div className="relative group">
165+
<Badge variant="destructive" className="animate-pulse cursor-help">
166+
Update Available
167+
</Badge>
168+
<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">
169+
<div className="text-center">
170+
<div className="font-semibold mb-1">How to update:</div>
171+
<div>Click the button to update</div>
172+
<div>or update manually:</div>
173+
<div>cd $PVESCRIPTLOCAL_DIR</div>
174+
<div>git pull</div>
175+
<div>npm install</div>
176+
<div>npm run build</div>
177+
<div>npm start</div>
178+
</div>
179+
<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>
180+
</div>
181+
</div>
182+
183+
<Button
184+
onClick={handleUpdate}
185+
disabled={isUpdating}
186+
size="sm"
187+
variant="destructive"
188+
className="text-xs h-6 px-2"
189+
>
190+
{isUpdating ? (
191+
<>
192+
<RefreshCw className="h-3 w-3 mr-1 animate-spin" />
193+
Updating...
194+
</>
195+
) : (
196+
<>
197+
<Download className="h-3 w-3 mr-1" />
198+
Update Now
199+
</>
200+
)}
201+
</Button>
202+
203+
<a
204+
href={releaseInfo.htmlUrl}
205+
target="_blank"
206+
rel="noopener noreferrer"
207+
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
208+
title="View latest release"
209+
>
210+
<ExternalLink className="h-3 w-3" />
211+
</a>
212+
213+
{updateResult && (
214+
<div className={`text-xs px-2 py-1 rounded ${
215+
updateResult.success
216+
? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
217+
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'
218+
}`}>
219+
{updateResult.message}
220+
</div>
221+
)}
222+
</div>
223+
)}
224+
225+
{isUpToDate && (
226+
<span className="text-xs text-green-600 dark:text-green-400">
227+
✓ Up to date
228+
</span>
229+
)}
230+
</div>
231+
</>
232+
);
233+
}

src/app/_components/ui/badge.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from "react"
2+
import { cn } from "~/lib/utils"
3+
4+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
5+
variant?: "default" | "secondary" | "destructive" | "outline"
6+
}
7+
8+
function Badge({ className, variant = "default", ...props }: BadgeProps) {
9+
const variantClasses = {
10+
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
11+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
12+
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
13+
outline: "text-foreground",
14+
}
15+
16+
return (
17+
<div
18+
className={cn(
19+
"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",
20+
variantClasses[variant],
21+
className
22+
)}
23+
{...props}
24+
/>
25+
)
26+
}
27+
28+
export { Badge }

src/app/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { InstalledScriptsTab } from './_components/InstalledScriptsTab';
88
import { ResyncButton } from './_components/ResyncButton';
99
import { Terminal } from './_components/Terminal';
1010
import { SettingsButton } from './_components/SettingsButton';
11+
import { VersionDisplay } from './_components/VersionDisplay';
1112
import { Button } from './_components/ui/button';
1213

1314
export default function Home() {
@@ -30,9 +31,12 @@ export default function Home() {
3031
<h1 className="text-4xl font-bold text-foreground mb-2">
3132
🚀 PVE Scripts Management
3233
</h1>
33-
<p className="text-muted-foreground">
34+
<p className="text-muted-foreground mb-4">
3435
Manage and execute Proxmox helper scripts locally with live output streaming
3536
</p>
37+
<div className="flex justify-center">
38+
<VersionDisplay />
39+
</div>
3640
</div>
3741

3842
{/* Controls */}

src/server/api/root.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { scriptsRouter } from "~/server/api/routers/scripts";
22
import { installedScriptsRouter } from "~/server/api/routers/installedScripts";
33
import { serversRouter } from "~/server/api/routers/servers";
4+
import { versionRouter } from "~/server/api/routers/version";
45
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
56

67
/**
@@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
1213
scripts: scriptsRouter,
1314
installedScripts: installedScriptsRouter,
1415
servers: serversRouter,
16+
version: versionRouter,
1517
});
1618

1719
// export type definition of API

0 commit comments

Comments
 (0)