Fix mobile view Bugs and add versions to static files for cache breaking

This commit is contained in:
Florian Zumpe 2026-06-17 17:29:39 +02:00
parent 13770f21e3
commit 7760ae4f49
12 changed files with 77 additions and 48 deletions

View File

@ -1,11 +0,0 @@
[run]
branch = True
relative_files = True
source = tools/python
[report]
show_missing = True
skip_empty = True
[xml]
output = coverage/python/coverage.xml

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "wegwichtel-next", "name": "wegwichtel-next",
"version": "0.12.10", "version": "0.12.11",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wegwichtel-next", "name": "wegwichtel-next",
"version": "0.12.10", "version": "0.12.11",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@file-type/av": "0.2.0", "@file-type/av": "0.2.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "wegwichtel-next", "name": "wegwichtel-next",
"version": "0.12.10", "version": "0.12.11",
"private": true, "private": true,
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.", "description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
"type": "module", "type": "module",

View File

@ -327,7 +327,7 @@ button,
margin: 0 0 1rem; margin: 0 0 1rem;
} }
#route-page fieldset button { #route-page fieldset button.ui-button {
display: grid; display: grid;
place-items: center; place-items: center;
min-width: 0; min-width: 0;
@ -341,18 +341,18 @@ button,
touch-action: manipulation; touch-action: manipulation;
} }
#route-page fieldset button:focus-visible { #route-page fieldset button.ui-button:focus-visible {
outline: .2rem solid var(--focus); outline: .2rem solid var(--focus);
outline-offset: .15rem; outline-offset: .15rem;
} }
#route-page fieldset button:disabled { #route-page fieldset button.ui-button:disabled {
cursor: default; cursor: default;
filter: grayscale(.65); filter: grayscale(.65);
opacity: .42; opacity: .42;
} }
#route-page fieldset img { #route-page fieldset button.ui-button img {
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
pointer-events: none; pointer-events: none;
@ -397,17 +397,18 @@ button,
margin: 0; margin: 0;
} }
#slideshow img { #slide-image {
display: block; display: block;
width: 100%; width: 100%;
height: min(55vh, 28rem);
height: min(55dvh, 28rem); height: min(55dvh, 28rem);
object-fit: contain; object-fit: contain;
background: #111; background: #111;
} }
#slideshow > fieldset { #slideshow > fieldset {
display: flex; display: grid;
flex-flow: row nowrap; grid-template-columns: var(--icon-button-size) minmax(0, 1fr) var(--icon-button-size);
align-items: center; align-items: center;
gap: .5rem; gap: .5rem;
width: 100%; width: 100%;
@ -419,8 +420,6 @@ button,
#slideshow > fieldset > button.ui-button { #slideshow > fieldset > button.ui-button {
display: grid; display: grid;
flex: 0 0 var(--icon-button-size);
align-self: center;
place-items: center; place-items: center;
width: var(--icon-button-size); width: var(--icon-button-size);
height: var(--icon-button-size); height: var(--icon-button-size);
@ -438,7 +437,6 @@ button,
#slideshow > fieldset > div { #slideshow > fieldset > div {
display: flex; display: flex;
flex: 1 1 0;
align-self: stretch; align-self: stretch;
min-width: 0; min-width: 0;
min-height: var(--icon-button-size); min-height: var(--icon-button-size);
@ -466,10 +464,16 @@ button,
} }
#poi-audio { #poi-audio {
display: block;
width: 100%; width: 100%;
max-width: 100%;
margin-top: 1rem; margin-top: 1rem;
} }
#poi-audio[hidden] {
display: none;
}
#navigation-footer { #navigation-footer {
position: fixed; position: fixed;
right: 0; right: 0;

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#315d3a"> <meta name="theme-color" content="#315d3a">
<title>Wegwichtel</title> <title>Wegwichtel</title>
<link rel="stylesheet" href="css/bootstrap.css"> <link rel="stylesheet" href="css/bootstrap.css?v=0.12.11">
</head> </head>
<body> <body>
<div id="bootstrap-screen" aria-live="polite" aria-busy="true"> <div id="bootstrap-screen" aria-live="polite" aria-busy="true">
@ -84,13 +84,13 @@
<fieldset> <fieldset>
<legend>Routensteuerung</legend> <legend>Routensteuerung</legend>
<button id="start-route" type="button" aria-label="Route starten" title="Route starten"> <button id="start-route" type="button" aria-label="Route starten" title="Route starten">
<img src="images/icons/play.svg" alt=""> <img src="images/icons/play.svg" alt="Route starten">
</button> </button>
<button id="pause-route" type="button" aria-label="Route pausieren" title="Route pausieren" disabled> <button id="pause-route" type="button" aria-label="Route pausieren" title="Route pausieren" disabled>
<img src="images/icons/pause.svg" alt=""> <img src="images/icons/pause.svg" alt="Route pausieren">
</button> </button>
<button id="stop-route" type="button" aria-label="Route beenden" title="Route beenden" disabled> <button id="stop-route" type="button" aria-label="Route beenden" title="Route beenden" disabled>
<img src="images/icons/stop.svg" alt=""> <img src="images/icons/stop.svg" alt="Route beenden">
</button> </button>
</fieldset> </fieldset>
<p id="tracking-status" class="notice" hidden></p> <p id="tracking-status" class="notice" hidden></p>
@ -117,20 +117,20 @@
<fieldset> <fieldset>
<legend>Bildnavigation</legend> <legend>Bildnavigation</legend>
<button id="slide-prev" type="button" aria-label="Vorheriges Bild" title="Vorheriges Bild"> <button id="slide-prev" type="button" aria-label="Vorheriges Bild" title="Vorheriges Bild">
<img src="images/icons/chevron-left.svg" alt=""> <img src="images/icons/chevron-left.svg" alt="Vorheriges Bild">
</button> </button>
<div> <div>
<p id="slide-caption"></p> <p id="slide-caption"></p>
<span id="slide-position">0 / 0</span> <span id="slide-position">0 / 0</span>
</div> </div>
<button id="slide-next" type="button" aria-label="Nächstes Bild" title="Nächstes Bild"> <button id="slide-next" type="button" aria-label="Nächstes Bild" title="Nächstes Bild">
<img src="images/icons/chevron-right.svg" alt=""> <img src="images/icons/chevron-right.svg" alt="Nächstes Bild">
</button> </button>
</fieldset> </fieldset>
</div> </div>
<p id="poi-description"></p> <p id="poi-description"></p>
<audio id="poi-audio" controls preload="auto" hidden></audio> <audio id="poi-audio" controls preload="metadata" aria-label="Audiowiedergabe zur Station" hidden>Audio kann auf diesem Gerät nicht wiedergegeben werden.</audio>
</div> </div>
</section> </section>
@ -145,6 +145,6 @@
</footer> </footer>
</main> </main>
<script src="js/bootstrap-loader.js"></script> <script src="js/bootstrap-loader.js?v=0.12.11"></script>
</body> </body>
</html> </html>

