-
-
+
+
+
+
+
+
Recurring Schedules
+
Automated patterns that generate scheduled actions
+
+
+
+
Loading recurring schedules...
-
-
-
-
-
Bulk Import — Select Folder
-
- Select your data folder directly — no zipping needed. Expected structure:
- [date]/[NRL name]/[Auto_####]/.
- NRL folders are matched to locations by name. MP3s are stored; Excel exports are skipped.
-
-
-
-
-
-
-
+
+
+
+
Upcoming Actions
+
Scheduled start/stop/download actions
-
-
-
-
+
+
+
+
Loading scheduled actions...
-
-
-
@@ -350,30 +374,6 @@
-
-
-
-
-
-
-
-
+
+
+
+
+ Sound Monitoring
+
+
+
+
+
+
+
+
+
+
+
@@ -792,34 +826,52 @@ async function quickUpdateStatus(newStatus) {
// Tab switching
function switchTab(tabName) {
- // Hide all tab panels
- document.querySelectorAll('.tab-panel').forEach(panel => {
- panel.classList.add('hidden');
- });
-
- // Reset all tab buttons
+ document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.add('hidden'));
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('border-seismo-orange', 'text-seismo-orange');
button.classList.add('border-transparent', 'text-gray-600', 'dark:text-gray-400');
});
- // Show selected tab panel
const panel = document.getElementById(`${tabName}-tab`);
- if (panel) {
- panel.classList.remove('hidden');
- }
+ if (panel) panel.classList.remove('hidden');
- // Highlight selected tab button
const button = document.querySelector(`[data-tab="${tabName}"]`);
if (button) {
button.classList.remove('border-transparent', 'text-gray-600', 'dark:text-gray-400');
button.classList.add('border-seismo-orange', 'text-seismo-orange');
}
- // Persist active tab in URL hash so refresh stays on this tab
history.replaceState(null, '', `#${tabName}`);
}
+function switchVibSubTab(name) {
+ document.querySelectorAll('.vib-sub-panel').forEach(p => p.classList.add('hidden'));
+ document.querySelectorAll('.vib-sub-tab').forEach(b => {
+ b.classList.remove('border-seismo-orange', 'text-seismo-orange');
+ b.classList.add('border-transparent', 'text-gray-500', 'dark:text-gray-400');
+ });
+ document.getElementById(`vib-sub-${name}`)?.classList.remove('hidden');
+ const btn = document.getElementById(`vib-sub-${name}-btn`);
+ if (btn) {
+ btn.classList.remove('border-transparent', 'text-gray-500', 'dark:text-gray-400');
+ btn.classList.add('border-seismo-orange', 'text-seismo-orange');
+ }
+}
+
+function switchSoundSubTab(name) {
+ document.querySelectorAll('.sound-sub-panel').forEach(p => p.classList.add('hidden'));
+ document.querySelectorAll('.sound-sub-tab').forEach(b => {
+ b.classList.remove('border-seismo-orange', 'text-seismo-orange');
+ b.classList.add('border-transparent', 'text-gray-500', 'dark:text-gray-400');
+ });
+ document.getElementById(`sound-sub-${name}`)?.classList.remove('hidden');
+ const btn = document.getElementById(`sound-sub-${name}-btn`);
+ if (btn) {
+ btn.classList.remove('border-transparent', 'text-gray-500', 'dark:text-gray-400');
+ btn.classList.add('border-seismo-orange', 'text-seismo-orange');
+ }
+}
+
// Load project details
async function loadProjectDetails() {
try {
@@ -849,23 +901,20 @@ async function loadProjectDetails() {
if (modeRadio) modeRadio.checked = true;
settingsUpdateModeStyles();
- // Update tab labels and visibility based on active modules
+ // Show/hide module tabs based on active modules
const hasSoundModule = projectModules.includes('sound_monitoring');
const hasVibrationModule = projectModules.includes('vibration_monitoring');
- if (hasSoundModule && !hasVibrationModule) {
- document.getElementById('locations-tab-label').textContent = 'NRLs';
- document.getElementById('locations-header').textContent = 'Noise Recording Locations';
- document.getElementById('add-location-label').textContent = 'Add NRL';
- }
- // Monitoring Sessions and Data Files tabs are sound-only
- // Data Files also hides the FTP browser section for manual projects
const isRemote = mode === 'remote';
- document.getElementById('sessions-tab-btn').classList.toggle('hidden', !hasSoundModule);
- document.getElementById('data-tab-btn').classList.toggle('hidden', !hasSoundModule);
- // Schedules and Assigned Units: hidden when no sound module; for sound, only show if remote
- document.getElementById('schedules-tab-btn')?.classList.toggle('hidden', !hasSoundModule || !isRemote);
- document.getElementById('units-tab-btn')?.classList.toggle('hidden', !hasSoundModule || !isRemote);
- // FTP browser within Data Files tab
+
+ document.getElementById('vibration-tab-btn').classList.toggle('hidden', !hasVibrationModule);
+ document.getElementById('sound-tab-btn').classList.toggle('hidden', !hasSoundModule);
+ document.getElementById('sound-settings-section')?.classList.toggle('hidden', !hasSoundModule);
+
+ // Within Sound: show Assigned Units + Schedules sub-tabs only for remote projects
+ document.getElementById('sound-sub-units-btn')?.classList.toggle('hidden', !isRemote);
+ document.getElementById('sound-sub-schedules-btn')?.classList.toggle('hidden', !isRemote);
+
+ // FTP browser: only show for remote projects
document.getElementById('ftp-browser')?.classList.toggle('hidden', !isRemote);
document.getElementById('settings-error').classList.add('hidden');
@@ -983,7 +1032,14 @@ function updateModeLabels() {
}
}
+// Tracks the active location type tab so "Add Location" opens with the right type
+let _activeLocationType = null;
+function setActiveLocationType(type) {
+ _activeLocationType = type;
+}
+
function openLocationModal(defaultType) {
+ defaultType = defaultType || _activeLocationType || defaultType;
editingLocationId = null;
document.getElementById('location-modal-title').textContent = 'Add Location';
document.getElementById('location-id').value = '';
@@ -1110,12 +1166,8 @@ document.getElementById('location-form').addEventListener('submit', async functi
}
closeLocationModal();
+ refreshLocationLists();
refreshProjectDashboard();
- // Refresh locations tab if visible
- htmx.ajax('GET', `/api/projects/${projectId}/locations`, {
- target: '#project-locations',
- swap: 'innerHTML'
- });
} catch (err) {
const errorEl = document.getElementById('location-error');
errorEl.textContent = err.message || 'Failed to save location.';
@@ -1123,6 +1175,15 @@ document.getElementById('location-form').addEventListener('submit', async functi
}
});
+function refreshLocationLists() {
+ htmx.ajax('GET', `/api/projects/${projectId}/locations?location_type=sound`, {
+ target: '#sound-locations', swap: 'innerHTML'
+ });
+ htmx.ajax('GET', `/api/projects/${projectId}/locations?location_type=vibration`, {
+ target: '#vibration-locations', swap: 'innerHTML'
+ });
+}
+
async function deleteLocation(locationId) {
if (!confirm('Delete this location?')) return;
@@ -1134,11 +1195,8 @@ async function deleteLocation(locationId) {
const data = await response.json().catch(() => ({}));
throw new Error(data.detail || 'Failed to delete location');
}
+ refreshLocationLists();
refreshProjectDashboard();
- htmx.ajax('GET', `/api/projects/${projectId}/locations`, {
- target: '#project-locations',
- swap: 'innerHTML'
- });
} catch (err) {
alert(err.message || 'Failed to delete location.');
}
@@ -1212,11 +1270,8 @@ document.getElementById('assign-form').addEventListener('submit', async function
throw new Error(data.detail || 'Failed to assign unit');
}
closeAssignModal();
+ refreshLocationLists();
refreshProjectDashboard();
- htmx.ajax('GET', `/api/projects/${projectId}/locations`, {
- target: '#project-locations',
- swap: 'innerHTML'
- });
} catch (err) {
const errorEl = document.getElementById('assign-error');
errorEl.textContent = err.message || 'Failed to assign unit.';
@@ -1235,11 +1290,8 @@ async function unassignUnit(assignmentId) {
const data = await response.json().catch(() => ({}));
throw new Error(data.detail || 'Failed to unassign unit');
}
+ refreshLocationLists();
refreshProjectDashboard();
- htmx.ajax('GET', `/api/projects/${projectId}/locations`, {
- target: '#project-locations',
- swap: 'innerHTML'
- });
} catch (err) {
alert(err.message || 'Failed to unassign unit.');
}
@@ -1839,11 +1891,21 @@ function submitUploadAll() {
document.addEventListener('DOMContentLoaded', function() {
loadProjectDetails();
- // Restore tab from URL hash (e.g. #schedules, #settings)
+ // Restore tab from URL hash
const hash = window.location.hash.replace('#', '');
- const validTabs = ['overview', 'locations', 'units', 'schedules', 'sessions', 'data', 'settings'];
- if (hash && validTabs.includes(hash)) {
- switchTab(hash);
+ const validTabs = ['overview', 'vibration', 'sound', 'settings'];
+ // Backwards compat: map old tab names to new ones
+ const hashMap = { locations: 'sound', units: 'sound', schedules: 'sound', sessions: 'sound', data: 'sound' };
+ const subTabMap = { units: 'units', schedules: 'schedules', sessions: 'sessions', data: 'data' };
+ if (hash) {
+ const mappedTab = hashMap[hash] || hash;
+ if (validTabs.includes(mappedTab)) {
+ switchTab(mappedTab);
+ // Open the relevant sub-tab for backwards compat
+ if (subTabMap[hash]) {
+ switchSoundSubTab(subTabMap[hash]);
+ }
+ }
}
});
diff --git a/templates/settings.html b/templates/settings.html
index c5d1c82..bd2d603 100644
--- a/templates/settings.html
+++ b/templates/settings.html
@@ -355,6 +355,25 @@