feat(pwa): start and keep working offline

The service worker now caches the app shell: page loads are network
first with the last index.html as fallback, hashed bundles come from the
cache, icons and manifest are refreshed in the background; API calls are
never intercepted. GET answers are kept per user in IndexedDB and served
when the server cannot be reached, so the app starts logged in with the
last known thoughts, tasks and lists. Captured thoughts and checked tasks
or list items go to an outbox that is sent in order when the connection
returns (also at start and every 30 s). A banner shows offline state and
waiting changes. Logging out clears cache and outbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 08:11:47 +01:00
1 parent 5e20710aa7
commit 7bba174454
9 files changed
+472 -76

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import React, { useEffect, useState } from 'react';
import { CloudOff, RefreshCw } from 'lucide-react';
import { outboxCount } from '../lib/offline';
// Small notice above the bottom navigation: offline (with the number of changes waiting), or
// "synchronisiere" while queued changes are being sent after the connection came back.
export const OfflineBanner: React.FC = () => {
const [online, setOnline] = useState(() => (typeof navigator === 'undefined' ? true : navigator.onLine));
const [pending, setPending] = useState(() => outboxCount());
useEffect(() => {
const update = () => {
setOnline(navigator.onLine);
setPending(outboxCount());
};
window.addEventListener('online', update);
window.addEventListener('offline', update);
window.addEventListener('mindsift:outbox-changed', update);
window.addEventListener('storage', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
window.removeEventListener('mindsift:outbox-changed', update);
window.removeEventListener('storage', update);
};
}, []);
if (online && pending === 0) return null;
const waiting = `${pending} ${pending === 1 ? 'Änderung wartet' : 'Änderungen warten'}`;
return (
<div
role="status"
className="fixed left-1/2 -translate-x-1/2 bottom-[calc(5.5rem+env(safe-area-inset-bottom))] sm:bottom-4 z-[60] max-w-[calc(100vw-1.5rem)] px-3 py-1.5 rounded-full border text-[11px] font-medium shadow-panel flex items-center gap-1.5 bg-panel-elevated border-status-warn/40 text-main"
>
{online ? (
<RefreshCw className="w-3.5 h-3.5 text-accent animate-spin shrink-0" />
) : (
<CloudOff className="w-3.5 h-3.5 text-status-warn shrink-0" />
)}
<span className="truncate">
{online
? `Synchronisiere – ${waiting}…`
: pending > 0
? `Offline · ${waiting} auf Verbindung`
: 'Offline – Änderungen werden bei Verbindung synchronisiert'}
</span>
</div>
);
};