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() {