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:
@@ -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();
|
||||
});
|
||||
|
||||
// ========================================
|
||||
|
||||
@@ -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">
|
||||
<option value="seismograph">Seismograph</option>
|
||||
<option value="modem">Modem</option>
|
||||
<option value="sound_level_meter">Sound Level Meter</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -351,6 +352,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sound Level Meter-specific fields -->
|
||||
<div id="editSlmFields" class="hidden space-y-4 border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">Sound Level Meter Information</p>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">SLM Model</label>
|
||||
<input type="text" name="slm_model" id="editSlmModel" placeholder="NL-43"
|
||||
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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Host/IP Address</label>
|
||||
<input type="text" name="slm_host" id="editSlmHost" placeholder="192.168.1.100"
|
||||
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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">TCP Port</label>
|
||||
<input type="number" name="slm_tcp_port" id="editSlmTcpPort" placeholder="2255"
|
||||
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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">FTP Port</label>
|
||||
<input type="number" name="slm_ftp_port" id="editSlmFtpPort" placeholder="21"
|
||||
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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Serial Number</label>
|
||||
<input type="text" name="slm_serial_number" id="editSlmSerialNumber" placeholder="SN123456"
|
||||
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">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Frequency Weighting</label>
|
||||
<select name="slm_frequency_weighting" id="editSlmFrequencyWeighting"
|
||||
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">
|
||||
<option value="">Not set</option>
|
||||
<option value="A">A-weighting</option>
|
||||
<option value="C">C-weighting</option>
|
||||
<option value="Z">Z-weighting (Flat)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Weighting</label>
|
||||
<select name="slm_time_weighting" id="editSlmTimeWeighting"
|
||||
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">
|
||||
<option value="">Not set</option>
|
||||
<option value="F">F (Fast)</option>
|
||||
<option value="S">S (Slow)</option>
|
||||
<option value="I">I (Impulse)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="deployed" id="editDeployedCheckbox" value="true" onchange="toggleEditModemPairing()"
|
||||
@@ -648,20 +699,30 @@
|
||||
const deviceType = document.getElementById('editDeviceTypeSelect').value;
|
||||
const seismoFields = document.getElementById('editSeismographFields');
|
||||
const modemFields = document.getElementById('editModemFields');
|
||||
const slmFields = document.getElementById('editSlmFields');
|
||||
|
||||
if (deviceType === 'seismograph') {
|
||||
seismoFields.classList.remove('hidden');
|
||||
modemFields.classList.add('hidden');
|
||||
// Enable seismograph fields, disable modem fields
|
||||
slmFields.classList.add('hidden');
|
||||
setFieldsDisabled(seismoFields, false);
|
||||
setFieldsDisabled(modemFields, true);
|
||||
setFieldsDisabled(slmFields, true);
|
||||
toggleEditModemPairing();
|
||||
} else {
|
||||
} else if (deviceType === 'modem') {
|
||||
seismoFields.classList.add('hidden');
|
||||
modemFields.classList.remove('hidden');
|
||||
// Enable modem fields, disable seismograph fields
|
||||
slmFields.classList.add('hidden');
|
||||
setFieldsDisabled(seismoFields, true);
|
||||
setFieldsDisabled(modemFields, false);
|
||||
setFieldsDisabled(slmFields, true);
|
||||
} else if (deviceType === 'sound_level_meter') {
|
||||
seismoFields.classList.add('hidden');
|
||||
modemFields.classList.add('hidden');
|
||||
slmFields.classList.remove('hidden');
|
||||
setFieldsDisabled(seismoFields, true);
|
||||
setFieldsDisabled(modemFields, true);
|
||||
setFieldsDisabled(slmFields, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,6 +772,15 @@
|
||||
document.getElementById('editPhoneNumber').value = unit.phone_number;
|
||||
document.getElementById('editHardwareModel').value = unit.hardware_model;
|
||||
|
||||
// SLM fields
|
||||
document.getElementById('editSlmModel').value = unit.slm_model || '';
|
||||
document.getElementById('editSlmHost').value = unit.slm_host || '';
|
||||
document.getElementById('editSlmTcpPort').value = unit.slm_tcp_port || '';
|
||||
document.getElementById('editSlmFtpPort').value = unit.slm_ftp_port || '';
|
||||
document.getElementById('editSlmSerialNumber').value = unit.slm_serial_number || '';
|
||||
document.getElementById('editSlmFrequencyWeighting').value = unit.slm_frequency_weighting || '';
|
||||
document.getElementById('editSlmTimeWeighting').value = unit.slm_time_weighting || '';
|
||||
|
||||
// Store unit ID for form submission
|
||||
document.getElementById('editUnitForm').dataset.unitId = unitId;
|
||||
|
||||
@@ -1206,6 +1276,18 @@
|
||||
alert(`Error renaming unit: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-open edit modal if ?edit=UNIT_ID query parameter is present
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const editUnitId = urlParams.get('edit');
|
||||
if (editUnitId) {
|
||||
// Wait a bit for the page to fully load, then open the edit modal
|
||||
setTimeout(() => {
|
||||
editUnit(editUnitId);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -859,11 +859,6 @@ async function loadRosterTable() {
|
||||
|
||||
function createRosterRow(unit) {
|
||||
const statusBadges = [];
|
||||
if (unit.deployed) {
|
||||
statusBadges.push('<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300">Deployed</span>');
|
||||
} else {
|
||||
statusBadges.push('<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">Benched</span>');
|
||||
}
|
||||
if (unit.retired) {
|
||||
statusBadges.push('<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300">Retired</span>');
|
||||
}
|
||||
@@ -880,8 +875,24 @@ function createRosterRow(unit) {
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">${unit.unit_type}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
${statusBadges.join('')}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="deployed-${unit.id}" value="true"
|
||||
${unit.deployed ? 'checked' : ''}
|
||||
onchange="toggleDeployed('${unit.id}', true)"
|
||||
class="w-4 h-4 text-green-600 focus:ring-green-500">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Deployed</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="deployed-${unit.id}" value="false"
|
||||
${!unit.deployed ? 'checked' : ''}
|
||||
onchange="toggleDeployed('${unit.id}', false)"
|
||||
class="w-4 h-4 text-gray-600 focus:ring-gray-500">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Benched</span>
|
||||
</label>
|
||||
</div>
|
||||
${statusBadges.length > 0 ? '<div class="flex flex-wrap gap-1">' + statusBadges.join('') + '</div>' : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
@@ -896,13 +907,6 @@ function createRosterRow(unit) {
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-right text-sm">
|
||||
<div class="flex justify-end gap-1">
|
||||
<button onclick="toggleDeployed('${unit.id}', ${unit.deployed})"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors"
|
||||
title="${unit.deployed ? 'Bench Unit' : 'Deploy Unit'}">
|
||||
<svg class="w-4 h-4 ${unit.deployed ? 'text-green-600 dark:text-green-400' : 'text-gray-400'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="toggleRetired('${unit.id}', ${unit.retired})"
|
||||
class="p-2 hover:bg-gray-100 dark:hover:bg-gray-600 rounded transition-colors"
|
||||
title="${unit.retired ? 'Unretire Unit' : 'Retire Unit'}">
|
||||
@@ -930,12 +934,12 @@ function createRosterRow(unit) {
|
||||
`;
|
||||
}
|
||||
|
||||
async function toggleDeployed(unitId, currentState) {
|
||||
async function toggleDeployed(unitId, newState) {
|
||||
try {
|
||||
const response = await fetch(`/api/roster/set-deployed/${unitId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `deployed=${!currentState}`
|
||||
body: `deployed=${newState}`
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -969,7 +973,7 @@ async function toggleRetired(unitId, currentState) {
|
||||
}
|
||||
|
||||
function editUnit(unitId) {
|
||||
window.location.href = `/unit/${unitId}`;
|
||||
window.location.href = `/roster?edit=${unitId}`;
|
||||
}
|
||||
|
||||
async function confirmDeleteUnit(unitId) {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button id="editButton" onclick="enterEditMode()" class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors flex items-center gap-2">
|
||||
<button id="editButton" onclick="window.location.href='/roster?edit=' + unitId" class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
|
||||
Reference in New Issue
Block a user