first commit

This commit is contained in:
2025-11-14 09:58:21 +00:00
commit bf10598861
4 changed files with 816 additions and 0 deletions

94
CLAUDE.md Normal file
View File

@@ -0,0 +1,94 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
This repository contains an n8n workflow configuration (`tracker.json`) that implements a Telegram-based location tracking system without a database. The workflow stores location data in a simple JSON file (`/tmp/n8n-locations.json`) on the n8n server.
## Workflow Architecture
The workflow has two main execution paths:
### 1. Location Capture Flow (Telegram Trigger → Storage)
- **Telegram Trigger**: Receives incoming Telegram messages
- **Hat Location?**: Filters messages containing location data
- **Location verarbeiten**: Extracts and formats location data (lat/lon, user info, timestamp)
- **Lade existierende Daten**: Reads existing locations from `/tmp/n8n-locations.json`
- **Merge mit History**: Combines new location with existing data (keeps last 100 entries)
- **Speichere in File**: Writes updated JSON back to `/tmp/n8n-locations.json`
- **Telegram Bestätigung**: Sends confirmation message with location details and map link
### 2. Location API Flow (Webhook → JSON Response)
- **Webhook - Location API**: Exposes `/location` endpoint with CORS enabled
- **Lade Daten für API**: Reads location data from file
- **Format API Response**: Formats data into structured JSON (current location, history, metadata)
- **JSON Response**: Returns JSON with CORS headers
## Key Technical Details
### Data Storage
- Storage location: `/tmp/n8n-locations.json`
- Format: JSON array of location objects
- Max retention: 100 most recent locations (oldest automatically removed)
- Data persistence: File-based (survives n8n restarts but may be lost on system restart due to `/tmp` location)
### Location Object Structure
```json
{
"latitude": number,
"longitude": number,
"timestamp": "ISO 8601 string",
"user_id": number,
"first_name": string,
"last_name": string,
"username": string,
"marker_label": string,
"display_time": "de-DE locale string",
"chat_id": number
}
```
### API Response Structure
```json
{
"success": true,
"current": <most recent location object>,
"history": [<array of all location objects>],
"total_points": number,
"last_updated": "ISO 8601 string"
}
```
### Web Interface
The workflow sends users a link to `https://web.unixweb.home64.de/tracker/index.html` for viewing locations on a map. This frontend is hosted separately and not included in this repository.
## Workflow Configuration
- **Telegram Credentials**: Uses "Telegram account n8n-munich-bot" (ID: dRHgVQKqowQHIait)
- **Webhook IDs**:
- Telegram trigger: `telegram-location-webhook`
- Location API: `location-api-endpoint`
- **Error Workflow**: ID `PhwIkaqyXRasTXDH` (configured but not included in this export)
- **Execution Order**: v1
- **Caller Policy**: workflowsFromSameOwner
## Modifying the Workflow
When editing this workflow:
1. The workflow is active by default - test changes carefully to avoid disrupting live tracking
2. JavaScript code nodes use n8n's code execution environment (not vanilla Node.js)
3. Shell commands execute in n8n's runtime environment - ensure `/tmp` is writable
4. CORS is configured for `*` (all origins) - restrict if needed for security
5. Date formatting uses `de-DE` locale - adjust if different locale needed
6. The 100-entry limit prevents unbounded growth - increase if more history is needed
## Deployment
To use this workflow:
1. Import `tracker.json` into n8n instance
2. Configure Telegram bot credentials
3. Ensure n8n has write permissions to `/tmp/n8n-locations.json`
4. Activate the workflow
5. Send a location via Telegram to test the capture flow
6. Access the webhook endpoint to verify API functionality

239
README.md Normal file
View File

