Files
smartphone-n8n-tracking/index.html
Joachim Hummel 1406445631 Fix device dropdown filter to show all MQTT devices
Changed strict equality (===) to loose equality (==) in loadLocations() when filtering MQTT data for device dropdown. This was preventing devices from appearing in the device selection dropdown because user_id is returned as string "0" from API.

This fix ensures both tid=10 (Joachim Pixel) and tid=11 (Huawei Smartphone) appear as selectable options in the device filter dropdown.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 18:25:34 +00:00

435 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MQTT Location Tracker</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { height: 100vh; width: 100%; }
.info {
position: absolute;
top: 10px;
right: 10px;
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 1000;
min-width: 250px;
max-height: 90vh;
overflow-y: auto;
}
.filter-section {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.filter-section:last-of-type {
border-bottom: none;
}
.filter-label {
font-size: 12px;
font-weight: bold;
color: #666;
margin-bottom: 5px;
display: block;
}
select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: white;
cursor: pointer;
transition: border-color 0.3s ease;
}
select:hover {
border-color: #4CAF50;
}
select:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.1);
}
.toggle-btn {
margin-top: 15px;
padding: 10px 15px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
width: 100%;
transition: all 0.3s ease;
font-size: 14px;
}
.toggle-btn.active {
background: #4CAF50;
color: white;
}
.toggle-btn.inactive {
background: #f44336;
color: white;
}
.toggle-btn:hover {
opacity: 0.8;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-indicator.active {
background: #4CAF50;
animation: pulse 2s infinite;
}
.status-indicator.inactive {
background: #999;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<div id="map"></div>
<div class="info">
<h3>📍 MQTT Tracker</h3>
<div id="status">Lade...</div>
<div class="filter-section">
<label class="filter-label">🗺️ Kartenebene</label>
<select id="mapLayerSelect" onchange="changeMapLayer()">
<option value="standard">Standard (OpenStreetMap)</option>
<option value="satellite">Satellit (Esri)</option>
<option value="terrain">Gelände (OpenTopoMap)</option>
<option value="dark">Dunkel (CartoDB Dark)</option>
</select>
</div>
<div class="filter-section">
<label class="filter-label">📱 Gerät</label>
<select id="deviceFilter" onchange="applyFilters()">
<option value="all">Alle Geräte</option>
</select>
</div>
<div class="filter-section">
<label class="filter-label">⏱️ Zeitraum</label>
<select id="timeFilter" onchange="applyFilters()">
<option value="1h" selected>Letzte Stunde</option>
<option value="3h">Letzte 3 Stunden</option>
<option value="6h">Letzte 6 Stunden</option>
<option value="12h">Letzte 12 Stunden</option>
<option value="24h">Letzte 24 Stunden</option>
</select>
</div>
<button id="toggleBtn" class="toggle-btn active" onclick="toggleAutoRefresh()">
<span class="status-indicator active"></span>
Auto-Refresh: AN
</button>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// Device Name Mapping (username -> sprechender Name)
const DEVICE_NAMES = {
'10': 'Joachim Pixel',
'11': 'Huawei Smartphone'
};
// Device Colors (für unterschiedliche Marker/Polylines)
const DEVICE_COLORS = {
'10': '#e74c3c', // Rot
'11': '#3498db', // Blau
'default': '#95a5a6' // Grau für unbekannte Geräte
};
// Karte initialisieren (München)
const map = L.map('map').setView([48.1351, 11.5820], 12);
// Map layers
const mapLayers = {
standard: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}),
satellite: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri'
}),
terrain: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
attribution: '© OpenTopoMap'
}),
dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '© CartoDB'
})
};
// Add default layer
let currentLayer = mapLayers.standard;
currentLayer.addTo(map);
// API URL
const API_URL = 'https://n8n.unixweb.home64.de/webhook/location';
// State
let autoRefreshEnabled = true;
let refreshInterval = null;
let allData = null;
let markerLayer = L.layerGroup().addTo(map);
let polylineLayer = L.layerGroup().addTo(map);
// Hilfsfunktion: Gerätename aus username holen
function getDeviceName(username) {
return DEVICE_NAMES[username] || `Unbekanntes Gerät (${username})`;
}
// Hilfsfunktion: Farbe für Gerät holen
function getDeviceColor(username) {
return DEVICE_COLORS[username] || DEVICE_COLORS.default;
}
// Change map layer
function changeMapLayer() {
const selectedLayer = document.getElementById('mapLayerSelect').value;
map.removeLayer(currentLayer);
currentLayer = mapLayers[selectedLayer];
currentLayer.addTo(map);
}
// Filter data by device
function filterByDevice(locations) {
const deviceFilter = document.getElementById('deviceFilter').value;
if (deviceFilter === 'all') return locations;
return locations.filter(loc => loc.username === deviceFilter);
}
// Filter data by time range
function filterByTime(locations) {
const timeFilter = document.getElementById('timeFilter').value;
const now = new Date();
const ranges = {
'1h': 60 * 60 * 1000,
'3h': 3 * 60 * 60 * 1000,
'6h': 6 * 60 * 60 * 1000,
'12h': 12 * 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000
};
const cutoffTime = now - ranges[timeFilter];
return locations.filter(loc => {
const locTime = new Date(loc.timestamp);
return locTime >= cutoffTime;
});
}
// Update device filter dropdown with available devices
function updateDeviceFilterOptions(locations) {
const deviceFilter = document.getElementById('deviceFilter');
const currentValue = deviceFilter.value;
// Get unique devices (username field)
const devices = new Set();
locations.forEach(loc => {
if (loc.username) {
devices.add(loc.username);
}
});
// Rebuild options
deviceFilter.innerHTML = '<option value="all">Alle Geräte</option>';
Array.from(devices).sort().forEach(username => {
const option = document.createElement('option');
option.value = username;
option.textContent = getDeviceName(username);
deviceFilter.appendChild(option);
});
// Restore previous selection if still available
if (currentValue !== 'all' && devices.has(currentValue)) {
deviceFilter.value = currentValue;
}
}
// Apply all filters and update map
function applyFilters() {
if (!allData || !allData.history) return;
// Nur MQTT-Daten (user_id = 0 oder "0")
let filteredData = allData.history.filter(loc => loc.user_id == 0);
// Apply filters in sequence
filteredData = filterByDevice(filteredData);
filteredData = filterByTime(filteredData);
// Update map
displayLocations(filteredData);
// Update status
document.getElementById('status').innerHTML =
`📊 Punkte: ${filteredData.length}<br>` +
`📱 Geräte: ${new Set(filteredData.map(l => l.username)).size}<br>` +
`${allData.success ? '✅ Verbunden' : '❌ Fehler'}`;
}
// Display filtered locations on map
function displayLocations(locations) {
// Clear existing markers and polylines
markerLayer.clearLayers();
polylineLayer.clearLayers();
if (locations.length === 0) return;
// Gruppiere Locations nach Gerät
const deviceGroups = {};
locations.forEach(loc => {
const device = loc.username || 'unknown';
if (!deviceGroups[device]) {
deviceGroups[device] = [];
}
deviceGroups[device].push(loc);
});
// Für jedes Gerät: Marker + Polyline
let firstLocation = null;
Object.keys(deviceGroups).forEach(device => {
const deviceLocs = deviceGroups[device];
const color = getDeviceColor(device);
// Sortiere nach Timestamp (neueste zuerst)
deviceLocs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// Add markers
deviceLocs.forEach((loc, index) => {
const isLatest = index === 0;
const lat = parseFloat(loc.latitude);
const lon = parseFloat(loc.longitude);
if (isNaN(lat) || isNaN(lon)) return;
// Popup-Inhalt mit zusätzlichen Infos
let popupContent = `<b>${getDeviceName(device)}</b><br>${loc.display_time || ''}`;
// Batterie
if (loc.battery !== undefined && loc.battery !== null) {
popupContent += `<br>🔋 Batterie: ${loc.battery}%`;
}
// Geschwindigkeit
if (loc.speed !== undefined && loc.speed !== null) {
const speedKmh = (loc.speed * 3.6).toFixed(1);
popupContent += `<br>🚗 Speed: ${speedKmh} km/h`;
}
// Marker Icon (neuester = größer)
const iconSize = isLatest ? [30, 45] : [20, 33];
const iconAnchor = isLatest ? [15, 45] : [10, 33];
const markerIcon = L.divIcon({
html: `<div style="background-color: ${color}; width: ${iconSize[0]}px; height: ${iconSize[1]}px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); border: 3px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3);"></div>`,
iconSize: iconSize,
iconAnchor: iconAnchor,
className: ''
});
L.marker([lat, lon], { icon: markerIcon })
.addTo(markerLayer)
.bindPopup(popupContent);
if (!firstLocation) {
firstLocation = [lat, lon];
}
});
// Add polyline if multiple points
if (deviceLocs.length > 1) {
const coords = deviceLocs
.map(h => {
const lat = parseFloat(h.latitude);
const lon = parseFloat(h.longitude);
return !isNaN(lat) && !isNaN(lon) ? [lat, lon] : null;
})
.filter(c => c !== null);
if (coords.length > 1) {
L.polyline(coords, { color: color, weight: 3, opacity: 0.7 }).addTo(polylineLayer);
}
}
});
// Zentriere auf erste Location
if (firstLocation) {
map.setView(firstLocation, 13);
}
}
async function loadLocations() {
try {
const response = await fetch(API_URL);
const data = await response.json();
allData = data;
// Nur MQTT-Daten für Device-Filter
const mqttData = data.history ? data.history.filter(loc => loc.user_id == 0) : [];
// Update device filter dropdown
if (mqttData.length > 0) {
updateDeviceFilterOptions(mqttData);
}
// Apply filters and display
applyFilters();
} catch (error) {
document.getElementById('status').innerHTML = '❌ Verbindungsfehler';
console.error(error);
}
}
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
const btn = document.getElementById('toggleBtn');
if (autoRefreshEnabled) {
btn.className = 'toggle-btn active';
btn.innerHTML = '<span class="status-indicator active"></span>Auto-Refresh: AN';
startAutoRefresh();
} else {
btn.className = 'toggle-btn inactive';
btn.innerHTML = '<span class="status-indicator inactive"></span>Auto-Refresh: AUS';
stopAutoRefresh();
}
}
function startAutoRefresh() {
if (refreshInterval) clearInterval(refreshInterval);
refreshInterval = setInterval(loadLocations, 5000);
}
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
}
// Initial laden
loadLocations();
// Auto-refresh starten
startAutoRefresh();
</script>
</body>
</html>