"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { io, Socket } from "socket.io-client"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Terminal, Send, Trash2, AlertCircle } from "lucide-react"; import { toast } from "sonner"; interface LogLine { text: string; timestamp: number; type: "info" | "warn" | "error" | "raw"; } function classifyLine(line: string): LogLine["type"] { if (/\[WARN\]|WARNING/i.test(line)) return "warn"; if (/\[ERROR\]|SEVERE|Exception|Error/i.test(line)) return "error"; return "info"; } function LineColor({ type }: { type: LogLine["type"] }) { const colors = { info: "text-zinc-300", warn: "text-amber-400", error: "text-red-400", raw: "text-zinc-500", }; return colors[type]; } const MAX_LINES = 1000; export default function ConsolePage() { const [lines, setLines] = useState([]); const [command, setCommand] = useState(""); const [connected, setConnected] = useState(false); const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); const socketRef = useRef(null); const scrollRef = useRef(null); const autoScrollRef = useRef(true); useEffect(() => { const socket = io("/console", { transports: ["websocket"], }); socketRef.current = socket; socket.on("connect", () => setConnected(true)); socket.on("disconnect", () => setConnected(false)); socket.on("connect_error", () => { toast.error("Failed to connect to server console"); }); socket.on("output", (data: { line: string; timestamp: number }) => { setLines((prev) => { const newLine: LogLine = { text: data.line, timestamp: data.timestamp, type: classifyLine(data.line), }; const updated = [...prev, newLine]; return updated.length > MAX_LINES ? updated.slice(-MAX_LINES) : updated; }); }); // Receive buffered history on connect socket.on("history", (data: { lines: string[] }) => { const historicalLines = data.lines.map((line) => ({ text: line, timestamp: Date.now(), type: classifyLine(line) as LogLine["type"], })); setLines(historicalLines); }); return () => { socket.disconnect(); }; }, []); // Auto-scroll to bottom useEffect(() => { if (autoScrollRef.current && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [lines]); const sendCommand = useCallback(() => { const cmd = command.trim(); if (!cmd || !socketRef.current) return; // Add to local history setHistory((prev) => { const updated = [cmd, ...prev.filter((h) => h !== cmd)].slice(0, 50); return updated; }); setHistoryIndex(-1); // Echo to console setLines((prev) => [ ...prev, { text: `> ${cmd}`, timestamp: Date.now(), type: "raw" }, ]); socketRef.current.emit("command", { command: cmd }); setCommand(""); }, [command]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { sendCommand(); } else if (e.key === "ArrowUp") { e.preventDefault(); const newIndex = Math.min(historyIndex + 1, history.length - 1); setHistoryIndex(newIndex); if (history[newIndex]) setCommand(history[newIndex]); } else if (e.key === "ArrowDown") { e.preventDefault(); const newIndex = Math.max(historyIndex - 1, -1); setHistoryIndex(newIndex); setCommand(newIndex === -1 ? "" : history[newIndex] ?? ""); } }; const formatTime = (ts: number) => new Date(ts).toLocaleTimeString("en", { hour12: false }); return (

Server Console

Real-time server output and command input

{connected ? "Connected" : "Disconnected"}
Console Output {lines.length} lines
{ const el = e.currentTarget; autoScrollRef.current = el.scrollTop + el.clientHeight >= el.scrollHeight - 20; }} className="h-full overflow-y-auto font-mono text-xs p-4 space-y-0.5" style={{ maxHeight: "calc(100vh - 280px)" }} > {lines.length === 0 ? (

Waiting for server output...

) : ( lines.map((line, i) => (
{formatTime(line.timestamp)} {line.text}
)) )}
{/* Command input */}
/ setCommand(e.target.value)} onKeyDown={handleKeyDown} placeholder="Enter server command... (↑↓ for history)" disabled={!connected} className="flex-1 bg-transparent py-2 text-sm text-white placeholder:text-zinc-600 outline-none font-mono" />
{!connected && (
Console disconnected. The server may be offline or restarting.
)}
); }