iOS: Ablauf-Benachrichtigungen, je Server einstellbar

Die App zeigte "Bald ablaufend" nur beim Oeffnen. Jetzt erinnert sie aktiv vor
dem Ablauf - mit frei zusammenstellbaren Regeln, pro Server getrennt.

Eine Regel ist "X Tage/Wochen vor Ablauf", optional nur fuer eine Kategorie
(inklusive deren Unterkategorien), einmalig oder taeglich wiederkehrend. Eine
Kategorie-Regel ersetzt fuer ihre Produkte die allgemeine - so kann Molkerei
frueher warnen als Chips. Dazu am Ablauftag eine letzte, deutliche Warnung fuer
alle Produkte (eigener Schalter). Die Meldungen kommen als Tages-Sammelmeldung,
je Server eine, zur pro Server eingestellten Uhrzeit.

Weil Vorrania im Heimnetz laeuft und von unterwegs oft nicht erreichbar ist,
kann die App nicht zuverlaessig im Hintergrund abfragen - und iOS verlangt
ohnehin, dass lokale Benachrichtigungen im Voraus geplant werden. Also: aus den
Ablaufdaten kuenftige Termine berechnen und einplanen; aufgefrischt beim
Oeffnen, im Vordergrund und best-effort per Hintergrund-Aktualisierung. Geplant
wird fuer alle eingerichteten Server, jeder mit seinem Token aus dem Keychain;
unerreichbare werden uebersprungen. iOS erlaubt 64 ausstehende Meldungen je App,
deshalb die naechstliegenden zuerst und bei 60 gekappt.

Einstellungen sitzen hinter der Glocke je Server in der Server-Verwaltung (nicht
admin-beschraenkt, es ist eine Geraete-Einstellung). Getippt wechselt die
Meldung auf den betroffenen Server und oeffnet "Bald ablaufend". Keine
Backend-Aenderung: die Kategorie kommt aus /products, der Ablauf aus /expiring.

Die Rechenlogik (ExpiryPlanner) ist bewusst netz- und iOS-frei und mit einem
eigenstaendigen Swift-Harnisch geprueft: Kategorie-Vorrang, Unterkategorien,
Rueckfall auf die allgemeine Regel, einmalig vs. wiederkehrend und die letzte
Warnung. Modelle gegen echte /expiring- und /products-Antworten abgeglichen,
App startet ohne Absturz (auch die BGTask-Registrierung).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 21:15:09 +02:00
parent 85ac3dfa50
commit 96172c6fa0
10 changed files with 782 additions and 10 deletions

View File

@@ -46,17 +46,31 @@ struct ServerListView: View {
@State private var editing: ServerProfile?
@State private var addShown = false
@State private var pendingDeletion: ServerProfile?
@State private var notifyFor: ServerProfile?
var body: some View {
NavigationStack {
List {
Section {
ForEach(session.profiles) { profile in
Button {
session.switchTo(profile.id)
dismiss()
} label: {
row(for: profile)
HStack(spacing: 8) {
// Zeile wechselt den Server, die Glocke oeffnet die
// Benachrichtigungen - zwei getrennte Ziele in einer Zeile.
Button {
session.switchTo(profile.id)
dismiss()
} label: {
row(for: profile)
}
.buttonStyle(.plain)
Button {
notifyFor = profile
} label: {
Image(systemName: NotificationStore.config(for: profile.id).enabled
? "bell.fill" : "bell")
.foregroundStyle(Color.accentColor)
}
.buttonStyle(.borderless)
}
.swipeActions(edge: .trailing) {
Button("Löschen", role: .destructive) { pendingDeletion = profile }
@@ -64,7 +78,7 @@ struct ServerListView: View {
}
}
} footer: {
Text("Jeder Server merkt sich seine eigene Anmeldung. Ein Wechsel meldet dich nicht ab.")
Text("Jeder Server merkt sich seine eigene Anmeldung. Ein Wechsel meldet dich nicht ab. Die Glocke stellt Ablauf-Erinnerungen ein.")
}
if session.profiles.isEmpty {
@@ -88,6 +102,16 @@ struct ServerListView: View {
.sheet(item: $editing) { profile in
ServerEditView(profile: profile)
}
.sheet(item: $notifyFor) { profile in
NavigationStack {
NotificationSettingsView(profileID: profile.id, serverName: profile.name)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Fertig") { notifyFor = nil }
}
}
}
}
.confirmationDialog(
pendingDeletion.map { "\($0.name)“ löschen?" } ?? "",
isPresented: Binding(get: { pendingDeletion != nil },