Zuletzt aktualisiert:

title: "Was koch ich draus? — Live-Activity Content-State-Vertrag (SILV-325)" category: dev date: 2026-06-18


Live-Activity Content-State-Vertrag: Co-Plan „Was koch ich draus?"

Wer: StratoClaude (Backend, pusht den State) → MacClaude (App baut das Widget CookSuggestActivity). Schwester zu live-activity-contract.md (Shopping) und live-activity-budget-contract.md (Budget). Charter-Regel: Backend liefert den State, die App rendert ihn. Datenfluss: cook-suggest-vertrag.md.

Die LA begleitet die Co-op-Session zu zweit („Plan mit Jaqueline das Essen"): der eine startet, der andere bekommt pushToStart + Push, beide sehen den Fortschritt live, tippen sich per Deep-Link in den jeweils richtigen Wizard-Schritt.


ActivityAttributes-Typ: CookSuggestActivityAttributes

Statische attributes (beim Start, ändern sich nicht):

Key Typ Bedeutung
sessionId String Session-ID (Token-Lookup + Deep-Link-Ziel)
date String (ISO YYYY-MM-DD) Tag, für den geplant wird
partnerName String? Name des/der anderen (Dynamic Island)
partnerTint String? Farb-Token des Partners (users.color)
partnerEmoji String? Profil-Emoji des Partners (users.emoji)

Dynamischer content-state (Start, Update):

Key Typ Bedeutung
state String Session-State (collectingdecided), 1:1 wie CookSessionState
statusLabel String server-formuliert, der EINE Satz: „Jaqueline füllt die Fragen aus" · „Beide bereit — die Küche denkt nach …" · „3 Vorschläge sind da" · „Jaqueline schlägt Linsen-Bolognese vor" · „Entschieden: Linsen-Bolognese"
deepLinkStep String? wohin der Tap führt: wizard · waiting · proposals · decided (Client mappt auf den Screen/Schritt)
proposalCount Int „N Vorschläge bereit" (0 außer in proposed/danach)
partnerPickTitle String? Titel des aktuellen Pick des Partners („Vorschlag: …?") — null, solange er nicht gewählt hat
partnerReady Bool Partner hat den Warteraum bestätigt (für den „der andere wartet"-Beat)
decidedTitle String? gesetzt in decided — der Abschluss-Beat
updatedAt String? YYYY-MM-DD HH:MM:SS (Europe/Berlin) — Client verwirft ältere Pushes (Flacker-Schutz)
rev Int monotone Session-Revision, +1 je Mutation — Client guardet/reconciled gegen rev (killt den Sekunden-Tie-Race, wie Shopping SILV-193)
ended Bool? bei decided/cancelled: finaler Push als APNs-end-Event (~2 min Nachklang), dann räumt iOS die Karte ab

Swift-Vorlage (Mac baut):

struct CookSuggestActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var state: String
        var statusLabel: String
        var deepLinkStep: String?
        var proposalCount: Int
        var partnerPickTitle: String?
        var partnerReady: Bool
        var decidedTitle: String?
        var updatedAt: String?
        var rev: Int
        var ended: Bool?
    }
    var sessionId: String
    var date: String
    var partnerName: String?
    var partnerTint: String?
    var partnerEmoji: String?
}

aps-Payload (analog Shopping/Budget):

{ "aps": { "timestamp": 1234567890, "event": "start|update|end",
  "content-state": { "state": "proposed", "statusLabel": "3 Vorschläge sind da",
                     "deepLinkStep": "proposals", "proposalCount": 3,
                     "partnerPickTitle": null, "partnerReady": true,
                     "decidedTitle": null, "updatedAt": "2026-06-18 19:12:03",
                     "rev": 7, "ended": null },
  "attributes-type": "CookSuggestActivityAttributes",
  "attributes": { "sessionId": "cs_abc", "date": "2026-06-18",
                  "partnerName": "Jaqueline", "partnerTint": "coral", "partnerEmoji": "🐠" } } }

context_key & Token-Lookup

Push-Trigger (welcher Zustandswechsel pusht)

Der Server pusht bei jeder Session-Mutation (voller Content-State, kein Delta): - answers/readystatusLabel („… füllt aus" / „… ist bereit"), partnerReady. - alle ready → generating → „Die Küche denkt nach …". - Job fertig → proposed → „N Vorschläge bereit", proposalCount, deepLinkStep:"proposals". - pickpartnerPickTitle (der andere sieht „Vorschlag: …?"). - confirm-Einigung → finalizing → „Wird angerichtet …". - decideddecidedTitle, ended:true (APNs end, ~2 min). - cancelled/letzter leave/TTL → ended:true (end).

Ehrlichkeit / Reconcile (wie Shopping SILV-193)

Kein Push ist eine Zustell-Garantie (APNs fire-and-forget). Darum: monotone rev im Content-State + der Client zieht beim Push-Aufwachen / Deep-Link den vollen Zustand via GET /api/cook-suggest/sessions/{id} (= Wahrheit). Push = Wecker, Pull = Wahrheit. Der Client guardet seine lokalen Updates gegen rev.