# Wegwichtel REST-API Diese Datei dokumentiert die vollständige HTTP-Schnittstelle des Wegwichtel-Servers. Sämtliche JSON-Ressourcen und aktiven GPX-, Bild- und Audiodateien werden unter `/api` bereitgestellt. Ein separates öffentliches `/media`-URL-Schema wird nicht verwendet. ## 1. Grundlagen ### Basisadressen Lokaler Standard: ```text http://127.0.0.1:47145 ``` API-Basis: ```text http://127.0.0.1:47145/api ``` Bei einer Nginx-Installation bleibt der Pfad gleich, beispielsweise: ```text https://wegwichtel.example.org/api ``` ### Formate - Listen- und Metadatenzugriffe liefern JSON. - Die Einzelendpunkte für GPX, Bilder und Audio liefern die jeweilige Datei direkt aus. Mit `?metadata=true` liefern die Bild- und Audioendpunkte stattdessen JSON-Metadaten. - Schreibzugriffe ohne Dateien akzeptieren `application/json`, `application/x-www-form-urlencoded` oder `multipart/form-data`. - GPX-Dateien werden ausschließlich als `multipart/form-data` übertragen. - Bild- und Audioendpunkte akzeptieren wahlweise `multipart/form-data` oder ein JSON-Objekt mit Base64-kodierten Dateidaten. - Der Server ermittelt den tatsächlichen MIME-Typ aus dem Dateiinhalt. Dateiname, Dateiendung, Multipart-`Content-Type`, JSON-Felder und Data-URL-Präfixe werden nicht als Typnachweis verwendet. - Für JSON wird `Content-Type: application/json` empfohlen. `Content-Type: text/json` wird aus Kompatibilitätsgründen ebenfalls akzeptiert. - Ein leeres JSON-Objekt (`{}`) enthält keine Datei und kann deshalb keinen Bild- oder Audio-Upload ausführen. - Pro Medien-Request wird genau eine Bild- beziehungsweise Audiodatei verarbeitet. - Base64 vergrößert die Requestgröße um ungefähr ein Drittel und benötigt beim Verarbeiten zusätzlichen Arbeitsspeicher. Für große Dateien ist `multipart/form-data` vorzuziehen. - Zeitstempel werden als SQLite- oder ISO-8601-Text ausgegeben. - Die Node.js-Anwendung besitzt keine eigene Authentifizierung. Ein vorgeschalteter Webserver kann Basic Auth erzwingen; die mitgelieferten Python-Werkzeuge erkennen `401 Unauthorized`, fragen Zugangsdaten ab und wiederholen den Request. ### Uploadgrenzen und Dateitypen Das Dateilimit pro Datei wird durch `MAX_UPLOAD_MB` festgelegt und beträgt standardmäßig `50 MB`. Unterstützte Dateitypen: | Ressource | Dateiendungen beziehungsweise MIME-Typen | |---|---| | GPX | GPX-XML mit ``-Wurzelelement | | Bilder | JPEG, PNG, WebP | | Audio | MP3, MP4/M4A, AAC, Ogg, WAV, WebM | Die Uploadfelder heißen: | Ressource | Multipart-Feld beziehungsweise JSON-Eigenschaft | |---|---| | GPX | `gpx` | | einzelnes Bild | `picture` | | einzelne Audiodatei | `audio` | Bei einem JSON-Medienupload ist `picture` beziehungsweise `audio` ein Objekt: | Eigenschaft | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `filename` | Text | nein | ursprünglicher Dateiname für Protokollierung; die Endung wird nicht zur Typbestimmung verwendet | | `base64` | Text | ja, sofern `dataUrl` fehlt | reiner Base64-Inhalt ohne Präfix | | `dataUrl` | Text | alternativ zu `base64` | vollständige Base64-Data-URL; der dort genannte Medientyp wird ignoriert | `base64` und `dataUrl` sind Alternativen. Der Server erkennt Format und Speicherendung ausschließlich anhand der dekodierten Bytes. Ein vom Client mitgesendetes `contentType`- oder `mimeType`-Feld wird ignoriert und ist nicht erforderlich. ### Inhaltsbasierte Dateityperkennung Binäre Bild- und Audiodateien werden mit `file-type` anhand ihrer Magic Bytes analysiert. Für Audio-/Video-Container wird zusätzlich `@file-type/av` verwendet, damit beispielsweise M4A und WebM möglichst zuverlässig als Audio oder Video unterschieden werden. GPX ist ein textbasiertes XML-Format und wird deshalb separat als UTF-8 gelesen und anhand des ``-Wurzelelements validiert; anschließend übernimmt der vorhandene GPX-Parser die fachliche Prüfung. Daraus folgen zwei wichtige Regeln: - Eine als `image/png` deklarierte Textdatei wird mit `415 Unsupported Media Type` abgewiesen. - Eine echte PNG-Datei wird auch dann akzeptiert, wenn sie `datei.bin` heißt oder im Multipart-Request kein Datei-`Content-Type` angegeben ist. ### Fehlerformat ```json { "error": "HttpError", "message": "Strecke nicht gefunden." } ``` Typische Statuscodes: | Status | Bedeutung | |---:|---| | `200` | Anfrage erfolgreich | | `201` | Ressource wurde angelegt | | `400` | Parameter oder Upload fehlt beziehungsweise ist ungültig | | `401` | vorgeschalteter Webserver verlangt Authentifizierung | | `404` | Route, POI oder Medienressource wurde nicht gefunden | | `409` | Ressource existiert bereits oder Dateisystemzustand verhindert die Operation | | `413` | Datei überschreitet `MAX_UPLOAD_MB` | | `415` | Dateityp wird nicht unterstützt | | `500` | Interner Serverfehler | ### IDs Alle IDs sind positive, von SQLite erzeugte Ganzzahlen. - `:id` bezeichnet bei `/routes/:id/...` die Route. - `:pictureId` bezeichnet einen Datensatz aus `poi_images`. - `:poiId` bezeichnet den POI und zugleich seine höchstens eine Audioressource. Beispiele verwenden überwiegend 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": "/api/routes/1/gpx", "createdAt": "2026-06-16 12:00:00", "updatedAt": "2026-06-16 12:00:00", "deletedAt": null, "proximityM": 324.8 } ``` ### 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/pois/7/audio", "images": [ { "id": 15, "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pois/7/pictures/15" } ] } ``` ### Bildressource ```json { "id": 15, "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pois/7/pictures/15", "createdAt": "2026-06-16 12:30:00" } ``` ### Audioressource Pro POI kann höchstens eine Audiodatei existieren. Deshalb wird die Audioressource über die `poiId` adressiert. ```json { "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "url": "/api/routes/1/pois/7/audio", "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 | | `GET` | `/api/routes/:id/gpx` | GPX-Datei der Route ausliefern | | `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/routes/:routeId/pois/:poiId` | einzelnen POI der Route lesen | | `POST` | `/api/routes/:id/pois` | POI-Metadaten anlegen | | `PUT` | `/api/routes/:routeId/pois/:poiId` | POI-Metadaten aktualisieren | | `DELETE` | `/api/routes/:routeId/pois/:poiId` | POI einschließlich Bildern und Audio löschen | | `GET` | `/api/routes/:routeId/pois/:poiId/pictures` | Bilder eines POIs auflisten | | `GET` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei ausliefern; optional Metadaten mit `?metadata=true` | | `POST` | `/api/routes/:routeId/pois/:poiId/pictures` | einzelnes Bild für den POI hochladen | | `PUT` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | Bilddatei oder Metadaten aktualisieren | | `DELETE` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | einzelnes Bild löschen | | `GET` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei ausliefern; optional Metadaten mit `?metadata=true` | | `POST` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei für den POI anlegen | | `PUT` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs ersetzen | | `DELETE` | `/api/routes/:routeId/pois/:poiId/audio` | Audiodatei des POIs löschen | ## 4. Systemzustand ### `GET /api/health` Parameter: keine. ```bash curl http://127.0.0.1:47145/api/health ``` ```json { "ok": true, "service": "wegwichtel", "socket": "127.0.0.1:47145", "sqliteVersion": "3.46.1", "timestamp": "2026-06-16T12:00:00.000Z" } ``` ## 5. Routen lesen ### `GET /api/routes` Ohne Positionsparameter werden alle aktiven Routen alphabetisch geliefert. Mit `lat` und `lon` wird die Entfernung zum Routenstart berechnet, anhand `radiusKm` gefiltert und nach Entfernung sortiert. #### Query-Parameter | Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `lat` | Dezimalzahl | nein | – | Breitengrad; nur zusammen mit `lon` wirksam | | `lon` | Dezimalzahl | nein | – | Längengrad; nur zusammen mit `lat` wirksam | | `radiusKm` | Dezimalzahl | nein | `DEFAULT_ROUTE_RADIUS_KM`, standardmäßig `25` | maximaler Abstand zum Routenstart | | `includeDeleted` | Boolean-Text | nein | `false` | der exakte Wert `true` schließt gelöschte Routen ein | 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` Liefert Route, GPX-Punkte und POIs einschließlich Medien-URLs. #### Pfadparameter | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `id` | Ganzzahl | ja | Route | #### Query-Parameter | Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `includeDeleted` | Boolean-Text | nein | `false` | mit `true` kann eine gelöschte Route gelesen werden | ```bash curl http://127.0.0.1:47145/api/routes/1 ``` ```bash curl 'http://127.0.0.1:47145/api/routes/1?includeDeleted=true' ``` ### `GET /api/routes/:id/gpx` Liefert die aktive GPX-Datei der Route direkt mit `Content-Type: application/gpx+xml` aus. Der in einer Routenressource enthaltene Wert `gpxUrl` verweist auf diesen Endpunkt. ```bash curl http://127.0.0.1:47145/api/routes/1/gpx \ --output schulwald-runde.gpx ``` Parameter außer der Routen-ID sind nicht vorgesehen. Gelöschte oder unbekannte Routen antworten mit `404`. ## 6. Route anlegen und bearbeiten ### `POST /api/routes` Legt eine Route aus einer GPX-Datei an. Content-Type: `multipart/form-data` | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---|---| | `gpx` | Datei | ja | – | GPX-Datei mit mindestens einem Trackpunkt; der Dateiname ist unerheblich | | `name` | Text | bedingt | Name aus GPX | erforderlich, wenn GPX keinen Namen enthält | | `slug` | Text | nein | aus `name` | interne URL-freundliche Kennung | | `description` | Text | nein | leer | Routenbeschreibung | | `schoolName` | Text | nein | leer | Schule oder Einrichtung | ```bash curl -X POST http://127.0.0.1:47145/api/routes \ -F 'name=Schulwald-Runde' \ -F 'slug=schulwald-runde-klasse-7a' \ -F 'description=Naturkundlicher Rundweg der Klasse 7a' \ -F 'schoolName=Beispielschule' \ -F 'gpx=@examples/sample-route.gpx' ``` Erfolg: `201 Created` und `Location: /api/routes/`. ### `PUT /api/routes/:id` Aktualisiert Metadaten. Eine optionale GPX-Datei ersetzt alle bisherigen Trackpunkte. | Feld | Typ | Pflicht | Verhalten ohne Feld | |---|---|---:|---| | `gpx` | Datei | nein | bisherige GPX-Punkte bleiben erhalten | | `name` | Text | nein | bisheriger Wert bleibt | | `description` | Text | nein | bisheriger Wert bleibt | | `schoolName` | Text | nein | bisheriger Wert bleibt | ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1 \ -F 'name=Schulwald-Runde 2026' \ -F 'description=Überarbeitete Strecke' \ -F 'schoolName=Beispielschule' \ -F 'gpx=@route-neu.gpx' ``` ### `POST /api/routes/:id/append` Hängt alle Trackpunkte einer GPX-Datei an die Route an und berechnet Streckenwerte neu. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `gpx` | Datei | ja | anzuhängende GPX-Datei | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/append \ -F 'gpx=@verlaengerung.gpx' ``` ## 7. POIs lesen und bearbeiten ### `GET /api/routes/:id/pois` Liefert alle POIs einer aktiven Route nach `sequence` und `id`. ```bash curl http://127.0.0.1:47145/api/routes/1/pois ``` ### `GET /api/routes/:routeId/pois/:poiId` Liefert einen einzelnen POI einschließlich seiner aktuellen Bild- und Audio-URLs. Route und POI werden gemeinsam geprüft; der POI muss zur angegebenen aktiven Route gehören. | Pfadparameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | Ganzzahl | ja | ID der aktiven Route | | `poiId` | Ganzzahl | ja | ID des POIs innerhalb dieser Route | ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7 ``` Eine unbekannte Route, ein unbekannter POI oder eine falsche Route-POI-Kombination liefert `404 Not Found`. ### `POST /api/routes/:id/pois` Legt ausschließlich die POI-Metadaten an. Bilder und Audio werden anschließend über die gesonderten Medienendpunkte hochgeladen. | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `title` | Text | nein | `Unbenannter POI` | Stationsname | | `description` | Text | nein | leer | Beschreibung | | `lat` | Dezimalzahl | ja | – | Breitengrad | | `lon` | Dezimalzahl | ja | – | Längengrad | | `triggerRadiusM` | Dezimalzahl | nein | `DEFAULT_POI_TRIGGER_METERS`, standardmäßig `80` | Aktivierungsradius in Metern | | `sequence` | Ganzzahl | nein | `0` | Reihenfolge in der Stationsliste | JSON-Beispiel mit allen Parametern: ```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": "Hier wird das Alter der Eiche erklärt.", "lat": 52.5208, "lon": 13.4070, "triggerRadiusM": 60, "sequence": 2 }' ``` Erfolg: `201 Created` und `Location: /api/routes//pois/`. ### `PUT /api/routes/:routeId/pois/:poiId` Aktualisiert ausschließlich POI-Metadaten. Nicht übergebene Werte bleiben erhalten. Route und POI werden gemeinsam validiert; ein POI kann über diesen Endpunkt keiner anderen Route zugeordnet werden. | Pfadparameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | Ganzzahl | ja | ID der aktiven Route | | `poiId` | Ganzzahl | ja | ID des POIs innerhalb dieser Route | | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `title` | Text | nein | neuer Stationsname | | `description` | Text | nein | neue Beschreibung | | `lat` | Dezimalzahl | nein | neuer Breitengrad | | `lon` | Dezimalzahl | nein | neuer Längengrad | | `triggerRadiusM` | Dezimalzahl | nein | neuer Aktivierungsradius | | `sequence` | nichtnegative Ganzzahl | nein | neue Reihenfolge | ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/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": 3 }' ``` ### `DELETE /api/routes/:routeId/pois/:poiId` Löscht den POI-Datensatz sowie alle zugehörigen Bilddatensätze, Bilddateien und die optionale Audiodatei. Dieser Vorgang ist im Gegensatz zum Soft Delete einer vollständigen Route nicht wiederherstellbar. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7 ``` ```json { "id": 7, "routeId": 1, "deleted": true } ``` ## 8. Bilder eines POIs einzeln verwalten Die Route und der POI sind Bestandteil jedes Bildpfades. Dadurch ist die Zuordnung eindeutig und beim Upload muss keine zusätzliche `poiId` übergeben werden. ### Gemeinsame Pfadparameter | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | positive Ganzzahl | ja | ID der aktiven Route | | `poiId` | positive Ganzzahl | ja | ID eines POIs, der zu dieser Route gehört | | `pictureId` | positive Ganzzahl | nur bei Einzelressourcen | ID des Bildes, das zu diesem POI gehört | ### `GET /api/routes/:routeId/pois/:poiId/pictures` Liefert alle Bilder des angegebenen POIs in Diashow-Reihenfolge. ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures ``` ```json { "pictures": [ { "id": 15, "routeId": 1, "poiId": 7, "poiTitle": "Die alte Eiche", "caption": "Blick auf die Baumkrone", "sequence": 0, "url": "/api/routes/1/pois/7/pictures/15", "createdAt": "2026-06-16 12:30:00" } ] } ``` ### `GET /api/routes/:routeId/pois/:poiId/pictures/:pictureId` Liefert standardmäßig die Bilddatei direkt aus. Genau dieser Pfad wird im Feld `url` der Bildressource und unter `pois[].images[].url` ausgegeben. Bild speichern: ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ --output bild-15.jpg ``` Metadaten statt Dateidaten abrufen: ```bash curl --get http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ --data-urlencode 'metadata=true' ``` | Query-Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Bilddatei liefern | Die API antwortet mit `404`, wenn Route, POI oder Bild nicht zusammengehören. ### `POST /api/routes/:routeId/pois/:poiId/pictures` Lädt genau ein Bild für den im Pfad angegebenen POI hoch. Zulässig sind zwei Übertragungsformen. #### Variante A: `multipart/form-data` | Feld | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `picture` | Datei | ja | – | JPEG-, PNG- oder WebP-Datei; der Typ wird aus dem Inhalt erkannt | | `caption` | Text | nein | leer | sichtbare Bildbeschreibung und Grundlage für den Alternativtext | | `sequence` | nichtnegative Ganzzahl | nein | nächster freier Wert des POIs | Reihenfolge in der Diashow | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/pictures \ -F 'caption=Blick auf die Baumkrone' \ -F 'sequence=0' \ -F 'picture=@eiche.jpg' ``` #### Variante B: JSON mit Base64 Empfohlener Content-Type: `application/json`. Der Server akzeptiert zusätzlich `text/json`. ```json { "caption": "Blick auf die Baumkrone", "sequence": 0, "picture": { "filename": "eiche.jpg", "base64": "/9j/4AAQSkZJRgABAQ..." } } ``` Beispiel mit `text/json` und einer separat erzeugten Payload-Datei: ```bash base64 < eiche.jpg | tr -d '\n' > eiche.jpg.b64 jq -n \ --arg caption 'Blick auf die Baumkrone' \ --argjson sequence 0 \ --rawfile data eiche.jpg.b64 \ '{ caption: $caption, sequence: $sequence, picture: { filename: "eiche.jpg", base64: $data } }' > picture.json curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/pictures \ -H 'Content-Type: text/json' \ --data-binary @picture.json ``` Alternativ kann eine Data-URL übertragen werden: ```json { "caption": "Blick auf die Baumkrone", "picture": { "filename": "eiche.png", "dataUrl": "data:image/png;base64,iVBORw0KGgoAAA..." } } ``` Ein Request mit `-d '{}'` schlägt mit `400 Bad Request` fehl, weil weder eine Multipart-Datei noch ein JSON-Dateiobjekt enthalten ist. Erfolg: `201 Created` und `Location: /api/routes/1/pois/7/pictures/`. ### `PUT /api/routes/:routeId/pois/:poiId/pictures/:pictureId` Aktualisiert Metadaten und kann optional die Datei ersetzen. Nicht übergebene Metadaten bleiben erhalten. Ein Bild kann über diesen Endpunkt nicht einem anderen POI zugeordnet werden; dafür muss es beim bisherigen POI gelöscht und beim Ziel-POI neu angelegt werden. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `picture` | Multipart-Datei oder JSON-Dateiobjekt | nein | ersetzt die bisherige Bilddatei | | `caption` | Text | nein | neue Bildbeschreibung; leerer Text entfernt die Beschreibung | | `sequence` | nichtnegative Ganzzahl | nein | neue Position in der Diashow | Nur Metadaten per JSON ändern: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ -H 'Content-Type: application/json' \ -d '{ "caption": "Nahaufnahme der Eichenblätter", "sequence": 1 }' ``` Datei und Metadaten per Multipart ersetzen: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ -F 'caption=Neue Aufnahme der Eiche' \ -F 'sequence=2' \ -F 'picture=@eiche-neu.webp' ``` Datei und Metadaten per JSON ersetzen: ```json { "caption": "Neue Aufnahme der Eiche", "sequence": 2, "picture": { "filename": "eiche-neu.webp", "base64": "UklGRiQAAABXRUJQVlA4..." } } ``` ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 \ -H 'Content-Type: application/json' \ --data-binary @picture-update.json ``` Wird eine Datei ersetzt, entfernt der Server die bisherige Datei nach erfolgreicher Datenbankaktualisierung. ### `DELETE /api/routes/:routeId/pois/:poiId/pictures/:pictureId` Entfernt Bilddatensatz und Datei. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/pictures/15 ``` ```json { "id": 15, "routeId": 1, "poiId": 7, "deleted": true } ``` ## 9. Audiodatei eines POIs verwalten Pro POI ist höchstens eine Audiodatei vorgesehen. Daher ist `/audio` selbst die Einzelressource; eine zusätzliche Audio-ID ist nicht erforderlich. ### Gemeinsame Pfadparameter | Parameter | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `routeId` | positive Ganzzahl | ja | ID der aktiven Route | | `poiId` | positive Ganzzahl | ja | ID eines POIs, der zu dieser Route gehört | ### `GET /api/routes/:routeId/pois/:poiId/audio` Liefert standardmäßig die Audiodatei des POIs direkt aus. Genau dieser Pfad wird in `audioUrl` und im Feld `url` der Audioressource ausgegeben. Der Endpunkt unterstützt HTTP-Range-Anfragen, damit Browser innerhalb der Audiodatei springen können. Audiodatei speichern: ```bash curl http://127.0.0.1:47145/api/routes/1/pois/7/audio \ --output ansage-7.mp3 ``` Metadaten statt Dateidaten abrufen: ```bash curl --get http://127.0.0.1:47145/api/routes/1/pois/7/audio \ --data-urlencode 'metadata=true' ``` | Query-Parameter | Typ | Pflicht | Standard | Beschreibung | |---|---|---:|---:|---| | `metadata` | Boolean | nein | `false` | bei `true` JSON-Metadaten statt der Audiodatei liefern | Antwortet mit `404`, wenn Route und POI nicht zusammengehören oder der POI keine Audiodatei besitzt. ### `POST /api/routes/:routeId/pois/:poiId/audio` Legt die Audiodatei des im Pfad angegebenen POIs an. Zulässig sind `multipart/form-data` und JSON mit Base64. #### Variante A: `multipart/form-data` | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `audio` | Datei | ja | MP3-, MP4/M4A-, AAC-, Ogg-, WAV- oder WebM-Datei; der Typ wird aus dem Inhalt erkannt | ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -F 'audio=@ansage.mp3' ``` #### Variante B: JSON mit Base64 ```json { "audio": { "filename": "ansage.mp3", "base64": "SUQzBAAAAAAAI1RTU0UAAA..." } } ``` ```bash base64 < ansage.mp3 | tr -d '\n' > ansage.mp3.b64 jq -n --rawfile data ansage.mp3.b64 \ '{ audio: { filename: "ansage.mp3", base64: $data } }' > audio.json curl -X POST http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -H 'Content-Type: application/json' \ --data-binary @audio.json ``` Auch hier wird `Content-Type: text/json` akzeptiert. Ein leeres `{}` enthält keine Audiodatei und liefert `400 Bad Request`. Erfolg: `201 Created` und `Location: /api/routes/1/pois/7/audio`. Existiert bereits eine Audiodatei, antwortet der Server mit `409`. Zum Ersetzen ist `PUT` zu verwenden. ### `PUT /api/routes/:routeId/pois/:poiId/audio` Ersetzt die vorhandene Audiodatei des POIs. Die Datei kann als Multipart-Upload oder als JSON-Dateiobjekt übertragen werden. | Feld | Typ | Pflicht | Beschreibung | |---|---|---:|---| | `audio` | Multipart-Datei oder JSON-Dateiobjekt | ja | neue Audiodatei | Multipart-Beispiel: ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -F 'audio=@ansage-neu.ogg' ``` JSON-Beispiel: ```json { "audio": { "filename": "ansage-neu.ogg", "base64": "T2dnUwACAAAAAAAAAAB..." } } ``` ```bash curl -X PUT http://127.0.0.1:47145/api/routes/1/pois/7/audio \ -H 'Content-Type: text/json' \ --data-binary @audio-update.json ``` Der Server entfernt die bisherige Datei nach erfolgreicher Aktualisierung. Besitzt der POI noch keine Audiodatei, antwortet der Server mit `404`; zum erstmaligen Anlegen ist `POST` zu verwenden. ### `DELETE /api/routes/:routeId/pois/:poiId/audio` Entfernt die Audiodatei und setzt `audioUrl` des POIs auf `null`. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1/pois/7/audio ``` ```json { "routeId": 1, "poiId": 7, "deleted": true } ``` ## 10. Route weich löschen und wiederherstellen ### `DELETE /api/routes/:id` Markiert die Route als gelöscht und verschiebt das vollständige Streckenverzeichnis mit GPX, Bildern und Audio nach `storage/trash/routes/`. Die Datenbankeinträge bleiben erhalten und ihre Pfade werden auf den Papierkorb umgeschrieben. ```bash curl -X DELETE http://127.0.0.1:47145/api/routes/1 ``` ```json { "route": { "id": 1, "status": "deleted", "gpxUrl": null }, "softDeleted": true } ``` ### `POST /api/routes/:id/restore` Verschiebt eine gelöschte Route zurück in den aktiven Speicher und schreibt alle GPX-, Bild- und Audiopfade zurück. Parameter: keine. ```bash curl -X POST http://127.0.0.1:47145/api/routes/1/restore ``` ## 11. Dateien über REST ausliefern Die API veröffentlicht keine internen Speicherpfade und keine `/media/...`-Adressen. Alle in JSON ausgegebenen Datei-URLs verweisen auf stabile REST-Endpunkte: ```text GET /api/routes/1/gpx GET /api/routes/1/pois/7/pictures/15 GET /api/routes/1/pois/7/audio ``` Die Zuordnung lautet: | JSON-Feld | Datei-Endpunkt | |---|---| | `route.gpxUrl` | `/api/routes/:id/gpx` | | `poi.audioUrl` | `/api/routes/:routeId/pois/:poiId/audio` | | `poi.images[].url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | | `picture.url` | `/api/routes/:routeId/pois/:poiId/pictures/:pictureId` | | `audio.url` | `/api/routes/:routeId/pois/:poiId/audio` | Die Dateinamen und relativen Pfade unter `storage/` bleiben ausschließlich interne Implementierungsdetails. Dateien gelöschter Routen liegen im Papierkorb und sind über keinen Datei-Endpunkt erreichbar. ## 12. Python-Werkzeuge Unter `tools/python/` liegen interaktive Skripte für Anlegen, Ändern, Erweitern, Wiederherstellen und Löschen von Routen, POIs, Bildern und Audio. Sie verwenden ausschließlich die Python-Standardbibliothek. Fehlende Parameter werden abgefragt. Antwortet ein vorgeschalteter Webserver mit HTTP 401, fragt die gemeinsame Request-Schicht Benutzername und Passwort ab und wiederholt den ursprünglichen Request. Details und Aufrufbeispiele stehen in [`Python-Werkzeuge.md`](Python-Werkzeuge.md). ## Gruppenbearbeitung Siehe [Gruppenbearbeitung.md](Gruppenbearbeitung.md). Alle Gruppenantworten sind `no-store`. POST/DELETE prüfen einen übermittelten Origin gegen den Host. | Methode und Pfad | Zweck | | --- | --- | | GET /api/groups | Gruppen der Sitzung auflisten; ohne Sitzung leere Liste | | POST /api/groups | Mit routeId und creator_id eine unabhängige Kopie öffnen | | POST /api/groups/join | Mit code (Array aus fünf Unicode-Symbolen; Wiederholungen erlaubt) und creator_id beitreten | | POST /api/groups/:id/code | Als Besitzer neuen Einladungscode erzeugen | | DELETE /api/groups/:id | Als Besitzer Einladung und Mitbearbeiterzugänge widerrufen | Der Server setzt `wegwichtel_group_session` als geschütztes Cookie auf `/api`. Nur Hashes werden gespeichert. Die creator_id allein erteilt keine Gruppenrechte. Gruppenantworten enthalten id, route, role, active und revision; nur das Erzeugen oder Erneuern liefert den Klartextcode und expiresAt (Unixsekunden). Die bisherigen `/api/routes/:id/...`-Endpunkte prüfen auch den Gruppenzugang, einschließlich GPX, Bilder und Audio. Unveröffentlichte Gruppenkopien sind nicht öffentlich gelistet. GET liefert `X-Group-Route` und `X-Group-Revision`; Änderungen müssen die geladene Revision als `If-Match` senden. Überlappende oder veraltete Änderungen ergeben 409. Erfolgreiche Änderungen erhöhen die Revision. API-Clients müssen Cookies beibehalten; Gruppenmedien können damit auch als normale Bild- und Audio-Ressourcen abgerufen werden. Freigegebene Strecken bleiben schreibgeschützt. ### Stationssperren für gemeinsame Entwürfe `POST /api/groups/routes/:routeId/pois/:poiId/lock` verwendet die berechtigte Gruppensitzung. `{}` beansprucht eine freie Station. Antwort: `token`, `version`, `mediaVersion`, `poi`. Eine vorhandene Bearbeitung ergibt 423 mit `details.code=point-locked` und `details.replace`. Nach einer bestätigten Rückfrage mit `{ "replace": "" }` erneut senden. Eine inzwischen veränderte Sperre braucht eine neue Bestätigung. `{ "token": "" }` aktualisiert den eigenen Inhaltsstand; `{ "release": true, "token": "…" }` gibt nur die eigene Sperre frei. Fremde creator_id-Angaben werden nicht übernommen. Laufende Schreibanfragen ergeben `point-busy` (423), ein verlorener Zugang zur Bearbeitung `point-lost` (423), ein veränderter Stationsinhalt `point-changed` (409). Für Schreibzugriffe auf `/api/routes/:id/pois/:poiId` und dessen Bild-/Tonpfade sind `X-Poi-Lock: ` und `If-Poi-Match: ` erforderlich. Anstelle von If-Match wird dort der Stationsinhalt geprüft. Erfolgreiche Antworten liefern `X-Poi-Id` und `X-Poi-Version`, solange die Station existiert. Diese Version bei mehreren Dateioperationen jeweils weiterverwenden. Eine neue Station wird über den bisherigen POST angelegt und vor den weiteren Dateioperationen gesperrt. Die Versionsabdrücke berücksichtigen Dateibytes, nicht bloß URLs oder Metadaten. ### Fristen, Aktivität und Status `GET /api/groups/routes/:routeId/pois/:poiId/lock` prüft eine Station innerhalb der berechtigten Gruppe und liefert ohne Antwortkörper **204** (frei) oder **423** (gesperrt). Nicht vorhandene Strecken/Stationen ergeben **404**, fehlende Anmeldung/Berechtigung weiterhin **401/403**. Die Abfrage verlängert keine Sperre. `POST` auf denselben Pfad mit `{ "heartbeat": true, "token": "" }` verlängert nur die eigene noch gültige Sperre und liefert **204**. Abgelaufene oder fremde Sperren ergeben **423**. Die App meldet nur tatsächliche Interaktion mit der Station, gebündelt alle 30 Sekunden. Nach 60 Minuten ohne Aktivität wird die Datei entfernt. Eine Wartung beim Start und jede Minute räumt sie auch ohne weitere Client-Anfragen auf. Laufende Schreibanfragen bleiben bis zu ihrem Abschluss geschützt. `GET /api/groups/:id/invitation` ist ausschließlich für die eröffnende Sitzung. Antwort: `group`, `busy`, `expired`, `expiresAt` und, wenn im Prozess verfügbar, `code`. Nach 30 Minuten werden Codes automatisch ersetzt, sobald alle Stationen frei sind. Während einer Stationssperre wird auch manuelles Erneuern mit **423** (`group-editing`) zurückgewiesen. Der bisherige Code läuft trotzdem ab; bestehende Mitglieder bleiben berechtigt. Die Wartung und dieser Abruf führen die Erneuerung aus. Klartext-Codes stehen nur im Prozessspeicher, dauerhaft wird ausschließlich ihr Hash gespeichert.