View File

@ -5,12 +5,18 @@
return $('#poi-audio')[0]; return $('#poi-audio')[0];
} }
function mediaUrl(url) {
if (!url) return '';
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}v=${Date.now()}`;
}
function unload() { function unload() {
const audio = element(); const audio = element();
audio.pause(); audio.pause();
audio.removeAttribute('src'); audio.removeAttribute('src');
audio.load(); audio.load();
$(audio).prop('hidden', true); $(audio).attr('hidden', '').hide();
} }
ns.AudioPlayer = { ns.AudioPlayer = {
@ -19,8 +25,8 @@
if (!url) return; if (!url) return;
const audio = element(); const audio = element();
audio.src = url; audio.src = mediaUrl(url);
$(audio).prop('hidden', false); $(audio).removeAttr('hidden').css('display', 'block');
audio.load(); audio.load();
}, },

View File

@ -1,12 +1,14 @@
(function () { (function () {
'use strict'; 'use strict';
const versions = Object.freeze({ jquery: '4.0.0', jqueryUi: '1.14.2' }); const versions = Object.freeze({ app: '0.12.11', 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 progress = document.getElementById('bootstrap-progress'); const progress = document.getElementById('bootstrap-progress');
const errorBox = document.getElementById('bootstrap-error'); const errorBox = document.getElementById('bootstrap-error');
const retry = document.getElementById('bootstrap-retry'); const retry = document.getElementById('bootstrap-retry');
const versioned = path => `${path}${path.includes('?') ? '&' : '?'}v=${encodeURIComponent(versions.app)}`;
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;
@ -18,7 +20,7 @@
function loadScript(src, test) { function loadScript(src, test) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = src; script.src = versioned(src);
script.async = false; script.async = false;
script.onload = () => { script.onload = () => {
if (test && !test()) reject(new Error(`Selbsttest fehlgeschlagen: ${src}`)); if (test && !test()) reject(new Error(`Selbsttest fehlgeschlagen: ${src}`));
@ -33,7 +35,7 @@
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
link.href = href; link.href = versioned(href);
link.onload = resolve; link.onload = resolve;
link.onerror = () => reject(new Error(`Stylesheet konnte nicht geladen werden: ${href}`)); link.onerror = () => reject(new Error(`Stylesheet konnte nicht geladen werden: ${href}`));
document.head.appendChild(link); document.head.appendChild(link);

View File

@ -5,6 +5,12 @@
let index = 0; let index = 0;
let swipe = null; let swipe = null;
function mediaUrl(url) {
if (!url) return '';
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}v=${Date.now()}`;
}
function activate(selector, handler) { function activate(selector, handler) {
$(document) $(document)
.on('pointerup', selector, function (event) { .on('pointerup', selector, function (event) {
@ -33,8 +39,12 @@
const image = images[index]; const image = images[index];
$('#slide-image') $('#slide-image')
.attr('src', image.url) .attr('src', mediaUrl(image.url))
.attr('alt', image.caption || 'Bild zur Station'); .attr('alt', image.caption || 'Bild zur Station')
.off('error.wegwichtel')
.on('error.wegwichtel', function () {
$(this).removeAttr('src').attr('alt', 'Bild konnte nicht geladen werden');
});
$('#slide-caption').text(image.caption || ''); $('#slide-caption').text(image.caption || '');
$('#slide-position').text(`${index + 1} / ${images.length}`); $('#slide-position').text(`${index + 1} / ${images.length}`);
refreshButton('#slide-prev', images.length < 2); refreshButton('#slide-prev', images.length < 2);

View File

@ -1 +0,0 @@
coverage==7.13.3

View File

@ -18,7 +18,19 @@ app.use(express.json({
})); }));
app.use(express.urlencoded({ extended: true, limit: '2mb' })); app.use(express.urlencoded({ extended: true, limit: '2mb' }));
app.use('/api', createApiRouter(db)); app.use('/api', createApiRouter(db));
app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] })); app.use(express.static(path.join(__dirname, 'public'), {
extensions: ['html'],
etag: true,
lastModified: true,
maxAge: 0,
setHeaders(res, filePath) {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
return;
}
res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
}
}));
app.use(notFoundHandler); app.use(notFoundHandler);
app.use(errorHandler); app.use(errorHandler);