@@ -0,0 +1,239 @@
# Telegram Location Tracker
Ein n8n-Workflow zur Verfolgung von Standorten über Telegram, ohne Datenbank-Anforderungen.
## Überblick
Dieser Workflow ermöglicht es Nutzern, ihre Standorte über einen Telegram-Bot zu teilen. Die Standortdaten werden in einer einfachen JSON-Datei gespeichert und können über eine API abgerufen werden, um sie auf einer Karte anzuzeigen.
## Funktionen
- **Standort-Erfassung**: Empfängt Standorte über Telegram und speichert sie automatisch
- **Historien-Verwaltung**: Behält die letzten 100 Standorte
- **API-Endpunkt**: Stellt Standortdaten per REST-API zur Verfügung
- **Web-Oberfläche**: Interaktive Karte mit Leaflet.js zur Visualisierung (index.html)
- **Bestätigungs-Nachrichten**: Sendet Bestätigungen mit Koordinaten und Kartenlink
- **Keine Datenbank**: Verwendet einfache dateibasierte Speicherung
## Voraussetzungen
- Eine laufende n8n-Instanz
- Ein Telegram-Bot mit gültigem API-Token
- Schreibrechte für `/tmp/n8n-locations.json` auf dem n8n-Server
## Installation
1. **Workflow importieren**:
- Öffne deine n8n-Instanz
- Navigiere zu "Workflows" → "Import from File"
- Wähle die `tracker.json` Datei aus
2. **Telegram-Bot konfigurieren**:
- Erstelle einen Bot über [@BotFather](https://t.me/botfather)
- Kopiere das API-Token
- In n8n: Gehe zu "Credentials" und füge die Telegram-API-Credentials hinzu
- Weise die Credentials dem "Telegram Trigger" und "Telegram Bestätigung" Node zu
3. **Workflow aktivieren**:
- Öffne den importierten Workflow
- Klicke auf "Active" um den Workflow zu aktivieren
4. **Testen**:
- Sende einen Standort an deinen Telegram-Bot
- Du solltest eine Bestätigungsnachricht mit den Koordinaten erhalten
## Verwendung
### Standort senden
1. Öffne den Chat mit deinem Telegram-Bot
2. Klicke auf das Büroklammer-Symbol (Anhang)
3. Wähle "Standort"
4. Sende deinen aktuellen Standort oder wähle einen auf der Karte
5. Der Bot bestätigt den empfangenen Standort mit Details
### Standorte abrufen
Der Workflow stellt einen API-Endpunkt zur Verfügung:
```
GET https://deine-n8n-instanz.de/webhook/location
```
**Beispiel-Antwort**:
```json
{
"success": true,
"current": {
"latitude": 48.1351,
"longitude": 11.5820,
"timestamp": "2025-11-14T10:30:00.000Z",
"user_id": 123456789,
"first_name": "Max",
"last_name": "Mustermann",
"username": "maxmuster",
"marker_label": "Max Mustermann",
"display_time": "14.11.2025, 11:30:00",
"chat_id": 123456789
},
"history": [...],
"total_points": 42,
"last_updated": "2025-11-14T10:30:00.000Z"
}
```
### Karten-Ansicht
Die Bestätigungsnachricht enthält einen Link zur Karten-Ansicht:
```
https://web.unixweb.home64.de/tracker/index.html
```
**Web-Oberfläche (index.html)**
Das Repository enthält eine vollständige Web-Oberfläche zur Visualisierung der Standortdaten:
**Features**:
- 📍 Interaktive Karte mit [Leaflet.js](https://leafletjs.com/)
- 🔄 Auto-Refresh alle 5 Sekunden (kann umgeschaltet werden)
- 📊 Aktuellster Standort als Marker mit Popup
- 📈 Standort-Historie als blaue Linie
- Status-Info mit Anzahl der Datenpunkte
- 🎯 Automatische Zentrierung auf aktuellen Standort
**Verwendung**:
1. Öffne die `index.html` in einem Browser
2. Die Karte lädt automatisch die neuesten Standorte
3. Klicke auf Marker für Details (Name, Zeitstempel)
4. Schalte Auto-Refresh nach Bedarf um
**Konfiguration**:
Passe die API-URL in `index.html` an deine n8n-Instanz an:
```javascript
// Zeile 85:
const API_URL = 'https://deine-n8n-instanz.de/webhook/location';
```
**Deployment**:
- Hoste die `index.html` auf einem Webserver (Apache, nginx, etc.)
- Oder öffne sie direkt als Datei im Browser (für lokale Tests)
- CORS muss in n8n aktiviert sein (ist standardmäßig der Fall)
## Workflow-Struktur
### Standort-Erfassung (Hauptfluss)
```
Telegram Trigger
Hat Location? (Filter)
Location verarbeiten (JS: Daten extrahieren)
Lade existierende Daten (Shell: cat JSON-Datei)
Merge mit History (JS: Neue Daten hinzufügen)
Speichere in File (Shell: JSON schreiben)
Telegram Bestätigung (Nachricht an User)
```
### API-Endpunkt
```
Webhook - Location API
Lade Daten für API (Shell: cat JSON-Datei)
Format API Response (JS: JSON formatieren)
JSON Response (CORS-Header + JSON zurückgeben)
```
## Datenspeicherung
- **Speicherort**: `/tmp/n8n-locations.json`
- **Format**: JSON-Array mit Standort-Objekten
- **Maximale Einträge**: 100 (älteste werden automatisch entfernt)
- **Persistenz**: Die Datei überlebt n8n-Neustarts, kann aber bei System-Neustarts verloren gehen (da in `/tmp`)
### Empfehlung für Produktion
Für produktiven Einsatz sollte der Speicherort von `/tmp/n8n-locations.json` zu einem persistenten Pfad geändert werden:
```javascript
// In den Nodes "Lade existierende Daten" und "Lade Daten für API":
cat /var/lib/n8n/locations.json 2>/dev/null || echo '[]'
// In dem Node "Speichere in File":
echo '...' > /var/lib/n8n/locations.json
```
## Anpassungen
### Anzahl gespeicherter Standorte ändern
Im Node "Merge mit History" die Zeile ändern:
```javascript
// Von 100 zu z.B. 500 ändern:
if (locations.length > 500) {
locations = locations.slice(0, 500);
}
```
### Datumsformat ändern
Im Node "Location verarbeiten" das Locale ändern:
```javascript
// Von 'de-DE' zu z.B. 'en-US' ändern:
const displayTime = new Date(messageDate * 1000).toLocaleString('en-US');
```
### CORS-Beschränkung
Im Node "Webhook - Location API" unter Options → Response Headers:
```javascript
// Aktuell: Alle Origins erlaubt
"Access-Control-Allow-Origin": "*"
// Besser für Produktion:
"Access-Control-Allow-Origin": "https://deine-domain.de"
```
## Sicherheitshinweise
- Der API-Endpunkt ist öffentlich zugänglich - implementiere ggf. Authentifizierung
- CORS ist für alle Origins geöffnet - beschränke dies in Produktion
- Die Telegram-Bot-Credentials sollten sicher verwahrt werden
- Standortdaten sind sensibel - beachte DSGVO-Anforderungen
## Fehlerbehebung
### "Standort gespeichert" wird nicht angezeigt
- Prüfe, ob der Workflow aktiv ist
- Prüfe die Telegram-Bot-Credentials
- Schau in die Workflow-Execution-Historie für Fehler
### API gibt leere Daten zurück
- Prüfe, ob die Datei `/tmp/n8n-locations.json` existiert
- Teste den Shell-Befehl: `cat /tmp/n8n-locations.json`
- Prüfe Dateiberechtigungen (n8n muss lesen können)
### Standorte gehen nach Neustart verloren
- Ändere den Speicherort von `/tmp/` zu einem persistenten Pfad
- Siehe "Empfehlung für Produktion" oben
## Lizenz
Dieses Projekt steht zur freien Verfügung.
## Support
Bei Fragen oder Problemen, erstelle bitte ein Issue in diesem Repository.

159
index.html Normal file
View File

@@ -0,0 +1,159 @@
<!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.eu/webhook/location';
// Auto-Refresh State
let autoRefreshEnabled = true;
let refreshInterval = null;
async function loadLocations() {
try {
const response = await fetch(API_URL);
const data = await response.json();
document.getElementById('status').innerHTML =
`Punkte: ${data.total_points || 0}<br>` +
`Status: ${data.success ? '✅ Verbunden' : '❌ Fehler'}`;
if (data.current) {
const loc = data.current;
L.marker([loc.latitude, loc.longitude])
.addTo(map)
.bindPopup(`${loc.marker_label}<br>${loc.display_time}`)
.openPopup();
map.setView([loc.latitude, loc.longitude], 15);
// Historie als Linie
if (data.history && data.history.length > 1) {
const coords = data.history.map(h => [h.latitude, h.longitude]);
L.polyline(coords, {color: 'blue', weight: 3}).addTo(map);
}
}
} catch (error) {
document.getElementById('status').innerHTML = '❌ Verbindungsfehler';
console.error(error);
}
}
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>

324
tracker.json Normal file
View File

@@ -0,0 +1,324 @@
{
"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": []
}