Skip to content

Commit 4faa74b

Browse files
feat: Server Color Coding System (#103)
* 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 * fix: resolve TypeScript errors in color coding implementation - Fix unsafe argument type errors in GeneralSettingsModal and ServerForm - Remove unused import in ServerList component * 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 * 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 * 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 * 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 * fix: remove remaining inWhiptailSession reference - Remove inWhiptailSession from useEffect dependency array - Fixes ReferenceError: inWhiptailSession is not defined - Terminal should now work without JavaScript errors * 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 * 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 * 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 * 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 * 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 * 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 * 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
1 parent aa9e155 commit 4faa74b

File tree

15 files changed

+421
-64
lines changed

15 files changed

+421
-64
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
'use client';
2+
3+
import { useState, useRef, useEffect } from 'react';
4+
import type { Server } from '../../types/server';
5+
6+
interface ColorCodedDropdownProps {
7+
servers: Server[];
8+
selectedServer: Server | null;
9+
onServerSelect: (server: Server | null) => void;
10+
placeholder?: string;
11+
disabled?: boolean;
12+
}
13+
14+
export function ColorCodedDropdown({
15+
servers,
16+
selectedServer,
17+
onServerSelect,
18+
placeholder = "Select a server...",
19+
disabled = false
20+
}: ColorCodedDropdownProps) {
21+
const [isOpen, setIsOpen] = useState(false);
22+
const dropdownRef = useRef<HTMLDivElement>(null);
23+
24+
// Close dropdown when clicking outside
25+
useEffect(() => {
26+
const handleClickOutside = (event: MouseEvent) => {
27+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
28+
setIsOpen(false);
29+
}
30+
};
31+
32+
document.addEventListener('mousedown', handleClickOutside);
33+
return () => {
34+
document.removeEventListener('mousedown', handleClickOutside);
35+
};
36+
}, []);
37+
38+
const handleServerClick = (server: Server) => {
39+
onServerSelect(server);
40+
setIsOpen(false);
41+
};
42+
43+
const handleClearSelection = () => {
44+
onServerSelect(null);
45+
setIsOpen(false);
46+
};
47+
48+
return (
49+
<div className="relative" ref={dropdownRef}>
50+
{/* Dropdown Button */}
51+
<button
52+
type="button"
53+
onClick={() => !disabled && setIsOpen(!isOpen)}
54+
disabled={disabled}
55+
className={`w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary bg-background text-foreground text-left flex items-center justify-between ${
56+
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-accent'
57+
}`}
58+
>
59+
<span className="truncate">
60+
{selectedServer ? (
61+
<span className="flex items-center gap-2">
62+
{selectedServer.color && (
63+
<span
64+
className="w-3 h-3 rounded-full flex-shrink-0"
65+
style={{ backgroundColor: selectedServer.color }}
66+
/>
67+
)}
68+
{selectedServer.name} ({selectedServer.ip}) - {selectedServer.user}
69+
</span>
70+
) : (
71+
placeholder
72+
)}
73+
</span>
74+
<svg
75+
className={`w-4 h-4 flex-shrink-0 transition-transform ${isOpen ? 'rotate-180' : ''}`}
76+
fill="none"
77+
stroke="currentColor"
78+
viewBox="0 0 24 24"
79+
>
80+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
81+
</svg>
82+
</button>
83+
84+
{/* Dropdown Menu */}
85+
{isOpen && (
86+
<div className="absolute z-50 w-full mt-1 bg-card border border-border rounded-md shadow-lg max-h-60 overflow-auto">
87+
{/* Clear Selection Option */}
88+
<button
89+
type="button"
90+
onClick={handleClearSelection}
91+
className="w-full px-3 py-2 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
92+
>
93+
{placeholder}
94+
</button>
95+
96+
{/* Server Options */}
97+
{servers.map((server) => (
98+
<button
99+
key={server.id}
100+
type="button"
101+
onClick={() => handleServerClick(server)}
102+
className={`w-full px-3 py-2 text-left text-sm transition-colors flex items-center gap-2 ${
103+
selectedServer?.id === server.id
104+
? 'bg-accent text-accent-foreground'
105+
: 'text-foreground hover:bg-accent hover:text-foreground'
106+
}`}
107+
>
108+
{server.color && (
109+
<span
110+
className="w-3 h-3 rounded-full flex-shrink-0"
111+
style={{ backgroundColor: server.color }}
112+
/>
113+
)}
114+
<span className="truncate">
115+
{server.name} ({server.ip}) - {server.user}
116+
</span>
117+
</button>
118+
))}
119+
</div>
120+
)}
121+
</div>
122+
);
123+
}

