diff --git a/api/flockyou.py b/api/flockyou.py index cef210a..29ee510 100644 --- a/api/flockyou.py +++ b/api/flockyou.py @@ -193,7 +193,7 @@ def flock_reader(): time.sleep(0.1) def add_detection_from_serial(data): - """Add detection from serial data""" + """Add detection from serial data - counts detections per MAC address""" global detections, gps_data, next_detection_id # Add GPS data if available @@ -214,16 +214,51 @@ def add_detection_from_serial(data): # Add server timestamp data['server_timestamp'] = datetime.now().isoformat() - # Add unique ID for aliasing - data['id'] = next_detection_id - next_detection_id += 1 - data['alias'] = '' # Empty alias by default + # Check if we already have a detection for this MAC address + mac_address = data.get('mac_address') + existing_detection = None - detections.append(data) + if mac_address: + for detection in detections: + if detection.get('mac_address') == mac_address: + existing_detection = detection + break - # Emit to connected clients - safe_socket_emit('new_detection', data) - print(f"New detection added: ID {data['id']}, Method: {data.get('detection_method')}, MAC: {data.get('mac_address')}") + if existing_detection: + # Update existing detection with new data and increment count + existing_detection['detection_count'] = existing_detection.get('detection_count', 1) + 1 + existing_detection['last_seen'] = datetime.now().isoformat() + existing_detection['last_rssi'] = data.get('rssi', existing_detection.get('last_rssi')) + existing_detection['last_channel'] = data.get('channel', existing_detection.get('last_channel')) + existing_detection['last_frequency'] = data.get('frequency', existing_detection.get('last_frequency')) + existing_detection['last_ssid'] = data.get('ssid', existing_detection.get('last_ssid')) + existing_detection['last_device_name'] = data.get('device_name', existing_detection.get('last_device_name')) + + # Preserve detection_method if not already set + if not existing_detection.get('detection_method') and data.get('detection_method'): + existing_detection['detection_method'] = data.get('detection_method') + + # Update GPS if new data is available + if data.get('gps'): + existing_detection['gps'] = data['gps'] + + # Emit updated detection + safe_socket_emit('detection_updated', existing_detection) + print(f"Updated detection: MAC {mac_address}, Count: {existing_detection['detection_count']}, Method: {existing_detection.get('detection_method')}") + else: + # Create new detection + data['id'] = next_detection_id + next_detection_id += 1 + data['alias'] = '' # Empty alias by default + data['detection_count'] = 1 + data['first_seen'] = datetime.now().isoformat() + data['last_seen'] = datetime.now().isoformat() + + detections.append(data) + + # Emit to connected clients + safe_socket_emit('new_detection', data) + print(f"New detection added: ID {data['id']}, Method: {data.get('detection_method')}, MAC: {mac_address}") def connection_monitor(): """Background thread for monitoring device connections""" @@ -637,17 +672,33 @@ def clear_detections(): @app.route('/api/test/detection', methods=['POST']) def test_detection(): """Test endpoint to add a sample detection""" - sample_detection = { - 'detection_method': 'probe_request', - 'protocol': 'wifi', - 'mac_address': 'AA:BB:CC:DD:EE:FF', - 'ssid': 'TestNetwork', - 'rssi': -45, - 'signal_strength': 'Excellent', - 'channel': 6, - 'detection_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'timestamp': datetime.now().isoformat() - } + if request.is_json: + # Use provided detection data + sample_detection = request.json + # Ensure required fields are present + if 'detection_method' not in sample_detection: + sample_detection['detection_method'] = 'probe_request' + if 'protocol' not in sample_detection: + sample_detection['protocol'] = 'wifi' + if 'mac_address' not in sample_detection: + sample_detection['mac_address'] = 'AA:BB:CC:DD:EE:FF' + if 'detection_time' not in sample_detection: + sample_detection['detection_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + if 'timestamp' not in sample_detection: + sample_detection['timestamp'] = datetime.now().isoformat() + else: + # Use default sample detection + sample_detection = { + 'detection_method': 'probe_request', + 'protocol': 'wifi', + 'mac_address': 'AA:BB:CC:DD:EE:FF', + 'ssid': 'TestNetwork', + 'rssi': -45, + 'signal_strength': 'Excellent', + 'channel': 6, + 'detection_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'timestamp': datetime.now().isoformat() + } add_detection_from_serial(sample_detection) return jsonify({'status': 'success', 'message': 'Test detection added'}) diff --git a/api/requirements.txt b/api/requirements.txt index 95b5ef0..2ea9410 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -4,3 +4,4 @@ python-socketio==5.8.0 python-engineio==4.7.1 pyserial==3.5 Werkzeug==2.3.7 +requests==2.31.0 diff --git a/api/templates/index.html b/api/templates/index.html index 203fa2a..6498bfb 100644 --- a/api/templates/index.html +++ b/api/templates/index.html @@ -518,15 +518,21 @@ .alias-display { cursor: pointer; - padding: 2px 6px; + padding: 0.25rem 0.5rem; border-radius: 4px; - transition: background-color 0.2s ease; display: inline-block; min-width: 100px; + background: #1e40af; + border: 1px solid #3b82f6; + color: #93c5fd; + font-weight: 600; + font-size: 0.85rem; } .alias-display:hover { - background-color: rgba(139, 92, 246, 0.2); + background: #2563eb; + border-color: #60a5fa; + color: white; } .alias-display em { @@ -662,90 +668,125 @@ } .detection-item { - padding: 0.75rem; - border-bottom: 1px solid #4c1d95; - transition: background-color 0.3s ease; - background: rgba(45, 27, 105, 0.4); + background: #1e3a8a; + border: 1px solid #3b82f6; + border-radius: 6px; + padding: 0.5rem; + margin-bottom: 0.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + min-height: 5rem; } .detection-item:hover { - background: rgba(74, 27, 105, 0.6); - } - - .detection-item:last-child { - border-bottom: none; + background: #1e40af; + border-color: #60a5fa; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); } .detection-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 0.3rem; + margin-bottom: 0.5rem; + } + + .detection-header-left { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .gps-link { + color: #93c5fd; + font-size: 0.8rem; + font-weight: 600; + text-decoration: none; + padding: 0.1rem 0.3rem; + border-radius: 3px; + background: rgba(147, 197, 253, 0.1); + border: 1px solid #3b82f6; + transition: all 0.2s ease; + } + + .gps-link:hover { + background: #3b82f6; + color: white; + text-decoration: none; + } + + .detection-type-badge { + display: flex; + align-items: center; + gap: 0.5rem; } .detection-type { - background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%); + background: #2563eb; color: white; - padding: 0.2rem 0.6rem; - border-radius: 12px; - font-size: 0.75rem; - font-weight: 600; + padding: 0.25rem 0.6rem; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .detection-count { + background: #059669; + color: white; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + font-weight: 700; + min-width: 2.5rem; + text-align: center; } .detection-time { - color: #d1d5db; - font-size: 0.8rem; + color: #93c5fd; + font-size: 0.9rem; + font-weight: 600; } .detection-details { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 0.25rem; - font-size: 0.8rem; - margin-top: 0.4rem; - } - - .detection-details.compact { - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 0.2rem; - font-size: 0.75rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; + font-size: 0.9rem; + align-items: center; } .detail-item { display: flex; - justify-content: space-between; align-items: center; - padding: 0.2rem 0; + white-space: nowrap; } .detail-label { - font-weight: 600; - color: #c084fc; - min-width: 80px; - font-size: 0.8rem; + font-weight: 700; + color: #93c5fd; + font-size: 0.9rem; + margin-right: 0.5rem; } .detail-value { - color: #e0e0e0; - text-align: right; - word-break: break-all; - max-width: 60%; - } - - .gps-info { - background: rgba(16, 185, 129, 0.2); - border: 1px solid #10b981; - border-radius: 6px; - padding: 0.4rem; - margin-top: 0.4rem; - } - - .gps-info h4 { - color: #34d399; - margin-bottom: 0.2rem; + color: #ffffff; + font-weight: 600; font-size: 0.9rem; } + .detection-timing { + grid-column: 1 / -1; + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + border-radius: 4px; + padding: 0.2rem; + margin-top: 0.15rem; + font-size: 0.65rem; + } + + + .no-detections { text-align: center; padding: 3rem; @@ -915,8 +956,6 @@ - - @@ -1260,7 +1299,7 @@ function updateStats() { const total = detections.length; const wifi = detections.filter(d => d.protocol === 'wifi').length; - const ble = detections.filter(d => d.protocol === 'ble').length; + const ble = detections.filter(d => d.protocol === 'bluetooth_le' || d.protocol === 'bluetooth_classic').length; const gps = detections.filter(d => d.gps).length; document.getElementById('totalDetections').textContent = total; @@ -1281,91 +1320,58 @@ `; return; } - - const isCompact = container.classList.contains('compact-view'); container.innerHTML = detectionsToRender.map(detection => { - // Build all available fields dynamically - const fields = []; + // Get detection count and timing info + const count = detection.detection_count || 1; + const lastSeen = detection.last_seen ? new Date(detection.last_seen).toLocaleTimeString() : 'Unknown'; - // Core fields (always show) - if (detection.protocol) fields.push(['Protocol', detection.protocol]); - if (detection.mac_address) fields.push(['MAC', detection.mac_address]); - if (detection.manufacturer) fields.push(['Manufacturer', detection.manufacturer]); - if (detection.rssi !== undefined) fields.push(['RSSI', `${detection.rssi} dBm`]); + // Use last known values for signal data + const rssi = detection.last_rssi !== undefined ? detection.last_rssi : detection.rssi; + const channel = detection.last_channel || detection.channel; + const ssid = detection.last_ssid || detection.ssid; + const deviceName = detection.last_device_name || detection.device_name; - // Additional fields (show more in normal view) - if (!isCompact) { - if (detection.signal_strength) fields.push(['Signal', detection.signal_strength]); - if (detection.channel) fields.push(['Channel', detection.channel]); - if (detection.frequency) fields.push(['Freq', detection.frequency]); - if (detection.ssid) fields.push(['SSID', detection.ssid]); - if (detection.device_name) fields.push(['Device', detection.device_name]); - if (detection.service_uuid) fields.push(['Service', detection.service_uuid]); - if (detection.tx_power) fields.push(['TX Power', detection.tx_power]); - if (detection.company_identifier) fields.push(['Company ID', detection.company_identifier]); - if (detection.advertisement_data) fields.push(['Adv Data', detection.advertisement_data]); - if (detection.scan_response) fields.push(['Scan Resp', detection.scan_response]); - if (detection.timestamp) fields.push(['Timestamp', detection.timestamp]); - if (detection.server_timestamp) fields.push(['Server Time', new Date(detection.server_timestamp).toLocaleTimeString()]); - } else { - // Compact view - show only essential fields - if (detection.ssid) fields.push(['SSID', detection.ssid]); - if (detection.device_name) fields.push(['Device', detection.device_name]); - if (detection.channel) fields.push(['Ch', detection.channel]); - } + // Build essential fields in a compact layout + const essentialFields = []; + if (detection.protocol) essentialFields.push(['Protocol', detection.protocol]); + if (detection.mac_address) essentialFields.push(['MAC', detection.mac_address]); + if (rssi !== undefined) essentialFields.push(['RSSI', `${rssi} dBm`]); + if (channel) essentialFields.push(['Channel', channel]); + if (ssid) essentialFields.push(['SSID', ssid]); + if (deviceName) essentialFields.push(['Device', deviceName]); + if (detection.manufacturer) essentialFields.push(['Manufacturer', detection.manufacturer]); // Build the details HTML - const detailsHtml = fields.map(([label, value]) => ` + const detailsHtml = essentialFields.map(([label, value]) => `