feat: Enhance dashboard with filtering options and sync SLM status

- Added a new filtering system to the dashboard for device types and statuses.
- Implemented asynchronous SLM status synchronization to update the Emitter table.
- Updated the status snapshot endpoint to sync SLM status before generating the snapshot.
- Refactored the dashboard HTML to include filter controls and JavaScript for managing filter state.
- Improved the unit detail page to handle modem associations and cascade updates to paired devices.
- Removed redundant code related to syncing start time for measuring devices.
This commit is contained in:
serversdwn
2026-01-28 20:02:10 +00:00
parent 6492fdff82
commit 5ee6f5eb28
7 changed files with 696 additions and 120 deletions

View File

@@ -43,7 +43,7 @@
</button>
</div>
<div class="flex gap-3">
<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">
<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">
<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>
@@ -153,7 +153,12 @@
</div>
<div>
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">Deployed With Modem</label>
<p id="viewDeployedWithModemId" class="mt-1 text-gray-900 dark:text-white font-medium">--</p>
<p id="viewDeployedWithModemContainer" class="mt-1">
<a id="viewDeployedWithModemLink" href="#" class="text-seismo-orange hover:text-orange-600 font-medium hover:underline hidden">
<span id="viewDeployedWithModemText">--</span>
</a>
<span id="viewDeployedWithModemNoLink" class="text-gray-900 dark:text-white font-medium">--</span>
</p>
</div>
</div>
</div>
@@ -336,8 +341,9 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Deployed With Modem</label>
<input type="text" name="deployed_with_modem_id" id="deployedWithModemId" placeholder="Modem ID"
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">
{% set picker_id = "-detail-seismo" %}
{% set input_name = "deployed_with_modem_id" %}
{% include "partials/modem_picker.html" with context %}
</div>
</div>
</div>
@@ -411,11 +417,9 @@
</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>
<select name="deployed_with_modem_id" id="slmDeployedWithModemId"
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="">No modem assigned</option>
<!-- Options will be populated by JavaScript -->
</select>
{% set picker_id = "-detail-slm" %}
{% set input_name = "deployed_with_modem_id" %}
{% include "partials/modem_picker.html" with context %}
<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>
@@ -442,6 +446,54 @@
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"></textarea>
</div>
<!-- Cascade to Paired Device Section -->
<div id="detailCascadeSection" class="hidden border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="flex items-center gap-2 mb-3">
<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="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>
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">
Also update paired device: <span id="detailPairedDeviceName" class="text-seismo-orange"></span>
</span>
</div>
<input type="hidden" name="cascade_to_unit_id" id="detailCascadeToUnitId" value="">
<div class="grid grid-cols-2 md:grid-cols-3 gap-2 bg-gray-50 dark:bg-slate-700/50 rounded-lg p-3">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="cascade_deployed" id="detailCascadeDeployed" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Deployed status</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="cascade_retired" id="detailCascadeRetired" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Retired status</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="cascade_project" id="detailCascadeProject" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Project</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="cascade_location" id="detailCascadeLocation" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Address</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="cascade_coordinates" id="detailCascadeCoordinates" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Coordinates</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="cascade_note" id="detailCascadeNote" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Notes</span>
</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
Check the fields you want to sync to the paired device
</p>
</div>
<!-- Save/Cancel Buttons -->
<div class="flex gap-3">
<button type="submit" class="flex-1 px-6 py-3 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg font-medium transition-colors">
@@ -482,6 +534,44 @@ async function fetchProjectDisplay(projectId) {
return projectId;
}
// Fetch modem display name (combines id, ip_address, hardware_model)
// Also returns the actual modem ID if found (for updating picker value)
async function fetchModemDisplay(modemIdOrIp) {
if (!modemIdOrIp) return { display: '', modemId: '' };
try {
// First try direct lookup by ID
let response = await fetch(`/api/roster/${encodeURIComponent(modemIdOrIp)}`);
if (response.ok) {
const modem = await response.json();
const parts = [modem.id];
if (modem.ip_address) parts.push(`(${modem.ip_address})`);
if (modem.hardware_model) parts.push(`- ${modem.hardware_model}`);
return { display: parts.join(' ') || modemIdOrIp, modemId: modem.id };
}
// If not found, maybe it's an IP address - search for it
response = await fetch(`/api/roster/search/modems?q=${encodeURIComponent(modemIdOrIp)}`);
if (response.ok) {
// The search returns HTML, so we need to look up differently
// Try fetching all modems and find by IP
const modemsResponse = await fetch('/api/roster/modems');
if (modemsResponse.ok) {
const modems = await modemsResponse.json();
const modem = modems.find(m => m.ip_address === modemIdOrIp);
if (modem) {
const parts = [modem.id];
if (modem.ip_address) parts.push(`(${modem.ip_address})`);
if (modem.hardware_model) parts.push(`- ${modem.hardware_model}`);
return { display: parts.join(' ') || modemIdOrIp, modemId: modem.id };
}
}
}
} catch (e) {
console.error('Failed to fetch modem:', e);
}
return { display: modemIdOrIp, modemId: modemIdOrIp };
}
// Load unit data on page load
async function loadUnitData() {
try {
@@ -634,7 +724,29 @@ function populateViewMode() {
// Seismograph fields
document.getElementById('viewLastCalibrated').textContent = currentUnit.last_calibrated || '--';
document.getElementById('viewNextCalibrationDue').textContent = currentUnit.next_calibration_due || '--';
document.getElementById('viewDeployedWithModemId').textContent = currentUnit.deployed_with_modem_id || '--';
// Deployed with modem - show as clickable link
const modemLink = document.getElementById('viewDeployedWithModemLink');
const modemNoLink = document.getElementById('viewDeployedWithModemNoLink');
const modemText = document.getElementById('viewDeployedWithModemText');
if (currentUnit.deployed_with_modem_id) {
// Fetch modem info to get the actual ID (in case stored as IP) and display text
fetchModemDisplay(currentUnit.deployed_with_modem_id).then(result => {
if (modemText) modemText.textContent = result.display;
if (modemLink) {
modemLink.href = `/unit/${encodeURIComponent(result.modemId)}`;
modemLink.classList.remove('hidden');
}
if (modemNoLink) modemNoLink.classList.add('hidden');
});
} else {
if (modemNoLink) {
modemNoLink.textContent = '--';
modemNoLink.classList.remove('hidden');
}
if (modemLink) modemLink.classList.add('hidden');
}
// Modem fields
document.getElementById('viewIpAddress').textContent = currentUnit.ip_address || '--';
@@ -689,7 +801,24 @@ function populateEditForm() {
// Seismograph fields
document.getElementById('lastCalibrated').value = currentUnit.last_calibrated || '';
document.getElementById('nextCalibrationDue').value = currentUnit.next_calibration_due || '';
document.getElementById('deployedWithModemId').value = currentUnit.deployed_with_modem_id || '';
// Populate modem picker for seismograph (uses -detail-seismo suffix)
const modemPickerValue = document.getElementById('modem-picker-value-detail-seismo');
const modemPickerSearch = document.getElementById('modem-picker-search-detail-seismo');
const modemPickerClear = document.getElementById('modem-picker-clear-detail-seismo');
if (currentUnit.deployed_with_modem_id) {
// Fetch modem display info (handles both modem ID and IP address lookups)
fetchModemDisplay(currentUnit.deployed_with_modem_id).then(result => {
// Update the hidden value with the actual modem ID (in case it was stored as IP)
if (modemPickerValue) modemPickerValue.value = result.modemId || currentUnit.deployed_with_modem_id;
if (modemPickerSearch) modemPickerSearch.value = result.display;
if (modemPickerClear) modemPickerClear.classList.remove('hidden');
});
} else {
if (modemPickerValue) modemPickerValue.value = '';
if (modemPickerSearch) modemPickerSearch.value = '';
if (modemPickerClear) modemPickerClear.classList.add('hidden');
}
// Modem fields
document.getElementById('ipAddress').value = currentUnit.ip_address || '';
@@ -703,10 +832,69 @@ function populateEditForm() {
document.getElementById('slmFrequencyWeighting').value = currentUnit.slm_frequency_weighting || '';
document.getElementById('slmTimeWeighting').value = currentUnit.slm_time_weighting || '';
document.getElementById('slmMeasurementRange').value = currentUnit.slm_measurement_range || '';
document.getElementById('slmDeployedWithModemId').value = currentUnit.deployed_with_modem_id || '';
// Populate modem picker for SLM (uses -detail-slm suffix)
const slmModemPickerValue = document.getElementById('modem-picker-value-detail-slm');
const slmModemPickerSearch = document.getElementById('modem-picker-search-detail-slm');
const slmModemPickerClear = document.getElementById('modem-picker-clear-detail-slm');
if (currentUnit.deployed_with_modem_id) {
// Fetch modem display info (handles both modem ID and IP address lookups)
fetchModemDisplay(currentUnit.deployed_with_modem_id).then(result => {
// Update the hidden value with the actual modem ID (in case it was stored as IP)
if (slmModemPickerValue) slmModemPickerValue.value = result.modemId || currentUnit.deployed_with_modem_id;
if (slmModemPickerSearch) slmModemPickerSearch.value = result.display;
if (slmModemPickerClear) slmModemPickerClear.classList.remove('hidden');
});
} else {
if (slmModemPickerValue) slmModemPickerValue.value = '';
if (slmModemPickerSearch) slmModemPickerSearch.value = '';
if (slmModemPickerClear) slmModemPickerClear.classList.add('hidden');
}
// Show/hide fields based on device type
toggleDetailFields();
// Check for paired device and show cascade section if applicable
checkAndShowCascadeSection();
}
// Check for paired device and show/hide cascade section
async function checkAndShowCascadeSection() {
const cascadeSection = document.getElementById('detailCascadeSection');
const cascadeToUnitId = document.getElementById('detailCascadeToUnitId');
const pairedDeviceName = document.getElementById('detailPairedDeviceName');
if (!cascadeSection) return;
// Reset cascade section
cascadeSection.classList.add('hidden');
if (cascadeToUnitId) cascadeToUnitId.value = '';
if (pairedDeviceName) pairedDeviceName.textContent = '';
// Reset checkboxes
['detailCascadeDeployed', 'detailCascadeRetired', 'detailCascadeProject',
'detailCascadeLocation', 'detailCascadeCoordinates', 'detailCascadeNote'].forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox) checkbox.checked = false;
});
let pairedUnitId = null;
// Check based on device type
if (currentUnit.device_type === 'modem' && currentUnit.deployed_with_unit_id) {
// Modem is paired with a seismograph or SLM
pairedUnitId = currentUnit.deployed_with_unit_id;
} else if ((currentUnit.device_type === 'seismograph' || currentUnit.device_type === 'sound_level_meter') && currentUnit.deployed_with_modem_id) {
// Seismograph or SLM is paired with a modem
pairedUnitId = currentUnit.deployed_with_modem_id;
}
if (pairedUnitId) {
// Show cascade section
cascadeSection.classList.remove('hidden');
if (cascadeToUnitId) cascadeToUnitId.value = pairedUnitId;
if (pairedDeviceName) pairedDeviceName.textContent = pairedUnitId;
}
}
// Toggle device-specific fields