diff --git a/templates/partials/slm_live_view.html b/templates/partials/slm_live_view.html index 50e0006..ae2ff03 100644 --- a/templates/partials/slm_live_view.html +++ b/templates/partials/slm_live_view.html @@ -45,73 +45,95 @@ -
- {% if is_measuring %} - - - Measuring - - {% else %} - - Stopped - - {% endif %} +
+
+ {% if is_measuring %} + + + Measuring + + {% else %} + + Stopped + + {% endif %} +
+ +
+
+ + + + 00:00:00 +
+
- -
- + +
+

Measurement Control

+
+ - + - + - + - + - + + + +
@@ -231,6 +253,116 @@
+ + +
+ +
+

Store Name & Time

+ + +
+ +
+ + + +
+

Range: 0000-9999. Used for file numbering.

+ +
+ + +
+ +
+
+ -- +
+ + +
+

Sync sets device clock to match server time.

+
+
+ + +
+

Measurement Settings

+ + +
+ +
+ + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ +

View all device settings for diagnostics

+
+
+
@@ -477,10 +609,19 @@ async function controlUnit(unitId, action) { const result = await response.json(); if (result.status === 'ok') { + // Handle timer based on action + if (action === 'start') { + startMeasurementTimer(unitId); + } else if (action === 'stop' || action === 'reset') { + stopMeasurementTimer(); + clearMeasurementStartTime(unitId); + } + // Note: pause does not stop timer - it keeps running + // Reload the live view to update status setTimeout(() => { htmx.ajax('GET', `/api/slm-dashboard/live-view/${unitId}`, { - target: '#live-view-panel', + target: '#slm-command-center', swap: 'innerHTML' }); }, 500); @@ -492,6 +633,487 @@ async function controlUnit(unitId, action) { } } +// Start measurement with overwrite check +async function startMeasurementWithCheck(unitId) { + try { + // Check for overwrite risk + const checkResponse = await fetch(`/api/slmm/${unitId}/overwrite-check`); + const checkResult = await checkResponse.json(); + + console.log('Overwrite check result:', checkResult); + + if (checkResult.status === 'ok') { + // API returns data directly, not nested under .data + const overwriteStatus = checkResult.overwrite_status; + const willOverwrite = checkResult.will_overwrite; + + if (willOverwrite === true || overwriteStatus === 'Exist') { + // Data exists - warn user + const confirmed = confirm( + `⚠️ WARNING: Data exists at the current store index!\n\n` + + `Overwrite Status: ${overwriteStatus}\n\n` + + `Starting measurement will OVERWRITE previous data.\n\n` + + `Are you sure you want to continue?` + ); + + if (!confirmed) { + return; // User cancelled + } + } + } + + // Proceed with start + await controlUnit(unitId, 'start'); + + } catch (error) { + console.error('Overwrite check failed:', error); + // Still allow start, but warn user + const proceed = confirm( + 'Could not verify overwrite status.\n\n' + + 'Do you want to start measurement anyway?' + ); + if (proceed) { + await controlUnit(unitId, 'start'); + } + } +} + +// Index Number (Store Name) functions +async function getIndexNumber(unitId) { + try { + const response = await fetch(`/api/slmm/${unitId}/index-number`); + const result = await response.json(); + + if (result.status === 'ok') { + const indexNumber = result.data?.index_number || result.index_number; + document.getElementById('index-number-input').value = parseInt(indexNumber); + + // Check for overwrite risk at this index + await checkOverwriteStatus(unitId); + } else { + alert(`Failed to get index number: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to get index number: ${error.message}`); + } +} + +async function setIndexNumber(unitId) { + const input = document.getElementById('index-number-input'); + const indexValue = parseInt(input.value); + + if (isNaN(indexValue) || indexValue < 0 || indexValue > 9999) { + alert('Please enter a valid index number (0-9999)'); + return; + } + + try { + const response = await fetch(`/api/slmm/${unitId}/index-number`, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ index: indexValue }) + }); + + const result = await response.json(); + + if (result.status === 'ok') { + alert(`Index number set to ${String(indexValue).padStart(4, '0')}`); + // Check for overwrite risk at new index + await checkOverwriteStatus(unitId); + } else { + alert(`Failed to set index number: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to set index number: ${error.message}`); + } +} + +async function checkOverwriteStatus(unitId) { + try { + const response = await fetch(`/api/slmm/${unitId}/overwrite-check`); + const result = await response.json(); + + console.log('Overwrite status check:', result); + + const warningDiv = document.getElementById('index-overwrite-warning'); + + if (result.status === 'ok') { + // API returns data directly, not nested under .data + const overwriteStatus = result.overwrite_status; + const willOverwrite = result.will_overwrite; + + if (willOverwrite === true || overwriteStatus === 'Exist') { + warningDiv.classList.remove('hidden'); + } else { + warningDiv.classList.add('hidden'); + } + } else { + warningDiv.classList.add('hidden'); + } + } catch (error) { + console.error('Failed to check overwrite status:', error); + // Hide warning on error + const warningDiv = document.getElementById('index-overwrite-warning'); + if (warningDiv) { + warningDiv.classList.add('hidden'); + } + } +} + +// Device Clock functions +async function getDeviceClock(unitId) { + try { + const response = await fetch(`/api/slmm/${unitId}/clock`); + const result = await response.json(); + + if (result.status === 'ok') { + const clockValue = result.data?.clock || result.clock; + document.getElementById('device-clock').textContent = clockValue; + } else { + alert(`Failed to get device clock: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to get device clock: ${error.message}`); + } +} + +async function syncDeviceClock(unitId) { + try { + // Format current time for NL43: YYYY/MM/DD,HH:MM:SS + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + + const datetime = `${year}/${month}/${day},${hours}:${minutes}:${seconds}`; + + const response = await fetch(`/api/slmm/${unitId}/clock`, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ datetime: datetime }) + }); + + const result = await response.json(); + + if (result.status === 'ok') { + alert('Device clock synchronized successfully!'); + await getDeviceClock(unitId); + } else { + alert(`Failed to sync clock: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to sync clock: ${error.message}`); + } +} + +// Frequency Weighting functions +async function getFrequencyWeighting(unitId) { + try { + const response = await fetch(`/api/slmm/${unitId}/frequency-weighting?channel=Main`); + const result = await response.json(); + + if (result.status === 'ok') { + const weighting = result.data?.frequency_weighting || result.frequency_weighting; + document.getElementById('frequency-weighting-select').value = weighting; + } else { + alert(`Failed to get frequency weighting: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to get frequency weighting: ${error.message}`); + } +} + +async function setFrequencyWeighting(unitId) { + const select = document.getElementById('frequency-weighting-select'); + const weighting = select.value; + + if (!weighting) { + alert('Please select a frequency weighting'); + return; + } + + try { + const response = await fetch(`/api/slmm/${unitId}/frequency-weighting`, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ weighting: weighting, channel: 'Main' }) + }); + + const result = await response.json(); + + if (result.status === 'ok') { + alert(`Frequency weighting set to ${weighting}`); + } else { + alert(`Failed to set frequency weighting: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to set frequency weighting: ${error.message}`); + } +} + +// Time Weighting functions +async function getTimeWeighting(unitId) { + try { + const response = await fetch(`/api/slmm/${unitId}/time-weighting?channel=Main`); + const result = await response.json(); + + if (result.status === 'ok') { + const weighting = result.data?.time_weighting || result.time_weighting; + document.getElementById('time-weighting-select').value = weighting; + } else { + alert(`Failed to get time weighting: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to get time weighting: ${error.message}`); + } +} + +async function setTimeWeighting(unitId) { + const select = document.getElementById('time-weighting-select'); + const weighting = select.value; + + if (!weighting) { + alert('Please select a time weighting'); + return; + } + + try { + const response = await fetch(`/api/slmm/${unitId}/time-weighting`, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ weighting: weighting, channel: 'Main' }) + }); + + const result = await response.json(); + + if (result.status === 'ok') { + alert(`Time weighting set to ${weighting}`); + } else { + alert(`Failed to set time weighting: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to set time weighting: ${error.message}`); + } +} + +// Get All Settings +async function getAllSettings(unitId) { + try { + const response = await fetch(`/api/slmm/${unitId}/settings/all`); + const result = await response.json(); + + if (result.status === 'ok') { + const settings = result.data?.settings || result.settings; + + // Format settings for display + let message = 'Current Device Settings:\n\n'; + for (const [key, value] of Object.entries(settings)) { + const label = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + message += `${label}: ${value}\n`; + } + + alert(message); + } else { + alert(`Failed to get settings: ${result.detail || 'Unknown error'}`); + } + } catch (error) { + alert(`Failed to get settings: ${error.message}`); + } +} + +// ======================================== +// Measurement Timer +// ======================================== +let measurementTimerInterval = null; +const TIMER_STORAGE_KEY = 'slm_measurement_start_'; + +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()); + + // Show timer container + const container = document.getElementById('elapsed-time-container'); + if (container) { + container.classList.remove('hidden'); + } + + // Update timer immediately + updateElapsedTime(unitId); + + // Update every second + measurementTimerInterval = setInterval(() => { + updateElapsedTime(unitId); + }, 1000); + + console.log('Measurement timer started for', unitId); +} + +function stopMeasurementTimer() { + if (measurementTimerInterval) { + clearInterval(measurementTimerInterval); + measurementTimerInterval = null; + } + + // Hide timer container + const container = document.getElementById('elapsed-time-container'); + if (container) { + container.classList.add('hidden'); + } + + console.log('Measurement timer stopped'); +} + +function updateElapsedTime(unitId) { + const startTimeStr = localStorage.getItem(TIMER_STORAGE_KEY + unitId); + if (!startTimeStr) { + return; + } + + const startTime = parseInt(startTimeStr); + const now = Date.now(); + const elapsedMs = now - startTime; + + // Convert to HH:MM:SS + const hours = Math.floor(elapsedMs / (1000 * 60 * 60)); + const minutes = Math.floor((elapsedMs % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((elapsedMs % (1000 * 60)) / 1000); + + const timeString = + String(hours).padStart(2, '0') + ':' + + String(minutes).padStart(2, '0') + ':' + + String(seconds).padStart(2, '0'); + + const display = document.getElementById('elapsed-time'); + if (display) { + display.textContent = timeString; + } +} + +function clearMeasurementStartTime(unitId) { + localStorage.removeItem(TIMER_STORAGE_KEY + unitId); + console.log('Cleared measurement start time for', unitId); +} + +// Resume timer if measurement is in progress +async function resumeMeasurementTimerIfNeeded(unitId, isMeasuring) { + const startTimeStr = localStorage.getItem(TIMER_STORAGE_KEY + unitId); + + if (isMeasuring && startTimeStr) { + // Measurement is active and we have a start time - resume timer + startMeasurementTimer(unitId); + } else if (!isMeasuring && startTimeStr) { + // Measurement stopped but we have a start time - clear it + clearMeasurementStartTime(unitId); + stopMeasurementTimer(); + } else if (isMeasuring && !startTimeStr) { + // Measurement is active but no start time recorded + // Try to get start time from last folder on FTP + console.log('Measurement active but no start time - fetching from FTP...'); + await fetchStartTimeFromFTP(unitId); + } +} + +// Fetch measurement start time from last folder on FTP +async function fetchStartTimeFromFTP(unitId) { + try { + const response = await fetch(`/api/slmm/${unitId}/ftp/files?path=/NL-43`); + const result = await response.json(); + + if (result.status === 'ok' && result.files && result.files.length > 0) { + // Filter for directories only + const folders = result.files.filter(f => f.is_dir || f.type === 'directory'); + + if (folders.length > 0) { + // Sort by modified timestamp (newest first) or by name + folders.sort((a, b) => { + // Try sorting by modified_timestamp first (ISO format) + if (a.modified_timestamp && b.modified_timestamp) { + return new Date(b.modified_timestamp) - new Date(a.modified_timestamp); + } + // Fall back to sorting by name (descending, assuming YYYYMMDD_HHMMSS format) + return b.name.localeCompare(a.name); + }); + + const lastFolder = folders[0]; + console.log('Last measurement folder:', lastFolder.name); + console.log('Folder details:', lastFolder); + + // Try to parse timestamp from folder name + // Common formats: YYYYMMDD_HHMMSS, YYYY-MM-DD_HH-MM-SS, or use modified time + const startTime = parseFolderTimestamp(lastFolder); + + if (startTime) { + console.log('Parsed start time from folder:', new Date(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'); + startMeasurementTimer(unitId); + } + } else { + // No folders found - start from now + console.warn('No measurement folders found, 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'); + startMeasurementTimer(unitId); + } + } catch (error) { + console.error('Error fetching start time from FTP:', error); + // Fallback - start from now + startMeasurementTimer(unitId); + } +} + +// Parse timestamp from folder name or modified time +function parseFolderTimestamp(folder) { + // Try parsing from folder name first + // Expected formats: YYYYMMDD_HHMMSS or YYYY-MM-DD_HH-MM-SS + const name = folder.name; + + // Pattern: YYYYMMDD_HHMMSS (e.g., 20250114_143052) + const pattern1 = /(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})/; + 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(); + } + + // Pattern: YYYY-MM-DD_HH-MM-SS (e.g., 2025-01-14_14-30-52) + const pattern2 = /(\d{4})-(\d{2})-(\d{2})[_T](\d{2})-(\d{2})-(\d{2})/; + 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(); + } + + // Try using modified_timestamp (ISO format from SLMM) + if (folder.modified_timestamp) { + return new Date(folder.modified_timestamp).getTime(); + } + + // Fallback to modified (string format) + if (folder.modified) { + const parsedTime = new Date(folder.modified).getTime(); + if (!isNaN(parsedTime)) { + return parsedTime; + } + } + + // Could not parse + return null; +} + // Auto-refresh status every 30 seconds let refreshInterval; @@ -577,14 +1199,29 @@ function stopAutoRefresh() { } } -// Start auto-refresh when page loads -document.addEventListener('DOMContentLoaded', startAutoRefresh); +// Start auto-refresh and load initial data when page loads +document.addEventListener('DOMContentLoaded', function() { + startAutoRefresh(); + + // Load initial device settings + const unitId = '{{ unit.id }}'; + getDeviceClock(unitId); + getIndexNumber(unitId); + getFrequencyWeighting(unitId); + getTimeWeighting(unitId); + + // Resume measurement timer if device is currently measuring + const isMeasuring = {{ 'true' if is_measuring else 'false' }}; + resumeMeasurementTimerIfNeeded(unitId, isMeasuring); +}); // Cleanup on page unload window.addEventListener('beforeunload', function() { if (window.currentWebSocket) { window.currentWebSocket.close(); } + // Timer will resume on next page load if measurement is still active + stopMeasurementTimer(); }); // ======================================== diff --git a/templates/roster.html b/templates/roster.html index 30a289f..c596f55 100644 --- a/templates/roster.html +++ b/templates/roster.html @@ -288,6 +288,7 @@ class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"> +
@@ -351,6 +352,56 @@
+ + +