From bf858f0a68db4dfef9073065cd9e18421c511e20 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 13:32:43 +0200 Subject: [PATCH 01/14] feat: implement server color coding feature - Add color column to servers table with migration - Add SERVER_COLOR_CODING_ENABLED environment variable - Create API route for color coding toggle settings - Add color field to Server and CreateServerData types - Update database CRUD operations to handle color field - Update server API routes to handle color field - Create colorUtils.ts with contrast calculation function - Add color coding toggle to GeneralSettingsModal - Add color picker to ServerForm component (only shown when enabled) - Apply colors to InstalledScriptsTab (borders and server column) - Apply colors to ScriptInstallationCard component - Apply colors to ServerList component - Fix 'Local' display issue in installed scripts table --- src/app/_components/GeneralSettingsModal.tsx | 49 ++++++++++++ src/app/_components/InstalledScriptsTab.tsx | 18 ++++- .../_components/ScriptInstallationCard.tsx | 19 ++++- src/app/_components/ServerForm.tsx | 42 ++++++++++- src/app/_components/ServerList.tsx | 12 ++- src/app/api/servers/[id]/route.ts | 5 +- src/app/api/servers/route.ts | 5 +- src/app/api/settings/color-coding/route.ts | 75 +++++++++++++++++++ src/env.js | 4 + src/lib/colorUtils.ts | 35 +++++++++ src/server/database.js | 26 +++++-- src/types/server.ts | 2 + 12 files changed, 270 insertions(+), 22 deletions(-) create mode 100644 src/app/api/settings/color-coding/route.ts create mode 100644 src/lib/colorUtils.ts diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index 66ac083..7d4d4b1 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -15,6 +15,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const [githubToken, setGithubToken] = useState(''); const [saveFilter, setSaveFilter] = useState(false); const [savedFilters, setSavedFilters] = useState(null); + const [colorCodingEnabled, setColorCodingEnabled] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isSaving, setIsSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); @@ -35,6 +36,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr void loadSaveFilter(); void loadSavedFilters(); void loadAuthCredentials(); + void loadColorCodingSetting(); } }, [isOpen]); @@ -148,6 +150,43 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr } }; + const loadColorCodingSetting = async () => { + try { + const response = await fetch('/api/settings/color-coding'); + if (response.ok) { + const data = await response.json(); + setColorCodingEnabled(data.enabled ?? false); + } + } catch (error) { + console.error('Error loading color coding setting:', error); + } + }; + + const saveColorCodingSetting = async (enabled: boolean) => { + try { + const response = await fetch('/api/settings/color-coding', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ enabled }), + }); + + if (response.ok) { + setColorCodingEnabled(enabled); + setMessage({ type: 'success', text: 'Color coding setting saved successfully' }); + setTimeout(() => setMessage(null), 3000); + } else { + setMessage({ type: 'error', text: 'Failed to save color coding setting' }); + setTimeout(() => setMessage(null), 3000); + } + } catch (error) { + console.error('Error saving color coding setting:', error); + setMessage({ type: 'error', text: 'Failed to save color coding setting' }); + setTimeout(() => setMessage(null), 3000); + } + }; + const loadAuthCredentials = async () => { setAuthLoading(true); try { @@ -345,6 +384,16 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr )} + +
+

Server Color Coding

+

Enable color coding for servers to visually distinguish them throughout the application.

