editor overhaul
Sonarqube Scanner / Prepare environment (push) Successful in 7s
Sonarqube Scanner / Build and test (push) Successful in 23s
Sonarqube Scanner / SonarQube analysis (push) Successful in 56s
Sonarqube Scanner / Deploy to Server (push) Successful in 8s

This commit is contained in:
Florian Zumpe
2026-10-18 14:26:25 +02:00
parent d33a67b8ae
commit 9c4d5f6324
17 changed files with 359 additions and 55 deletions
+14 -24
View File
@@ -312,14 +312,10 @@
</header>
<div class="info-dialog-content">
<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>
<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><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>
<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>
<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>
<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><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>
</article>
<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>
<h3>Wer für Wegwichtel verantwortlich ist</h3>
<p>
<strong>Staatliche Grundschule Herpf</strong><br><span>Zum Eichig 3</span><br><span>98617 Meiningen / OT Herpf</span></p>
<h3>Kontakt</h3>
<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>
<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>Inhaber und Betreiber von Wegwichtel</h3>
<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>
<h3>Entwicklung und Pflege</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>
<h3>Inhaltlich verantwortlich</h3>
<p>Frau Wichert</p>
<p>Florian Zumpe</p>
<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>
<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>
<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>
@@ -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>
</article>
<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>Stand: 4. Oktober 2026</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: 17. Oktober 2026</p>
<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>
<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>
+78 -16
View File
@@ -368,6 +368,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const label = document.createElement('div');
appendText(label, 'strong', routeName(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');
actions.className = classes.iconRow;
@@ -458,6 +461,36 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
setStatus('');
// Local drafts are usable even while this request is waiting for a connection.
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) {
@@ -532,13 +565,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const uploadButton = byId(ids.routeUpload);
if (state.currentSource === 'local') {
const uploaded = Boolean(route?.serverRouteId);
setHidden(uploadButton, false);
setDisabled(uploadButton, false);
if (uploadButton) uploadButton.textContent = uploaded ? ns.I18n.t("editor.submitted") : ns.I18n.t("editor.submit");
byId(ids.routeActivated).textContent = uploaded
? ns.I18n.t("editor.submitted-hint")
: ns.I18n.t("editor.draft-hint");
renderLocalSubmissionStatus(route);
} else {
setHidden(uploadButton, true);
setDisabled(uploadButton, true);
@@ -946,23 +975,56 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
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) {
const id = routePackage.route.serverRouteId;
const replacedSubmission = Boolean(id || routePackage.route.submissionStatus);
if (id) {
const payload = await readJson(`/routes/${encodeURIComponent(id)}`);
const existing = payload.route || payload;
if (existing.activatedAt || existing.activated_at) {
throw new Error(ns.I18n.t("editor.read-only-hint"));
const existing = await findSubmittedRoute(id);
if (!existing || existing.status === 'deleted') {
// Preserve the old trash entry; submit a new draft with fresh references.
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 createdId = routeId(payload.route || payload);
if (!createdId) throw new Error(ns.I18n.t("editor.error.send-route"));
routePackage.route.serverRouteId = createdId;
routePackage.route.submissionStatus = null;
checkpointLocalUpload(routePackage);
return { id: createdId, previousPois: [] };
return { id: createdId, previousPois: [], recreated: replacedSubmission };
}
async function syncLocalPoi(base, poi, index, routePackage) {
@@ -1011,7 +1073,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
async function createServerRouteFromLocal(routePackage) {
const { id, previousPois } = await prepareServerRoute(routePackage);
const { id, previousPois, recreated } = await prepareServerRoute(routePackage);
const base = `/routes/${encodeURIComponent(id)}/pois`;
const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
@@ -1023,7 +1085,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await syncLocalPictures(mediaBase, poi, oldPoi, routePackage);
}
await removeMissingServerItems(previousPois, orderedPois, 'serverPoiId', base);
return id;
return { id, recreated };
}
async function uploadLocalRoute() {
@@ -1039,7 +1101,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const routePackage = ns.LocalRoutes.get(localId);
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 saved = ns.LocalRoutes.save({
...routePackage,
@@ -1053,7 +1115,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois;
fillRouteForm(state.currentRoute);
await loadRoutes();
setStatus(ns.I18n.t("editor.submit-success"));
setStatus(ns.I18n.t(recreated ? 'editor.resubmit-success' : 'editor.submit-success'));
} finally {
// Keep acknowledged server IDs even if a later media request failed.
const latest = ns.LocalRoutes.get(routeId(state.currentRoute));
+1
View File
@@ -556,6 +556,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
createdAt: inputRoute.createdAt || now,
updatedAt: now,
serverRouteId: inputRoute.serverRouteId ?? null,
submissionStatus: ['rejected', 'removed'].includes(inputRoute.submissionStatus) ? inputRoute.submissionStatus : null,
uploadedAt: inputRoute.uploadedAt || null
};
const pois = (routePackage.pois || []).map((poi, index) => ({
+5
View File
@@ -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-open": "Vereinbarung für Bilder und Ton lesen",
"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.",
"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.",
+5
View File
@@ -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-open": "Read the agreement for pictures and recordings",
"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.",
"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.",