ζ-3.6: Drop-Notification-Banner für Submission-Conflicts

Auto-Flush hat 4xx-Errors (duplicate, validation_failed, unauthorized)
bisher stillschweigend gedroppt — User offline einreichen, im Web
denselben Text posten, Online gehen → die Native-Submission war weg
ohne Hinweis.

SubmissionQueue:
- struct DropRecord (textPreview, authorName, code, message, droppedAt)
- private(set) var dropNotifications: [DropRecord]
- tryFlush sammelt jetzt einen Pre-Delete-Snapshot in dropNotifications
- consumeDropNotifications() leert die Liste — UI ruft beim
  Banner-Quittieren auf

SubmitQuoteView:
- droppedBanner zeigt alle gedroppten Drafts mit Text-Preview +
  lokalisierter Error-Message
- "Quittieren"-Button leert nur die UI-State (Server-Drop ist final)
- harvestDropNotifications() läuft nach jedem flushPending

iOS + macOS BUILD SUCCEEDED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-19 16:49:17 +02:00
parent 53f8043a2d
commit c6127a2d31
2 changed files with 77 additions and 1 deletions

View file

@ -21,10 +21,33 @@ final class SubmissionQueue {
let container: ModelContainer
private var inFlight: Bool = false
/// Sammelt Drop-Records vom letzten tryFlush eine UI-Schicht kann
/// sie als "diese N Submissions wurden permanent verworfen"-Banner
/// anzeigen, statt dass der User stumm verliert.
struct DropRecord: Identifiable, Sendable {
let id = UUID()
let textPreview: String
let authorName: String?
let code: String?
let message: String
let droppedAt: Date
}
/// Drop-Notifications der letzten Session UI liest und konsumiert
/// via `consumeDropNotifications()`.
private(set) var dropNotifications: [DropRecord] = []
init(container: ModelContainer) {
self.container = container
}
/// Leert die Liste Caller hat sie gerade angezeigt.
func consumeDropNotifications() -> [DropRecord] {
let snapshot = dropNotifications
dropNotifications.removeAll()
return snapshot
}
/// Hängt einen Draft an die Queue.
func deposit(_ draft: QuoteDraft, error: String? = nil) {
let ctx = ModelContext(container)
@ -91,6 +114,15 @@ final class SubmissionQueue {
} catch let error as ZitareAPIError where shouldDrop(error) {
Log.app.warning("Dropping pending submission (permanent error: \(error.code ?? "?", privacy: .public))")
entry.lastError = error.errorDescription
// Notify UI: dieser Draft wurde verworfen. Pre-Snapshot
// der Daten vor dem Delete machen.
dropNotifications.append(DropRecord(
textPreview: String(entry.text.prefix(120)),
authorName: entry.authorName,
code: error.code,
message: error.errorDescription ?? "Fehler",
droppedAt: Date()
))
ctx.delete(entry)
} catch let error as LocalizedError {
entry.lastError = error.errorDescription