diff --git a/CHANGELOG.md b/CHANGELOG.md index 669cbb6..b00a416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,56 @@ Alle Änderungen werden hier dokumentiert. Format orientiert an [Keep a Changelog](https://keepachangelog.com), Versionierung nach [Semver](https://semver.org). +## [1.7.0] — 2026-05-17 + +Minor — **`ManaAppLog`** + `ManaAppConfig.appGroup`/`logSubsystem`. +Audit 2026-05-17 V4. Ersetzt das hand-getippte `Log.swift`-Boilerplate +in jeder App durch einen Config-getriebenen Wrapper. + +### Neu + +- `ManaAppLog` (public struct, Sendable) — Factory für OSLog-Logger + gegen ein `ManaAppConfig`. Standard-Kategorien `app`/`auth`/`api`/ + `db`/`web`, plus `category("…")` für app-spezifische Kategorien. +- `ManaAppConfig.appGroup: String?` (default `nil`) — Single-Source + für den App-Group-String, der heute in jeder App 3-4× hardcoded + steht. Apps ohne Widget/ShareExt setzen weiterhin nichts. +- `ManaAppConfig.logSubsystem: String` (default = `keychainService`) + — Subsystem für `ManaAppLog`. In allen Apps heute schon + `ev.mana.`, deshalb default sinnvoll. + +### Geändert + +- Nichts breaking. Beide neuen Felder haben Default-Implementations + im Protocol-Extension, bestehende Konsumenten von `ManaAppConfig` + brauchen nichts anzupassen. +- `DefaultManaAppConfig.init` hat zwei zusätzliche optionale Parameter + (`appGroup`, `logSubsystem`), beide mit `nil`-Defaults — Quellkompatibel. + +### Tests + +- 4 neue ManaAppConfig-Tests + 5 neue ManaAppLog-Tests. 85/85 grün + (vorher 76/76). + +### Migrations-Hinweis + +Apps können ihre lokale `Log.swift` von ~13 LOC auf ~5 LOC schrumpfen: + +```swift +import ManaCore + +enum Log { + private static let mana = ManaAppLog(AppConfig.manaAppConfig) + static let app = mana.app + static let auth = mana.auth + static let api = mana.api + static let study = mana.category("study") // app-spezifisch +} +``` + +Plus `AppConfig.manaAppConfig` kann `appGroup: "group.ev.mana."` +ergänzen, damit der App-Group-String single-source ist. + ## [1.6.0] — 2026-05-17 Minor — **`ManaTheme`-Variants** in ManaTokens. Acht Web-Themes aus diff --git a/Sources/ManaCore/Auth/ManaAppConfig.swift b/Sources/ManaCore/Auth/ManaAppConfig.swift index 599ad7f..53d535a 100644 --- a/Sources/ManaCore/Auth/ManaAppConfig.swift +++ b/Sources/ManaCore/Auth/ManaAppConfig.swift @@ -21,6 +21,31 @@ public protocol ManaAppConfig: Sendable { /// Apple-Developer-Team-ID provisioniert sein und das Entitlement /// `keychain-access-groups` mit demselben Wert tragen. var keychainAccessGroup: String? { get } + + /// App-Group für Daten-Sharing zwischen App ↔ Widget ↔ ShareExt. + /// Üblich `group.ev.mana.`. `nil` für Apps ohne Extensions. + /// + /// Single-Source für den App-Group-String, der heute in jeder App + /// 3-4× hardcoded steht (AppConfig + App-Entitlement + Widget- + /// Entitlement + ShareExt-Entitlement). Die Entitlements bleiben + /// hardcoded (das verlangt iOS), aber im Swift-Code ist der Wert + /// damit single-source. + var appGroup: String? { get } + + /// OSLog-Subsystem für App-Logger, üblich `ev.mana.`. Default + /// ist `keychainService` (der schon der Konvention folgt). + var logSubsystem: String { get } +} + +// MARK: - Default-Implementationen + +public extension ManaAppConfig { + /// Default `nil` — Apps ohne Widget/ShareExt müssen nichts setzen. + var appGroup: String? { nil } + + /// Default = `keychainService`. Beide folgen heute in allen Apps + /// derselben Konvention `ev.mana.`. + var logSubsystem: String { keychainService } } /// Standard-Implementierung von ``ManaAppConfig``. Apps können diese @@ -29,14 +54,22 @@ public struct DefaultManaAppConfig: ManaAppConfig { public let authBaseURL: URL public let keychainService: String public let keychainAccessGroup: String? + public let appGroup: String? + public let logSubsystem: String public init( authBaseURL: URL, keychainService: String, - keychainAccessGroup: String? = nil + keychainAccessGroup: String? = nil, + appGroup: String? = nil, + logSubsystem: String? = nil ) { self.authBaseURL = authBaseURL self.keychainService = keychainService self.keychainAccessGroup = keychainAccessGroup + self.appGroup = appGroup + // Konvention: log-Subsystem = keychainService, falls nicht + // explizit anders gewünscht. + self.logSubsystem = logSubsystem ?? keychainService } } diff --git a/Sources/ManaCore/Telemetry/ManaAppLog.swift b/Sources/ManaCore/Telemetry/ManaAppLog.swift new file mode 100644 index 0000000..e11fbc2 --- /dev/null +++ b/Sources/ManaCore/Telemetry/ManaAppLog.swift @@ -0,0 +1,67 @@ +import Foundation +import OSLog + +/// Convenience-Factory für OSLog-Logger gegen ein `ManaAppConfig`. +/// +/// Hintergrund: Jede mana-App hatte vor v1.7.0 ein eigenes `Log.swift` +/// mit 4-6 hand-getippten `Logger(subsystem: "ev.mana.", …)`- +/// Aufrufen — der Subsystem-String und die Standard-Kategorien +/// (`app`/`auth`/`api`) wiederholten sich 8× identisch. `ManaAppLog` +/// kapselt den Subsystem-Lookup gegen die App-Config; Standard- +/// Kategorien sind Convenience-Accessoren, app-spezifische gehen über +/// ``ManaAppLog/category(_:)``. +/// +/// **Verwendung** (in der App): +/// +/// ```swift +/// import ManaCore +/// +/// enum Log { +/// private static let mana = ManaAppLog(AppConfig.manaAppConfig) +/// static let app = mana.app +/// static let auth = mana.auth +/// static let api = mana.api +/// // App-spezifische Kategorien: +/// static let study = mana.category("study") +/// static let sync = mana.category("sync") +/// } +/// ``` +/// +/// Die Standard-Kategorien sind bewusst eine kleine Schublade +/// (app/auth/api/db/web). Cards/Memoro/Manaspur sollen ihre `study`-/ +/// `audio`-/`tracking`-Kategorien weiterhin app-spezifisch deklarieren. +public struct ManaAppLog: Sendable { + public let subsystem: String + + /// Direkter Constructor (für Tests oder andere Subsysteme). + public init(subsystem: String) { + self.subsystem = subsystem + } + + /// Constructor aus einer ``ManaAppConfig``. Nutzt `logSubsystem` + /// (Default = `keychainService`, beides üblich `ev.mana.`). + public init(_ config: ManaAppConfig) { + self.subsystem = config.logSubsystem + } + + /// Allgemeiner App-Logger. + public var app: Logger { Logger(subsystem: subsystem, category: "app") } + + /// Auth-bezogene Events. + public var auth: Logger { Logger(subsystem: subsystem, category: "auth") } + + /// API-/Netzwerk-Calls. + public var api: Logger { Logger(subsystem: subsystem, category: "api") } + + /// Datenbank-/SwiftData-/Persistenz-Events. + public var db: Logger { Logger(subsystem: subsystem, category: "db") } + + /// Web-/WKWebView-bezogene Events (für Hybrid-Apps). + public var web: Logger { Logger(subsystem: subsystem, category: "web") } + + /// App-spezifische Kategorie. Beliebige Strings, weil Console.app + /// Kategorien als Free-Text filtert. + public func category(_ name: String) -> Logger { + Logger(subsystem: subsystem, category: name) + } +} diff --git a/Tests/ManaCoreTests/ManaAppConfigTests.swift b/Tests/ManaCoreTests/ManaAppConfigTests.swift index 0d5699e..301621c 100644 --- a/Tests/ManaCoreTests/ManaAppConfigTests.swift +++ b/Tests/ManaCoreTests/ManaAppConfigTests.swift @@ -24,4 +24,42 @@ struct ManaAppConfigTests { ) #expect(config.keychainAccessGroup == nil) } + + @Test("AppGroup ist optional, default nil") + func appGroupDefaultsNil() { + let config = DefaultManaAppConfig( + authBaseURL: URL(string: "https://auth.mana.how")!, + keychainService: "ev.mana.cards" + ) + #expect(config.appGroup == nil) + } + + @Test("AppGroup wird durchgereicht wenn gesetzt") + func appGroupPassedThrough() { + let config = DefaultManaAppConfig( + authBaseURL: URL(string: "https://auth.mana.how")!, + keychainService: "ev.mana.cards", + appGroup: "group.ev.mana.cards" + ) + #expect(config.appGroup == "group.ev.mana.cards") + } + + @Test("LogSubsystem default = keychainService") + func logSubsystemDefaultsToKeychainService() { + let config = DefaultManaAppConfig( + authBaseURL: URL(string: "https://auth.mana.how")!, + keychainService: "ev.mana.cards" + ) + #expect(config.logSubsystem == "ev.mana.cards") + } + + @Test("LogSubsystem kann explizit überschrieben werden") + func logSubsystemOverride() { + let config = DefaultManaAppConfig( + authBaseURL: URL(string: "https://auth.mana.how")!, + keychainService: "ev.mana.cards", + logSubsystem: "ev.mana.cards.debug" + ) + #expect(config.logSubsystem == "ev.mana.cards.debug") + } } diff --git a/Tests/ManaCoreTests/ManaAppLogTests.swift b/Tests/ManaCoreTests/ManaAppLogTests.swift new file mode 100644 index 0000000..4d71776 --- /dev/null +++ b/Tests/ManaCoreTests/ManaAppLogTests.swift @@ -0,0 +1,53 @@ +import Foundation +import Testing +@testable import ManaCore + +@Suite("ManaAppLog") +struct ManaAppLogTests { + @Test("Init aus Config nimmt logSubsystem") + func initFromConfig() { + let config = DefaultManaAppConfig( + authBaseURL: URL(string: "https://auth.mana.how")!, + keychainService: "ev.mana.cards" + ) + let log = ManaAppLog(config) + #expect(log.subsystem == "ev.mana.cards") + } + + @Test("Explizit gesetztes logSubsystem überschreibt keychainService") + func initFromConfigWithCustomSubsystem() { + let config = DefaultManaAppConfig( + authBaseURL: URL(string: "https://auth.mana.how")!, + keychainService: "ev.mana.cards", + logSubsystem: "ev.mana.cards.test" + ) + let log = ManaAppLog(config) + #expect(log.subsystem == "ev.mana.cards.test") + } + + @Test("Direkter String-Constructor") + func initFromString() { + let log = ManaAppLog(subsystem: "ev.mana.test") + #expect(log.subsystem == "ev.mana.test") + } + + @Test("Standard-Kategorien sind erreichbar") + func standardCategoriesExist() { + let log = ManaAppLog(subsystem: "ev.mana.test") + // Logger ist nicht Equatable, aber wir können Existenz prüfen + // indem wir den Wert binden — Compile genügt. + let _ = log.app + let _ = log.auth + let _ = log.api + let _ = log.db + let _ = log.web + } + + @Test("category() liefert beliebige Custom-Kategorie") + func customCategory() { + let log = ManaAppLog(subsystem: "ev.mana.test") + let _ = log.category("study") + let _ = log.category("tracking") + let _ = log.category("llm") + } +}