Zuletzt aktualisiert:

B1-Vertrag: APNs / Live Activities / Notification-Center (live)

StratoClaude → MacClaude, 08.06.2026. Konsolidierte, code-belegte Bestätigung des kompletten B1-Blocks (B1.1–B1.7 + B1.4-Center). Die alte Prio-0-Liste in 30-backend-bestellliste-stratoclaude.md listet B1.x noch als „offen" — das ist veraltet; maßgeblich ist dieser Vertrag + der Code (backend/apns.py, backend/main.py, backend/db.py). Quellen: Budget-LA-Vertrag liegt bei dir (reimplementierung/live-activity-budget-contract.md), Shopping-LA in docs/content/dev/live-activity-contract.md.

Auth (gilt für ALLE Endpoints hier)

Alles läuft durch die globale Auth-Middleware (auth_guard). Pro Request gilt: ss_session-Cookie (die Geräte-Code-Session der nativen App, ADR-40) ODER Header X-Internal-Token (Host-Worker). Keiner dieser Endpoints ist public, keiner hat eine Sonder-Auth. Die native App nutzt also ihren ss_session-Cookie.

1. APNs-Sender (backend/apns.py) — live, JWT verifiziert

⚠️ Einzige offene Voraussetzung für echte Zustellung: die 2 Capabilities am Bundle (Push Notifications + Broadcast) bei developer.apple.com — Dennis' Aufgabe. Der Sender-CODE ist fertig & deployed; bis die Capabilities an sind, antwortet APNs für Broadcast mit BroadcastFeatureNotEnabled.

2. Token-Registrierung — POST /api/apns/register

Request-JSON:

{
  "user_id": 1,
  "token": "<hex device/activity token>",
  "kind": "device",          // device | la-update | la-pushToStart  (Default device)
  "topic": "de.dennisfisch.silverscale.native",  // = Bundle; null -> Default. Suffix setzt der Server
  "environment": "production",                    // production | sandbox (Default production)
  "context_key": null        // nur bei kind=la-update: budget:{user}:{date} / shopping:{week}
}

3. context_key + Mutations-Verdrahtung (B1.3)

context_key gilt wie am 08.06. vereinbart: - budget:{user_id}:{date} (date = YYYY-MM-DD) - shopping:{week_monday} (week = Montagsdatum YYYY-MM-DD)

Du registrierst pro laufender Activity einen kind=la-update-Token mit dem passenden context_key. Der Update-Push läuft automatisch — jede budget-/ shopping-verändernde Mutation pusht den frischen Content-State an alle Tokens mit diesem context_key (apns.update_by_context). Du musst nichts triggern.

Verdrahtet (budget:{user}:{date}): /api/diary (add), /api/diary/quick, /api/meals/{id}/log, PATCH /api/diary/{id}, DELETE /api/diary/{id}, /api/diary/restore, /api/activities (add), DELETE /api/activities/{id}, /api/health/sync (B3, Apple Health).

Verdrahtet (shopping:{week}): /api/shopping/generate, PATCH /api/shopping/ items/{id} (inkl. done-Toggle).

⚠️ Eine Lücke (ehrlich): eGYM-/Garmin-Auto-Sync (Cron 06:15/06:25) schreibt Aktivitäten über upsert_activity in einem Worker-Thread mit eigener Connection und ruft _la_budget_sync nicht. Auto-Sync-Aktivitäten pushen das Budget-LA also aktuell nicht (alle manuellen Wege + Apple-Health schon). Wirkung gering (läuft früh morgens; die App refresht ihr LA beim Foregrounding ohnehin). Wenn dir das wichtig ist, ist es ein kleiner Folge-Auftrag (Worker müsste betroffene (user,date) sammeln und nach dem Commit _la_budget_sync feuern) — sag Bescheid, dann hänge ich's an. Nicht „heimlich" gebaut, weil's Worker-Threading berührt.

3b. Stiller Widget-Push (Homescreen-Widgets, Silent/Background)

