feat: BL-07–BL-10 — notes search, weather refresh, vCard import/export, PWA offline page

- Notes: client-side full-text search bar (filters title + content)
- Dashboard: weather refresh button + 30-min auto-refresh interval
- Contacts: vCard 3.0 export per contact (GET /:id/vcard); vCard import
  via file input with client-side parser (FN, TEL, EMAIL, ADR, NOTE, CATEGORIES)
- PWA: /offline.html served when network unavailable; cached in app-shell (sw v20)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ulas
2026-03-31 10:35:03 +02:00
parent 0defc3c589
commit 4fe4f6cb38
10 changed files with 374 additions and 10 deletions
+11 -4
View File
@@ -12,9 +12,9 @@
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
*/
const SHELL_CACHE = 'oikos-shell-v19';
const PAGES_CACHE = 'oikos-pages-v19';
const ASSETS_CACHE = 'oikos-assets-v19';
const SHELL_CACHE = 'oikos-shell-v20';
const PAGES_CACHE = 'oikos-pages-v20';
const ASSETS_CACHE = 'oikos-assets-v20';
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
// App-Shell: sofort benötigt für ersten Render
@@ -41,6 +41,7 @@ const APP_SHELL = [
'/styles/budget.css',
'/styles/settings.css',
'/components/oikos-install-prompt.js',
'/offline.html',
'/manifest.json',
'/favicon.ico',
'/icons/favicon-32.png',
@@ -157,6 +158,10 @@ async function networkFirst(request, cacheName) {
const shell = await cache.match('/index.html');
if (shell) return shell;
// Letzter Ausweg: Offline-Seite
const offline = await caches.match('/offline.html');
if (offline) return offline;
return new Response('Keine Verbindung', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
@@ -192,10 +197,12 @@ async function staleWhileRevalidate(request, cacheName) {
const networkResponse = await networkPromise;
if (networkResponse) return networkResponse;
// Offline-Fallback: SPA-Shell für Navigation
// Offline-Fallback für Navigation
if (request.mode === 'navigate') {
const shell = await caches.match('/index.html');
if (shell) return shell;
const offline = await caches.match('/offline.html');
if (offline) return offline;
}
// Letzter Ausweg: leere 503-Antwort statt Promise-Rejection