src/app/_components/ExecutionModeModal.tsx

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import { useState, useEffect } from 'react';
44
import type { Server } from '../../types/server';
55
import { Button } from './ui/button';
6+
import { ColorCodedDropdown } from './ColorCodedDropdown';
67
import { SettingsModal } from './SettingsModal';
78

9+
810
interface ExecutionModeModalProps {
911
isOpen: boolean;
1012
onClose: () => void;
@@ -70,6 +72,12 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
7072
onClose();
7173
};
7274

75+
76+
const handleServerSelect = (server: Server | null) => {
77+
setSelectedServer(server);
78+
};
79+
80+
7381
if (!isOpen) return null;
7482

7583
return (
@@ -138,23 +146,12 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
138146
</Button>
139147
</div>
140148
) : (
141-
<select
142-
id="server"
143-
value={selectedServer?.id ?? ''}
144-
onChange={(e) => {
145-
const serverId = parseInt(e.target.value);
146-
const server = servers.find(s => s.id === serverId);
147-
setSelectedServer(server ?? null);
148-
}}
149-
className="w-full px-3 py-2 border border-input rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary bg-background text-foreground"
150-
>
151-
<option value="">Select a server...</option>
152-
{servers.map((server) => (
153-
<option key={server.id} value={server.id}>
154-
{server.name} ({server.ip}) - {server.user}
155-
</option>
156-
))}
157-
</select>
149+
<ColorCodedDropdown
150+
servers={servers}
151+
selectedServer={selectedServer}
152+
onServerSelect={handleServerSelect}
153+
placeholder="Select a server..."
154+
/>
158155
)}
159156
</div>
160157

src/app/_components/GeneralSettingsModal.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
1515
const [githubToken, setGithubToken] = useState('');
1616
const [saveFilter, setSaveFilter] = useState(false);
1717
const [savedFilters, setSavedFilters] = useState<any>(null);
18+
const [colorCodingEnabled, setColorCodingEnabled] = useState(false);
1819
const [isLoading, setIsLoading] = useState(false);
1920
const [isSaving, setIsSaving] = useState(false);
2021
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
@@ -35,6 +36,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
3536
void loadSaveFilter();
3637
void loadSavedFilters();
3738
void loadAuthCredentials();
39+
void loadColorCodingSetting();
3840
}
3941
}, [isOpen]);
4042

@@ -148,6 +150,43 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
148150
}
149151
};
150152

153+
const loadColorCodingSetting = async () => {
154+
try {
155+
const response = await fetch('/api/settings/color-coding');
156+
if (response.ok) {
157+
const data = await response.json();
158+
setColorCodingEnabled(Boolean(data.enabled));
159+
}
160+
} catch (error) {
161+
console.error('Error loading color coding setting:', error);
162+
}
163+
};
164+
165+
const saveColorCodingSetting = async (enabled: boolean) => {
166+
try {
167+
const response = await fetch('/api/settings/color-coding', {
168+
method: 'POST',
169+
headers: {
170+
'Content-Type': 'application/json',
171+
},
172+
body: JSON.stringify({ enabled }),
173+
});
174+
175+
if (response.ok) {
176+
setColorCodingEnabled(enabled);
177+
setMessage({ type: 'success', text: 'Color coding setting saved successfully' });
178+
setTimeout(() => setMessage(null), 3000);
179+
} else {
180+
setMessage({ type: 'error', text: 'Failed to save color coding setting' });
181+
setTimeout(() => setMessage(null), 3000);
182+
}
183+
} catch (error) {
184+
console.error('Error saving color coding setting:', error);
185+
setMessage({ type: 'error', text: 'Failed to save color coding setting' });
186+
setTimeout(() => setMessage(null), 3000);
187+
}
188+
};
189+
151190
const loadAuthCredentials = async () => {
152191
setAuthLoading(true);
153192
try {
@@ -345,6 +384,16 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
345384
</div>
346385
)}
347386
</div>
387+
388+
<div className="p-4 border border-border rounded-lg">
389+
<h4 className="font-medium text-foreground mb-2">Server Color Coding</h4>
390+
<p className="text-sm text-muted-foreground mb-4">Enable color coding for servers to visually distinguish them throughout the application.</p>
391+
<Toggle
392+
checked={colorCodingEnabled}
393+
onCheckedChange={saveColorCodingSetting}
394+
label="Enable server color coding"
395+
/>
396+
</div>
348397
</div>
349398
</div>
350399
)}

