Compare commits

..

2 Commits

Author SHA1 Message Date
elpatron f01c5dc86f chore: release v0.1.0.4 2026-05-29 17:40:31 +02:00
elpatron 1f089fdaa7 feat: PWA-Updates erkennen und Nutzer zum Reload auffordern.
Wechselt auf prompt-Modus mit Update-Banner, periodischer SW-Prüfung und no-cache-Headern für Service Worker und index.html.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 17:40:23 +02:00
10 changed files with 237 additions and 4 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.0.4
0.1.0.5
+11
View File
@@ -3,6 +3,17 @@ server {
server_name localhost;
client_max_body_size 50M;
# Service worker and app shell must revalidate so PWA updates are detected
location ~* ^/(sw\.js|workbox-.*\.js|manifest\.webmanifest)$ {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
location = /index.html {
root /usr/share/nginx/html;
add_header Cache-Control "no-cache, must-revalidate";
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
+108
View File
@@ -2396,6 +2396,114 @@ html.theme-cupertino .events-scroll-container {
}
}
/* PWA update prompt */
.pwa-update-banner {
position: fixed;
top: calc(12px + env(safe-area-inset-top, 0px));
left: 16px;
right: 16px;
z-index: 1300;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 14px;
align-items: center;
padding: 14px 44px 14px 14px;
border-radius: 14px;
border: 1px solid var(--app-accent-border);
background: var(--app-surface);
box-shadow: var(--app-card-shadow);
animation: fadeIn 0.35s ease-out;
max-width: 640px;
margin: 0 auto;
}
.pwa-update-icon {
color: var(--app-accent-light);
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 10px;
background: var(--app-accent-bg);
flex-shrink: 0;
}
.pwa-update-body {
min-width: 0;
}
.pwa-update-title {
margin: 0 0 4px 0;
font-size: 15px;
font-weight: 600;
color: var(--app-text-heading);
}
.pwa-update-text {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--app-text-muted);
}
.pwa-update-actions {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
}
.pwa-update-btn {
white-space: nowrap;
padding: 10px 14px;
font-size: 14px;
}
.pwa-update-link {
background: none;
border: none;
color: var(--app-text-muted);
font-size: 12px;
cursor: pointer;
padding: 2px 4px;
}
.pwa-update-link:hover {
color: var(--app-text);
}
.pwa-update-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: var(--app-text-muted);
cursor: pointer;
padding: 4px;
border-radius: 6px;
}
.pwa-update-close:hover {
color: var(--app-text);
background: var(--app-surface-inset);
}
@media (max-width: 720px) {
.pwa-update-banner {
grid-template-columns: auto 1fr;
padding-right: 40px;
}
.pwa-update-actions {
grid-column: 1 / -1;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
}
.app-version-footer {
position: fixed;
left: 0;
+2
View File
@@ -20,6 +20,7 @@ import {
import { startBackgroundSync, stopBackgroundSync, syncAllLogbooks, subscribeToSyncState } from './services/sync.js'
import ReadOnlyViewer from './components/ReadOnlyViewer.tsx'
import PwaInstallPrompt from './components/PwaInstallPrompt.tsx'
import PwaUpdatePrompt from './components/PwaUpdatePrompt.tsx'
import AppFooter from './components/AppFooter.tsx'
import { db } from './services/db.js'
import { useLiveQuery } from 'dexie-react-hooks'
@@ -318,6 +319,7 @@ function App() {
export default function AppWrapper() {
return (
<DialogProvider>
<PwaUpdatePrompt />
<App />
<AppFooter />
</DialogProvider>
+62
View File
@@ -0,0 +1,62 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RefreshCw, X } from 'lucide-react'
import { usePwaUpdate } from '../hooks/usePwaUpdate.js'
export default function PwaUpdatePrompt() {
const { t } = useTranslation()
const { needRefresh, updateApp } = usePwaUpdate()
const [updating, setUpdating] = useState(false)
const [dismissed, setDismissed] = useState(false)
if (!needRefresh || dismissed) return null
const handleUpdate = async () => {
setUpdating(true)
try {
await updateApp()
} finally {
setUpdating(false)
}
}
return (
<div className="pwa-update-banner" role="alert" aria-live="polite">
<div className="pwa-update-icon" aria-hidden="true">
<RefreshCw size={22} />
</div>
<div className="pwa-update-body">
<p className="pwa-update-title">{t('pwa.update_title')}</p>
<p className="pwa-update-text">{t('pwa.update_desc')}</p>
</div>
<div className="pwa-update-actions">
<button
type="button"
className="btn primary pwa-update-btn"
onClick={handleUpdate}
disabled={updating}
>
{updating ? t('pwa.update_reloading') : t('pwa.update_now')}
</button>
<button
type="button"
className="pwa-update-link"
onClick={() => setDismissed(true)}
>
{t('pwa.later')}
</button>
</div>
<button
type="button"
className="pwa-update-close"
onClick={() => setDismissed(true)}
aria-label={t('pwa.later')}
>
<X size={18} />
</button>
</div>
)
}
+37
View File
@@ -0,0 +1,37 @@
import { useRegisterSW } from 'virtual:pwa-register/react'
const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000
function scheduleUpdateChecks(registration: ServiceWorkerRegistration) {
const checkForUpdate = () => {
registration.update().catch(() => {})
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
checkForUpdate()
}
})
window.setInterval(checkForUpdate, UPDATE_CHECK_INTERVAL_MS)
}
export function usePwaUpdate() {
const {
needRefresh: [needRefresh],
updateServiceWorker
} = useRegisterSW({
immediate: true,
onRegisteredSW(_swUrl: string, registration: ServiceWorkerRegistration | undefined) {
if (registration) {
scheduleUpdateChecks(registration)
}
}
})
const updateApp = async () => {
await updateServiceWorker(true)
}
return { needRefresh, updateApp }
}
+5 -1
View File
@@ -66,7 +66,11 @@
"platform_ios": "Installation über Safari",
"platform_android": "Installation über den Browser",
"platform_desktop": "Installation als Desktop-App",
"settings_section": "App-Installation"
"settings_section": "App-Installation",
"update_title": "Update verfügbar",
"update_desc": "Eine neue Version von Kapteins Daagbok ist bereit. Bitte aktualisieren, um die neuesten Änderungen zu erhalten.",
"update_now": "Jetzt aktualisieren",
"update_reloading": "Wird geladen…"
},
"sync": {
"status_synced": "Synchronisiert",
+5 -1
View File
@@ -66,7 +66,11 @@
"platform_ios": "Install via Safari",
"platform_android": "Install via browser",
"platform_desktop": "Install as desktop app",
"settings_section": "App installation"
"settings_section": "App installation",
"update_title": "Update available",
"update_desc": "A new version of Kapteins Daagbok is ready. Reload to get the latest changes.",
"update_now": "Reload now",
"update_reloading": "Reloading…"
},
"sync": {
"status_synced": "Synced",
+1
View File
@@ -1,3 +1,4 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pwa/react" />
declare const __APP_VERSION__: string
+5 -1
View File
@@ -38,8 +38,12 @@ export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
registerType: 'prompt',
includeAssets: ['favicon.ico', 'logo.png'],
workbox: {
cleanupOutdatedCaches: true,
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2,webmanifest}']
},
manifest: {
name: 'Kapteins Daagbok',
short_name: 'Daagbok',