feat(dashboard): clarify the fleet status card and swap map locations to project monitoring location coords.
feat: Location no longer assigned directly to unit, locations and coords are assigned to location only, unit only is deployed or benched.
This commit is contained in:
+86
-51
@@ -150,46 +150,55 @@ setInterval(_refreshPendingDeployBanner, 30000);
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3 card-content" id="fleet-summary-content">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 dark:text-gray-400">Total Units</span>
|
||||
<span id="total-units" class="text-3xl md:text-2xl font-bold text-gray-900 dark:text-white">--</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 dark:text-gray-400">Deployed</span>
|
||||
<span id="deployed-units" class="text-3xl md:text-2xl font-bold text-blue-600 dark:text-blue-400">--</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-gray-600 dark:text-gray-400">Benched</span>
|
||||
<span id="benched-units" class="text-3xl md:text-2xl font-bold text-gray-600 dark:text-gray-400">--</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-orange-600 dark:text-orange-400">Allocated</span>
|
||||
<span id="allocated-units" class="text-3xl md:text-2xl font-bold text-orange-500 dark:text-orange-400">--</span>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500 mb-2 italic">By Device Type:</p>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<div class="space-y-4 card-content" id="fleet-summary-content">
|
||||
<!-- Seismographs -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1.5">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1.5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
<a href="/seismographs" class="text-sm text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400">Seismographs</a>
|
||||
<a href="/seismographs" class="text-sm font-semibold text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400">Seismographs</a>
|
||||
</div>
|
||||
<span id="seismo-count" class="font-semibold text-blue-600 dark:text-blue-400">--</span>
|
||||
<span id="seismo-count" class="text-lg font-bold text-blue-600 dark:text-blue-400">--</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="pl-6 flex flex-col gap-0.5 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Deployed</span>
|
||||
<span id="seismo-deployed" class="font-medium text-gray-800 dark:text-gray-200">--</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Benched</span>
|
||||
<span id="seismo-benched" class="font-medium text-gray-800 dark:text-gray-200">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sound Level Meters -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1.5">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-4 h-4 mr-1.5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z"></path>
|
||||
</svg>
|
||||
<a href="/sound-level-meters" class="text-sm text-gray-600 dark:text-gray-400 hover:text-purple-600 dark:hover:text-purple-400">Sound Level Meters</a>
|
||||
<a href="/sound-level-meters" class="text-sm font-semibold text-gray-800 dark:text-gray-200 hover:text-purple-600 dark:hover:text-purple-400">Sound Level Meters</a>
|
||||
</div>
|
||||
<span id="slm-count" class="text-lg font-bold text-purple-600 dark:text-purple-400">--</span>
|
||||
</div>
|
||||
<div class="pl-6 flex flex-col gap-0.5 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Deployed</span>
|
||||
<span id="slm-deployed" class="font-medium text-gray-800 dark:text-gray-200">--</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">Benched</span>
|
||||
<span id="slm-benched" class="font-medium text-gray-800 dark:text-gray-200">--</span>
|
||||
</div>
|
||||
<span id="slm-count" class="font-semibold text-purple-600 dark:text-purple-400">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-3 mt-3">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500 mb-2 italic">Deployed Status:</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500 mb-2 italic">Call-in Status:</p>
|
||||
<div class="flex justify-between items-center mb-2" title="Units reporting normally (last seen < 12 hours)">
|
||||
<div class="flex items-center">
|
||||
<span class="w-3 h-3 rounded-full bg-green-500 mr-2 flex items-center justify-center">
|
||||
@@ -628,9 +637,14 @@ function updateFleetMapFiltered(allUnits) {
|
||||
fleetMarkers.forEach(marker => fleetMap.removeLayer(marker));
|
||||
fleetMarkers = [];
|
||||
|
||||
// Get deployed units with coordinates that pass the filter
|
||||
// Get deployed units with coordinates that pass the filter.
|
||||
// Modems are not plotted — they inherit the paired device's location,
|
||||
// which would just stack a duplicate marker on the same pin.
|
||||
const deployedUnits = Object.entries(allUnits || {})
|
||||
.filter(([_, u]) => u.deployed && u.coordinates && unitPassesFilter(u));
|
||||
.filter(([_, u]) => u.deployed
|
||||
&& u.coordinates
|
||||
&& (u.device_type || 'seismograph') !== 'modem'
|
||||
&& unitPassesFilter(u));
|
||||
|
||||
if (deployedUnits.length === 0) {
|
||||
return;
|
||||
@@ -672,10 +686,12 @@ function updateFleetMapFiltered(allUnits) {
|
||||
// Popup with device type
|
||||
const deviceLabel = getDeviceTypeLabel(deviceType);
|
||||
|
||||
const locName = unit.location_name || '';
|
||||
marker.bindPopup(`
|
||||
<div class="p-2">
|
||||
<h3 class="font-bold text-lg">${id}</h3>
|
||||
<p class="text-sm text-gray-600">${deviceLabel}</p>
|
||||
${locName ? `<p class="text-sm text-gray-700">📍 ${locName}</p>` : ''}
|
||||
<p class="text-sm">Status: <span style="color: ${markerColor}">${unit.status}</span></p>
|
||||
${unit.note ? `<p class="text-sm text-gray-600">${unit.note}</p>` : ''}
|
||||
<a href="/unit/${id}" class="text-blue-600 hover:underline text-sm">View Details</a>
|
||||
@@ -783,32 +799,51 @@ function updateDashboard(event) {
|
||||
timeZoneName: 'short'
|
||||
});
|
||||
|
||||
// ===== Fleet summary numbers (always unfiltered) =====
|
||||
document.getElementById('total-units').textContent = data.summary?.total ?? 0;
|
||||
document.getElementById('deployed-units').textContent = data.summary?.active ?? 0;
|
||||
document.getElementById('benched-units').textContent = data.summary?.benched ?? 0;
|
||||
document.getElementById('allocated-units').textContent = data.summary?.allocated ?? 0;
|
||||
document.getElementById('status-ok').textContent = data.summary?.ok ?? 0;
|
||||
document.getElementById('status-pending').textContent = data.summary?.pending ?? 0;
|
||||
document.getElementById('status-missing').textContent = data.summary?.missing ?? 0;
|
||||
// ===== Fleet Summary: per-device-type counts (always unfiltered) =====
|
||||
// Deployed = unit has an active UnitAssignment (location_id set by
|
||||
// the snapshot helper). Benched = no active assignment.
|
||||
// Retired, out-for-calibration, and roster-unknown units (emitters
|
||||
// not in the roster) are excluded from totals.
|
||||
const counts = {
|
||||
seismograph: { total: 0, deployed: 0, benched: 0 },
|
||||
sound_level_meter: { total: 0, deployed: 0, benched: 0 },
|
||||
};
|
||||
let monitoredOk = 0, monitoredPending = 0, monitoredMissing = 0;
|
||||
const unknownIds = new Set(Object.keys(data.unknown || {}));
|
||||
|
||||
// ===== Device type counts (always unfiltered) =====
|
||||
let seismoCount = 0;
|
||||
let slmCount = 0;
|
||||
let modemCount = 0;
|
||||
Object.values(data.units || {}).forEach(unit => {
|
||||
if (unit.retired) return; // Don't count retired units
|
||||
const deviceType = unit.device_type || 'seismograph';
|
||||
if (deviceType === 'seismograph') {
|
||||
seismoCount++;
|
||||
} else if (deviceType === 'sound_level_meter') {
|
||||
slmCount++;
|
||||
} else if (deviceType === 'modem') {
|
||||
modemCount++;
|
||||
Object.entries(data.units || {}).forEach(([uid, unit]) => {
|
||||
if (unit.retired || unit.out_for_calibration) return;
|
||||
if (unknownIds.has(uid)) return;
|
||||
const dt = unit.device_type || 'seismograph';
|
||||
const bucket = counts[dt];
|
||||
if (!bucket) return; // skip modems and anything else
|
||||
|
||||
bucket.total++;
|
||||
if (unit.location_id) {
|
||||
bucket.deployed++;
|
||||
} else {
|
||||
bucket.benched++;
|
||||
}
|
||||
|
||||
// Status tally only for seismographs + SLMs that are actually
|
||||
// deployed (assigned). Mirrors the per-device buckets so the
|
||||
// sum matches.
|
||||
if (unit.location_id) {
|
||||
if (unit.status === 'OK') monitoredOk++;
|
||||
else if (unit.status === 'Pending') monitoredPending++;
|
||||
else if (unit.status === 'Missing') monitoredMissing++;
|
||||
}
|
||||
});
|
||||
document.getElementById('seismo-count').textContent = seismoCount;
|
||||
document.getElementById('slm-count').textContent = slmCount;
|
||||
|
||||
document.getElementById('seismo-count').textContent = counts.seismograph.total;
|
||||
document.getElementById('seismo-deployed').textContent = counts.seismograph.deployed;
|
||||
document.getElementById('seismo-benched').textContent = counts.seismograph.benched;
|
||||
document.getElementById('slm-count').textContent = counts.sound_level_meter.total;
|
||||
document.getElementById('slm-deployed').textContent = counts.sound_level_meter.deployed;
|
||||
document.getElementById('slm-benched').textContent = counts.sound_level_meter.benched;
|
||||
document.getElementById('status-ok').textContent = monitoredOk;
|
||||
document.getElementById('status-pending').textContent = monitoredPending;
|
||||
document.getElementById('status-missing').textContent = monitoredMissing;
|
||||
|
||||
// ===== Apply filters and render map + alerts =====
|
||||
renderFilteredDashboard(data);
|
||||
|
||||
+47
-35
@@ -129,6 +129,15 @@
|
||||
<span id="viewProjectNoLink" class="text-gray-900 dark:text-white font-medium">Not assigned</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">Deployment Location</label>
|
||||
<p id="viewLocationContainer" class="mt-1">
|
||||
<a id="viewLocationLink" href="#" class="text-seismo-orange hover:text-orange-600 font-medium hover:underline hidden">
|
||||
<span id="viewLocationText">--</span>
|
||||
</a>
|
||||
<span id="viewLocationNoLink" class="text-gray-500 dark:text-gray-400 italic">Not deployed</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-gray-500 dark:text-gray-400">Address</label>
|
||||
<p id="viewAddress" class="mt-1 text-gray-900 dark:text-white font-medium">--</p>
|
||||
@@ -639,18 +648,12 @@
|
||||
{% include "partials/project_picker.html" with context %}
|
||||
</div>
|
||||
|
||||
<!-- Address -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address</label>
|
||||
<input type="text" name="address" id="address" placeholder="123 Main St, City, State"
|
||||
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>
|
||||
|
||||
<!-- Coordinates -->
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Coordinates</label>
|
||||
<input type="text" name="coordinates" id="coordinates" placeholder="34.0522,-118.2437"
|
||||
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 font-mono">
|
||||
<!-- Address / coordinates are managed on the project's
|
||||
MonitoringLocation, not the unit itself. Edit them on
|
||||
the project page. -->
|
||||
<div class="md:col-span-2 rounded-lg bg-gray-50 dark:bg-slate-700/50 border border-gray-200 dark:border-gray-700 p-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
Address & coordinates are set on the deployment location.
|
||||
Open the project to edit them.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -848,16 +851,6 @@
|
||||
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">
|
||||
@@ -1168,8 +1161,28 @@ function populateViewMode() {
|
||||
if (projectLink) projectLink.classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('viewAddress').textContent = currentUnit.address || '--';
|
||||
document.getElementById('viewCoordinates').textContent = currentUnit.coordinates || '--';
|
||||
// Deployment Location — comes from the active UnitAssignment →
|
||||
// MonitoringLocation. Show project link if present, otherwise
|
||||
// "Not deployed" placeholder.
|
||||
const locLink = document.getElementById('viewLocationLink');
|
||||
const locText = document.getElementById('viewLocationText');
|
||||
const locNoLink = document.getElementById('viewLocationNoLink');
|
||||
const activeLoc = currentUnit.active_location;
|
||||
if (activeLoc && activeLoc.location_id) {
|
||||
if (locText) locText.textContent = activeLoc.name || activeLoc.address || 'Active location';
|
||||
if (locLink) {
|
||||
locLink.href = `/projects/${activeLoc.project_id}`;
|
||||
locLink.classList.remove('hidden');
|
||||
}
|
||||
if (locNoLink) locNoLink.classList.add('hidden');
|
||||
} else {
|
||||
if (locLink) locLink.classList.add('hidden');
|
||||
if (locNoLink) locNoLink.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Address / coordinates also come from the active assignment.
|
||||
document.getElementById('viewAddress').textContent = (activeLoc && activeLoc.address) || '--';
|
||||
document.getElementById('viewCoordinates').textContent = (activeLoc && activeLoc.coordinates) || '--';
|
||||
|
||||
// Seismograph fields
|
||||
document.getElementById('viewLastCalibrated').textContent = currentUnit.last_calibrated || '--';
|
||||
@@ -1327,8 +1340,6 @@ function populateEditForm() {
|
||||
if (projectPickerClear) projectPickerClear.classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('address').value = currentUnit.address || '';
|
||||
document.getElementById('coordinates').value = currentUnit.coordinates || '';
|
||||
document.getElementById('deployed').checked = currentUnit.deployed;
|
||||
document.getElementById('outForCalibration').checked = currentUnit.out_for_calibration || false;
|
||||
document.getElementById('retired').value = currentUnit.retired ? 'true' : '';
|
||||
@@ -1609,8 +1620,13 @@ function initUnitMap() {
|
||||
// Update marker (can be called multiple times)
|
||||
updateMapMarker(lat, lon);
|
||||
|
||||
// Update location text
|
||||
// Update location text — prefer the assignment's location name, fall
|
||||
// back to address, then coordinates.
|
||||
const locationParts = [];
|
||||
const loc = currentUnit.active_location;
|
||||
if (loc && loc.name) {
|
||||
locationParts.push(loc.name);
|
||||
}
|
||||
if (currentUnit.address) {
|
||||
locationParts.push(currentUnit.address);
|
||||
}
|
||||
@@ -1724,13 +1740,12 @@ async function uploadPhoto(file) {
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Show success message with metadata info
|
||||
// Show success message with metadata info. Location is on the
|
||||
// assignment's MonitoringLocation now, so we just surface what GPS
|
||||
// came in — the backend no longer mutates the unit row.
|
||||
let message = 'Photo uploaded successfully!';
|
||||
if (result.metadata && result.metadata.coordinates) {
|
||||
message += ` GPS location detected: ${result.metadata.coordinates}`;
|
||||
if (result.coordinates_updated) {
|
||||
message += ' (Unit coordinates updated automatically)';
|
||||
}
|
||||
} else {
|
||||
message += ' No GPS data found in photo.';
|
||||
}
|
||||
@@ -1738,11 +1753,8 @@ async function uploadPhoto(file) {
|
||||
statusDiv.className = 'mt-4 p-4 rounded-lg bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200';
|
||||
statusDiv.textContent = message;
|
||||
|
||||
// Reload photos and unit data
|
||||
// Reload photos
|
||||
await loadPhotos();
|
||||
if (result.coordinates_updated) {
|
||||
await loadUnitData();
|
||||
}
|
||||
|
||||
// Hide status after 5 seconds
|
||||
setTimeout(() => {
|
||||
|
||||
Reference in New Issue
Block a user