Wegwichtel/docs/REST-API.md
2026-06-16 23:52:37 +02:00

641 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.