Refactor index.html to MQTT-only location tracker with device mapping

- Remove Telegram source filter and references
- Add device name mapping for username field (10→Joachim Pixel, 11→Huawei Smartphone)
- Implement color-coded markers and polylines per device
- Update time filter: default to 1h, add 3h/12h options, remove 7d/30d
- Add battery and speed info to location popups
- Improve marker design with custom droplet-shaped icons
- Filter only MQTT data (user_id = 0)
- Group locations by device with separate polylines
- Keep map layer selection (OSM, Satellite, Terrain, Dark)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-14 17:52:21 +00:00
parent 1e229582d4
commit e5ca23a276

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Location Test</title> <title>MQTT Location Tracker</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style> <style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; } body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
@@ -99,7 +99,7 @@
<body> <body>
<div id="map"></div> <div id="map"></div>
<div class="info"> <div class="info">
<h3>📍 Location Tracker</h3> <h3>📍 MQTT Tracker</h3>
<div id="status">Lade...</div> <div id="status">Lade...</div>
<div class="filter-section"> <div class="filter-section">
@@ -113,30 +113,20 @@
</div> </div>
<div class="filter-section"> <div class="filter-section">
<label class="filter-label">📡 Datenquelle</label> <label class="filter-label">📱 Gerät</label>
<select id="sourceFilter" onchange="applyFilters()"> <select id="deviceFilter" onchange="applyFilters()">
<option value="all">Alle Quellen</option> <option value="all">Alle Geräte</option>
<option value="telegram">Nur Telegram</option>
<option value="mqtt">Nur MQTT/OwnTracks</option>
</select>
</div>
<div class="filter-section">
<label class="filter-label">👤 Benutzer/Gerät</label>
<select id="userFilter" onchange="applyFilters()">
<option value="all">Alle anzeigen</option>
</select> </select>
</div> </div>
<div class="filter-section"> <div class="filter-section">
<label class="filter-label">⏱️ Zeitraum</label> <label class="filter-label">⏱️ Zeitraum</label>
<select id="timeFilter" onchange="applyFilters()"> <select id="timeFilter" onchange="applyFilters()">
<option value="all">Alle Zeitpunkte</option> <option value="1h" selected>Letzte Stunde</option>
<option value="1h">Letzte Stunde</option> <option value="3h">Letzte 3 Stunden</option>
<option value="6h">Letzte 6 Stunden</option> <option value="6h">Letzte 6 Stunden</option>
<option value="12h">Letzte 12 Stunden</option>
<option value="24h">Letzte 24 Stunden</option> <option value="24h">Letzte 24 Stunden</option>
<option value="7d">Letzte 7 Tage</option>
<option value="30d">Letzte 30 Tage</option>
</select> </select>
</div> </div>
@@ -148,6 +138,19 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<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) // Karte initialisieren (München)
const map = L.map('map').setView([48.1351, 11.5820], 12); const map = L.map('map').setView([48.1351, 11.5820], 12);
@@ -171,7 +174,7 @@
let currentLayer = mapLayers.standard; let currentLayer = mapLayers.standard;
currentLayer.addTo(map); currentLayer.addTo(map);
// API URL - anpassen an deine Domain // API URL
const API_URL = 'https://n8n.unixweb.home64.de/webhook/location'; const API_URL = 'https://n8n.unixweb.home64.de/webhook/location';
// State // State
@@ -181,6 +184,16 @@
let markerLayer = L.layerGroup().addTo(map); let markerLayer = L.layerGroup().addTo(map);
let polylineLayer = 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 // Change map layer
function changeMapLayer() { function changeMapLayer() {
const selectedLayer = document.getElementById('mapLayerSelect').value; const selectedLayer = document.getElementById('mapLayerSelect').value;
@@ -189,44 +202,25 @@
currentLayer.addTo(map); currentLayer.addTo(map);
} }
// Filter data by source (Telegram/MQTT) // Filter data by device
function filterBySource(locations) { function filterByDevice(locations) {
const sourceFilter = document.getElementById('sourceFilter').value; const deviceFilter = document.getElementById('deviceFilter').value;
if (sourceFilter === 'all') return locations; if (deviceFilter === 'all') return locations;
return locations.filter(loc => { return locations.filter(loc => loc.username === deviceFilter);
if (sourceFilter === 'telegram') {
return loc.user_id > 0; // Telegram has real user_id
} else if (sourceFilter === 'mqtt') {
return loc.user_id === 0; // MQTT has user_id = 0
}
return true;
});
}
// Filter data by user/device
function filterByUser(locations) {
const userFilter = document.getElementById('userFilter').value;
if (userFilter === 'all') return locations;
return locations.filter(loc => {
const identifier = `${loc.first_name || ''} ${loc.last_name || ''}`.trim() || loc.username || 'Unknown';
return identifier === userFilter;
});
} }
// Filter data by time range // Filter data by time range
function filterByTime(locations) { function filterByTime(locations) {
const timeFilter = document.getElementById('timeFilter').value; const timeFilter = document.getElementById('timeFilter').value;
if (timeFilter === 'all') return locations;
const now = new Date(); const now = new Date();
const ranges = { const ranges = {
'1h': 60 * 60 * 1000, '1h': 60 * 60 * 1000,
'3h': 3 * 60 * 60 * 1000,
'6h': 6 * 60 * 60 * 1000, '6h': 6 * 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000, '12h': 12 * 60 * 60 * 1000,
'7d': 7 * 24 * 60 * 60 * 1000, '24h': 24 * 60 * 60 * 1000
'30d': 30 * 24 * 60 * 60 * 1000
}; };
const cutoffTime = now - ranges[timeFilter]; const cutoffTime = now - ranges[timeFilter];
@@ -237,30 +231,31 @@
}); });
} }
// Update user filter dropdown with available users // Update device filter dropdown with available devices
function updateUserFilterOptions(locations) { function updateDeviceFilterOptions(locations) {
const userFilter = document.getElementById('userFilter'); const deviceFilter = document.getElementById('deviceFilter');
const currentValue = userFilter.value; const currentValue = deviceFilter.value;
// Get unique users // Get unique devices (username field)
const users = new Set(); const devices = new Set();
locations.forEach(loc => { locations.forEach(loc => {
const identifier = `${loc.first_name || ''} ${loc.last_name || ''}`.trim() || loc.username || 'Unknown'; if (loc.username) {
users.add(identifier); devices.add(loc.username);
}
}); });
// Rebuild options // Rebuild options
userFilter.innerHTML = '<option value="all">Alle anzeigen</option>'; deviceFilter.innerHTML = '<option value="all">Alle Geräte</option>';
Array.from(users).sort().forEach(user => { Array.from(devices).sort().forEach(username => {
const option = document.createElement('option'); const option = document.createElement('option');
option.value = user; option.value = username;
option.textContent = user; option.textContent = getDeviceName(username);
userFilter.appendChild(option); deviceFilter.appendChild(option);
}); });
// Restore previous selection if still available // Restore previous selection if still available
if (currentValue !== 'all' && users.has(currentValue)) { if (currentValue !== 'all' && devices.has(currentValue)) {
userFilter.value = currentValue; deviceFilter.value = currentValue;
} }
} }
@@ -268,11 +263,11 @@
function applyFilters() { function applyFilters() {
if (!allData || !allData.history) return; if (!allData || !allData.history) return;
let filteredData = [...allData.history]; // Nur MQTT-Daten (user_id = 0)
let filteredData = allData.history.filter(loc => loc.user_id === 0);
// Apply filters in sequence // Apply filters in sequence
filteredData = filterBySource(filteredData); filteredData = filterByDevice(filteredData);
filteredData = filterByUser(filteredData);
filteredData = filterByTime(filteredData); filteredData = filterByTime(filteredData);
// Update map // Update map
@@ -280,8 +275,9 @@
// Update status // Update status
document.getElementById('status').innerHTML = document.getElementById('status').innerHTML =
`Punkte: ${filteredData.length} / ${allData.total_points || 0}<br>` + `📊 Punkte: ${filteredData.length}<br>` +
`Status: ${allData.success ? '✅ Verbunden' : '❌ Fehler'}`; `📱 Geräte: ${new Set(filteredData.map(l => l.username)).size}<br>` +
`${allData.success ? '✅ Verbunden' : '❌ Fehler'}`;
} }
// Display filtered locations on map // Display filtered locations on map
@@ -292,31 +288,87 @@
if (locations.length === 0) return; if (locations.length === 0) return;
// Add markers // Gruppiere Locations nach Gerät
locations.forEach((loc, index) => { const deviceGroups = {};
const isLatest = index === 0; locations.forEach(loc => {
const markerIcon = L.icon({ const device = loc.username || 'unknown';
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-${isLatest ? 'red' : 'blue'}.png`, if (!deviceGroups[device]) {
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png', deviceGroups[device] = [];
iconSize: [25, 41], }
iconAnchor: [12, 41], deviceGroups[device].push(loc);
popupAnchor: [1, -34], });
shadowSize: [41, 41]
// 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];
}
}); });
L.marker([loc.latitude, loc.longitude], { icon: markerIcon }) // Add polyline if multiple points
.addTo(markerLayer) if (deviceLocs.length > 1) {
.bindPopup(`${loc.marker_label}<br>${loc.display_time}`); 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 (isLatest) { if (coords.length > 1) {
map.setView([loc.latitude, loc.longitude], 15); L.polyline(coords, { color: color, weight: 3, opacity: 0.7 }).addTo(polylineLayer);
}
} }
}); });
// Add polyline if multiple points // Zentriere auf erste Location
if (locations.length > 1) { if (firstLocation) {
const coords = locations.map(h => [h.latitude, h.longitude]); map.setView(firstLocation, 13);
L.polyline(coords, { color: 'blue', weight: 3 }).addTo(polylineLayer);
} }
} }
@@ -327,9 +379,12 @@
allData = data; allData = data;
// Update user filter dropdown // Nur MQTT-Daten für Device-Filter
if (data.history && data.history.length > 0) { const mqttData = data.history ? data.history.filter(loc => loc.user_id === 0) : [];
updateUserFilterOptions(data.history);
// Update device filter dropdown
if (mqttData.length > 0) {
updateDeviceFilterOptions(mqttData);
} }
// Apply filters and display // Apply filters and display