Cross-Device-Live-Sync gegen mana-presence (Port 3079). Was geht: - `PresenceClient` @Observable + interner Actor: - PUT /v1/sessions bei Player-Open/Pause/Close mit MoodlitSessionPayload - SSE /v1/stream/moodlit als langlebige URLSession.bytes-Connection - Reconnect mit exponential backoff (500ms → 30s) - Last-Event-ID-Catch-Up - Echo-Filter: eigene originDevice-ID wird in remote ignoriert - 401-Retry mit Refresh-Token - AppConfig.presenceBaseURL (prod presence.mana.how, MANA_PRESENCE_URL-Override) - MoodlitNativeApp injiziert PresenceClient als Environment - RootView: connect on .active + signedIn, disconnect on .background + signedOut - MoodPlayerView: publishSession on open/pause/close, endSession on close - MoodListView: "Läuft auf einem anderen Gerät"-Banner + Tap-to-Mirror UX-Entscheidung V1: Banner zeigt remote-Mood wenn lokaler Player zu ist. Auto-Switch innerhalb laufender Player kommt in μ-8.5 (Bidirectional-Mirror). Eigenes Device-Echo wird per UUID in App-Group-UserDefaults gefiltert. Build iOS+macOS grün, 11/11 Tests grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
1.9 KiB
Swift
66 lines
1.9 KiB
Swift
import Foundation
|
|
|
|
/// Wire-Format der Mood-Session in `mana-presence` für `appId="moodlit"`.
|
|
/// Schema-Ownership liegt bei Moodlit — `mana-presence` validiert nur
|
|
/// Größe und broadcastet 1:1. Sollte sich das Format ändern, hier UND
|
|
/// `Code/moodlit/apps/web/src/lib/sync/presence.ts` UND `apps/api/...`
|
|
/// synchron mitziehen.
|
|
public struct MoodlitSessionPayload: Codable, Sendable, Equatable {
|
|
public let moodId: String
|
|
public let colors: [String]
|
|
public let animation: String
|
|
public let brightness: Int
|
|
public let speed: String
|
|
|
|
public init(moodId: String, colors: [String], animation: String, brightness: Int, speed: String) {
|
|
self.moodId = moodId
|
|
self.colors = colors
|
|
self.animation = animation
|
|
self.brightness = brightness
|
|
self.speed = speed
|
|
}
|
|
}
|
|
|
|
/// Server-Repräsentation einer aktiven Session — wird via GET / PUT
|
|
/// REST und via SSE-Stream übertragen.
|
|
public struct PresenceSession: Codable, Sendable, Equatable {
|
|
public let userId: String
|
|
public let appId: String
|
|
public let payload: MoodlitSessionPayload
|
|
public let source: String
|
|
public let sourceRef: String?
|
|
public let originDevice: String
|
|
public let isPaused: Bool
|
|
public let startedAt: String
|
|
public let updatedAt: String
|
|
public let revision: Int64
|
|
}
|
|
|
|
/// Body für `PUT /v1/sessions`.
|
|
public struct PresenceUpsertBody: Encodable, Sendable {
|
|
public let appId: String
|
|
public let payload: MoodlitSessionPayload
|
|
public let source: String
|
|
public let sourceRef: String?
|
|
public let originDevice: String
|
|
public let isPaused: Bool?
|
|
public let startedAt: String?
|
|
|
|
public init(
|
|
appId: String = "moodlit",
|
|
payload: MoodlitSessionPayload,
|
|
source: String,
|
|
sourceRef: String? = nil,
|
|
originDevice: String,
|
|
isPaused: Bool? = nil,
|
|
startedAt: String? = nil
|
|
) {
|
|
self.appId = appId
|
|
self.payload = payload
|
|
self.source = source
|
|
self.sourceRef = sourceRef
|
|
self.originDevice = originDevice
|
|
self.isPaused = isPaused
|
|
self.startedAt = startedAt
|
|
}
|
|
}
|