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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user