+ +
)} diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index fd43668..44a3e78 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -6,6 +6,7 @@ import { Terminal } from './Terminal'; import { StatusBadge } from './Badge'; import { Button } from './ui/button'; import { ScriptInstallationCard } from './ScriptInstallationCard'; +import { getContrastColor } from '../../lib/colorUtils'; interface InstalledScript { id: number; @@ -17,6 +18,7 @@ interface InstalledScript { server_ip: string | null; server_user: string | null; server_password: string | null; + server_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; output_log: string | null; @@ -773,7 +775,11 @@ export function InstalledScriptsTab() { {filteredScripts.map((script) => ( - + {editingScriptId === script.id ? (
@@ -811,8 +817,14 @@ export function InstalledScriptsTab() { )} - - {script.server_name ?? 'Local'} + + {script.server_name ?? '-'} diff --git a/src/app/_components/ScriptInstallationCard.tsx b/src/app/_components/ScriptInstallationCard.tsx index 8a09249..94e2dc9 100644 --- a/src/app/_components/ScriptInstallationCard.tsx +++ b/src/app/_components/ScriptInstallationCard.tsx @@ -2,6 +2,7 @@ import { Button } from './ui/button'; import { StatusBadge } from './Badge'; +import { getContrastColor } from '../../lib/colorUtils'; interface InstalledScript { id: number; @@ -13,6 +14,7 @@ interface InstalledScript { server_ip: string | null; server_user: string | null; server_password: string | null; + server_color: string | null; installation_date: string; status: 'in_progress' | 'success' | 'failed'; output_log: string | null; @@ -50,7 +52,10 @@ export function ScriptInstallationCard({ }; return ( -
+
{/* Header with Script Name and Status */}
@@ -102,9 +107,15 @@ export function ScriptInstallationCard({ {/* Server */}
Server
-
- {script.server_name ?? 'Local'} -
+ + {script.server_name ?? '-'} +
{/* Installation Date */} diff --git a/src/app/_components/ServerForm.tsx b/src/app/_components/ServerForm.tsx index d72129c..a1332f9 100644 --- a/src/app/_components/ServerForm.tsx +++ b/src/app/_components/ServerForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import type { CreateServerData } from '../../types/server'; import { Button } from './ui/button'; import { SSHKeyInput } from './SSHKeyInput'; @@ -23,11 +23,28 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel ssh_key: '', ssh_key_passphrase: '', ssh_port: 22, + color: '#3b82f6', } ); const [errors, setErrors] = useState>>({}); const [sshKeyError, setSshKeyError] = useState(''); + const [colorCodingEnabled, setColorCodingEnabled] = useState(false); + + useEffect(() => { + const loadColorCodingSetting = async () => { + try { + const response = await fetch('/api/settings/color-coding'); + if (response.ok) { + const data = await response.json(); + setColorCodingEnabled(data.enabled ?? false); + } + } catch (error) { + console.error('Error loading color coding setting:', error); + } + }; + void loadColorCodingSetting(); + }, []); const validateForm = (): boolean => { const newErrors: Partial> = {}; @@ -95,7 +112,8 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel auth_type: 'password', ssh_key: '', ssh_key_passphrase: '', - ssh_port: 22 + ssh_port: 22, + color: '#3b82f6' }); } } @@ -206,6 +224,26 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel
+ + {colorCodingEnabled && ( +
+ +
+ + + Choose a color to identify this server + +
+
+ )}
{/* Password Authentication */} diff --git a/src/app/_components/ServerList.tsx b/src/app/_components/ServerList.tsx index f870602..940c11b 100644 --- a/src/app/_components/ServerList.tsx +++ b/src/app/_components/ServerList.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import type { Server, CreateServerData } from '../../types/server'; import { ServerForm } from './ServerForm'; import { Button } from './ui/button'; +import { getContrastColor } from '../../lib/colorUtils'; interface ServerListProps { servers: Server[]; @@ -85,7 +86,11 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) { return (
{servers.map((server) => ( -
+
{editingId === server.id ? (

Edit Server

@@ -95,6 +100,11 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) { ip: server.ip, user: server.user, password: server.password, + auth_type: server.auth_type, + ssh_key: server.ssh_key, + ssh_key_passphrase: server.ssh_key_passphrase, + ssh_port: server.ssh_port, + color: server.color, }} onSubmit={handleUpdate} isEditing={true} diff --git a/src/app/api/servers/[id]/route.ts b/src/app/api/servers/[id]/route.ts index 1ee896c..d74a1c1 100644 --- a/src/app/api/servers/[id]/route.ts +++ b/src/app/api/servers/[id]/route.ts @@ -52,7 +52,7 @@ export async function PUT( } const body = await request.json(); - const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port }: CreateServerData = body; + const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color }: CreateServerData = body; // Validate required fields if (!name || !ip || !user) { @@ -120,7 +120,8 @@ export async function PUT( auth_type: authType, ssh_key, ssh_key_passphrase, - ssh_port: ssh_port ?? 22 + ssh_port: ssh_port ?? 22, + color }); return NextResponse.json( diff --git a/src/app/api/servers/route.ts b/src/app/api/servers/route.ts index 7c6d165..04ed2b6 100644 --- a/src/app/api/servers/route.ts +++ b/src/app/api/servers/route.ts @@ -20,7 +20,7 @@ export async function GET() { export async function POST(request: NextRequest) { try { const body = await request.json(); - const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port }: CreateServerData = body; + const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color }: CreateServerData = body; // Validate required fields if (!name || !ip || !user) { @@ -78,7 +78,8 @@ export async function POST(request: NextRequest) { auth_type: authType, ssh_key, ssh_key_passphrase, - ssh_port: ssh_port ?? 22 + ssh_port: ssh_port ?? 22, + color }); return NextResponse.json( diff --git a/src/app/api/settings/color-coding/route.ts b/src/app/api/settings/color-coding/route.ts new file mode 100644 index 0000000..34f49e5 --- /dev/null +++ b/src/app/api/settings/color-coding/route.ts @@ -0,0 +1,75 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +export async function POST(request: NextRequest) { + try { + const { enabled } = await request.json(); + + if (typeof enabled !== 'boolean') { + return NextResponse.json( + { error: 'Enabled must be a boolean value' }, + { status: 400 } + ); + } + + // Path to the .env file + const envPath = path.join(process.cwd(), '.env'); + + // Read existing .env file + let envContent = ''; + if (fs.existsSync(envPath)) { + envContent = fs.readFileSync(envPath, 'utf8'); + } + + // Check if SERVER_COLOR_CODING_ENABLED already exists + const colorCodingRegex = /^SERVER_COLOR_CODING_ENABLED=.*$/m; + const colorCodingMatch = colorCodingRegex.exec(envContent); + + if (colorCodingMatch) { + // Replace existing SERVER_COLOR_CODING_ENABLED + envContent = envContent.replace(colorCodingRegex, `SERVER_COLOR_CODING_ENABLED=${enabled}`); + } else { + // Add new SERVER_COLOR_CODING_ENABLED + envContent += (envContent.endsWith('\n') ? '' : '\n') + `SERVER_COLOR_CODING_ENABLED=${enabled}\n`; + } + + // Write back to .env file + fs.writeFileSync(envPath, envContent); + + return NextResponse.json({ success: true, message: 'Color coding setting saved successfully' }); + } catch (error) { + console.error('Error saving color coding setting:', error); + return NextResponse.json( + { error: 'Failed to save color coding setting' }, + { status: 500 } + ); + } +} + +export async function GET() { + try { + // Path to the .env file + const envPath = path.join(process.cwd(), '.env'); + + if (!fs.existsSync(envPath)) { + return NextResponse.json({ enabled: false }); + } + + const envContent = fs.readFileSync(envPath, 'utf8'); + + // Extract SERVER_COLOR_CODING_ENABLED + const colorCodingRegex = /^SERVER_COLOR_CODING_ENABLED=(.*)$/m; + const colorCodingMatch = colorCodingRegex.exec(envContent); + const enabled = colorCodingMatch ? colorCodingMatch[1]?.trim().toLowerCase() === 'true' : false; + + return NextResponse.json({ enabled }); + } catch (error) { + console.error('Error reading color coding setting:', error); + return NextResponse.json( + { error: 'Failed to read color coding setting' }, + { status: 500 } + ); + } +} diff --git a/src/env.js b/src/env.js index 0414abb..cb4aae6 100644 --- a/src/env.js +++ b/src/env.js @@ -31,6 +31,8 @@ export const env = createEnv({ AUTH_ENABLED: z.string().optional(), AUTH_SETUP_COMPLETED: z.string().optional(), JWT_SECRET: z.string().optional(), + // Server Color Coding Configuration + SERVER_COLOR_CODING_ENABLED: z.string().optional(), }, /** @@ -68,6 +70,8 @@ export const env = createEnv({ AUTH_ENABLED: process.env.AUTH_ENABLED, AUTH_SETUP_COMPLETED: process.env.AUTH_SETUP_COMPLETED, JWT_SECRET: process.env.JWT_SECRET, + // Server Color Coding Configuration + SERVER_COLOR_CODING_ENABLED: process.env.SERVER_COLOR_CODING_ENABLED, // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, }, /** diff --git a/src/lib/colorUtils.ts b/src/lib/colorUtils.ts new file mode 100644 index 0000000..3e1fbd0 --- /dev/null +++ b/src/lib/colorUtils.ts @@ -0,0 +1,35 @@ +/** + * Calculate the appropriate text color (black or white) for a given background color + * to ensure optimal readability based on luminance + */ +export function getContrastColor(hexColor: string): 'black' | 'white' { + if (!hexColor || hexColor.length !== 7 || !hexColor.startsWith('#')) { + return 'black'; // Default to black for invalid colors + } + + // Remove the # and convert to RGB + const r = parseInt(hexColor.slice(1, 3), 16); + const g = parseInt(hexColor.slice(3, 5), 16); + const b = parseInt(hexColor.slice(5, 7), 16); + + // Calculate relative luminance using the standard formula + // https://www.w3.org/WAI/GL/wiki/Relative_luminance + const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; + + // Return black for light backgrounds, white for dark backgrounds + return luminance > 0.5 ? 'black' : 'white'; +} + +/** + * Check if a color string is a valid hex color + */ +export function isValidHexColor(color: string): boolean { + return /^#[0-9A-F]{6}$/i.test(color); +} + +/** + * Get a default color for servers that don't have one set + */ +export function getDefaultServerColor(): string { + return '#3b82f6'; // Blue-500 from Tailwind +} diff --git a/src/server/database.js b/src/server/database.js index 0aec476..cf100d2 100644 --- a/src/server/database.js +++ b/src/server/database.js @@ -21,6 +21,7 @@ class DatabaseService { ssh_key TEXT, ssh_key_passphrase TEXT, ssh_port INTEGER DEFAULT 22, + color TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) @@ -59,6 +60,14 @@ class DatabaseService { // Column already exists, ignore error } + try { + this.db.exec(` + ALTER TABLE servers ADD COLUMN color TEXT + `); + } catch (e) { + // Column already exists, ignore error + } + // Update existing servers to have auth_type='password' if not set this.db.exec(` UPDATE servers SET auth_type = 'password' WHERE auth_type IS NULL @@ -100,12 +109,12 @@ class DatabaseService { * @param {import('../types/server').CreateServerData} serverData */ createServer(serverData) { - const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port } = serverData; + const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color } = serverData; const stmt = this.db.prepare(` - INSERT INTO servers (name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO servers (name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); - return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22); + return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22, color); } getAllServers() { @@ -126,13 +135,13 @@ class DatabaseService { * @param {import('../types/server').CreateServerData} serverData */ updateServer(id, serverData) { - const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port } = serverData; + const { name, ip, user, password, auth_type, ssh_key, ssh_key_passphrase, ssh_port, color } = serverData; const stmt = this.db.prepare(` UPDATE servers - SET name = ?, ip = ?, user = ?, password = ?, auth_type = ?, ssh_key = ?, ssh_key_passphrase = ?, ssh_port = ? + SET name = ?, ip = ?, user = ?, password = ?, auth_type = ?, ssh_key = ?, ssh_key_passphrase = ?, ssh_port = ?, color = ? WHERE id = ? `); - return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22, id); + return stmt.run(name, ip, user, password, auth_type || 'password', ssh_key, ssh_key_passphrase, ssh_port || 22, color, id); } /** @@ -170,7 +179,8 @@ class DatabaseService { s.name as server_name, s.ip as server_ip, s.user as server_user, - s.password as server_password + s.password as server_password, + s.color as server_color FROM installed_scripts inst LEFT JOIN servers s ON inst.server_id = s.id ORDER BY inst.installation_date DESC diff --git a/src/types/server.ts b/src/types/server.ts index 70fdf2d..a3da080 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -8,6 +8,7 @@ export interface Server { ssh_key?: string; ssh_key_passphrase?: string; ssh_port?: number; + color?: string; created_at: string; updated_at: string; } @@ -21,6 +22,7 @@ export interface CreateServerData { ssh_key?: string; ssh_key_passphrase?: string; ssh_port?: number; + color?: string; } export interface UpdateServerData extends CreateServerData { From 674839ddf6c2108751a718ee7990a9afd90a8a82 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 13:33:59 +0200 Subject: [PATCH 02/14] fix: resolve TypeScript errors in color coding implementation - Fix unsafe argument type errors in GeneralSettingsModal and ServerForm - Remove unused import in ServerList component --- src/app/_components/GeneralSettingsModal.tsx | 2 +- src/app/_components/ServerForm.tsx | 2 +- src/app/_components/ServerList.tsx | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index 7d4d4b1..bfe710f 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -155,7 +155,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const response = await fetch('/api/settings/color-coding'); if (response.ok) { const data = await response.json(); - setColorCodingEnabled(data.enabled ?? false); + setColorCodingEnabled(Boolean(data.enabled)); } } catch (error) { console.error('Error loading color coding setting:', error); diff --git a/src/app/_components/ServerForm.tsx b/src/app/_components/ServerForm.tsx index a1332f9..06111d1 100644 --- a/src/app/_components/ServerForm.tsx +++ b/src/app/_components/ServerForm.tsx @@ -37,7 +37,7 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel const response = await fetch('/api/settings/color-coding'); if (response.ok) { const data = await response.json(); - setColorCodingEnabled(data.enabled ?? false); + setColorCodingEnabled(Boolean(data.enabled)); } } catch (error) { console.error('Error loading color coding setting:', error); diff --git a/src/app/_components/ServerList.tsx b/src/app/_components/ServerList.tsx index 940c11b..2e9b490 100644 --- a/src/app/_components/ServerList.tsx +++ b/src/app/_components/ServerList.tsx @@ -4,7 +4,6 @@ import { useState } from 'react'; import type { Server, CreateServerData } from '../../types/server'; import { ServerForm } from './ServerForm'; import { Button } from './ui/button'; -import { getContrastColor } from '../../lib/colorUtils'; interface ServerListProps { servers: Server[]; From 5680b5a2b0177f7d5874adff5a61cac27f163609 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 13:37:51 +0200 Subject: [PATCH 03/14] feat: add color-coded dropdown for server selection - Create ColorCodedDropdown component with server color indicators - Replace HTML select with custom dropdown in ExecutionModeModal - Add color dots next to server names in dropdown options - Maintain all existing functionality with improved visual design --- src/app/_components/ColorCodedDropdown.tsx | 123 +++++++++++++++++++++ src/app/_components/ExecutionModeModal.tsx | 28 ++--- 2 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 src/app/_components/ColorCodedDropdown.tsx diff --git a/src/app/_components/ColorCodedDropdown.tsx b/src/app/_components/ColorCodedDropdown.tsx new file mode 100644 index 0000000..39b9548 --- /dev/null +++ b/src/app/_components/ColorCodedDropdown.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import type { Server } from '../../types/server'; + +interface ColorCodedDropdownProps { + servers: Server[]; + selectedServer: Server | null; + onServerSelect: (server: Server | null) => void; + placeholder?: string; + disabled?: boolean; +} + +export function ColorCodedDropdown({ + servers, + selectedServer, + onServerSelect, + placeholder = "Select a server...", + disabled = false +}: ColorCodedDropdownProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const handleServerClick = (server: Server) => { + onServerSelect(server); + setIsOpen(false); + }; + + const handleClearSelection = () => { + onServerSelect(null); + setIsOpen(false); + }; + + return ( +
+ {/* Dropdown Button */} + + + {/* Dropdown Menu */} + {isOpen && ( +
+ {/* Clear Selection Option */} + + + {/* Server Options */} + {servers.map((server) => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/app/_components/ExecutionModeModal.tsx b/src/app/_components/ExecutionModeModal.tsx index 6299dea..030383f 100644 --- a/src/app/_components/ExecutionModeModal.tsx +++ b/src/app/_components/ExecutionModeModal.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import type { Server } from '../../types/server'; import { Button } from './ui/button'; +import { ColorCodedDropdown } from './ColorCodedDropdown'; interface ExecutionModeModalProps { isOpen: boolean; @@ -51,6 +52,10 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E onClose(); }; + const handleServerSelect = (server: Server | null) => { + setSelectedServer(server); + }; + const handleModeChange = (mode: 'local' | 'ssh') => { setSelectedMode(mode); if (mode === 'local') { @@ -161,23 +166,12 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E

Add servers in Settings to use SSH execution

) : ( - + )}
)} From dd92ed3d2e8a1a6de24acd0b5afe7ee715241c7b Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 13:41:33 +0200 Subject: [PATCH 04/14] fix: generate new execution ID for each script run - Change executionId from useState to allow updates - Generate new execution ID in startScript function for each run - Fixes issue where scripts couldn't be run multiple times without page reload - Resolves 'Script execution already running' error on subsequent runs --- src/app/_components/Terminal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index f14e36d..7a337a9 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -34,7 +34,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate const xtermRef = useRef(null); const fitAddonRef = useRef(null); const wsRef = useRef(null); - const [executionId] = useState(() => `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`); + const [executionId, setExecutionId] = useState(() => `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`); const isConnectingRef = useRef(false); const hasConnectedRef = useRef(false); @@ -350,11 +350,15 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate const startScript = () => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) { + // Generate a new execution ID for each script run + const newExecutionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + setExecutionId(newExecutionId); + setIsStopped(false); wsRef.current.send(JSON.stringify({ action: 'start', scriptPath, - executionId, + executionId: newExecutionId, mode, server, isUpdate, From 774a176eed1da82df1efee206602923940422d66 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 13:44:49 +0200 Subject: [PATCH 05/14] fix: improve whiptail handling and execution ID generation - Remove premature terminal clearing for whiptail sessions - Let whiptail handle its own display without interference - Generate new execution ID for both initial and manual script runs - Fix whiptail session state management - Should resolve blank screen and script restart issues --- src/app/_components/Terminal.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index 7a337a9..a81eb9d 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -53,19 +53,17 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate break; case 'output': // Write directly to terminal - xterm.js handles ANSI codes natively - // Detect whiptail sessions and clear immediately + // Detect whiptail sessions but don't interfere with the display if (message.data.includes('whiptail') || message.data.includes('dialog') || message.data.includes('Choose an option')) { setInWhiptailSession(true); - // Clear terminal immediately when whiptail starts - xtermRef.current.clear(); - xtermRef.current.write('\x1b[2J\x1b[H'); - } - - // Check for screen clearing sequences and handle them properly - if (message.data.includes('\x1b[2J') || message.data.includes('\x1b[H\x1b[2J')) { - // This is a clear screen sequence, ensure it's processed correctly + // Don't clear the terminal - let whiptail handle its own display + // Just write the data normally xtermRef.current.write(message.data); } else { + // Check if we're exiting a whiptail session + if (inWhiptailSession && (message.data.includes('exited') || message.data.includes('finished') || message.data.includes('completed'))) { + setInWhiptailSession(false); + } // Let xterm handle all ANSI sequences naturally xtermRef.current.write(message.data); } @@ -89,6 +87,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate case 'end': // Reset whiptail session setInWhiptailSession(false); + setIsRunning(false); // Check if this is an LXC creation script const isLxcCreation = scriptPath.includes('ct/') || @@ -107,7 +106,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate } else { xtermRef.current.writeln(`${prefix}✅ ${message.data}`); } - setIsRunning(false); break; } }, [scriptPath, containerId, scriptName]); @@ -298,10 +296,14 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate // Only auto-start on initial connection, not on reconnections if (isInitialConnection && !isRunning) { + // Generate a new execution ID for the initial run + const newExecutionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + setExecutionId(newExecutionId); + const message = { action: 'start', scriptPath, - executionId, + executionId: newExecutionId, mode, server, isUpdate, From 2abdaedf22f7f3a412fad4b6aa1759a607ad16eb Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 13:47:05 +0200 Subject: [PATCH 06/14] fix: revert problematic whiptail changes that broke terminal display - Remove complex whiptail session handling that caused blank screen - Simplify output handling to just write data directly to terminal - Keep execution ID generation fix for multiple script runs - Remove unused inWhiptailSession state variable - Terminal should now display output normally again --- src/app/_components/Terminal.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index a81eb9d..d05a53d 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -27,7 +27,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate const [mobileInput, setMobileInput] = useState(''); const [showMobileInput, setShowMobileInput] = useState(false); const [lastInputSent, setLastInputSent] = useState(null); - const [inWhiptailSession, setInWhiptailSession] = useState(false); const [isMobile, setIsMobile] = useState(false); const [isStopped, setIsStopped] = useState(false); const terminalRef = useRef(null); @@ -53,20 +52,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate break; case 'output': // Write directly to terminal - xterm.js handles ANSI codes natively - // Detect whiptail sessions but don't interfere with the display - if (message.data.includes('whiptail') || message.data.includes('dialog') || message.data.includes('Choose an option')) { - setInWhiptailSession(true); - // Don't clear the terminal - let whiptail handle its own display - // Just write the data normally - xtermRef.current.write(message.data); - } else { - // Check if we're exiting a whiptail session - if (inWhiptailSession && (message.data.includes('exited') || message.data.includes('finished') || message.data.includes('completed'))) { - setInWhiptailSession(false); - } - // Let xterm handle all ANSI sequences naturally - xtermRef.current.write(message.data); - } + xtermRef.current.write(message.data); break; case 'error': // Check if this looks like ANSI terminal output (contains escape codes) @@ -85,8 +71,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate } break; case 'end': - // Reset whiptail session - setInWhiptailSession(false); setIsRunning(false); // Check if this is an LXC creation script From 28d08412cd6d56b074458cdda18b5c2fcdaafe05 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:09:04 +0200 Subject: [PATCH 07/14] fix: remove remaining inWhiptailSession reference - Remove inWhiptailSession from useEffect dependency array - Fixes ReferenceError: inWhiptailSession is not defined - Terminal should now work without JavaScript errors --- src/app/_components/Terminal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index d05a53d..ff7c5f0 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -247,7 +247,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate fitAddonRef.current = null; } }; - }, [executionId, isClient, inWhiptailSession, isMobile]); + }, [executionId, isClient, isMobile]); useEffect(() => { // Prevent multiple connections in React Strict Mode From 2cd25eb4d55953979e45b3935600a9b62476914a Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:10:07 +0200 Subject: [PATCH 08/14] debug: add console logging to terminal message handling - Add debug logs to see what messages are being received - Help diagnose why terminal shows blank screen - Will remove debug logs once issue is identified --- data/settings.db-journal | Bin 0 -> 12824 bytes src/app/_components/Terminal.tsx | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 data/settings.db-journal diff --git a/data/settings.db-journal b/data/settings.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..e4eb89ca56de855e5cce6b2ea6e6490604e062c9 GIT binary patch literal 12824 zcmeI2QE%H+6vyqfX}hIKrh%wf2D`995-n@mltQ(s(z5iXvzEG<>ul?j%bfA9UBb8oL53&MCy z?L7Me&N70KfZesR0gwLA1Ttwq+Np;^_Qz~Ddo%M(W|-MY|DOIKt)^F2e_C}`@1%ZB zeGd+}ApsW zdIn!G@m->((5gy_T7A@@gcp*+Rl+y3LnK+H`&1!KMLMV|$K(k;Ce@aDD9Lc`0hQHK zOpg&N~iPpFXO!|5q-eA8asD*S4Z{aM~%#r+GARK zQW*P5GAWGBC6fQtIM2iG#Ae-mu~;JT)4B#_LHD9F%@=gKuI`03g$BoVSrn5|-IYp- zwC$c90xS;e$>p_jS^V}v5b~L|^GrcM0T!Jw5?6%(Xg99<~V-qDer=kav ze%}0}RD-)0Ht?o<$n%7og4!p3*$mn+Xt&|T3jgq>Zo=KqREz8Za|TT7*{2t%)DIOZ z?aRSXErf#_5EY3+>lDU9jUJ7LdciL~lwq(mC` z4R;WX7>!R98Z-L_G{5igf>9bN(!M`s%=0E(zgkzptJU1W0%PE`MTM8z{RM%*IZayyL(qW4yVMk9^5lRr_Kh31-z$E z7e}26r^snz!B6cHTv^QiDK`CSc6f% { if (!xtermRef.current) return; + console.log('Terminal received message:', message); // Debug log + const timestamp = new Date(message.timestamp).toLocaleTimeString(); const prefix = `[${timestamp}] `; switch (message.type) { case 'start': + console.log('START message:', message.data); // Debug log xtermRef.current.writeln(`${prefix}[START] ${message.data}`); setIsRunning(true); break; case 'output': + console.log('OUTPUT message:', message.data); // Debug log // Write directly to terminal - xterm.js handles ANSI codes natively xtermRef.current.write(message.data); break; From 679afa82ae12551d76b0471d9d74a88426ca43a7 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:11:07 +0200 Subject: [PATCH 09/14] fix: prevent WebSocket reconnection loop - Remove executionId from useEffect dependency arrays - Fixes terminal constantly reconnecting and showing blank screen - WebSocket now maintains stable connection during script execution - Removes debug console logs --- data/settings.db-journal | Bin 12824 -> 0 bytes src/app/_components/Terminal.tsx | 8 ++------ 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 data/settings.db-journal diff --git a/data/settings.db-journal b/data/settings.db-journal deleted file mode 100644 index e4eb89ca56de855e5cce6b2ea6e6490604e062c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12824 zcmeI2QE%H+6vyqfX}hIKrh%wf2D`995-n@mltQ(s(z5iXvzEG<>ul?j%bfA9UBb8oL53&MCy z?L7Me&N70KfZesR0gwLA1Ttwq+Np;^_Qz~Ddo%M(W|-MY|DOIKt)^F2e_C}`@1%ZB zeGd+}ApsW zdIn!G@m->((5gy_T7A@@gcp*+Rl+y3LnK+H`&1!KMLMV|$K(k;Ce@aDD9Lc`0hQHK zOpg&N~iPpFXO!|5q-eA8asD*S4Z{aM~%#r+GARK zQW*P5GAWGBC6fQtIM2iG#Ae-mu~;JT)4B#_LHD9F%@=gKuI`03g$BoVSrn5|-IYp- zwC$c90xS;e$>p_jS^V}v5b~L|^GrcM0T!Jw5?6%(Xg99<~V-qDer=kav ze%}0}RD-)0Ht?o<$n%7og4!p3*$mn+Xt&|T3jgq>Zo=KqREz8Za|TT7*{2t%)DIOZ z?aRSXErf#_5EY3+>lDU9jUJ7LdciL~lwq(mC` z4R;WX7>!R98Z-L_G{5igf>9bN(!M`s%=0E(zgkzptJU1W0%PE`MTM8z{RM%*IZayyL(qW4yVMk9^5lRr_Kh31-z$E z7e}26r^snz!B6cHTv^QiDK`CSc6f% { if (!xtermRef.current) return; - console.log('Terminal received message:', message); // Debug log - const timestamp = new Date(message.timestamp).toLocaleTimeString(); const prefix = `[${timestamp}] `; switch (message.type) { case 'start': - console.log('START message:', message.data); // Debug log xtermRef.current.writeln(`${prefix}[START] ${message.data}`); setIsRunning(true); break; case 'output': - console.log('OUTPUT message:', message.data); // Debug log // Write directly to terminal - xterm.js handles ANSI codes natively xtermRef.current.write(message.data); break; @@ -251,7 +247,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate fitAddonRef.current = null; } }; - }, [executionId, isClient, isMobile]); + }, [isClient, isMobile]); useEffect(() => { // Prevent multiple connections in React Strict Mode @@ -336,7 +332,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate wsRef.current.close(); } }; - }, [scriptPath, executionId, mode, server, isUpdate, containerId, handleMessage, isMobile, isRunning]); + }, [scriptPath, mode, server, isUpdate, containerId, handleMessage, isMobile, isRunning]); const startScript = () => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) { From 9673bcd181be0a96c0f1bb7fac5972cbdbbeff03 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:15:25 +0200 Subject: [PATCH 10/14] fix: prevent WebSocket reconnection on second script run - Remove handleMessage from useEffect dependency array - Fixes loop of START messages and connection blinking on subsequent runs - WebSocket connection now stable for multiple script executions - handleMessage recreation no longer triggers WebSocket reconnection --- data/settings.db-journal | Bin 0 -> 12824 bytes src/app/_components/Terminal.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 data/settings.db-journal diff --git a/data/settings.db-journal b/data/settings.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..013297667a3a98a410d4916d03900152acdd0723 GIT binary patch literal 12824 zcmeI2O>E;t6vyqf`E1kdE{KAH#7KaWu<1wAcDLJ7Xqrq~)6`83m@ z-b}}_Ij+B*7v6XY&aQKu4|do326ps+R^WORynEe0%Ei8mHDWJM{4{Yku^9a=`gK%_ zhQ_}iH^yHZ`+4l)*lOgD$TyL9BiF)@!YAQ$=t<~{&_?jD;J3kg@WsH7fe!=o{$KrH z`OAP}8&iNOz!Z396c8TlN4QrOIR3f0SGl@+pee0st6|kl&9JSRnmyAi^IKLUJDXjY z%FMza%4C-^b4v@iw3cj``k|>>mhj*nMb;ig*4?343mzY<6j>5Q)-FX>WhmCXN3R`< ztTIK`Hbs^=6l>0-*A_)qi6U#0BI})@SXqx=0!7xwP^^rnU+WZEMT#s;kyRLqHS5tU zPm#4Y3~SyS$K2DiW|!u@KCV(^y-ktz)-bHOJDxbMP-MMHk+n>bwM3D1hazi{BJ1{0 ztVK^w+@i=@pvF2cOnnIp^-08@Y|z!YE#Fa?+b zOaZ3Av!=iczL6Q9*0OA+*;MN?d9LOAWsDz*^OkD9ubNhXy#K-P?vylJMfY{{NU_lz zya~ed+$usGH}Vi1hxbEzTL_LWKJDjwmVpPrnEw3v=)@`K58vRzH)20bB%P;1h!nPR8T>r?+k8psA7t6WRIl6`YeWl5%S$j7T9$MiAMw zIub+)uj2|TSA?xxWgl(geUz(8+kyz!ZedYM_iDG4BNa*bo`l40_*6?JR27AtD(+X& z3~z9(ECrcq6A>Ky@7XGMQ-a%P7AY}pO@m9_Jg z0>38>u9%-XAv$Yy`D$MDV?e4Hirp9(UA<*1T1z!$NJl5VE?n&{|DAG@rhSuoqSo5B zrnlrHy?!|@I}y56#SK$auPIF@I1m#jHofE~swghf$(w(bYH;^rw=J)SBu|LRZT-R% zSGN!CS`A*xa}Mt-8r=POdZf2)qixGg{qT}1#qA0f)l z>Pe?~TZF+6L6uM1*Neo~86aeLaqyQ6;P>M%!p8{jO0)L20x z-ka9y%LInSLU|-GdGjW}- { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) { From 91f8b39a354c85e889458ef39ca5b2db42b06e85 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:16:28 +0200 Subject: [PATCH 11/14] debug: add logging to identify WebSocket reconnection cause - Add console logs to useEffect and startScript - Track what dependencies are changing - Identify why WebSocket reconnects on second run --- data/settings.db-journal | Bin 12824 -> 0 bytes src/app/_components/Terminal.tsx | 14 ++++++++++++++ 2 files changed, 14 insertions(+) delete mode 100644 data/settings.db-journal diff --git a/data/settings.db-journal b/data/settings.db-journal deleted file mode 100644 index 013297667a3a98a410d4916d03900152acdd0723..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12824 zcmeI2O>E;t6vyqf`E1kdE{KAH#7KaWu<1wAcDLJ7Xqrq~)6`83m@ z-b}}_Ij+B*7v6XY&aQKu4|do326ps+R^WORynEe0%Ei8mHDWJM{4{Yku^9a=`gK%_ zhQ_}iH^yHZ`+4l)*lOgD$TyL9BiF)@!YAQ$=t<~{&_?jD;J3kg@WsH7fe!=o{$KrH z`OAP}8&iNOz!Z396c8TlN4QrOIR3f0SGl@+pee0st6|kl&9JSRnmyAi^IKLUJDXjY z%FMza%4C-^b4v@iw3cj``k|>>mhj*nMb;ig*4?343mzY<6j>5Q)-FX>WhmCXN3R`< ztTIK`Hbs^=6l>0-*A_)qi6U#0BI})@SXqx=0!7xwP^^rnU+WZEMT#s;kyRLqHS5tU zPm#4Y3~SyS$K2DiW|!u@KCV(^y-ktz)-bHOJDxbMP-MMHk+n>bwM3D1hazi{BJ1{0 ztVK^w+@i=@pvF2cOnnIp^-08@Y|z!YE#Fa?+b zOaZ3Av!=iczL6Q9*0OA+*;MN?d9LOAWsDz*^OkD9ubNhXy#K-P?vylJMfY{{NU_lz zya~ed+$usGH}Vi1hxbEzTL_LWKJDjwmVpPrnEw3v=)@`K58vRzH)20bB%P;1h!nPR8T>r?+k8psA7t6WRIl6`YeWl5%S$j7T9$MiAMw zIub+)uj2|TSA?xxWgl(geUz(8+kyz!ZedYM_iDG4BNa*bo`l40_*6?JR27AtD(+X& z3~z9(ECrcq6A>Ky@7XGMQ-a%P7AY}pO@m9_Jg z0>38>u9%-XAv$Yy`D$MDV?e4Hirp9(UA<*1T1z!$NJl5VE?n&{|DAG@rhSuoqSo5B zrnlrHy?!|@I}y56#SK$auPIF@I1m#jHofE~swghf$(w(bYH;^rw=J)SBu|LRZT-R% zSGN!CS`A*xa}Mt-8r=POdZf2)qixGg{qT}1#qA0f)l z>Pe?~TZF+6L6uM1*Neo~86aeLaqyQ6;P>M%!p8{jO0)L20x z-ka9y%LInSLU|-GdGjW}- { + console.log('WebSocket useEffect triggered with dependencies:', { + scriptPath, + mode, + server: server?.name, + isUpdate, + containerId, + isMobile, + isRunning + }); + // Prevent multiple connections in React Strict Mode if (hasConnectedRef.current || isConnectingRef.current || (wsRef.current && wsRef.current.readyState === WebSocket.OPEN)) { + console.log('Skipping WebSocket connection - already connected or connecting'); return; } @@ -335,9 +346,12 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate }, [scriptPath, mode, server, isUpdate, containerId, isMobile, isRunning]); const startScript = () => { + console.log('startScript called, isRunning:', isRunning, 'wsReadyState:', wsRef.current?.readyState); + if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) { // Generate a new execution ID for each script run const newExecutionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + console.log('Starting script with new executionId:', newExecutionId); setExecutionId(newExecutionId); setIsStopped(false); From 97aef211519cd5124f81c1ca635c92cedb3ce83a Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:17:42 +0200 Subject: [PATCH 12/14] fix: remove isRunning from WebSocket useEffect dependencies - isRunning state change was causing WebSocket reconnection loop - Each script start changed isRunning from false to true - This triggered useEffect to reconnect WebSocket - Removing isRunning from dependencies breaks the loop - WebSocket connection now stable during script execution --- data/settings.db-journal | Bin 0 -> 12824 bytes src/app/_components/Terminal.tsx | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 data/settings.db-journal diff --git a/data/settings.db-journal b/data/settings.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..9c523e30b58c7f5dc78d6ada07df54a04a582ace GIT binary patch literal 12824 zcmeI2L2u(k6vyqPX}d|2th$IEg2YH5CDCS^Cacn=t4fZ`^#u16Hzzq&aTzIx8PTeTdvay6fW3{n8^ZcKAzj<#u zPKV>h$Hgn519!_D7lYHyaRs~jKNDC^gLk)*A9L9svi|g{K0Y-okU<4QeMt~7u1f~)YUVfV7-d*STTX)~%dg_s; z=&N?$?pm5j>~fcsgR+Ee_p58w^*fa{_(7HGW@Tfu@}Z{7re&O1s%;A|FCx@Bk5KC@ zLai4wY26F)dLE&c6QLG~P|J=`%bH1RBgFnRLM<~wEh9oLeI~6BLcE5vXsw0kiNTv| zt#7V}bLiPjTH%=ZBtk7MGOdeOyT>qr&$-_N#InC~+27%V9gF}Yzz8q`i~u9R2rvSS z03*N%FanGKBk*4%@OCU-j%m70l)*sl$@Kfz*k36=p5txRdahdbJl+4nA3SbqL`BDj zHB<rTyV#6{FUn|m!Prw^;_x8IhVMx%-vYd{*=k5 z#Z+R7IV)xa7y(9r5nu!u0Y>0oA@GICFRbKp{FfU%#B8LCLaPbDPo!*8eAL>*Ch z+`(fL%?uliYaD2OTAiub_6x)6O{?!(szP9G2ur?ga7PmMFxtkATBj+YdZ*onUdnFa zfmGXTU1`$nz5Zj=%Mwx?(wE7(NnIValOJ_5kLr(beYY^S6GT)PSC>%!nR=dw(~&{? z`C_qzVEcd!WuAM%o$LyleM_%Kx2yRM6@g&{2`A4A!Z!g5L!yeK&p;cb% zSN^N)^}*?OVe^Q4d8}yg_7m!n;Skdy^1wK`My0Xe#=?&1rE0-XYCu#(ZQQ_+3w3-j zO7((Uye~qsG%-vA2iQ+r>U8n5kLDI~Te-#0WnyV3C#vOm86(q)LMGZ!h3*ecS}+Qu zwy@()8Plv0)vq-qupYy{J6@lI@hrIzD4P4B5^bVy)EHN5*E04p>e;_#={eV^xGKS1Meg8$uu literal 0 HcmV?d00001 diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index 6899dfa..e672d5e 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -266,6 +266,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate return; } + console.log('Creating new WebSocket connection...'); + // Close any existing connection first if (wsRef.current) { wsRef.current.close(); @@ -343,7 +345,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate wsRef.current.close(); } }; - }, [scriptPath, mode, server, isUpdate, containerId, isMobile, isRunning]); + }, [scriptPath, mode, server, isUpdate, containerId, isMobile]); const startScript = () => { console.log('startScript called, isRunning:', isRunning, 'wsReadyState:', wsRef.current?.readyState); From 2d85cc74d13cc64d013512ff822769781ec028dd Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:19:51 +0200 Subject: [PATCH 13/14] feat: preselect SSH mode in execution modal and clean up debug logs - Preselect SSH execution mode by default since it's the only available option - Remove debug console logs from Terminal component - Clean up code for production readiness --- data/settings.db-journal | Bin 12824 -> 0 bytes src/app/_components/ExecutionModeModal.tsx | 2 +- src/app/_components/Terminal.tsx | 16 ---------------- 3 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 data/settings.db-journal diff --git a/data/settings.db-journal b/data/settings.db-journal deleted file mode 100644 index 9c523e30b58c7f5dc78d6ada07df54a04a582ace..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12824 zcmeI2L2u(k6vyqPX}d|2th$IEg2YH5CDCS^Cacn=t4fZ`^#u16Hzzq&aTzIx8PTeTdvay6fW3{n8^ZcKAzj<#u zPKV>h$Hgn519!_D7lYHyaRs~jKNDC^gLk)*A9L9svi|g{K0Y-okU<4QeMt~7u1f~)YUVfV7-d*STTX)~%dg_s; z=&N?$?pm5j>~fcsgR+Ee_p58w^*fa{_(7HGW@Tfu@}Z{7re&O1s%;A|FCx@Bk5KC@ zLai4wY26F)dLE&c6QLG~P|J=`%bH1RBgFnRLM<~wEh9oLeI~6BLcE5vXsw0kiNTv| zt#7V}bLiPjTH%=ZBtk7MGOdeOyT>qr&$-_N#InC~+27%V9gF}Yzz8q`i~u9R2rvSS z03*N%FanGKBk*4%@OCU-j%m70l)*sl$@Kfz*k36=p5txRdahdbJl+4nA3SbqL`BDj zHB<rTyV#6{FUn|m!Prw^;_x8IhVMx%-vYd{*=k5 z#Z+R7IV)xa7y(9r5nu!u0Y>0oA@GICFRbKp{FfU%#B8LCLaPbDPo!*8eAL>*Ch z+`(fL%?uliYaD2OTAiub_6x)6O{?!(szP9G2ur?ga7PmMFxtkATBj+YdZ*onUdnFa zfmGXTU1`$nz5Zj=%Mwx?(wE7(NnIValOJ_5kLr(beYY^S6GT)PSC>%!nR=dw(~&{? z`C_qzVEcd!WuAM%o$LyleM_%Kx2yRM6@g&{2`A4A!Z!g5L!yeK&p;cb% zSN^N)^}*?OVe^Q4d8}yg_7m!n;Skdy^1wK`My0Xe#=?&1rE0-XYCu#(ZQQ_+3w3-j zO7((Uye~qsG%-vA2iQ+r>U8n5kLDI~Te-#0WnyV3C#vOm86(q)LMGZ!h3*ecS}+Qu zwy@()8Plv0)vq-qupYy{J6@lI@hrIzD4P4B5^bVy)EHN5*E04p>e;_#={eV^xGKS1Meg8$uu diff --git a/src/app/_components/ExecutionModeModal.tsx b/src/app/_components/ExecutionModeModal.tsx index 030383f..edbd00b 100644 --- a/src/app/_components/ExecutionModeModal.tsx +++ b/src/app/_components/ExecutionModeModal.tsx @@ -16,7 +16,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E const [servers, setServers] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [selectedMode, setSelectedMode] = useState<'local' | 'ssh'>('local'); + const [selectedMode, setSelectedMode] = useState<'local' | 'ssh'>('ssh'); const [selectedServer, setSelectedServer] = useState(null); useEffect(() => { diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index e672d5e..58bd6dc 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -250,24 +250,11 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate }, [isClient, isMobile]); useEffect(() => { - console.log('WebSocket useEffect triggered with dependencies:', { - scriptPath, - mode, - server: server?.name, - isUpdate, - containerId, - isMobile, - isRunning - }); - // Prevent multiple connections in React Strict Mode if (hasConnectedRef.current || isConnectingRef.current || (wsRef.current && wsRef.current.readyState === WebSocket.OPEN)) { - console.log('Skipping WebSocket connection - already connected or connecting'); return; } - console.log('Creating new WebSocket connection...'); - // Close any existing connection first if (wsRef.current) { wsRef.current.close(); @@ -348,12 +335,9 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate }, [scriptPath, mode, server, isUpdate, containerId, isMobile]); const startScript = () => { - console.log('startScript called, isRunning:', isRunning, 'wsReadyState:', wsRef.current?.readyState); - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) { // Generate a new execution ID for each script run const newExecutionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - console.log('Starting script with new executionId:', newExecutionId); setExecutionId(newExecutionId); setIsStopped(false); From d327a236642529d2ae3263eb2593d7a5383d0a94 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 10 Oct 2025 14:25:41 +0200 Subject: [PATCH 14/14] fix: resolve build errors and warnings - Add missing SettingsModal import to ExecutionModeModal - Remove unused selectedMode and handleModeChange variables - Add ESLint disable comments for intentional useEffect dependency exclusions - Build now passes successfully with no errors or warnings --- src/app/_components/ExecutionModeModal.tsx | 12 +----------- src/app/_components/Terminal.tsx | 4 ++-- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/app/_components/ExecutionModeModal.tsx b/src/app/_components/ExecutionModeModal.tsx index a04b5de..5e11d68 100644 --- a/src/app/_components/ExecutionModeModal.tsx +++ b/src/app/_components/ExecutionModeModal.tsx @@ -3,8 +3,8 @@ import { useState, useEffect } from 'react'; import type { Server } from '../../types/server'; import { Button } from './ui/button'; - import { ColorCodedDropdown } from './ColorCodedDropdown'; +import { SettingsModal } from './SettingsModal'; interface ExecutionModeModalProps { @@ -18,9 +18,6 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E const [servers, setServers] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - - const [selectedMode, setSelectedMode] = useState<'local' | 'ssh'>('ssh'); - const [selectedServer, setSelectedServer] = useState(null); const [hasAutoExecuted, setHasAutoExecuted] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false); @@ -80,13 +77,6 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E setSelectedServer(server); }; - const handleModeChange = (mode: 'local' | 'ssh') => { - setSelectedMode(mode); - if (mode === 'local') { - setSelectedServer(null); - } - }; - if (!isOpen) return null; diff --git a/src/app/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index 138499d..dbe6520 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -247,7 +247,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate fitAddonRef.current = null; } }; - }, [isClient, isMobile]); + }, [isClient, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { // Prevent multiple connections in React Strict Mode @@ -331,7 +331,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate wsRef.current.close(); } }; - }, [scriptPath, mode, server, isUpdate, containerId, isMobile]); + }, [scriptPath, mode, server, isUpdate, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps const startScript = () => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) {