src/app/_components/InstalledScriptsTab.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Terminal } from './Terminal';
66
import { StatusBadge } from './Badge';
77
import { Button } from './ui/button';
88
import { ScriptInstallationCard } from './ScriptInstallationCard';
9+
import { getContrastColor } from '../../lib/colorUtils';
910

1011
interface InstalledScript {
1112
id: number;
@@ -17,6 +18,7 @@ interface InstalledScript {
1718
server_ip: string | null;
1819
server_user: string | null;
1920
server_password: string | null;
21+
server_color: string | null;
2022
installation_date: string;
2123
status: 'in_progress' | 'success' | 'failed';
2224
output_log: string | null;
@@ -773,7 +775,11 @@ export function InstalledScriptsTab() {
773775
</thead>
774776
<tbody className="bg-card divide-y divide-gray-200">
775777
{filteredScripts.map((script) => (
776-
<tr key={script.id} className="hover:bg-accent">
778+
<tr
779+
key={script.id}
780+
className="hover:bg-accent"
781+
style={{ borderLeft: `4px solid ${script.server_color ?? 'transparent'}` }}
782+
>
777783
<td className="px-6 py-4 whitespace-nowrap">
778784
{editingScriptId === script.id ? (
779785
<div className="space-y-2">
@@ -811,8 +817,14 @@ export function InstalledScriptsTab() {
811817
)}
812818
</td>
813819
<td className="px-6 py-4 whitespace-nowrap">
814-
<span className="text-sm text-muted-foreground">
815-
{script.server_name ?? 'Local'}
820+
<span
821+
className="text-sm px-3 py-1 rounded"
822+
style={{
823+
backgroundColor: script.server_color ?? 'transparent',
824+
color: script.server_color ? getContrastColor(script.server_color) : 'inherit'
825+
}}
826+
>
827+
{script.server_name ?? '-'}
816828
</span>
817829
</td>
818830
<td className="px-6 py-4 whitespace-nowrap">

src/app/_components/ScriptInstallationCard.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Button } from './ui/button';
44
import { StatusBadge } from './Badge';
5+
import { getContrastColor } from '../../lib/colorUtils';
56

67
interface InstalledScript {
78
id: number;
@@ -13,6 +14,7 @@ interface InstalledScript {
1314
server_ip: string | null;
1415
server_user: string | null;
1516
server_password: string | null;
17+
server_color: string | null;
1618
installation_date: string;
1719
status: 'in_progress' | 'success' | 'failed';
1820
output_log: string | null;
@@ -50,7 +52,10 @@ export function ScriptInstallationCard({
5052
};
5153

5254
return (
53-
<div className="bg-card border border-border rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow">
55+
<div
56+
className="bg-card border border-border rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow"
57+
style={{ borderLeft: `4px solid ${script.server_color ?? 'transparent'}` }}
58+
>
5459
{/* Header with Script Name and Status */}
5560
<div className="flex items-start justify-between mb-3">
5661
<div className="flex-1 min-w-0">
@@ -102,9 +107,15 @@ export function ScriptInstallationCard({
102107
{/* Server */}
103108
<div>
104109
<div className="text-xs font-medium text-muted-foreground mb-1">Server</div>
105-
<div className="text-sm text-muted-foreground">
106-
{script.server_name ?? 'Local'}
107-
</div>
110+
<span
111+
className="text-sm px-3 py-1 rounded inline-block"
112+
style={{
113+
backgroundColor: script.server_color ?? 'transparent',
114+
color: script.server_color ? getContrastColor(script.server_color) : 'inherit'
115+
}}
116+
>
117+
{script.server_name ?? '-'}
118+
</span>
108119
</div>
109120

110121
{/* Installation Date */}

0 commit comments

Comments
 (0)