feat: PWA Cache-Busting via filemtime (claude-db #201) — kein manuelles Hochzählen mehr
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 1s
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 1s
Das bisherige 'CACHE = baustelle-vN' Pattern erforderte, dass ich bei jeder Änderung drei Stellen synchron halte (sw.js, index.html ?v=, manifest). Eddy hat klargestellt dass das nicht akzeptabel ist. Fix nach KB #201 Pattern (referenz: dolibarr.stundenzettel v2.2.0): 1. index.html → index.php PHP berechnet bei jedem Request filemtime() von app.css, app.js, lib/*.js, manifest.webmanifest. Die mtimes kommen automatisch beim Deploy (rsync preserviert sie default) und werden als ?v= an alle Asset-URLs gehängt. 2. sw.js liest Version aus eigener URL-Query: const SW_VERSION = (new URL(self.location.href)).searchParams.get('v') const CACHE = 'baustelle-' + SW_VERSION activate() löscht alle caches die mit 'baustelle-' anfangen aber nicht der aktuelle sind. 3. Client-Registration mit Auto-Update: - setInterval 60s reg.update() - visibilitychange-Listener für Tab-Fokus - updatefound → SKIP_WAITING postMessage - controllerchange → einmaliger location.reload 4. SHELL pre-cache enthält nur statische Dateien (index.php, share.html, icons). CSS/JS werden beim ersten fetch dynamisch gecached — so gibt es keinen Mix zwischen alten und neuen ?v= Versionen. 5. manifest.webmanifest start_url auf /custom/baustelle/index.php Ergebnis: Ich deploye → mtime ändert sich → neue URLs → Browser holt frische Files → SW aktiviert automatisch beim nächsten Tab-Fokus. Nie wieder manuelles Hochzählen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> [deploy]
This commit is contained in:
parent
5e80d78f41
commit
7c9563ec90
4 changed files with 109 additions and 83 deletions
70
index.html
70
index.html
|
|
@ -1,70 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<meta name="theme-color" content="#1a1a1f">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>Baustelle</title>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="icon" type="image/svg+xml" href="icons/icon.svg">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="icons/icon-192.png">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="icons/icon-512.png">
|
||||
<link rel="apple-touch-icon" href="icons/icon-192.png">
|
||||
<link rel="stylesheet" href="app.css?v=9">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<header id="topbar">
|
||||
<button id="back-btn" class="icon-btn" style="display:none">←</button>
|
||||
<h1 id="page-title">Baustelle</h1>
|
||||
<button id="help-btn" class="icon-btn" title="Hilfe">❓</button>
|
||||
<span id="status-badge">🟢</span>
|
||||
</header>
|
||||
|
||||
<main id="main"></main>
|
||||
|
||||
<nav id="bottom-nav" style="display:none">
|
||||
<button data-route="orders" class="active">📋 Aufträge</button>
|
||||
<button data-route="reports">📑 Berichte</button>
|
||||
<button data-route="settings">⚙️</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<script src="lib/idb.js?v=9"></script>
|
||||
<script src="lib/api.js?v=9"></script>
|
||||
<script src="lib/offline.js?v=9"></script>
|
||||
<script src="lib/router.js?v=9"></script>
|
||||
<script src="app.js?v=9"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('sw.js').then(reg => {
|
||||
// Update-Check: wenn ein neuer SW wartet, sofort aktivieren
|
||||
reg.addEventListener('updatefound', () => {
|
||||
const nw = reg.installing;
|
||||
if (!nw) return;
|
||||
nw.addEventListener('statechange', () => {
|
||||
if (nw.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// Neue Version ist bereit — sofort übernehmen
|
||||
nw.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
});
|
||||
});
|
||||
// Beim Controller-Change einmal neu laden (damit neuer SW aktive Clients kontrolliert)
|
||||
let refreshed = false;
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (refreshed) return;
|
||||
refreshed = true;
|
||||
window.location.reload();
|
||||
});
|
||||
}).catch(e => console.warn('SW reg failed', e));
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
97
index.php
Normal file
97
index.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
/* Baustelle PWA — Einstiegsseite.
|
||||
* Cache-Versionierung via filemtime() nach Pattern claude-db #201.
|
||||
* Bei jedem Deploy ändern sich die mtimes → neue URLs → Browser holt frische Files.
|
||||
*/
|
||||
$cssVersion = @filemtime(__DIR__.'/app.css') ?: time();
|
||||
$jsVersion = max(
|
||||
@filemtime(__DIR__.'/app.js') ?: time(),
|
||||
@filemtime(__DIR__.'/lib/idb.js') ?: time(),
|
||||
@filemtime(__DIR__.'/lib/api.js') ?: time(),
|
||||
@filemtime(__DIR__.'/lib/offline.js')?: time(),
|
||||
@filemtime(__DIR__.'/lib/router.js') ?: time()
|
||||
);
|
||||
$manifestVersion = @filemtime(__DIR__.'/manifest.webmanifest') ?: time();
|
||||
$swVersion = max($cssVersion, $jsVersion, $manifestVersion);
|
||||
|
||||
// Einstiegsseite NIE cachen
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<meta name="theme-color" content="#1a1a1f">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>Baustelle</title>
|
||||
<link rel="manifest" href="manifest.webmanifest?v=<?php echo $manifestVersion; ?>">
|
||||
<link rel="icon" type="image/svg+xml" href="icons/icon.svg">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="icons/icon-192.png">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="icons/icon-512.png">
|
||||
<link rel="apple-touch-icon" href="icons/icon-192.png">
|
||||
<link rel="stylesheet" href="app.css?v=<?php echo $cssVersion; ?>">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<header id="topbar">
|
||||
<button id="back-btn" class="icon-btn" style="display:none">←</button>
|
||||
<h1 id="page-title">Baustelle</h1>
|
||||
<button id="help-btn" class="icon-btn" title="Hilfe">❓</button>
|
||||
<span id="status-badge">🟢</span>
|
||||
</header>
|
||||
|
||||
<main id="main"></main>
|
||||
|
||||
<nav id="bottom-nav" style="display:none">
|
||||
<button data-route="orders" class="active">📋 Aufträge</button>
|
||||
<button data-route="reports">📑 Berichte</button>
|
||||
<button data-route="settings">⚙️</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<script src="lib/idb.js?v=<?php echo $jsVersion; ?>"></script>
|
||||
<script src="lib/api.js?v=<?php echo $jsVersion; ?>"></script>
|
||||
<script src="lib/offline.js?v=<?php echo $jsVersion; ?>"></script>
|
||||
<script src="lib/router.js?v=<?php echo $jsVersion; ?>"></script>
|
||||
<script src="app.js?v=<?php echo $jsVersion; ?>"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('sw.js?v=<?php echo $swVersion; ?>').then(function (reg) {
|
||||
// Periodisch updates prüfen
|
||||
setInterval(function () { reg.update(); }, 60000);
|
||||
// Bei Tab-Wechsel zurück: sofort prüfen
|
||||
document.addEventListener('visibilitychange', function () {
|
||||
if (document.visibilityState === 'visible') reg.update();
|
||||
});
|
||||
// Neuer SW installed → sofort aktivieren
|
||||
reg.addEventListener('updatefound', function () {
|
||||
var nw = reg.installing;
|
||||
if (!nw) return;
|
||||
nw.addEventListener('statechange', function () {
|
||||
if (nw.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
nw.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}).catch(function (e) { console.warn('SW reg failed', e); });
|
||||
// Controller-Change → einmal neu laden
|
||||
var reloaded = false;
|
||||
navigator.serviceWorker.addEventListener('controllerchange', function () {
|
||||
if (reloaded) return;
|
||||
reloaded = true;
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
"short_name": "Baustelle",
|
||||
"description": "Mobile Doku für Baustellen — Fotos, Sprachnotizen, Skizzen",
|
||||
"id": "/custom/baustelle/",
|
||||
"start_url": "/custom/baustelle/",
|
||||
"start_url": "/custom/baustelle/index.php",
|
||||
"scope": "/custom/baustelle/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
|
|
|
|||
23
sw.js
23
sw.js
|
|
@ -1,25 +1,23 @@
|
|||
/* Baustelle PWA Service Worker — v9
|
||||
* Pattern nach claude-db #31:
|
||||
/* Baustelle PWA Service Worker
|
||||
* Pattern nach claude-db #201 (filemtime-basiert):
|
||||
* - Cache-Version kommt aus URL-Query ?v=<mtime>, kein manuelles Hochzählen
|
||||
* - Network-First für eigene Assets (immer aktuell, Fallback Cache)
|
||||
* - skipWaiting() + clients.claim() damit Updates sofort greifen
|
||||
* - Web Share Target via POST → share.html
|
||||
*/
|
||||
|
||||
const CACHE = 'baustelle-v10';
|
||||
// Cache-Version dynamisch aus ?v=<mtime> — wird von index.php gesetzt
|
||||
const SW_VERSION = (new URL(self.location.href)).searchParams.get('v') || 'static';
|
||||
const CACHE = 'baustelle-' + SW_VERSION;
|
||||
const SHELL = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./index.php',
|
||||
'./share.html',
|
||||
'./app.css',
|
||||
'./app.js',
|
||||
'./manifest.webmanifest',
|
||||
'./lib/idb.js',
|
||||
'./lib/api.js',
|
||||
'./lib/offline.js',
|
||||
'./lib/router.js',
|
||||
'./icons/icon-192.png',
|
||||
'./icons/icon-512.png',
|
||||
'./icons/icon.svg',
|
||||
// CSS/JS werden mit ?v=<mtime> geladen und dynamisch gecached
|
||||
// beim ersten fetch — nicht in SHELL damit Pre-Cache nicht mit alten v's läuft
|
||||
];
|
||||
|
||||
self.addEventListener('install', (e) => {
|
||||
|
|
@ -34,7 +32,8 @@ self.addEventListener('activate', (e) => {
|
|||
e.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys.filter(k => k !== CACHE).map(k => caches.delete(k))
|
||||
keys.filter(k => k.startsWith('baustelle-') && k !== CACHE)
|
||||
.map(k => caches.delete(k))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue