Add Sound Level Meter support to roster management

- Updated roster.html to include a new option for Sound Level Meter in the device type selection.
- Added specific fields for Sound Level Meter information, including model, host/IP address, TCP and FTP ports, serial number, frequency weighting, and time weighting.
- Enhanced JavaScript to handle the visibility and state of Sound Level Meter fields based on the selected device type.
- Modified the unit editing functionality to populate Sound Level Meter fields with existing data when editing a unit.
- Updated settings.html to change the deployment status display from badges to radio buttons for better user interaction.
- Adjusted the toggleDeployed function to accept the new state directly instead of the current state.
- Changed the edit button in unit_detail.html to redirect to the roster edit page with the appropriate unit ID.
This commit is contained in:
serversdwn
2026-01-14 21:59:13 +00:00
parent be83cb3fe7
commit d349af9444
4 changed files with 805 additions and 82 deletions

View File

@@ -45,73 +45,95 @@
</button>
<!-- Measurement Status Badge -->
<div>
{% if is_measuring %}
<span class="px-4 py-2 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 rounded-lg font-medium flex items-center">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
Measuring
</span>
{% else %}
<span class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium">
Stopped
</span>
{% endif %}
<div class="flex flex-col items-end gap-2">
<div>
{% if is_measuring %}
<span class="px-4 py-2 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-400 rounded-lg font-medium flex items-center">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
Measuring
</span>
{% else %}
<span class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg font-medium">
Stopped
</span>
{% endif %}
</div>
<!-- Elapsed Time Display -->
<div id="elapsed-time-container" class="{% if not is_measuring %}hidden{% endif %}">
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span id="elapsed-time" class="font-mono font-medium text-gray-900 dark:text-white">00:00:00</span>
</div>
</div>
</div>
</div>
</div>
<!-- Control Buttons -->
<div class="flex gap-2 mb-6">
<button onclick="controlUnit('{{ unit.id }}', 'start')"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Start
</button>
<!-- Measurement Controls -->
<div class="mb-6 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Measurement Control</h3>
<div class="flex gap-2 flex-wrap">
<button onclick="startMeasurementWithCheck('{{ unit.id }}')"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Start
</button>
<button onclick="controlUnit('{{ unit.id }}', 'pause')"
class="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Pause
</button>
<button onclick="controlUnit('{{ unit.id }}', 'pause')"
class="px-4 py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Pause
</button>
<button onclick="controlUnit('{{ unit.id }}', 'stop')"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>
</svg>
Stop
</button>
<button onclick="controlUnit('{{ unit.id }}', 'stop')"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>
</svg>
Stop
</button>
<button onclick="controlUnit('{{ unit.id }}', 'reset')"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Reset
</button>
<button onclick="controlUnit('{{ unit.id }}', 'reset')"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Reset
</button>
<button id="start-stream-btn" onclick="initLiveDataStream('{{ unit.id }}')"
class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg flex items-center ml-auto">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
Start Live Stream
</button>
<button onclick="controlUnit('{{ unit.id }}', 'store')"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
</svg>
Store
</button>
<button id="stop-stream-btn" onclick="stopLiveDataStream()" style="display: none;"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center ml-auto">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>
</svg>
Stop Live Stream
</button>
<button id="start-stream-btn" onclick="initLiveDataStream('{{ unit.id }}')"
class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg flex items-center ml-auto">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
Start Live Stream
</button>
<button id="stop-stream-btn" onclick="stopLiveDataStream()" style="display: none;"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center ml-auto">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>
</svg>
Stop Live Stream
</button>
</div>
</div>
<!-- Current Metrics -->
@@ -231,6 +253,116 @@
</div>
</div>
</div>
<!-- Device Settings & Commands -->
<div class="mt-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Store Name & Clock -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">Store Name & Time</h3>
<!-- Store Name (Index Number) -->
<div class="mb-4">
<label class="block text-xs text-gray-600 dark:text-gray-400 mb-2">Store Name (Index Number)</label>
<div class="flex items-center gap-2">
<input type="number" id="index-number-input"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
min="0" max="9999" placeholder="0000">
<button onclick="getIndexNumber('{{ unit.id }}')"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
Get
</button>
<button onclick="setIndexNumber('{{ unit.id }}')"
class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg text-sm">
Set
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Range: 0000-9999. Used for file numbering.</p>
<div id="index-overwrite-warning" class="hidden mt-2 p-2 bg-red-100 dark:bg-red-900/30 border border-red-300 dark:border-red-700 rounded text-xs text-red-800 dark:text-red-400">
⚠️ <strong>Warning:</strong> Data exists at this index. Starting measurement will overwrite previous data!
</div>
</div>
<!-- Device Clock -->
<div>
<label class="block text-xs text-gray-600 dark:text-gray-400 mb-2">Device Clock</label>
<div class="flex items-center gap-2">
<div id="device-clock" class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-700/50 text-gray-900 dark:text-white font-mono text-sm">
--
</div>
<button onclick="getDeviceClock('{{ unit.id }}')"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
Refresh
</button>
<button onclick="syncDeviceClock('{{ unit.id }}')"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm">
Sync
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Sync sets device clock to match server time.</p>
</div>
</div>
<!-- Measurement Settings -->
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">Measurement Settings</h3>
<!-- Frequency Weighting -->
<div class="mb-4">
<label class="block text-xs text-gray-600 dark:text-gray-400 mb-2">Frequency Weighting</label>
<div class="flex items-center gap-2">
<select id="frequency-weighting-select"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
<option value="">--</option>
<option value="A">A</option>
<option value="C">C</option>
<option value="Z">Z</option>
</select>
<button onclick="getFrequencyWeighting('{{ unit.id }}')"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
Get
</button>
<button onclick="setFrequencyWeighting('{{ unit.id }}')"
class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg text-sm">
Set
</button>
</div>
</div>
<!-- Time Weighting -->
<div class="mb-4">
<label class="block text-xs text-gray-600 dark:text-gray-400 mb-2">Time Weighting</label>
<div class="flex items-center gap-2">
<select id="time-weighting-select"
class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
<option value="">--</option>
<option value="F">F (Fast)</option>
<option value="S">S (Slow)</option>
<option value="I">I (Impulse)</option>
</select>
<button onclick="getTimeWeighting('{{ unit.id }}')"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
Get
</button>
<button onclick="setTimeWeighting('{{ unit.id }}')"
class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg text-sm">
Set
</button>
</div>
</div>
<!-- All Settings Query -->
<div>
<button onclick="getAllSettings('{{ unit.id }}')"
class="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm flex items-center justify-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path>
</svg>
Query All Settings
</button>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 text-center">View all device settings for diagnostics</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@@ -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();
});
// ========================================