- 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.
567 lines
31 KiB
HTML
567 lines
31 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Pair Devices - Terra-View{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto">
|
|
<!-- Header -->
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Pair Devices</h1>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
Select a recorder (seismograph or SLM) and a modem to create a bidirectional pairing.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Selection Summary Bar -->
|
|
<div id="selection-bar" class="mb-6 p-4 bg-white dark:bg-slate-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center justify-between flex-wrap gap-4">
|
|
<div class="flex items-center gap-6">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Recorder:</span>
|
|
<span id="selected-recorder" class="font-mono font-medium text-gray-900 dark:text-white">None selected</span>
|
|
</div>
|
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
|
|
</svg>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Modem:</span>
|
|
<span id="selected-modem" class="font-mono font-medium text-gray-900 dark:text-white">None selected</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<button id="clear-selection-btn"
|
|
onclick="clearSelection()"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
disabled>
|
|
Clear
|
|
</button>
|
|
<button id="pair-btn"
|
|
onclick="pairDevices()"
|
|
class="px-4 py-2 text-sm font-medium text-white bg-seismo-orange rounded-lg hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
disabled>
|
|
Pair Devices
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Two Column Layout -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Left Column: Recorders (Seismographs + SLMs) -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
|
</svg>
|
|
Recorders
|
|
<span id="recorder-count" class="text-sm font-normal text-gray-500 dark:text-gray-400">({{ recorders|length }})</span>
|
|
</h2>
|
|
</div>
|
|
<!-- Recorder Search & Filters -->
|
|
<div class="space-y-2">
|
|
<input type="text" id="recorder-search" placeholder="Search by ID..."
|
|
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange focus:border-seismo-orange"
|
|
oninput="filterRecorders()">
|
|
<div class="flex items-center gap-4">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" id="recorder-hide-paired" onchange="filterRecorders()" class="rounded border-gray-300 dark:border-gray-600 text-seismo-orange focus:ring-seismo-orange">
|
|
<span class="text-xs text-gray-600 dark:text-gray-400">Hide paired</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" id="recorder-deployed-only" onchange="filterRecorders()" class="rounded border-gray-300 dark:border-gray-600 text-seismo-orange focus:ring-seismo-orange">
|
|
<span class="text-xs text-gray-600 dark:text-gray-400">Deployed only</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="max-h-[600px] overflow-y-auto">
|
|
<div id="recorders-list" class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{% for unit in recorders %}
|
|
<div class="device-row recorder-row p-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors"
|
|
data-id="{{ unit.id }}"
|
|
data-deployed="{{ unit.deployed|lower }}"
|
|
data-paired-with="{{ unit.deployed_with_modem_id or '' }}"
|
|
data-device-type="{{ unit.device_type }}"
|
|
onclick="selectRecorder('{{ unit.id }}')">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full flex items-center justify-center
|
|
{% if unit.device_type == 'slm' %}bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400
|
|
{% else %}bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400{% endif %}">
|
|
{% if unit.device_type == 'slm' %}
|
|
<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="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path>
|
|
</svg>
|
|
{% else %}
|
|
<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="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
|
</svg>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<div class="font-mono font-medium text-gray-900 dark:text-white">{{ unit.id }}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ unit.device_type|capitalize }}
|
|
{% if not unit.deployed %}<span class="text-yellow-600 dark:text-yellow-400">(Benched)</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
{% if unit.deployed_with_modem_id %}
|
|
<span class="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
|
|
→ {{ unit.deployed_with_modem_id }}
|
|
</span>
|
|
{% endif %}
|
|
<div class="w-5 h-5 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center selection-indicator">
|
|
<svg class="w-3 h-3 text-seismo-orange hidden check-icon" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
No recorders found in roster
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Modems -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>
|
|
</svg>
|
|
Modems
|
|
<span id="modem-count" class="text-sm font-normal text-gray-500 dark:text-gray-400">({{ modems|length }})</span>
|
|
</h2>
|
|
</div>
|
|
<!-- Modem Search & Filters -->
|
|
<div class="space-y-2">
|
|
<input type="text" id="modem-search" placeholder="Search by ID, IP, or phone..."
|
|
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange focus:border-seismo-orange"
|
|
oninput="filterModems()">
|
|
<div class="flex items-center gap-4">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" id="modem-hide-paired" onchange="filterModems()" class="rounded border-gray-300 dark:border-gray-600 text-seismo-orange focus:ring-seismo-orange">
|
|
<span class="text-xs text-gray-600 dark:text-gray-400">Hide paired</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" id="modem-deployed-only" onchange="filterModems()" class="rounded border-gray-300 dark:border-gray-600 text-seismo-orange focus:ring-seismo-orange">
|
|
<span class="text-xs text-gray-600 dark:text-gray-400">Deployed only</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="max-h-[600px] overflow-y-auto">
|
|
<div id="modems-list" class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{% for unit in modems %}
|
|
<div class="device-row modem-row p-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors"
|
|
data-id="{{ unit.id }}"
|
|
data-deployed="{{ unit.deployed|lower }}"
|
|
data-paired-with="{{ unit.deployed_with_unit_id or '' }}"
|
|
data-ip="{{ unit.ip_address or '' }}"
|
|
data-phone="{{ unit.phone_number or '' }}"
|
|
onclick="selectModem('{{ unit.id }}')">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-8 h-8 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center text-amber-600 dark:text-amber-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="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"></path>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="font-mono font-medium text-gray-900 dark:text-white">{{ unit.id }}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
{% if unit.ip_address %}<span class="font-mono">{{ unit.ip_address }}</span>{% endif %}
|
|
{% if unit.phone_number %}{% if unit.ip_address %} · {% endif %}{{ unit.phone_number }}{% endif %}
|
|
{% if not unit.ip_address and not unit.phone_number %}Modem{% endif %}
|
|
{% if not unit.deployed %}<span class="text-yellow-600 dark:text-yellow-400">(Benched)</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
{% if unit.deployed_with_unit_id %}
|
|
<span class="px-2 py-1 text-xs rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
|
|
← {{ unit.deployed_with_unit_id }}
|
|
</span>
|
|
{% endif %}
|
|
<div class="w-5 h-5 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center selection-indicator">
|
|
<svg class="w-3 h-3 text-seismo-orange hidden check-icon" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
No modems found in roster
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Existing Pairings Section -->
|
|
<div class="mt-8 bg-white dark:bg-slate-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
|
<svg class="w-5 h-5 text-green-500" 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>
|
|
Existing Pairings
|
|
<span id="pairing-count" class="text-sm font-normal text-gray-500 dark:text-gray-400">({{ pairings|length }})</span>
|
|
</h2>
|
|
</div>
|
|
<div class="max-h-[400px] overflow-y-auto">
|
|
<div id="pairings-list" class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{% for pairing in pairings %}
|
|
<div class="pairing-row p-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
<div class="flex items-center gap-4">
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2 py-1 text-sm font-mono rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400">
|
|
{{ pairing.recorder_id }}
|
|
</span>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ pairing.recorder_type }}</span>
|
|
</div>
|
|
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path>
|
|
</svg>
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2 py-1 text-sm font-mono rounded bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400">
|
|
{{ pairing.modem_id }}
|
|
</span>
|
|
{% if pairing.modem_ip %}
|
|
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">{{ pairing.modem_ip }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<button onclick="unpairDevices('{{ pairing.recorder_id }}', '{{ pairing.modem_id }}')"
|
|
class="p-2 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 rounded-lg transition-colors"
|
|
title="Unpair devices">
|
|
<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="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
No pairings found. Select a recorder and modem above to create one.
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast notification -->
|
|
<div id="toast" class="fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg transform translate-y-full opacity-0 transition-all duration-300 z-50"></div>
|
|
|
|
<script>
|
|
let selectedRecorder = null;
|
|
let selectedModem = null;
|
|
|
|
function selectRecorder(id) {
|
|
// Deselect previous
|
|
document.querySelectorAll('.recorder-row').forEach(row => {
|
|
row.classList.remove('bg-seismo-orange/10', 'dark:bg-seismo-orange/20');
|
|
row.querySelector('.selection-indicator').classList.remove('border-seismo-orange', 'bg-seismo-orange');
|
|
row.querySelector('.selection-indicator').classList.add('border-gray-300', 'dark:border-gray-600');
|
|
row.querySelector('.check-icon').classList.add('hidden');
|
|
});
|
|
|
|
// Toggle selection
|
|
if (selectedRecorder === id) {
|
|
selectedRecorder = null;
|
|
document.getElementById('selected-recorder').textContent = 'None selected';
|
|
} else {
|
|
selectedRecorder = id;
|
|
document.getElementById('selected-recorder').textContent = id;
|
|
|
|
// Highlight selected
|
|
const row = document.querySelector(`.recorder-row[data-id="${id}"]`);
|
|
if (row) {
|
|
row.classList.add('bg-seismo-orange/10', 'dark:bg-seismo-orange/20');
|
|
row.querySelector('.selection-indicator').classList.remove('border-gray-300', 'dark:border-gray-600');
|
|
row.querySelector('.selection-indicator').classList.add('border-seismo-orange', 'bg-seismo-orange');
|
|
row.querySelector('.check-icon').classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
updateButtons();
|
|
}
|
|
|
|
function selectModem(id) {
|
|
// Deselect previous
|
|
document.querySelectorAll('.modem-row').forEach(row => {
|
|
row.classList.remove('bg-seismo-orange/10', 'dark:bg-seismo-orange/20');
|
|
row.querySelector('.selection-indicator').classList.remove('border-seismo-orange', 'bg-seismo-orange');
|
|
row.querySelector('.selection-indicator').classList.add('border-gray-300', 'dark:border-gray-600');
|
|
row.querySelector('.check-icon').classList.add('hidden');
|
|
});
|
|
|
|
// Toggle selection
|
|
if (selectedModem === id) {
|
|
selectedModem = null;
|
|
document.getElementById('selected-modem').textContent = 'None selected';
|
|
} else {
|
|
selectedModem = id;
|
|
document.getElementById('selected-modem').textContent = id;
|
|
|
|
// Highlight selected
|
|
const row = document.querySelector(`.modem-row[data-id="${id}"]`);
|
|
if (row) {
|
|
row.classList.add('bg-seismo-orange/10', 'dark:bg-seismo-orange/20');
|
|
row.querySelector('.selection-indicator').classList.remove('border-gray-300', 'dark:border-gray-600');
|
|
row.querySelector('.selection-indicator').classList.add('border-seismo-orange', 'bg-seismo-orange');
|
|
row.querySelector('.check-icon').classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
updateButtons();
|
|
}
|
|
|
|
function updateButtons() {
|
|
const pairBtn = document.getElementById('pair-btn');
|
|
const clearBtn = document.getElementById('clear-selection-btn');
|
|
|
|
pairBtn.disabled = !(selectedRecorder && selectedModem);
|
|
clearBtn.disabled = !(selectedRecorder || selectedModem);
|
|
}
|
|
|
|
function clearSelection() {
|
|
if (selectedRecorder) selectRecorder(selectedRecorder);
|
|
if (selectedModem) selectModem(selectedModem);
|
|
}
|
|
|
|
function filterRecorders() {
|
|
const searchTerm = document.getElementById('recorder-search').value.toLowerCase().trim();
|
|
const hidePaired = document.getElementById('recorder-hide-paired').checked;
|
|
const deployedOnly = document.getElementById('recorder-deployed-only').checked;
|
|
|
|
let visibleRecorders = 0;
|
|
|
|
document.querySelectorAll('.recorder-row').forEach(row => {
|
|
const id = row.dataset.id.toLowerCase();
|
|
const pairedWith = row.dataset.pairedWith;
|
|
const deployed = row.dataset.deployed === 'true';
|
|
|
|
let show = true;
|
|
if (searchTerm && !id.includes(searchTerm)) show = false;
|
|
if (hidePaired && pairedWith) show = false;
|
|
if (deployedOnly && !deployed) show = false;
|
|
|
|
row.style.display = show ? '' : 'none';
|
|
if (show) visibleRecorders++;
|
|
});
|
|
|
|
document.getElementById('recorder-count').textContent = `(${visibleRecorders})`;
|
|
}
|
|
|
|
function filterModems() {
|
|
const searchTerm = document.getElementById('modem-search').value.toLowerCase().trim();
|
|
const hidePaired = document.getElementById('modem-hide-paired').checked;
|
|
const deployedOnly = document.getElementById('modem-deployed-only').checked;
|
|
|
|
let visibleModems = 0;
|
|
|
|
document.querySelectorAll('.modem-row').forEach(row => {
|
|
const id = row.dataset.id.toLowerCase();
|
|
const ip = (row.dataset.ip || '').toLowerCase();
|
|
const phone = (row.dataset.phone || '').toLowerCase();
|
|
const pairedWith = row.dataset.pairedWith;
|
|
const deployed = row.dataset.deployed === 'true';
|
|
|
|
let show = true;
|
|
if (searchTerm && !id.includes(searchTerm) && !ip.includes(searchTerm) && !phone.includes(searchTerm)) show = false;
|
|
if (hidePaired && pairedWith) show = false;
|
|
if (deployedOnly && !deployed) show = false;
|
|
|
|
row.style.display = show ? '' : 'none';
|
|
if (show) visibleModems++;
|
|
});
|
|
|
|
document.getElementById('modem-count').textContent = `(${visibleModems})`;
|
|
}
|
|
|
|
function saveScrollPositions() {
|
|
const recordersList = document.getElementById('recorders-list').parentElement;
|
|
const modemsList = document.getElementById('modems-list').parentElement;
|
|
const pairingsList = document.getElementById('pairings-list').parentElement;
|
|
|
|
sessionStorage.setItem('pairDevices_recorderScroll', recordersList.scrollTop);
|
|
sessionStorage.setItem('pairDevices_modemScroll', modemsList.scrollTop);
|
|
sessionStorage.setItem('pairDevices_pairingScroll', pairingsList.scrollTop);
|
|
|
|
// Save recorder filter state
|
|
sessionStorage.setItem('pairDevices_recorderSearch', document.getElementById('recorder-search').value);
|
|
sessionStorage.setItem('pairDevices_recorderHidePaired', document.getElementById('recorder-hide-paired').checked);
|
|
sessionStorage.setItem('pairDevices_recorderDeployedOnly', document.getElementById('recorder-deployed-only').checked);
|
|
|
|
// Save modem filter state
|
|
sessionStorage.setItem('pairDevices_modemSearch', document.getElementById('modem-search').value);
|
|
sessionStorage.setItem('pairDevices_modemHidePaired', document.getElementById('modem-hide-paired').checked);
|
|
sessionStorage.setItem('pairDevices_modemDeployedOnly', document.getElementById('modem-deployed-only').checked);
|
|
}
|
|
|
|
function restoreScrollPositions() {
|
|
const recorderScroll = sessionStorage.getItem('pairDevices_recorderScroll');
|
|
const modemScroll = sessionStorage.getItem('pairDevices_modemScroll');
|
|
const pairingScroll = sessionStorage.getItem('pairDevices_pairingScroll');
|
|
|
|
if (recorderScroll) {
|
|
document.getElementById('recorders-list').parentElement.scrollTop = parseInt(recorderScroll);
|
|
}
|
|
if (modemScroll) {
|
|
document.getElementById('modems-list').parentElement.scrollTop = parseInt(modemScroll);
|
|
}
|
|
if (pairingScroll) {
|
|
document.getElementById('pairings-list').parentElement.scrollTop = parseInt(pairingScroll);
|
|
}
|
|
|
|
// Restore recorder filter state
|
|
const recorderSearch = sessionStorage.getItem('pairDevices_recorderSearch');
|
|
const recorderHidePaired = sessionStorage.getItem('pairDevices_recorderHidePaired');
|
|
const recorderDeployedOnly = sessionStorage.getItem('pairDevices_recorderDeployedOnly');
|
|
|
|
if (recorderSearch) document.getElementById('recorder-search').value = recorderSearch;
|
|
if (recorderHidePaired === 'true') document.getElementById('recorder-hide-paired').checked = true;
|
|
if (recorderDeployedOnly === 'true') document.getElementById('recorder-deployed-only').checked = true;
|
|
|
|
// Restore modem filter state
|
|
const modemSearch = sessionStorage.getItem('pairDevices_modemSearch');
|
|
const modemHidePaired = sessionStorage.getItem('pairDevices_modemHidePaired');
|
|
const modemDeployedOnly = sessionStorage.getItem('pairDevices_modemDeployedOnly');
|
|
|
|
if (modemSearch) document.getElementById('modem-search').value = modemSearch;
|
|
if (modemHidePaired === 'true') document.getElementById('modem-hide-paired').checked = true;
|
|
if (modemDeployedOnly === 'true') document.getElementById('modem-deployed-only').checked = true;
|
|
|
|
// Apply filters if any were set
|
|
if (recorderSearch || recorderHidePaired === 'true' || recorderDeployedOnly === 'true') {
|
|
filterRecorders();
|
|
}
|
|
if (modemSearch || modemHidePaired === 'true' || modemDeployedOnly === 'true') {
|
|
filterModems();
|
|
}
|
|
|
|
// Clear stored values
|
|
sessionStorage.removeItem('pairDevices_recorderScroll');
|
|
sessionStorage.removeItem('pairDevices_modemScroll');
|
|
sessionStorage.removeItem('pairDevices_pairingScroll');
|
|
sessionStorage.removeItem('pairDevices_recorderSearch');
|
|
sessionStorage.removeItem('pairDevices_recorderHidePaired');
|
|
sessionStorage.removeItem('pairDevices_recorderDeployedOnly');
|
|
sessionStorage.removeItem('pairDevices_modemSearch');
|
|
sessionStorage.removeItem('pairDevices_modemHidePaired');
|
|
sessionStorage.removeItem('pairDevices_modemDeployedOnly');
|
|
}
|
|
|
|
// Restore scroll positions on page load
|
|
document.addEventListener('DOMContentLoaded', restoreScrollPositions);
|
|
|
|
async function pairDevices() {
|
|
if (!selectedRecorder || !selectedModem) return;
|
|
|
|
const pairBtn = document.getElementById('pair-btn');
|
|
pairBtn.disabled = true;
|
|
pairBtn.textContent = 'Pairing...';
|
|
|
|
try {
|
|
const response = await fetch('/api/roster/pair-devices', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
recorder_id: selectedRecorder,
|
|
modem_id: selectedModem
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
showToast(`Paired ${selectedRecorder} with ${selectedModem}`, 'success');
|
|
// Save scroll positions before reload
|
|
saveScrollPositions();
|
|
setTimeout(() => window.location.reload(), 500);
|
|
} else {
|
|
showToast(result.detail || 'Failed to pair devices', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Error pairing devices: ' + error.message, 'error');
|
|
} finally {
|
|
pairBtn.disabled = false;
|
|
pairBtn.textContent = 'Pair Devices';
|
|
}
|
|
}
|
|
|
|
async function unpairDevices(recorderId, modemId) {
|
|
if (!confirm(`Unpair ${recorderId} from ${modemId}?`)) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/roster/unpair-devices', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
recorder_id: recorderId,
|
|
modem_id: modemId
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
showToast(`Unpaired ${recorderId} from ${modemId}`, 'success');
|
|
// Save scroll positions before reload
|
|
saveScrollPositions();
|
|
setTimeout(() => window.location.reload(), 500);
|
|
} else {
|
|
showToast(result.detail || 'Failed to unpair devices', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Error unpairing devices: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.className = 'fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg transform transition-all duration-300 z-50';
|
|
|
|
if (type === 'success') {
|
|
toast.classList.add('bg-green-500', 'text-white');
|
|
} else if (type === 'error') {
|
|
toast.classList.add('bg-red-500', 'text-white');
|
|
} else {
|
|
toast.classList.add('bg-gray-800', 'text-white');
|
|
}
|
|
|
|
// Show
|
|
toast.classList.remove('translate-y-full', 'opacity-0');
|
|
|
|
// Hide after 3 seconds
|
|
setTimeout(() => {
|
|
toast.classList.add('translate-y-full', 'opacity-0');
|
|
}, 3000);
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.bg-seismo-orange\/10 {
|
|
background-color: rgb(249 115 22 / 0.1);
|
|
}
|
|
.dark\:bg-seismo-orange\/20:is(.dark *) {
|
|
background-color: rgb(249 115 22 / 0.2);
|
|
}
|
|
</style>
|
|
{% endblock %}
|