Calendarr direkt vom Server
Bisher las Onyx nur den Schnappschuss aus dem gemeinsamen Ordner. Der ist so aktuell wie der letzte Start der Calendarr-App — wer sie eine Woche nicht öffnet, sieht wochenalte Termine, und Onyx kann nichts dafür und nichts dagegen. Der Live-Weg stand im Plan als „bewusst weggelassen, später ohne Umbau nachrüstbar". Das stimmte: das Quellenprotokoll hat gehalten. Ein einziger Aufruf genügt. /api/caldav/events fasst CalDAV-Konten, lokale Kalender, Geburtstage, iCal-Abos und Google bereits zusammen — nachgebaut wird nichts. Angemeldet wird mit Adresse, Benutzername und Passwort statt mit einem Token, und der Grund steht im Backend: auth.py:17 ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days Ein eingetragenes Token wäre nach einer Woche tot. Onyx holt sich seines deshalb selbst und erneuert es, bevor es abläuft — die Gültigkeit liest es aus dem Token, statt sieben Tage anzunehmen; die stehen heute so im Server und morgen vielleicht nicht mehr. Kommt trotzdem ein 401, wird einmal neu angemeldet und der Abruf wiederholt. Adresse und Benutzername stehen in den Einstellungen, das Passwort im Schlüsselbund — `defaults read` ist keine Stelle, an der ein Passwort mitgelesen werden soll. Geprüft wird beim Verbinden, nicht erst beim nächsten Laden: ein Tippfehler, den man am Folgetag bemerkt, ist ein vermeidbarer. Der Schnappschuss bleibt das Netz darunter. Ohne Zugangsdaten ändert sich nichts, und ohne Netzwerk hält er, was Calendarr zuletzt geschrieben hat. Nachgesehen wird am Server alle fünf Minuten statt alle zwei: ein Dateizugriff kostet nichts, eine Anfrage über das Netz schon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1636,6 +1636,102 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"options.calendar.server.address": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Serveradresse"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Server address"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options.calendar.server.connect": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Verbinden"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Connect"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options.calendar.server.disconnect": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Trennen"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Disconnect"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options.calendar.server.hint": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Ohne Verbindung liest Onyx den Schnappschuss, den die Calendarr-App schreibt — der ist nur so aktuell wie ihr letzter Start."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Without a connection Onyx reads the snapshot the Calendarr app writes — only as current as its last launch."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options.calendar.server.password": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Passwort"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Password"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options.calendar.server.username": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Benutzername"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Username"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options.calendar.source": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
|
||||
@@ -90,6 +90,7 @@ private struct CalendarOptions: View {
|
||||
}
|
||||
|
||||
if model.kind != .appleCalendar {
|
||||
CalendarrServerSettings(model: model)
|
||||
CalendarrStatus(state: calendarrState,
|
||||
isLoading: model.isLoading,
|
||||
onRefresh: { model.refresh() })
|
||||
@@ -271,3 +272,101 @@ private extension Color {
|
||||
blue: Double(value & 0xFF) / 255)
|
||||
}
|
||||
}
|
||||
|
||||
/// Die Anmeldung am Calendarr-Server.
|
||||
///
|
||||
/// Der Schnappschuss im gemeinsamen Ordner ist nur so aktuell wie der letzte
|
||||
/// Start der Calendarr-App. Wer die Zugangsdaten hier einträgt, holt Onyx die
|
||||
/// Termine selbst — und dann ist es egal, ob Calendarr überhaupt läuft.
|
||||
///
|
||||
/// Adresse und Benutzername stehen in den Einstellungen, das Passwort im
|
||||
/// Schlüsselbund.
|
||||
private struct CalendarrServerSettings: View {
|
||||
|
||||
let model: CalendarModel
|
||||
|
||||
@State private var credentials = CalendarrCredentials()
|
||||
@State private var server = ""
|
||||
@State private var username = ""
|
||||
@State private var password = ""
|
||||
@State private var isChecking = false
|
||||
@State private var failure: String?
|
||||
|
||||
private var account: CalendarrAccount? { credentials.account }
|
||||
|
||||
var body: some View {
|
||||
if let account {
|
||||
LabeledContent {
|
||||
HStack(spacing: 10) {
|
||||
Text(account.username).foregroundStyle(.secondary)
|
||||
Button {
|
||||
credentials.clear()
|
||||
model.calendarrAccountChanged()
|
||||
} label: {
|
||||
Text("options.calendar.server.disconnect")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label {
|
||||
Text(account.server.host() ?? account.server.absoluteString)
|
||||
} icon: {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Onyx.Color.positive)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Drei Felder und ein Knopf. Geprüft wird beim Verbinden, nicht
|
||||
// erst beim nächsten Laden: ein Tippfehler, den man am Folgetag
|
||||
// bemerkt, ist ein vermeidbarer.
|
||||
TextField(text: $server) { Text("options.calendar.server.address") }
|
||||
.textContentType(.URL)
|
||||
TextField(text: $username) { Text("options.calendar.server.username") }
|
||||
.textContentType(.username)
|
||||
SecureField(text: $password) { Text("options.calendar.server.password") }
|
||||
.textContentType(.password)
|
||||
|
||||
HStack {
|
||||
if let failure {
|
||||
Text(failure).font(.callout).foregroundStyle(Onyx.Color.warning)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
Text("options.calendar.server.hint")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Spacer(minLength: 12)
|
||||
Button(action: connect) {
|
||||
if isChecking {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Text("options.calendar.server.connect")
|
||||
}
|
||||
}
|
||||
.disabled(isChecking || server.isEmpty || username.isEmpty || password.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func connect() {
|
||||
guard let url = CalendarrLiveDecoder.server(from: server) else {
|
||||
failure = CalendarrLiveError.badServer.errorDescription
|
||||
return
|
||||
}
|
||||
isChecking = true
|
||||
failure = nil
|
||||
Task {
|
||||
defer { isChecking = false }
|
||||
switch await CalendarrLiveSource.check(server: url, username: username,
|
||||
password: password) {
|
||||
case .success:
|
||||
credentials.save(server: url, username: username, password: password)
|
||||
// Das Passwort nicht im Formular stehen lassen — es liegt jetzt
|
||||
// im Schlüsselbund, und ein zweiter Ort ist einer zu viel.
|
||||
password = ""
|
||||
model.calendarrAccountChanged()
|
||||
case .failure(let error):
|
||||
failure = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user