editor overhaul
This commit is contained in:
@@ -45,20 +45,21 @@ Die notwendigen Arbeiten stehen in
|
|||||||
|
|
||||||
Vor verbindlicher Verwendung sind insbesondere diese Platzhalter zu ersetzen:
|
Vor verbindlicher Verwendung sind insbesondere diese Platzhalter zu ersetzen:
|
||||||
|
|
||||||
- vollständiger Name und Anschrift des tatsächlichen Betreibers als Lizenznehmer;
|
- Anschrift von Florian Zumpe als Betreiber und Lizenznehmer;
|
||||||
- Rechtsgrundlagen, Empfänger, Hostinganbieter sowie Lösch- und Sicherungsfristen.
|
- Rechtsgrundlagen, Empfänger, Hostinganbieter sowie Lösch- und Sicherungsfristen.
|
||||||
|
|
||||||
Kontakt für Wegwichtel, Rechtefragen, Datenschutzanfragen, Widerruf und Entfernung:
|
Kontakt für Wegwichtel, Rechtefragen, Datenschutzanfragen, Widerruf und Entfernung:
|
||||||
[info@wegwichtel.de](mailto:info@wegwichtel.de). Die Adresse ist eingerichtet
|
[info@wegwichtel.de](mailto:info@wegwichtel.de). Die Adresse ist eingerichtet
|
||||||
und ersetzt die bisherigen Kontaktplatzhalter. Die Kontaktdaten der Schule
|
und ersetzt die bisherigen Kontaktplatzhalter.
|
||||||
und der Datenschutzstellen bleiben als Kontakte dieser Stellen erhalten.
|
|
||||||
|
|
||||||
Außerdem müssen die Texte und das Zustimmungsverfahren zum tatsächlichen
|
Seit dem 17. Oktober 2026 ist Florian Zumpe als Projektinhaber, Betreiber und
|
||||||
Betriebsmodell der Schule passen und rechtlich geprüft werden. Die bereits
|
Lizenznehmer für den Betrieb von Wegwichtel festgelegt. Er übernimmt Entwicklung,
|
||||||
vorhandenen Schul- und Behördenkontakte im Impressum wurden nicht verändert;
|
Betrieb und Pflege. Die Grundschule Herpf soll eine gesonderte Nutzungsfreigabe
|
||||||
die Kontaktadresse für Wegwichtel und die technische Verantwortung wurde ergänzt. Die frühere
|
erhalten. Die bisherigen Schul- und Behördenkontakte wurden als Betreiberangaben
|
||||||
pauschale Rechtezuordnung zur Schule wurde durch die Trennung von Urheberschaft
|
aus Impressum und Datenschutz entfernt. Die Anschrift von Florian Zumpe ist noch
|
||||||
und Nutzungsrechten ersetzt.
|
zu ergänzen. Die Rechte an fremden Beiträgen und Envato-Grafiken bleiben bei den
|
||||||
|
jeweiligen Berechtigten. Texte und Zustimmungsverfahren müssen zum tatsächlichen
|
||||||
|
Betriebsmodell passen; siehe [Projektverantwortung.md](Projektverantwortung.md).
|
||||||
|
|
||||||
## HTTP-Rechtehinweise
|
## HTTP-Rechtehinweise
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,32 @@ verlorenen HTTP-Erfolg nach serverseitigem POST ist zusätzlich eine spätere
|
|||||||
Idempotenzlösung erforderlich. Parallele Bearbeitung ist noch nicht freigegeben;
|
Idempotenzlösung erforderlich. Parallele Bearbeitung ist noch nicht freigegeben;
|
||||||
die aktuelle Übertragung behandelt den lokalen Entwurf als vollständigen Stand.
|
die aktuelle Übertragung behandelt den lokalen Entwurf als vollständigen Stand.
|
||||||
|
|
||||||
|
## Fehlende Server-Strecke, Papierkorb und lokaler Status
|
||||||
|
|
||||||
|
Beim Öffnen der Entwurfsliste werden lokale Strecken sofort angezeigt. Bereits
|
||||||
|
abgesandte Strecken werden im Hintergrund mit `includeDeleted=true` geprüft.
|
||||||
|
Liegt die Strecke im Papierkorb, wird lokal „Abgelehnt“ angezeigt. Existiert die
|
||||||
|
Server-ID auch mit dieser Abfrage nicht mehr (HTTP 404), erscheint „Entfernt“.
|
||||||
|
Der Status wird auf dem Gerät gespeichert und bleibt ohne Internet sichtbar.
|
||||||
|
Fehlende Berechtigung, Netzwerk- und Serverfehler verändern ihn nicht.
|
||||||
|
Eine erfolgreiche Abfrage nach manueller Wiederherstellung entfernt den alten
|
||||||
|
Ablehnungsstatus. Ungespeicherte Eingaben werden durch die Prüfung nicht ersetzt.
|
||||||
|
|
||||||
|
Beim bewussten erneuten Einsenden einer entfernten oder abgelehnten Strecke
|
||||||
|
werden alte Server-Zuordnungen für Strecke, Stationen, Bilder und Ton entfernt.
|
||||||
|
Der lokale Inhalt und seine Dateireferenzen bleiben erhalten. Eine neue Strecke
|
||||||
|
mit neuer ID und derselben Erstellerkennung wird zur Prüfung angelegt. Der alte
|
||||||
|
Papierkorb-Eintrag wird nicht wiederhergestellt oder verändert. Die neue Strecke
|
||||||
|
muss erneut freigegeben werden. Bestätigte neue IDs werden auch bei einem späteren
|
||||||
|
Übertragungsfehler gespeichert; weitere Versuche setzen dort fort.
|
||||||
|
|
||||||
|
Nach erfolgreicher neuer Einreichung wird der Status gelöscht und angezeigt:
|
||||||
|
„Die Strecke wurde neu angelegt und zur Prüfung gesendet.“ Eine vorhandene,
|
||||||
|
noch nicht freigegebene Strecke wird unter derselben ID aktualisiert und erhält
|
||||||
|
die normale Erfolgsmeldung. Freigegebene Strecken bleiben gegen Änderungen gesperrt.
|
||||||
|
Nur eine bestätigte 404-Antwort oder ein bestätigter Papierkorb-Status löst eine
|
||||||
|
neue Einreichung aus, keine Netzwerk-, Berechtigungs- oder anderen Serverfehler.
|
||||||
|
|
||||||
## Datenbank und Betrieb
|
## Datenbank und Betrieb
|
||||||
|
|
||||||
Das Schema und die Startprüfung enthalten die nullable Spalte
|
Das Schema und die Startprüfung enthalten die nullable Spalte
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ Die in der Anwendung enthaltenen Texte sind Vorlagen und ersetzen keine Prüfung
|
|||||||
|
|
||||||
## Betreiber und Impressum
|
## Betreiber und Impressum
|
||||||
|
|
||||||
- vollständiger Name und Rechtsform der Betreiberin
|
- Betreiber und Projektinhaber: Florian Zumpe (festgelegt); gegebenenfalls erforderliche weitere Angaben zum tatsächlichen Betriebsmodell prüfen
|
||||||
- vertretungsberechtigte Person
|
- Vertretungsangaben nur, falls für das tatsächliche Betriebsmodell erforderlich
|
||||||
- ladungsfähige Anschrift
|
- ladungsfähige Anschrift
|
||||||
- eingerichtete Kontaktadresse für Wegwichtel: info@wegwichtel.de
|
- eingerichtete Kontaktadresse für Wegwichtel: info@wegwichtel.de
|
||||||
- weiterer unmittelbarer Kontaktweg
|
- weiterer unmittelbarer Kontaktweg
|
||||||
@@ -14,7 +14,7 @@ Die in der Anwendung enthaltenen Texte sind Vorlagen und ersetzen keine Prüfung
|
|||||||
|
|
||||||
## Datenschutz
|
## Datenschutz
|
||||||
|
|
||||||
- Verantwortliche Stelle und Datenschutzkontakt
|
- Verantwortliche Stelle für den App-Betrieb: Florian Zumpe; Kontakt info@wegwichtel.de; Anschrift ergänzen
|
||||||
- Hostinganbieter und Ort der Verarbeitung
|
- Hostinganbieter und Ort der Verarbeitung
|
||||||
- Art und Dauer der Serverprotokollierung
|
- Art und Dauer der Serverprotokollierung
|
||||||
- Rechtsgrundlagen für Serverbetrieb und freiwillige Browser-Speicherung
|
- Rechtsgrundlagen für Serverbetrieb und freiwillige Browser-Speicherung
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Projektverantwortung
|
||||||
|
|
||||||
|
Projektinhaber und Betreiber von Wegwichtel ist Florian Zumpe. Er entwickelt
|
||||||
|
und erhält das Projekt und übernimmt Betrieb und Pflege. Kontakt:
|
||||||
|
info@wegwichtel.de. Seine ladungsfähige Anschrift ist in den statischen
|
||||||
|
Rechtstexten noch als Platzhalter eingetragen.
|
||||||
|
|
||||||
|
Die Staatliche Grundschule Herpf soll nach einer gesonderten Freigabe die App
|
||||||
|
nutzen können. Umfang, Dauer und Bedingungen dieser Freigabe sind noch
|
||||||
|
festzulegen. Aus dieser Absicht folgt keine Übertragung des Projekts auf die
|
||||||
|
Schule. Im Impressum und für den App-Betrieb steht Florian Zumpe.
|
||||||
|
|
||||||
|
Die Rechte an Beiträgen anderer Personen und an lizenzierten Grafiken bleiben
|
||||||
|
bei den jeweiligen Berechtigten. Die Betreiberangabe ersetzt keine Lizenz und
|
||||||
|
keinen Nachweis der Zustimmung. Die Texte von Impressum, Datenschutz und
|
||||||
|
Nutzungsvereinbarung bleiben statisch auf Deutsch in public/index.html.
|
||||||
@@ -163,3 +163,13 @@ Bestätigter Ausgangsstand am 5. Oktober 2026: Commit
|
|||||||
erfolgreich; Quality Gate OK; alle 121 Findings CLOSED/FIXED; beide Security
|
erfolgreich; Quality Gate OK; alle 121 Findings CLOSED/FIXED; beide Security
|
||||||
Hotspots REVIEWED/SAFE. Dieser Stand ist eine historische Referenz und ersetzt
|
Hotspots REVIEWED/SAFE. Dieser Stand ist eine historische Referenz und ersetzt
|
||||||
nicht die Prüfung einer neuen Analyse.
|
nicht die Prüfung einer neuen Analyse.
|
||||||
|
|
||||||
|
## Projektinhaber und Betreiber
|
||||||
|
|
||||||
|
Florian Zumpe ist Projektinhaber, Betreiber und Verantwortlicher für die
|
||||||
|
Entwicklung, den Betrieb und die Pflege von Wegwichtel. Die Grundschule Herpf
|
||||||
|
soll nach einer gesonderten Freigabe zur Nutzung berechtigt werden. Schule,
|
||||||
|
Schulleitung, Schulträger und Schulamt nicht als Betreiber der Website führen.
|
||||||
|
Die konkrete Nutzungsfreigabe für die Schule bleibt gesondert festzulegen.
|
||||||
|
Kontakt: info@wegwichtel.de. Die Anschrift ist noch zu ergänzen.
|
||||||
|
Rechte fremder Urheber und bestehende Lizenzen bleiben davon unberührt.
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ als offenen Platzhalter führen.
|
|||||||
|
|
||||||
## Vor verbindlicher Verwendung und Freigabe zu erledigen
|
## Vor verbindlicher Verwendung und Freigabe zu erledigen
|
||||||
|
|
||||||
1. Den tatsächlichen Lizenznehmer, Rechtsgrundlagen und Fristen
|
1. Lizenznehmer für den Betrieb von Wegwichtel ist Florian Zumpe. Seine
|
||||||
ergänzen. Den Entwurf und die Zustimmung für Kinder rechtlich prüfen lassen.
|
Anschrift, Rechtsgrundlagen und Fristen ergänzen. Den Entwurf und die Zustimmung für Kinder rechtlich prüfen lassen.
|
||||||
2. Die relevante Vereinbarung vor dem Einsenden zugänglich machen und eine
|
2. Die relevante Vereinbarung vor dem Einsenden zugänglich machen und eine
|
||||||
nicht vorausgewählte, ausdrückliche Zustimmung einholen. Eine bloße
|
nicht vorausgewählte, ausdrückliche Zustimmung einholen. Eine bloße
|
||||||
Datenschutzerklärung und eine zufällige Gerätekennung sind kein Rechtenachweis.
|
Datenschutzerklärung und eine zufällige Gerätekennung sind kein Rechtenachweis.
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ Noch offen für gemeinsame Bearbeitung:
|
|||||||
|
|
||||||
Die Ersteller-ID ersetzt keine Anmeldung. Der Moderationszugang ist separat
|
Die Ersteller-ID ersetzt keine Anmeldung. Der Moderationszugang ist separat
|
||||||
über Nginx und eine Passwortdatei geschützt (siehe [MODERATION-MEDIEN.md](../MODERATION-MEDIEN.md)).
|
über Nginx und eine Passwortdatei geschützt (siehe [MODERATION-MEDIEN.md](../MODERATION-MEDIEN.md)).
|
||||||
|
|
||||||
|
Zusätzlich vorgemerkt: [mögliche Streckenduplikate online prüfen](TODO-STRECKENDUPLIKATE.md).
|
||||||
|
Identische Dateien, ähnliche Wege und bewusst neue Einreichungen nach Ablehnung
|
||||||
|
oder Entfernung müssen getrennt beurteilt werden.
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# TODO: Mögliche Streckenduplikate in der Moderation prüfen
|
||||||
|
|
||||||
|
Am 5. Oktober 2026 vorgemerkt, noch nicht implementiert.
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Online mögliche Duplikate zur manuellen Moderation anzeigen. Erneute Einreichungen
|
||||||
|
nach Ablehnung oder Entfernung bleiben erlaubt. Ein Treffer darf nicht automatisch
|
||||||
|
eine Freigabe übernehmen, eine alte Strecke wiederherstellen oder einen Upload blockieren.
|
||||||
|
|
||||||
|
## Lösungsansätze
|
||||||
|
|
||||||
|
- SHA-256 der GPX-Datei für bytegenau identische Dateien berechnen. Änderungen an
|
||||||
|
XML-Schreibweise, Zeitstempeln oder Leerzeichen ändern bereits diese Prüfsumme.
|
||||||
|
- Zusätzlich eine stabile Darstellung der Routen- und Stationskoordinaten hashen.
|
||||||
|
XML-Schreibweise, Zeitstempel, Titel und interne IDs nicht als Wegidentität werten.
|
||||||
|
Reihenfolge, Rundung, fehlende Koordinaten und Stationsanker ausdrücklich definieren.
|
||||||
|
- Für GPS-Abweichungen einen geometrischen Ähnlichkeitsvergleich evaluieren.
|
||||||
|
Zunächst Kandidaten über die vorhandenen Min-/Max-Grenzen auswählen, dann
|
||||||
|
Wegverlauf, Start, Ende, Länge und Stationen mit festgelegter Toleranz vergleichen.
|
||||||
|
- Gleiche Wege und gleiche Inhalte unterscheiden: Derselbe Weg kann andere
|
||||||
|
Aufgaben, Bilder und Tonaufnahmen enthalten.
|
||||||
|
- Betroffene IDs, Status und Grund der Ähnlichkeit in der Moderation anzeigen.
|
||||||
|
Entscheiden, ob Papierkorb-Einträge in den Vergleich einbezogen werden.
|
||||||
|
- Fehlende GPS-Daten berücksichtigen; leere Dateien nicht pauschal als sinnvolle
|
||||||
|
Duplikate werten. Tests für identische Dateien, XML-/Zeitabweichungen, GPS-Rauschen,
|
||||||
|
Rückwege, andere Reihenfolge und Stationen sowie erneute Einreichungen vorsehen.
|
||||||
|
|
||||||
|
Die GPX-Datei bleibt Datenquelle; keine zweite vollständige Geometrie in SQLite
|
||||||
|
ablegen. Hashes sind Vergleichswerte, kein Berechtigungs- oder Urhebernachweis.
|
||||||
+14
-24
@@ -312,14 +312,10 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="info-dialog-content">
|
<div class="info-dialog-content">
|
||||||
<article class="info-page" data-info-page="privacy" lang="de" hidden>
|
<article class="info-page" data-info-page="privacy" lang="de" hidden>
|
||||||
<p class="legal-warning"><strong>Hinweis:</strong><span>Diese Angaben sind noch nicht vollständig. Die Kontakte stammen von der Grundschule Herpf. Wer die Website betreibt, welche Verbindungsdaten gespeichert werden und wie lange sie bleiben, muss noch ergänzt werden.</span></p>
|
<p class="legal-warning"><strong>Hinweis:</strong><span>Die Anschrift, Angaben zum Hosting und zur Speicherung von Verbindungsdaten müssen noch ergänzt werden.</span></p>
|
||||||
<h3>1. Verantwortliche Stelle und Datenschutzkontakt</h3>
|
<h3>1. Verantwortliche Stelle und Datenschutzkontakt</h3>
|
||||||
<p>
|
<p><strong>Florian Zumpe</strong><br><span>[Anschrift von Florian Zumpe ergänzen]</span><br><span>E-Mail: <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a></span></p>
|
||||||
<strong>Staatliche Grundschule Herpf</strong><br><span>Zum Eichig 3</span><br><span>98617 Meiningen / OT Herpf</span><br><span>Telefon: (036943) 63722</span><br><span>E-Mail: info@grundschule-herpf.de</span></p>
|
<p>Florian Zumpe entwickelt, betreibt und pflegt Wegwichtel. Fragen zum Datenschutz, Auskunfts- und Löschanfragen sowie Widerrufe kannst du an die Kontaktadresse oben senden.</p>
|
||||||
<p><span>Datenschutzangaben der Schule:</span><br>
|
|
||||||
<strong>Staatliches Schulamt Südthüringen</strong><br><span>Datenschutzbeauftragter</span><br><span>Hölderlinstraße 1</span><br><span>98527 Suhl</span><br><span>Telefon: 03681 / 7341 25</span><br><span>Fax: 03681 / 7341 09</span><br><span>E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de</span></p>
|
|
||||||
<p>Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden.</p>
|
|
||||||
<p>Für Fragen zu Wegwichtel, Datenschutzanfragen, Widerruf und Entfernung von Aufnahmen erreichst du die Betreiber unter <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a>.</p>
|
|
||||||
<h3>2. Wenn du Wegwichtel öffnest</h3>
|
<h3>2. Wenn du Wegwichtel öffnest</h3>
|
||||||
<p>Wenn du Wegwichtel öffnest, können Verbindungsdaten verarbeitet werden. Dazu gehören zum Beispiel die Internetadresse deines Geräts, Datum, Uhrzeit und Zeitzone, die aufgerufene Seite, übertragene Daten und ob der Aufruf geklappt hat. Auch die vorherige Seite und Angaben zu deinem Gerät, Internetbrowser und dessen Sprache können dazugehören. Diese Daten helfen dabei, die Website bereitzustellen, Fehler zu finden und sie zu schützen.</p>
|
<p>Wenn du Wegwichtel öffnest, können Verbindungsdaten verarbeitet werden. Dazu gehören zum Beispiel die Internetadresse deines Geräts, Datum, Uhrzeit und Zeitzone, die aufgerufene Seite, übertragene Daten und ob der Aufruf geklappt hat. Auch die vorherige Seite und Angaben zu deinem Gerät, Internetbrowser und dessen Sprache können dazugehören. Diese Daten helfen dabei, die Website bereitzustellen, Fehler zu finden und sie zu schützen.</p>
|
||||||
<p><span>Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:</span><strong>[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]</strong></p>
|
<p><span>Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:</span><strong>[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]</strong></p>
|
||||||
@@ -356,24 +352,18 @@
|
|||||||
<strong>Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)</strong><br><span>Postfach 90 04 55, 99107 Erfurt</span><br><span>Häßlerstraße 8, 99096 Erfurt</span><br><span>Telefon: 0361 / 57 311 29 00</span><br><span>Telefax: 0361 / 57 311 29 04</span><br><span>E-Mail: poststelle@datenschutz.thueringen.de</span><br><span>Website: www.tlfdi.de</span></p>
|
<strong>Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)</strong><br><span>Postfach 90 04 55, 99107 Erfurt</span><br><span>Häßlerstraße 8, 99096 Erfurt</span><br><span>Telefon: 0361 / 57 311 29 00</span><br><span>Telefax: 0361 / 57 311 29 04</span><br><span>E-Mail: poststelle@datenschutz.thueringen.de</span><br><span>Website: www.tlfdi.de</span></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="info-page" data-info-page="imprint" lang="de" hidden>
|
<article class="info-page" data-info-page="imprint" lang="de" hidden>
|
||||||
<p class="legal-warning"><strong>Hinweis:</strong><span>Diese Angaben stammen von der Grundschule Herpf. Vor der Veröffentlichung muss geprüft werden, ob für Wegwichtel noch weitere oder andere Angaben nötig sind.</span></p>
|
<p class="legal-warning"><strong>Hinweis:</strong><span>Die Anschrift und gegebenenfalls weitere Pflichtangaben müssen vor der Veröffentlichung ergänzt werden.</span></p>
|
||||||
<h3>Wer für Wegwichtel verantwortlich ist</h3>
|
<h3>Inhaber und Betreiber von Wegwichtel</h3>
|
||||||
<p>
|
<p><strong>Florian Zumpe</strong><br><span>[Anschrift von Florian Zumpe ergänzen]</span><br><span>E-Mail: <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a></span></p>
|
||||||
<strong>Staatliche Grundschule Herpf</strong><br><span>Zum Eichig 3</span><br><span>98617 Meiningen / OT Herpf</span></p>
|
<h3>Entwicklung und Pflege</h3>
|
||||||
<h3>Kontakt</h3>
|
<p>Florian Zumpe setzt das Projekt um und übernimmt den Betrieb und die weitere Pflege. Die Staatliche Grundschule Herpf soll die App nach einer gesonderten Nutzungsfreigabe verwenden können.</p>
|
||||||
<p><span>Telefon: (036943) 63722</span><br><span>Telefax: (036943) 63843</span><br><span>E-Mail: info@grundschule-herpf.de</span></p>
|
|
||||||
<p>Kontakt für Wegwichtel: <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a></p>
|
|
||||||
<h3>Leitung der Schule</h3>
|
|
||||||
<p>Frau Wichert, Schulleiterin</p>
|
|
||||||
<h3>Schulträger</h3>
|
|
||||||
<p>
|
|
||||||
<strong>Landkreis Schmalkalden-Meiningen</strong><br><span>Landratsamt</span><br><span>Obertshäuser Platz 1</span><br><span>98617 Meiningen</span></p>
|
|
||||||
<h3>Inhaltlich verantwortlich</h3>
|
<h3>Inhaltlich verantwortlich</h3>
|
||||||
<p>Frau Wichert</p>
|
<p>Florian Zumpe</p>
|
||||||
<h3>Technisch verantwortlich</h3>
|
<h3>Technisch verantwortlich</h3>
|
||||||
<p>Florian Zumpe<br><span>[Anschrift von Florian Zumpe ergänzen]</span><br><span>E-Mail: <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a></span></p>
|
<p>Florian Zumpe<br><span>[Anschrift von Florian Zumpe ergänzen]</span><br><span>E-Mail: <a href="mailto:info@wegwichtel.de">info@wegwichtel.de</a></span></p>
|
||||||
<h3>Weitere Pflichtangaben</h3>
|
<h3>Weitere Pflichtangaben</h3>
|
||||||
<p>Je nachdem, wer Wegwichtel betreibt, können weitere Angaben nötig sein. Dazu gehören zum Beispiel die Verantwortlichen für den Betrieb der Website. Diese Angaben müssen noch geprüft werden.</p>
|
<p><strong>[Weiteren unmittelbaren Kontaktweg ergänzen]</strong></p>
|
||||||
|
<p><strong>[Weitere für den tatsächlichen Betrieb erforderliche Pflichtangaben prüfen und gegebenenfalls ergänzen]</strong></p>
|
||||||
<h3>Haftung für Inhalte und Verweise</h3>
|
<h3>Haftung für Inhalte und Verweise</h3>
|
||||||
<p>Die Inhalte werden sorgfältig erstellt und regelmäßig geprüft. Falls etwas falsch, veraltet oder nicht erlaubt ist, melde dich beim Kontakt oben. Für andere verlinkte Seiten sind deren Betreiber verantwortlich.</p>
|
<p>Die Inhalte werden sorgfältig erstellt und regelmäßig geprüft. Falls etwas falsch, veraltet oder nicht erlaubt ist, melde dich beim Kontakt oben. Für andere verlinkte Seiten sind deren Betreiber verantwortlich.</p>
|
||||||
<h3>Bildquelle der Gruppensymbole</h3>
|
<h3>Bildquelle der Gruppensymbole</h3>
|
||||||
@@ -382,10 +372,10 @@
|
|||||||
<p>Die Urheberrechte an Bildern, Tonaufnahmen und anderen Beiträgen bleiben bei den jeweiligen Urhebern. Die Betreiber nutzen eingesandte Aufnahmen im Rahmen der vereinbarten Nutzungsrechte. Die Veröffentlichung erlaubt anderen Personen keine freie Weiterverwendung. Gesetzlich erlaubte Nutzungen und ausdrücklich erteilte Lizenzen bleiben unberührt.</p>
|
<p>Die Urheberrechte an Bildern, Tonaufnahmen und anderen Beiträgen bleiben bei den jeweiligen Urhebern. Die Betreiber nutzen eingesandte Aufnahmen im Rahmen der vereinbarten Nutzungsrechte. Die Veröffentlichung erlaubt anderen Personen keine freie Weiterverwendung. Gesetzlich erlaubte Nutzungen und ausdrücklich erteilte Lizenzen bleiben unberührt.</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="info-page" data-info-page="terms" lang="de" hidden>
|
<article class="info-page" data-info-page="terms" lang="de" hidden>
|
||||||
<p class="legal-warning"><strong>Entwurf:</strong> Diese Vereinbarung muss vor ihrer verbindlichen Verwendung rechtlich geprüft werden. Betreiber und das Verfahren zur Zustimmung und Urhebernennung sind noch zu ergänzen.</p>
|
<p class="legal-warning"><strong>Entwurf:</strong> Diese Vereinbarung muss vor ihrer verbindlichen Verwendung rechtlich geprüft werden. Die Anschrift des Betreibers und das Verfahren zur Zustimmung und Urhebernennung sind noch zu ergänzen.</p>
|
||||||
<p>Stand: 4. Oktober 2026</p>
|
<p>Stand: 17. Oktober 2026</p>
|
||||||
<h3>1. Für wen diese Vereinbarung gilt</h3>
|
<h3>1. Für wen diese Vereinbarung gilt</h3>
|
||||||
<p>Diese Vereinbarung regelt das Einsenden von Bildern und Tonaufnahmen an Wegwichtel. Nutzungsrechte werden dem Betreiber <strong>[vollständiger Name und Anschrift des tatsächlichen Betreibers ergänzen]</strong> eingeräumt. Die bloße Nutzung der App oder das Speichern eines Entwurfs auf deinem Gerät räumt keine Rechte ein.</p>
|
<p>Diese Vereinbarung regelt das Einsenden von Bildern und Tonaufnahmen an Wegwichtel. Nutzungsrechte werden dem Betreiber <strong>Florian Zumpe, [Anschrift von Florian Zumpe ergänzen]</strong> eingeräumt. Die bloße Nutzung der App oder das Speichern eines Entwurfs auf deinem Gerät räumt keine Rechte ein.</p>
|
||||||
<h3>2. Erlaubnis für Bilder und Tonaufnahmen</h3>
|
<h3>2. Erlaubnis für Bilder und Tonaufnahmen</h3>
|
||||||
<p>Mit dem Hochladen deiner Bilder und Tonaufnahmen unter Zustimmung zu dieser Nutzungsvereinbarung räumst du dem Betreiber ein einfaches, nicht ausschließliches, räumlich unbeschränktes Nutzungsrecht für den Betrieb von Wegwichtel ein. Es umfasst das Speichern und Vervielfältigen, die redaktionelle Prüfung sowie nach Freigabe das öffentliche Zugänglichmachen und Wiedergeben im Zusammenhang mit den Strecken der App. Dazu gehören die Anzeige von Bildern, das Abspielen von Tonaufnahmen sowie die von der App angebotene Speicherung ausgewählter Strecken auf Geräten für die Nutzung ohne Internet.</p>
|
<p>Mit dem Hochladen deiner Bilder und Tonaufnahmen unter Zustimmung zu dieser Nutzungsvereinbarung räumst du dem Betreiber ein einfaches, nicht ausschließliches, räumlich unbeschränktes Nutzungsrecht für den Betrieb von Wegwichtel ein. Es umfasst das Speichern und Vervielfältigen, die redaktionelle Prüfung sowie nach Freigabe das öffentliche Zugänglichmachen und Wiedergeben im Zusammenhang mit den Strecken der App. Dazu gehören die Anzeige von Bildern, das Abspielen von Tonaufnahmen sowie die von der App angebotene Speicherung ausgewählter Strecken auf Geräten für die Nutzung ohne Internet.</p>
|
||||||
<p>Der Betreiber darf die dafür notwendigen technischen Anpassungen vornehmen, etwa Dateiformate umwandeln, Dateien komprimieren und Bildgrößen anpassen. Inhaltliche Veränderungen oder entstellende Bearbeitungen sind dadurch nicht erlaubt. Beauftragte Hosting- und technische Dienstleister dürfen die Aufnahmen nur soweit verwenden, wie dies zur Bereitstellung von Wegwichtel erforderlich ist. Besucher erhalten nur die für die Nutzung innerhalb der App und ihrer vorgesehenen Offline-Funktion erforderliche Erlaubnis.</p>
|
<p>Der Betreiber darf die dafür notwendigen technischen Anpassungen vornehmen, etwa Dateiformate umwandeln, Dateien komprimieren und Bildgrößen anpassen. Inhaltliche Veränderungen oder entstellende Bearbeitungen sind dadurch nicht erlaubt. Beauftragte Hosting- und technische Dienstleister dürfen die Aufnahmen nur soweit verwenden, wie dies zur Bereitstellung von Wegwichtel erforderlich ist. Besucher erhalten nur die für die Nutzung innerhalb der App und ihrer vorgesehenen Offline-Funktion erforderliche Erlaubnis.</p>
|
||||||
|
|||||||
+78
-16
@@ -368,6 +368,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
const label = document.createElement('div');
|
const label = document.createElement('div');
|
||||||
appendText(label, 'strong', routeName(route));
|
appendText(label, 'strong', routeName(route));
|
||||||
appendText(label, 'small', routeSource(route) === 'local' ? ns.I18n.t("app.draft") : ns.I18n.t("editor.published-route"));
|
appendText(label, 'small', routeSource(route) === 'local' ? ns.I18n.t("app.draft") : ns.I18n.t("editor.published-route"));
|
||||||
|
if (routeSource(route) === 'local' && route.submissionStatus) {
|
||||||
|
appendText(label, 'small', ns.I18n.t(`editor.${route.submissionStatus}`));
|
||||||
|
}
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = classes.iconRow;
|
actions.className = classes.iconRow;
|
||||||
@@ -458,6 +461,36 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
setStatus('');
|
setStatus('');
|
||||||
// Local drafts are usable even while this request is waiting for a connection.
|
// Local drafts are usable even while this request is waiting for a connection.
|
||||||
void refreshPublicRoutes(requestGeneration);
|
void refreshPublicRoutes(requestGeneration);
|
||||||
|
void refreshSubmittedRoutes(requestGeneration);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSubmittedRoutes(requestGeneration) {
|
||||||
|
const submitted = localRouteSummaries().filter(route => route.serverRouteId);
|
||||||
|
await Promise.allSettled(submitted.map(async route => {
|
||||||
|
const existing = await findSubmittedRoute(route.serverRouteId);
|
||||||
|
if (requestGeneration !== routeListGeneration) return;
|
||||||
|
const latest = ns.LocalRoutes.get(route.id);
|
||||||
|
if (!latest || String(latest.route.serverRouteId) !== String(route.serverRouteId)) return;
|
||||||
|
const submissionStatus = submittedStatus(existing);
|
||||||
|
if ((latest.route.submissionStatus || null) === submissionStatus) return;
|
||||||
|
latest.route.submissionStatus = submissionStatus;
|
||||||
|
const saved = ns.LocalRoutes.save(latest);
|
||||||
|
if (state.currentSource === 'local' && routeId(state.currentRoute) === route.id) {
|
||||||
|
state.currentRoute.submissionStatus = submissionStatus;
|
||||||
|
renderLocalSubmissionStatus(saved.route);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
if (requestGeneration !== routeListGeneration) return;
|
||||||
|
state.routes = [...localRouteSummaries(), ...state.routes.filter(route => routeSource(route) === 'server')];
|
||||||
|
renderRouteList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLocalSubmissionStatus(route) {
|
||||||
|
const uploaded = Boolean(route?.serverRouteId);
|
||||||
|
const uploadButton = byId(ids.routeUpload);
|
||||||
|
if (uploadButton) uploadButton.textContent = ns.I18n.t(uploaded ? 'editor.submitted' : 'editor.submit');
|
||||||
|
const hint = uploaded ? 'editor.submitted-hint' : 'editor.draft-hint';
|
||||||
|
byId(ids.routeActivated).textContent = ns.I18n.t(route?.submissionStatus ? `editor.${route.submissionStatus}-hint` : hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshPublicRoutes(requestGeneration) {
|
async function refreshPublicRoutes(requestGeneration) {
|
||||||
@@ -532,13 +565,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
|
|
||||||
const uploadButton = byId(ids.routeUpload);
|
const uploadButton = byId(ids.routeUpload);
|
||||||
if (state.currentSource === 'local') {
|
if (state.currentSource === 'local') {
|
||||||
const uploaded = Boolean(route?.serverRouteId);
|
|
||||||
setHidden(uploadButton, false);
|
setHidden(uploadButton, false);
|
||||||
setDisabled(uploadButton, false);
|
setDisabled(uploadButton, false);
|
||||||
if (uploadButton) uploadButton.textContent = uploaded ? ns.I18n.t("editor.submitted") : ns.I18n.t("editor.submit");
|
renderLocalSubmissionStatus(route);
|
||||||
byId(ids.routeActivated).textContent = uploaded
|
|
||||||
? ns.I18n.t("editor.submitted-hint")
|
|
||||||
: ns.I18n.t("editor.draft-hint");
|
|
||||||
} else {
|
} else {
|
||||||
setHidden(uploadButton, true);
|
setHidden(uploadButton, true);
|
||||||
setDisabled(uploadButton, true);
|
setDisabled(uploadButton, true);
|
||||||
@@ -946,23 +975,56 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
return ns.LocalRoutes.save(routePackage);
|
return ns.LocalRoutes.save(routePackage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submittedStatus(existing) {
|
||||||
|
if (!existing) return 'removed';
|
||||||
|
return existing.status === 'deleted' ? 'rejected' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findSubmittedRoute(id) {
|
||||||
|
try {
|
||||||
|
const payload = await readJson(`/routes/${encodeURIComponent(id)}?includeDeleted=true`);
|
||||||
|
return payload.route || payload;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) return null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSubmittedReferences(routePackage) {
|
||||||
|
routePackage.route.serverRouteId = null;
|
||||||
|
routePackage.route.uploadedAt = null;
|
||||||
|
for (const poi of routePackage.pois || []) {
|
||||||
|
delete poi.serverPoiId;
|
||||||
|
delete poi.serverAudio;
|
||||||
|
for (const image of poi.images || []) delete image.serverPictureId;
|
||||||
|
}
|
||||||
|
checkpointLocalUpload(routePackage);
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareServerRoute(routePackage) {
|
async function prepareServerRoute(routePackage) {
|
||||||
const id = routePackage.route.serverRouteId;
|
const id = routePackage.route.serverRouteId;
|
||||||
|
const replacedSubmission = Boolean(id || routePackage.route.submissionStatus);
|
||||||
if (id) {
|
if (id) {
|
||||||
const payload = await readJson(`/routes/${encodeURIComponent(id)}`);
|
const existing = await findSubmittedRoute(id);
|
||||||
const existing = payload.route || payload;
|
if (!existing || existing.status === 'deleted') {
|
||||||
if (existing.activatedAt || existing.activated_at) {
|
// Preserve the old trash entry; submit a new draft with fresh references.
|
||||||
throw new Error(ns.I18n.t("editor.read-only-hint"));
|
routePackage.route.submissionStatus = submittedStatus(existing);
|
||||||
|
clearSubmittedReferences(routePackage);
|
||||||
|
} else {
|
||||||
|
if (existing.activatedAt || existing.activated_at) {
|
||||||
|
throw new Error(ns.I18n.t("editor.read-only-hint"));
|
||||||
|
}
|
||||||
|
await sendForm(`/routes/${encodeURIComponent(id)}`, 'PUT', localRouteUploadForm(routePackage));
|
||||||
|
return { id, previousPois: existing.pois || [] };
|
||||||
}
|
}
|
||||||
await sendForm(`/routes/${encodeURIComponent(id)}`, 'PUT', localRouteUploadForm(routePackage));
|
|
||||||
return { id, previousPois: existing.pois || [] };
|
|
||||||
}
|
}
|
||||||
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
|
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
|
||||||
const createdId = routeId(payload.route || payload);
|
const createdId = routeId(payload.route || payload);
|
||||||
if (!createdId) throw new Error(ns.I18n.t("editor.error.send-route"));
|
if (!createdId) throw new Error(ns.I18n.t("editor.error.send-route"));
|
||||||
routePackage.route.serverRouteId = createdId;
|
routePackage.route.serverRouteId = createdId;
|
||||||
|
routePackage.route.submissionStatus = null;
|
||||||
checkpointLocalUpload(routePackage);
|
checkpointLocalUpload(routePackage);
|
||||||
return { id: createdId, previousPois: [] };
|
return { id: createdId, previousPois: [], recreated: replacedSubmission };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncLocalPoi(base, poi, index, routePackage) {
|
async function syncLocalPoi(base, poi, index, routePackage) {
|
||||||
@@ -1011,7 +1073,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createServerRouteFromLocal(routePackage) {
|
async function createServerRouteFromLocal(routePackage) {
|
||||||
const { id, previousPois } = await prepareServerRoute(routePackage);
|
const { id, previousPois, recreated } = await prepareServerRoute(routePackage);
|
||||||
const base = `/routes/${encodeURIComponent(id)}/pois`;
|
const base = `/routes/${encodeURIComponent(id)}/pois`;
|
||||||
const orderedPois = [...(routePackage.pois || [])]
|
const orderedPois = [...(routePackage.pois || [])]
|
||||||
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
|
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
|
||||||
@@ -1023,7 +1085,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
await syncLocalPictures(mediaBase, poi, oldPoi, routePackage);
|
await syncLocalPictures(mediaBase, poi, oldPoi, routePackage);
|
||||||
}
|
}
|
||||||
await removeMissingServerItems(previousPois, orderedPois, 'serverPoiId', base);
|
await removeMissingServerItems(previousPois, orderedPois, 'serverPoiId', base);
|
||||||
return id;
|
return { id, recreated };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadLocalRoute() {
|
async function uploadLocalRoute() {
|
||||||
@@ -1039,7 +1101,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
const routePackage = ns.LocalRoutes.get(localId);
|
const routePackage = ns.LocalRoutes.get(localId);
|
||||||
if (!routePackage) throw new Error(ns.I18n.t("editor.error.draft-missing"));
|
if (!routePackage) throw new Error(ns.I18n.t("editor.error.draft-missing"));
|
||||||
|
|
||||||
const serverRouteId = await createServerRouteFromLocal(routePackage);
|
const { id: serverRouteId, recreated } = await createServerRouteFromLocal(routePackage);
|
||||||
const uploadedAt = new Date().toISOString();
|
const uploadedAt = new Date().toISOString();
|
||||||
const saved = ns.LocalRoutes.save({
|
const saved = ns.LocalRoutes.save({
|
||||||
...routePackage,
|
...routePackage,
|
||||||
@@ -1053,7 +1115,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
state.pois = saved.pois;
|
state.pois = saved.pois;
|
||||||
fillRouteForm(state.currentRoute);
|
fillRouteForm(state.currentRoute);
|
||||||
await loadRoutes();
|
await loadRoutes();
|
||||||
setStatus(ns.I18n.t("editor.submit-success"));
|
setStatus(ns.I18n.t(recreated ? 'editor.resubmit-success' : 'editor.submit-success'));
|
||||||
} finally {
|
} finally {
|
||||||
// Keep acknowledged server IDs even if a later media request failed.
|
// Keep acknowledged server IDs even if a later media request failed.
|
||||||
const latest = ns.LocalRoutes.get(routeId(state.currentRoute));
|
const latest = ns.LocalRoutes.get(routeId(state.currentRoute));
|
||||||
|
|||||||
@@ -556,6 +556,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
|
|||||||
createdAt: inputRoute.createdAt || now,
|
createdAt: inputRoute.createdAt || now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
serverRouteId: inputRoute.serverRouteId ?? null,
|
serverRouteId: inputRoute.serverRouteId ?? null,
|
||||||
|
submissionStatus: ['rejected', 'removed'].includes(inputRoute.submissionStatus) ? inputRoute.submissionStatus : null,
|
||||||
uploadedAt: inputRoute.uploadedAt || null
|
uploadedAt: inputRoute.uploadedAt || null
|
||||||
};
|
};
|
||||||
const pois = (routePackage.pois || []).map((poi, index) => ({
|
const pois = (routePackage.pois || []).map((poi, index) => ({
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ globalThis.Wegwichtel ??= {};
|
|||||||
"rights-hint": "Wenn du Bilder oder Tonaufnahmen sendest, erlaubst du Wegwichtel, sie nach der Nutzungsvereinbarung zu verwenden. Sende nur Aufnahmen, für die du diese Erlaubnis geben darfst. Kinder brauchen die Zustimmung ihrer Sorgeberechtigten.",
|
"rights-hint": "Wenn du Bilder oder Tonaufnahmen sendest, erlaubst du Wegwichtel, sie nach der Nutzungsvereinbarung zu verwenden. Sende nur Aufnahmen, für die du diese Erlaubnis geben darfst. Kinder brauchen die Zustimmung ihrer Sorgeberechtigten.",
|
||||||
"rights-open": "Vereinbarung für Bilder und Ton lesen",
|
"rights-open": "Vereinbarung für Bilder und Ton lesen",
|
||||||
"submit": "Zur Prüfung senden",
|
"submit": "Zur Prüfung senden",
|
||||||
|
"rejected": "Abgelehnt",
|
||||||
|
"rejected-hint": "Diese Strecke wurde abgelehnt. Du kannst deinen Entwurf überarbeiten und erneut zur Prüfung senden.",
|
||||||
|
"removed": "Entfernt",
|
||||||
|
"removed-hint": "Diese Strecke ist nicht mehr online. Du kannst deinen Entwurf erneut zur Prüfung senden.",
|
||||||
|
"resubmit-success": "Die Strecke wurde neu angelegt und zur Prüfung gesendet.",
|
||||||
"submitted-hint": "Du kannst Änderungen erneut zur Prüfung senden, solange die Strecke noch nicht freigegeben wurde.",
|
"submitted-hint": "Du kannst Änderungen erneut zur Prüfung senden, solange die Strecke noch nicht freigegeben wurde.",
|
||||||
"draft-hint": "Dieser Entwurf bleibt auf deinem Gerät. Mit „Zur Prüfung senden“ reichst du ihn zur Freigabe ein.",
|
"draft-hint": "Dieser Entwurf bleibt auf deinem Gerät. Mit „Zur Prüfung senden“ reichst du ihn zur Freigabe ein.",
|
||||||
"read-only-hint": "Diese Strecke ist für alle freigegeben und kann hier nicht mehr verändert werden.",
|
"read-only-hint": "Diese Strecke ist für alle freigegeben und kann hier nicht mehr verändert werden.",
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ globalThis.Wegwichtel ??= {};
|
|||||||
"rights-hint": "By sending pictures or recordings, you allow Wegwichtel to use them under its terms of use. Only send recordings you are allowed to share this way. Children need their parent or guardian’s permission.",
|
"rights-hint": "By sending pictures or recordings, you allow Wegwichtel to use them under its terms of use. Only send recordings you are allowed to share this way. Children need their parent or guardian’s permission.",
|
||||||
"rights-open": "Read the agreement for pictures and recordings",
|
"rights-open": "Read the agreement for pictures and recordings",
|
||||||
"submit": "Send for review",
|
"submit": "Send for review",
|
||||||
|
"rejected": "Not approved",
|
||||||
|
"rejected-hint": "This trail was not approved. You can improve your draft and send it for review again.",
|
||||||
|
"removed": "Removed",
|
||||||
|
"removed-hint": "This trail is no longer online. You can send your draft for review again.",
|
||||||
|
"resubmit-success": "A new trail was created and sent for review.",
|
||||||
"submitted-hint": "You can send changes for review until the route has been approved.",
|
"submitted-hint": "You can send changes for review until the route has been approved.",
|
||||||
"draft-hint": "This draft stays on your device. Choose “Send for review” to submit it for approval.",
|
"draft-hint": "This draft stays on your device. Choose “Send for review” to submit it for approval.",
|
||||||
"read-only-hint": "This trail is available to everyone and can no longer be changed here.",
|
"read-only-hint": "This trail is available to everyone and can no longer be changed here.",
|
||||||
|
|||||||
@@ -535,6 +535,8 @@ test('Express API router executes route, POI, file and restore handlers in-proce
|
|||||||
|
|
||||||
const deleted = await json(`/api/routes/${routeId}`, { method: 'DELETE' });
|
const deleted = await json(`/api/routes/${routeId}`, { method: 'DELETE' });
|
||||||
assert.equal(deleted.body.softDeleted, true);
|
assert.equal(deleted.body.softDeleted, true);
|
||||||
|
await json(`/api/routes/${routeId}`, {}, 404);
|
||||||
|
assert.equal((await json(`/api/routes/${routeId}?includeDeleted=true`)).body.status, 'deleted');
|
||||||
const restored = await json(`/api/routes/${routeId}/restore`, { method: 'POST' });
|
const restored = await json(`/api/routes/${routeId}/restore`, { method: 'POST' });
|
||||||
assert.equal(restored.body.restored, true);
|
assert.equal(restored.body.restored, true);
|
||||||
|
|
||||||
|
|||||||
+128
-1
@@ -268,7 +268,7 @@ function buildEditorContext() {
|
|||||||
if (url === '/api/routes' && method === 'POST') return jsonResponse({ id: 22, name: 'Neue lokale Strecke', pois: [] }, true, 201);
|
if (url === '/api/routes' && method === 'POST') return jsonResponse({ id: 22, name: 'Neue lokale Strecke', pois: [] }, true, 201);
|
||||||
if (url === '/api/routes/22/pois' && method === 'POST') return jsonResponse({ id: 23, title: 'Aussicht' }, true, 201);
|
if (url === '/api/routes/22/pois' && method === 'POST') return jsonResponse({ id: 23, title: 'Aussicht' }, true, 201);
|
||||||
if (/^\/api\/routes\/22\/pois\/23\/(audio|pictures)(?:\/24)?$/.test(url) && (method === 'POST' || method === 'PUT')) return jsonResponse({ id: 24 }, true, 201);
|
if (/^\/api\/routes\/22\/pois\/23\/(audio|pictures)(?:\/24)?$/.test(url) && (method === 'POST' || method === 'PUT')) return jsonResponse({ id: 24 }, true, 201);
|
||||||
if (url === '/api/routes/22' && method === 'GET') return jsonResponse({ id: 22, activatedAt: controls.approved ? '2026-10-03' : null, pois: [{ id: 23, audioUrl: '/audio', images: [{ id: 24 }] }] });
|
if (url === '/api/routes/22?includeDeleted=true' && method === 'GET') return jsonResponse({ id: 22, activatedAt: controls.approved ? '2026-10-03' : null, pois: [{ id: 23, audioUrl: '/audio', images: [{ id: 24 }] }] });
|
||||||
if (url === '/api/routes/22' && method === 'PUT') return jsonResponse({ id: 22 });
|
if (url === '/api/routes/22' && method === 'PUT') return jsonResponse({ id: 22 });
|
||||||
if (url === '/api/routes/22/pois/23' && method === 'PUT') return jsonResponse({ id: 23 });
|
if (url === '/api/routes/22/pois/23' && method === 'PUT') return jsonResponse({ id: 23 });
|
||||||
if (/^\/api\/routes\/22\/pois\/23(?:\/audio|\/pictures\/24)?$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
if (/^\/api\/routes\/22\/pois\/23(?:\/audio|\/pictures\/24)?$/.test(url) && method === 'DELETE') return jsonResponse({ deleted: true });
|
||||||
@@ -389,6 +389,133 @@ function buildEditorContext() {
|
|||||||
return { calls, context, elements, packages, saves, storedMediaCalls, controls };
|
return { calls, context, elements, packages, saves, storedMediaCalls, controls };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submittedEditor(response) {
|
||||||
|
const harness = buildEditorContext();
|
||||||
|
const { context, packages, calls, elements } = harness;
|
||||||
|
packages.set('local-existing', {
|
||||||
|
id: 'local-existing', gpx: '<gpx/>',
|
||||||
|
route: { id: 'local-existing', name: 'Waldweg', creator_id: 'a'.repeat(64),
|
||||||
|
serverRouteId: '99', uploadedAt: '2026-10-01T10:00:00Z' },
|
||||||
|
pois: [{ id: 7, title: 'Aussicht', description: 'Wiese', sequence: 0,
|
||||||
|
lat: 50, lon: 10, serverPoiId: 98, serverAudio: true,
|
||||||
|
audio: { name: 'ton.mp3', mediaKey: 'audio-key', type: 'audio/mpeg' },
|
||||||
|
images: [{ id: 'local-image', name: 'bild.png', mediaKey: 'picture-key',
|
||||||
|
type: 'image/png', serverPictureId: 97 }] }]
|
||||||
|
});
|
||||||
|
const originalFetch = context.fetch;
|
||||||
|
context.fetch = async (url, options = {}) => {
|
||||||
|
if (url === '/api/routes/99?includeDeleted=true') {
|
||||||
|
calls.push({ url, method: 'GET' });
|
||||||
|
return response();
|
||||||
|
}
|
||||||
|
return originalFetch(url, options);
|
||||||
|
};
|
||||||
|
await loadI18n(context);
|
||||||
|
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||||
|
vm.runInNewContext(await fs.readFile(sourcePath, 'utf8'), context, { filename: sourcePath });
|
||||||
|
await context.Wegwichtel.EditorUi.open();
|
||||||
|
for (let i = 0; i < 4; i += 1) await tick();
|
||||||
|
const list = elements.get('editor-route-list');
|
||||||
|
const item = list.children.find(child => child.dataset.routeId === 'local-existing');
|
||||||
|
list.dispatchEvent(pointerEvent(item.children[1].children[0]));
|
||||||
|
for (let i = 0; i < 4; i += 1) await tick();
|
||||||
|
return harness;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDraft(harness) {
|
||||||
|
harness.elements.get('editor-route-upload').dispatchEvent(pointerEvent());
|
||||||
|
for (let i = 0; i < 12; i += 1) await tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('removed and rejected submissions show their saved status and create fresh route and media IDs', async t => {
|
||||||
|
for (const status of ['removed', 'rejected']) {
|
||||||
|
await t.test(status, async () => {
|
||||||
|
const response = status === 'removed'
|
||||||
|
? () => jsonResponse({ message: 'Strecke nicht gefunden.' }, false, 404)
|
||||||
|
: () => jsonResponse({ id: 99, status: 'deleted', activatedAt: '2026-10-01', pois: [] });
|
||||||
|
const harness = await submittedEditor(response);
|
||||||
|
const { calls, packages, elements } = harness;
|
||||||
|
assert.equal(packages.get('local-existing').route.submissionStatus, status);
|
||||||
|
const expectedLabel = status === 'removed' ? 'Entfernt' : 'Abgelehnt';
|
||||||
|
const item = elements.get('editor-route-list').children.find(child => child.dataset.routeId === 'local-existing');
|
||||||
|
assert.ok(item.children[0].children.some(child => child.textContent === expectedLabel));
|
||||||
|
assert.match(elements.get('editor-route-activated').textContent, status === 'removed' ? /nicht mehr online/ : /abgelehnt/);
|
||||||
|
await submitDraft(harness);
|
||||||
|
const saved = packages.get('local-existing');
|
||||||
|
assert.equal(saved.route.serverRouteId, '22');
|
||||||
|
assert.equal(saved.route.submissionStatus, null);
|
||||||
|
assert.equal(saved.route.creator_id, 'a'.repeat(64));
|
||||||
|
assert.equal(saved.pois[0].serverPoiId, '23');
|
||||||
|
assert.equal(saved.pois[0].images[0].serverPictureId, 24);
|
||||||
|
assert.equal(saved.pois[0].images[0].mediaKey, 'picture-key');
|
||||||
|
assert.equal(saved.pois[0].audio.mediaKey, 'audio-key');
|
||||||
|
assert.equal(elements.get('editor-status').textContent, 'Die Strecke wurde neu angelegt und zur Prüfung gesendet.');
|
||||||
|
assert.deepEqual(calls.filter(call => call.method === 'POST').map(call => call.url), [
|
||||||
|
'/api/routes', '/api/routes/22/pois', '/api/routes/22/pois/23/audio', '/api/routes/22/pois/23/pictures'
|
||||||
|
]);
|
||||||
|
assert.equal(calls.some(call => call.method === 'PUT' || call.method === 'DELETE' || call.url.includes('/restore')), false);
|
||||||
|
await submitDraft(harness);
|
||||||
|
assert.equal(calls.filter(call => call.method === 'POST' && call.url === '/api/routes').length, 1);
|
||||||
|
assert.ok(calls.some(call => call.method === 'PUT' && call.url === '/api/routes/22'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submission status refresh preserves unsaved form text and clears rejection after restoration', async () => {
|
||||||
|
let status = 'active';
|
||||||
|
const harness = await submittedEditor(() => jsonResponse({ id: 99, status }));
|
||||||
|
harness.elements.get('editor-route-name').value = 'Noch nicht gespeichert';
|
||||||
|
for (const serverStatus of ['deleted', 'active']) {
|
||||||
|
status = serverStatus;
|
||||||
|
await harness.context.Wegwichtel.EditorUi.refresh();
|
||||||
|
for (let i = 0; i < 4; i += 1) await tick();
|
||||||
|
assert.equal(harness.packages.get('local-existing').route.submissionStatus, status === 'deleted' ? 'rejected' : null);
|
||||||
|
assert.equal(harness.elements.get('editor-route-name').value, 'Noch nicht gespeichert');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('authorization, server and network failures preserve the submitted IDs and never create a route', async t => {
|
||||||
|
for (const status of [401, 403, 500, 'offline']) {
|
||||||
|
await t.test(String(status), async () => {
|
||||||
|
const harness = await submittedEditor(() => {
|
||||||
|
if (status === 'offline') throw new TypeError('Failed to fetch');
|
||||||
|
return jsonResponse({ message: 'Testfehler' }, false, status);
|
||||||
|
});
|
||||||
|
await submitDraft(harness);
|
||||||
|
assert.equal(harness.calls.some(call => call.method !== 'GET'), false);
|
||||||
|
const saved = harness.packages.get('local-existing');
|
||||||
|
assert.equal(saved.route.serverRouteId, '99');
|
||||||
|
assert.equal(saved.pois[0].serverPoiId, 98);
|
||||||
|
assert.equal(saved.pois[0].images[0].serverPictureId, 97);
|
||||||
|
assert.equal(harness.elements.get('editor-route-upload').disabled, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed fresh creation preserves local files and retries without obsolete server references', async () => {
|
||||||
|
const harness = await submittedEditor(() => jsonResponse({}, false, 404));
|
||||||
|
const originalFetch = harness.context.fetch;
|
||||||
|
let fail = true;
|
||||||
|
harness.context.fetch = async (url, options = {}) => {
|
||||||
|
if (fail && url === '/api/routes' && options.method === 'POST') {
|
||||||
|
fail = false;
|
||||||
|
return jsonResponse({ message: 'Testfehler' }, false, 500);
|
||||||
|
}
|
||||||
|
return originalFetch(url, options);
|
||||||
|
};
|
||||||
|
await submitDraft(harness);
|
||||||
|
const saved = harness.packages.get('local-existing');
|
||||||
|
assert.equal(saved.route.serverRouteId, null);
|
||||||
|
assert.equal(saved.route.uploadedAt, null);
|
||||||
|
assert.equal(saved.route.submissionStatus, 'removed');
|
||||||
|
assert.equal(saved.pois[0].serverPoiId, undefined);
|
||||||
|
assert.equal(saved.pois[0].serverAudio, undefined);
|
||||||
|
assert.equal(saved.pois[0].images[0].serverPictureId, undefined);
|
||||||
|
assert.equal(saved.pois[0].audio.mediaKey, 'audio-key');
|
||||||
|
await submitDraft(harness);
|
||||||
|
assert.equal(harness.packages.get('local-existing').route.serverRouteId, '22');
|
||||||
|
});
|
||||||
|
|
||||||
test('public editor workflow saves new route and GPX Station locally without POSTing a route', async () => {
|
test('public editor workflow saves new route and GPX Station locally without POSTing a route', async () => {
|
||||||
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
const sourcePath = path.join(root, 'public/js/editor-ui.js');
|
||||||
const source = await fs.readFile(sourcePath, 'utf8');
|
const source = await fs.readFile(sourcePath, 'utf8');
|
||||||
|
|||||||
@@ -262,3 +262,15 @@ test('legal overlay handlers synchronize switches without advancing the start sc
|
|||||||
elements.get('info-close').dispatchEvent(pointerEvent());
|
elements.get('info-close').dispatchEvent(pointerEvent());
|
||||||
assert.equal(elements.get('info-dialog').open, false);
|
assert.equal(elements.get('info-dialog').open, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Florian Zumpe is the operator across static legal pages and school contacts are not used', async () => {
|
||||||
|
const html = await read('public/index.html');
|
||||||
|
for (const name of ['privacy', 'imprint', 'terms']) {
|
||||||
|
const article = html.match(new RegExp(`<article[^>]+data-info-page="${name}"[\\s\\S]*?</article>`))[0];
|
||||||
|
assert.match(article, /Florian Zumpe/);
|
||||||
|
assert.match(article, /\[Anschrift von Florian Zumpe ergänzen\]/);
|
||||||
|
assert.match(article, /info@wegwichtel\.de/);
|
||||||
|
assert.doesNotMatch(article, /info@grundschule-herpf|Frau Wichert|Landkreis Schmalkalden|Staatliches Schulamt|data-i18n/);
|
||||||
|
}
|
||||||
|
assert.match(html, /Grundschule Herpf soll die App nach einer gesonderten Nutzungsfreigabe verwenden/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -762,3 +762,16 @@ test('route selection reads metadata only and station media is loaded and releas
|
|||||||
assert.equal(await api.getAppPoi('missing', 0), null);
|
assert.equal(await api.getAppPoi('missing', 0), null);
|
||||||
assert.equal(api.get('lazy').pois[0].images[0].url, undefined);
|
assert.equal(api.get('lazy').pois[0].images[0].url, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('submission status persists with the server ID and rejects unknown status values', async () => {
|
||||||
|
const { api, localStorage } = await loadLocalRoutes();
|
||||||
|
const id = api.createRouteId();
|
||||||
|
for (const submissionStatus of ['rejected', 'removed', null, 'invalid']) {
|
||||||
|
const expected = submissionStatus === 'invalid' ? null : submissionStatus;
|
||||||
|
api.save({ id, gpx: api.emptyGpx('Waldweg'), route: { name: 'Waldweg', serverRouteId: 42, submissionStatus }, pois: [] });
|
||||||
|
assert.equal(api.get(id).route.submissionStatus, expected);
|
||||||
|
assert.equal(api.get(id).route.serverRouteId, 42);
|
||||||
|
const stored = JSON.parse(localStorage.getItem(`${api.STORAGE_PREFIX}.${encodeURIComponent(id)}.json`));
|
||||||
|
assert.equal(stored.route.submissionStatus, expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user