Bei Network-Failure landet der Quote-Draft jetzt in einer persistenten SwiftData-Queue (\`PendingSubmission\`) statt im Error-Banner. Beim nächsten App-Launch ODER beim Wechsel auf scenePhase.active wird der Flush automatisch versucht. Retry-Policy: - 5xx oder Transport-Failure (NSURLErrorDomain) → in Queue, Retry - 4xx mit code (validation_failed, duplicate, unauthorized) → permanenter Fehler, kein Retry (User-Aktion nötig) - Hard-Limit 50 Retries pro Entry, danach pausiert App-Group-Store \`submissions.store\` (parallel zu snapshot.store) im \`group.ev.mana.zitare\`-Container. Fallback auf In-Memory falls Disk-Init scheitert (App-Group noch nicht aktiviert im Apple-Dev-Portal). UI-Pieces: - Pending-Banner zeigt Queue-Tiefe wenn > 0 - Queued-Banner nach erfolgreichem Enqueue - Form-Reset nach Enqueue (User sieht: "weg, kommt nach") - onChange(scenePhase) → Auto-Flush bei Foreground - ZitareNativeApp.task: Flush am Launch Files: - Sources/Core/Submit/PendingSubmissionModel.swift (neu, @Model) - Sources/Core/Submit/SubmissionQueue.swift (neu, @Observable @MainActor) - Sources/App/ZitareNativeApp.swift: Container-Init + environment-Wiring - Sources/Features/Submit/SubmitQuoteView.swift: enqueue + flush + banners iOS + macOS BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
2.9 KiB
Swift
84 lines
2.9 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
|
|
/// Pending Quote-Submission, die wegen Network-Fehler nicht durchging.
|
|
/// Wird beim nächsten Foreground/Reconnect via `SubmissionQueue.tryFlush`
|
|
/// nachgereicht.
|
|
///
|
|
/// Eigener Store (`submissions.store`) im App-Group-Container, damit
|
|
/// Snapshot-Sync und Pending-Submissions sich nicht in den Quere kommen.
|
|
@Model
|
|
final class PendingSubmission {
|
|
@Attribute(.unique) var id: UUID
|
|
var createdAt: Date
|
|
var lastTriedAt: Date?
|
|
var retryCount: Int
|
|
var lastError: String?
|
|
|
|
// QuoteDraft-Felder, flach gespeichert für SwiftData-Kompatibilität.
|
|
var text: String
|
|
var language: String
|
|
var authorName: String?
|
|
var authorSlug: String?
|
|
var sourceTitle: String?
|
|
var sourceKindRaw: String?
|
|
var sourceYear: Int?
|
|
var editReason: String?
|
|
var acceptedTos: Bool
|
|
|
|
init(draft: QuoteDraft, id: UUID = UUID(), createdAt: Date = Date()) {
|
|
self.id = id
|
|
self.createdAt = createdAt
|
|
self.retryCount = 0
|
|
self.text = draft.text
|
|
self.language = draft.language
|
|
self.authorName = draft.authorName
|
|
self.authorSlug = draft.authorSlug
|
|
self.sourceTitle = draft.sourceTitle
|
|
self.sourceKindRaw = draft.sourceKind?.rawValue
|
|
self.sourceYear = draft.sourceYear
|
|
self.editReason = draft.editReason
|
|
self.acceptedTos = draft.acceptedTos
|
|
}
|
|
|
|
func toDraft() -> QuoteDraft {
|
|
QuoteDraft(
|
|
text: text,
|
|
language: language,
|
|
authorName: authorName,
|
|
authorSlug: authorSlug,
|
|
sourceTitle: sourceTitle,
|
|
sourceKind: sourceKindRaw.flatMap(QuoteDraft.SourceKind.init(rawValue:)),
|
|
sourceYear: sourceYear,
|
|
editReason: editReason,
|
|
acceptedTos: acceptedTos
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Helper für den ModelContainer. Eigener Store, damit das
|
|
/// Snapshot-Schema nicht mit-migriert wird, wenn wir Submission-
|
|
/// Felder ändern.
|
|
enum PendingSubmissionContainer {
|
|
static let appGroup = "group.ev.mana.zitare"
|
|
|
|
static func defaultStoreURL() -> URL {
|
|
let fm = FileManager.default
|
|
if let groupURL = fm.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
|
|
return groupURL.appendingPathComponent("submissions.store")
|
|
}
|
|
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
?? URL(fileURLWithPath: NSTemporaryDirectory())
|
|
return docs.appendingPathComponent("submissions.store")
|
|
}
|
|
|
|
static func make(inMemory: Bool = false) throws -> ModelContainer {
|
|
let schema = Schema([PendingSubmission.self])
|
|
let config: ModelConfiguration = if inMemory {
|
|
ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
|
} else {
|
|
ModelConfiguration("submissions", schema: schema, url: defaultStoreURL())
|
|
}
|
|
return try ModelContainer(for: schema, configurations: [config])
|
|
}
|
|
}
|