release nouri 0.6.0 polish backup and pwa

This commit is contained in:
2026-04-12 17:46:18 +02:00
parent 9ff7a6d57c
commit 555fddab80
31 changed files with 1257 additions and 164 deletions
+14 -1
View File
@@ -6,9 +6,16 @@
"start_url": "/",
"scope": "/",
"display": "standalone",
"display_override": ["standalone", "minimal-ui"],
"background_color": "#fff6ef",
"theme_color": "#efab72",
"theme_color": "#de9862",
"categories": ["food", "lifestyle", "productivity"],
"icons": [
{
"src": "/static/brand/pwa-180.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/static/brand/pwa-192.png",
"sizes": "192x192",
@@ -18,6 +25,12 @@
"src": "/static/brand/pwa-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/static/brand/pwa-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+79
View File
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nouri offline</title>
<style>
:root {
color-scheme: light;
--bg: #fff6ef;
--surface: rgba(255, 255, 255, 0.92);
--line: rgba(126, 104, 85, 0.14);
--text: #352d2b;
--muted: #7d7069;
--accent: #de9862;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 1.5rem;
font-family: "Avenir Next", "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(255, 205, 174, 0.42), transparent 24rem),
radial-gradient(circle at 90% 8%, rgba(190, 226, 203, 0.34), transparent 24rem),
linear-gradient(180deg, var(--bg), #fdf0e6);
}
.card {
width: min(28rem, 100%);
padding: 1.4rem;
border-radius: 24px;
background: var(--surface);
border: 1px solid var(--line);
box-shadow: 0 22px 48px rgba(125, 92, 68, 0.12);
}
h1 {
margin: 0 0 0.45rem;
font-family: "Iowan Old Style", "Palatino Linotype", serif;
font-size: 2rem;
}
p {
margin: 0.35rem 0;
line-height: 1.55;
}
.muted {
color: var(--muted);
}
a {
display: inline-block;
margin-top: 1rem;
padding: 0.82rem 1.1rem;
border-radius: 999px;
background: var(--accent);
color: white;
text-decoration: none;
}
</style>
</head>
<body>
<main class="card">
<h1>Nouri ist gerade kurz offline</h1>
<p>Die App bleibt da und versucht es gleich wieder. Sobald die Verbindung zurück ist, kannst du normal weitermachen.</p>
<p class="muted">Ein Teil der Oberfläche ist schon lokal verfügbar. Für aktuelle Haushaltsdaten braucht Nouri aber wieder eine Verbindung.</p>
<a href="/">Erneut versuchen</a>
</main>
</body>
</html>
+36 -27
View File
@@ -1,15 +1,32 @@
const CACHE_NAME = "nouri-v0-5-1-1";
const CACHE_NAME = "nouri-v0-6-0";
const OFFLINE_URL = "/static/pwa/offline.html";
const STATIC_ASSETS = [
"/static/css/styles.css",
"/static/js/theme.js",
"/static/js/ui.js",
"/static/js/planner.js",
"/static/js/pwa.js",
"/static/brand/pwa-180.png",
"/static/brand/pwa-192.png",
"/static/brand/pwa-512.png",
"/static/brand/pwa-maskable-512.png",
"/static/brand/pwa-badge.png",
"/static/brand/favicon.svg",
"/app.webmanifest",
OFFLINE_URL,
];
const cacheFirst = async (request) => {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response && response.ok && response.type === "basic") {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
};
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)).then(() => self.skipWaiting())
@@ -28,40 +45,32 @@ self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
const requestUrl = new URL(event.request.url);
const isStaticAsset =
requestUrl.origin === self.location.origin &&
(
requestUrl.pathname.startsWith("/static/")
|| requestUrl.pathname === "/app.webmanifest"
|| requestUrl.pathname === "/service-worker.js"
);
const isSameOrigin = requestUrl.origin === self.location.origin;
const isStaticAsset = isSameOrigin && (
requestUrl.pathname.startsWith("/static/")
|| requestUrl.pathname === "/app.webmanifest"
|| requestUrl.pathname === "/service-worker.js"
);
const isUpload = isSameOrigin && requestUrl.pathname.startsWith("/uploads/");
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
fetch(event.request)
.then((response) => {
const copy = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, copy));
return response;
})
.catch(async () => {
return (await caches.match(event.request)) || caches.match(OFFLINE_URL);
})
);
return;
}
if (!isStaticAsset) {
return;
if (isStaticAsset || isUpload) {
event.respondWith(cacheFirst(event.request));
}
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) {
return cached;
}
return fetch(event.request).then((response) => {
if (!response || response.status !== 200 || response.type !== "basic") {
return response;
}
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
return response;
});
})
);
});
self.addEventListener("push", (event) => {