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:
46
ios/Sources/ServerFetch.swift
Normal file
46
ios/Sources/ServerFetch.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
/// Liest Daten von einem **bestimmten** Server, ohne den aktiven zu wechseln.
|
||||
///
|
||||
/// `APIClient` ist fest auf das aktive Profil verdrahtet (Session.shared).
|
||||
/// Fuer die Benachrichtigungen muss die App aber jeden eingerichteten Server
|
||||
/// abfragen - deshalb dieser parallele Pfad, der baseURL und Token ausdruecklich
|
||||
/// bekommt. Modelle und Fehlerbedeutung sind dieselben wie im `APIClient`.
|
||||
enum ServerFetch {
|
||||
/// Alles, was der Planer je Server braucht: ablaufende Chargen im Horizont,
|
||||
/// die Produkte (fuer die Kategoriezuordnung) und der Kategorie-Baum.
|
||||
struct Snapshot {
|
||||
let expiring: [ExpiringItem]
|
||||
let products: [Product]
|
||||
let categories: [CategoryItem]
|
||||
}
|
||||
|
||||
static func snapshot(baseURL: URL, token: String, horizonDays: Int) async throws -> Snapshot {
|
||||
async let expiring: [ExpiringItem] = get(
|
||||
"/expiring?days=\(horizonDays)", baseURL: baseURL, token: token)
|
||||
async let products: [Product] = get("/products", baseURL: baseURL, token: token)
|
||||
async let categories: [CategoryItem] = get("/categories", baseURL: baseURL, token: token)
|
||||
return try await Snapshot(expiring: expiring, products: products, categories: categories)
|
||||
}
|
||||
|
||||
private static func get<T: Decodable>(
|
||||
_ path: String, baseURL: URL, token: String
|
||||
) async throws -> T {
|
||||
guard let url = URL(string: "api" + path, relativeTo: baseURL) else {
|
||||
throw APIError.notConfigured
|
||||
}
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
// Etwas grosszuegiger als der interaktive Client: Der Abruf laeuft im
|
||||
// Hintergrund, ein langsamer Heimserver soll nicht sofort scheitern.
|
||||
request.timeoutInterval = 20
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
|
||||
if http.statusCode == 401 { throw APIError.unauthorized }
|
||||
throw APIError.server("Serverfehler (\(http.statusCode))")
|
||||
}
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user