diff --git a/python/PiFinder/server.py b/python/PiFinder/server.py index 3eef87d9d..e6de70957 100644 --- a/python/PiFinder/server.py +++ b/python/PiFinder/server.py @@ -791,20 +791,43 @@ def stream_logs(): if position == 0 or new_lines: return { 'logs': new_lines, - 'position': new_position + 'position': new_position, + 'file_size': file_size } else: return { 'logs': [], - 'position': position + 'position': position, + 'file_size': file_size } except FileNotFoundError: logger.error(f"Log file not found: {log_file}") - return {'logs': [], 'position': 0} + return {'logs': [], 'position': 0, 'file_size': 0} except Exception as e: logger.error(f"Error streaming logs: {e}") - return {'logs': [], 'position': position} + return {'logs': [], 'position': position, 'file_size': 0} + + @app.route("/logs/file_info") + @auth_required + def get_log_file_info(): + try: + log_file = "/home/pifinder/PiFinder_data/pifinder.log" + file_size = os.path.getsize(log_file) + + # Count total lines + total_lines = 0 + with open(log_file, 'r') as f: + total_lines = sum(1 for _ in f) + + return { + 'filename': os.path.basename(log_file), + 'total_lines': total_lines, + 'file_size': file_size + } + except Exception as e: + logger.error(f"Error getting log file info: {e}") + return {'error': str(e)} @app.route("/logs/current_level") @auth_required @@ -852,7 +875,7 @@ def download_logs(): # Add all log files log_dir = "/home/pifinder/PiFinder_data" for filename in os.listdir(log_dir): - if filename.startswith("pifinder") and filename.endswith(".log"): + if filename.startswith("pifinder.log"): file_path = os.path.join(log_dir, filename) zipf.write(file_path, filename) diff --git a/python/logconf_default.json b/python/logconf_default.json index 1a1fe1758..42ff7cb49 100644 --- a/python/logconf_default.json +++ b/python/logconf_default.json @@ -82,7 +82,7 @@ ///////////////////////////////////////////////////////////////// ////// GPS Subsystem "GPS": { - "level": "DEBUG" // Set this to DEBUG, to see results parsed from the GPS + "level": "INFO" // Set this to DEBUG, to see results parsed from the GPS }, "GPS.parser": { "level": "INFO" // Set this to DEBUG, to see results parsed from the GPS diff --git a/python/views/logs.tpl b/python/views/logs.tpl index c8c0cf957..3b811ea78 100644 --- a/python/views/logs.tpl +++ b/python/views/logs.tpl @@ -51,17 +51,43 @@ margin-right: 0; white-space: nowrap; } - .controls select { - height: 36px; - margin: 0; + .log-level-text { + color: #d4d4d4; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; padding: 0 10px; - width: auto; - min-width: fit-content; + } + .loading-spinner { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid rgba(255,255,255,.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 1s ease-in-out infinite; + margin-right: 8px; + vertical-align: middle; + } + @keyframes spin { + to { transform: rotate(360deg); } + } + .btn:disabled { + opacity: 0.7; + cursor: not-allowed; } .log-stats { color: #888; font-size: 0.9em; margin-bottom: 10px; + display: flex; + gap: 15px; + align-items: center; + } + .log-stats .separator { + color: #666; + } + .log-stats span { + display: inline-flex; + align-items: center; } /* Add horizontal scrollbar styles */ .log-container::-webkit-scrollbar, @@ -122,24 +148,14 @@ - - - + Global Level: INFO
- Total lines: 0 + File: pifinder.log + | + Position: 0/0 lines + | + Last update: just now
@@ -163,6 +179,55 @@ const BUFFER_SIZE = 100; const LINE_HEIGHT = 20; let updateInterval; let lastLine = ''; +let totalLinesSeen = 0; +let lastUpdateTime = Date.now(); +let currentLine = 0; +let totalFileLines = 0; +let isAtEndOfFile = false; + +function updateStats() { + // Update current line to show the last line number in the view + currentLine = Math.min(totalLinesSeen, totalFileLines); + document.getElementById('currentLine').textContent = currentLine; + document.getElementById('totalLines').textContent = totalFileLines; + + // Only update the time display if we're not paused + if (!isPaused) { + // Calculate time since last update + const now = Date.now(); + const secondsAgo = Math.floor((now - lastUpdateTime) / 1000); + let lastUpdateText; + + if (secondsAgo < 1) { + lastUpdateText = 'just now'; + } else if (secondsAgo < 60) { + lastUpdateText = `${secondsAgo}s ago`; + } else if (secondsAgo < 3600) { + const minutes = Math.floor(secondsAgo / 60); + lastUpdateText = `${minutes}m ago`; + } else { + const hours = Math.floor(secondsAgo / 3600); + lastUpdateText = `${hours}h ago`; + } + + document.getElementById('lastUpdate').textContent = lastUpdateText; + } +} + +function updateFileInfo() { + fetch('/logs/file_info') + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error getting file info:', data.error); + return; + } + document.getElementById('logFilename').textContent = data.filename; + totalFileLines = data.total_lines; + updateStats(); + }) + .catch(error => console.error('Error fetching file info:', error)); +} function fetchLogs() { if (isPaused) return; @@ -170,19 +235,36 @@ function fetchLogs() { fetch(`/logs/stream?position=${currentPosition}`) .then(response => response.json()) .then(data => { - if (!data.logs || data.logs.length === 0) return; + if (!data.logs || data.logs.length === 0) { + // If we're at the end of the file, update the last update time + if (!isAtEndOfFile) { + isAtEndOfFile = true; + lastUpdateTime = Date.now(); + updateStats(); + } + return; + } - currentPosition = data.position; - const logContent = document.getElementById('logContent'); + isAtEndOfFile = false; + let newLinesAdded = false; // Add new logs to buffer, skipping duplicates data.logs.forEach(line => { if (line !== lastLine) { logBuffer.push(line); lastLine = line; + totalLinesSeen++; + newLinesAdded = true; } }); + // Only update position and last update time if new lines were actually added + if (newLinesAdded) { + currentPosition = data.position; + lastUpdateTime = Date.now(); + updateStats(); + } + // Trim buffer if it exceeds size if (logBuffer.length > BUFFER_SIZE) { logBuffer = logBuffer.slice(-BUFFER_SIZE); @@ -229,6 +311,10 @@ function togglePause() { const pauseButton = document.getElementById('pauseButton'); pauseButton.textContent = isPaused ? 'Resume' : 'Pause'; + // Update the last update time when pause state changes + lastUpdateTime = Date.now(); + updateStats(); + if (!isPaused) { // Resume fetching from last position fetchLogs(); @@ -238,6 +324,8 @@ function togglePause() { function restartFromEnd() { currentPosition = 0; logBuffer = []; + totalLinesSeen = 0; // Reset the line counter + isAtEndOfFile = false; isPaused = false; document.getElementById('pauseButton').textContent = 'Pause'; fetchLogs(); @@ -249,6 +337,9 @@ document.addEventListener('DOMContentLoaded', () => { const loadingMessage = document.querySelector('.loading-message'); loadingMessage.style.display = 'flex'; + // Get initial file info + updateFileInfo(); + // Start log fetching fetchLogs(); updateInterval = setInterval(fetchLogs, 1000); @@ -266,9 +357,6 @@ document.addEventListener('DOMContentLoaded', () => { subtree: true, characterData: true }); - - // Load component levels - updateComponentLevels(); }); // Cleanup on page unload @@ -276,10 +364,50 @@ window.addEventListener('beforeunload', () => { clearInterval(updateInterval); }); +// Add download functionality +document.getElementById('downloadButton').addEventListener('click', function() { + const button = this; + const originalContent = button.innerHTML; + + // Disable button and show loading state + button.disabled = true; + button.innerHTML = 'Preparing download...'; + + // Start the download + fetch('/logs/download') + .then(response => { + if (!response.ok) throw new Error('Download failed'); + return response.blob(); + }) + .then(blob => { + // Create a download link + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `logs_${new Date().toISOString().slice(0,19).replace(/[:]/g, '-')}.zip`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + // Reset button state + button.innerHTML = originalContent; + button.disabled = false; + }) + .catch(error => { + console.error('Download error:', error); + button.innerHTML = 'errorDownload Failed'; + setTimeout(() => { + button.innerHTML = originalContent; + button.disabled = false; + }, 2000); + }); +}); + // Add copy to clipboard functionality document.getElementById('copyButton').addEventListener('click', function() { - const logContent = document.getElementById('logContent'); - const text = logContent.innerText; + // Use the logBuffer directly instead of DOM elements + const text = logBuffer.join('\n'); navigator.clipboard.writeText(text).then(() => { // Visual feedback @@ -299,94 +427,21 @@ document.getElementById('copyButton').addEventListener('click', function() { }); }); -// Log level management -function updateComponentLevels() { +// Add this function to update the global level text +function updateGlobalLevelText() { fetch('/logs/components') .then(response => response.json()) .then(data => { - const componentSelect = document.getElementById('componentSelect'); - componentSelect.innerHTML = ''; - - // Sort components alphabetically - const sortedComponents = Object.entries(data.components).sort(([a], [b]) => a.localeCompare(b)); - - sortedComponents.forEach(([component, levels]) => { - const option = document.createElement('option'); - option.value = component; - option.textContent = component; - componentSelect.appendChild(option); - }); + const globalLevel = data.global_level || 'INFO'; + document.getElementById('globalLevelText').textContent = globalLevel; }) - .catch(error => console.error('Error fetching component levels:', error)); + .catch(error => console.error('Error fetching global level:', error)); } -// Handle global level change -document.getElementById('globalLevel').addEventListener('change', function(e) { - const newLevel = e.target.value; - fetch('/logs/level', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: `level=${encodeURIComponent(newLevel)}` - }) - .then(response => response.json()) - .then(result => { - if (result.status === 'success') { - console.log(`Changed global log level to ${newLevel}`); - } else { - console.error('Failed to update global log level:', result.message); - } - }) - .catch(error => console.error('Error updating global log level:', error)); -}); - -// Handle component selection -document.getElementById('componentSelect').addEventListener('change', function(e) { - const component = e.target.value; - if (!component) { - document.getElementById('componentLevel').style.display = 'none'; - return; - } - - // Show level select and set current level - const levelSelect = document.getElementById('componentLevel'); - levelSelect.style.display = 'block'; - - // Get current level for selected component - fetch('/logs/components') - .then(response => response.json()) - .then(data => { - const currentLevel = data.components[component].current_level; - levelSelect.value = currentLevel; - }); -}); - -// Handle component level change -document.getElementById('componentLevel').addEventListener('change', function(e) { - const component = document.getElementById('componentSelect').value; - const newLevel = e.target.value; - - fetch('/logs/component_level', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: `component=${encodeURIComponent(component)}&level=${encodeURIComponent(newLevel)}` - }) - .then(response => response.json()) - .then(result => { - if (result.status === 'success') { - console.log(`Changed ${component} log level to ${newLevel}`); - } else { - console.error('Failed to update log level:', result.message); - } - }) - .catch(error => console.error('Error updating log level:', error)); -}); - -// Initial load of components -updateComponentLevels(); +// Call updateGlobalLevelText periodically +setInterval(updateGlobalLevelText, 5000); +// Initial call +updateGlobalLevelText(); // Set up button event listeners document.getElementById('pauseButton').addEventListener('click', function() {