BIG update: Update to 0.5.1. Added:

-Project management
-Modem Managerment
-Modem/unit pairing

and more
This commit is contained in:
serversdwn
2026-01-28 03:27:50 +00:00
parent 44d7841852
commit 6492fdff82
24 changed files with 2459 additions and 90 deletions

View File

@@ -131,10 +131,8 @@
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">Project ID</label>
<input type="text" name="project_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"
placeholder="PROJ-001">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project</label>
{% include "partials/project_picker.html" with context %}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Location</label>
@@ -159,8 +157,8 @@
</div>
<div id="modemPairingField" class="hidden">
<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" 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 = "-add-seismo" %}
{% include "partials/modem_picker.html" with context %}
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Only needed when deployed</p>
</div>
</div>
@@ -183,6 +181,21 @@
<input type="text" name="hardware_model" placeholder="e.g., Raven XTV"
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">Deployment Type</label>
<select name="deployment_type"
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 assigned</option>
<option value="seismograph">Seismograph</option>
<option value="slm">Sound Level Meter (SLM)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Deployed With Unit</label>
{% set picker_id = "-add-modem" %}
{% set device_type_filter = "" %}
{% include "partials/unit_picker.html" with context %}
</div>
</div>
<!-- Sound Level Meter-specific fields -->
@@ -297,9 +310,9 @@
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">Project ID</label>
<input type="text" name="project_id" id="editProjectId"
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">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project</label>
{% set picker_id = "-edit" %}
{% include "partials/project_picker.html" with context %}
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address</label>
@@ -327,8 +340,8 @@
</div>
<div id="editModemPairingField" class="hidden">
<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="editDeployedWithModemId" 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 = "-edit-seismo" %}
{% include "partials/modem_picker.html" with context %}
</div>
</div>
@@ -350,6 +363,21 @@
<input type="text" name="hardware_model" id="editHardwareModel" placeholder="e.g., Raven XTV"
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">Deployment Type</label>
<select name="deployment_type" id="editDeploymentType"
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 assigned</option>
<option value="seismograph">Seismograph</option>
<option value="slm">Sound Level Meter (SLM)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Deployed With Unit</label>
{% set picker_id = "-edit-modem" %}
{% set device_type_filter = "" %}
{% include "partials/unit_picker.html" with context %}
</div>
</div>
<!-- Sound Level Meter-specific fields -->
@@ -419,6 +447,55 @@
<textarea name="note" id="editNote" rows="3"
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="editCascadeSection" 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="editPairedDeviceName" class="text-seismo-orange"></span>
</span>
</div>
<input type="hidden" name="cascade_to_unit_id" id="editCascadeToUnitId" value="">
<div class="grid grid-cols-2 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="editCascadeDeployed" 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="editCascadeRetired" 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="editCascadeProject" 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="editCascadeLocation" 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="editCascadeCoordinates" 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="editCascadeNote" 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>
<div class="flex gap-3 pt-4">
<button type="submit" class="flex-1 px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors">
Save Changes
@@ -650,11 +727,7 @@
document.getElementById('addUnitForm').addEventListener('htmx:afterRequest', function(event) {
if (event.detail.successful) {
closeAddUnitModal();
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
refreshDeviceList();
// Show success message
alert('Unit added successfully!');
} else {
@@ -692,6 +765,33 @@
function closeEditUnitModal() {
document.getElementById('editUnitModal').classList.add('hidden');
document.getElementById('editUnitForm').reset();
// Also clear the project picker
const projectPickerValue = document.getElementById('project-picker-value-edit');
const projectPickerSearch = document.getElementById('project-picker-search-edit');
const projectPickerClear = document.getElementById('project-picker-clear-edit');
if (projectPickerValue) projectPickerValue.value = '';
if (projectPickerSearch) projectPickerSearch.value = '';
if (projectPickerClear) projectPickerClear.classList.add('hidden');
}
// Fetch project display name for edit modal
async function fetchProjectDisplayForEdit(projectId) {
if (!projectId) return '';
try {
const response = await fetch(`/api/projects/${projectId}`);
if (response.ok) {
const project = await response.json();
const parts = [
project.project_number,
project.client_name,
project.name
].filter(Boolean);
return parts.join(' - ') || projectId;
}
} catch (e) {
console.error('Failed to fetch project:', e);
}
return projectId;
}
// Toggle device-specific fields in edit modal
@@ -753,7 +853,23 @@
document.getElementById('editUnitId').value = unit.id;
document.getElementById('editDeviceTypeSelect').value = unit.device_type;
document.getElementById('editUnitType').value = unit.unit_type;
document.getElementById('editProjectId').value = unit.project_id;
// Populate project picker (uses -edit suffix)
const projectPickerValue = document.getElementById('project-picker-value-edit');
const projectPickerSearch = document.getElementById('project-picker-search-edit');
const projectPickerClear = document.getElementById('project-picker-clear-edit');
if (projectPickerValue) projectPickerValue.value = unit.project_id || '';
if (unit.project_id) {
// Fetch project display name
fetchProjectDisplayForEdit(unit.project_id).then(displayText => {
if (projectPickerSearch) projectPickerSearch.value = displayText;
if (projectPickerClear) projectPickerClear.classList.remove('hidden');
});
} else {
if (projectPickerSearch) projectPickerSearch.value = '';
if (projectPickerClear) projectPickerClear.classList.add('hidden');
}
document.getElementById('editAddress').value = unit.address;
document.getElementById('editCoordinates').value = unit.coordinates;
document.getElementById('editNote').value = unit.note;
@@ -765,12 +881,62 @@
// Seismograph fields
document.getElementById('editLastCalibrated').value = unit.last_calibrated;
document.getElementById('editNextCalibrationDue').value = unit.next_calibration_due;
document.getElementById('editDeployedWithModemId').value = unit.deployed_with_modem_id;
// Populate modem picker for seismograph (uses -edit-seismo suffix)
const modemPickerValue = document.getElementById('modem-picker-value-edit-seismo');
const modemPickerSearch = document.getElementById('modem-picker-search-edit-seismo');
const modemPickerClear = document.getElementById('modem-picker-clear-edit-seismo');
if (modemPickerValue) modemPickerValue.value = unit.deployed_with_modem_id || '';
if (unit.deployed_with_modem_id) {
// Fetch modem display (ID + IP + note)
fetch(`/api/roster/${unit.deployed_with_modem_id}`)
.then(r => r.ok ? r.json() : null)
.then(modem => {
if (modem && modemPickerSearch) {
let display = modem.id;
if (modem.ip_address) display += ` - ${modem.ip_address}`;
if (modem.note) display += ` - ${modem.note}`;
modemPickerSearch.value = display;
if (modemPickerClear) modemPickerClear.classList.remove('hidden');
}
})
.catch(() => {
if (modemPickerSearch) modemPickerSearch.value = unit.deployed_with_modem_id;
});
} else {
if (modemPickerSearch) modemPickerSearch.value = '';
if (modemPickerClear) modemPickerClear.classList.add('hidden');
}
// Modem fields
document.getElementById('editIpAddress').value = unit.ip_address;
document.getElementById('editPhoneNumber').value = unit.phone_number;
document.getElementById('editHardwareModel').value = unit.hardware_model;
document.getElementById('editDeploymentType').value = unit.deployment_type || '';
// Populate unit picker for modem (uses -edit-modem suffix)
const unitPickerValue = document.getElementById('unit-picker-value-edit-modem');
const unitPickerSearch = document.getElementById('unit-picker-search-edit-modem');
const unitPickerClear = document.getElementById('unit-picker-clear-edit-modem');
if (unitPickerValue) unitPickerValue.value = unit.deployed_with_unit_id || '';
if (unit.deployed_with_unit_id) {
// Fetch unit display (ID + note)
fetch(`/api/roster/${unit.deployed_with_unit_id}`)
.then(r => r.ok ? r.json() : null)
.then(linkedUnit => {
if (linkedUnit && unitPickerSearch) {
const display = linkedUnit.note ? `${linkedUnit.id} - ${linkedUnit.note}` : linkedUnit.id;
unitPickerSearch.value = display;
if (unitPickerClear) unitPickerClear.classList.remove('hidden');
}
})
.catch(() => {
if (unitPickerSearch) unitPickerSearch.value = unit.deployed_with_unit_id;
});
} else {
if (unitPickerSearch) unitPickerSearch.value = '';
if (unitPickerClear) unitPickerClear.classList.add('hidden');
}
// SLM fields
document.getElementById('editSlmModel').value = unit.slm_model || '';
@@ -781,6 +947,35 @@
document.getElementById('editSlmFrequencyWeighting').value = unit.slm_frequency_weighting || '';
document.getElementById('editSlmTimeWeighting').value = unit.slm_time_weighting || '';
// Cascade section - show if there's a paired device
const cascadeSection = document.getElementById('editCascadeSection');
const cascadeToUnitId = document.getElementById('editCascadeToUnitId');
const pairedDeviceName = document.getElementById('editPairedDeviceName');
// Determine paired device based on device type
let pairedUnitId = null;
if (unit.device_type === 'modem' && unit.deployed_with_unit_id) {
pairedUnitId = unit.deployed_with_unit_id;
} else if ((unit.device_type === 'seismograph' || unit.device_type === 'sound_level_meter') && unit.deployed_with_modem_id) {
pairedUnitId = unit.deployed_with_modem_id;
}
if (pairedUnitId) {
cascadeToUnitId.value = pairedUnitId;
pairedDeviceName.textContent = pairedUnitId;
cascadeSection.classList.remove('hidden');
// Reset checkboxes
document.getElementById('editCascadeDeployed').checked = false;
document.getElementById('editCascadeRetired').checked = false;
document.getElementById('editCascadeProject').checked = false;
document.getElementById('editCascadeLocation').checked = false;
document.getElementById('editCascadeCoordinates').checked = false;
document.getElementById('editCascadeNote').checked = false;
} else {
cascadeToUnitId.value = '';
cascadeSection.classList.add('hidden');
}
// Store unit ID for form submission
document.getElementById('editUnitForm').dataset.unitId = unitId;
@@ -814,11 +1009,7 @@
if (response.ok) {
closeEditUnitModal();
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
refreshDeviceList();
alert('Unit updated successfully!');
} else {
const result = await response.json();
@@ -846,11 +1037,7 @@
});
if (response.ok) {
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
refreshDeviceList();
alert(`Unit ${deployed ? 'deployed' : 'benched'} successfully!`);
} else {
const result = await response.json();
@@ -878,11 +1065,7 @@
});
if (response.ok) {
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
refreshDeviceList();
alert(`Unit ${unitId} moved to ignore list`);
} else {
const result = await response.json();
@@ -905,11 +1088,7 @@
});
if (response.ok) {
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
refreshDeviceList();
alert(`Unit ${unitId} deleted successfully`);
} else {
const result = await response.json();
@@ -948,11 +1127,7 @@
`;
resultDiv.classList.remove('hidden');
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
refreshDeviceList();
// Close modal after 2 seconds
setTimeout(() => closeImportModal(), 2000);
@@ -968,35 +1143,26 @@
}
});
// Handle roster tab switching with auto-refresh
let currentRosterEndpoint = '/partials/roster-deployed'; // Default to deployed tab
// Refresh device list (applies current client-side filters after load)
function refreshDeviceList() {
htmx.ajax('GET', '/partials/devices-all', {
target: '#device-content',
swap: 'innerHTML'
}).then(() => {
// Re-apply filters after content loads
setTimeout(filterDevices, 100);
});
}
document.addEventListener('DOMContentLoaded', function() {
const tabButtons = document.querySelectorAll('.roster-tab-button');
tabButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active-roster-tab class from all buttons
tabButtons.forEach(btn => btn.classList.remove('active-roster-tab'));
// Add active-roster-tab class to clicked button
this.classList.add('active-roster-tab');
// Update current endpoint for auto-refresh
currentRosterEndpoint = this.getAttribute('data-endpoint');
});
});
// Auto-refresh the current active tab every 10 seconds
// Auto-refresh device list every 30 seconds (increased from 10s to reduce flicker)
setInterval(() => {
const rosterContent = document.getElementById('roster-content');
if (rosterContent) {
// Use HTMX to trigger a refresh of the current endpoint
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
const deviceContent = document.getElementById('device-content');
if (deviceContent && !document.querySelector('.modal:not(.hidden)')) {
// Only auto-refresh if no modal is open
refreshDeviceList();
}
}, 10000); // 10 seconds
}, 30000);
});
// Un-ignore Unit (remove from ignored list)
@@ -1345,4 +1511,7 @@
}
</style>
<!-- Include Project Create Modal for inline project creation -->
{% include "partials/project_create_modal.html" %}
{% endblock %}