allow update for uploaded routes and implement creator id for routes
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Successful in 21s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m3s
Sonarqube Scanner / Deploy to Server (push) Successful in 6s

This commit is contained in:
Florian Zumpe
2026-10-03 16:43:08 +02:00
parent 4b8bdcad5f
commit 0db91660b3
20 changed files with 423 additions and 91 deletions
+85 -50
View File
@@ -202,6 +202,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function showListView() {
ns.RouteRecorder?.endEditing();
setDialogTitle('Streckenverwaltung');
setHidden(byId(ids.routeListView), false);
setHidden(byId(ids.routeDetailView), true);
@@ -469,6 +470,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function closeEditor() {
ns.RouteRecorder?.endEditing();
const dialog = byId(ids.dialog);
if (dialog?.open && typeof dialog.close === 'function') dialog.close();
else if (dialog?.open) dialog.removeAttribute('open');
@@ -507,7 +509,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (state.currentSource === 'local') {
const uploaded = Boolean(route?.serverRouteId);
setHidden(uploadButton, false);
setDisabled(uploadButton, uploaded);
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")
@@ -761,6 +763,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function loadRoute(route) {
const id = routeId(route);
if (!id) return;
if (ns.RouteRecorder?.routeId && (routeSource(route) !== 'local' || ns.RouteRecorder.routeId !== id)) {
ns.RouteRecorder.endEditing();
}
setStatus(ns.I18n.t("editor.route.loading"));
state.currentSource = routeSource(route);
@@ -773,6 +778,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
gpx: routePackage.gpx,
_source: 'local'
};
if (byId(ids.dialog)?.open) ns.RouteRecorder?.beginEditing(id);
state.pois = [...routePackage.pois].sort((left, right) => Number(left.sequence) - Number(right.sequence));
} else {
const payload = await readJson(`/routes/${encodeURIComponent(id)}`);
@@ -789,6 +795,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function newRoute() {
ns.RouteRecorder?.endEditing();
state.currentSource = 'local';
state.currentRoute = {
id: '',
@@ -839,7 +846,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
});
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
state.currentSource = 'local';
if (isNew) ns.RouteRecorder?.start(saved.id);
if (isNew && byId(ids.dialog)?.open) ns.RouteRecorder?.beginEditing(saved.id);
state.pois = saved.pois;
await loadRoutes();
await loadRoute({ id: saved.id, _source: 'local' });
@@ -884,6 +891,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const data = new FormData();
data.append('name', routePackage.route?.name || ns.I18n.t("editor.untitled-route"));
data.append('description', routePackage.route?.description || '');
data.append('creator_id', routePackage.route?.creator_id || ns.LocalRoutes.creatorId());
const uploadGpx = ns.LocalRoutes.gpxForUpload(routePackage);
const gpx = new Blob([uploadGpx], { type: 'application/gpx+xml' });
data.append('gpx', gpx, safeGpxFilename(routePackage.route?.name));
@@ -898,72 +906,89 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
return blob;
}
async function uploadStoredMedia(path, fieldName, media, fallbackName) {
async function uploadStoredMedia(path, fieldName, media, fallbackName, method = 'POST') {
const blob = await storedMediaBlob(media);
if (!blob) return;
const data = new FormData();
data.append(fieldName, blob, media.name || fallbackName);
await sendForm(path, 'POST', data);
}
async function uploadLocalPoiMedia(serverRouteId, serverPoiId, poi) {
const basePath = `/routes/${encodeURIComponent(serverRouteId)}/pois/${encodeURIComponent(serverPoiId)}`;
if (poi.audio) {
await uploadStoredMedia(`${basePath}/audio`, 'audio', poi.audio, 'audio.bin');
}
for (const [index, image] of (poi.images || []).entries()) {
if (!image) continue;
await uploadStoredMedia(`${basePath}/pictures`, 'picture', image, `bild-${index + 1}.bin`);
}
if (fieldName === 'picture') data.append('caption', media.caption || '');
return sendForm(path, method, data);
}
async function createServerRouteFromLocal(routePackage) {
let createdRouteId = null;
try {
const checkpoint = () => {
const latest = ns.LocalRoutes.get(routePackage.id);
// GPS samples may arrive while media requests are pending.
if (latest) routePackage.gpx = latest.gpx;
return ns.LocalRoutes.save(routePackage);
};
let id = routePackage.route.serverRouteId;
let previousPois = [];
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"));
}
previousPois = existing.pois || [];
await sendForm(`/routes/${encodeURIComponent(id)}`, 'PUT', localRouteUploadForm(routePackage));
} else {
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
const serverRoute = payload.route || payload;
createdRouteId = routeId(serverRoute);
if (!createdRouteId) throw new Error(ns.I18n.t("editor.error.send-route"));
const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
for (const [index, poi] of orderedPois.entries()) {
const poiTitle = poi.title || ns.I18n.t("editor.station-number", { value1: index + 1 });
const poiPayload = await sendJson(`/routes/${encodeURIComponent(createdRouteId)}/pois`, 'POST', {
description: poi.description || '',
lat: poi.lat,
lon: poi.lon,
sequence: index,
title: poiTitle,
id = routeId(payload.route || payload);
if (!id) throw new Error(ns.I18n.t("editor.error.send-route"));
routePackage.route.serverRouteId = id;
checkpoint();
}
const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
for (const [index, poi] of orderedPois.entries()) {
const base = `/routes/${encodeURIComponent(id)}/pois`;
const payload = await sendJson(poi.serverPoiId ? `${base}/${poi.serverPoiId}` : base,
poi.serverPoiId ? 'PUT' : 'POST', {
description: poi.description || '', lat: poi.lat, lon: poi.lon, sequence: index,
title: poi.title || ns.I18n.t("editor.station-number", { value1: index + 1 }),
triggerRadiusM: poi.triggerRadiusM || 25
});
const serverPoi = poiPayload.poi || poiPayload;
const serverPoiId = poiId(serverPoi);
if (!serverPoiId) throw new Error(ns.I18n.t("editor.error.send-station", { value1: poiTitle }));
await uploadLocalPoiMedia(createdRouteId, serverPoiId, poi);
poi.serverPoiId = poiId(payload.poi || payload);
if (!poi.serverPoiId) throw new Error(ns.I18n.t("editor.error.send-station", { value1: poi.title || '' }));
checkpoint();
const mediaBase = `${base}/${poi.serverPoiId}`;
const oldPoi = previousPois.find(old => String(old.id) === String(poi.serverPoiId));
if (poi.audio) {
await uploadStoredMedia(`${mediaBase}/audio`, 'audio', poi.audio, 'audio.bin',
oldPoi?.audioUrl || poi.serverAudio ? 'PUT' : 'POST');
poi.serverAudio = true;
checkpoint();
} else if (oldPoi?.audioUrl) {
await sendDelete(`${mediaBase}/audio`);
poi.serverAudio = false;
checkpoint();
}
return createdRouteId;
} catch (error) {
if (createdRouteId) {
try {
await sendDelete(`/routes/${encodeURIComponent(createdRouteId)}`);
} catch {
// Der ursprüngliche Uploadfehler ist wichtiger als ein fehlgeschlagener Rollback.
for (const [imageIndex, image] of (poi.images || []).entries()) {
const path = image.serverPictureId ? `${mediaBase}/pictures/${image.serverPictureId}` : `${mediaBase}/pictures`;
const picture = await uploadStoredMedia(path, 'picture', image, `bild-${imageIndex + 1}.bin`,
image.serverPictureId ? 'PUT' : 'POST');
image.serverPictureId = (picture.picture || picture).id;
if (!image.serverPictureId) throw new Error(ns.I18n.t("editor.error.send-file", { value1: image.name || '' }));
checkpoint();
}
for (const oldImage of oldPoi?.images || []) {
if (!(poi.images || []).some(image => String(image.serverPictureId) === String(oldImage.id))) {
await sendDelete(`${mediaBase}/pictures/${oldImage.id}`);
}
}
throw error;
}
for (const oldPoi of previousPois) {
if (!orderedPois.some(poi => String(poi.serverPoiId) === String(oldPoi.id))) {
await sendDelete(`/routes/${encodeURIComponent(id)}/pois/${oldPoi.id}`);
}
}
return id;
}
async function uploadLocalRoute() {
if (state.currentSource !== 'local') throw new Error(ns.I18n.t("editor.error.submit-own-draft"));
const values = routeFromForm();
if (!values.name) throw new Error(ns.I18n.t("editor.error.route-name"));
if (state.currentRoute?.serverRouteId) {
setStatus(ns.I18n.t("editor.error.already-submitted"));
return;
}
const uploadButton = byId(ids.routeUpload);
setDisabled(uploadButton, true);
try {
@@ -989,7 +1014,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await loadRoutes();
setStatus(ns.I18n.t("editor.submit-success"));
} finally {
if (!state.currentRoute?.serverRouteId) setDisabled(uploadButton, false);
// Keep acknowledged server IDs even if a later media request failed.
const latest = ns.LocalRoutes.get(routeId(state.currentRoute));
if (latest) {
state.currentRoute = { ...latest.route, id: latest.id, gpx: latest.gpx, _source: 'local' };
state.pois = latest.pois;
fillRouteForm(state.currentRoute);
}
setDisabled(uploadButton, false);
}
}
@@ -1392,6 +1424,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function bind() {
for (const eventName of ['cancel', 'close']) {
byId(ids.dialog)?.addEventListener(eventName, () => ns.RouteRecorder?.endEditing());
}
activate(byId(ids.button), () => guarded(openEditor));
activate(byId(ids.close), closeEditor);
activate(byId(ids.newRoute), newRoute);