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);
+ }
}
});