Skip to content

Commit 66f8a84

Browse files
Various small fixes (#349)
* Fix script viewer to support vm/ and tools/ scripts - Update ScriptDetailModal to extract scriptName from any path (ct/, vm/, tools/) - Refactor TextViewer to use actual script paths from install_methods - Remove hardcoded path assumptions and use dynamic script paths - Only show Install Script tab for ct/ scripts that have install scripts - Rename CT Script tab to Script for better clarity * Fix downloaded scripts count to include vm/ and tools/ scripts - Update matching logic to use same robust approach as DownloadedScriptsTab - Add normalized slug matching to handle filename-based slugs vs JSON slugs - Add multiple fallback matching strategies for better script detection - Fixes issue where scripts in vm/ and tools/ directories weren't being counted * Filter categories to only show those with scripts - Add filter to exclude categories with count 0 from category sidebar - Only categories with at least one script will be displayed - Reduces UI clutter by hiding empty categories * Fix intermittent page reloads from VersionDisplay reconnect logic - Add guards to prevent reload when not updating - Use refs to track isUpdating and isNetworkError state in interval callbacks - Add hasReloadedRef flag to prevent multiple reloads - Clear reconnect interval when update completes or component unmounts - Only start reconnect attempts when actually updating - Prevents false positive reloads when server responds normally * Fix Next.js HMR WebSocket and static asset handling - Add WebSocket upgrade detection to only intercept /ws/script-execution - Pass all other WebSocket upgrades (including HMR) to Next.js handler - Ensure _next routes and static assets are properly handled by Next.js - Fixes 400 errors for Next.js HMR WebSocket connections - Fixes 403 errors for static assets by ensuring proper routing * Fix WebSocket upgrade handling to properly route Next.js HMR - Create WebSocketServer with noServer: true to avoid auto-attaching - Manually handle upgrade events to route /ws/script-execution to our WebSocketServer - Route all other WebSocket upgrades (including Next.js HMR) to Next.js handler - This ensures Next.js HMR WebSocket connections are properly handled - Fixes 400 errors for /_next/webpack-hmr WebSocket connections * Revert WebSocket handling to simpler approach - Go back to attaching WebSocketServer directly with path option - Remove manual upgrade event handling that was causing errors - The path option should filter to only /ws/script-execution - Next.js should handle its own HMR WebSocket upgrades naturally * Fix WebSocket upgrade handling to preserve Next.js HMR handlers - Save existing upgrade listeners before adding our own - Call existing listeners for non-matching paths to allow Next.js HMR - Only handle /ws/script-execution ourselves - This ensures Next.js can handle its own WebSocket upgrades for HMR * Fix random page reloads during normal app usage - Memoize startReconnectAttempts with useCallback to prevent recreation on every render - Fix useEffect dependency arrays to include memoized function - Add stricter guards checking refs before starting reconnect attempts - Ensure reconnect logic only runs when actually updating (not during normal usage) - Add early return in fallback useEffect to prevent false triggers - Add ref guards in ResyncButton to prevent multiple simultaneous sync operations - Only reload after sync if it was user-initiated * Fix critical bug: prevent reloads from stale updateLogsData.isComplete - Add isUpdating guard before processing updateLogsData.isComplete - Reset shouldSubscribe when update completes or fails - Prevent stale isComplete data from triggering reloads during normal usage * Add update confirmation modal with changelog display - Add UpdateConfirmationModal component that shows changelog before update - Modify getVersionStatus to include release body (changelog) in response - Update VersionDisplay to show confirmation modal instead of starting update directly - Users must review changelog and click 'Proceed with Update' to start update - Ensures users see potential breaking changes before updating
1 parent 2a9921a commit 66f8a84

File tree

9 files changed

+507
-173
lines changed

9 files changed

+507
-173
lines changed

server.js

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,27 @@ class ScriptExecutionHandler {
7979
* @param {import('http').Server} server
8080
*/
8181
constructor(server) {
82+
// Create WebSocketServer without attaching to server
83+
// We'll handle upgrades manually to avoid interfering with Next.js HMR
8284
this.wss = new WebSocketServer({
83-
server,
84-
path: '/ws/script-execution'
85+
noServer: true
8586
});
8687
this.activeExecutions = new Map();
8788
this.db = getDatabase();
8889
this.setupWebSocket();
8990
}
91+
92+
/**
93+
* Handle WebSocket upgrade for our endpoint
94+
* @param {import('http').IncomingMessage} request
95+
* @param {import('stream').Duplex} socket
96+
* @param {Buffer} head
97+
*/
98+
handleUpgrade(request, socket, head) {
99+
this.wss.handleUpgrade(request, socket, head, (ws) => {
100+
this.wss.emit('connection', ws, request);
101+
});
102+
}
90103

91104
/**
92105
* Parse Container ID from terminal output
@@ -1159,12 +1172,22 @@ app.prepare().then(() => {
11591172
const parsedUrl = parse(req.url || '', true);
11601173
const { pathname, query } = parsedUrl;
11611174

1162-
if (pathname === '/ws/script-execution') {
1175+
// Check if this is a WebSocket upgrade request
1176+
const isWebSocketUpgrade = req.headers.upgrade === 'websocket';
1177+
1178+
// Only intercept WebSocket upgrades for /ws/script-execution
1179+
// Let Next.js handle all other WebSocket upgrades (like HMR) and all HTTP requests
1180+
if (isWebSocketUpgrade && pathname === '/ws/script-execution') {
11631181
// WebSocket upgrade will be handled by the WebSocket server
1182+
// Don't call handle() for this path - let WebSocketServer handle it
11641183
return;
11651184
}
11661185

1167-
// Let Next.js handle all other requests including HMR
1186+
// Let Next.js handle all other requests including:
1187+
// - HTTP requests to /ws/script-execution (non-WebSocket)
1188+
// - WebSocket upgrades to other paths (like /_next/webpack-hmr)
1189+
// - All static assets (_next routes)
1190+
// - All other routes
11681191
await handle(req, res, parsedUrl);
11691192
} catch (err) {
11701193
console.error('Error occurred handling', req.url, err);
@@ -1175,6 +1198,33 @@ app.prepare().then(() => {
11751198

11761199
// Create WebSocket handlers
11771200
const scriptHandler = new ScriptExecutionHandler(httpServer);
1201+
1202+
// Handle WebSocket upgrades manually to avoid interfering with Next.js HMR
1203+
// We need to preserve Next.js's upgrade handlers and call them for non-matching paths
1204+
// Save any existing upgrade listeners (Next.js might have set them up)
1205+
const existingUpgradeListeners = httpServer.listeners('upgrade').slice();
1206+
httpServer.removeAllListeners('upgrade');
1207+
1208+
// Add our upgrade handler that routes based on path
1209+
httpServer.on('upgrade', (request, socket, head) => {
1210+
const parsedUrl = parse(request.url || '', true);
1211+
const { pathname } = parsedUrl;
1212+
1213+
if (pathname === '/ws/script-execution') {
1214+
// Handle our custom WebSocket endpoint
1215+
scriptHandler.handleUpgrade(request, socket, head);
1216+
} else {
1217+
// For all other paths (including Next.js HMR), call existing listeners
1218+
// This allows Next.js to handle its own WebSocket upgrades
1219+
for (const listener of existingUpgradeListeners) {
1220+
try {
1221+
listener.call(httpServer, request, socket, head);
1222+
} catch (err) {
1223+
console.error('Error in upgrade listener:', err);
1224+
}
1225+
}
1226+
}
1227+
});
11781228
// Note: TerminalHandler removed as it's not being used by the current application
11791229

11801230
httpServer

src/app/_components/CategorySidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,10 @@ export function CategorySidebar({
187187
'Miscellaneous': 'box'
188188
};
189189

190-
// Sort categories by count (descending) and then alphabetically
190+
// Filter categories to only show those with scripts, then sort by count (descending) and alphabetically
191191
const sortedCategories = categories
192192
.map(category => [category, categoryCounts[category] ?? 0] as const)
193+
.filter(([, count]) => count > 0) // Only show categories with at least one script
193194
.sort(([a, countA], [b, countB]) => {
194195
if (countB !== countA) return countB - countA;
195196
return a.localeCompare(b);

src/app/_components/ResyncButton.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import { useState } from 'react';
3+
import { useState, useRef } from 'react';
44
import { api } from '~/trpc/react';
55
import { Button } from './ui/button';
66
import { ContextualHelpIcon } from './ContextualHelpIcon';
@@ -9,31 +9,47 @@ export function ResyncButton() {
99
const [isResyncing, setIsResyncing] = useState(false);
1010
const [lastSync, setLastSync] = useState<Date | null>(null);
1111
const [syncMessage, setSyncMessage] = useState<string | null>(null);
12+
const hasReloadedRef = useRef<boolean>(false);
13+
const isUserInitiatedRef = useRef<boolean>(false);
1214

1315
const resyncMutation = api.scripts.resyncScripts.useMutation({
1416
onSuccess: (data) => {
1517
setIsResyncing(false);
1618
setLastSync(new Date());
1719
if (data.success) {
1820
setSyncMessage(data.message ?? 'Scripts synced successfully');
19-
// Reload the page after successful sync
20-
setTimeout(() => {
21-
window.location.reload();
22-
}, 2000); // Wait 2 seconds to show the success message
21+
// Only reload if this was triggered by user action
22+
if (isUserInitiatedRef.current && !hasReloadedRef.current) {
23+
hasReloadedRef.current = true;
24+
setTimeout(() => {
25+
window.location.reload();
26+
}, 2000); // Wait 2 seconds to show the success message
27+
} else {
28+
// Reset flag if reload didn't happen
29+
isUserInitiatedRef.current = false;
30+
}
2331
} else {
2432
setSyncMessage(data.error ?? 'Failed to sync scripts');
2533
// Clear message after 3 seconds for errors
2634
setTimeout(() => setSyncMessage(null), 3000);
35+
isUserInitiatedRef.current = false;
2736
}
2837
},
2938
onError: (error) => {
3039
setIsResyncing(false);
3140
setSyncMessage(`Error: ${error.message}`);
3241
setTimeout(() => setSyncMessage(null), 3000);
42+
isUserInitiatedRef.current = false;
3343
},
3444
});
3545

3646
const handleResync = async () => {
47+
// Prevent multiple simultaneous sync operations
48+
if (isResyncing) return;
49+
50+
// Mark as user-initiated before starting
51+
isUserInitiatedRef.current = true;
52+
hasReloadedRef.current = false;
3753
setIsResyncing(true);
3854
setSyncMessage(null);
3955
resyncMutation.mutate();

src/app/_components/ScriptDetailModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ export function ScriptDetailModal({
882882
<TextViewer
883883
scriptName={
884884
script.install_methods
885-
?.find((method) => method.script?.startsWith("ct/"))
885+
?.find((method) => method.script && (method.script.startsWith("ct/") || method.script.startsWith("vm/") || method.script.startsWith("tools/")))
886886
?.script?.split("/")
887887
.pop() ?? `${script.slug}.sh`
888888
}

0 commit comments

Comments
 (0)