import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Terminal, Wifi, WifiOff, X, Plus, ChevronDown } from 'lucide-react'; // Serveurs SSH prédéfinis const PRESET_SERVERS = [ { id: 'recette', label: 'Recette (78.138.58.109)', host: '78.138.58.109', port: 22, username: 'root', }, { id: 'production', label: 'Production (180.149.196.138)', host: '180.149.196.138', port: 22, username: 'root', }, ]; export default function TerminalPage() { const terminalRef = useRef(null); const xtermRef = useRef(null); const fitAddonRef = useRef(null); const wsRef = useRef(null); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [xtermLoaded, setXtermLoaded] = useState(false); // Formulaire de connexion const [form, setForm] = useState({ preset: 'recette', host: '78.138.58.109', port: 22, username: 'root', password: '', }); const [showForm, setShowForm] = useState(true); const [error, setError] = useState(''); // Charger xterm.js dynamiquement depuis CDN useEffect(() => { const loadXterm = async () => { if (window.Terminal) { setXtermLoaded(true); return; } // Charger le CSS xterm const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css'; document.head.appendChild(link); // Charger xterm.js await loadScript('https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js'); await loadScript('https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js'); setXtermLoaded(true); }; loadXterm(); }, []); const loadScript = (src) => { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); }; // Initialiser xterm quand il est chargé et que le terminal est visible useEffect(() => { if (!xtermLoaded || !terminalRef.current || xtermRef.current) return; const term = new window.Terminal({ cursorBlink: true, fontSize: 14, fontFamily: '"Cascadia Code", "Fira Code", "JetBrains Mono", monospace', theme: { background: '#0d1117', foreground: '#e6edf3', cursor: '#58a6ff', cursorAccent: '#0d1117', black: '#484f58', red: '#ff7b72', green: '#3fb950', yellow: '#d29922', blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#b1bac4', brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341', brightBlue: '#79c0ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc', }, scrollback: 1000, allowProposedApi: true, }); const fitAddon = new window.FitAddon.FitAddon(); term.loadAddon(fitAddon); term.open(terminalRef.current); fitAddon.fit(); xtermRef.current = term; fitAddonRef.current = fitAddon; term.writeln('\x1b[1;34m╔══════════════════════════════════════════════════╗\x1b[0m'); term.writeln('\x1b[1;34m║ Terminal SSH — Dashboard Recette Santinova ║\x1b[0m'); term.writeln('\x1b[1;34m╚══════════════════════════════════════════════════╝\x1b[0m'); term.writeln(''); term.writeln('\x1b[90mSélectionnez un serveur et connectez-vous pour démarrer.\x1b[0m'); term.writeln(''); // Gérer la saisie utilisateur term.onData((data) => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type: 'input', data })); } }); // Gérer le redimensionnement const resizeObserver = new ResizeObserver(() => { if (fitAddonRef.current) { fitAddonRef.current.fit(); if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type: 'resize', rows: term.rows, cols: term.cols, })); } } }); if (terminalRef.current) { resizeObserver.observe(terminalRef.current); } return () => { resizeObserver.disconnect(); }; }, [xtermLoaded]); const handlePresetChange = (presetId) => { const preset = PRESET_SERVERS.find(s => s.id === presetId); if (preset) { setForm(prev => ({ ...prev, preset: presetId, host: preset.host, port: preset.port, username: preset.username, })); } else { setForm(prev => ({ ...prev, preset: 'custom' })); } }; const handleConnect = useCallback(() => { if (!form.host || !form.username || !form.password) { setError('Hôte, utilisateur et mot de passe sont requis'); return; } setError(''); setConnecting(true); setShowForm(false); const token = localStorage.getItem('dashboard_token'); const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws-ssh?token=${token}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { ws.send(JSON.stringify({ type: 'connect', host: form.host, port: parseInt(form.port), username: form.username, password: form.password, })); }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); switch (msg.type) { case 'status': if (xtermRef.current) { xtermRef.current.writeln(`\x1b[33m${msg.message}\x1b[0m`); } break; case 'connected': setConnected(true); setConnecting(false); if (xtermRef.current) { xtermRef.current.writeln(`\x1b[32m✓ ${msg.message}\x1b[0m`); xtermRef.current.writeln(''); xtermRef.current.focus(); } break; case 'output': if (xtermRef.current) { const data = atob(msg.data); xtermRef.current.write(data); } break; case 'disconnected': setConnected(false); setConnecting(false); if (xtermRef.current) { xtermRef.current.writeln(''); xtermRef.current.writeln(`\x1b[33m⚠ ${msg.message}\x1b[0m`); } break; case 'error': setConnected(false); setConnecting(false); setError(msg.message); if (xtermRef.current) { xtermRef.current.writeln(`\x1b[31m✗ Erreur: ${msg.message}\x1b[0m`); } setShowForm(true); break; } } catch (err) { console.error('Erreur parsing message SSH:', err); } }; ws.onclose = () => { setConnected(false); setConnecting(false); }; ws.onerror = () => { setConnected(false); setConnecting(false); setError('Erreur de connexion WebSocket'); setShowForm(true); }; }, [form]); const handleDisconnect = () => { if (wsRef.current) { wsRef.current.send(JSON.stringify({ type: 'disconnect' })); wsRef.current.close(); wsRef.current = null; } setConnected(false); setConnecting(false); setShowForm(true); if (xtermRef.current) { xtermRef.current.writeln(''); xtermRef.current.writeln('\x1b[90mDéconnecté. Configurez une nouvelle connexion.\x1b[0m'); } }; return (
Connexion SSH sécurisée aux serveurs de l'infrastructure