`;
}
@@ -280,6 +296,90 @@ async function downloadFTPFile(unitId, remotePath, fileName) {
}
}
+async function downloadFTPFolder(unitId, remotePath, folderName) {
+ const btn = event.target;
+ const originalHTML = btn.innerHTML;
+ btn.disabled = true;
+ btn.innerHTML = '
Downloading...';
+
+ try {
+ const response = await fetch(`/api/slmm/${unitId}/ftp/download-folder`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ remote_path: remotePath
+ })
+ });
+
+ if (response.ok) {
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${folderName}.zip`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ // Show success message
+ alert(`✓ Folder "${folderName}" downloaded successfully as ZIP file!`);
+ } else {
+ const errorData = await response.json();
+ alert('Folder download failed: ' + (errorData.detail || 'Unknown error'));
+ }
+ } catch (error) {
+ alert('Error downloading folder: ' + error);
+ } finally {
+ btn.disabled = false;
+ btn.innerHTML = originalHTML;
+ }
+}
+
+async function downloadFolderToServer(unitId, remotePath, folderName) {
+ const btn = event.target;
+ const originalHTML = btn.innerHTML;
+ btn.disabled = true;
+ btn.innerHTML = '
Downloading...';
+
+ // Get location_id from the unit's data attribute
+ const unitContainer = btn.closest('[id^="ftp-files-"]');
+ const locationId = unitContainer.dataset.locationId;
+
+ try {
+ const response = await fetch(`/api/projects/{{ project_id }}/ftp-download-folder-to-server`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ unit_id: unitId,
+ remote_path: remotePath,
+ location_id: locationId
+ })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ // Show success message
+ alert(`✓ Folder "${folderName}" downloaded to server successfully as ZIP!\n\nFile ID: ${data.file_id}\nSize: ${formatFileSize(data.file_size)}`);
+
+ // Refresh the downloaded files list
+ htmx.trigger('#project-files', 'refresh');
+ } else {
+ alert('Folder download to server failed: ' + (data.detail || 'Unknown error'));
+ }
+ } catch (error) {
+ alert('Error downloading folder to server: ' + error);
+ } finally {
+ btn.disabled = false;
+ btn.innerHTML = originalHTML;
+ }
+}
+
async function downloadToServer(unitId, remotePath, fileName) {
const btn = event.target;
const originalText = btn.innerHTML;
diff --git a/templates/partials/slm_live_view.html b/templates/partials/slm_live_view.html
index ae2ff03..c6a942d 100644
--- a/templates/partials/slm_live_view.html
+++ b/templates/partials/slm_live_view.html
@@ -1,5 +1,15 @@
+
+
+
@@ -201,7 +211,7 @@
+ style="width: {% if current_status and current_status.battery_level %}{{ current_status.battery_level }}{% else %}0{% endif %}%">
@@ -934,9 +944,15 @@ function startMeasurementTimer(unitId) {
// Stop any existing timer
stopMeasurementTimer();
- // Record start time in localStorage
- const startTime = Date.now();
- localStorage.setItem(TIMER_STORAGE_KEY + unitId, startTime.toString());
+ // Only set start time if not already set (preserve existing start time)
+ const existingStartTime = localStorage.getItem(TIMER_STORAGE_KEY + unitId);
+ if (!existingStartTime) {
+ const startTime = Date.now();
+ localStorage.setItem(TIMER_STORAGE_KEY + unitId, startTime.toString());
+ console.log('No existing start time, setting to now:', new Date(startTime));
+ } else {
+ console.log('Using existing start time from localStorage:', new Date(parseInt(existingStartTime)));
+ }
// Show timer container
const container = document.getElementById('elapsed-time-container');
@@ -1023,12 +1039,19 @@ async function resumeMeasurementTimerIfNeeded(unitId, isMeasuring) {
// Fetch measurement start time from last folder on FTP
async function fetchStartTimeFromFTP(unitId) {
try {
+ console.log('Fetching FTP files from /NL-43...');
const response = await fetch(`/api/slmm/${unitId}/ftp/files?path=/NL-43`);
const result = await response.json();
+ console.log('FTP response status:', response.status);
+ console.log('FTP response data:', result);
+
if (result.status === 'ok' && result.files && result.files.length > 0) {
+ console.log(`Found ${result.files.length} files/folders`);
+
// Filter for directories only
const folders = result.files.filter(f => f.is_dir || f.type === 'directory');
+ console.log(`Found ${folders.length} folders:`, folders.map(f => f.name));
if (folders.length > 0) {
// Sort by modified timestamp (newest first) or by name
@@ -1050,26 +1073,27 @@ async function fetchStartTimeFromFTP(unitId) {
const startTime = parseFolderTimestamp(lastFolder);
if (startTime) {
- console.log('Parsed start time from folder:', new Date(startTime));
+ console.log('✓ Parsed start time from folder:', new Date(startTime));
+ console.log('✓ Storing start time in localStorage:', startTime);
localStorage.setItem(TIMER_STORAGE_KEY + unitId, startTime.toString());
startMeasurementTimer(unitId);
} else {
// Can't parse folder time - start from now
- console.warn('Could not parse folder timestamp, starting timer from now');
+ console.warn('✗ Could not parse folder timestamp, starting timer from now');
startMeasurementTimer(unitId);
}
} else {
// No folders found - start from now
- console.warn('No measurement folders found, starting timer from now');
+ console.warn('✗ No measurement folders found in FTP response, starting timer from now');
startMeasurementTimer(unitId);
}
} else {
// FTP failed or no files - start from now
- console.warn('Could not access FTP, starting timer from now');
+ console.warn('✗ FTP request failed or returned no files:', result);
startMeasurementTimer(unitId);
}
} catch (error) {
- console.error('Error fetching start time from FTP:', error);
+ console.error('✗ Error fetching start time from FTP:', error);
// Fallback - start from now
startMeasurementTimer(unitId);
}
@@ -1077,6 +1101,8 @@ async function fetchStartTimeFromFTP(unitId) {
// Parse timestamp from folder name or modified time
function parseFolderTimestamp(folder) {
+ console.log('Parsing timestamp from folder:', folder.name);
+
// Try parsing from folder name first
// Expected formats: YYYYMMDD_HHMMSS or YYYY-MM-DD_HH-MM-SS
const name = folder.name;
@@ -1086,7 +1112,9 @@ function parseFolderTimestamp(folder) {
const match1 = name.match(pattern1);
if (match1) {
const [_, year, month, day, hour, min, sec] = match1;
- return new Date(year, month - 1, day, hour, min, sec).getTime();
+ const timestamp = new Date(year, month - 1, day, hour, min, sec).getTime();
+ console.log(` ✓ Matched pattern YYYYMMDD_HHMMSS: ${new Date(timestamp)}`);
+ return timestamp;
}
// Pattern: YYYY-MM-DD_HH-MM-SS (e.g., 2025-01-14_14-30-52)
@@ -1094,27 +1122,91 @@ function parseFolderTimestamp(folder) {
const match2 = name.match(pattern2);
if (match2) {
const [_, year, month, day, hour, min, sec] = match2;
- return new Date(year, month - 1, day, hour, min, sec).getTime();
+ const timestamp = new Date(year, month - 1, day, hour, min, sec).getTime();
+ console.log(` ✓ Matched pattern YYYY-MM-DD_HH-MM-SS: ${new Date(timestamp)}`);
+ return timestamp;
}
- // Try using modified_timestamp (ISO format from SLMM)
+ console.log(' No pattern matched in folder name, trying modified_timestamp field');
+
+ // Try using modified_timestamp (ISO format from SLMM - already in UTC)
if (folder.modified_timestamp) {
- return new Date(folder.modified_timestamp).getTime();
+ // SLMM returns timestamps in UTC format without 'Z' suffix
+ // Append 'Z' to ensure browser parses as UTC
+ let utcTimestamp = folder.modified_timestamp;
+ if (!utcTimestamp.endsWith('Z')) {
+ utcTimestamp = utcTimestamp + 'Z';
+ }
+ const timestamp = new Date(utcTimestamp).getTime();
+ console.log(` ✓ Using modified_timestamp (UTC): ${folder.modified_timestamp} → ${new Date(timestamp).toISOString()} → ${new Date(timestamp).toString()}`);
+ return timestamp;
}
// Fallback to modified (string format)
if (folder.modified) {
const parsedTime = new Date(folder.modified).getTime();
if (!isNaN(parsedTime)) {
+ console.log(` ✓ Using modified field: ${new Date(parsedTime)}`);
return parsedTime;
}
+ console.log(` ✗ Could not parse modified field: ${folder.modified}`);
}
// Could not parse
+ console.log(' ✗ Could not parse any timestamp from folder');
return null;
}
+// Apply status data to the DOM (used by bootstrap data and live refresh)
+function applyDeviceStatusData(data) {
+ if (!data) {
+ return;
+ }
+
+ const batteryLevelRaw = data.battery_level ?? '--';
+ const batteryLevelElement = document.getElementById('battery-level');
+ if (batteryLevelElement) {
+ const displayBattery = batteryLevelRaw === '' || batteryLevelRaw === '--'
+ ? '--'
+ : `${batteryLevelRaw}%`;
+ batteryLevelElement.textContent = displayBattery;
+ }
+
+ const batteryBar = document.getElementById('battery-bar');
+ if (batteryBar && batteryLevelRaw !== '' && batteryLevelRaw !== '--') {
+ const level = Number(batteryLevelRaw);
+ if (!Number.isNaN(level)) {
+ batteryBar.style.width = `${level}%`;
+ if (level > 50) {
+ batteryBar.className = 'bg-green-500 h-2 rounded-full transition-all';
+ } else if (level > 20) {
+ batteryBar.className = 'bg-yellow-500 h-2 rounded-full transition-all';
+ } else {
+ batteryBar.className = 'bg-red-500 h-2 rounded-full transition-all';
+ }
+ }
+ }
+
+ if (document.getElementById('power-source')) {
+ document.getElementById('power-source').textContent = data.power_source || '--';
+ }
+
+ if (document.getElementById('sd-remaining')) {
+ const sdRemainingRaw = data.sd_remaining_mb ?? '--';
+ document.getElementById('sd-remaining').textContent = sdRemainingRaw === '--'
+ ? '--'
+ : `${sdRemainingRaw} MB`;
+ }
+ if (document.getElementById('sd-ratio')) {
+ const sdRatioRaw = data.sd_free_ratio ?? '--';
+ document.getElementById('sd-ratio').textContent = sdRatioRaw === '--'
+ ? '--'
+ : `${sdRatioRaw}% free`;
+ }
+}
+
+
// Auto-refresh status every 30 seconds
let refreshInterval;
const REFRESH_INTERVAL_MS = 30000; // 30 seconds
@@ -1125,46 +1217,8 @@ function updateDeviceStatus() {
.then(response => response.json())
.then(result => {
if (result.status === 'ok' && result.data) {
- const data = result.data;
+ applyDeviceStatusData(result.data);
- // Update battery
- if (document.getElementById('battery-level')) {
- const batteryLevel = data.battery_level || '--';
- document.getElementById('battery-level').textContent = batteryLevel === '--' ? '--' : `${batteryLevel}%`;
-
- // Update battery bar
- const batteryBar = document.getElementById('battery-bar');
- if (batteryBar && batteryLevel !== '--') {
- const level = parseInt(batteryLevel);
- batteryBar.style.width = `${level}%`;
-
- // Color based on level
- if (level > 50) {
- batteryBar.className = 'bg-green-500 h-2 rounded-full transition-all';
- } else if (level > 20) {
- batteryBar.className = 'bg-yellow-500 h-2 rounded-full transition-all';
- } else {
- batteryBar.className = 'bg-red-500 h-2 rounded-full transition-all';
- }
- }
- }
-
- // Update power source
- if (document.getElementById('power-source')) {
- document.getElementById('power-source').textContent = data.power_source || '--';
- }
-
- // Update SD card info
- if (document.getElementById('sd-remaining')) {
- const sdRemaining = data.sd_remaining_mb || '--';
- document.getElementById('sd-remaining').textContent = sdRemaining === '--' ? '--' : `${sdRemaining} MB`;
- }
- if (document.getElementById('sd-ratio')) {
- const sdRatio = data.sd_free_ratio || '--';
- document.getElementById('sd-ratio').textContent = sdRatio === '--' ? '--' : `${sdRatio}% free`;
- }
-
- // Update last update timestamp
if (document.getElementById('last-update')) {
const now = new Date();
document.getElementById('last-update').textContent = now.toLocaleTimeString();
@@ -1199,21 +1253,51 @@ function stopAutoRefresh() {
}
}
-// Start auto-refresh and load initial data when page loads
-document.addEventListener('DOMContentLoaded', function() {
+// Bootstrap data from server
+const SLM_BOOTSTRAP_DATA = JSON.parse(document.getElementById('slm-bootstrap-data').textContent);
+
+// Initialize immediately when script loads (HTMX loads this partial after DOMContentLoaded)
+(async function() {
+ console.log('Initializing SLM live view...');
+
+ const unitId = SLM_BOOTSTRAP_DATA.unit_id;
+ const isMeasuring = SLM_BOOTSTRAP_DATA.is_measuring;
+ const measurementStartTime = SLM_BOOTSTRAP_DATA.measurement_start_time;
+ console.log('Is measuring:', isMeasuring);
+ console.log('Measurement start time from backend:', measurementStartTime);
+
+ // Start auto-refresh
startAutoRefresh();
- // Load initial device settings
- const unitId = '{{ unit.id }}';
- getDeviceClock(unitId);
- getIndexNumber(unitId);
- getFrequencyWeighting(unitId);
- getTimeWeighting(unitId);
+ // Load initial device settings with delays (NL-43 requires 1 second between commands)
+ // These are background updates, so we can space them out
+ setTimeout(() => getIndexNumber(unitId), 1500);
+ setTimeout(() => getDeviceClock(unitId), 3000);
+ setTimeout(() => getFrequencyWeighting(unitId), 4500);
+ setTimeout(() => getTimeWeighting(unitId), 6000);
- // Resume measurement timer if device is currently measuring
- const isMeasuring = {{ 'true' if is_measuring else 'false' }};
- resumeMeasurementTimerIfNeeded(unitId, isMeasuring);
-});
+ // Initialize measurement timer if device is currently measuring
+ if (isMeasuring && measurementStartTime) {
+ // Backend has synced the start time from FTP, so database timestamp is now accurate
+ let utcTimestamp = measurementStartTime;
+ if (!utcTimestamp.endsWith('Z') && !utcTimestamp.includes('+') && !utcTimestamp.includes('-', 10)) {
+ utcTimestamp = utcTimestamp + 'Z';
+ }
+
+ const startTimeMs = new Date(utcTimestamp).getTime();
+ localStorage.setItem(TIMER_STORAGE_KEY + unitId, startTimeMs.toString());
+
+ console.log('✓ Timer initialized from synced database timestamp');
+ console.log(' Start time (UTC):', new Date(startTimeMs).toISOString());
+ console.log(' Start time (Local):', new Date(startTimeMs).toString());
+
+ startMeasurementTimer(unitId);
+ } else if (isMeasuring && !measurementStartTime) {
+ // Fallback: Measurement active but no start time - try FTP
+ console.warn('⚠ Measurement active but no start time, fetching from FTP...');
+ setTimeout(() => resumeMeasurementTimerIfNeeded(unitId, isMeasuring), 500);
+ }
+})();
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
@@ -1485,12 +1569,22 @@ async function loadFTPFiles(unitId, path) {
if (isDir) {
html += `
-
-
+
+
${icon}
${escapeHtml(file.name)}
-
${dateText}
+
+
${dateText}
+
+
`;
} else {
@@ -1593,6 +1687,58 @@ async function downloadFTPFile(unitId, filePath, fileName) {
}
}
+async function downloadFTPFolderModal(unitId, folderPath, folderName, btnElement) {
+ // Get the button element - either passed as argument or from event
+ const downloadBtn = btnElement || event.target;
+ const originalText = downloadBtn.innerHTML;
+
+ try {
+ // Show download indicator
+ downloadBtn.innerHTML = '
';
+ downloadBtn.disabled = true;
+
+ const response = await fetch(`/api/slmm/${unitId}/ftp/download-folder`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({ remote_path: folderPath })
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.detail || 'Folder download failed');
+ }
+
+ // The response is a ZIP file
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${folderName}.zip`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+
+ // Reset button
+ downloadBtn.innerHTML = originalText;
+ downloadBtn.disabled = false;
+
+ // Show success message briefly
+ const originalBtnClass = downloadBtn.className;
+ downloadBtn.className = downloadBtn.className.replace('bg-blue-600', 'bg-green-600');
+ setTimeout(() => {
+ downloadBtn.className = originalBtnClass;
+ }, 2000);
+
+ } catch (error) {
+ console.error('Failed to download folder:', error);
+ alert('Failed to download folder: ' + error.message);
+ // Reset button on error
+ downloadBtn.innerHTML = originalText;
+ downloadBtn.disabled = false;
+ }
+}
+
async function downloadToServer(unitId, filePath, fileName) {
try {
const response = await fetch(`/api/slmm/${unitId}/ftp/download`, {