add upload
Sonarqube Scanner / Build and analyze (push) Failing after 1m4s

This commit is contained in:
Florian Zumpe
2026-09-27 18:11:08 +02:00
parent becd48b487
commit d2f31e39e8
5 changed files with 197 additions and 14 deletions
+140 -5
View File
@@ -42,6 +42,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
routeListView: 'editor-route-list-view',
routeName: 'editor-route-name',
routeSave: 'editor-route-save',
routeUpload: 'editor-route-upload',
status: 'editor-status'
});
@@ -240,7 +241,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function handleResponse(response) {
if (response.ok) return parseJson(response);
const message = await response.text();
throw new Error(message || `HTTP ${response.status}`);
const error = new Error(message || `HTTP ${response.status}`);
error.status = response.status;
throw error;
}
async function readJson(path) {
@@ -468,9 +471,19 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route);
byId(ids.routeDescription).value = text(route?.description);
byId(ids.routeGpx).value = '';
const uploadButton = byId(ids.routeUpload);
if (state.currentSource === 'local') {
byId(ids.routeActivated).textContent = 'Lokaler Entwurf: GPX, Texte und Medien bleiben ausschließlich im Browser. Eine Server-Freigabe ist noch nicht implementiert.';
const uploaded = Boolean(route?.serverRouteId);
setHidden(uploadButton, false);
setDisabled(uploadButton, uploaded);
if (uploadButton) uploadButton.textContent = uploaded ? 'Bereits übertragen' : 'An Server übertragen';
byId(ids.routeActivated).textContent = uploaded
? `Lokaler Entwurf. Bereits als unveröffentlichte Server-Strecke ${route.serverRouteId} zur Prüfung übertragen.`
: 'Lokaler Entwurf: GPX, Texte und Medien bleiben im Browser, bis „An Server übertragen“ gewählt wird.';
} else {
setHidden(uploadButton, true);
setDisabled(uploadButton, true);
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
? `Veröffentlicht am ${route.activatedAt || route.activated_at}. Der Editor verändert die Freigabe nicht.`
: 'Server-Strecke ohne Freigabedatum. Der Editor verändert activated_at nicht.';
@@ -697,7 +710,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const file = byId(ids.routeGpx)?.files?.[0];
data.append('name', values.name);
data.append('description', values.description);
if (file) data.append('file', file);
if (file) data.append('gpx', file);
return data;
}
@@ -727,7 +740,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois;
await loadRoutes();
await loadRoute({ id: saved.id, _source: 'local' });
setStatus('Strecke erfolgreich lokal gespeichert. Es wurden keine Daten an den Server übertragen.');
setStatus('Speichern erfolgreich.');
}
async function saveServerRoute(values) {
@@ -742,7 +755,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.currentRoute = { ...saved, _source: 'server' };
await loadRoutes();
await loadRoute({ id, _source: 'server' });
setStatus('Strecke erfolgreich gespeichert. activated_at wurde nicht verändert.');
setStatus('Speichern erfolgreich.');
}
async function saveRoute() {
@@ -752,6 +765,127 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
else await saveLocalRoute(values);
}
function safeGpxFilename(name) {
const base = text(name || 'wegwichtel-route')
.normalize('NFKD')
.replaceAll(/[^a-zA-Z0-9_-]+/g, '-')
.replaceAll(/^-+|-+$/g, '')
.slice(0, 80);
return `${base || 'wegwichtel-route'}.gpx`;
}
function localRouteUploadForm(routePackage) {
const data = new FormData();
data.append('name', routePackage.route?.name || 'Unbenannte Strecke');
data.append('description', routePackage.route?.description || '');
const gpx = new Blob([routePackage.gpx], { type: 'application/gpx+xml' });
data.append('gpx', gpx, safeGpxFilename(routePackage.route?.name));
return data;
}
async function storedMediaBlob(media) {
if (!media?.dataUrl) return null;
const response = await fetch(media.dataUrl);
if (!response.ok || typeof response.blob !== 'function') {
throw new Error(`Die Mediendatei ${media.name || ''} konnte nicht für den Upload vorbereitet werden.`.trim());
}
return response.blob();
}
async function uploadStoredMedia(path, fieldName, media, fallbackName) {
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?.dataUrl) {
await uploadStoredMedia(`${basePath}/audio`, 'audio', poi.audio, 'audio.bin');
}
for (const [index, image] of (poi.images || []).entries()) {
if (!image?.dataUrl) continue;
await uploadStoredMedia(`${basePath}/pictures`, 'picture', image, `bild-${index + 1}.bin`);
}
}
async function createServerRouteFromLocal(routePackage) {
let createdRouteId = null;
try {
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
const serverRoute = payload.route || payload;
createdRouteId = routeId(serverRoute);
if (!createdRouteId) throw new Error('Der Server hat keine Strecken-ID für den Upload zurückgegeben.');
const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
for (const [index, poi] of orderedPois.entries()) {
const poiPayload = await sendJson(`/routes/${encodeURIComponent(createdRouteId)}/pois`, 'POST', {
description: poi.description || '',
lat: poi.lat,
lon: poi.lon,
sequence: index,
title: poi.title || `Station ${index + 1}`,
triggerRadiusM: poi.triggerRadiusM || 25
});
const serverPoi = poiPayload.poi || poiPayload;
const serverPoiId = poiId(serverPoi);
if (!serverPoiId) throw new Error(`Für ${poi.title || `Station ${index + 1}`} wurde keine Server-POI-ID zurückgegeben.`);
await uploadLocalPoiMedia(createdRouteId, serverPoiId, poi);
}
return createdRouteId;
} catch (error) {
if (createdRouteId) {
try {
await sendDelete(`/routes/${encodeURIComponent(createdRouteId)}`);
} catch {
// Der ursprüngliche Uploadfehler ist wichtiger als ein fehlgeschlagener Rollback.
}
}
throw error;
}
}
async function uploadLocalRoute() {
if (state.currentSource !== 'local') throw new Error('Nur lokale Strecken können hochgeladen werden.');
const values = routeFromForm();
if (!values.name) throw new Error('Der Name der Strecke ist erforderlich.');
if (state.currentRoute?.serverRouteId) {
setStatus('Diese Strecke wurde bereits an den Server übertragen.');
return;
}
const uploadButton = byId(ids.routeUpload);
setDisabled(uploadButton, true);
try {
await saveLocalRoute(values);
setStatus('Übertragung läuft …');
const localId = routeId(state.currentRoute);
const routePackage = ns.LocalRoutes.get(localId);
if (!routePackage) throw new Error('Der lokale Streckenentwurf wurde nicht gefunden.');
const serverRouteId = await createServerRouteFromLocal(routePackage);
const uploadedAt = new Date().toISOString();
const saved = ns.LocalRoutes.save({
...routePackage,
route: {
...routePackage.route,
serverRouteId,
uploadedAt
}
});
state.currentRoute = { ...saved.route, id: saved.id, gpx: saved.gpx, _source: 'local' };
state.pois = saved.pois;
fillRouteForm(state.currentRoute);
await loadRoutes();
setStatus('Upload erfolgreich.');
} finally {
if (!state.currentRoute?.serverRouteId) setDisabled(uploadButton, false);
}
}
function currentPosition() {
return typeof ns.App?.currentPosition === 'function' ? ns.App.currentPosition() : null;
}
@@ -1072,6 +1206,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
activate(byId(ids.close), closeEditor);
activate(byId(ids.newRoute), newRoute);
activate(byId(ids.routeBack), () => guarded(loadRoutes).then(showListView));
activate(byId(ids.routeUpload), () => guarded(uploadLocalRoute));
activate(byId(ids.addPoi), newPoi);
activate(byId(ids.poiAudioAdd), () => openFilePicker(ids.poiAudio));
activate(byId(ids.poiImagesAdd), () => openFilePicker(ids.poiImages));
+3 -1
View File
@@ -231,7 +231,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
name: String(inputRoute.name || ''),
description: String(inputRoute.description || ''),
createdAt: inputRoute.createdAt || now,
updatedAt: now
updatedAt: now,
serverRouteId: inputRoute.serverRouteId ?? null,
uploadedAt: inputRoute.uploadedAt || null
};
const pois = (routePackage.pois || []).map((poi, index) => ({
...poi,