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 6bd6d99..5e11d68 100644 --- a/src/app/_components/ExecutionModeModal.tsx +++ b/src/app/_components/ExecutionModeModal.tsx @@ -3,8 +3,10 @@ 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 { isOpen: boolean; onClose: () => void; @@ -70,6 +72,12 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E onClose(); }; + + const handleServerSelect = (server: Server | null) => { + setSelectedServer(server); + }; + + if (!isOpen) return null; return ( @@ -138,23 +146,12 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E ) : ( - + )} diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index 66ac083..bfe710f 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(Boolean(data.enabled)); + } + } 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..06111d1 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(Boolean(data.enabled)); + } + } 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..2e9b490 100644 --- a/src/app/_components/ServerList.tsx +++ b/src/app/_components/ServerList.tsx @@ -85,7 +85,11 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) { return (
{servers.map((server) => ( -
+
{editingId === server.id ? (

Edit Server

@@ -95,6 +99,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/_components/Terminal.tsx b/src/app/_components/Terminal.tsx index 10e2bfc..dbe6520 100644 --- a/src/app/_components/Terminal.tsx +++ b/src/app/_components/Terminal.tsx @@ -27,14 +27,13 @@ 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); 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); @@ -53,22 +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 and clear immediately - 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 - xtermRef.current.write(message.data); - } else { - // 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) @@ -87,8 +71,7 @@ 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 const isLxcCreation = scriptPath.includes('ct/') || @@ -107,7 +90,6 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate } else { xtermRef.current.writeln(`${prefix}✅ ${message.data}`); } - setIsRunning(false); break; } }, [scriptPath, containerId, scriptName]); @@ -265,7 +247,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate fitAddonRef.current = null; } }; - }, [executionId, isClient, inWhiptailSession, isMobile]); + }, [isClient, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { // Prevent multiple connections in React Strict Mode @@ -298,10 +280,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, @@ -345,15 +331,19 @@ 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, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps 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, 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 {