diff --git a/app/static/js/app.js b/app/static/js/app.js
index 88af604..a647bc2 100644
--- a/app/static/js/app.js
+++ b/app/static/js/app.js
@@ -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(`${c.name}
${c.type_label || 'Cache'} (cached)`);
+
+ 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();
}
diff --git a/app/templates/base.html b/app/templates/base.html
index 465438d..b84e264 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -300,11 +300,14 @@