# Wegwichtel REST-API Diese Datei dokumentiert die vollständige HTTP-Schnittstelle des Wegwichtel-Servers. Die API-Basis lautet `/api`. Bild- und Audiodateien werden ebenfalls ausschließlich über API-Endpunkte ausgeliefert; die Clientanwendung verwendet dafür keine direkten Speicherpfade. ## 1. Grundlagen ### Basisadressen Lokaler Standard: ```text http://127.0.0.1:47145 ``` API-Basis: ```text http://127.0.0.1:47145/api ``` Bei vorgeschaltetem Nginx bleibt der Pfad erhalten, zum Beispiel: ```text https://wegwichtel.example.org/api ``` ### Formate - JSON-Lesezugriffe liefern `application/json`. - Bild- und Audio-GETs liefern den jeweiligen binären Medientyp. - Schreibzugriffe ohne Datei akzeptieren JSON, URL-encoded Formulare oder `multipart/form-data`. - Datei-Uploads verwenden `multipart/form-data`. - Pro Medienrequest wird genau eine Datei verarbeitet. - Zeitstempel werden als SQLite- beziehungsweise ISO-8601-Text ausgegeben. - Die derzeitige API besitzt noch keine Authentifizierung. Schreibzugriffe dürfen nicht ungeschützt öffentlich erreichbar sein. ### Uploadgrenzen und unterstützte Dateitypen Das Dateilimit pro Upload wird mit `MAX_UPLOAD_MB` festgelegt und beträgt standardmäßig `50 MB`. | Ressource | Uploadfeld | Dateitypen | |---|---|---| | GPX | `gpx` | `.gpx`, `application/gpx+xml`, XML | | Bild | `picture` | JPEG, PNG, WebP | | Audio | `audio` | MP3, MP4/M4A, AAC, Ogg, WAV, WebM | ### Fehlerformat ```json { "error": "HttpError", "message": "Strecke nicht gefunden." } ``` Typische Statuscodes: | Status | Bedeutung | |---:|---| | `200` | Anfrage erfolgreich | | `201` | Ressource wurde angelegt | | `400` | Parameter oder Datei fehlt beziehungsweise ist ungültig | | `404` | Route, POI oder Medienressource wurde nicht gefunden | | `409` | Ressource existiert bereits oder widerspricht dem aktuellen Zustand | | `413` | Datei überschreitet `MAX_UPLOAD_MB` | | `415` | Dateityp wird nicht unterstützt | | `500` | Interner Serverfehler | ### IDs und Medienadressierung - `:id` bezeichnet bei `/routes/:id/...` die Route. - `:pictureId` bezeichnet ein einzelnes Bild aus `poi_images`. - `poiId` bezeichnet einen POI innerhalb einer Route. - Ein POI kann mehrere Bilder, aber höchstens eine Audiodatei besitzen. - Bilder erhalten deshalb eine eigene Pfad-ID. - Audio verwendet keinen zusätzlichen Pfadabschnitt; der POI wird über `poiId` angegeben. Beispiele verwenden Route `1`, POI `7` und Bild `15`. ## 2. Datenmodelle ### Route ```json { "id": 1, "slug": "schulwald-runde", "name": "Schulwald-Runde", "description": "Naturkundlicher Rundweg", "schoolName": "Beispielschule", "status": "active", "start": { "lat": 52.5208, "lon": 13.407 }, "center": { "lat": 52.521, "lon": 13.408 }, "bounds": { "minLat": 52.5208, "minLon": 13.407, "maxLat": 52.5212, "maxLon": 13.409 }, "distanceM": 842.6, "elevationGainM": 14.2, "pointCount": 87, "gpxUrl": "/media/routes/1/route.gpx", "createdAt": "2026-06-16 12:00:00", "updatedAt": "2026-06-16 12:00:00", "deletedAt": null } ``` ### POI ```json { "id": 7, "routeId": 1, "title": "Die alte Eiche", "description": "Hier wird das Alter der Eiche erklärt.", "lat": 52.5208, "lon": 13.407, "triggerRadiusM": 60, "sequence": 2, "audioUrl": "/api/routes/1/audio?poiId=7", "images": [ { "id": 15, "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pictures/15" } ] } ``` ### Bildmetadaten ```json { "id": 15, "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pictures/15", "createdAt": "2026-06-16 12:30:00" } ``` ### Audiometadaten ```json { "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "url": "/api/routes/1/audio?poiId=7", "updatedAt": "2026-06-16 12:35:00" } ``` ## 3. Endpunktübersicht | Methode | Pfad | Zweck | |---|---|---| | `GET` | `/api/health` | Server- und SQLite-Zustand | | `GET` | `/api/routes` | Routen auflisten | | `GET` | `/api/routes/:id` | Route mit Punkten und POIs lesen | | `POST` | `/api/routes` | Route anlegen | | `PUT` | `/api/routes/:id` | Route aktualisieren | | `POST` | `/api/routes/:id/append` | GPX-Punkte anhängen | | `DELETE` | `/api/routes/:id` | Route weich löschen | | `POST` | `/api/routes/:id/restore` | Route wiederherstellen | | `GET` | `/api/routes/:id/pois` | POIs einer Route auflisten | | `GET` | `/api/pois/:id` | einzelnen POI lesen | | `POST` | `/api/routes/:id/pois` | POI anlegen | | `PUT` | `/api/pois/:id` | POI aktualisieren | | `GET` | `/api/routes/:id/pictures` | Bildmetadaten auflisten | | `GET` | `/api/routes/:id/pictures/:pictureId` | Bilddatei ausliefern | | `POST` | `/api/routes/:id/pictures` | Bild anlegen | | `PUT` | `/api/routes/:id/pictures/:pictureId` | Bilddatei oder Metadaten aktualisieren | | `DELETE` | `/api/routes/:id/pictures/:pictureId` | Bild löschen | | `GET` | `/api/routes/:id/audio` | Audio auflisten oder für einen POI ausliefern | | `POST` | `/api/routes/:id/audio` | Audio für einen POI anlegen | | `PUT` | `/api/routes/:id/audio` | Audio eines POIs ersetzen | | `DELETE` | `/api/routes/:id/audio` | Audio eines POIs löschen | ## 4. Systemzustand ### GET /api/health Parameter: keine. ```bash curl http://127.0.0.1:47145/api/health ``` Beispielantwort: ```json { "ok": true, "service": "wegwichtel", "socket": "127.0.0.1:47145", "sqliteVersion": "3.46.1", "timestamp": "2026-06-16T12:00:00.000Z" } ``` ## 5. Routen ### GET /api/routes Listet aktive Routen auf. Werden `lat` und `lon` gemeinsam angegeben, berechnet der Server die Entfernung zum Routenstart, filtert mit `radiusKm` und sortiert nach Entfernung. Query-Parameter: | Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---|---| | `lat` | Dezimalzahl | nein | – | Breitengrad des Ausgangspunkts | | `lon` | Dezimalzahl | nein | – | Längengrad des Ausgangspunkts | | `radiusKm` | Dezimalzahl | nein | `DEFAULT_ROUTE_RADIUS_KM` | maximaler Abstand zum Routenstart | | `includeDeleted` | `true`/`false` | nein | `false` | gelöschte Routen einschließen | Alle aktiven Routen: ```bash curl http://127.0.0.1:47145/api/routes ``` Mit allen Query-Parametern: ```bash curl --get http://127.0.0.1:47145/api/routes \ --data-urlencode 'lat=52.5208' \ --data-urlencode 'lon=13.4070' \ --data-urlencode 'radiusKm=12.5' \ --data-urlencode 'includeDeleted=true' ``` ### GET /api/routes/:id Pfadparameter: | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `id` | positive Ganzzahl | ja | Route | Query-Parameter: | Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---|---| | `includeDeleted` | `true`/`false` | nein | `false` | auch eine weich gelöschte Route lesen | ```bash curl http://127.0.0.1:47145/api/routes/1 ``` Die Antwort enthält `points`, `pois`, `images[].url` und `audioUrl`. Die Medien-URLs zeigen auf die API-Endpunkte. ### POST /api/routes Content-Type: `multipart/form-data`. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `gpx` | Datei | ja | GPX-Datei | | `name` | Text | nein, wenn GPX einen Namen enthält | Anzeigename | | `slug` | Text | nein | gewünschter URL-tauglicher Bezeichner | | `description` | Text | nein | Beschreibung | | `schoolName` | Text | nein | Schule oder Projekt | ```bash curl -X POST http://127.0.0.1:47145/api/routes \ -F 'name=Schulwald-Runde' \ -F 'slug=schulwald-runde' \ -F 'description=Naturkundlicher Rundweg' \ -F 'schoolName=Beispielschule' \ -F 'gpx=@route.gpx;type=application/gpx+xml' ``` ### PUT /api/routes/:id Aktualisiert Routendaten. Eine neue GPX-Datei ersetzt die gespeicherten Trackpunkte. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `name` | Text | nein | neuer Name | | `description` | Text | nein | neue Beschreibung | | `schoolName` | Text | nein | neue Schule | | `gpx` | Datei | nein | vollständige Ersatzstrecke | ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1 \ -F 'name=Schulwald-Runde 2026' \ -F 'description=Überarbeitete Route' \ -F 'schoolName=Beispielschule' \ -F 'gpx=@route-neu.gpx;type=application/gpx+xml' ``` ### POST /api/routes/:id/append Hängt GPX-Punkte an eine Route an. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `gpx` | Datei | ja | GPX-Datei mit zusätzlichen Punkten | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/append \ -F 'gpx=@erweiterung.gpx;type=application/gpx+xml' ``` ### DELETE /api/routes/:id Markiert die Route als gelöscht und verschiebt GPX, Bilder und Audio in den Papierkorb. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1 ``` ### POST /api/routes/:id/restore Stellt eine weich gelöschte Route einschließlich ihrer Dateien wieder her. ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/restore ``` ## 6. POIs ### GET /api/routes/:id/pois ```bash curl http://127.0.0.1:47145/api/routes/1/pois ``` ### GET /api/pois/:id ```bash curl http://127.0.0.1:47145/api/pois/7 ``` ### POST /api/routes/:id/pois Akzeptiert JSON, URL-encoded Formulare oder Multipart ohne Mediendateien. | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---|---| | `title` | Text | nein | `Unbenannter POI` | Titel | | `description` | Text | nein | leer | Beschreibung | | `lat` | Dezimalzahl | ja | – | Breitengrad | | `lon` | Dezimalzahl | ja | – | Längengrad | | `triggerRadiusM` | Dezimalzahl | nein | `DEFAULT_POI_TRIGGER_METERS` | Aktivierungsradius in Metern | | `sequence` | Ganzzahl ≥ 0 | nein | `0` | Reihenfolge | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/pois \ -H 'Content-Type: application/json' \ -d '{ "title": "Die alte Eiche", "description": "Informationen zur Baumart", "lat": 52.5208, "lon": 13.4070, "triggerRadiusM": 60, "sequence": 2 }' ``` ### PUT /api/pois/:id Alle Felder sind optional; nicht angegebene Werte bleiben erhalten. ```bash curl -X PUT http://127.0.0.1:47145/api/pois/7 \ -H 'Content-Type: application/json' \ -d '{ "title": "Die sehr alte Eiche", "description": "Aktualisierte Beschreibung", "lat": 52.5209, "lon": 13.4071, "triggerRadiusM": 45, "sequence": 1 }' ``` ## 7. Bilder Ein POI kann mehrere Bilder besitzen. Die Collection wird über die Route adressiert; jedes Bild hat zusätzlich eine `pictureId`. ### GET /api/routes/:id/pictures Liefert Bildmetadaten als JSON. Query-Parameter: | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `poiId` | positive Ganzzahl | nein | auf Bilder eines POIs einschränken | Alle Bilder der Route: ```bash curl http://127.0.0.1:47145/api/routes/1/pictures ``` Nur Bilder von POI 7: ```bash curl --get http://127.0.0.1:47145/api/routes/1/pictures \ --data-urlencode 'poiId=7' ``` Beispielantwort: ```json { "pictures": [ { "id": 15, "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pictures/15", "createdAt": "2026-06-16 12:30:00" } ] } ``` ### GET /api/routes/:id/pictures/:pictureId Liefert die Bilddatei binär mit dem passenden `Content-Type`, beispielsweise `image/jpeg`, `image/png` oder `image/webp`. ```bash curl http://127.0.0.1:47145/api/routes/1/pictures/15 \ --output bild-15.jpg ``` Bildmetadaten werden über `GET /api/routes/:id/pictures` gelesen. ### POST /api/routes/:id/pictures Content-Type: `multipart/form-data`. | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---|---| | `picture` | Datei | ja | – | Bilddatei | | `poiId` | positive Ganzzahl | ja | – | zugehöriger POI | | `caption` | Text | nein | leer | sichtbare Bildbeschreibung und Alternativtext | | `sequence` | Ganzzahl ≥ 0 | nein | nächste freie Position | Reihenfolge in der Diashow | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/pictures \ -F 'poiId=7' \ -F 'caption=Blick auf die Baumkrone' \ -F 'sequence=0' \ -F 'picture=@eiche.jpg;type=image/jpeg' ``` Die Antwort enthält Metadaten. Der `Location`-Header zeigt auf `/api/routes/1/pictures/:pictureId`. ### PUT /api/routes/:id/pictures/:pictureId Aktualisiert Metadaten und optional die Datei. Der Request kann JSON ohne Datei oder Multipart mit Datei verwenden. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `picture` | Datei | nein | Ersatzdatei | | `poiId` | positive Ganzzahl | nein | Bild einem anderen POI derselben Route zuordnen | | `caption` | Text | nein | Bildbeschreibung | | `sequence` | Ganzzahl ≥ 0 | nein | Reihenfolge | Nur Metadaten ändern: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pictures/15 \ -H 'Content-Type: application/json' \ -d '{ "poiId": 7, "caption": "Nahaufnahme der Eichenblätter", "sequence": 1 }' ``` Datei und Metadaten ersetzen: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pictures/15 \ -F 'poiId=7' \ -F 'caption=Neue Bildbeschreibung' \ -F 'sequence=1' \ -F 'picture=@eiche-neu.webp;type=image/webp' ``` ### DELETE /api/routes/:id/pictures/:pictureId Löscht den Datenbankeintrag und die einzelne Bilddatei. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/pictures/15 ``` ## 8. Audio Pro POI existiert höchstens eine Audiodatei. Deshalb gibt es keine zusätzliche Audio-ID und keinen Pfad wie `/audio/:poiId`. Alle Operationen verwenden `/api/routes/:id/audio`; der konkrete POI wird mit `poiId` angegeben. ### GET /api/routes/:id/audio Ohne `poiId` liefert der Endpunkt alle Audiometadaten der Route als JSON: ```bash curl http://127.0.0.1:47145/api/routes/1/audio ``` Beispielantwort: ```json { "audio": [ { "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "url": "/api/routes/1/audio?poiId=7", "updatedAt": "2026-06-16 12:35:00" } ] } ``` Mit `poiId` liefert derselbe Endpunkt die Audiodatei binär: | Query-Parameter | Typ | Pflicht für Dateiausgabe | Beschreibung | |---|---|---:|---| | `poiId` | positive Ganzzahl | ja | POI, dessen Audio geliefert wird | ```bash curl --get http://127.0.0.1:47145/api/routes/1/audio \ --data-urlencode 'poiId=7' \ --output ansage.mp3 ``` Der Response-`Content-Type` entspricht dem gespeicherten Format, beispielsweise `audio/mpeg` oder `audio/ogg`. ### POST /api/routes/:id/audio Legt die Audiodatei für einen POI an. Besteht bereits eine Datei, antwortet der Server mit `409`; zum Ersetzen wird `PUT` verwendet. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `audio` | Datei | ja | Audiodatei | | `poiId` | positive Ganzzahl | ja | zugehöriger POI | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/audio \ -F 'poiId=7' \ -F 'audio=@ansage.mp3;type=audio/mpeg' ``` Der `Location`-Header lautet beispielsweise: ```text /api/routes/1/audio?poiId=7 ``` ### PUT /api/routes/:id/audio Ersetzt eine vorhandene Audiodatei. `poiId` darf als Query-Parameter oder als Multipart-Feld angegeben werden. Query-Variante: ```bash curl -X PUT 'http://127.0.0.1:47145/api/routes/1/audio?poiId=7' \ -F 'audio=@ansage-neu.ogg;type=audio/ogg' ``` Multipart-Variante: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/audio \ -F 'poiId=7' \ -F 'audio=@ansage-neu.ogg;type=audio/ogg' ``` | Parameter | Ort | Typ | Pflicht | Beschreibung | |---|---|---|---:|---| | `poiId` | Query oder Formular | positive Ganzzahl | ja | POI | | `audio` | Formular | Datei | ja | Ersatzdatei | ### DELETE /api/routes/:id/audio Löscht die Audiodatei des angegebenen POIs und setzt `audioUrl` anschließend auf `null`. Empfohlen als Query-Parameter: ```bash curl -X DELETE 'http://127.0.0.1:47145/api/routes/1/audio?poiId=7' ``` Alternativ als JSON-Body: ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/audio \ -H 'Content-Type: application/json' \ -d '{ "poiId": 7 }' ``` ## 9. Verwendung durch die Clientanwendung Die Clientanwendung konstruiert Medienadressen ausschließlich über das API-Modul: ```javascript Wegwichtel.Api.pictureUrl(routeId, pictureId); // /api/routes/1/pictures/15 Wegwichtel.Api.audioUrl(routeId, poiId); // /api/routes/1/audio?poiId=7 ``` Die Diashow setzt die API-Bildadresse als `src` des Bildes. Der HTML5-Audioplayer setzt die API-Audioadresse als `src` und lädt sie mit `preload="auto"`. Das Erreichen eines POIs startet keine automatische Wiedergabe. ## 10. Nginx-Hinweis Nginx sollte `/api/` vollständig an den Node.js-Prozess weiterleiten. Da Bilder und Audio über `/api` ausgeliefert werden, muss der Proxy binäre Responses unverändert durchreichen: ```nginx location ^~ /api/ { proxy_pass http://127.0.0.1:47145; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 120s; } ``` Für Uploads muss außerdem `client_max_body_size` mindestens so groß wie `MAX_UPLOAD_MB` gewählt werden.