| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Live Logs</title> |
|
|
| <style> |
| body { |
| margin: 0; |
| background: #0b1020; |
| color: #e2e8f0; |
| font-family: monospace; |
| display: flex; |
| flex-direction: column; |
| height: 90vh; |
| } |
| |
| header { |
| padding: 10px; |
| background: #111827; |
| border-bottom: 1px solid #1f2937; |
| } |
| |
| #logs { |
| flex: 1; |
| overflow-y: auto; |
| padding: 10px; |
| font-size: 13px; |
| } |
| |
| .log { |
| margin-bottom: 6px; |
| word-break: break-word; |
| } |
| |
| .time { |
| color: #64748b; |
| margin-right: 6px; |
| } |
| |
| footer { |
| position: relative; |
| display: flex; |
| padding: 8px; |
| background: #111827; |
| border-top: 1px solid #1f2937; |
| } |
| |
| input { |
| flex: 1; |
| padding: 10px; |
| background: #0b1020; |
| border: none; |
| color: white; |
| border-radius: 6px; |
| outline: none; |
| } |
| |
| button { |
| margin-left: 8px; |
| padding: 10px 14px; |
| background: #2563eb; |
| border: none; |
| color: white; |
| border-radius: 6px; |
| } |
| |
| #suggestions { |
| position: absolute; |
| bottom: 60px; |
| left: 10px; |
| right: 10px; |
| background: #111827; |
| border: 1px solid #1f2937; |
| border-radius: 6px; |
| max-height: 150px; |
| overflow-y: auto; |
| display: none; |
| z-index: 10; |
| } |
| |
| .suggestion-item { |
| padding: 8px; |
| cursor: pointer; |
| } |
| |
| .suggestion-item:hover { |
| background: #1f2937; |
| } |
| </style> |
| </head> |
|
|
| <body> |
|
|
| <header>📡 Live Logs Terminal</header> |
|
|
| <div id="logs"></div> |
|
|
| <footer> |
| <input id="cmd" placeholder="Enter command..." autocomplete="off" /> |
| <button onclick="sendCmd()">Send</button> |
|
|
| <div id="suggestions"></div> |
| </footer> |
|
|
| <script src="/socket.io/socket.io.js"></script> |
| <script> |
| const socket = io(); |
| const logsDiv = document.getElementById('logs'); |
| const input = document.getElementById('cmd'); |
| const suggestionBox = document.getElementById('suggestions'); |
| |
| |
| const LIMIT = 50; |
| const MAX_DOM_LOGS = 200; |
| |
| let offset = 0; |
| let loading = false; |
| let hasMore = true; |
| |
| |
| function formatTime(ts) { |
| return new Date(ts).toLocaleString('en-IN', { |
| timeZone: 'Asia/Kolkata', |
| hour12: false |
| }); |
| } |
| |
| |
| function createLogElement(log) { |
| const div = document.createElement('div'); |
| div.className = 'log'; |
| |
| div.innerHTML = ` |
| <span class="time">[${formatTime(log.timestamp)}]</span> |
| ${log.html} |
| `; |
| return div; |
| } |
| |
| |
| function addLog(log) { |
| const div = createLogElement(log); |
| |
| const isNearBottom = |
| logsDiv.scrollHeight - logsDiv.scrollTop <= logsDiv.clientHeight + 50; |
| |
| logsDiv.appendChild(div); |
| |
| |
| if (logsDiv.children.length > MAX_DOM_LOGS) { |
| logsDiv.removeChild(logsDiv.firstChild); |
| } |
| |
| if (isNearBottom) { |
| logsDiv.scrollTop = logsDiv.scrollHeight; |
| } |
| } |
| |
| |
| async function loadLogs() { |
| if (loading || !hasMore) return; |
| loading = true; |
| |
| const res = await fetch(`/api/logs?limit=${LIMIT}&offset=${offset}`); |
| const data = await res.json(); |
| |
| hasMore = data.hasMore; |
| |
| const prevHeight = logsDiv.scrollHeight; |
| |
| data.logs.reverse().forEach(log => { |
| logsDiv.prepend(createLogElement(log)); |
| }); |
| |
| offset += data.logs.length; |
| |
| |
| logsDiv.scrollTop = logsDiv.scrollHeight - prevHeight; |
| |
| loading = false; |
| } |
| |
| |
| function getHistory() { |
| return JSON.parse(localStorage.getItem('cmd_history') || '[]'); |
| } |
| |
| function saveToHistory(cmd) { |
| let history = getHistory(); |
| |
| history = history.filter(c => c !== cmd); |
| history.unshift(cmd); |
| |
| if (history.length > 50) history.pop(); |
| |
| localStorage.setItem('cmd_history', JSON.stringify(history)); |
| } |
| |
| |
| function showSuggestions(value) { |
| const history = getHistory(); |
| |
| const filtered = history.filter(cmd => |
| cmd.toLowerCase().includes(value.toLowerCase()) |
| ); |
| |
| if (!value || filtered.length === 0) { |
| suggestionBox.style.display = 'none'; |
| return; |
| } |
| |
| suggestionBox.innerHTML = ''; |
| |
| filtered.slice(0, 5).forEach(cmd => { |
| const div = document.createElement('div'); |
| div.className = 'suggestion-item'; |
| div.textContent = cmd; |
| |
| div.onclick = () => { |
| input.value = cmd; |
| suggestionBox.style.display = 'none'; |
| }; |
| |
| suggestionBox.appendChild(div); |
| }); |
| |
| suggestionBox.style.display = 'block'; |
| } |
| |
| |
| input.addEventListener('input', () => { |
| showSuggestions(input.value); |
| }); |
| |
| input.addEventListener('keypress', e => { |
| if (e.key === 'Enter') sendCmd(); |
| }); |
| |
| |
| function sendCmd() { |
| const val = input.value.trim(); |
| if (!val) return; |
| |
| socket.emit('command', { command: val }); |
| saveToHistory(val); |
| |
| input.value = ''; |
| suggestionBox.style.display = 'none'; |
| |
| if (val === "clear") { |
| logsDiv.innerHTML = ""; |
| offset = 0; |
| hasMore = true; |
| return; |
| } |
| } |
| |
| |
| logsDiv.addEventListener('scroll', () => { |
| if (logsDiv.scrollTop < 50) { |
| loadLogs(); |
| } |
| }); |
| |
| |
| loadLogs(); |
| |
| |
| socket.on('log', addLog); |
| |
| </script> |
|
|
| </body> |
| </html> |