Add:
- pair_devices.html template for device pairing interface - SLMM device control lock prevents flooding nl43. Fix: - Polling intervals for SLMM. - modem view now list - device pairing much improved. - various other tweaks through out UI. - SLMM Scheduled downloads fixed.
This commit is contained in:
@@ -341,9 +341,20 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Deployed With Modem</label>
|
||||
{% set picker_id = "-detail-seismo" %}
|
||||
{% set input_name = "deployed_with_modem_id" %}
|
||||
{% include "partials/modem_picker.html" with context %}
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
{% set picker_id = "-detail-seismo" %}
|
||||
{% set input_name = "deployed_with_modem_id" %}
|
||||
{% include "partials/modem_picker.html" with context %}
|
||||
</div>
|
||||
<button type="button" onclick="openPairDeviceModal('seismograph')"
|
||||
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg transition-colors flex items-center gap-1"
|
||||
title="Pair with modem">
|
||||
<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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,9 +428,20 @@
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Deployed With Modem</label>
|
||||
{% set picker_id = "-detail-slm" %}
|
||||
{% set input_name = "deployed_with_modem_id" %}
|
||||
{% include "partials/modem_picker.html" with context %}
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
{% set picker_id = "-detail-slm" %}
|
||||
{% set input_name = "deployed_with_modem_id" %}
|
||||
{% include "partials/modem_picker.html" with context %}
|
||||
</div>
|
||||
<button type="button" onclick="openPairDeviceModal('sound_level_meter')"
|
||||
class="px-3 py-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg transition-colors flex items-center gap-1"
|
||||
title="Pair with modem">
|
||||
<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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Select the modem that provides network connectivity for this SLM</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -941,6 +963,16 @@ document.getElementById('editForm').addEventListener('submit', async function(e)
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const deviceType = formData.get('device_type');
|
||||
|
||||
// Fix: FormData contains both modem picker hidden inputs (seismo and slm).
|
||||
// We need to ensure only the correct one is submitted based on device type.
|
||||
// Delete all deployed_with_modem_id entries and re-add the correct one.
|
||||
const modemId = getCorrectModemPickerValue(deviceType);
|
||||
formData.delete('deployed_with_modem_id');
|
||||
if (modemId) {
|
||||
formData.append('deployed_with_modem_id', modemId);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/roster/edit/${unitId}`, {
|
||||
@@ -962,6 +994,19 @@ document.getElementById('editForm').addEventListener('submit', async function(e)
|
||||
}
|
||||
});
|
||||
|
||||
// Get the correct modem picker value based on device type
|
||||
function getCorrectModemPickerValue(deviceType) {
|
||||
if (deviceType === 'seismograph') {
|
||||
const picker = document.getElementById('modem-picker-value-detail-seismo');
|
||||
return picker ? picker.value : '';
|
||||
} else if (deviceType === 'sound_level_meter') {
|
||||
const picker = document.getElementById('modem-picker-value-detail-slm');
|
||||
return picker ? picker.value : '';
|
||||
}
|
||||
// Modems don't have a deployed_with_modem_id
|
||||
return '';
|
||||
}
|
||||
|
||||
// Delete unit
|
||||
async function deleteUnit() {
|
||||
if (!confirm(`Are you sure you want to PERMANENTLY delete unit ${unitId}?\n\nThis action cannot be undone!`)) {
|
||||
@@ -1356,8 +1401,239 @@ loadUnitData().then(() => {
|
||||
loadPhotos();
|
||||
loadUnitHistory();
|
||||
});
|
||||
|
||||
// ===== Pair Device Modal Functions =====
|
||||
let pairModalModems = []; // Cache loaded modems
|
||||
let pairModalDeviceType = ''; // Current device type
|
||||
|
||||
function openPairDeviceModal(deviceType) {
|
||||
const modal = document.getElementById('pairDeviceModal');
|
||||
const searchInput = document.getElementById('pairModemSearch');
|
||||
const hideBenchedCheckbox = document.getElementById('pairHideBenched');
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
pairModalDeviceType = deviceType;
|
||||
|
||||
// Reset search and filter
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (hideBenchedCheckbox) hideBenchedCheckbox.checked = false;
|
||||
|
||||
// Show modal
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Focus search input
|
||||
setTimeout(() => {
|
||||
if (searchInput) searchInput.focus();
|
||||
}, 100);
|
||||
|
||||
// Load available modems
|
||||
loadAvailableModems();
|
||||
}
|
||||
|
||||
function closePairDeviceModal() {
|
||||
const modal = document.getElementById('pairDeviceModal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
pairModalModems = [];
|
||||
}
|
||||
|
||||
async function loadAvailableModems() {
|
||||
const listContainer = document.getElementById('pairModemList');
|
||||
listContainer.innerHTML = '<div class="text-center py-4"><div class="animate-spin inline-block w-6 h-6 border-2 border-seismo-orange border-t-transparent rounded-full"></div><p class="mt-2 text-sm text-gray-500">Loading modems...</p></div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/roster/modems');
|
||||
if (!response.ok) throw new Error('Failed to load modems');
|
||||
|
||||
pairModalModems = await response.json();
|
||||
|
||||
if (pairModalModems.length === 0) {
|
||||
listContainer.innerHTML = '<p class="text-center py-4 text-gray-500">No modems found in roster</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Render the list
|
||||
renderModemList();
|
||||
} catch (error) {
|
||||
listContainer.innerHTML = `<p class="text-center py-4 text-red-500">Error: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function filterPairModemList() {
|
||||
renderModemList();
|
||||
}
|
||||
|
||||
function renderModemList() {
|
||||
const listContainer = document.getElementById('pairModemList');
|
||||
const searchInput = document.getElementById('pairModemSearch');
|
||||
const hideBenchedCheckbox = document.getElementById('pairHideBenched');
|
||||
|
||||
const searchTerm = (searchInput?.value || '').toLowerCase().trim();
|
||||
const hideBenched = hideBenchedCheckbox?.checked || false;
|
||||
|
||||
// Filter modems
|
||||
let filteredModems = pairModalModems.filter(modem => {
|
||||
// Filter by benched status
|
||||
if (hideBenched && !modem.deployed) return false;
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm) {
|
||||
const searchFields = [
|
||||
modem.id,
|
||||
modem.ip_address || '',
|
||||
modem.phone_number || '',
|
||||
modem.note || ''
|
||||
].join(' ').toLowerCase();
|
||||
if (!searchFields.includes(searchTerm)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredModems.length === 0) {
|
||||
listContainer.innerHTML = '<p class="text-center py-4 text-gray-500">No modems match your criteria</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build modem list
|
||||
let html = '';
|
||||
for (const modem of filteredModems) {
|
||||
const displayParts = [modem.id];
|
||||
if (modem.ip_address) displayParts.push(`(${modem.ip_address})`);
|
||||
if (modem.note) displayParts.push(`- ${modem.note.substring(0, 30)}${modem.note.length > 30 ? '...' : ''}`);
|
||||
|
||||
const deployedBadge = modem.deployed
|
||||
? '<span class="px-2 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 text-xs rounded">Deployed</span>'
|
||||
: '<span class="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 text-xs rounded">Benched</span>';
|
||||
|
||||
const pairedBadge = modem.deployed_with_unit_id
|
||||
? `<span class="px-2 py-0.5 bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 text-xs rounded">Paired: ${modem.deployed_with_unit_id}</span>`
|
||||
: '';
|
||||
|
||||
html += `
|
||||
<div class="flex items-center justify-between p-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-0"
|
||||
onclick="selectModemForPairing('${modem.id}', '${displayParts.join(' ').replace(/'/g, "\\'")}')">
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-gray-900 dark:text-white">
|
||||
<span class="text-seismo-orange">${modem.id}</span>
|
||||
${modem.ip_address ? `<span class="text-gray-400 ml-2 font-mono text-sm">${modem.ip_address}</span>` : ''}
|
||||
</div>
|
||||
${modem.note ? `<div class="text-sm text-gray-500 dark:text-gray-400 truncate">${modem.note}</div>` : ''}
|
||||
</div>
|
||||
<div class="flex gap-2 ml-3">
|
||||
${deployedBadge}
|
||||
${pairedBadge}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function selectModemForPairing(modemId, displayText) {
|
||||
// Update the correct picker based on device type
|
||||
let pickerId = '';
|
||||
if (pairModalDeviceType === 'seismograph') {
|
||||
pickerId = '-detail-seismo';
|
||||
} else if (pairModalDeviceType === 'sound_level_meter') {
|
||||
pickerId = '-detail-slm';
|
||||
}
|
||||
|
||||
const valueInput = document.getElementById('modem-picker-value' + pickerId);
|
||||
const searchInput = document.getElementById('modem-picker-search' + pickerId);
|
||||
const clearBtn = document.getElementById('modem-picker-clear' + pickerId);
|
||||
|
||||
if (valueInput) valueInput.value = modemId;
|
||||
if (searchInput) searchInput.value = displayText;
|
||||
if (clearBtn) clearBtn.classList.remove('hidden');
|
||||
|
||||
// Close modal
|
||||
closePairDeviceModal();
|
||||
}
|
||||
|
||||
// Clear pairing (unpair device from modem)
|
||||
function clearPairing(deviceType) {
|
||||
let pickerId = '';
|
||||
if (deviceType === 'seismograph') {
|
||||
pickerId = '-detail-seismo';
|
||||
} else if (deviceType === 'sound_level_meter') {
|
||||
pickerId = '-detail-slm';
|
||||
}
|
||||
|
||||
const valueInput = document.getElementById('modem-picker-value' + pickerId);
|
||||
const searchInput = document.getElementById('modem-picker-search' + pickerId);
|
||||
const clearBtn = document.getElementById('modem-picker-clear' + pickerId);
|
||||
|
||||
if (valueInput) valueInput.value = '';
|
||||
if (searchInput) searchInput.value = '';
|
||||
if (clearBtn) clearBtn.classList.add('hidden');
|
||||
|
||||
closePairDeviceModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Pair Device Modal -->
|
||||
<div id="pairDeviceModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
|
||||
<div class="flex items-center justify-center min-h-screen px-4">
|
||||
<!-- Backdrop -->
|
||||
<div class="fixed inset-0 bg-black/50" onclick="closePairDeviceModal()"></div>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<div class="relative bg-white dark:bg-slate-800 rounded-xl shadow-xl max-w-lg w-full max-h-[80vh] overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Pair with Modem</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Select a modem to pair with this device</p>
|
||||
</div>
|
||||
<button onclick="closePairDeviceModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter -->
|
||||
<div class="px-6 py-3 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-900/50">
|
||||
<div class="flex gap-3 items-center">
|
||||
<!-- Search Input -->
|
||||
<div class="flex-1 relative">
|
||||
<input type="text"
|
||||
id="pairModemSearch"
|
||||
placeholder="Search by ID, IP, or note..."
|
||||
oninput="filterPairModemList()"
|
||||
class="w-full px-4 py-2 pl-10 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 focus:border-seismo-orange text-sm">
|
||||
<svg class="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Hide Benched Toggle -->
|
||||
<label class="flex items-center gap-2 cursor-pointer whitespace-nowrap">
|
||||
<input type="checkbox"
|
||||
id="pairHideBenched"
|
||||
onchange="filterPairModemList()"
|
||||
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Deployed only</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modem List -->
|
||||
<div id="pairModemList" class="max-h-80 overflow-y-auto">
|
||||
<!-- Populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-slate-900">
|
||||
<button onclick="closePairDeviceModal()" class="w-full px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 rounded-lg transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include Project Create Modal for inline project creation -->
|
||||
{% include "partials/project_create_modal.html" %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user