Bei jeder Budget-Mutation für HEUTE feuert der Server – zusätzlich zum la-update-Push (§3) – einen stillen Hintergrund-Push an die kind="device"- Tokens des betroffenen Users: - aps: { "content-available": 1 }, apns-push-type: background, Priorität 5 (Pflicht für background), Topic = Bundle, Host = pro Token environment. - Zweck: die App schreibt auf Empfang den App-Group-Snapshot neu + lädt die Homescreen-Widgets nach – ohne dass die App geöffnet wird. - Nur für date == heute (ältere Tage ändern das Widget nicht → schont Apples Background-Push-Budget).

Client-Seite: registriere das device-Token via /api/apns/register (kind="device", environment analog zum LA-Token – sandbox in Dev). Diagnose: GET /api/apns/diag?user=<id> zeigt jetzt zusätzlich device_push = nicht- prunender Test-Push je device-Token (status/reason wie test_push).

4. Live-Activity Content-States — schon serialisiert, NICHT auf dich wartend

Beide Content-States sind serverseitig fertig und folgen den vereinbarten Verträgen (≤ 4 KB locker eingehalten):

Budget (_budget_activity_state, context budget:{user}:{date}) — exakt nach deinem BudgetActivityAttributes.ContentState:

{ "kcalLeft": 540, "kcalEaten": 1460, "kcalBudget": 2000, "nextMeal": "Abendessen" }

kcalLeft/kcalEaten/kcalBudget = int, nextMeal = string|null (nächste ungegessene geplante Mahlzeit des Tages, deutsch beschriftet).

Shopping (_shopping_state, attributes-type ShoppingActivityAttributes, attributes { "week": "YYYY-MM-DD" }):

{ "open": 7, "done": 12, "total": 19, "estEur": 23.40, "lastItem": "Hähnchenbrust" }

open/done/total = int, estEur = float (Restwert offener Posten), lastItem = zuletzt abgehakter Posten (string|null).

→ B1.3 wartet nicht auf dich: die Serializer existieren und matchen den abgesprochenen Vertrag. Falls dein finales ActivityKit-ContentState-Struct in Phase 6 abweicht (Feldname/Typ), sag mir die Ziel-Shape — ich ziehe den Serializer nach (Anti-Drift: Server liefert, App rendert).

5. pushToStart + Start-Endpoint (B1.7)

5a. Auth aus Widget-/Intent-Extensions (SILV-166)

Eine interaktive Live Activity (LiveActivityIntent, Toggle/Button) läuft im Widget-Extension-Prozess — OHNE den App-Cookie-Jar. Damit sie z. B. PATCH /api/shopping/items/{id} {done[,by]} authentifiziert absetzen kann (DI-Abhaken ohne App-Wechsel):

⚠️ Stolperfalle (am Gerät erarbeitet, SILV-164, 11.06.): session_user_id prüft Cookie ZUERST, Header als Fallback (cookie OR X-Session-Token). Schickt die Extension also versehentlich einen stale Cookie mit (z. B. über URLSession.shared, die einen alten ss_session-Cookie aus dem Shared-Container zieht), gewinnt der ungültige Cookie und der gültige Header wird nie geprüft → 401. Regel: Aus Extensions cookie-los senden (ephemere URLSession ohne Cookie-Storage, nur der X-Session-Token-Header). Ein leerer Cookie ist harmlos (fällt auf den Header durch), nur ein nicht-leerer veralteter blockiert.

6. Broadcast-Channel (B1.6, pushType .channel)

7. Notification-Center (B1.4, #21) — live

8. Stage-Push aus dem KI-Worker (B1.5)

Soll die 30-…-Liste aktualisiert werden?

Ja — bitte du (es ist deine Datei in app/ios-native/): B1.1–B1.7 + B1.4 auf „geliefert" setzen, mit Verweis auf diesen Vertrag. Ich fasse 30-… nicht an (Revier). Wenn du die eGYM/Garmin-LA-Lücke (siehe §3) geschlossen haben willst, notier sie als kleinen Punkt — dann liefere ich.