From 40f230864b70e4b40ed4df748ffa0ab70aba4f56 Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Tue, 16 Jun 2026 17:01:53 +0200 Subject: [PATCH] Reworked pause and stop buttons and documentation --- README.md | 45 +-- docs/REST-API.md | 636 ++++++++++++++++++++++++++++++++++++++ public/css/app.css | 13 + public/index.html | 2 +- public/js/app.js | 25 +- public/js/audio-player.js | 36 ++- 6 files changed, 710 insertions(+), 47 deletions(-) create mode 100644 docs/REST-API.md diff --git a/README.md b/README.md index 5b67911..6254968 100644 --- a/README.md +++ b/README.md @@ -68,44 +68,9 @@ public/vendor/jquery-ui/images/*.png Die Anwendung verwendet aus jQuery UI insbesondere die Widgets **Button** und **Controlgroup**. Das offizielle vollständige jQuery-UI-Bundle bleibt lokal verfügbar, damit weitere aktuelle Widgets ohne erneuten CDN-Bezug ergänzt werden können. Die frühere jQuery-Mobile-Seitensteuerung wurde durch eine eigene, History-API-basierte Navigation ersetzt. -## REST-API +## API-Dokumentation -| Methode | Pfad | Zweck | -|---|---|---| -| GET | `/api/health` | Server- und SQLite-Selbsttest | -| GET | `/api/routes?lat=…&lon=…&radiusKm=25` | aktive, optional nahe Routen | -| GET | `/api/routes/:id` | Route, Trackpunkte, POIs und Medien-URLs | -| GET | `/api/routes/:id/pois` | POIs einer Route | -| GET | `/api/pois/:id` | einzelner POI samt Bild- und Audio-URLs | -| POST | `/api/routes` | neue Route; `multipart/form-data`, Feld `gpx` | -| PUT | `/api/routes/:id` | Metadaten und optional GPX vollständig ersetzen | -| POST | `/api/routes/:id/append` | zusätzliche GPX-Trackpunkte anhängen | -| POST | `/api/routes/:id/pois` | POI anlegen; Felder `audio` und `images` | -| PUT | `/api/pois/:id` | POI ändern und weitere Medien ergänzen | -| DELETE | `/api/routes/:id` | Route samt Medien in den Papierkorb verschieben | -| POST | `/api/routes/:id/restore` | Route aus dem Papierkorb wiederherstellen | - -### Route anlegen - -```bash -curl -X POST http://127.0.0.1:47145/api/routes \ - -F 'name=Schulwald-Runde' \ - -F 'schoolName=Beispielschule' \ - -F 'description=Naturkundlicher Rundweg' \ - -F 'gpx=@examples/sample-route.gpx;type=application/gpx+xml' -``` - -### POI mit Bildern und Audio anlegen - -```bash -curl -X POST http://127.0.0.1:47145/api/routes/1/pois \ - -F 'title=Die alte Eiche' \ - -F 'description=Hier wird das Alter der Eiche erklärt.' \ - -F 'lat=52.5208' -F 'lon=13.4070' -F 'triggerRadiusM=60' \ - -F 'audio=@ansage.mp3;type=audio/mpeg' \ - -F 'images=@eiche-1.jpg;type=image/jpeg' \ - -F 'images=@eiche-2.jpg;type=image/jpeg' -``` +Die vollständige REST-API ist einschließlich aller Pfad-, Query-, Formular- und Datei-Parameter sowie ausführlicher `curl`-Beispiele in [`docs/REST-API.md`](docs/REST-API.md) dokumentiert. Die README beschreibt bewusst nur Installation, Architektur und Bedienverhalten. ## Dateistruktur @@ -171,8 +136,10 @@ Beim Start der Route wird der aktuellen Position nächstgelegene GPX-Trackpunkt Die Routenansicht besitzt drei nebeneinanderliegende, semantische Schaltflächen mit lokalen SVG-Symbolen: - **Route starten beziehungsweise fortsetzen:** aktiviert `navigator.geolocation.watchPosition()` und die Geräteausrichtung. -- **Route pausieren:** beendet den aktiven GPS-Watcher und den Kompass-Listener, behält aber Routenfortschritt und bereits ausgelöste POIs bei. -- **Route beenden:** beendet alle Sensor-Listener, setzt den Fortschritt zurück und leert die Liste bereits ausgelöster POIs. +- **Route pausieren:** beendet den aktiven GPS-Watcher und den Kompass-Listener, pausiert eine laufende Audioansage und behält Routenfortschritt, Audioquelle, Abspielposition und bereits ausgelöste POIs bei. +- **Route beenden:** beendet alle Sensor-Listener, hält die Audioansage an, entlädt ihre Quelldatei, setzt den Fortschritt zurück und kehrt zur Routenauswahl zurück. + +Die aktuell geöffnete beziehungsweise automatisch ausgelöste Station wird in der Stationsliste sichtbar hervorgehoben und mit `aria-current="step"` semantisch gekennzeichnet. Während die Route läuft, werden Positionsänderungen fortlaufend verarbeitet. Eine Browser-Webanwendung ist jedoch kein nativer Hintergrunddienst: Betriebssystem und Browser können die Aktualisierung bei gesperrtem Bildschirm, Energiesparmodus oder im Hintergrund drosseln beziehungsweise anhalten. diff --git a/docs/REST-API.md b/docs/REST-API.md new file mode 100644 index 0000000..638e2d0 --- /dev/null +++ b/docs/REST-API.md @@ -0,0 +1,636 @@ +# Wegwichtel REST-API + +Diese Datei dokumentiert die vollständige HTTP-Schnittstelle des aktuellen Wegwichtel-Servers. Die API wird unter `/api` bereitgestellt. Aktive GPX-, Bild- und Audiodateien werden zusätzlich unter `/media` ausgeliefert. + +## 1. Grundlagen + +### Basisadresse + +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 + +- Lesezugriffe liefern JSON. +- Routen- und POI-Schreibzugriffe mit Dateien verwenden `multipart/form-data`. +- Fehler werden als JSON ausgegeben. +- Zeitstempel stammen aus SQLite beziehungsweise JavaScript und werden als Text oder ISO-8601-Zeitstempel ausgegeben. +- Die aktuelle API besitzt noch keine Authentifizierung. Schreibzugriffe dürfen deshalb nicht ungeschützt öffentlich erreichbar sein. + +### Allgemeines Fehlerformat + +```json +{ + "error": "HttpError", + "message": "Strecke nicht gefunden." +} +``` + +Bei Detailinformationen kann zusätzlich `details` enthalten sein: + +```json +{ + "error": "HttpError", + "message": "Die GPX-Datei ist kein gültiges XML.", + "details": "Parsermeldung" +} +``` + +Typische Statuscodes: + +| Status | Bedeutung | +|---:|---| +| `200` | Anfrage erfolgreich | +| `201` | Ressource wurde angelegt | +| `400` | Parameter oder Upload unvollständig beziehungsweise ungültig | +| `404` | Route, POI oder Datei wurde nicht gefunden | +| `409` | Dateisystemzustand verhindert Löschen oder Wiederherstellen | +| `413` | Eine hochgeladene Datei überschreitet das konfigurierte Limit | +| `415` | Dateityp wird nicht unterstützt | +| `500` | Interner Serverfehler | + +### IDs + +Die Pfadparameter `:id` sind positive, von SQLite erzeugte Ganzzahlen. Beispiele verwenden überwiegend Route `1` und POI `7`. + +### Uploadgrenzen und Dateitypen + +Das Dateilimit pro Datei wird durch `MAX_UPLOAD_MB` festgelegt und beträgt standardmäßig `50 MB`. Multer akzeptiert höchstens 25 Dateien pro Request. + +Erlaubte Uploadtypen: + +- GPX/XML: `.gpx`, `application/gpx+xml`, `application/xml`, `text/xml` +- Bilder: JPEG, PNG, WebP +- Audio: MP3, MP4/M4A, AAC, Ogg, WAV, WebM + +Beim Anlegen oder Aktualisieren eines POIs gelten zusätzlich: + +- höchstens eine Datei im Feld `audio` +- höchstens 20 Dateien im Feld `images` + +## 2. Datenmodelle + +### Kurzfassung einer Route + +```json +{ + "id": 1, + "slug": "schulwald-runde", + "name": "Schulwald-Runde", + "description": "Naturkundlicher Rundweg", + "schoolName": "Beispielschule", + "status": "active", + "start": { "lat": 52.5208, "lon": 13.4070 }, + "center": { "lat": 52.5210, "lon": 13.4080 }, + "bounds": { + "minLat": 52.5208, + "minLon": 13.4070, + "maxLat": 52.5212, + "maxLon": 13.4090 + }, + "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, + "proximityM": 324.8 +} +``` + +`proximityM` ist `null`, wenn beim Listenaufruf keine gültige Position übergeben wurde. Bei gelöschten Routen ist `gpxUrl` `null`, weil Dateien im Papierkorb nicht öffentlich ausgeliefert werden. + +### Vollständige Route + +`GET /api/routes/:id` ergänzt die Kurzfassung um `points` und `pois`: + +```json +{ + "id": 1, + "name": "Schulwald-Runde", + "points": [ + { + "sequence": 0, + "lat": 52.5208, + "lon": 13.4070, + "elevation": 71.4, + "recordedAt": "2026-06-16T09:00:00Z" + } + ], + "pois": [] +} +``` + +### POI + +```json +{ + "id": 7, + "routeId": 1, + "title": "Die alte Eiche", + "description": "Hier wird das Alter der Eiche erklärt.", + "lat": 52.5208, + "lon": 13.4070, + "triggerRadiusM": 60, + "sequence": 2, + "audioUrl": "/media/routes/1/audio/uuid.mp3", + "images": [ + { + "id": 15, + "caption": "", + "sequence": 0, + "url": "/media/routes/1/images/uuid.jpg" + } + ] +} +``` + +## 3. Systemzustand + +### `GET /api/health` + +Prüft den Node.js-Prozess und eine SQLite-Abfrage. + +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" +} +``` + +## 4. Routen lesen + +### `GET /api/routes` + +Liefert standardmäßig alle aktiven Routen alphabetisch. Werden `lat` und `lon` gemeinsam übergeben, berechnet der Server die Entfernung zum Startpunkt, filtert anhand `radiusKm` und sortiert nach Entfernung. + +#### Query-Parameter + +| Parameter | Typ | Pflicht | Standard | Beschreibung | +|---|---|---:|---:|---| +| `lat` | Dezimalzahl | nein | – | Breitengrad der aktuellen Position; nur zusammen mit `lon` wirksam | +| `lon` | Dezimalzahl | nein | – | Längengrad der aktuellen Position; nur zusammen mit `lat` wirksam | +| `radiusKm` | Dezimalzahl | nein | `DEFAULT_ROUTE_RADIUS_KM`, standardmäßig `25` | maximaler Abstand zum Routenstart; nur bei gültigem `lat` und `lon` wirksam | +| `includeDeleted` | Boolean-Text | nein | `false` | nur der exakte Wert `true` schließt gelöschte Routen ein | + +Alle aktiven Routen: + +```bash +curl http://127.0.0.1:47145/api/routes +``` + +Alle Parameter in einem Beispiel: + +```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' +``` + +Antwort: + +```json +{ + "routes": [ + { + "id": 1, + "name": "Schulwald-Runde", + "status": "active", + "proximityM": 324.8 + } + ] +} +``` + +Hinweise: + +- Eine ungültige Zahl wird intern wie ein nicht gesetzter Wert behandelt. +- Der Positionsfilter wird nur aktiv, wenn sowohl `lat` als auch `lon` gültige Zahlen sind. +- `radiusKm` wird derzeit nicht auf einen Mindest- oder Höchstwert begrenzt. + +### `GET /api/routes/:id` + +Liefert eine Route mit sämtlichen GPX-Punkten und POIs. + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID der Route | + +#### Query-Parameter + +| Parameter | Typ | Pflicht | Standard | Beschreibung | +|---|---|---:|---:|---| +| `includeDeleted` | Boolean-Text | nein | `false` | mit `true` kann auch eine als gelöscht markierte Route gelesen werden | + +Aktive Route: + +```bash +curl http://127.0.0.1:47145/api/routes/1 +``` + +Gelöschte Route ausdrücklich einschließen: + +```bash +curl 'http://127.0.0.1:47145/api/routes/1?includeDeleted=true' +``` + +Bei einer gelöschten Route sind Medien-URLs `null`, weil der Papierkorb nicht unter `/media` veröffentlicht wird. + +### `GET /api/routes/:id/pois` + +Liefert alle POIs einer aktiven Route in der Reihenfolge `sequence`, anschließend `id`. + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID der aktiven Route | + +Weitere Parameter: keine. + +```bash +curl http://127.0.0.1:47145/api/routes/1/pois +``` + +Antwort: + +```json +{ + "pois": [ + { + "id": 7, + "routeId": 1, + "title": "Die alte Eiche", + "sequence": 2, + "audioUrl": "/media/routes/1/audio/uuid.mp3", + "images": [] + } + ] +} +``` + +## 5. POIs lesen + +### `GET /api/pois/:id` + +Liefert einen einzelnen POI einschließlich Bild- und Audio-URLs. Der POI muss zu einer aktiven Route gehören. + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID des POIs | + +Weitere Parameter: keine. + +```bash +curl http://127.0.0.1:47145/api/pois/7 +``` + +## 6. Route anlegen + +### `POST /api/routes` + +Legt eine neue Route aus einer GPX-Datei an. Trackpunkte, Streckenlänge, Höhengewinn, Startpunkt, Mittelpunkt und Grenzen werden aus der Datei berechnet. + +Content-Type: `multipart/form-data` + +#### Formular- und Dateiparameter + +| Feld | Typ | Pflicht | Standard | Beschreibung | +|---|---|---:|---|---| +| `gpx` | Datei | ja | – | GPX-Datei mit mindestens einem verwertbaren `trkpt` | +| `name` | Text | bedingt | GPX-Track- oder Metadatenname | Routenname; erforderlich, falls die GPX-Datei keinen Namen enthält | +| `slug` | Text | nein | aus `name` abgeleitet | URL-freundliche interne Kennung; Kollisionen erhalten automatisch `-2`, `-3` usw. | +| `description` | Text | nein | leer | Beschreibung der Route | +| `schoolName` | Text | nein | leer | Name der Schule oder Einrichtung | + +Beispiel mit allen Parametern: + +```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;type=application/gpx+xml' +``` + +Erfolg: + +- Status `201 Created` +- Header `Location: /api/routes/` +- Body: vollständige neu angelegte Route + +## 7. Route vollständig aktualisieren + +### `PUT /api/routes/:id` + +Ändert Metadaten einer aktiven Route. Eine optionale GPX-Datei ersetzt alle bisherigen Trackpunkte und die Datei `route.gpx`. + +Content-Type: `multipart/form-data` + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID der aktiven Route | + +#### Formular- und Dateiparameter + +| Feld | Typ | Pflicht | Verhalten bei Auslassung | Beschreibung | +|---|---|---:|---|---| +| `name` | Text | nein | alter Wert bleibt | neuer Routenname | +| `description` | Text | nein | alter Wert bleibt | neue Beschreibung; leere Zeichenfolge löscht den Inhalt | +| `schoolName` | Text | nein | alter Wert bleibt | neuer Schulname; leere Zeichenfolge löscht den Inhalt | +| `gpx` | Datei | nein | Track bleibt unverändert | ersetzt GPX-Datei, Trackpunkte und berechnete Kennzahlen vollständig | + +`slug` kann über diesen Endpunkt derzeit nicht geändert werden. + +Beispiel mit allen Parametern: + +```bash +curl -X PUT http://127.0.0.1:47145/api/routes/1 \ + -F 'name=Schulwald-Runde – überarbeitet' \ + -F 'description=Neue Wegführung ab dem Schulhof' \ + -F 'schoolName=Beispielschule Nord' \ + -F 'gpx=@examples/sample-route.gpx;type=application/gpx+xml' +``` + +Nur die Beschreibung ändern: + +```bash +curl -X PUT http://127.0.0.1:47145/api/routes/1 \ + -F 'description=Nur dieser Wert wird geändert.' +``` + +Antwort: vollständige aktualisierte Route. + +## 8. GPX-Punkte an eine Route anhängen + +### `POST /api/routes/:id/append` + +Hängt sämtliche verwertbaren Trackpunkte einer GPX-Datei an eine aktive Route an und berechnet die Streckenkennzahlen neu. + +Content-Type: `multipart/form-data` + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID der aktiven Route | + +#### Dateiparameter + +| Feld | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `gpx` | Datei | ja | GPX-Datei mit den anzuhängenden Trackpunkten | + +```bash +curl -X POST http://127.0.0.1:47145/api/routes/1/append \ + -F 'gpx=@weiterer-abschnitt.gpx;type=application/gpx+xml' +``` + +Hinweis: Die Punkte werden in SQLite ergänzt. Die gespeicherte Originaldatei `route.gpx` wird durch diesen Endpunkt derzeit nicht zu einer konsolidierten GPX-Datei erweitert. + +## 9. POI anlegen + +### `POST /api/routes/:id/pois` + +Legt einen POI für eine aktive Route an und speichert optional eine Audioansage und mehrere Bilder. + +Content-Type: `multipart/form-data` + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID der aktiven Route | + +#### Formular- und Dateiparameter + +| Feld | Typ | Pflicht | Standard | Beschreibung | +|---|---|---:|---|---| +| `title` | Text | nein | `Unbenannter POI` | Titel der Station | +| `description` | Text | nein | leer | Beschreibung der Station | +| `lat` | Dezimalzahl | ja | – | Breitengrad des POIs | +| `lon` | Dezimalzahl | ja | – | Längengrad des POIs | +| `triggerRadiusM` | Dezimalzahl | nein | `DEFAULT_POI_TRIGGER_METERS`, standardmäßig `80` | Entfernung in Metern, ab der die Station automatisch ausgelöst wird | +| `sequence` | Ganzzahl | nein | `0` | Sortierreihenfolge innerhalb der Route | +| `audio` | Datei | nein | keine | eine Audioansage | +| `images` | Datei, wiederholbar | nein | keine | bis zu 20 Bilder; jedes Bild wird als eigenes Feld `images` gesendet | + +Beispiel mit allen Parametern: + +```bash +curl -X POST http://127.0.0.1:47145/api/routes/1/pois \ + -F 'title=Die alte Eiche' \ + -F 'description=Hier wird das Alter der Eiche erklärt.' \ + -F 'lat=52.5208' \ + -F 'lon=13.4070' \ + -F 'triggerRadiusM=60' \ + -F 'sequence=2' \ + -F 'audio=@ansage.mp3;type=audio/mpeg' \ + -F 'images=@eiche-1.jpg;type=image/jpeg' \ + -F 'images=@eiche-2.webp;type=image/webp' +``` + +Erfolg: + +- Status `201 Created` +- Header `Location: /api/pois/` +- Body: neu angelegter POI + +Bildunterschriften können in der aktuellen API noch nicht per Parameter gesetzt werden und bleiben leer. + +## 10. POI aktualisieren und Medien ergänzen + +### `PUT /api/pois/:id` + +Ändert einen POI einer aktiven Route. Eine neue Audiodatei ersetzt den in der Datenbank referenzierten Audiopfad. Neue Bilder werden an die vorhandene Bilderliste angehängt. + +Content-Type: `multipart/form-data` + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID des POIs | + +#### Formular- und Dateiparameter + +| Feld | Typ | Pflicht | Verhalten bei Auslassung | Beschreibung | +|---|---|---:|---|---| +| `title` | Text | nein | alter Wert bleibt | neuer Titel | +| `description` | Text | nein | alter Wert bleibt | neue Beschreibung | +| `lat` | Dezimalzahl | nein | alter Wert bleibt | neuer Breitengrad | +| `lon` | Dezimalzahl | nein | alter Wert bleibt | neuer Längengrad | +| `triggerRadiusM` | Dezimalzahl | nein | alter Wert bleibt | neuer automatischer Auslöseradius in Metern | +| `sequence` | Ganzzahl | nein | alter Wert bleibt | neue Sortierreihenfolge | +| `audio` | Datei | nein | alte Referenz bleibt | neue Audioansage; maximal eine Datei | +| `images` | Datei, wiederholbar | nein | Bilder bleiben unverändert | bis zu 20 zusätzliche Bilder | + +Beispiel mit allen Parametern: + +```bash +curl -X PUT http://127.0.0.1:47145/api/pois/7 \ + -F 'title=Die sehr alte Eiche' \ + -F 'description=Überarbeitete Ansage und neue Fotos.' \ + -F 'lat=52.5209' \ + -F 'lon=13.4072' \ + -F 'triggerRadiusM=45' \ + -F 'sequence=3' \ + -F 'audio=@ansage-neu.ogg;type=audio/ogg' \ + -F 'images=@eiche-3.png;type=image/png' \ + -F 'images=@eiche-4.jpg;type=image/jpeg' +``` + +Wichtige aktuelle Einschränkungen: + +- Einzelne Bilder können noch nicht per API gelöscht, umsortiert oder beschriftet werden. +- Neu hochgeladene Bilder werden hinter vorhandenen Bildern einsortiert. +- Beim Ersetzen der Audio-Referenz wird die vorherige Audiodatei derzeit nicht automatisch aus dem aktiven Verzeichnis entfernt. + +## 11. Route zum Löschen markieren + +### `DELETE /api/routes/:id` + +Markiert eine aktive Route als gelöscht und verschiebt ihr gesamtes Verzeichnis einschließlich GPX, Bildern und Audio nach `storage/trash/routes`. Die Datenbankzeilen bleiben erhalten; alle gespeicherten Pfade werden auf den Papierkorb umgeschrieben. + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID der aktiven Route | + +Weitere Parameter: keine. + +```bash +curl -X DELETE http://127.0.0.1:47145/api/routes/1 +``` + +Antwort: + +```json +{ + "route": { + "id": 1, + "status": "deleted", + "gpxUrl": null, + "deletedAt": "2026-06-16 12:30:00" + }, + "softDeleted": true +} +``` + +Es werden keine Dateien endgültig entfernt. + +## 12. Route wiederherstellen + +### `POST /api/routes/:id/restore` + +Stellt eine als gelöscht markierte Route wieder her, verschiebt ihr Verzeichnis nach `storage/active/routes/` zurück und korrigiert alle GPX-, Bild- und Audiopfade. + +#### Pfadparameter + +| Parameter | Typ | Pflicht | Beschreibung | +|---|---|---:|---| +| `id` | Ganzzahl | ja | ID der gelöschten Route | + +Weitere Parameter: keine. Der Request besitzt keinen Body. + +```bash +curl -X POST http://127.0.0.1:47145/api/routes/1/restore +``` + +Antwort: + +```json +{ + "route": { + "id": 1, + "status": "active", + "gpxUrl": "/media/routes/1/route.gpx", + "deletedAt": null + }, + "restored": true +} +``` + +## 13. Medien abrufen + +Aktive Dateien werden nicht unter `/api`, sondern statisch unter `/media` ausgeliefert. Die benötigten URLs stehen in `gpxUrl`, `audioUrl` und `images[].url`. + +Parameter: keine zusätzlichen Query- oder Formularparameter. Der komplette Pfad stammt aus der jeweiligen API-Antwort. + +GPX-Datei: + +```bash +curl --output route.gpx \ + http://127.0.0.1:47145/media/routes/1/route.gpx +``` + +Audio: + +```bash +curl --output ansage.mp3 \ + http://127.0.0.1:47145/media/routes/1/audio/DATEINAME.mp3 +``` + +Bild: + +```bash +curl --output station.jpg \ + http://127.0.0.1:47145/media/routes/1/images/DATEINAME.jpg +``` + +Nur `storage/active` wird veröffentlicht. Dateien gelöschter Routen unter `storage/trash` sind nicht über HTTP erreichbar. + +## 14. Endpunktübersicht + +| Methode | Pfad | Zweck | +|---|---|---| +| `GET` | `/api/health` | Server und SQLite prüfen | +| `GET` | `/api/routes` | Routen auflisten und optional räumlich filtern | +| `GET` | `/api/routes/:id` | vollständige Route lesen | +| `GET` | `/api/routes/:id/pois` | POIs einer Route lesen | +| `GET` | `/api/pois/:id` | einzelnen POI lesen | +| `POST` | `/api/routes` | Route aus GPX anlegen | +| `PUT` | `/api/routes/:id` | Route aktualisieren oder GPX ersetzen | +| `POST` | `/api/routes/:id/append` | GPX-Punkte anhängen | +| `POST` | `/api/routes/:id/pois` | POI und Medien anlegen | +| `PUT` | `/api/pois/:id` | POI aktualisieren und Medien ergänzen | +| `DELETE` | `/api/routes/:id` | Route in den Papierkorb verschieben | +| `POST` | `/api/routes/:id/restore` | Route wiederherstellen | +| `GET` | `/media/routes/...` | aktive GPX-, Bild- und Audiodateien abrufen | diff --git a/public/css/app.css b/public/css/app.css index 9dbaa06..9c223d9 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -219,6 +219,19 @@ button, opacity: .65; } +#poi-list button[aria-current="step"] { + border-color: var(--primary); + background: #e7f1e9; + box-shadow: inset .35rem 0 0 var(--primary), 0 .12rem .35rem rgba(0, 0, 0, .08); +} + +#poi-list button[aria-current="step"] > strong::after { + content: " · aktuelle Station"; + color: var(--primary-dark); + font-size: .85rem; + font-weight: 600; +} + .route-list > li > p { margin: 0; padding: 1rem; diff --git a/public/index.html b/public/index.html index 834875e..f1e8bce 100644 --- a/public/index.html +++ b/public/index.html @@ -118,7 +118,7 @@

- + diff --git a/public/js/app.js b/public/js/app.js index cbcd903..8a81a57 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -9,6 +9,7 @@ routeState: 'idle', activePage: null, routeProgressIndex: null, + activePoiId: null, deviceHeading: null }; @@ -169,21 +170,36 @@ $('#stop-route').prop('disabled', state.routeState === 'idle'); } - function endRoute({ announce = true } = {}) { + function setActivePoi(poiId) { + state.activePoiId = poiId == null ? null : Number(poiId); + $('#poi-list [data-poi-id]').removeAttr('aria-current'); + if (state.activePoiId != null) { + $(`#poi-list [data-poi-id="${state.activePoiId}"]`).attr('aria-current', 'step'); + } + } + + function endRoute({ announce = true, returnToRoutes = false } = {}) { const wasActive = state.routeState !== 'idle'; state.routeState = 'idle'; state.routeProgressIndex = null; state.triggered.clear(); stopRouteSensors(); + ns.AudioPlayer.unload(); + setActivePoi(null); hideNavigation(); - if (announce && wasActive) { + if (announce && wasActive && !returnToRoutes) { $('#tracking-status').prop('hidden', false).text('Route beendet.'); } else { $('#tracking-status').prop('hidden', true).text(''); } updateRouteControls(); + + if (returnToRoutes) { + state.route = null; + navigate('#routes-page', { replace: true }); + } } function pauseRoute() { @@ -191,6 +207,7 @@ state.routeState = 'paused'; stopRouteSensors(); + ns.AudioPlayer.pause(); $('#tracking-status') .prop('hidden', false) .text('Route pausiert. Die Standortverfolgung ist angehalten.'); @@ -204,6 +221,7 @@ state.route = route; state.triggered.clear(); state.routeProgressIndex = null; + setActivePoi(null); $('#route-title').text(route.name); $('#route-description').text(route.description || ''); $('#route-distance').text(ns.Distance.format(route.distanceM)); @@ -231,6 +249,7 @@ } function showPoi(poi, automatic) { + setActivePoi(poi.id); $('#poi-title').text(poi.title); $('#poi-description').text(poi.description || ''); ns.Slideshow.show(poi.images); @@ -425,7 +444,7 @@ }); activate('#stop-route', function () { - endRoute(); + endRoute({ returnToRoutes: true }); }); window.addEventListener('popstate', event => { diff --git a/public/js/audio-player.js b/public/js/audio-player.js index 8e4bdaa..3fa6682 100644 --- a/public/js/audio-player.js +++ b/public/js/audio-player.js @@ -1,11 +1,39 @@ (function (ns, $) { 'use strict'; + + function element() { + return $('#poi-audio')[0]; + } + + function unload() { + const audio = element(); + audio.pause(); + audio.removeAttribute('src'); + audio.load(); + $(audio).prop('hidden', true); + } + ns.AudioPlayer = { load(url) { - const audio = $('#poi-audio'); - audio[0].pause(); audio.attr('src', url || '').toggle(Boolean(url)); - if (url) audio[0].load(); + unload(); + if (!url) return; + + const audio = element(); + audio.src = url; + $(audio).prop('hidden', false); + audio.load(); }, - play() { const audio = $('#poi-audio')[0]; if (audio.src) return audio.play(); } + + play() { + const audio = element(); + if (audio.getAttribute('src')) return audio.play(); + return Promise.resolve(); + }, + + pause() { + element().pause(); + }, + + unload }; }(window.Wegwichtel, window.jQuery));