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 %}
+
+
+
-
-
-
+
+
+
Measurement Control
+
+
-
+
-
+
-
+
-
+
-
+
+
+
+
@@ -231,6 +253,116 @@
+
+
+
+
+
+
Store Name & Time
+
+
+
+
+
+
+
+
+
+
Range: 0000-9999. Used for file numbering.
+
+ ⚠️ Warning: Data exists at this index. Starting measurement will overwrite previous data!
+
+
+
+
+
+
+
+
+ --
+
+
+
+
+
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 @@
+
+
+
Sound Level Meter Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+