Reworked pause and stop buttons and documentation

This commit is contained in:
Florian Zumpe 2026-06-16 17:01:53 +02:00
parent 4529749baa
commit 40f230864b
6 changed files with 710 additions and 47 deletions

View File

@ -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. 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 | 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.
|---|---|---|
| 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'
```
## Dateistruktur ## 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: 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 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 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, setzt den Fortschritt zurück und leert die Liste bereits ausgelöster POIs. - **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. 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.

636
docs/REST-API.md Normal file
View File

@ -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/<id>`
- 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/<id>`
- 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/<id>` 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 |

View File

@ -219,6 +219,19 @@ button,
opacity: .65; 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 { .route-list > li > p {
margin: 0; margin: 0;
padding: 1rem; padding: 1rem;

View File

@ -118,7 +118,7 @@
</div> </div>
<p id="poi-description"></p> <p id="poi-description"></p>
<audio id="poi-audio" controls preload="metadata"></audio> <audio id="poi-audio" controls preload="metadata" hidden></audio>
</div> </div>
</section> </section>

View File

@ -9,6 +9,7 @@
routeState: 'idle', routeState: 'idle',
activePage: null, activePage: null,
routeProgressIndex: null, routeProgressIndex: null,
activePoiId: null,
deviceHeading: null deviceHeading: null
}; };
@ -169,21 +170,36 @@
$('#stop-route').prop('disabled', state.routeState === 'idle'); $('#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'; const wasActive = state.routeState !== 'idle';
state.routeState = 'idle'; state.routeState = 'idle';
state.routeProgressIndex = null; state.routeProgressIndex = null;
state.triggered.clear(); state.triggered.clear();
stopRouteSensors(); stopRouteSensors();
ns.AudioPlayer.unload();
setActivePoi(null);
hideNavigation(); hideNavigation();
if (announce && wasActive) { if (announce && wasActive && !returnToRoutes) {
$('#tracking-status').prop('hidden', false).text('Route beendet.'); $('#tracking-status').prop('hidden', false).text('Route beendet.');
} else { } else {
$('#tracking-status').prop('hidden', true).text(''); $('#tracking-status').prop('hidden', true).text('');
} }
updateRouteControls(); updateRouteControls();
if (returnToRoutes) {
state.route = null;
navigate('#routes-page', { replace: true });
}
} }
function pauseRoute() { function pauseRoute() {
@ -191,6 +207,7 @@
state.routeState = 'paused'; state.routeState = 'paused';
stopRouteSensors(); stopRouteSensors();
ns.AudioPlayer.pause();
$('#tracking-status') $('#tracking-status')
.prop('hidden', false) .prop('hidden', false)
.text('Route pausiert. Die Standortverfolgung ist angehalten.'); .text('Route pausiert. Die Standortverfolgung ist angehalten.');
@ -204,6 +221,7 @@
state.route = route; state.route = route;
state.triggered.clear(); state.triggered.clear();
state.routeProgressIndex = null; state.routeProgressIndex = null;
setActivePoi(null);
$('#route-title').text(route.name); $('#route-title').text(route.name);
$('#route-description').text(route.description || ''); $('#route-description').text(route.description || '');
$('#route-distance').text(ns.Distance.format(route.distanceM)); $('#route-distance').text(ns.Distance.format(route.distanceM));
@ -231,6 +249,7 @@
} }
function showPoi(poi, automatic) { function showPoi(poi, automatic) {
setActivePoi(poi.id);
$('#poi-title').text(poi.title); $('#poi-title').text(poi.title);
$('#poi-description').text(poi.description || ''); $('#poi-description').text(poi.description || '');
ns.Slideshow.show(poi.images); ns.Slideshow.show(poi.images);
@ -425,7 +444,7 @@
}); });
activate('#stop-route', function () { activate('#stop-route', function () {
endRoute(); endRoute({ returnToRoutes: true });
}); });
window.addEventListener('popstate', event => { window.addEventListener('popstate', event => {

View File

@ -1,11 +1,39 @@
(function (ns, $) { (function (ns, $) {
'use strict'; '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 = { ns.AudioPlayer = {
load(url) { load(url) {
const audio = $('#poi-audio'); unload();
audio[0].pause(); audio.attr('src', url || '').toggle(Boolean(url)); if (!url) return;
if (url) audio[0].load();
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)); }(window.Wegwichtel, window.jQuery));