feat(map): add Cached switch to show cache-only contacts on map

The Map modal (from main menu) now has a "Cached" toggle switch that,
when enabled, also displays contacts stored in the DB cache alongside
device contacts. Cached markers are slightly smaller and more
transparent to visually distinguish them from device contacts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-03-11 07:01:53 +01:00
parent 3fb1c09dc1
commit 92b55d9bdb
2 changed files with 65 additions and 9 deletions
+61 -8
View File
@@ -23,7 +23,8 @@ let markersGroup = null;
let contactsGeoCache = {}; // { 'contactName': { lat, lon }, ... }
let contactsPubkeyMap = {}; // { 'contactName': 'full_pubkey', ... }
let blockedContactNames = new Set(); // Names of blocked contacts
let allContactsWithGps = []; // Cached contacts for map filtering
let allContactsWithGps = []; // Device contacts for map filtering
let allCachedContactsWithGps = []; // Cache-only contacts for map
// SocketIO state
let chatSocket = null; // SocketIO connection to /chat namespace
@@ -152,10 +153,27 @@ function updateMapMarkers() {
markersGroup.clearLayers();
const selectedTypes = getSelectedMapTypes();
const showCached = document.getElementById('mapCachedSwitch')?.checked || false;
// Device contacts filtered by type
const deviceKeySet = new Set(allContactsWithGps.map(c => c.public_key));
const filteredContacts = allContactsWithGps.filter(c => selectedTypes.includes(c.type));
if (filteredContacts.length === 0) {
// Cache-only contacts (not on device) filtered by type
const TYPE_LABEL_TO_NUM = { 'CLI': 1, 'REP': 2, 'ROOM': 3, 'SENS': 4 };
let cachedFiltered = [];
if (showCached) {
cachedFiltered = allCachedContactsWithGps
.filter(c => !deviceKeySet.has(c.public_key))
.filter(c => {
const typeNum = TYPE_LABEL_TO_NUM[c.type_label];
return typeNum ? selectedTypes.includes(typeNum) : false;
});
}
const allFiltered = [...filteredContacts, ...cachedFiltered];
if (allFiltered.length === 0) {
leafletMap.setView([52.0, 19.0], 6);
return;
}
@@ -179,6 +197,24 @@ function updateMapMarkers() {
bounds.push([c.adv_lat, c.adv_lon]);
});
cachedFiltered.forEach(c => {
const typeNum = TYPE_LABEL_TO_NUM[c.type_label] || 1;
const color = CONTACT_TYPE_COLORS[typeNum] || '#2196F3';
L.circleMarker([c.adv_lat, c.adv_lon], {
radius: 8,
fillColor: color,
color: '#999',
weight: 1,
opacity: 0.8,
fillOpacity: 0.5
})
.addTo(markersGroup)
.bindPopup(`<b>${c.name}</b><br><span class="text-muted">${c.type_label || 'Cache'} (cached)</span>`);
bounds.push([c.adv_lat, c.adv_lon]);
});
if (bounds.length === 1) {
leafletMap.setView(bounds[0], 13);
} else {
@@ -203,16 +239,27 @@ async function showAllContactsOnMap() {
markersGroup.clearLayers();
try {
const response = await fetch('/api/contacts/detailed');
const data = await response.json();
// Fetch device and cached contacts in parallel
const [deviceResp, cachedResp] = await Promise.all([
fetch('/api/contacts/detailed'),
fetch('/api/contacts/cached?format=full')
]);
const deviceData = await deviceResp.json();
const cachedData = await cachedResp.json();
if (data.success && data.contacts) {
allContactsWithGps = data.contacts.filter(c =>
if (deviceData.success && deviceData.contacts) {
allContactsWithGps = deviceData.contacts.filter(c =>
c.adv_lat && c.adv_lon && (c.adv_lat !== 0 || c.adv_lon !== 0)
);
updateMapMarkers();
}
if (cachedData.success && cachedData.contacts) {
allCachedContactsWithGps = cachedData.contacts.filter(c =>
c.adv_lat && c.adv_lon && (c.adv_lat !== 0 || c.adv_lon !== 0)
);
}
updateMapMarkers();
} catch (err) {
console.error('Error loading contacts for map:', err);
}
@@ -232,6 +279,12 @@ async function showAllContactsOnMap() {
}
});
// Setup cached switch listener
const cachedSwitch = document.getElementById('mapCachedSwitch');
if (cachedSwitch) {
cachedSwitch.onchange = () => updateMapMarkers();
}
modalEl.addEventListener('shown.bs.modal', onShown);
modal.show();
}
+4 -1
View File
@@ -300,11 +300,14 @@
<!-- Type filter (hidden for single contact view) -->
<div id="mapTypeFilter" class="d-none px-3 py-2 border-bottom bg-light">
<div class="d-flex flex-wrap gap-2 align-items-center">
<span class="small text-muted me-1">Show:</span>
<span class="map-filter-badge active" data-type="1" id="mapFilterCLI">CLI</span>
<span class="map-filter-badge active" data-type="2" id="mapFilterREP">REP</span>
<span class="map-filter-badge active" data-type="3" id="mapFilterROOM">ROOM</span>
<span class="map-filter-badge active" data-type="4" id="mapFilterSENS">SENS</span>
<div class="form-check form-switch ms-auto mb-0">
<input class="form-check-input" type="checkbox" id="mapCachedSwitch">
<label class="form-check-label small" for="mapCachedSwitch">Cached</label>
</div>
</div>
</div>
<div id="leafletMap" style="height: 400px; width: 100%;"></div>