Phase 7: Benachrichtigungen und Termine
Terminkalender mit Serienterminen, Selbstanmeldung, Warteliste und Anwesenheitsliste. Vier Zustellwege hinter Schnittstellen: echtes SMTP, ntfy (selbst gehostet), Telegram und Web-Push. Kalenderabonnement als ICS-Feed, digitaler Dienstausweis mit signiertem QR und Pruefseite. Niemand wird ungefragt benachrichtigt: ein neuer Zustellweg hat eine leere Ereignisliste, und der Seed legt bewusst keine an. Was das Netz der Wehr verlaesst, bekommt nur die Kurzfassung ohne Namen und Details. Kein CalDAV (Lesen genuegt, ICS kann jeder Kalender) und kein WhatsApp (Verarbeitung ueber Meta, Kosten je Konversation, Telefonbuchabgleich) -- beides im README begruendet. 56 neue Unit-Tests, 17 neue E2E-Tests, Version 0.8.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@@ -1,10 +1,10 @@
|
||||
# Anleitung
|
||||
|
||||
Bedienung von FLORIAN, Stand Phase 6 (Version 0.7.0). Die Bilder zeigen die
|
||||
Bedienung von FLORIAN, Stand Phase 7 (Version 0.8.0). Die Bilder zeigen die
|
||||
Beispielwehr aus dem Seed und werden mit `npm run screenshots` neu erzeugt.
|
||||
|
||||
> Benachrichtigungen, lokale KI und die Kassenverwaltung kommen in den
|
||||
> folgenden Phasen dazu. Siehe [ROADMAP.md](ROADMAP.md).
|
||||
> Die lokale KI und die Kassenverwaltung kommen in den folgenden Phasen dazu.
|
||||
> Siehe [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -1279,7 +1279,230 @@ GEOCODING_LAENDER=de,at,ch
|
||||
Ohne Konfiguration bleibt die Suche leer. Koordinaten lassen sich dann von Hand
|
||||
eintragen oder weglassen.
|
||||
|
||||
## 39. Rollen und was sie dürfen
|
||||
## 39. Der Terminkalender
|
||||
|
||||

|
||||
|
||||
Unter **Termine** steht, was ansteht: Dienste, Übungen, Lehrgänge,
|
||||
Arbeitsdienste, Veranstaltungen, Sitzungen. Die Liste blickt nach vorn — das
|
||||
ist, was fast immer gemeint ist.
|
||||
|
||||
An jeder Zeile stehen zwei Knöpfe: **Zusagen** und **Absagen**. Mehr braucht
|
||||
das normale Mitglied nicht.
|
||||
|
||||
### Rückmeldung ist die Ausnahme
|
||||
|
||||
Beim Übungsabend meldet sich niemand an, man kommt oder man kommt nicht. Erst
|
||||
beim Lehrgang mit zwölf Plätzen und beim Ausflug mit Busbestellung wird beim
|
||||
Anlegen **Rückmeldung verlangen** angekreuzt.
|
||||
|
||||
Wer dann Plätze begrenzt, bekommt automatisch eine **Warteliste**: Zusagen über
|
||||
die Platzzahl hinaus landen dort, nummeriert in der Reihenfolge der Anmeldung.
|
||||
Sagt jemand ab, rückt der Erste von selbst nach und erfährt es. Niemand muss
|
||||
eine Liste führen.
|
||||
|
||||
### Serientermine
|
||||
|
||||
Beim Anlegen unter **Wiederholung** wählen: wöchentlich, alle zwei Wochen,
|
||||
monatlich, jährlich. Dazu ein Datum, bis zu dem die Serie läuft.
|
||||
|
||||
Darunter steht dann in einem Kasten, **wie viele Termine daraus werden** —
|
||||
lesen Sie das, bevor Sie auf „Anlegen" drücken. Wöchentlich über ein Jahr sind
|
||||
52 Einträge.
|
||||
|
||||
FLORIAN legt jeden Abend als eigenen Termin an, nicht als Regel. Das ist
|
||||
Absicht: so lässt sich der Abend im August absagen, der im Dezember eine Woche
|
||||
vorziehen, und für jeden gibt es eine eigene Teilnehmer- und Anwesenheitsliste.
|
||||
Bei „monatlich" bleibt der Tag im Monat gleich — der 31. rutscht im Februar auf
|
||||
den 28., nicht in den März.
|
||||
|
||||
### Absagen statt löschen
|
||||
|
||||
Ein Termin, zu dem schon jemand zugesagt hat, lässt sich nicht löschen. Er wird
|
||||
**abgesagt**, mit Grund. Er bleibt dann durchgestrichen im Kalender stehen, der
|
||||
Grund steht daneben, und wer zugesagt hatte, bekommt eine Nachricht — nicht die
|
||||
ganze Wehr.
|
||||
|
||||
Gehört der Termin zu einer Serie, können Sie im selben Schritt **alle folgenden
|
||||
Termine** absagen. Vergangene bleiben unberührt; man sagt nicht rückwirkend ab,
|
||||
was schon stattgefunden hat.
|
||||
|
||||
### Anwesenheitsliste
|
||||
|
||||
Auf jedem Termin oben rechts. Sie zeigt **alle aktiven Mitglieder**, nicht nur
|
||||
die Angemeldeten — auf einem Blatt zum Abhaken muss jeder stehen. Daneben, was
|
||||
jemand gemeldet hatte.
|
||||
|
||||
Wer das Recht dazu hat, hakt gleich hier ab; sonst führt **Drucken** zu einem
|
||||
Blatt mit leeren Kästchen.
|
||||
|
||||
> Zusage und Anwesenheit sind zwei verschiedene Dinge. Wer zugesagt hat und
|
||||
> nicht kam, steht genau so in der Liste — das ist der Zweck der Trennung.
|
||||
|
||||
---
|
||||
|
||||
## 40. Benachrichtigungen einrichten
|
||||
|
||||

|
||||
|
||||
> **Niemand wird ungefragt benachrichtigt.** Wer hier nichts einträgt, bekommt
|
||||
> nichts. Es gibt keine Voreinstellung, die alle anmeldet.
|
||||
|
||||
Unter **Benachrichtigungen** richten Sie Ihre eigenen Wege ein — Ihre, nicht
|
||||
die von jemand anderem. Auch der Kommandant kann niemanden für Sie anmelden.
|
||||
|
||||
### Die vier Wege
|
||||
|
||||
| Weg | Was Sie brauchen | Bleibt im Haus? |
|
||||
|---|---|---|
|
||||
| **E-Mail** | nichts, es gilt Ihre hinterlegte Adresse | ja |
|
||||
| **ntfy** | die ntfy-App und ein Thema | ja, wenn der Server der Wehr gehört |
|
||||
| **Telegram** | Ihre Chat-Kennung vom Bot | **nein** |
|
||||
| **Browser-Push** | nur die Erlaubnis des Browsers | **nein** |
|
||||
|
||||
Neben den Wegen, die das Netz der Wehr verlassen, steht ein Warnhinweis. Dorthin
|
||||
gehen nur **Kurzfassungen**: „3 Prüfungen fällig", nicht welche. Wer es genau
|
||||
wissen will, bekommt es per E-Mail oder schaut in FLORIAN nach.
|
||||
|
||||
### ntfy einrichten
|
||||
|
||||
Der bequemste Weg, wenn die Wehr einen eigenen ntfy betreibt:
|
||||
|
||||
1. Die App „ntfy" aus dem Store laden.
|
||||
2. In der App den Server eintragen, der auf dieser Seite steht.
|
||||
3. In FLORIAN **ntfy** wählen; das vorgeschlagene Thema übernehmen.
|
||||
4. In der App dasselbe Thema abonnieren.
|
||||
|
||||
> **Das Thema ist Ihr Schlüssel.** Wer den Namen kennt, liest mit. Deshalb
|
||||
> schlägt FLORIAN einen zufälligen vor — nehmen Sie nicht „florian-mueller".
|
||||
|
||||
### Browser-Push
|
||||
|
||||
**Einrichten** drücken; der Browser fragt nach Erlaubnis. Das Abo gilt für
|
||||
**diesen einen Browser auf diesem einen Gerät** — auf dem Diensthandy und am
|
||||
Rechner im Büro also zweimal einrichten.
|
||||
|
||||
Auf dem iPhone funktioniert es erst, wenn FLORIAN über „Zum Home-Bildschirm"
|
||||
abgelegt wurde. Vorher bietet der Browser es gar nicht an.
|
||||
|
||||
### Wofür?
|
||||
|
||||
Unter jedem Weg stehen die sechs Anlässe zum Ankreuzen:
|
||||
|
||||
| Anlass | Wann er kommt |
|
||||
|---|---|
|
||||
| Prüfung fällig | im morgendlichen Lauf |
|
||||
| Mangel gemeldet | sofort |
|
||||
| Qualifikation läuft ab | 90, 30 und 7 Tage vorher |
|
||||
| Neuer Termin | beim Anlegen |
|
||||
| Erinnerung an einen Termin | 7 Tage und 1 Tag vorher |
|
||||
| Einsatzbericht freigegeben | bei der Freigabe |
|
||||
|
||||
### Der Probeknopf
|
||||
|
||||
**Drücken Sie ihn.** Er steht direkt an jedem Weg und schickt eine
|
||||
Probenachricht durch dieselbe Maschinerie wie alles andere. Ohne ihn merken Sie
|
||||
einen Tippfehler im Thema erst in drei Wochen — dann, wenn die Nachricht
|
||||
wichtig gewesen wäre.
|
||||
|
||||
Sie geht mit dem nächsten Lauf raus, üblicherweise binnen einer Minute.
|
||||
|
||||
### Was ankam
|
||||
|
||||
Ganz unten steht, was FLORIAN Ihnen zuletzt geschickt hat und ob es angekommen
|
||||
ist. Steht dort mehrfach **Fehlgeschlagen**, ist der Weg kaputt — der Grund
|
||||
steht daneben.
|
||||
|
||||
---
|
||||
|
||||
## 41. Den Kalender aufs Handy holen
|
||||
|
||||
Auf derselben Seite unter **Kalender abonnieren**. Namen für das Gerät eingeben,
|
||||
**Adresse erzeugen** — und die Adresse gleich übernehmen.
|
||||
|
||||
> **Sie wird genau einmal angezeigt.** FLORIAN speichert nur ihre Prüfsumme.
|
||||
> Wer sie verliert, erzeugt eine neue und widerruft die alte.
|
||||
|
||||
Die Adresse dann eintragen:
|
||||
|
||||
| Gerät | Weg |
|
||||
|---|---|
|
||||
| **iPhone/iPad** | Einstellungen → Kalender → Accounts → Account hinzufügen → Andere → Kalenderabo hinzufügen |
|
||||
| **Android** | in Google Kalender am Rechner: Andere Kalender → Per URL |
|
||||
| **Thunderbird** | Neuer Kalender → Im Netzwerk → iCalendar (ICS) |
|
||||
| **Outlook** | Kalender hinzufügen → Aus dem Internet abonnieren |
|
||||
|
||||
Danach stehen die Termine der Wehr neben allem anderen im Kalender. Neue
|
||||
Termine und Absagen kommen von selbst nach; die meisten Programme fragen
|
||||
stündlich nach.
|
||||
|
||||
**Nur eigene Termine** angekreuzt heißt: nur die, zu denen Sie sich
|
||||
zurückgemeldet haben.
|
||||
|
||||
> **Die Adresse ist ein Schlüssel.** Wer sie hat, sieht die Termine der Wehr —
|
||||
> ohne Anmeldung. Geben Sie sie nicht weiter; bei einem verlorenen Handy
|
||||
> genügt ein Klick auf **Widerrufen**.
|
||||
|
||||
Ausgeliefert werden ausschließlich Termine — keine Teilnehmerlisten, keine
|
||||
Mängel, keine Einsätze. Wer aus der Wehr austritt, verliert das Abo.
|
||||
|
||||
---
|
||||
|
||||
## 42. Textvorlagen und der Dienstausweis
|
||||
|
||||
### Textvorlagen
|
||||
|
||||

|
||||
|
||||
Für Kommandant, Schriftführer und Administrator: unter **Benachrichtigungen →
|
||||
Vorlagen bearbeiten** steht, was in den Nachrichten steht. Was hier geändert
|
||||
wird, gilt für **alle**.
|
||||
|
||||
Links der Text, rechts die Vorschau mit erfundenen Beispielwerten. Die
|
||||
**Platzhalter** darunter sind anklickbar und werden beim Versand ersetzt:
|
||||
`{titel}` wird zu „Übungsabend", `{datum}` zum Datum.
|
||||
|
||||
Ein Platzhalter, den es für diesen Anlass nicht gibt, wird beim Speichern
|
||||
abgewiesen — mit der Liste der erlaubten daneben. Und einer, der stehen bleibt,
|
||||
bleibt auch in der fertigen Nachricht sichtbar: eine Lücke, die niemand
|
||||
bemerkt, wäre schlimmer.
|
||||
|
||||
Ohne eigene Vorlage greift der eingebaute Text. **Auf Standard zurücksetzen**
|
||||
stellt ihn wieder her.
|
||||
|
||||
> Bei Telegram und Browser-Push steht ein Warnhinweis über dem Feld. Was Sie
|
||||
> dort hineinschreiben, läuft über einen fremden Server — also keine Namen,
|
||||
> keine Adressen, keine Einsatzdetails.
|
||||
|
||||
### Der digitale Dienstausweis
|
||||
|
||||

|
||||
|
||||
Unter **Dienstausweis**: ein QR-Code mit Ihrem Namen, Ihrer Wehr, Ihrer
|
||||
Funktion und Ihren gültigen Qualifikationen. Zum Vorzeigen an der Absperrung,
|
||||
beim Lehrgang oder bei einer Kontrolle.
|
||||
|
||||
Wer ihn scannt, landet auf einer Prüfseite und sieht genau das — **ohne sich
|
||||
anzumelden**. Denn wer prüft, hat keinen Zugang zu FLORIAN.
|
||||
|
||||
Drei Dinge sollten Sie darüber wissen:
|
||||
|
||||
- **Der Code gilt fünf Minuten.** Danach zeigt die Seite ihn selbst neu an. Ein
|
||||
abfotografierter Code nützt nichts.
|
||||
- **Er öffnet nichts.** Die Prüfseite zeigt an, sie lässt nicht hinein.
|
||||
- **Es steht nicht mehr darauf als auf einem Ausweis aus Papier.** Keine
|
||||
Anschrift, kein Geburtsdatum, keine Telefonnummer.
|
||||
|
||||
Die Prüfseite nennt außerdem, wann der Code erzeugt wurde. Das ist der Punkt,
|
||||
auf den ein Prüfender schauen sollte: „vor zwei Minuten" ist ein Ausweis,
|
||||
„gültig" allein wäre wertlos.
|
||||
|
||||
Wer aus der Wehr ausgetreten ist, bekommt dort **Kein aktives Mitglied** — auch
|
||||
wenn sein Code technisch noch stimmt.
|
||||
|
||||
---
|
||||
|
||||
## 43. Rollen und was sie dürfen
|
||||
|
||||
Neun Rollen, jede mit einem festen Satz Rechte. Die Zuordnung steht zentral in
|
||||
`src/lib/authz.ts` und wird beim Seed in die Datenbank gespiegelt — eine Wehr
|
||||
@@ -1292,9 +1515,9 @@ kann sie also nachträglich zuschneiden, ohne dass jemand Code ändert.
|
||||
| **Gerätewart** | Fahrzeuge und Geräte anlegen, ändern, aussondern; Prüfungen planen und durchführen |
|
||||
| **Atemschutzgerätewart** | zusätzlich Atemschutzgeräte, Träger und die Überwachung nach FwDV 7 |
|
||||
| **Kassier** | Buchungen und Kassenabschluss (Modul noch nicht geplant, siehe Roadmap) |
|
||||
| **Schriftführer** | Mitglieder pflegen, Einsätze und Berichte erfassen |
|
||||
| **Ausbilder** | Übungen planen, Teilnahme und Qualifikationen erfassen |
|
||||
| **Mitglied** | eigene Daten, Listen lesen |
|
||||
| **Schriftführer** | Mitglieder pflegen, Einsätze und Berichte erfassen, den Terminkalender führen |
|
||||
| **Ausbilder** | Übungen und Übungstermine planen, Teilnahme, Anwesenheit und Qualifikationen erfassen |
|
||||
| **Mitglied** | eigene Daten, Listen lesen, Termine sehen und sich zurückmelden, eigene Benachrichtigungswege einrichten |
|
||||
| **Terminal** | nur anmelden und erfassen — nie verwalten |
|
||||
|
||||
Zwei Dinge sind bewusst so gebaut:
|
||||
@@ -1306,9 +1529,14 @@ Trennung nach Verantwortung, nicht nach Bequemlichkeit.
|
||||
zusätzlich mit PIN oder Karte an und handelt dann unter seiner eigenen Person.
|
||||
Das Terminal selbst ist nur die Tür.
|
||||
|
||||
**Seine Benachrichtigungswege richtet jeder selbst ein.** Das ist ein
|
||||
Grundrecht jedes Mitglieds und keine Sache des Kommandanten — es gibt bewusst
|
||||
keinen Weg, jemand anderem einen Zustellweg einzutragen. Was für alle gilt,
|
||||
sind nur die Textvorlagen, und die hängen an einem eigenen Recht.
|
||||
|
||||
---
|
||||
|
||||
## 40. Anmeldung über authentik einrichten
|
||||
## 44. Anmeldung über authentik einrichten
|
||||
|
||||
authentik läuft **getrennt** von FLORIAN und ist nicht Teil dieses Stacks.
|
||||
|
||||
@@ -1361,7 +1589,7 @@ ganze Wehr aus.
|
||||
|
||||
---
|
||||
|
||||
## 41. PIN und Ausweiskarte verwalten
|
||||
## 45. PIN und Ausweiskarte verwalten
|
||||
|
||||
Wie sich am Terminal angemeldet wird, steht in Kapitel 24. Hier geht es darum,
|
||||
was dahinter liegt.
|
||||
@@ -1384,7 +1612,7 @@ Huber), PIN `471203`.
|
||||
|
||||
---
|
||||
|
||||
## 42. Was protokolliert wird
|
||||
## 46. Was protokolliert wird
|
||||
|
||||
Jede schreibende Änderung erzeugt einen Eintrag mit Zeitpunkt, Person,
|
||||
betroffenem Datensatz und den geänderten Feldern im Vorher-Nachher-Vergleich.
|
||||
@@ -1409,7 +1637,7 @@ jeweiligen Detailseite sichtbar.
|
||||
|
||||
---
|
||||
|
||||
## 43. Wenn etwas nicht geht
|
||||
## 47. Wenn etwas nicht geht
|
||||
|
||||
| Beobachtung | Wahrscheinliche Ursache | Was hilft |
|
||||
|---|---|---|
|
||||
@@ -1438,6 +1666,17 @@ jeweiligen Detailseite sichtbar.
|
||||
| Nach dem Import steht überall „ungeprüft" | Die Quelle hat keine Statusspalte | Absicht: ohne Angabe wird nicht „einsatzbereit" behauptet. Zustand nachpflegen |
|
||||
| Die Lagekarte gleicht nicht ab | Der Ereignisstrom ist unterbrochen | Steht oben in der Lagekarte. Bei einem Reverse Proxy `X-Accel-Buffering: no` durchreichen |
|
||||
| Die Förderstrecke braucht unerwartet viele Pumpen | Fördermenge zu hoch für den Durchmesser | Der Verlust steigt im Quadrat: 800 statt 400 l/min kostet die vierfache Reibung. Größeren Schlauch wählen oder weniger fördern |
|
||||
| Es kommt keine Benachrichtigung an | Kein Zustellweg eingerichtet, oder kein Anlass angekreuzt | Unter **Benachrichtigungen** einen Weg einrichten und ankreuzen, wofür. Ohne beides kommt absichtlich nichts |
|
||||
| Die Probenachricht bleibt aus | Der Kanal ist auf dem Server nicht eingerichtet | In der Auswahl steht „(nicht eingerichtet)". `MAIL_ENABLED`, `NTFY_ENABLED`, `TELEGRAM_ENABLED` oder `WEBPUSH_ENABLED` setzen |
|
||||
| Im Verlauf steht mehrfach „Fehlgeschlagen" | Der Weg ist kaputt — der Grund steht daneben | Bei ntfy meist ein Tippfehler im Thema, bei Telegram eine falsche Chat-Kennung |
|
||||
| Browser-Push lässt sich nicht einrichten | Keine VAPID-Schlüssel auf dem Server | `npm run vapid` und die Ausgabe in die `.env` übernehmen |
|
||||
| Push kam bisher an, jetzt nicht mehr | Der öffentliche VAPID-Schlüssel wurde gewechselt | Alle Abos sind damit wertlos. Jeder muss Push neu erlauben — deshalb gehören die Schlüssel ins Backup |
|
||||
| Auf dem iPhone gibt es keinen Push-Knopf | Web-Push geht dort nur als abgelegte App | Über „Teilen → Zum Home-Bildschirm" ablegen, dann erneut versuchen |
|
||||
| Der Kalender im Handy aktualisiert sich nicht | Das Programm fragt nur stündlich nach | Abwarten oder im Kalenderprogramm „Aktualisieren" auslösen |
|
||||
| Der Kalender ist plötzlich leer | Der Zugang wurde widerrufen, oder die Person ist ausgetreten | Unter **Benachrichtigungen** eine neue Adresse erzeugen |
|
||||
| Eine Vorlage lässt sich nicht speichern | Ein Platzhalter gehört nicht zu diesem Anlass | Die Meldung nennt ihn und listet die erlaubten auf |
|
||||
| Ein Termin lässt sich nicht löschen | Es liegen schon Rückmeldungen vor | Absagen statt löschen — dann sehen die Angemeldeten den Grund |
|
||||
| Die Zusage wurde zur Warteliste | Die Plätze sind belegt | Sagt jemand ab, rücken Sie von selbst nach und werden benachrichtigt |
|
||||
|
||||
Zustand aller Randsysteme auf einen Blick:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Aufwandsschätzung
|
||||
|
||||
Schätzung je Phase, daneben der tatsächliche Aufwand aus dem
|
||||
[Devlog](DEVLOG.md). Stand: 3. September 2026.
|
||||
[Devlog](DEVLOG.md). Stand: 4. September 2026.
|
||||
|
||||
## Wie hier gerechnet wird
|
||||
|
||||
@@ -34,10 +34,10 @@ Faktoren, die in die Schätzung eingehen:
|
||||
| 1 | Stammdaten | 10–14 PT | 2,5–3,5 h | **2:05 h** |
|
||||
| 2 | Wartung und Prüfung | 18–24 PT | 4,5–6 h | **2:25 h** |
|
||||
| 3 | Einsätze und Übungen | 12–16 PT | 3–4 h | **2:10 h** |
|
||||
| 4 | Kiosk-PWA mit Offline | 16–22 PT | 4–5,5 h | offen |
|
||||
| 5 | Atemschutz (FwDV 7) | 14–18 PT | 3,5–4,5 h | offen |
|
||||
| 6 | Kartenmodule | 14–18 PT | 3,5–4,5 h | offen |
|
||||
| 7 | Benachrichtigungen und Termine | 10–13 PT | 2,5–3,5 h | offen |
|
||||
| 4 | Kiosk-PWA mit Offline | 16–22 PT | 4–5,5 h | **3:00 h** |
|
||||
| 5 | Atemschutz (FwDV 7) | 14–18 PT | 3,5–4,5 h | **2:40 h** |
|
||||
| 6 | Kartenmodule | 14–18 PT | 3,5–4,5 h | **2:50 h** |
|
||||
| 7 | Benachrichtigungen und Termine | 10–13 PT | 2,5–3,5 h | **3:10 h** |
|
||||
| 8 | Lokale KI | 9–12 PT | 2,5–3 h | offen |
|
||||
| 9 | Kassenverwaltung | 8–11 PT | 2–3 h | offen |
|
||||
| | **Summe** | **123–164 PT** | **28–37 h** | |
|
||||
@@ -339,7 +339,7 @@ das ehrlich angezeigt wird. In der Aufstellung steht davon keine Zeile.
|
||||
|
||||
## Phase 7 — Benachrichtigungen und Termine
|
||||
|
||||
**Vergleichsaufwand: 10–13 PT**
|
||||
**Vergleichsaufwand: 10–13 PT** · **Tatsächlich: 3:10 h Sitzungszeit**
|
||||
|
||||
| Baustein | PT |
|
||||
|---|---:|
|
||||
@@ -347,11 +347,24 @@ das ehrlich angezeigt wird. In der Aufstellung steht davon keine Zeile.
|
||||
| Vorlagen mit Platzhaltern, je Ereignis und Rolle | 2,0 |
|
||||
| Abonnements je Person | 1,0 |
|
||||
| Kalender mit Serienterminen und Selbstanmeldung | 3,0 |
|
||||
| CalDAV-/ICS-Feed je Person mit Token | 1,5 |
|
||||
| ICS-Feed je Person mit Token | 1,5 |
|
||||
| Digitaler Dienstausweis mit signiertem QR und Prüfseite | 2,0 |
|
||||
|
||||
Das Grundgerüst der Kanalabstraktion steht bereits aus Phase 0
|
||||
(`lib/integrations/`), das spart hier etwa einen Personentag.
|
||||
Das Grundgerüst der Kanalabstraktion stand bereits aus Phase 0
|
||||
(`lib/integrations/`), das hat etwa einen Personentag gespart.
|
||||
|
||||
Die einzige Phase bisher, die über der geschätzten Sitzungszeit liegt — und
|
||||
zwar an einer Stelle, die in der Aufstellung nicht vorkommt. „Kalender mit
|
||||
Serienterminen" klingt nach einer Datumsrechnung; teuer war die Entscheidung
|
||||
**gegen** eine Regel und **für** ausgeschriebene Termine, und dann die
|
||||
Kalenderarithmetik dahinter: `setMonth(+1)` auf dem 31. Januar ergibt den
|
||||
3. März, der 29. Februar existiert in vier von fünf Jahren nicht, und ein
|
||||
Übungsabend muss über die ganze Serie auf demselben Wochentag bleiben. Das sind
|
||||
drei Tests, die man erst schreibt, wenn man den Fall kennt.
|
||||
|
||||
Die 1,5 PT für den Feed waren dagegen zu hoch angesetzt, weil sie CalDAV
|
||||
unterstellen. ICS ist ein Textformat; der ganze Erzeuger sind hundertsechzig
|
||||
Zeilen samt Maskierung und Zeilenumbruch nach 75 Oktetten.
|
||||
|
||||
## Phase 8 — Lokale KI
|
||||
|
||||
|
||||
@@ -18,7 +18,126 @@ Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte
|
||||
| Phase 4 — Kiosk-PWA im Gerätehaus | 3:00 h | 1 | 03.09.2026 |
|
||||
| Phase 5 — Atemschutz nach FwDV 7 | 2:40 h | 1 | 03.09.2026 |
|
||||
| Phase 6 — Kartenmodule | 2:50 h | 1 | 04.09.2026 |
|
||||
| **Summe** | **16:10 h** | **8** | |
|
||||
| Phase 7 — Benachrichtigungen und Termine | 3:10 h | 1 | 04.09.2026 |
|
||||
| **Summe** | **19:20 h** | **9** | |
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-04 · Phase 7 — Benachrichtigungen und Termine
|
||||
|
||||
**04:10 – 07:20 · 3:10 h · Version 0.8.0**
|
||||
|
||||
### Was entstanden ist
|
||||
|
||||
Der Jahresplan der Wehr und die Wege, auf denen die Mannschaft davon erfährt.
|
||||
|
||||
| Bereich | Dateien |
|
||||
|---|---|
|
||||
| Rechnen | `lib/termin/serie.ts`, `ics.ts`, `lib/benachrichtigung/vorlagen.ts`, `versand.ts`, `lib/dienstausweis.ts` |
|
||||
| Kanäle | `lib/integrations/mail.ts` (echtes SMTP), `ntfy.ts`, `webpush.ts` |
|
||||
| Server | `server/routers/termin/termin.ts`, `benachrichtigung.ts`, `stammdaten/dienstausweis.ts` |
|
||||
| Öffentlich | `api/kalender/[token]`, `ausweis/[code]`, `api/ausweis/[code]/foto` |
|
||||
| Oberfläche | `app/(app)/termine/`, `benachrichtigungen/`, `ausweis/` |
|
||||
| Hintergrund | `worker/benachrichtigung.ts`, zwei neue Zeitpläne |
|
||||
| Tests | 56 Unit, 17 E2E je Browser |
|
||||
|
||||
### Entscheidungen
|
||||
|
||||
**Niemand wird ungefragt benachrichtigt.** Ein neu eingerichteter Zustellweg
|
||||
hat eine leere Ereignisliste. Es gibt keinen Weg, jemand anderem einen Kanal
|
||||
einzutragen — auch nicht für den Kommandanten. Der Seed legt bewusst *keine*
|
||||
Zustellkanäle an, obwohl das die Beispieldaten hübscher machen würde. Wer eine
|
||||
Wehr ungefragt anmeldet, hat binnen einer Woche eine Wehr, die die
|
||||
Benachrichtigungen abschaltet, und danach kommen auch die wichtigen nicht mehr
|
||||
an.
|
||||
|
||||
**Serientermine werden ausgeschrieben, nicht gerechnet.** Eine Regel wäre
|
||||
weniger Zeilen in der Datenbank und mehr Sonderfälle im Code: der Übungsabend
|
||||
fällt im August aus, im Dezember ist er eine Woche früher, und für jeden
|
||||
einzelnen gibt es Zusagen und eine Anwesenheitsliste. Ausgeschrieben ist jede
|
||||
dieser Ausnahmen einfach ein Datensatz, den man ändert. Der Preis sind hundert
|
||||
Zeilen für zwei Jahre — Kleingeld, und die Zahl steht vor dem Anlegen in der
|
||||
Maske.
|
||||
|
||||
**Gerechnet wird auf dem Kalender, nicht in Millisekunden.** „Monatlich" heißt
|
||||
derselbe Tag im nächsten Monat. `setMonth(+1)` auf dem 31. Januar ergibt den
|
||||
3. März, weil der Februar keinen 31. hat — ein Fehler, der in einer
|
||||
Übungsserie erst im Frühjahr auffällt. Fällt der Tag im Zielmonat aus, rutscht
|
||||
der Termin auf den letzten des Monats.
|
||||
|
||||
**Anlegen und Zustellen sind getrennt.** Wo ein Ereignis entsteht, wird eine
|
||||
Zeile geschrieben, sonst nichts. Der Worker nimmt sich, was auf WARTET steht.
|
||||
Wären es eine Sache, hinge das Speichern eines Termins am Antwortverhalten von
|
||||
Telegram. So ist die schlimmste Folge eines toten Mailservers eine
|
||||
Benachrichtigung, die auf FEHLGESCHLAGEN steht und sichtbar ist.
|
||||
|
||||
**Intern bekommt den langen Text, extern die Kurzfassung.** Der Mailserver
|
||||
einer Wehr ist ihr eigener oder der der Gemeinde, und ein selbst betriebener
|
||||
ntfy steht im Gerätehaus — dorthin dürfen Namen, Gerätebezeichnungen und
|
||||
Einsatznummern. Telegram und Web-Push laufen über fremde Server und bekommen
|
||||
„3 Prüfungen fällig", nicht welche. Das steht in `KANAL_EXTERN` an einer
|
||||
Stelle, wird im Test für *jeden* externen Kanal geprüft und in der Oberfläche
|
||||
zweimal gesagt: neben dem Kanal und über dem Vorlagenfeld.
|
||||
|
||||
**Kein CalDAV, sondern ICS.** CalDAV ist ein Schreib-Lese-Protokoll mit
|
||||
Rechteverwaltung und Konfliktauflösung. Gebraucht wird Lesen. Eine Adresse, die
|
||||
eine ICS-Datei ausliefert, kann jeder Kalender — das Format ist eine Liste von
|
||||
Zeilen, und die interessanten Teile (Zeitzone, Umbruch nach 75 Oktetten) stehen
|
||||
in zwanzig Zeilen. Eine Abhängigkeit dafür wäre in einer Anwendung ohne
|
||||
Netzzugang der schlechtere Tausch.
|
||||
|
||||
**Der Ausweis gilt fünf Minuten, die Anmeldemarke eine.** Beide sind mit
|
||||
AUTH_SECRET signiert; was sie trennt, ist der Zweck im signierten Teil. Ohne
|
||||
den ginge das eine als das andere durch, und der Ausweis wäre plötzlich ein
|
||||
Anmeldeweg. Die fünf Minuten sind der Unterschied zwischen „wird gescannt" und
|
||||
„wird vorgezeigt, abgelesen und weggesteckt" — und die Prüfseite nennt den
|
||||
Zeitpunkt der Ausstellung, weil ein Ausweis, der „gültig" sagt ohne zu sagen
|
||||
seit wann, wertlos ist.
|
||||
|
||||
**Fehlgeschlagene Benachrichtigungen werden nicht abgeräumt.** Der
|
||||
Aufbewahrungslauf nimmt sich nur die zugestellten. Eine fehlgeschlagene ist der
|
||||
Hinweis darauf, dass ein Kanal kaputt ist, und der verliert seinen Wert nicht
|
||||
durch Zeitablauf.
|
||||
|
||||
**Erinnert wird um sieben, zugestellt jede Minute.** Eine Push-Meldung um drei
|
||||
Uhr weckt Leute, die am nächsten Tag arbeiten müssen. Eine Absage am
|
||||
Dienstagabend muss dagegen ankommen, solange sie noch etwas nützt.
|
||||
|
||||
### Wo es gehakt hat
|
||||
|
||||
| Problem | Ursache | Lösung |
|
||||
|---|---|---|
|
||||
| Die Vorlagenmaske überschrieb den gerade getippten Text | Der Effekt, der den passenden Text lädt, hing an `vorlagen.data`. Wird die Liste im Hintergrund nachgeladen — nach dem Speichern, beim Zurückkommen ins Fenster —, war die Eingabe weg | Merker auf `ereignis:kanal`; geladen wird nur beim Wechsel der Auswahl. Gefunden hat es der E2E-Test, nicht das Ausprobieren |
|
||||
| `personId` wurde zweimal gesetzt und überschrieb sich | Einschränkung und Ausschluss als zwei Objekt-Einträge im selben `where` — der zweite gewinnt still | Beide in eine Bedingung zusammengeführt |
|
||||
| Der Aufräumlauf durfte nicht löschen | `REVOKE DELETE ON "benachrichtigung"` gilt der Anwendungsrolle, und `fuerOrganisation` benutzt genau die | Über `ohneMandantenfilter` mit ausdrücklichem Mandantenfilter in der Bedingung — begründet, wie es die Regel verlangt |
|
||||
| Zwei E2E-Selektoren trafen zu viel | „Absagen" steckt auch in „Termin absagen"; `{titel}` steht im Textfeld *und* auf dem Platzhalterknopf | `exact: true` und Suche innerhalb des Dialogs |
|
||||
| Der Warteliste-Test hing an der Reihenfolge im Seed | Ob der angemeldete Nutzer schon einen Platz hat, entscheidet die Mitgliedsnummer | Test prüft jetzt die Anzeige der Warteliste — die gilt für alle |
|
||||
|
||||
### Abweichung von der Vorgabe
|
||||
|
||||
**Kein CalDAV**, obwohl Phase-7.md „CalDAV-/ICS-Feed" nennt. Begründung oben;
|
||||
sie steht auch im README und in der Roadmap.
|
||||
|
||||
**Termine einer Serie werden einzeln geändert, nicht „alle folgenden".** Der
|
||||
Fall ist selten und mehrdeutig — meint „alle folgenden" auch die, die jemand
|
||||
schon von Hand verschoben hat? Absagen geht dagegen für die ganze Serie, weil
|
||||
das der Fall ist, der wirklich vorkommt (Sommerpause, Bauarbeiten am
|
||||
Gerätehaus).
|
||||
|
||||
**Kein Push für iOS ohne Umweg.** Web-Push funktioniert auf dem iPhone erst,
|
||||
wenn FLORIAN zum Startbildschirm hinzugefügt wurde. Das steht in der
|
||||
Oberfläche; ein eigener Weg dafür wäre eine App im Store, und die gibt es
|
||||
bewusst nicht.
|
||||
|
||||
### Nachweise
|
||||
|
||||
- `npx tsc --noEmit` — ohne Ausgabe
|
||||
- `npm run test` — 422 Tests grün, davon 15 neue gegen eine echte Datenbank
|
||||
- `npx playwright test tests/e2e/termine.spec.ts` — 17 grün
|
||||
- `npm run build` — 50 Routen, darunter `/termine`, `/benachrichtigungen`,
|
||||
`/ausweis` und die beiden öffentlichen Endpunkte
|
||||
- `npm run db:seed` — 19 Termine, Warteliste und abgehakte Anwesenheit
|
||||
- `docker compose config` — ntfy im Profil `push` prüft durch
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ Phase 3 ████████████████████ fertig
|
||||
Phase 4 ████████████████████ fertig 0.5.0
|
||||
Phase 5 ████████████████████ fertig 0.6.0
|
||||
Phase 6 ████████████████████ fertig 0.7.0
|
||||
Phase 7 ░░░░░░░░░░░░░░░░░░░░ offen 0.8.0
|
||||
Phase 8 ░░░░░░░░░░░░░░░░░░░░ offen 0.9.0
|
||||
Phase 7 ████████████████████ fertig 0.8.0
|
||||
Phase 8 ░░░░░░░░░░░░░░░░░░░░ als Nächstes 0.9.0
|
||||
Phase 9 ░░░░░░░░░░░░░░░░░░░░ offen 0.10.0
|
||||
Freigabe ░░░░░░░░░░░░░░░░░░░░ offen 1.0.0
|
||||
```
|
||||
@@ -148,25 +148,53 @@ analogen, bis die Wehr ausdrücklich anders entscheidet.
|
||||
**Danach kann die Wehr:** auf der Anfahrt sehen, wo der nächste Hydrant liegt
|
||||
und was ihn hergibt.
|
||||
|
||||
## Phase 7 — Benachrichtigungen und Termine · als Nächstes
|
||||
## Phase 7 — Benachrichtigungen und Termine · fertig
|
||||
|
||||
- Kanäle: E-Mail, Telegram, ntfy, Web-Push. WhatsApp bewusst nicht.
|
||||
- Vorlagen mit Platzhaltern, je Ereignis und Rolle konfigurierbar
|
||||
- Kalender: Dienste, Übungen, Lehrgänge, Arbeitsdienste; Serientermine,
|
||||
Selbstanmeldung mit Teilnehmerlimit, Anwesenheitsliste zum Ausdrucken
|
||||
- CalDAV-/ICS-Feed je Person
|
||||
- Digitaler Dienstausweis mit signiertem QR und Prüfseite
|
||||
Die Wehr weiß, wann der nächste Dienst ist — ohne dass jemand eine Rundmail
|
||||
tippt.
|
||||
|
||||
- Terminkalender mit Dienst, Übung, Lehrgang, Arbeitsdienst, Veranstaltung und
|
||||
Sitzung. Serientermine werden ausgeschrieben, nicht als Regel gespeichert.
|
||||
- Selbstanmeldung mit Platzbegrenzung und einer Warteliste, die von selbst
|
||||
nachrückt. Absage mit Grund, wahlweise für alle folgenden Termine einer Serie.
|
||||
- Anwesenheitsliste über alle aktiven Mitglieder — zum Abhaken am Tablet oder
|
||||
zum Ausdrucken. Zusage und Anwesenheit sind getrennt.
|
||||
- Vier Kanäle hinter Schnittstellen: E-Mail (echtes SMTP statt Platzhalter),
|
||||
ntfy (selbst gehostet), Telegram, Web-Push. Was das Netz der Wehr verlässt,
|
||||
bekommt nur die Kurzfassung.
|
||||
- Vorlagen je Ereignis und Kanal mit Platzhaltern, Vorschau und eingebautem
|
||||
Standardtext für alles.
|
||||
- ICS-Abonnement je Person, Token nur als Hash gespeichert, einzeln widerrufbar.
|
||||
- Digitaler Dienstausweis: signierter QR, fünf Minuten gültig, Prüfseite ohne
|
||||
Anmeldung.
|
||||
|
||||
**Danach kann die Wehr:** ihren Jahresplan führen, Rückmeldungen einsammeln,
|
||||
Anwesenheit nachweisen und jedes Mitglied auf dem Weg erreichen, den es selbst
|
||||
gewählt hat.
|
||||
|
||||
### Kein CalDAV
|
||||
|
||||
CalDAV ist ein Schreib-Lese-Protokoll mit Rechteverwaltung und
|
||||
Konfliktauflösung. Gebraucht wird hier nur Lesen — die Termine entstehen in
|
||||
FLORIAN, nicht im Kalender des Anwenders. Dafür genügt eine Adresse, die eine
|
||||
ICS-Datei ausliefert; jeder Kalender kann das. CalDAV wäre ein Vielfaches an
|
||||
Aufwand für dieselbe Wirkung.
|
||||
|
||||
### Warum kein WhatsApp
|
||||
|
||||
Die WhatsApp Business API kostet je Konversation und verlangt einen
|
||||
zugelassenen Anbieter dazwischen — laufende Kosten für eine Wehr, die keinen
|
||||
Etat dafür hat. Der schwerere Grund ist der Datenschutz: die Nummern aller
|
||||
Mitglieder gingen an Meta, was sich mit einer Anwendung, die bewusst im
|
||||
Gerätehaus steht, nicht verträgt. Telegram, ntfy und Web-Push decken denselben
|
||||
Bedarf ab, ntfy sogar vollständig selbst gehostet.
|
||||
Etat dafür hat. Der schwerere Grund ist der Datenschutz: die Nachrichten liefen
|
||||
über Server von Meta, die Wehr müsste einen Vertrag zur Auftragsverarbeitung
|
||||
schließen, und die gewöhnliche App gleicht zusätzlich das Telefonbuch ab. Das
|
||||
verträgt sich nicht mit einer Anwendung, die bewusst im Gerätehaus steht.
|
||||
Telegram, ntfy und Web-Push decken denselben Bedarf ab, ntfy sogar vollständig
|
||||
selbst gehostet.
|
||||
|
||||
## Phase 8 — Lokale KI
|
||||
Wer seine Wehr trotzdem über WhatsApp erreichen will, hat den Weg schon: die
|
||||
Gruppe, die es ohnehin gibt. Ein Link zum Termin hineinkopiert kostet nichts.
|
||||
|
||||
## Phase 8 — Lokale KI · als Nächstes
|
||||
|
||||
Alles gegen Ollama, kein externer Anbieter, komplett abschaltbar.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 792 KiB After Width: | Height: | Size: 808 KiB |
|
Before Width: | Height: | Size: 816 KiB After Width: | Height: | Size: 831 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 1012 KiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 258 KiB After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 830 KiB After Width: | Height: | Size: 888 KiB |
|
Before Width: | Height: | Size: 846 KiB After Width: | Height: | Size: 904 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 663 KiB After Width: | Height: | Size: 719 KiB |
|
Before Width: | Height: | Size: 670 KiB After Width: | Height: | Size: 724 KiB |
|
Before Width: | Height: | Size: 362 KiB After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 535 KiB After Width: | Height: | Size: 539 KiB |
|
Before Width: | Height: | Size: 540 KiB After Width: | Height: | Size: 541 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 317 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 888 KiB |
|
After Width: | Height: | Size: 907 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 201 KiB |