View File

@ -20,11 +20,12 @@ function sendFileResource(res, file, contentType = null) {
if (contentType) res.type(contentType); if (contentType) res.type(contentType);
res.setHeader('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.filename)}`); res.setHeader('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.filename)}`);
res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Cache-Control', 'no-cache, max-age=0, must-revalidate');
return res.sendFile(file.absolutePath, { return res.sendFile(file.absolutePath, {
acceptRanges: true, acceptRanges: true,
cacheControl: true, cacheControl: true,
immutable: false, immutable: false,
maxAge: '1h' maxAge: 0
}); });
} }

View File

@ -168,7 +168,9 @@ test('reaching a POI preloads audio without autoplay and uses the vibration wrap
assert.doesNotMatch(app, /navigator\.vibrate/); assert.doesNotMatch(app, /navigator\.vibrate/);
assert.match(app, /function stopRouteSensors\(\)[\s\S]*ns\.Vibration\.stop\(\)/); assert.match(app, /function stopRouteSensors\(\)[\s\S]*ns\.Vibration\.stop\(\)/);
assert.match(audio, /audio\.load\(\)/); assert.match(audio, /audio\.load\(\)/);
assert.match(html, /<audio id="poi-audio" controls preload="auto" hidden>/); assert.match(html, /<audio id="poi-audio" controls preload="metadata" aria-label="Audiowiedergabe zur Station" hidden>/);
assert.match(audio, /removeAttr\('hidden'\)/);
assert.match(audio, /Date\.now\(\)/);
assert.match(loader, /'vibration'/); assert.match(loader, /'vibration'/);
}); });
@ -182,12 +184,16 @@ test('POI page uses icon-based back and slideshow controls in a single responsiv
assert.match(html, /id="slide-prev"[\s\S]*images\/icons\/chevron-left\.svg/); assert.match(html, /id="slide-prev"[\s\S]*images\/icons\/chevron-left\.svg/);
assert.match(html, /id="slide-next"[\s\S]*images\/icons\/chevron-right\.svg/); assert.match(html, /id="slide-next"[\s\S]*images\/icons\/chevron-right\.svg/);
assert.match(html, /<fieldset>[\s\S]*<legend>Bildnavigation<\/legend>/); assert.match(html, /<fieldset>[\s\S]*<legend>Bildnavigation<\/legend>/);
assert.match(css, /#slideshow > fieldset[\s\S]*display:\s*flex/); assert.match(css, /#slideshow > fieldset[\s\S]*display:\s*grid/);
assert.match(css, /#slideshow > fieldset[\s\S]*flex-flow:\s*row nowrap/); assert.match(css, /grid-template-columns:\s*var\(--icon-button-size\) minmax\(0, 1fr\) var\(--icon-button-size\)/);
assert.match(css, /#slideshow > fieldset[\s\S]*align-items:\s*center/); assert.match(css, /#slideshow > fieldset[\s\S]*align-items:\s*center/);
assert.match(css, /#slideshow > fieldset > button\.ui-button[\s\S]*flex:\s*0 0 var\(--icon-button-size\)/); assert.match(css, /#slideshow > fieldset > button\.ui-button[\s\S]*width:\s*var\(--icon-button-size\)/);
assert.match(css, /#slideshow > fieldset > div[\s\S]*flex:\s*1 1 0/); assert.match(css, /#slideshow > fieldset > div[\s\S]*min-width:\s*0/);
assert.match(css, /#slideshow > fieldset > div[\s\S]*justify-content:\s*center/); assert.match(css, /#slideshow > fieldset > div[\s\S]*justify-content:\s*center/);
assert.match(css, /#slide-caption[\s\S]*overflow-wrap:\s*anywhere/); assert.match(css, /#slide-caption[\s\S]*overflow-wrap:\s*anywhere/);
assert.match(css, /#slide-image[\s\S]*object-fit:\s*contain/);
assert.doesNotMatch(css, /#slideshow img\s*\{/);
assert.match(html, /images\/icons\/play\.svg" alt="Route starten"/);
assert.match(html, /images\/icons\/chevron-left\.svg" alt="Vorheriges Bild"/);
assert.doesNotMatch(loader, /controlgroup/); assert.doesNotMatch(loader, /controlgroup/);
}); });