diff --git a/index.js b/index.js index df41134..0bc77f8 100644 --- a/index.js +++ b/index.js @@ -72,6 +72,7 @@ app.get('/tail/api/files', (req, res) => { app.get('/tail/api/files/:endpoint', (req, res) => { const endpoint = path.normalize(req.params.endpoint) const file = path.join(directory, endpoint) + const isReconnect = req.query.reconnect === 'true' // Security check - ensure file is within the directory if (!file.startsWith(path.resolve(directory))) { @@ -95,14 +96,20 @@ app.get('/tail/api/files/:endpoint', (req, res) => { } }) + // For reconnections, start from current end, for new connections start from 'end' + const beginAt = isReconnect ? fs.statSync(file).size : 'end' + const stream = ts.createReadStream(file, { - beginAt: 'end', + beginAt: beginAt, endOnError: true, detectTruncate: true }) // Send initial message - res.write(Buffer.from(`data: Tailing ${endpoint}...\n\n`)) + const initialMessage = isReconnect + ? `Reconnected to ${endpoint}...` + : `Tailing ${endpoint}...` + res.write(Buffer.from(`data: ${initialMessage}\n\n`)) // Handle stream events stream.on('error', error => { diff --git a/public/main.css b/public/main.css index c86af7b..9a6b930 100644 --- a/public/main.css +++ b/public/main.css @@ -571,6 +571,32 @@ input:checked + .slider:before { background: rgba(6, 182, 212, 0.1); } +.log-line.reconnect-message { + border-left-color: var(--warning-color); + background: rgba(245, 158, 11, 0.15); + color: var(--warning-color); + font-style: italic; + text-align: center; + margin: var(--spacing-sm) 0; +} + +.log-line.reconnect-message i { + margin-right: var(--spacing-xs); +} + +.log-line.reconnect-success { + border-left-color: var(--success-color); + background: rgba(16, 185, 129, 0.15); + color: var(--success-color); + font-style: italic; + text-align: center; + margin: var(--spacing-sm) 0; +} + +.log-line.reconnect-success i { + margin-right: var(--spacing-xs); +} + /* Loading states */ .loading { display: flex; diff --git a/public/main.js b/public/main.js index 0189c2f..943dfb9 100644 --- a/public/main.js +++ b/public/main.js @@ -234,7 +234,7 @@ class LogTailMonitor { /** * Start tailing a specific file */ - async startTailing(filename) { + async startTailing(filename, isReconnect = false) { this.showLoading('Connecting to ' + filename + '...'); // Stop current connection @@ -254,17 +254,28 @@ class LogTailMonitor { } }); - // Clear existing logs - this.clearLogs(); + // Clear existing logs only if it's not a reconnection + if (!isReconnect) { + this.clearLogs(); + } else { + // Add a reconnection indicator to the existing logs + this.addReconnectionMessage(); + } try { - // Start SSE connection - this.eventSource = new EventSource(`api/files/${encodeURIComponent(filename)}`); + // Start SSE connection with reconnect parameter if needed + const url = `api/files/${encodeURIComponent(filename)}${isReconnect ? '?reconnect=true' : ''}`; + this.eventSource = new EventSource(url); this.eventSource.onopen = () => { this.hideLoading(); this.setConnectionStatus(true); console.log('SSE connection established'); + + // If this is a reconnection, add a success message + if (isReconnect) { + this.addReconnectionSuccessMessage(); + } }; this.eventSource.onmessage = (event) => { @@ -283,11 +294,11 @@ class LogTailMonitor { console.log('SSE connection closed'); } - // Auto-reconnect after delay + // Auto-reconnect after delay, but don't clear logs setTimeout(() => { if (this.currentFile && this.eventSource.readyState === EventSource.CLOSED) { console.log('Attempting to reconnect...'); - this.startTailing(this.currentFile); + this.startTailing(this.currentFile, true); // true = isReconnect } }, 5000); }; @@ -568,6 +579,36 @@ class LogTailMonitor { this.elements.loadingOverlay.classList.remove('visible'); } + /** + * Add a reconnection message to the log + */ + addReconnectionMessage() { + const reconnectLine = document.createElement('div'); + reconnectLine.className = 'log-line reconnect-message'; + reconnectLine.innerHTML = ' Reconnecting to log stream...'; + + this.elements.logOutput.appendChild(reconnectLine); + + if (this.autoScroll) { + this.scrollToBottom(); + } + } + + /** + * Add a successful reconnection message to the log + */ + addReconnectionSuccessMessage() { + const successLine = document.createElement('div'); + successLine.className = 'log-line reconnect-success'; + successLine.innerHTML = ' Successfully reconnected - continuing log stream...'; + + this.elements.logOutput.appendChild(successLine); + + if (this.autoScroll) { + this.scrollToBottom(); + } + } + /** * Escape HTML to prevent XSS */ @@ -580,15 +621,18 @@ class LogTailMonitor { // Initialize the application when DOM is ready document.addEventListener('DOMContentLoaded', () => { - new LogTailMonitor(); + window.logMonitor = new LogTailMonitor(); }); -// Handle page visibility changes +// Handle page visibility changes - attempt reconnection instead of reload document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - // Refresh connection when page becomes visible - setTimeout(() => { - window.location.reload(); - }, 1000); + if (document.visibilityState === 'visible' && window.logMonitor) { + // Try to reconnect to current file if connection was lost + if (window.logMonitor.currentFile && (!window.logMonitor.eventSource || window.logMonitor.eventSource.readyState === EventSource.CLOSED)) { + console.log('Page became visible, attempting to reconnect...'); + setTimeout(() => { + window.logMonitor.startTailing(window.logMonitor.currentFile, true); + }, 1000); + } } });