feat: Schnellerfassung vom Handy, auch ohne Netz

Ein Feld, ein Knopf, grosse Flaeche. Was ohne Netz erfasst wird, liegt in
IndexedDB, bis es hinausgeht — nicht in localStorage: Ein Eintrag, der
beim Aufraeumen des Speichers verschwindet, ist eine verlorene Aufgabe.

Erst anstellen, dann senden. Andersherum ginge der Eintrag verloren, wenn
der Browser mitten im Senden geschlossen wird. Jede Erfassung traegt eine
vom Geraet vergebene Kennung, damit eine doppelt zugestellte Nachricht
keine zweite Aufgabe ergibt.

Der Dienstarbeiter haelt genau diese eine Seite offline bereit und sonst
nichts. Ein halb veralteter Planer waere schlimmer als gar keiner — ein
Eingabefeld dagegen veraltet nicht, und ohne die Seite naehme die
Warteschlange im Geraet niemandem etwas ab.

Dazu das Manifest, damit sich der Planer als Anwendung ablegen laesst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-12 11:09:39 +02:00
co-authored by Claude Opus 5
parent b17c42af56
commit 9565b9a920
10 changed files with 562 additions and 5 deletions
+39
View File
@@ -0,0 +1,39 @@
{
"name": "Planer",
"short_name": "Planer",
"description": "Aufgaben, Ideen, Projekte und Zeitblöcke.",
"lang": "de",
"dir": "ltr",
"start_url": "/erfassen",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#f7f8fa",
"theme_color": "#f7f8fa",
"icons": [
{
"src": "/symbol.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/symbol-flaeche.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "Erfassen",
"short_name": "Erfassen",
"url": "/erfassen"
},
{
"name": "Heute",
"short_name": "Heute",
"url": "/"
}
]
}
+59 -5
View File
@@ -1,18 +1,72 @@
/*
* Service Worker des Planers.
*
* Er tut genau eine Sache: Push-Nachrichten anzeigen und beim Klick die
* richtige Seite öffnen. Kein Zwischenspeichern von Seiten — ein halb
* veralteter Planer wäre schlimmer als gar keiner.
* Zwei Aufgaben: Push-Nachrichten anzeigen, und die Seite "Erfassen" auch
* ohne Netz bereithalten.
*
* Zwischengespeichert wird **genau diese eine Seite** und sonst nichts. Ein
* halb veralteter Planer wäre schlimmer als gar keiner — ein Eingabefeld
* dagegen veraltet nicht, und ohne die Seite nützte die Warteschlange im
* Gerät niemandem: Man käme gar nicht erst hin.
*/
self.addEventListener('install', () => {
const LAGER = 'planer-erfassen-v1'
const OFFLINE_SEITE = '/erfassen'
self.addEventListener('install', (ereignis) => {
ereignis.waitUntil(
caches
.open(LAGER)
.then((lager) => lager.add(new Request(OFFLINE_SEITE, { cache: 'reload' })))
.catch(() => undefined),
)
// Sofort übernehmen, nicht auf das Schließen aller Tabs warten.
self.skipWaiting()
})
self.addEventListener('activate', (ereignis) => {
ereignis.waitUntil(self.clients.claim())
ereignis.waitUntil(
caches
.keys()
.then((namen) =>
Promise.all(namen.filter((n) => n !== LAGER).map((n) => caches.delete(n))),
)
.then(() => self.clients.claim()),
)
})
self.addEventListener('fetch', (ereignis) => {
const anfrage = ereignis.request
if (anfrage.method !== 'GET') return
const adresse = new URL(anfrage.url)
if (adresse.origin !== self.location.origin) return
if (adresse.pathname !== OFFLINE_SEITE) return
// Zuerst das Netz: Wer online ist, bekommt die frische Seite. Erst wenn das
// scheitert, kommt die aufgehobene Fassung.
ereignis.respondWith(
fetch(anfrage)
.then((antwort) => {
if (antwort.ok) {
const kopie = antwort.clone()
void caches.open(LAGER).then((lager) => lager.put(OFFLINE_SEITE, kopie))
}
return antwort
})
.catch(() =>
caches
.match(OFFLINE_SEITE)
.then(
(aufgehoben) =>
aufgehoben ??
new Response('Offline und nichts aufgehoben.', {
status: 503,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
}),
),
),
)
})
self.addEventListener('push', (ereignis) => {
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Planer">
<!-- Maskierbare Fassung: Das Motiv sitzt in der sicheren Zone, damit ein
Betriebssystem daraus einen Kreis oder ein Quadrat schneiden kann. -->
<rect width="64" height="64" fill="#2563eb" />
<path d="M20 34l8 8 16-18" fill="none" stroke="#ffffff" stroke-width="6"
stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 435 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Planer">
<!-- Ein Blatt mit Haken: Liste plus Erledigtes. Bewusst ohne Farbverlauf,
damit es auch als kleines Symbol lesbar bleibt. -->
<rect x="10" y="6" width="44" height="52" rx="8" fill="#2563eb" />
<rect x="20" y="2" width="8" height="10" rx="4" fill="#1e40af" />
<rect x="36" y="2" width="8" height="10" rx="4" fill="#1e40af" />
<path d="M21 33l8 8 15-16" fill="none" stroke="#ffffff" stroke-width="6"
stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 573 B