Überarbeite Dokumentation auf aktuellen MQTT-only Stand

- README.md komplett neu geschrieben:
  - Fokus auf n8n-tracker.json (MQTT-only)
  - Entfernt: Telegram-Workflows, Datei-basierte Speicherung
  - Hinzugefügt: OwnTracks-Setup, Geräte-Mapping, erweiterte Features
  - Neue Sektionen: Sicherheitshinweise, DSGVO-Compliance
  - Praktische Code-Beispiele für Customization

- CLAUDE.md aktualisiert:
  - Neue Workflow-Architektur dokumentiert
  - NocoDB-Schema mit battery/speed Feldern
  - Web-Interface Details (Filter, Kartenebenen, Marker)
  - Wichtige Gotchas und Edge Cases hinzugefügt

- Dateien bereinigt:
  - Gelöscht: tracker.json, tracker-db.json, tracker-mqtt.json
  - Gelöscht: index_owntrack.html, locations-example.csv
  - Hinzugefügt: n8n-tracker.json (aktueller Workflow)
  - Hinzugefügt: database-example.csv (aktuelles Schema)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-14 18:47:05 +00:00
parent 4bec87d42b
commit 59c46a023b
9 changed files with 1368 additions and 1979 deletions

474
CLAUDE.md
View File

@@ -4,108 +4,109 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Overview ## Overview
This repository contains three n8n workflow configurations for location tracking: This repository contains an **MQTT-based location tracking system** using n8n and NocoDB:
1. **tracker.json** - Telegram-based with file storage using `/tmp/n8n-locations.json` (simpler, no database required) - **n8n-tracker.json** - MQTT/OwnTracks workflow with NocoDB storage
2. **tracker-db.json** - Telegram-based with NocoDB storage (production-ready, persistent storage) - **index.html** - Web interface with device filtering, time-based filtering, and multiple map layers
3. **tracker-mqtt.json** - MQTT-based with NocoDB storage (for OwnTracks/MQTT location data)
The Telegram workflows share the same architecture but differ in storage backend. The MQTT workflow integrates OwnTracks-compatible location data into the same NocoDB database. The system subscribes to MQTT topics (OwnTracks-compatible), processes location data, stores it in NocoDB, and provides both a REST API and web visualization.
## Workflow Architecture ## Workflow Architecture
### Telegram-based Workflows The **n8n-tracker.json** workflow contains two independent execution flows:
Both Telegram workflows have two main execution paths: ### 1. MQTT Location Capture Flow
**1. Location Capture Flow (Telegram Trigger → Storage)** ```
MQTT Trigger (owntracks/#)
MQTT Location verarbeiten (JavaScript: Parse JSON, transform data)
Speichere in NocoDB (Create record with lat/lon, battery, speed, etc.)
```
tracker.json (File-based): **Key nodes:**
- Telegram Trigger → Hat Location? → Location verarbeiten → Lade existierende Daten → Merge mit History → Speichere in File → Telegram Bestätigung - **MQTT Trigger**: Subscribes to `owntracks/#` topic, receives JSON messages from OwnTracks devices
- **MQTT Location verarbeiten**:
- Parses the `message` field (JSON string)
- Validates required fields (lat, lon, tst)
- Transforms OwnTracks format to NocoDB schema
- Extracts telemetry data (battery, velocity, accuracy, altitude, etc.)
- Converts Unix timestamp to ISO 8601 format
- **Speichere in NocoDB**: Stores location with metadata in database
tracker-db.json (NocoDB-based): ### 2. Location API Flow
- Telegram Trigger → Hat Location? → Location verarbeiten → Speichere in NocoDB → [Hole letzten Eintrag + Zähle Einträge] → Merge → Bereite Bestätigung vor → Telegram Bestätigung
Key nodes: ```
- **Telegram Trigger**: Receives incoming Telegram messages Webhook - Location API (GET /webhook/location)
- **Hat Location?**: Filters messages containing location data
- **Location verarbeiten**: Extracts and formats location data (lat/lon, user info, timestamp) using JavaScript Lade Daten aus NocoDB (Get all records)
- **Storage**: Either file-based (shell commands) or NocoDB API calls
- **Telegram Bestätigung**: Sends confirmation with location details and map link Format API Response (JavaScript: Sort, structure JSON)
JSON Response (CORS-enabled)
```
**2. Location API Flow (Webhook → JSON Response)** - See section below **Key nodes:**
- **Webhook - Location API**: Public endpoint at `/webhook/location` with CORS enabled
### MQTT-based Workflow - **Lade Daten aus NocoDB**: Fetches all location records from database
- **Format API Response**: Sorts by timestamp (newest first), builds response structure
**tracker-mqtt.json** has a simpler, single-path architecture: - **JSON Response**: Returns structured JSON with CORS headers
MQTT Trigger → Ist Location? → MQTT Location verarbeiten → Speichere in NocoDB
Key nodes:
- **MQTT Trigger**: Subscribes to MQTT topic `owntracks/#` (OwnTracks-compatible)
- **Ist Location?**: Filters for messages with `_type: "location"`
- **MQTT Location verarbeiten**: Transforms MQTT/OwnTracks data format to match NocoDB schema
- **Speichere in NocoDB**: Stores location in same database as Telegram data
### Location API Flow (Shared)
**tracker.json (File-based)**:
- Webhook - Location API → Lade Daten für API (shell: cat) → Format API Response → JSON Response
**tracker-db.json (NocoDB-based)**:
- Webhook - Location API → Lade Daten aus NocoDB → Format API Response → JSON Response
Both expose `/location` endpoint with CORS enabled, returning current location, history, and metadata. The MQTT workflow shares the same NocoDB database and thus the same API.
## Key Technical Details ## Key Technical Details
### Data Storage ### Data Storage
**tracker.json (File-based)**: **NocoDB Database Configuration:**
- Location: `/tmp/n8n-locations.json` - **Project ID**: `pdxl4cx4dbu9nxi`
- Format: JSON array of location objects - **Table ID**: `m8pqj5ixgnnrzkg`
- Max retention: 100 most recent locations (oldest automatically removed) - **Credential ID**: `T9XuGr6CJD2W2BPO` (NocoDB Token account)
- Persistence: Survives n8n restarts but may be lost on system restart due to `/tmp` location - **Persistence**: Full database persistence (no client-side limit)
**tracker-db.json & tracker-mqtt.json (NocoDB)**: ### NocoDB Schema
- Location: NocoDB database (Project: `pdxl4cx4dbu9nxi`, Table: `m8pqj5ixgnnrzkg`)
- Format: NocoDB records (no client-side limit)
- Persistence: Full database persistence
- API: Uses n8n's NocoDB node with token authentication (ID: `6fNBtcghMe8wFoE5`)
- Both workflows write to the same database table
### Location Object Structure (NocoDB Schema) The database stores location records with the following fields:
The NocoDB database uses the following schema for all location sources: ```
latitude (Decimal) - Geographic latitude
```json longitude (Decimal) - Geographic longitude
{ timestamp (DateTime) - ISO 8601 timestamp
"latitude": number, user_id (Number) - Always 0 for MQTT devices
"longitude": number, first_name (Text) - Tracker ID (e.g., "10", "11")
"timestamp": "ISO 8601 string", last_name (Text) - Source type (e.g., "fused")
"user_id": number, username (Text) - Same as tracker ID
"first_name": string, marker_label (Text) - Display label for map markers
"last_name": string, display_time (Text) - Formatted timestamp (de-DE locale)
"username": string, chat_id (Number) - Always 0 for MQTT devices
"marker_label": string, battery (Number) - Battery percentage (0-100)
"display_time": "de-DE locale string", speed (Decimal) - Velocity in m/s
"chat_id": number
}
``` ```
**Data Mapping for MQTT/OwnTracks**: ### OwnTracks Data Mapping
- `latitude``lat`
- `longitude``lon`
- `timestamp``tst` (Unix timestamp converted to ISO 8601)
- `user_id``0` (MQTT has no user ID)
- `first_name``tid` (tracker ID, e.g., "le")
- `last_name``source` (e.g., "fused")
- `username``tid`
- `marker_label` ← Combination of `tid` and optionally `SSID`
- `display_time` ← Formatted timestamp
- `chat_id``0` (MQTT has no chat ID)
Additional MQTT data (accuracy, altitude, battery, velocity, etc.) is available in the transformation node but not stored in the database. The MQTT transformation node maps OwnTracks JSON fields to NocoDB schema:
| NocoDB Field | OwnTracks Field | Transformation |
|--------------|-----------------|----------------|
| `latitude` | `lat` | Direct mapping |
| `longitude` | `lon` | Direct mapping |
| `timestamp` | `tst` | Unix timestamp → ISO 8601 |
| `user_id` | - | Static: `0` |
| `first_name` | `tid` | Tracker ID (device identifier) |
| `last_name` | `source` | Location source (e.g., "fused") |
| `username` | `tid` | Same as tracker ID |
| `marker_label` | `tid` | Used for map display |
| `display_time` | `tst` | Formatted with `de-DE` locale |
| `chat_id` | - | Static: `0` |
| `battery` | `batt` | Battery percentage |
| `speed` | `vel` | Velocity in m/s |
**Additional OwnTracks data available but NOT stored:**
- `acc` - Accuracy in meters
- `alt` - Altitude
- `cog` - Course over ground
- `conn` - Connection type (w=WiFi, m=Mobile)
- `_id` - Device identifier
### API Response Structure ### API Response Structure
```json ```json
@@ -118,116 +119,257 @@ Additional MQTT data (accuracy, altitude, battery, velocity, etc.) is available
} }
``` ```
### Web Interface ### Web Interface (index.html)
The workflow sends users a link to `https://web.unixweb.home64.de/tracker/index.html` for viewing locations on a map.
The `index.html` file in this repository provides a standalone web interface: The web interface is a single-page application built with Leaflet.js:
- **Map Library**: Leaflet.js (loaded from CDN)
- **API Endpoint**: Configured to `https://n8n.unixweb.eu/webhook/location` (index.html:85)
- **Features**: Auto-refresh (5 second interval), location history polyline, marker popups
- **Hosting**: Can be hosted on any web server or opened as a local file
## Workflow Configuration **Configuration:**
- **API Endpoint**: `https://n8n.unixweb.home64.de/webhook/location` (line 178)
- **Default View**: Munich (48.1351, 11.5820) at zoom level 12
- **Auto-refresh**: 5 second interval (configurable)
### Shared Configuration (Both Workflows) **Key Features:**
- **Telegram Credentials**: "Telegram account n8n-munich-bot" (ID: `dRHgVQKqowQHIait`) 1. **Multiple Map Layers** (lines 158-171):
- **Webhook IDs**: - Standard (OpenStreetMap)
- Telegram trigger: `telegram-location-webhook` - Satellite (Esri World Imagery)
- Location API: `location-api-endpoint` - Terrain (OpenTopoMap)
- Dark Mode (CartoDB Dark)
2. **Device Mapping** (lines 142-152):
- Hardcoded device names: `'10'` → "Joachim Pixel", `'11'` → "Huawei Smartphone"
- Device-specific colors: Red (#e74c3c) for device 10, Blue (#3498db) for device 11
- **Important**: Device names are mapped from the `username` field (which contains the tracker ID)
3. **Filtering System**:
- **Device Filter**: Dropdown populated dynamically from available `username` values
- **Time Filter**: 1h, 3h, 6h, 12h, 24h (default: 1 hour)
- Filter logic: Always filters to `user_id == 0` (MQTT-only), then applies device and time filters
4. **Visualization** (lines 284-376):
- **Markers**: Circular SVG icons with navigation-style clock hand
- **Size**: Latest location = 32x32px, history = 16x16px
- **Colors**: Device-specific colors from `DEVICE_COLORS` mapping
- **Polylines**: Shows movement path per device, color-coded
- **Popups**: Show device name, timestamp, battery %, speed (km/h)
**Important Implementation Details:**
- The `username` field filter logic (line 267) filters MQTT data by checking `user_id == 0`
- Device colors and names must be updated in the hardcoded mappings (lines 142-152)
- Speed conversion: OwnTracks velocity (m/s) is converted to km/h with `speed * 3.6` (line 329)
## Workflow Configuration (n8n-tracker.json)
**Workflow Settings:**
- **Name**: "Telegram Location Tracker - NocoDB"
- **Workflow ID**: `6P6dKqi4IKcJ521m`
- **Version ID**: `de17706a-a0ea-42ce-a069-dd09dce421d2`
- **Execution Order**: v1 - **Execution Order**: v1
- **Caller Policy**: workflowsFromSameOwner - **Caller Policy**: workflowsFromSameOwner
- **Status**: Both workflows set to `active: true` by default - **Status**: `active: true`
- **Error Workflow**: `0bBZzSE6SUzVsif5`
- **Tags**: "owntrack"
### tracker.json Specific **Credentials:**
- **Storage**: Shell commands (cat, echo) for file I/O - **MQTT**: Credential ID `L07VVR2BDfDda6Zo` ("MQTT account")
- **Error Workflow**: ID `PhwIkaqyXRasTXDH` (configured but not in this export) - **NocoDB**: Credential ID `T9XuGr6CJD2W2BPO` ("NocoDB Token account")
### tracker-db.json Specific **Node Configuration:**
- **NocoDB Credentials**: "NocoDB Token account" (ID: `6fNBtcghMe8wFoE5`) - **MQTT Trigger**:
- **Project ID**: `pdxl4cx4dbu9nxi` - Topic: `owntracks/#`
- **Table ID**: `m8pqj5ixgnnrzkg` - Subscribes to all OwnTracks topics
- **Sort Order**: Uses array structure `[{field: "timestamp", direction: "desc"}]` for sorting locations - No message filtering at trigger level (all messages pass through)
### tracker-mqtt.json Specific - **MQTT Location verarbeiten** (Code Node):
- **MQTT Credentials**: Requires MQTT broker credentials (placeholder ID: `MQTT_CREDENTIAL_ID`) - Parses JSON from `message` field
- **Topic**: `owntracks/#` (subscribes to all OwnTracks topics) - Validates required fields: `lat`, `lon`, `tst`
- **Message Filter**: Only processes messages with `_type: "location"` - Skips invalid messages with `continue`
- **NocoDB Config**: Same as tracker-db.json (shares database) - Sets `alwaysOutputData: true` to handle empty results
- **Status**: Set to `active: false` by default (activate after configuring MQTT credentials) - Timezone: Europe/Berlin for `display_time`
## Modifying the Workflows - **Speichere in NocoDB**:
- Operation: `create`
- Maps 12 fields from JSON to NocoDB columns
- Includes telemetry: `battery` (from `mqtt_data.battery`), `speed` (from `mqtt_data.velocity`)
### Important Considerations - **Webhook - Location API**:
1. **Both workflows are active by default** - test changes carefully to avoid disrupting live tracking - Path: `/location`
2. **JavaScript code nodes** use n8n's execution environment (not vanilla Node.js) - Webhook ID: `location-api-endpoint`
3. **Date formatting** uses `de-DE` locale - change in "Location verarbeiten" node if needed - Response Mode: `lastNode`
4. **CORS** is configured for `*` (all origins) - restrict for production security - CORS: Allowed origins = `*`
### tracker.json Specific ## Common Modifications
- Shell commands execute in n8n's runtime environment - ensure `/tmp` is writable
- The 100-entry limit prevents unbounded growth - adjust in "Merge mit History" node
- Consider moving from `/tmp` to persistent storage for production (see README.md)
### tracker-db.json & tracker-mqtt.json Specific ### Adding a New Device
- NocoDB sorting requires array structure: `[{field: "timestamp", direction: "desc"}]`
- No client-side entry limit (relies on database capacity)
- Requires valid NocoDB credentials and accessible database
- Both workflows write to the same NocoDB table
### tracker-mqtt.json Specific **Step 1: Update index.html device mappings (lines 142-152)**
- Requires MQTT broker configuration before activation ```javascript
- Topic pattern can be adjusted to match your MQTT setup const DEVICE_NAMES = {
- Data transformation maps OwnTracks format to NocoDB schema '10': 'Joachim Pixel',
- MQTT data fields (accuracy, battery, velocity) are extracted but not persisted to database '11': 'Huawei Smartphone',
'12': 'New Device Name' // Add this line
};
### Common Modifications const DEVICE_COLORS = {
'10': '#e74c3c',
'11': '#3498db',
'12': '#2ecc71', // Add this line (green)
'default': '#95a5a6'
};
```
**Change history limit (tracker.json only)**: **Step 2: Configure OwnTracks app**
In "Merge mit History" node, change `locations.slice(0, 100)` to desired limit - Set Tracker ID (`tid`) to match the key (e.g., "12")
- Topic will be `owntracks/user/12`
- The workflow automatically picks up new devices
**Change date format**: ### Changing Date/Time Format
In "Location verarbeiten" node, change `.toLocaleString('de-DE')` to desired locale
**Restrict CORS**: **In n8n workflow node "MQTT Location verarbeiten" (line 124):**
In "Webhook - Location API" node, change `Access-Control-Allow-Origin: *` to specific domain ```javascript
// Current: German format with Berlin timezone
const displayTime = new Date(timestampMs).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
**Update web interface URL**: // Change to US format:
In "Telegram Bestätigung" node and `index.html:85`, update API endpoint URL const displayTime = new Date(timestampMs).toLocaleString('en-US', { timeZone: 'America/New_York' });
// Change to ISO format:
const displayTime = new Date(timestampMs).toISOString();
```
### Restricting CORS (Security)
**In n8n workflow node "Webhook - Location API" (lines 65-75):**
```javascript
// Current (insecure):
{ "name": "Access-Control-Allow-Origin", "value": "*" }
// Change to specific domain:
{ "name": "Access-Control-Allow-Origin", "value": "https://web.unixweb.home64.de" }
```
### Adding New NocoDB Fields
**Example: Store accuracy and altitude**
**Step 1: Add columns in NocoDB:**
- `accuracy` (Number)
- `altitude` (Number)
**Step 2: Update "MQTT Location verarbeiten" node (line 124):**
```javascript
mqtt_data: {
accuracy: mqttData.acc,
altitude: mqttData.alt,
battery: mqttData.batt,
velocity: mqttData.vel,
// ... existing fields
}
```
**Step 3: Update "Speichere in NocoDB" node to map new fields:**
```javascript
{ "fieldName": "accuracy", "fieldValue": "={{ $json.mqtt_data.accuracy }}" },
{ "fieldName": "altitude", "fieldValue": "={{ $json.mqtt_data.altitude }}" }
```
### Changing MQTT Topic Filter
**In node "MQTT Trigger" (line 104):**
```javascript
// Current: All OwnTracks topics
topics: "owntracks/#"
// Change to specific user:
topics: "owntracks/joachim/#"
// Change to specific device:
topics: "owntracks/joachim/pixel"
// Multiple topics:
topics: "owntracks/joachim/#,owntracks/lisa/#"
```
### Updating API Endpoint URL
**In index.html (line 178):**
```javascript
// Current:
const API_URL = 'https://n8n.unixweb.home64.de/webhook/location';
// Change to your n8n instance:
const API_URL = 'https://your-n8n.example.com/webhook/location';
```
### Changing Auto-Refresh Interval
**In index.html (line 419):**
```javascript
// Current: 5 seconds
refreshInterval = setInterval(loadLocations, 5000);
// Change to 10 seconds:
refreshInterval = setInterval(loadLocations, 10000);
// Change to 30 seconds:
refreshInterval = setInterval(loadLocations, 30000);
```
## Repository Contents ## Repository Contents
- **tracker.json** - Telegram with file-based storage (simple, no database) | File | Description |
- **tracker-db.json** - Telegram with NocoDB storage (production-ready) |------|-------------|
- **tracker-mqtt.json** - MQTT/OwnTracks with NocoDB storage (IoT devices) | `n8n-tracker.json` | n8n workflow - MQTT location capture + API endpoint |
- **index.html** - Leaflet.js web interface for map visualization | `index.html` | Web interface with multi-layer maps and device filtering |
- **locations-example.csv** - Example data format for testing | `database-example.csv` | Sample NocoDB export showing actual data structure |
- **README.md** - Detailed German documentation with setup and usage instructions | `README.md` | Comprehensive German documentation (setup, usage, troubleshooting) |
| `CLAUDE.md` | This file - technical architecture documentation |
## MQTT/OwnTracks Integration ## Important Gotchas and Edge Cases
The **tracker-mqtt.json** workflow is designed for OwnTracks-compatible MQTT location data. ### 1. Device Identification via `username` Field
- The `username` field contains the OwnTracks tracker ID (`tid`), not a username
- The web interface filters devices by `username`, not by `first_name` or `marker_label`
- **Example**: Device with `tid: "10"` will have `username: "10"` in database
- Device names are hardcoded in `index.html` (lines 142-145) - must be manually updated
### Setup Requirements ### 2. MQTT Message Validation
1. Configure MQTT broker credentials in n8n - The workflow does NOT filter by `_type: "location"` despite what older documentation says
2. Update the credential ID in tracker-mqtt.json (currently placeholder: `MQTT_CREDENTIAL_ID`) - All MQTT messages are processed; validation happens in the JavaScript code node
3. Import workflow into n8n - Messages missing `lat`, `lon`, or `tst` are silently skipped with `continue`
4. Activate the workflow - **Result**: Non-location MQTT messages don't cause errors, they're just ignored
### OwnTracks Configuration ### 3. Time Filter Default
Configure your OwnTracks app/device to publish to the same MQTT broker: - The web interface defaults to **1 hour** time filter (line 125)
- **Topic pattern**: `owntracks/user/device` (workflow subscribes to `owntracks/#`) - This means newly deployed users won't see historical data unless they change the filter
- **Mode**: MQTT - Consider changing default to `24h` or `all` for better initial experience
- **Expected message format**: JSON with `_type: "location"`
### Data Flow ### 4. Circular Marker Icon Implementation
1. OwnTracks device publishes location to MQTT broker - Markers use SVG `divIcon` with a navigation-style clock hand (lines 337-345)
2. n8n MQTT trigger receives message - The clock hand is purely decorative, does NOT represent actual direction/heading
3. Filter checks for `_type: "location"` - This replaced standard Leaflet pin icons in recent commits (see commit 4bec87d)
4. Data transformation maps MQTT fields to NocoDB schema
5. Location stored in same database as Telegram locations
6. Available via same `/location` API endpoint
### Distinguishing Data Sources ### 5. Speed Unit Conversion
In the database: - OwnTracks sends velocity in **m/s** (`vel` field)
- **Telegram entries**: Have real `user_id` and `chat_id` values, `first_name`/`last_name` from Telegram profile - Stored in database as m/s in `speed` column
- **MQTT entries**: Have `user_id: 0` and `chat_id: 0`, use `tid` (tracker ID) in name fields - Converted to km/h in web UI with `speed * 3.6` (line 329)
- **Important**: If you display speed elsewhere, remember to convert
### 6. Battery Data May Be Null
- Not all OwnTracks messages include battery data
- The code checks for `battery !== undefined && battery !== null` before displaying (line 323)
- Same applies to `speed` field (line 328)
### 7. CORS Configuration
- API has CORS set to `*` (all origins allowed)
- This is intentional for development but **insecure for production**
- See "Restricting CORS" section for how to fix
### 8. Error Workflow Reference
- The workflow references error workflow ID `0bBZzSE6SUzVsif5`
- This error workflow is NOT included in the repository export
- If importing to a new n8n instance, errors will fail silently without this workflow
### 9. Timezone Handling
- All timestamps are converted to Europe/Berlin timezone (line 124 in workflow)
- This is hardcoded in the JavaScript transformation
- Database stores ISO 8601 UTC timestamps, but `display_time` is Berlin time

1332
README.md

File diff suppressed because it is too large Load Diff

12
database-example.csv Normal file
View File

@@ -0,0 +1,12 @@
latitude,longitude,timestamp,user_id,first_name,last_name,username,marker_label,display_time,chat_id,battery,speed
48.1383784,11.4276172,2025-11-14T18:00:37.000Z,0,11,fused,11,11,"14.11.2025, 19:00:37",0,73,0
48.1383761,11.4276122,2025-11-14T18:00:43.000Z,0,11,fused,11,11,"14.11.2025, 19:00:43",0,73,0
48.1383761,11.4276122,2025-11-14T18:00:43.000Z,0,11,fused,11,11,"14.11.2025, 19:00:43",0,73,0
48.1383739,11.4276184,2025-11-14T18:00:49.000Z,0,11,fused,11,11,"14.11.2025, 19:00:49",0,73,0
48.1383635,11.4276113,2025-11-14T18:00:56.000Z,0,11,fused,11,11,"14.11.2025, 19:00:56",0,73,0
48.1383631,11.4276126,2025-11-14T18:00:58.000Z,0,11,fused,11,11,"14.11.2025, 19:00:58",0,73,0
48.1383735,11.4276116,2025-11-14T18:01:08.000Z,0,11,fused,11,11,"14.11.2025, 19:01:08",0,73,0
48.1383595,11.4276089,2025-11-14T18:01:14.000Z,0,11,fused,11,11,"14.11.2025, 19:01:14",0,73,0
48.1383581,11.4275939,2025-11-14T18:01:20.000Z,0,11,fused,11,11,"14.11.2025, 19:01:20",0,73,0
48.1383497,11.4276117,2025-11-14T18:01:26.000Z,0,11,fused,11,11,"14.11.2025, 19:01:26",0,73,0
1 latitude longitude timestamp user_id first_name last_name username marker_label display_time chat_id battery speed
2 48.1383784 11.4276172 2025-11-14T18:00:37.000Z 0 11 fused 11 11 14.11.2025, 19:00:37 0 73 0
3 48.1383761 11.4276122 2025-11-14T18:00:43.000Z 0 11 fused 11 11 14.11.2025, 19:00:43 0 73 0
4 48.1383761 11.4276122 2025-11-14T18:00:43.000Z 0 11 fused 11 11 14.11.2025, 19:00:43 0 73 0
5 48.1383739 11.4276184 2025-11-14T18:00:49.000Z 0 11 fused 11 11 14.11.2025, 19:00:49 0 73 0
6 48.1383635 11.4276113 2025-11-14T18:00:56.000Z 0 11 fused 11 11 14.11.2025, 19:00:56 0 73 0
7 48.1383631 11.4276126 2025-11-14T18:00:58.000Z 0 11 fused 11 11 14.11.2025, 19:00:58 0 73 0
8 48.1383735 11.4276116 2025-11-14T18:01:08.000Z 0 11 fused 11 11 14.11.2025, 19:01:08 0 73 0
9 48.1383595 11.4276089 2025-11-14T18:01:14.000Z 0 11 fused 11 11 14.11.2025, 19:01:14 0 73 0
10 48.1383581 11.4275939 2025-11-14T18:01:20.000Z 0 11 fused 11 11 14.11.2025, 19:01:20 0 73 0
11 48.1383497 11.4276117 2025-11-14T18:01:26.000Z 0 11 fused 11 11 14.11.2025, 19:01:26 0 73 0

View File

@@ -1,224 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Location Test</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: 200px;
}
.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>📍 Location Tracker</h3>
<div id="status">Lade...</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>
// Karte initialisieren (München)
const map = L.map('map').setView([48.1351, 11.5820], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
// API URL - anpassen an deine Domain
const API_URL = 'https://n8n.unixweb.home64.de/webhook/location';
// Auto-Refresh State
let autoRefreshEnabled = true;
let refreshInterval = null;
let markers = [];
let polylines = [];
async function loadLocations() {
try {
console.log('Fetching locations from:', API_URL);
const response = await fetch(API_URL);
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Data received:', data);
// Alte Marker und Linien entfernen
markers.forEach(marker => map.removeLayer(marker));
polylines.forEach(polyline => map.removeLayer(polyline));
markers = [];
polylines = [];
// Status aktualisieren
const statusDiv = document.getElementById('status');
statusDiv.innerHTML =
`Punkte: ${data.total_points || 0}<br>` +
`Status: ✅ Verbunden`;
if (data.current) {
const loc = data.current;
console.log('Current location object:', loc);
console.log('latitude field:', loc.latitude, 'type:', typeof loc.latitude);
console.log('longitude field:', loc.longitude, 'type:', typeof loc.longitude);
const lat = parseFloat(loc.latitude);
const lon = parseFloat(loc.longitude);
console.log('Parsed lat:', lat, 'lon:', lon);
if (isNaN(lat) || isNaN(lon)) {
throw new Error(`Invalid coordinates: lat=${loc.latitude} (${typeof loc.latitude}), lon=${loc.longitude} (${typeof loc.longitude})`);
}
// Popup-Inhalt aufbauen
let popupContent = `${loc.marker_label || 'Unknown'}<br>${loc.display_time || ''}`;
// Batterie hinzufügen, falls vorhanden
if (loc.battery !== undefined && loc.battery !== null) {
popupContent += `<br>🔋 Batterie: ${loc.battery}%`;
}
// Geschwindigkeit hinzufügen, falls vorhanden
if (loc.speed !== undefined && loc.speed !== null) {
const speedKmh = (loc.speed * 3.6).toFixed(1); // m/s zu km/h
popupContent += `<br>🚗 Speed: ${speedKmh} km/h`;
}
const marker = L.marker([lat, lon])
.addTo(map)
.bindPopup(popupContent)
.openPopup();
markers.push(marker);
map.setView([lat, lon], 15);
// Historie als Linie
if (data.history && data.history.length > 1) {
const coords = data.history
.map(h => {
const hlat = parseFloat(h.latitude);
const hlon = parseFloat(h.longitude);
if (!isNaN(hlat) && !isNaN(hlon)) {
return [hlat, hlon];
}
return null;
})
.filter(c => c !== null);
if (coords.length > 1) {
const polyline = L.polyline(coords, {color: 'blue', weight: 3}).addTo(map);
polylines.push(polyline);
}
}
} else {
console.log('No current location in response');
statusDiv.innerHTML += '<br>Keine aktuelle Position';
}
} catch (error) {
console.error('Load error:', error);
console.error('Error stack:', error.stack);
document.getElementById('status').innerHTML =
`❌ Verbindungsfehler<br><small>${error.message}</small>`;
}
}
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
const btn = document.getElementById('toggleBtn');
const indicator = btn.querySelector('.status-indicator');
if (autoRefreshEnabled) {
// Aktiviere Auto-Refresh
btn.className = 'toggle-btn active';
btn.innerHTML = '<span class="status-indicator active"></span>Auto-Refresh: AN';
startAutoRefresh();
} else {
// Deaktiviere Auto-Refresh
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>

View File

@@ -1,9 +0,0 @@
latitude,longitude,timestamp,user_id,first_name,last_name,username,marker_label,display_time,chat_id
48.1351,11.5820,2025-01-15T10:30:00.000Z,123456789,Max,Mustermann,maxmustermann,Max Mustermann,"15.1.2025, 11:30:00",987654321
48.1375,11.5750,2025-01-15T11:45:00.000Z,123456789,Max,Mustermann,maxmustermann,Max Mustermann,"15.1.2025, 12:45:00",987654321
48.1400,11.5680,2025-01-15T13:15:00.000Z,123456789,Max,Mustermann,maxmustermann,Max Mustermann,"15.1.2025, 14:15:00",987654321
48.1425,11.5610,2025-01-15T14:30:00.000Z,123456789,Max,Mustermann,maxmustermann,Max Mustermann,"15.1.2025, 15:30:00",987654321
48.1450,11.5540,2025-01-15T15:45:00.000Z,123456789,Max,Mustermann,maxmustermann,Max Mustermann,"15.1.2025, 16:45:00",987654321
48.1475,11.5470,2025-01-15T17:00:00.000Z,987654321,Anna,Schmidt,annaschmidt,Anna Schmidt,"15.1.2025, 18:00:00",123456789
48.1500,11.5400,2025-01-15T18:15:00.000Z,987654321,Anna,Schmidt,annaschmidt,Anna Schmidt,"15.1.2025, 19:15:00",123456789
48.1525,11.5330,2025-01-15T19:30:00.000Z,987654321,Anna,Schmidt,annaschmidt,Anna Schmidt,"15.1.2025, 20:30:00",123456789
1 latitude longitude timestamp user_id first_name last_name username marker_label display_time chat_id
2 48.1351 11.5820 2025-01-15T10:30:00.000Z 123456789 Max Mustermann maxmustermann Max Mustermann 15.1.2025, 11:30:00 987654321
3 48.1375 11.5750 2025-01-15T11:45:00.000Z 123456789 Max Mustermann maxmustermann Max Mustermann 15.1.2025, 12:45:00 987654321
4 48.1400 11.5680 2025-01-15T13:15:00.000Z 123456789 Max Mustermann maxmustermann Max Mustermann 15.1.2025, 14:15:00 987654321
5 48.1425 11.5610 2025-01-15T14:30:00.000Z 123456789 Max Mustermann maxmustermann Max Mustermann 15.1.2025, 15:30:00 987654321
6 48.1450 11.5540 2025-01-15T15:45:00.000Z 123456789 Max Mustermann maxmustermann Max Mustermann 15.1.2025, 16:45:00 987654321
7 48.1475 11.5470 2025-01-15T17:00:00.000Z 987654321 Anna Schmidt annaschmidt Anna Schmidt 15.1.2025, 18:00:00 123456789
8 48.1500 11.5400 2025-01-15T18:15:00.000Z 987654321 Anna Schmidt annaschmidt Anna Schmidt 15.1.2025, 19:15:00 123456789
9 48.1525 11.5330 2025-01-15T19:30:00.000Z 987654321 Anna Schmidt annaschmidt Anna Schmidt 15.1.2025, 20:30:00 123456789

290
n8n-tracker.json Normal file
View File

@@ -0,0 +1,290 @@
{
"name": "Telegram Location Tracker - NocoDB",
"nodes": [
{
"parameters": {
"path": "location",
"responseMode": "lastNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "116b19ae-d634-48cd-aae8-3d9a4dbfebf3",
"name": "Webhook - Location API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
256,
336
],
"webhookId": "location-api-endpoint"
},
{
"parameters": {
"authentication": "nocoDbApiToken",
"operation": "getAll",
"projectId": "pdxl4cx4dbu9nxi",
"table": "m8pqj5ixgnnrzkg",
"returnAll": true,
"options": {}
},
"id": "e9b8577e-b736-4f3f-98e9-1309f5373f43",
"name": "Lade Daten aus NocoDB",
"type": "n8n-nodes-base.nocoDb",
"typeVersion": 2,
"position": [
448,
336
],
"credentials": {
"nocoDbApiToken": {
"id": "T9XuGr6CJD2W2BPO",
"name": "NocoDB Token account"
}
}
},
{
"parameters": {
"jsCode": "// Baue API Response aus NocoDB-Daten\nconst locations = $input.all().map(item => item.json);\n\n// Sortiere nach timestamp (neueste zuerst)\nlocations.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));\n\nconst current = locations.length > 0 ? locations[0] : null;\n\nreturn [{\n json: {\n success: true,\n current: current,\n history: locations,\n total_points: locations.length,\n last_updated: current ? current.timestamp : null\n }\n}];"
},
"id": "771d0ec3-2248-4159-b03c-9382f60f765f",
"name": "Format API Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
336
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "2d096d00-8844-473a-91f0-5a9962b5ac28",
"name": "JSON Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
816,
336
]
},
{
"parameters": {
"height": 544,
"width": 1104,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"typeVersion": 1,
"id": "388be06b-01ba-4e7b-8e5a-5be25fa4efa0",
"name": "Sticky Note"
},
{
"parameters": {
"topics": "owntracks/#",
"options": {}
},
"id": "cadaab24-22b6-4c44-bee3-2a10f7b4a68c",
"name": "MQTT Trigger",
"type": "n8n-nodes-base.mqttTrigger",
"typeVersion": 1,
"position": [
336,
128
],
"credentials": {
"mqtt": {
"id": "L07VVR2BDfDda6Zo",
"name": "MQTT account"
}
}
},
{
"parameters": {
"jsCode": "// Extrahiere MQTT Location-Daten für NocoDB\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n try {\n // Parse den JSON-String aus dem message-Feld\n const mqttData = JSON.parse(item.json.message);\n \n // Validiere erforderliche Felder\n if (!mqttData.lat || !mqttData.lon || !mqttData.tst) {\n console.log('Fehlende Felder:', mqttData);\n continue;\n }\n \n // Timestamp (tst = Unix timestamp in seconds)\n const timestampMs = mqttData.tst * 1000;\n const timestamp = new Date(timestampMs).toISOString();\n const displayTime = new Date(timestampMs).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });\n \n // Erstelle marker_label aus tid (tracker ID)\n const trackerLabel = mqttData.tid || 'Unknown';\n \n results.push({\n json: {\n latitude: mqttData.lat,\n longitude: mqttData.lon,\n timestamp: timestamp,\n user_id: 0,\n first_name: trackerLabel,\n last_name: mqttData.source || 'mqtt',\n username: mqttData.tid || '',\n marker_label: trackerLabel,\n display_time: displayTime,\n chat_id: 0,\n // Optional: Zusätzliche MQTT-Daten\n mqtt_data: {\n accuracy: mqttData.acc,\n altitude: mqttData.alt,\n battery: mqttData.batt,\n velocity: mqttData.vel,\n course: mqttData.cog,\n connection: mqttData.conn,\n device_id: mqttData._id\n }\n }\n });\n } catch (error) {\n console.error('Parse-Fehler:', error.message);\n continue;\n }\n}\n\nreturn results;"
},
"id": "cc71dfb1-3f18-4e57-af6d-df952615ff46",
"name": "MQTT Location verarbeiten",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
592,
128
],
"alwaysOutputData": true
},
{
"parameters": {
"authentication": "nocoDbApiToken",
"operation": "create",
"projectId": "pdxl4cx4dbu9nxi",
"table": "m8pqj5ixgnnrzkg",
"fieldsUi": {
"fieldValues": [
{
"fieldName": "latitude",
"fieldValue": "={{ $json.latitude }}"
},
{
"fieldName": "longitude",
"fieldValue": "={{ $json.longitude }}"
},
{
"fieldName": "timestamp",
"fieldValue": "={{ $json.timestamp }}"
},
{
"fieldName": "user_id",
"fieldValue": "={{ $json.user_id }}"
},
{
"fieldName": "first_name",
"fieldValue": "={{ $json.first_name }}"
},
{
"fieldName": "last_name",
"fieldValue": "={{ $json.last_name }}"
},
{
"fieldName": "username",
"fieldValue": "={{ $json.username }}"
},
{
"fieldName": "marker_label",
"fieldValue": "={{ $json.marker_label }}"
},
{
"fieldName": "display_time",
"fieldValue": "={{ $json.display_time }}"
},
{
"fieldName": "chat_id",
"fieldValue": "={{ $json.chat_id }}"
},
{
"fieldName": "battery",
"fieldValue": "={{ $json.mqtt_data.battery }}"
},
{
"fieldName": "speed",
"fieldValue": "={{ $json.mqtt_data.velocity }}"
}
]
}
},
"id": "ea8305b8-3d7f-4b9a-8ba6-04406d21815f",
"name": "Speichere in NocoDB",
"type": "n8n-nodes-base.nocoDb",
"typeVersion": 2,
"position": [
832,
128
],
"credentials": {
"nocoDbApiToken": {
"id": "T9XuGr6CJD2W2BPO",
"name": "NocoDB Token account"
}
}
}
],
"pinData": {},
"connections": {
"Webhook - Location API": {
"main": [
[
{
"node": "Lade Daten aus NocoDB",
"type": "main",
"index": 0
}
]
]
},
"Lade Daten aus NocoDB": {
"main": [
[
{
"node": "Format API Response",
"type": "main",
"index": 0
}
]
]
},
"Format API Response": {
"main": [
[
{
"node": "JSON Response",
"type": "main",
"index": 0
}
]
]
},
"MQTT Trigger": {
"main": [
[
{
"node": "MQTT Location verarbeiten",
"type": "main",
"index": 0
}
]
]
},
"MQTT Location verarbeiten": {
"main": [
[
{
"node": "Speichere in NocoDB",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"errorWorkflow": "0bBZzSE6SUzVsif5"
},
"versionId": "de17706a-a0ea-42ce-a069-dd09dce421d2",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "2f7fd37175cefa82de44e06b8af9ab9c01e7956018045d6efc4f7bf1588a41eb"
},
"id": "6P6dKqi4IKcJ521m",
"tags": [
{
"updatedAt": "2025-11-14T13:41:54.886Z",
"createdAt": "2025-11-14T13:41:54.886Z",
"id": "i5Azv4hS7iOZaiHp",
"name": "owntrack"
}
]
}

View File

@@ -1,465 +0,0 @@
{
"name": "Telegram Location Tracker - NocoDB",
"nodes": [
{
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"id": "0ff38437-1b45-4a44-80bb-708f48e3a0d2",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [
80,
128
],
"webhookId": "telegram-location-webhook",
"credentials": {
"telegramApi": {
"id": "dRHgVQKqowQHIait",
"name": "Telegram account n8n-munich-bot"
}
}
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.message.location }}",
"operation": "isNotEmpty"
}
]
}
},
"id": "cf11bd9c-c392-45d0-9fcf-b673c7302f98",
"name": "Hat Location?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
304,
128
]
},
{
"parameters": {
"jsCode": "// Extrahiere Location-Daten für NocoDB\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const location = item.json.message.location;\n const from = item.json.message.from;\n const messageDate = item.json.message.date;\n \n const timestamp = new Date(messageDate * 1000).toISOString();\n const displayTime = new Date(messageDate * 1000).toLocaleString('de-DE');\n \n results.push({\n json: {\n latitude: location.latitude,\n longitude: location.longitude,\n timestamp: timestamp,\n user_id: from.id,\n first_name: from.first_name || '',\n last_name: from.last_name || '',\n username: from.username || '',\n marker_label: `${from.first_name || ''} ${from.last_name || ''}`.trim(),\n display_time: displayTime,\n chat_id: item.json.message.chat.id,\n battery: null,\n speed: null\n }\n });\n}\n\nreturn results;"
},
"id": "648a42c5-99ce-4c7b-8a85-380cd40050d0",
"name": "Location verarbeiten",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
528,
112
]
},
{
"parameters": {
"authentication": "nocoDbApiToken",
"operation": "create",
"projectId": "pdxl4cx4dbu9nxi",
"table": "m8pqj5ixgnnrzkg",
"fieldsUi": {
"fieldValues": [
{
"fieldName": "latitude",
"fieldValue": "={{ $json.latitude }}"
},
{
"fieldName": "longitude",
"fieldValue": "={{ $json.longitude }}"
},
{
"fieldName": "timestamp",
"fieldValue": "={{ $json.timestamp }}"
},
{
"fieldName": "user_id",
"fieldValue": "={{ $json.user_id }}"
},
{
"fieldName": "first_name",
"fieldValue": "={{ $json.first_name }}"
},
{
"fieldName": "last_name",
"fieldValue": "={{ $json.last_name }}"
},
{
"fieldName": "username",
"fieldValue": "={{ $json.username }}"
},
{
"fieldName": "marker_label",
"fieldValue": "={{ $json.marker_label }}"
},
{
"fieldName": "display_time",
"fieldValue": "={{ $json.display_time }}"
},
{
"fieldName": "chat_id",
"fieldValue": "={{ $json.chat_id }}"
},
{
"fieldName": "battery",
"fieldValue": "={{ $json.battery }}"
},
{
"fieldName": "speed",
"fieldValue": "={{ $json.speed }}"
}
]
}
},
"id": "0bd51abc-74cb-4220-aa42-f7801c8c79f1",
"name": "Speichere in NocoDB",
"type": "n8n-nodes-base.nocoDb",
"typeVersion": 2,
"position": [
752,
112
],
"credentials": {
"nocoDbApiToken": {
"id": "6fNBtcghMe8wFoE5",
"name": "NocoDB Token account"
}
}
},
{
"parameters": {
"authentication": "nocoDbApiToken",
"operation": "getAll",
"projectId": "pdxl4cx4dbu9nxi",
"table": "m8pqj5ixgnnrzkg",
"options": {}
},
"id": "b87051cc-a53b-4c8e-8380-1e8d1d69e75b",
"name": "Hole letzten Eintrag",
"type": "n8n-nodes-base.nocoDb",
"typeVersion": 2,
"position": [
976,
112
],
"credentials": {
"nocoDbApiToken": {
"id": "6fNBtcghMe8wFoE5",
"name": "NocoDB Token account"
}
}
},
{
"parameters": {
"authentication": "nocoDbApiToken",
"operation": "getAll",
"projectId": "pdxl4cx4dbu9nxi",
"table": "m8pqj5ixgnnrzkg",
"returnAll": true,
"options": {}
},
"id": "78a7fd6d-640d-4659-b0c7-11fefc827eaf",
"name": "Zähle Einträge",
"type": "n8n-nodes-base.nocoDb",
"typeVersion": 2,
"position": [
976,
240
],
"credentials": {
"nocoDbApiToken": {
"id": "6fNBtcghMe8wFoE5",
"name": "NocoDB Token account"
}
}
},
{
"parameters": {
"jsCode": "// Kombiniere Daten für Telegram-Bestätigung\nconst lastEntry = $('Hole letzten Eintrag').all();\nconst allEntries = $('Zähle Einträge').all();\nconst originalData = $('Location verarbeiten').first().json;\n\nreturn [{\n json: {\n latitude: originalData.latitude,\n longitude: originalData.longitude,\n display_time: originalData.display_time,\n chat_id: originalData.chat_id,\n total: allEntries.length\n }\n}];"
},
"id": "25429eb3-aa61-428d-9ec2-2f365e9669eb",
"name": "Bereite Bestätigung vor",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1328,
128
]
},
{
"parameters": {
"chatId": "={{ $json.chat_id }}",
"text": "=✅ Standort gespeichert!\n\n📍 Koordinaten:\nLat: {{ $json.latitude }}\nLon: {{ $json.longitude }}\n\n🕐 Zeit: {{ $json.display_time }}\n\n🗺 Karte:\nhttps://web.unixweb.home64.de/tracker/index.html\n\n📊 Gespeicherte Punkte: {{ $json.total }}",
"additionalFields": {}
},
"id": "8a3be707-feb5-42c2-a551-5ebc4aa123f9",
"name": "Telegram Bestätigung",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [
1520,
128
],
"webhookId": "9df3ad7d-315d-4782-b4ec-4fb154c0b46d",
"credentials": {
"telegramApi": {
"id": "dRHgVQKqowQHIait",
"name": "Telegram account n8n-munich-bot"
}
}
},
{
"parameters": {
"path": "location",
"responseMode": "lastNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "e8d5b4b8-0495-4599-be54-a21ff208f26b",
"name": "Webhook - Location API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
80,
416
],
"webhookId": "location-api-endpoint"
},
{
"parameters": {
"authentication": "nocoDbApiToken",
"operation": "getAll",
"projectId": "pdxl4cx4dbu9nxi",
"table": "m8pqj5ixgnnrzkg",
"returnAll": true,
"options": {}
},
"id": "61d1ee53-9536-419b-ab5a-aff4281b08ac",
"name": "Lade Daten aus NocoDB",
"type": "n8n-nodes-base.nocoDb",
"typeVersion": 2,
"position": [
304,
416
],
"credentials": {
"nocoDbApiToken": {
"id": "6fNBtcghMe8wFoE5",
"name": "NocoDB Token account"
}
}
},
{
"parameters": {
"jsCode": "// Baue API Response aus NocoDB-Daten\nconst locations = $input.all().map(item => item.json);\n\n// Sortiere nach timestamp (neueste zuerst)\nlocations.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));\n\nconst current = locations.length > 0 ? locations[0] : null;\n\nreturn [{\n json: {\n success: true,\n current: current,\n history: locations,\n total_points: locations.length,\n last_updated: current ? current.timestamp : null\n }\n}];"
},
"id": "6e2d1c8b-1c23-44d4-9faf-2ab85a200e18",
"name": "Format API Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
528,
416
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "81534ffa-a120-437c-a2cd-5b68991d95fe",
"name": "JSON Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
752,
416
]
},
{
"parameters": {
"height": 544,
"width": 1632,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"typeVersion": 1,
"id": "4fe64055-f0c3-4377-a4f5-be668e6856ef",
"name": "Sticky Note"
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
1168,
128
],
"id": "c7c605e5-9033-4542-a5b5-7847080de63b",
"name": "Merge"
}
],
"pinData": {},
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Hat Location?",
"type": "main",
"index": 0
}
]
]
},
"Hat Location?": {
"main": [
[
{
"node": "Location verarbeiten",
"type": "main",
"index": 0
}
]
]
},
"Location verarbeiten": {
"main": [
[
{
"node": "Speichere in NocoDB",
"type": "main",
"index": 0
}
]
]
},
"Speichere in NocoDB": {
"main": [
[
{
"node": "Hole letzten Eintrag",
"type": "main",
"index": 0
},
{
"node": "Zähle Einträge",
"type": "main",
"index": 0
}
]
]
},
"Hole letzten Eintrag": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Zähle Einträge": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Bereite Bestätigung vor": {
"main": [
[
{
"node": "Telegram Bestätigung",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Location API": {
"main": [
[
{
"node": "Lade Daten aus NocoDB",
"type": "main",
"index": 0
}
]
]
},
"Lade Daten aus NocoDB": {
"main": [
[
{
"node": "Format API Response",
"type": "main",
"index": 0
}
]
]
},
"Format API Response": {
"main": [
[
{
"node": "JSON Response",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Bereite Bestätigung vor",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "2fe5806f-3c70-4680-b7eb-66f72a413ea9",
"meta": {
"instanceId": "12d864c68e4fb5dfd100dc0c683b95f43cd55af7e9efa82e25407fac5a3824a5"
},
"id": "8fjExqEGYvh8XWVu",
"tags": []
}

View File

@@ -1,185 +0,0 @@
{
"name": "MQTT Location Tracker - NocoDB",
"nodes": [
{
"parameters": {
"topics": "owntracks/#",
"options": {}
},
"id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"name": "MQTT Trigger",
"type": "n8n-nodes-base.mqttTrigger",
"typeVersion": 1,
"position": [
80,
128
],
"credentials": {
"mqtt": {
"id": "MQTT_CREDENTIAL_ID",
"name": "MQTT Broker"
}
}
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json._type }}",
"value2": "location"
}
]
}
},
"id": "b2c3d4e5-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
"name": "Ist Location?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
304,
128
]
},
{
"parameters": {
"jsCode": "// Extrahiere MQTT Location-Daten für NocoDB\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const data = item.json;\n \n // Validiere erforderliche Felder\n if (!data.lat || !data.lon || !data.tst) {\n continue; // Überspringe ungültige Einträge\n }\n \n // Timestamp (tst = Unix timestamp in seconds)\n // Verwende tst oder fallback auf aktuelle Zeit\n const timestampMs = (data.tst && !isNaN(data.tst)) ? data.tst * 1000 : Date.now();\n const timestamp = new Date(timestampMs).toISOString();\n const displayTime = new Date(timestampMs).toLocaleString('de-DE');\n \n // Erstelle marker_label aus tid (tracker ID)\n const trackerLabel = data.tid || 'Unknown';\n \n // Optional: Verwende SSID als zusätzliche Info wenn vorhanden\n const markerLabel = data.SSID ? `${trackerLabel} (${data.SSID})` : trackerLabel;\n \n results.push({\n json: {\n latitude: data.lat,\n longitude: data.lon,\n timestamp: timestamp,\n user_id: 0, // MQTT hat keine User-ID, default auf 0\n first_name: trackerLabel,\n last_name: data.source || 'mqtt', // z.B. \"fused\"\n username: data.tid || '',\n marker_label: markerLabel,\n display_time: displayTime,\n chat_id: 0, // MQTT hat keine Chat-ID, default auf 0\n // Optional: Zusätzliche MQTT-Daten als Referenz\n mqtt_data: {\n accuracy: data.acc,\n altitude: data.alt,\n battery: data.batt,\n velocity: data.vel,\n course: data.cog,\n connection: data.conn,\n bssid: data.BSSID\n }\n }\n });\n}\n\nreturn results;"
},
"id": "c3d4e5f6-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"name": "MQTT Location verarbeiten",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
528,
112
]
},
{
"parameters": {
"authentication": "nocoDbApiToken",
"operation": "create",
"projectId": "pdxl4cx4dbu9nxi",
"table": "m8pqj5ixgnnrzkg",
"fieldsUi": {
"fieldValues": [
{
"fieldName": "latitude",
"fieldValue": "={{ $json.latitude }}"
},
{
"fieldName": "longitude",
"fieldValue": "={{ $json.longitude }}"
},
{
"fieldName": "timestamp",
"fieldValue": "={{ $json.timestamp }}"
},
{
"fieldName": "user_id",
"fieldValue": "={{ $json.user_id }}"
},
{
"fieldName": "first_name",
"fieldValue": "={{ $json.first_name }}"
},
{
"fieldName": "last_name",
"fieldValue": "={{ $json.last_name }}"
},
{
"fieldName": "username",
"fieldValue": "={{ $json.username }}"
},
{
"fieldName": "marker_label",
"fieldValue": "={{ $json.marker_label }}"
},
{
"fieldName": "display_time",
"fieldValue": "={{ $json.display_time }}"
},
{
"fieldName": "chat_id",
"fieldValue": "={{ $json.chat_id }}"
}
]
}
},
"id": "d4e5f6a7-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
"name": "Speichere in NocoDB",
"type": "n8n-nodes-base.nocoDb",
"typeVersion": 2,
"position": [
752,
112
],
"credentials": {
"nocoDbApiToken": {
"id": "6fNBtcghMe8wFoE5",
"name": "NocoDB Token account"
}
}
},
{
"parameters": {
"height": 320,
"width": 896,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"typeVersion": 1,
"id": "e5f6a7b8-9c0d-1e2f-3a4b-5c6d7e8f9a0b",
"name": "Sticky Note"
}
],
"pinData": {},
"connections": {
"MQTT Trigger": {
"main": [
[
{
"node": "Ist Location?",
"type": "main",
"index": 0
}
]
]
},
"Ist Location?": {
"main": [
[
{
"node": "MQTT Location verarbeiten",
"type": "main",
"index": 0
}
]
]
},
"MQTT Location verarbeiten": {
"main": [
[
{
"node": "Speichere in NocoDB",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "mqtt-location-tracker-v1",
"meta": {
"instanceId": "12d864c68e4fb5dfd100dc0c683b95f43cd55af7e9efa82e25407fac5a3824a5"
},
"id": "MqttLocationTracker01",
"tags": []
}

View File

@@ -1,324 +0,0 @@
{
"name": "Telegram Location Tracker - Native (No DB)",
"nodes": [
{
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"id": "b7769320-830c-4a76-9086-ea4067969ad7",
"name": "Telegram Trigger",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [
0,
16
],
"webhookId": "telegram-location-webhook",
"credentials": {
"telegramApi": {
"id": "dRHgVQKqowQHIait",
"name": "Telegram account n8n-munich-bot"
}
}
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.message.location }}",
"operation": "isNotEmpty"
}
]
}
},
"id": "57f03b50-3184-4d1e-9d33-f628f93f466b",
"name": "Hat Location?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
224,
16
]
},
{
"parameters": {
"jsCode": "// Extrahiere Location-Daten\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const location = item.json.message.location;\n const from = item.json.message.from;\n const messageDate = item.json.message.date;\n \n const timestamp = new Date(messageDate * 1000).toISOString();\n const displayTime = new Date(messageDate * 1000).toLocaleString('de-DE');\n \n results.push({\n json: {\n latitude: location.latitude,\n longitude: location.longitude,\n timestamp: timestamp,\n user_id: from.id,\n first_name: from.first_name || '',\n last_name: from.last_name || '',\n username: from.username || '',\n marker_label: `${from.first_name || ''} ${from.last_name || ''}`.trim(),\n display_time: displayTime,\n chat_id: item.json.message.chat.id\n }\n });\n}\n\nreturn results;"
},
"id": "dafcade6-0563-47b9-bd0d-dff0f61d1dbb",
"name": "Location verarbeiten",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
0
]
},
{
"parameters": {
"command": "cat /tmp/n8n-locations.json 2>/dev/null || echo '[]'"
},
"id": "d3b9945b-4d17-40c0-ae30-3d45801bae1e",
"name": "Lade existierende Daten",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
672,
0
]
},
{
"parameters": {
"jsCode": "// Neue Location zu File hinzufügen\nconst newLocation = $('Location verarbeiten').first().json;\n\n// Lade bestehende Locations\nlet locations = [];\ntry {\n const fileContent = $input.first().json.stdout;\n locations = JSON.parse(fileContent);\n} catch (e) {\n locations = [];\n}\n\n// Füge neue Location hinzu\nlocations.unshift(newLocation); // Am Anfang einfügen (neueste zuerst)\n\n// Behalte nur die letzten 100 Einträge\nif (locations.length > 100) {\n locations = locations.slice(0, 100);\n}\n\nreturn [{\n json: {\n locations: locations,\n json_string: JSON.stringify(locations, null, 2),\n current: newLocation,\n total: locations.length\n }\n}];"
},
"id": "4fadae07-460f-4c29-800b-1fdbffe25991",
"name": "Merge mit History",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
0
]
},
{
"parameters": {
"command": "=echo '{{ $json.json_string.replace(/'/g, \"'\\\\'''\") }}' > /tmp/n8n-locations.json"
},
"id": "c519118b-0626-4ace-befc-ee9859d73504",
"name": "Speichere in File",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
1104,
0
]
},
{
"parameters": {
"chatId": "={{ $('Location verarbeiten').first().json.chat_id }}",
"text": "=✅ Standort gespeichert!\n\n📍 Koordinaten:\nLat: {{ $('Location verarbeiten').first().json.latitude }}\nLon: {{ $('Location verarbeiten').first().json.longitude }}\n\n🕐 Zeit: {{ $('Location verarbeiten').first().json.display_time }}\n\n🗺 Karte:\nhttps://web.unixweb.home64.de/tracker/index.html\n\n📊 Gespeicherte Punkte: {{ $json.total }}",
"additionalFields": {}
},
"id": "aa023c84-7664-4641-b3fc-cd30b9f69941",
"name": "Telegram Bestätigung",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [
1328,
0
],
"webhookId": "9df3ad7d-315d-4782-b4ec-4fb154c0b46d",
"credentials": {
"telegramApi": {
"id": "dRHgVQKqowQHIait",
"name": "Telegram account n8n-munich-bot"
}
}
},
{
"parameters": {
"path": "location",
"responseMode": "lastNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "b47e8370-3276-4f49-a2c5-25ef07b83ce8",
"name": "Webhook - Location API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
0,
208
],
"webhookId": "location-api-endpoint"
},
{
"parameters": {
"command": "cat /tmp/n8n-locations.json 2>/dev/null || echo '[]'"
},
"id": "200dbfff-2932-4e92-9e47-181ff706c0de",
"name": "Lade Daten für API",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
224,
208
]
},
{
"parameters": {
"jsCode": "// Parse locations und baue API Response\nlet locations = [];\ntry {\n const fileContent = $input.first().json.stdout;\n locations = JSON.parse(fileContent);\n} catch (e) {\n locations = [];\n}\n\nconst current = locations.length > 0 ? locations[0] : null;\n\nreturn [{\n json: {\n success: true,\n current: current,\n history: locations,\n total_points: locations.length,\n last_updated: current ? current.timestamp : null\n }\n}];"
},
"id": "7c56b850-f1dd-4c51-ac8d-4313e199e6e7",
"name": "Format API Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
208
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "9b720b0b-b4f8-4599-9e2f-d6e8f67819ab",
"name": "JSON Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
672,
208
]
},
{
"parameters": {
"height": 544,
"width": 1632,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-80,
-112
],
"typeVersion": 1,
"id": "29506fa4-6cb3-403f-bd64-6a2f49a2eb85",
"name": "Sticky Note"
}
],
"pinData": {},
"connections": {
"Telegram Trigger": {
"main": [
[
{
"node": "Hat Location?",
"type": "main",
"index": 0
}
]
]
},
"Hat Location?": {
"main": [
[
{
"node": "Location verarbeiten",
"type": "main",
"index": 0
}
]
]
},
"Location verarbeiten": {
"main": [
[
{
"node": "Lade existierende Daten",
"type": "main",
"index": 0
}
]
]
},
"Lade existierende Daten": {
"main": [
[
{
"node": "Merge mit History",
"type": "main",
"index": 0
}
]
]
},
"Merge mit History": {
"main": [
[
{
"node": "Speichere in File",
"type": "main",
"index": 0
}
]
]
},
"Speichere in File": {
"main": [
[
{
"node": "Telegram Bestätigung",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Location API": {
"main": [
[
{
"node": "Lade Daten für API",
"type": "main",
"index": 0
}
]
]
},
"Lade Daten für API": {
"main": [
[
{
"node": "Format API Response",
"type": "main",
"index": 0
}
]
]
},
"Format API Response": {
"main": [
[
{
"node": "JSON Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false,
"errorWorkflow": "PhwIkaqyXRasTXDH"
},
"versionId": "b87a412a-4833-4f0d-aa33-0ba9074238d9",
"meta": {
"instanceId": "12d864c68e4fb5dfd100dc0c683b95f43cd55af7e9efa82e25407fac5a3824a5"
},
"id": "5dUTiJE61r7opoul",
"tags": []
}