+
+
+
+ Module status
+
+ Active
+ On hold
+ Completed
+
+
+
+
+
+
Failed to load events: ${lsEsc(e.message)}
`;
}
}
-function _renderProjectEvents(events, total, container) {
+// Rebuild the Location dropdown from whatever locations actually have events in
+// the current fetch, preserving the operator's current selection if still valid.
+function _pvePopulateLocations() {
+ const sel = document.getElementById('pve-loc');
+ if (!sel) return;
+ const prev = sel.value;
+ const seen = new Map();
+ _pveAllEvents.forEach(ev => {
+ if (ev.location_id && !seen.has(ev.location_id)) seen.set(ev.location_id, ev.location_name || ev.location_id);
+ });
+ const opts = ['
All locations '];
+ [...seen.entries()]
+ .sort((a, b) => String(a[1]).localeCompare(String(b[1])))
+ .forEach(([id, name]) => opts.push(`
${lsEsc(name)} `));
+ sel.innerHTML = opts.join('');
+ if (prev && seen.has(prev)) sel.value = prev;
+}
+
+function _pveSortBy(key) {
+ if (_pveSort.key === key) {
+ _pveSort.dir = (_pveSort.dir === 'asc') ? 'desc' : 'asc';
+ } else {
+ _pveSort.key = key;
+ _pveSort.dir = 'desc'; // numbers + dates most useful high→low first
+ }
+ _pveApplyAndRender();
+}
+
+const _PVE_NUM_KEYS = new Set(['tran_ppv', 'vert_ppv', 'long_ppv', 'peak_vector_sum', 'mic_ppv']);
+const _PVE_STR_KEYS = new Set(['location_name', 'serial']);
+
+function _pveApplyAndRender() {
+ const container = document.getElementById('pve-container');
+ if (!container) return;
+
+ const locId = document.getElementById('pve-loc')?.value || '';
+ let rows = locId ? _pveAllEvents.filter(ev => ev.location_id === locId) : _pveAllEvents.slice();
+
+ const { key, dir } = _pveSort;
+ const mul = dir === 'asc' ? 1 : -1;
+ rows.sort((a, b) => {
+ if (_PVE_NUM_KEYS.has(key)) {
+ const av = (a[key] == null) ? -Infinity : Number(a[key]);
+ const bv = (b[key] == null) ? -Infinity : Number(b[key]);
+ return (av - bv) * mul;
+ }
+ if (_PVE_STR_KEYS.has(key)) {
+ return String(a[key] || '').toLowerCase().localeCompare(String(b[key] || '').toLowerCase()) * mul;
+ }
+ // timestamp — ISO strings sort lexicographically
+ return String(a.timestamp || '').localeCompare(String(b.timestamp || '')) * mul;
+ });
+
+ _renderProjectEvents(rows, container, locId);
+}
+
+function _pveTh(label, key, align) {
+ const active = _pveSort.key === key;
+ const arrow = active ? (_pveSort.dir === 'asc' ? '▲' : '▼') : '
▼ ';
+ const alignCls = align === 'right' ? 'text-right' : 'text-left';
+ return `
+ ${label}${arrow} `;
+}
+
+function _renderProjectEvents(events, container, locId) {
if (!events.length) {
container.innerHTML = '
No events for the current filter.
';
return;
@@ -1106,19 +1240,22 @@ function _renderProjectEvents(events, total, container) {
${ft}
`;
}).join('');
+ const scope = locId
+ ? `Showing ${events.length.toLocaleString()} event${events.length === 1 ? '' : 's'} at this location`
+ : `Showing ${events.length.toLocaleString()} of ${_pveTotal.toLocaleString()} events`;
container.innerHTML = `
-
Showing ${events.length} of ${total.toLocaleString()} events
+
${scope}
- Timestamp
- Location
- Serial
- Tran
- Vert
- Long
- PVS
- Mic
+ ${_pveTh('Timestamp', 'timestamp')}
+ ${_pveTh('Location', 'location_name')}
+ ${_pveTh('Serial', 'serial')}
+ ${_pveTh('Tran', 'tran_ppv')}
+ ${_pveTh('Vert', 'vert_ppv')}
+ ${_pveTh('Long', 'long_ppv')}
+ ${_pveTh('PVS', 'peak_vector_sum')}
+ ${_pveTh('Mic', 'mic_ppv')}
Flags
@@ -1140,6 +1277,41 @@ function switchSoundSubTab(name) {
}
}
+// ── Per-module status (active / on_hold / completed) ─────────────────────
+// Each module has its own lifecycle independent of the parent project, so the
+// sound side can be "completed" while vibration keeps running.
+const _MODULE_STATUS_LABEL = { active: 'Active', on_hold: 'On hold', completed: 'Completed' };
+async function setModuleStatus(moduleType, status, selectEl) {
+ const prev = selectEl ? selectEl.getAttribute('data-prev') : null;
+ try {
+ const r = await fetch(`/api/projects/${projectId}/modules/${moduleType}/status`, {
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ status })
+ });
+ if (!r.ok) throw new Error('HTTP ' + r.status);
+ if (selectEl) selectEl.setAttribute('data-prev', status);
+ // Refresh the header so the module chip's status badge updates.
+ if (window.htmx) htmx.ajax('GET', `/api/projects/${projectId}/header`, { target: '#project-header', swap: 'innerHTML' });
+ const name = (moduleType === 'sound_monitoring') ? 'Sound' : 'Vibration';
+ if (window.showToast) showToast(`${name} module marked ${_MODULE_STATUS_LABEL[status] || status}.`, 'success');
+ } catch (e) {
+ if (selectEl && prev) selectEl.value = prev; // revert the dropdown on failure
+ if (window.showToast) showToast('Could not update module status.', 'error');
+ else alert('Could not update module status.');
+ }
+}
+
+function _renderSoundModeChip(mode) {
+ const chip = document.getElementById('sound-mode-chip');
+ if (!chip) return;
+ const remote = mode === 'remote';
+ chip.classList.remove('hidden');
+ chip.className = 'inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium ' +
+ (remote ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300'
+ : 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300');
+ chip.textContent = remote ? 'Remote — data via FTP' : 'Manual — SD card upload';
+}
+
// Load project details
async function loadProjectDetails() {
try {
@@ -1169,6 +1341,14 @@ async function loadProjectDetails() {
if (modeRadio) modeRadio.checked = true;
settingsUpdateModeStyles();
+ // Per-module status selects + the (sound-scoped) data-collection chip.
+ const ms = data.module_status || {};
+ const ssel = document.getElementById('sound-module-status');
+ if (ssel) { ssel.value = ms.sound_monitoring || 'active'; ssel.setAttribute('data-prev', ssel.value); }
+ const vsel = document.getElementById('vibration-module-status');
+ if (vsel) { vsel.value = ms.vibration_monitoring || 'active'; vsel.setAttribute('data-prev', vsel.value); }
+ _renderSoundModeChip(mode);
+
// Show/hide module tabs based on active modules
const hasSoundModule = projectModules.includes('sound_monitoring');
const hasVibrationModule = projectModules.includes('vibration_monitoring');