Skip to content

Commit b7fad61

Browse files
Update update.sh
1 parent b267157 commit b7fad61

File tree

3 files changed

+208
-240
lines changed

3 files changed

+208
-240
lines changed

src/app/_components/VersionDisplay.tsx

Lines changed: 122 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@ import { api } from "~/trpc/react";
44
import { Badge } from "./ui/badge";
55
import { Button } from "./ui/button";
66
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
7-
import { useState } from "react";
7+
import { useState, useEffect, useRef } from "react";
8+
9+
// Loading overlay component with log streaming
10+
function LoadingOverlay({
11+
isNetworkError = false,
12+
logs = []
13+
}: {
14+
isNetworkError?: boolean;
15+
logs?: string[];
16+
}) {
17+
const logsEndRef = useRef<HTMLDivElement>(null);
18+
19+
// Auto-scroll to bottom when new logs arrive
20+
useEffect(() => {
21+
logsEndRef.current?.scrollIntoView({ behavior: 'smooth' });
22+
}, [logs]);
823

9-
// Loading overlay component
10-
function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }) {
1124
return (
1225
<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">
26+
<div className="bg-white dark:bg-gray-800 rounded-lg p-8 shadow-2xl border border-gray-200 dark:border-gray-700 max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
1427
<div className="flex flex-col items-center space-y-4">
1528
<div className="relative">
1629
<Loader2 className="h-12 w-12 animate-spin text-blue-600 dark:text-blue-400" />
@@ -28,11 +41,24 @@ function LoadingOverlay({ isNetworkError = false }: { isNetworkError?: boolean }
2841
</p>
2942
<p className="text-xs text-gray-500 dark:text-gray-500 mt-2">
3043
{isNetworkError
31-
? 'This may take a few moments. The page will reload automatically. You may see a blank page for up to a minute!.'
44+
? 'This may take a few moments. The page will reload automatically.'
3245
: 'The server will restart automatically when complete.'
3346
}
3447
</p>
3548
</div>
49+
50+
{/* Log output */}
51+
{logs.length > 0 && (
52+
<div className="w-full mt-4 bg-gray-900 dark:bg-gray-950 rounded-lg p-4 font-mono text-xs text-green-400 max-h-60 overflow-y-auto">
53+
{logs.map((log, index) => (
54+
<div key={index} className="mb-1 whitespace-pre-wrap break-words">
55+
{log}
56+
</div>
57+
))}
58+
<div ref={logsEndRef} />
59+
</div>
60+
)}
61+
3662
<div className="flex space-x-1">
3763
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce"></div>
3864
<div className="w-2 h-2 bg-blue-600 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
@@ -48,79 +74,118 @@ export function VersionDisplay() {
4874
const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery();
4975
const [isUpdating, setIsUpdating] = useState(false);
5076
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
51-
const [updateStartTime, setUpdateStartTime] = useState<number | null>(null);
5277
const [isNetworkError, setIsNetworkError] = useState(false);
78+
const [updateLogs, setUpdateLogs] = useState<string[]>([]);
79+
const [shouldSubscribe, setShouldSubscribe] = useState(false);
80+
const lastLogTimeRef = useRef<number>(Date.now());
81+
const reconnectIntervalRef = useRef<NodeJS.Timeout | null>(null);
5382

5483
const executeUpdate = api.version.executeUpdate.useMutation({
55-
onSuccess: (result: any) => {
56-
const now = Date.now();
57-
const elapsed = updateStartTime ? now - updateStartTime : 0;
58-
59-
84+
onSuccess: (result) => {
6085
setUpdateResult({ success: result.success, message: result.message });
6186

6287
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
88+
// Start subscribing to update logs
89+
setShouldSubscribe(true);
90+
setUpdateLogs(['Update started...']);
7791
} 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);
92+
setIsUpdating(false);
8393
}
8494
},
8595
onError: (error) => {
86-
const now = Date.now();
87-
const elapsed = updateStartTime ? now - updateStartTime : 0;
96+
setUpdateResult({ success: false, message: error.message });
97+
setIsUpdating(false);
98+
}
99+
});
100+
101+
// Subscribe to update progress
102+
api.version.streamUpdateProgress.useSubscription(undefined, {
103+
enabled: shouldSubscribe,
104+
onData: (data) => {
105+
lastLogTimeRef.current = Date.now();
88106

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');
107+
if (data.type === 'log') {
108+
setUpdateLogs(prev => [...prev, data.message]);
109+
} else if (data.type === 'complete') {
110+
setUpdateLogs(prev => [...prev, 'Update complete! Server restarting...']);
111+
setIsNetworkError(true);
112+
} else if (data.type === 'error') {
113+
setUpdateLogs(prev => [...prev, `Error: ${data.message}`]);
114+
}
115+
},
116+
onError: (error) => {
117+
// Connection lost - likely server restarted
118+
console.log('Update stream connection lost, server likely restarting');
119+
setIsNetworkError(true);
120+
setUpdateLogs(prev => [...prev, 'Connection lost - server restarting...']);
121+
},
122+
});
123+
124+
// Monitor for server connection loss and auto-reload
125+
useEffect(() => {
126+
if (!shouldSubscribe) return;
127+
128+
// Check if logs have stopped coming for a while
129+
const checkInterval = setInterval(() => {
130+
const timeSinceLastLog = Date.now() - lastLogTimeRef.current;
94131

95-
if (isNetworkError && elapsed < 60000) { // If it's a network error within 30 seconds, treat as success
132+
// If no logs for 3 seconds and we're updating, assume server is restarting
133+
if (timeSinceLastLog > 3000 && isUpdating) {
96134
setIsNetworkError(true);
97-
setUpdateResult({ success: true, message: 'Update in progress... Server is restarting.' });
135+
setUpdateLogs(prev => [...prev, 'Server restarting... waiting for reconnection...']);
98136

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
137+
// Start trying to reconnect
138+
startReconnectAttempts();
139+
}
140+
}, 1000);
141+
142+
return () => clearInterval(checkInterval);
143+
}, [shouldSubscribe, isUpdating]);
144+
145+
// Attempt to reconnect and reload page when server is back
146+
const startReconnectAttempts = () => {
147+
if (reconnectIntervalRef.current) return;
148+
149+
setUpdateLogs(prev => [...prev, 'Attempting to reconnect...']);
150+
151+
reconnectIntervalRef.current = setInterval(async () => {
152+
try {
153+
// Try to fetch the root path to check if server is back
154+
const response = await fetch('/', { method: 'HEAD' });
155+
if (response.ok || response.status === 200) {
156+
setUpdateLogs(prev => [...prev, 'Server is back online! Reloading...']);
157+
158+
// Clear interval and reload
159+
if (reconnectIntervalRef.current) {
160+
clearInterval(reconnectIntervalRef.current);
161+
}
162+
104163
setTimeout(() => {
105164
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);
165+
}, 1000);
166+
}
167+
} catch {
168+
// Server still down, keep trying
115169
}
116-
}
117-
});
170+
}, 2000);
171+
};
172+
173+
// Cleanup reconnect interval on unmount
174+
useEffect(() => {
175+
return () => {
176+
if (reconnectIntervalRef.current) {
177+
clearInterval(reconnectIntervalRef.current);
178+
}
179+
};
180+
}, []);
118181

