diff --git a/app/static/css/style.css b/app/static/css/style.css index 889d332..1be6d46 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -1425,3 +1425,9 @@ main { padding: 0 2px; border-radius: 2px; } + +/* Own device map marker */ +.own-device-marker { + background: transparent; + border: none; +} diff --git a/app/static/js/app.js b/app/static/js/app.js index 3b92b12..79d1a76 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -25,6 +25,7 @@ let contactsPubkeyMap = {}; // { 'contactName': 'full_pubkey', ... } let blockedContactNames = new Set(); // Names of blocked contacts let allContactsWithGps = []; // Device contacts for map filtering let allCachedContactsWithGps = []; // Cache-only contacts for map +let _selfInfo = null; // Own device info (for map marker) // SocketIO state let chatSocket = null; // SocketIO connection to /chat namespace @@ -179,9 +180,25 @@ function updateMapMarkers() { } const bounds = []; + + // Add own device marker (star shape, distinct from contacts) + if (_selfInfo && _selfInfo.adv_lat && _selfInfo.adv_lon && (_selfInfo.adv_lat !== 0 || _selfInfo.adv_lon !== 0)) { + const ownIcon = L.divIcon({ + html: '', + iconSize: [20, 20], + iconAnchor: [10, 10], + className: 'own-device-marker' + }); + L.marker([_selfInfo.adv_lat, _selfInfo.adv_lon], { icon: ownIcon }) + .addTo(markersGroup) + .bindPopup(`${_selfInfo.name || 'This device'}
Own device`); + bounds.push([_selfInfo.adv_lat, _selfInfo.adv_lon]); + } + filteredContacts.forEach(c => { const color = CONTACT_TYPE_COLORS[c.type] || '#2196F3'; const typeName = CONTACT_TYPE_NAMES[c.type] || 'Unknown'; + const lastSeen = c.last_advert ? formatTimeAgo(c.last_advert) : ''; L.circleMarker([c.adv_lat, c.adv_lon], { radius: 10, @@ -192,7 +209,7 @@ function updateMapMarkers() { fillOpacity: 0.8 }) .addTo(markersGroup) - .bindPopup(`${c.name}
${typeName}`); + .bindPopup(`${c.name}
${typeName}${lastSeen ? `
Last seen: ${lastSeen}` : ''}`); bounds.push([c.adv_lat, c.adv_lon]); }); @@ -200,6 +217,7 @@ function updateMapMarkers() { cachedFiltered.forEach(c => { const typeNum = TYPE_LABEL_TO_NUM[c.type_label] || 1; const color = CONTACT_TYPE_COLORS[typeNum] || '#2196F3'; + const lastSeen = c.last_seen ? formatTimeAgo(c.last_seen) : ''; L.circleMarker([c.adv_lat, c.adv_lon], { radius: 8, @@ -210,14 +228,14 @@ function updateMapMarkers() { fillOpacity: 0.5 }) .addTo(markersGroup) - .bindPopup(`${c.name}
${c.type_label || 'Cache'} (cached)`); + .bindPopup(`${c.name}
${c.type_label || 'Cache'} (cached)${lastSeen ? `
Last seen: ${lastSeen}` : ''}`); bounds.push([c.adv_lat, c.adv_lon]); }); if (bounds.length === 1) { leafletMap.setView(bounds[0], 13); - } else { + } else if (bounds.length > 1) { leafletMap.fitBounds(bounds, { padding: [20, 20] }); } } @@ -239,14 +257,24 @@ async function showAllContactsOnMap() { markersGroup.clearLayers(); try { - // Fetch device and cached contacts in parallel - const [deviceResp, cachedResp] = await Promise.all([ + // Fetch device info, device contacts, and cached contacts in parallel + const [deviceInfoResp, deviceResp, cachedResp] = await Promise.all([ + fetch('/api/device/info'), fetch('/api/contacts/detailed'), fetch('/api/contacts/cached?format=full') ]); + const deviceInfoData = await deviceInfoResp.json(); const deviceData = await deviceResp.json(); const cachedData = await cachedResp.json(); + // Parse self info for own device marker + if (deviceInfoData.success && deviceInfoData.info) { + try { + const jsonMatch = deviceInfoData.info.match(/\{[\s\S]*\}/); + _selfInfo = jsonMatch ? JSON.parse(jsonMatch[0]) : null; + } catch (e) { _selfInfo = null; } + } + if (deviceData.success && deviceData.contacts) { allContactsWithGps = deviceData.contacts.filter(c => c.adv_lat && c.adv_lon && (c.adv_lat !== 0 || c.adv_lon !== 0) @@ -2072,6 +2100,19 @@ function formatTime(timestamp) { } } +/** + * Format a unix timestamp as relative time (e.g., "5 min ago", "2h ago") + */ +function formatTimeAgo(timestamp) { + const now = Math.floor(Date.now() / 1000); + const diff = now - timestamp; + if (diff < 60) return 'just now'; + if (diff < 3600) return `${Math.floor(diff / 60)} min ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; + if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; + return new Date(timestamp * 1000).toLocaleDateString(); +} + /** * Update character counter (counts UTF-8 bytes, not characters) */