design overhaul and default route
Sonarqube Scanner / Build and analyze (push) Failing after 10s

This commit is contained in:
Florian Zumpe
2026-09-27 20:21:29 +02:00
parent 98a7293efd
commit 8fb5b3a397
9 changed files with 615 additions and 97 deletions
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Wegwichtel Presentation Demo"
xmlns="http://www.topografix.com/GPX/1/1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<name>Wegwichtel Demo – Rundweg</name>
<desc>Beispielstrecke für Präsentations- und Funktionstests. Keine reale Wegführung.</desc>
</metadata>
<trk>
<name>Wegwichtel Demo – Rundweg</name>
<trkseg>
<trkpt lat="50.579880" lon="10.431420"><ele>328</ele></trkpt>
<trkpt lat="50.580190" lon="10.431960"><ele>330</ele></trkpt>
<trkpt lat="50.580540" lon="10.432520"><ele>332</ele></trkpt>
<trkpt lat="50.580820" lon="10.433170"><ele>334</ele></trkpt>
<trkpt lat="50.581090" lon="10.433840"><ele>337</ele></trkpt>
<trkpt lat="50.581350" lon="10.434420"><ele>339</ele></trkpt>
<trkpt lat="50.581190" lon="10.435050"><ele>338</ele></trkpt>
<trkpt lat="50.580820" lon="10.435460"><ele>336</ele></trkpt>
<trkpt lat="50.580410" lon="10.435130"><ele>333</ele></trkpt>
<trkpt lat="50.580060" lon="10.434570"><ele>331</ele></trkpt>
<trkpt lat="50.579760" lon="10.433880"><ele>329</ele></trkpt>
<trkpt lat="50.579650" lon="10.433120"><ele>327</ele></trkpt>
<trkpt lat="50.579720" lon="10.432310"><ele>327</ele></trkpt>
<trkpt lat="50.579880" lon="10.431420"><ele>328</ele></trkpt>
</trkseg>
</trk>
</gpx>
+38
View File
@@ -0,0 +1,38 @@
{
"name": "Wegwichtel Demo – Rundweg",
"description": "Beispielstrecke für Präsentations- und Funktionstests. Keine reale Wegführung.",
"pois": [
{
"title": "Demo-Start",
"description": "Hier beginnt die Präsentationsstrecke.",
"lat": 50.57988,
"lon": 10.43142,
"triggerRadiusM": 25,
"sequence": 0
},
{
"title": "Wiesenblick",
"description": "Ein Beispiel-Trackpunkt für Texte, Bilder und Audio.",
"lat": 50.58109,
"lon": 10.43384,
"triggerRadiusM": 25,
"sequence": 1
},
{
"title": "Waldrand",
"description": "Zweiter Beispiel-Trackpunkt der Demo-Strecke.",
"lat": 50.58082,
"lon": 10.43546,
"triggerRadiusM": 25,
"sequence": 2
},
{
"title": "Demo-Ziel",
"description": "Hier endet die Präsentationsstrecke.",
"lat": 50.57972,
"lon": 10.43231,
"triggerRadiusM": 25,
"sequence": 3
}
]
}
+1 -1
View File
@@ -28,7 +28,7 @@
"file-type": "22.0.1", "file-type": "22.0.1",
"jquery": "4.0.0", "jquery": "4.0.0",
"jquery-ui": "1.14.2", "jquery-ui": "1.14.2",
"multer": "^2.4.0" "multer": "2.2.0"
}, },
"devDependencies": { "devDependencies": {
"c8": "11.0.0" "c8": "11.0.0"
+273 -83
View File
@@ -1,14 +1,16 @@
:root { :root {
--bootstrap-bg: #f3f7ee; --bootstrap-bg: #f3f7ee;
--bootstrap-card-bg: #ffffff; --bootstrap-card-bg: #ffffff;
--bootstrap-text: #1c2b20; --bootstrap-text: #10291b;
--bootstrap-muted: #53605a; --bootstrap-muted: #67726c;
--bootstrap-accent: #315d3a; --bootstrap-accent: #286438;
--bootstrap-accent-strong: #25462c; --bootstrap-accent-strong: #194d2b;
--bootstrap-border: #d6dfd3; --bootstrap-border: #dce6dc;
--bootstrap-error: #8b1e1e; --bootstrap-error: #a32020;
--bootstrap-shadow: 0 1rem 3rem rgb(29 52 36 / 18%); --bootstrap-shadow: 0 1.2rem 3.6rem rgb(29 52 36 / 16%);
--bootstrap-fade-duration: 180ms;
} }
#bootstrap-screen { #bootstrap-screen {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -18,7 +20,7 @@
justify-items: center; justify-items: center;
min-height: 100svh; min-height: 100svh;
box-sizing: border-box; box-sizing: border-box;
padding: max(1rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) max(1rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left)); padding: max(5.25rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) max(1rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
overscroll-behavior-y: contain; overscroll-behavior-y: contain;
@@ -26,127 +28,315 @@
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
color: var(--bootstrap-text); color: var(--bootstrap-text);
background: background:
radial-gradient(circle at 20% 10%, rgb(135 185 112 / 32%), transparent 34rem), radial-gradient(circle at 12% 6%, rgb(126 183 101 / 24%), transparent 27rem),
radial-gradient(circle at 85% 90%, rgb(230 198 116 / 25%), transparent 28rem), radial-gradient(circle at 88% 94%, rgb(235 196 104 / 18%), transparent 25rem),
var(--bootstrap-bg); linear-gradient(180deg, #f7faf4 0%, var(--bootstrap-bg) 100%);
} }
#bootstrap-screen[hidden] { #bootstrap-screen[hidden] {
display: none; display: none;
} }
#bootstrap-screen > section { #bootstrap-screen > section {
width: min(100%, 34rem); width: min(100%, 42rem);
box-sizing: border-box; box-sizing: border-box;
padding: clamp(1.25rem, 5vw, 2rem); padding: clamp(1.1rem, 4vw, 1.8rem);
border: 1px solid var(--bootstrap-border); border: 1px solid rgb(214 224 214 / 80%);
border-radius: 1.5rem; border-radius: clamp(1.4rem, 5vw, 2rem);
box-shadow: var(--bootstrap-shadow); box-shadow: var(--bootstrap-shadow);
background: var(--bootstrap-card-bg); background: rgb(255 255 255 / 96%);
} }
#bootstrap-screen [aria-hidden="true"] { .bootstrap-brand {
display: inline-block; display: grid;
margin-bottom: .25rem; grid-template-columns: auto minmax(0, 1fr);
font-size: 2rem; align-items: center;
gap: 1rem;
margin-bottom: 1rem;
} }
.bootstrap-logo {
width: clamp(4.25rem, 15vw, 5.75rem);
height: auto;
filter: drop-shadow(0 .35rem .6rem rgb(49 93 58 / 12%));
}
#bootstrap-screen h1 { #bootstrap-screen h1 {
margin: 0 0 .5rem; margin: 0 0 .3rem;
font-size: clamp(1.8rem, 8vw, 2.5rem); color: #0d2b1b;
line-height: 1.05; font-size: clamp(2rem, 8vw, 3rem);
line-height: 1;
letter-spacing: -.035em;
} }
#bootstrap-screen p { #bootstrap-screen .bootstrap-brand p {
margin: .65rem 0; margin: 0;
color: var(--bootstrap-muted); color: var(--bootstrap-muted);
font-size: clamp(.98rem, 3.8vw, 1.18rem);
line-height: 1.35;
}
.bootstrap-hero {
overflow: hidden;
margin: .35rem 0 1rem;
border-radius: 1.5rem;
background: #eef7f1;
}
.bootstrap-hero svg {
display: block;
width: 100%;
height: auto;
max-height: 16rem;
}
.bootstrap-action-slot {
display: grid;
margin: 1rem 0;
}
.bootstrap-action-slot > * {
grid-area: 1 / 1;
}
.bootstrap-action-status,
#bootstrap-enter.ui-button,
.bootstrap-enter {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 4rem;
box-sizing: border-box;
margin: 0;
padding: .8rem 1.25rem;
border: 0;
border-radius: 999px;
font: inherit;
font-size: clamp(1.05rem, 4.4vw, 1.28rem);
font-weight: 750;
letter-spacing: .005em;
}
.bootstrap-action-status {
gap: .75rem;
color: #eff8f0;
background: linear-gradient(135deg, #387347, var(--bootstrap-accent));
box-shadow: 0 .45rem 1.1rem rgb(40 100 56 / 18%);
opacity: 1;
transform: translateY(0);
transition: opacity var(--bootstrap-fade-duration) ease, transform var(--bootstrap-fade-duration) ease;
}
.bootstrap-action-status.is-leaving {
opacity: 0;
transform: translateY(-.2rem);
pointer-events: none;
}
.bootstrap-loading-dot {
flex: 0 0 auto;
width: .65rem;
height: .65rem;
border-radius: 50%;
background: #d9f0de;
box-shadow: 0 0 0 0 rgb(217 240 222 / 50%);
animation: bootstrap-pulse 1.35s ease-out infinite;
}
.bootstrap-status-text {
opacity: 1;
transition: opacity var(--bootstrap-fade-duration) ease;
}
.bootstrap-status-text.is-fading {
opacity: 0;
}
#bootstrap-enter.ui-button,
.bootstrap-enter {
gap: .75rem;
color: #fff;
background: linear-gradient(135deg, #2d743d, var(--bootstrap-accent-strong));
box-shadow: 0 .55rem 1.25rem rgb(25 77 43 / 24%);
cursor: pointer;
opacity: 0;
transform: translateY(.2rem);
transition: opacity var(--bootstrap-fade-duration) ease, transform var(--bootstrap-fade-duration) ease, background-color .15s ease;
}
#bootstrap-enter.is-visible {
opacity: 1;
transform: translateY(0);
}
#bootstrap-enter svg {
width: 1.5rem;
height: 1.5rem;
margin-left: auto;
fill: none;
stroke: currentColor;
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
#bootstrap-enter:hover,
#bootstrap-enter:focus-visible {
background: #173f24;
}
.bootstrap-features {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
margin: 1rem 0;
padding: .8rem .4rem;
border: 1px solid #e2ebe2;
border-radius: 1.35rem;
background: linear-gradient(135deg, #f4f9f4, #fbfcf8);
}
.bootstrap-features > div {
display: grid;
place-items: center;
gap: .4rem;
min-width: 0;
padding: .3rem .45rem;
text-align: center;
}
.bootstrap-features > div + div {
border-left: 1px solid #dce6dc;
}
.bootstrap-features svg {
width: 2rem;
height: 2rem;
fill: none;
stroke: var(--bootstrap-accent);
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
}
.bootstrap-features > div:nth-child(2) svg {
stroke: #7351b4;
}
.bootstrap-features > div:nth-child(3) svg {
fill: #e96c2a;
stroke: #e96c2a;
}
.bootstrap-features span {
color: #24402c;
font-size: clamp(.78rem, 3vw, .95rem);
font-weight: 650;
line-height: 1.2;
} }
#bootstrap-screen .privacy-summary { #bootstrap-screen .privacy-summary {
display: grid; display: grid;
gap: .75rem; gap: .65rem;
margin: 1rem 0; margin: .85rem 0 0;
padding: .85rem; padding: .85rem;
border: 1px solid var(--bootstrap-border); border: 1px solid var(--bootstrap-border);
border-radius: 1rem; border-radius: 1.15rem;
background: #f9fbf6; background: #f9fbf6;
} }
#bootstrap-screen .privacy-summary > p { #bootstrap-screen .privacy-summary > p {
margin: 0; margin: 0;
color: var(--bootstrap-muted);
font-size: .92rem;
line-height: 1.4;
} }
#bootstrap-screen .privacy-summary > .switch-row.switch-disabled { #bootstrap-screen .privacy-summary > .switch-row.switch-disabled {
display: none; display: none;
} }
#bootstrap-screen progress { .storage-cleanup-status {
width: 100%; min-height: 1.2rem;
height: .9rem; margin: .75rem 0 0;
overflow: hidden;
border: 0;
border-radius: 999px;
background: #dce7d8;
}
#bootstrap-screen progress::-webkit-progress-bar {
border-radius: 999px;
background: #dce7d8;
}
#bootstrap-screen progress::-webkit-progress-value {
border-radius: 999px;
background: var(--bootstrap-accent);
}
#bootstrap-screen progress::-moz-progress-bar {
border-radius: 999px;
background: var(--bootstrap-accent);
}
#bootstrap-steps {
display: grid;
gap: .35rem;
margin: 1rem 0;
padding-left: 1.4rem;
color: var(--bootstrap-muted); color: var(--bootstrap-muted);
font-size: .86rem;
text-align: center;
} }
#bootstrap-steps li[data-state="done"] { .bootstrap-error {
color: var(--bootstrap-accent-strong); margin: .65rem 0 0;
} padding: .75rem .9rem;
border-radius: .9rem;
#bootstrap-steps li[data-state="active"] {
font-weight: 700;
color: var(--bootstrap-text);
}
#bootstrap-error {
color: var(--bootstrap-error); color: var(--bootstrap-error);
background: #fff0f0;
font-weight: 700; font-weight: 700;
} }
#bootstrap-retry,
#bootstrap-enter { .bootstrap-retry,
#bootstrap-retry.ui-button {
width: 100%; width: 100%;
min-height: 3rem; min-height: 3rem;
margin-top: .75rem; box-sizing: border-box;
border: 0; margin-top: .65rem;
border: 1px solid #d8abab;
border-radius: 999px; border-radius: 999px;
color: #ffffff; color: #861d1d;
background: var(--bootstrap-accent); background: #fff;
font: inherit; font: inherit;
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
} }
#bootstrap-retry:hover,
#bootstrap-enter:hover, .bootstrap-ready,
#bootstrap-retry:focus-visible, .bootstrap-state-model {
#bootstrap-enter:focus-visible { display: none !important;
background: var(--bootstrap-accent-strong);
} }
.bootstrap-ready { @keyframes bootstrap-pulse {
font-weight: 700; 0% { box-shadow: 0 0 0 0 rgb(217 240 222 / 55%); }
color: var(--bootstrap-accent-strong); 65%, 100% { box-shadow: 0 0 0 .7rem rgb(217 240 222 / 0%); }
} }
.storage-cleanup-status { @media (max-width: 34rem) {
min-height: 1.25rem; #bootstrap-screen {
font-size: .95rem; padding-right: max(.75rem, env(safe-area-inset-right));
padding-left: max(.75rem, env(safe-area-inset-left));
}
#bootstrap-screen > section {
padding: 1rem;
}
.bootstrap-brand {
gap: .75rem;
}
.bootstrap-hero svg {
max-height: 12.5rem;
}
.bootstrap-features {
padding: .65rem .25rem;
}
}
@media (max-height: 700px) {
.bootstrap-hero svg {
max-height: 10rem;
}
}
@media (prefers-reduced-motion: reduce) {
.bootstrap-loading-dot {
animation: none;
}
.bootstrap-status-text,
.bootstrap-action-status,
#bootstrap-enter.ui-button,
.bootstrap-enter {
transition: none;
}
} }
+72 -10
View File
@@ -22,12 +22,77 @@
</nav> </nav>
</div> </div>
<div id="bootstrap-screen" aria-live="polite" aria-busy="true"> <div id="bootstrap-screen" aria-live="polite" aria-busy="true">
<section> <section class="bootstrap-card">
<span aria-hidden="true">🧭</span> <header class="bootstrap-brand">
<svg class="bootstrap-logo" viewBox="0 0 96 96" aria-hidden="true" focusable="false">
<circle cx="48" cy="48" r="39" fill="#fff7d7" stroke="#f4b400" stroke-width="6"/>
<path d="M48 15 57 39 81 48 57 57 48 81 39 57 15 48 39 39Z" fill="#d9ddd8"/>
<path d="m48 48 24-23-13 31Z" fill="#e53935"/>
<path d="m48 48-24 23 13-31Z" fill="#2d9cdb"/>
<circle cx="48" cy="48" r="6" fill="#f8fbf7" stroke="#315d3a" stroke-width="3"/>
<circle cx="80" cy="16" r="8" fill="#fff" stroke="#f4b400" stroke-width="5"/>
</svg>
<div>
<h1>Wegwichtel</h1> <h1>Wegwichtel</h1>
<p>Der Wegwichtel bereitet Routen und Medien vor.</p> <p>Der Wegwichtel bereitet Routen und Medien vor.</p>
</div>
</header>
<div class="bootstrap-hero" aria-hidden="true">
<svg viewBox="0 0 720 300" preserveAspectRatio="xMidYMid slice" focusable="false">
<rect width="720" height="300" rx="32" fill="#eef7f1"/>
<circle cx="255" cy="68" r="27" fill="#ffd66b"/>
<path d="M0 150C92 84 172 113 250 156c81 44 139-14 228-36 82-20 149 2 242 62v118H0Z" fill="#a9d4ca"/>
<path d="M0 191c111-67 217-16 306 25 89 40 179-50 283-49 53 0 92 15 131 43v90H0Z" fill="#74aa62"/>
<path d="M0 224c93-51 194-19 283 17 85 34 189-35 279-31 60 3 110 18 158 51v39H0Z" fill="#5e914e"/>
<g fill="#477d49">
<circle cx="75" cy="200" r="34"/><circle cx="118" cy="210" r="26"/><circle cx="610" cy="210" r="31"/><circle cx="654" cy="218" r="25"/>
</g>
<g fill="#607d8b" opacity=".9">
<path d="M305 174h28v36h-28zM339 161h35v49h-35zM381 180h34v30h-34z"/>
<path d="m350 160 8-24 8 24Z"/>
</g>
<path d="M250 300c34-52 88-69 137-74 53-5 113 5 153 22-65 9-127 29-155 52Z" fill="#f6dfb7"/>
<g transform="translate(538 117)">
<rect x="36" y="24" width="13" height="121" rx="6" fill="#7b5231"/>
<path d="M0 17h105l25 21-25 21H0Z" fill="#f5a332"/>
<path d="M14 65h91l22 19-22 19H14Z" fill="#4f914b"/>
</g>
</svg>
</div>
<div class="bootstrap-action-slot">
<output id="bootstrap-action-status" class="bootstrap-action-status" aria-live="polite">
<span class="bootstrap-loading-dot" aria-hidden="true"></span>
<span id="bootstrap-status-text" class="bootstrap-status-text">Wegwichtel wird vorbereitet …</span>
</output>
<button id="bootstrap-enter" class="bootstrap-enter" type="button" hidden>
<span>Los geht’s</span>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 5 7 7-7 7"/></svg>
</button>
</div>
<p id="bootstrap-error" class="bootstrap-error" hidden></p>
<button id="bootstrap-retry" class="bootstrap-retry" type="button" hidden>Erneut versuchen</button>
<p id="bootstrap-ready" class="bootstrap-ready" hidden>Alles ist bereit.</p>
<div class="bootstrap-features" aria-label="Wegwichtel Funktionen">
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m3 7 8-3 10 4 8-3v20l-8 3-10-4-8 3Zm8-3v20m10-16v20"/></svg>
<span>Routen<br>entdecken</span>
</div>
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="M7 18v-4a9 9 0 0 1 18 0v4M7 18H4v7h5v-7Zm18 0h3v7h-5v-7Z"/></svg>
<span>Spannende<br>Geschichten</span>
</div>
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m16 3 3.9 8 8.8 1.3-6.4 6.2 1.5 8.8-7.8-4.1-7.8 4.1 1.5-8.8-6.4-6.2 8.8-1.3Z"/></svg>
<span>Kindgerechte<br>Erlebnisse</span>
</div>
</div>
<div class="privacy-summary"> <div class="privacy-summary">
<p><strong>Cookies:</strong> Diese Anwendung setzt derzeit keine Cookies und verwendet weder Werbung noch Tracking.</p> <p><strong>Datenschutzfreundlich:</strong> Keine Werbung, kein Tracking und derzeit keine Cookies.</p>
<label class="switch-row switch-disabled"> <label class="switch-row switch-disabled">
<span> <span>
<strong>Optionale Cookies</strong> <strong>Optionale Cookies</strong>
@@ -39,15 +104,16 @@
<label class="switch-row"> <label class="switch-row">
<span> <span>
<strong>Streckenfortschritt lokal speichern</strong> <strong>Streckenfortschritt lokal speichern</strong>
<small>Speichert ausgewählte Routen, besuchte und angesehene Stationen sowie abgespielte Audiodateien ausschließlich in diesem Browser.</small> <small>Speichert ausgewählte Routen und besuchte Stationen ausschließlich in diesem Browser.</small>
</span> </span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false"> <input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span> <span class="switch-track" aria-hidden="true"></span>
</label> </label>
</div> </div>
<p id="storage-cleanup-status" class="storage-cleanup-status" hidden></p> <p id="storage-cleanup-status" class="storage-cleanup-status" hidden></p>
<progress id="bootstrap-progress" max="6" value="0"></progress>
<ol id="bootstrap-steps"> <progress id="bootstrap-progress" class="bootstrap-state-model" max="6" value="0" hidden></progress>
<ol id="bootstrap-steps" class="bootstrap-state-model" hidden>
<li data-step="jquery">jQuery laden</li> <li data-step="jquery">jQuery laden</li>
<li data-step="jquery-ui">jQuery UI laden</li> <li data-step="jquery-ui">jQuery UI laden</li>
<li data-step="modules">Anwendungsmodule laden</li> <li data-step="modules">Anwendungsmodule laden</li>
@@ -55,10 +121,6 @@
<li data-step="routes">Routen laden</li> <li data-step="routes">Routen laden</li>
<li data-step="location">Standort ermitteln</li> <li data-step="location">Standort ermitteln</li>
</ol> </ol>
<p id="bootstrap-ready" class="bootstrap-ready" hidden>Alles ist bereit. Tippe oder klicke auf diese Startseite, um die Anwendung zu öffnen.</p>
<p id="bootstrap-error" hidden></p>
<button id="bootstrap-retry" type="button" hidden>Erneut versuchen</button>
<button id="bootstrap-enter" type="button" hidden>Anwendung öffnen</button>
</section> </section>
</div> </div>
<main id="app-shell" hidden> <main id="app-shell" hidden>
+61 -1
View File
@@ -3,6 +3,14 @@
const versions = Object.freeze({ app: '0.12.14', jquery: '4.0.0', jqueryUi: '1.14.2' }); const versions = Object.freeze({ app: '0.12.14', jquery: '4.0.0', jqueryUi: '1.14.2' });
const steps = ['jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location']; const steps = ['jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location'];
const stepLabels = Object.freeze({
jquery: 'jQuery laden …',
'jquery-ui': 'jQuery UI laden …',
modules: 'Anwendungsmodule laden …',
server: 'Server prüfen …',
routes: 'Routen laden …',
location: 'Standort ermitteln …'
});
const screen = document.getElementById('bootstrap-screen'); const screen = document.getElementById('bootstrap-screen');
const progress = document.getElementById('bootstrap-progress'); const progress = document.getElementById('bootstrap-progress');
const errorBox = document.getElementById('bootstrap-error'); const errorBox = document.getElementById('bootstrap-error');
@@ -10,16 +18,54 @@
const enter = document.getElementById('bootstrap-enter'); const enter = document.getElementById('bootstrap-enter');
const readyText = document.getElementById('bootstrap-ready'); const readyText = document.getElementById('bootstrap-ready');
const cleanupStatus = document.getElementById('storage-cleanup-status'); const cleanupStatus = document.getElementById('storage-cleanup-status');
const actionStatus = document.getElementById('bootstrap-action-status');
const statusText = document.getElementById('bootstrap-status-text');
let ready = false; let ready = false;
let statusTransition = 0;
const versioned = path => `${path}${path.includes('?') ? '&' : '?'}v=${encodeURIComponent(versions.app)}`; const versioned = path => `${path}${path.includes('?') ? '&' : '?'}v=${encodeURIComponent(versions.app)}`;
function setStatusText(text, immediate = false) {
if (!statusText) return;
statusTransition += 1;
const transition = statusTransition;
if (immediate) {
statusText.textContent = text;
statusText.classList.remove('is-fading');
return;
}
statusText.classList.add('is-fading');
globalThis.setTimeout(() => {
if (transition !== statusTransition) return;
statusText.textContent = text;
statusText.classList.remove('is-fading');
}, 180);
}
function nextPendingStep() {
return steps.find(step => !document.querySelector(`[data-step="${step}"]`)?.classList.contains('done'));
}
function announceStep(step) {
if (stepLabels[step]) setStatusText(stepLabels[step]);
}
function mark(step, state) { function mark(step, state) {
const item = document.querySelector(`[data-step="${step}"]`); const item = document.querySelector(`[data-step="${step}"]`);
if (!item) return; if (!item) return;
item.classList.remove('done', 'failed'); item.classList.remove('done', 'failed');
item.classList.add(state); item.classList.add(state);
item.dataset.state = state;
progress.value = document.querySelectorAll('#bootstrap-steps .done').length; progress.value = document.querySelectorAll('#bootstrap-steps .done').length;
if (state === 'failed') {
setStatusText(`Fehler bei: ${stepLabels[step] || step}`);
return;
}
if (state === 'done') {
const next = nextPendingStep();
if (next) announceStep(next);
}
} }
function loadScript(src, test) { function loadScript(src, test) {
@@ -78,7 +124,14 @@
screen.setAttribute('aria-busy', 'false'); screen.setAttribute('aria-busy', 'false');
screen.classList.add('ready'); screen.classList.add('ready');
readyText.hidden = false; readyText.hidden = false;
setStatusText('Vorbereitung abgeschlossen.');
actionStatus?.classList.add('is-leaving');
enter.hidden = false; enter.hidden = false;
globalThis.requestAnimationFrame(() => enter.classList.add('is-visible'));
globalThis.setTimeout(() => {
if (actionStatus) actionStatus.hidden = true;
}, 220);
enter.focus(); enter.focus();
} }
@@ -88,11 +141,18 @@
screen.classList.remove('ready'); screen.classList.remove('ready');
readyText.hidden = true; readyText.hidden = true;
enter.hidden = true; enter.hidden = true;
enter.classList.remove('is-visible');
errorBox.hidden = true; errorBox.hidden = true;
retry.hidden = true; retry.hidden = true;
if (actionStatus) {
actionStatus.hidden = false;
actionStatus.classList.remove('is-leaving');
}
try { try {
setStatusText('Lokalen Speicher prüfen …', true);
await cleanupOldProgress(); await cleanupOldProgress();
announceStep('jquery');
await loadScript( await loadScript(
`vendor/jquery/jquery-${versions.jquery}.min.js`, `vendor/jquery/jquery-${versions.jquery}.min.js`,
@@ -116,7 +176,7 @@
showReadyState(); showReadyState();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
const current = steps.find(step => !document.querySelector(`[data-step="${step}"]`)?.classList.contains('done')); const current = nextPendingStep();
if (current) mark(current, 'failed'); if (current) mark(current, 'failed');
errorBox.textContent = error.message; errorBox.textContent = error.message;
errorBox.hidden = false; errorBox.hidden = false;
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-https://wegwichtel.de/api}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
GPX_FILE="$ROOT_DIR/examples/presentation-route.gpx"
JSON_FILE="$ROOT_DIR/examples/presentation-route.json"
for command in curl python3; do
command -v "$command" >/dev/null 2>&1 || {
echo "Fehler: $command wird benötigt." >&2
exit 1
}
done
read_json() {
python3 - "$JSON_FILE" "$1" <<'PY'
import json
import sys
with open(sys.argv[1], encoding='utf-8') as handle:
data = json.load(handle)
value = data
for part in sys.argv[2].split('.'):
value = value[int(part)] if part.isdigit() else value[part]
print(value)
PY
}
NAME="$(read_json name)"
DESCRIPTION="$(read_json description)"
echo "Lege Beispielstrecke über $BASE_URL an …"
ROUTE_RESPONSE="$(curl --fail --silent --show-error \
-X POST "$BASE_URL/routes" \
-F "name=$NAME" \
-F "description=$DESCRIPTION" \
-F "gpx=@$GPX_FILE;type=application/gpx+xml")"
ROUTE_ID="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"$ROUTE_RESPONSE")"
rollback() {
echo "Upload fehlgeschlagen. Entferne die unvollständige Beispielstrecke $ROUTE_ID …" >&2
curl --fail --silent --show-error -X DELETE "$BASE_URL/routes/$ROUTE_ID" >/dev/null || true
}
trap rollback ERR
POI_COUNT="$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1], encoding="utf-8"))["pois"]))' "$JSON_FILE")"
for ((index=0; index<POI_COUNT; index+=1)); do
TITLE="$(read_json "pois.$index.title")"
POI_DESCRIPTION="$(read_json "pois.$index.description")"
LAT="$(read_json "pois.$index.lat")"
LON="$(read_json "pois.$index.lon")"
RADIUS="$(read_json "pois.$index.triggerRadiusM")"
SEQUENCE="$(read_json "pois.$index.sequence")"
curl --fail --silent --show-error \
-X POST "$BASE_URL/routes/$ROUTE_ID/pois" \
-F "title=$TITLE" \
-F "description=$POI_DESCRIPTION" \
-F "lat=$LAT" \
-F "lon=$LON" \
-F "triggerRadiusM=$RADIUS" \
-F "sequence=$SEQUENCE" >/dev/null
echo " Trackpunkt $((index + 1))/$POI_COUNT: $TITLE"
done
trap - ERR
cat <<EOF
Beispielstrecke erfolgreich angelegt.
Server-ID: $ROUTE_ID
API: $BASE_URL/routes/$ROUTE_ID
Hinweis: POST /routes setzt activated_at nicht. Die Strecke bleibt daher zunächst unveröffentlicht und ist für die serverseitige Prüfung vorgesehen.
EOF
+42
View File
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '..');
async function read(relativePath) {
return fs.readFile(path.join(root, relativePath), 'utf8');
}
test('start screen uses the presentation-style hero and one action slot', async () => {
const html = await read('public/index.html');
assert.match(html, /class="bootstrap-brand"/);
assert.match(html, /class="bootstrap-hero"/);
assert.match(html, /class="bootstrap-features"/);
assert.match(html, /id="bootstrap-action-status"/);
assert.match(html, /id="bootstrap-status-text"/);
assert.match(html, /id="bootstrap-enter"[^>]*hidden/);
assert.match(html, />Los geht’s</);
assert.match(html, /id="bootstrap-steps"[^>]*hidden/);
assert.match(html, /id="bootstrap-progress"[^>]*hidden/);
});
test('bootstrap status text fades between loading steps and is replaced by the start button', async () => {
const css = await read('public/css/bootstrap.css');
const loader = await read('public/js/bootstrap-loader.js');
const statusTextRule = css.match(/\.bootstrap-status-text\s*\{([\s\S]*?)\}/)?.[1] ?? '';
const fadingRule = css.match(/\.bootstrap-status-text\.is-fading\s*\{([\s\S]*?)\}/)?.[1] ?? '';
assert.match(statusTextRule, /transition:\s*opacity/);
assert.match(fadingRule, /opacity:\s*0/);
assert.match(loader, /statusText\.classList\.add\('is-fading'\)/);
assert.match(loader, /statusText\.classList\.remove\('is-fading'\)/);
assert.match(loader, /server:\s*'Server prüfen …'/);
assert.match(loader, /routes:\s*'Routen laden …'/);
assert.match(loader, /location:\s*'Standort ermitteln …'/);
assert.match(loader, /actionStatus\?\.classList\.add\('is-leaving'\)/);
assert.match(loader, /enter\.classList\.add\('is-visible'\)/);
});
+22
View File
@@ -0,0 +1,22 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
const root = path.resolve(import.meta.dirname, '..');
async function read(relativePath) {
return fs.readFile(path.join(root, relativePath), 'utf8');
}
test('presentation example uses real GPX track points and the existing upload API', async () => {
const gpx = await read('examples/presentation-route.gpx');
const metadata = JSON.parse(await read('examples/presentation-route.json'));
const script = await read('scripts/upload-presentation-route.sh');
assert.ok((gpx.match(/<trkpt\b/g) || []).length >= 8);
assert.doesNotMatch(gpx, /<rtept\b/);
assert.ok(metadata.pois.length >= 3);
assert.match(script, /POST "\$BASE_URL\/routes"/);
assert.match(script, /POST "\$BASE_URL\/routes\/\$ROUTE_ID\/pois"/);
});