119182
const handleUpdate = () => {
120183
setIsUpdating(true);
121184
setUpdateResult(null);
122185
setIsNetworkError(false);
123-
setUpdateStartTime(Date.now());
186+
setUpdateLogs([]);
187+
setShouldSubscribe(false);
188+
lastLogTimeRef.current = Date.now();
124189
executeUpdate.mutate();
125190
};
126191

@@ -152,7 +217,7 @@ export function VersionDisplay() {
152217
return (
153218
<>
154219
{/* Loading overlay */}
155-
{isUpdating && <LoadingOverlay isNetworkError={isNetworkError} />}
220+
{isUpdating && <LoadingOverlay isNetworkError={isNetworkError} logs={updateLogs} />}
156221

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

src/server/api/routers/version.ts

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
2-
import { readFile } from "fs/promises";
2+
import { readFile, writeFile } from "fs/promises";
33
import { join } from "path";
44
import { spawn } from "child_process";
55
import { env } from "~/env";
6+
import { observable } from '@trpc/server/observable';
7+
import { existsSync, statSync } from "fs";
68

79
interface GitHubRelease {
810
tag_name: string;
@@ -125,21 +127,78 @@ export const versionRouter = createTRPCRouter({
125127
}
126128
}),
127129

130+
// Stream update progress by monitoring the log file
131+
streamUpdateProgress: publicProcedure
132+
.subscription(() => {
133+
return observable<{ type: 'log' | 'complete' | 'error'; message: string }>((emit) => {
134+
const logPath = join(process.cwd(), 'update.log');
135+
let lastSize = 0;
136+
let checkInterval: NodeJS.Timeout;
137+
138+
const checkLogFile = async () => {
139+
try {
140+
if (!existsSync(logPath)) {
141+
return;
142+
}
143+
144+
const stats = statSync(logPath);
145+
const currentSize = stats.size;
146+
147+
if (currentSize > lastSize) {
148+
// Read only the new content
149+
const fileHandle = await readFile(logPath, 'utf-8');
150+
const newContent = fileHandle.slice(lastSize);
151+
lastSize = currentSize;
152+
153+
// Emit new log lines
154+
const lines = newContent.split('\n').filter(line => line.trim());
155+
for (const line of lines) {
156+
emit.next({ type: 'log', message: line });
157+
}
158+
}
159+
} catch (error) {
160+
// File might be being written to, ignore errors
161+
}
162+
};
163+
164+
// Start monitoring the log file
165+
checkInterval = setInterval(checkLogFile, 500);
166+
167+
// Also check immediately
168+
checkLogFile().catch(console.error);
169+
170+
// Cleanup function
171+
return () => {
172+
clearInterval(checkInterval);
173+
};
174+
});
175+
}),
176+
128177
// Execute update script
129178
executeUpdate: publicProcedure
130179
.mutation(async () => {
131180
try {
132181
const updateScriptPath = join(process.cwd(), 'update.sh');
182+
const logPath = join(process.cwd(), 'update.log');
183+
184+
// Clear/create the log file
185+
await writeFile(logPath, '', 'utf-8');
133186

134187
// Spawn the update script as a detached process using nohup
135188
// This allows it to run independently and kill the parent Node.js process
136-
const child = spawn('nohup', ['bash', updateScriptPath], {
189+
// Redirect output to log file
190+
const child = spawn('bash', [updateScriptPath], {
137191
cwd: process.cwd(),
138-
stdio: ['ignore', 'ignore', 'ignore'],
192+
stdio: ['ignore', 'pipe', 'pipe'],
139193
shell: false,
140194
detached: true
141195
});
142196

197+
// Capture stdout and stderr to log file
198+
const logStream = require('fs').createWriteStream(logPath, { flags: 'a' });
199+
child.stdout?.pipe(logStream);
200+
child.stderr?.pipe(logStream);
201+
143202
// Unref the child process so it doesn't keep the parent alive
144203
child.unref();
145204

0 commit comments

Comments
 (0)