Files
pingu-concerts/docs/wiki/Firebase.md
T

190 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Firebase
Das Firebase-Projekt heißt **MetalCircle**. Android ist dauerhaft als `de.pinguholic.concerts` registriert. Manuelle Testnachrichten funktionieren bereits. Automatischer Versand für Freundschaftsanfragen, Direktnachrichten und Veranstaltungseinladungen ist implementiert und benötigt einen separaten serverseitigen Zugang sowie `PUSH_ENABLED=true`.
## Unterschiedliche Konfigurationsdateien
- `android/android/app/google-services.json`: lokale, Git-ignorierte Android-Client-Konfiguration. Projekt-/App-Kennungen und der Client-API-Key werden vom Build in die APK übernommen; sie sind kein Backend-Privatschlüssel.
- **Service-Account-JSON**: privater Schlüssel für das Backend. Niemals in Git, APK, Docker-Image, Webassets, Chat, Wiki oder Logs aufnehmen. Die Android-Datei ersetzt diesen Zugang nicht.
## Lokale Entwicklungs-/Testumgebung
1. In Firebase **MetalCircle** auswählen und die tatsächliche **Projekt-ID** notieren; sie kann vom Anzeigenamen abweichen.
2. In der zugehörigen Google Cloud Console die **Firebase Cloud Messaging API (HTTP v1)** prüfen/aktivieren.
3. Der lokale Test verwendet den dedizierten Service Account `metalcircle-push-local@metalcircle-30d9b.iam.gserviceaccount.com`. Für dessen Versand ist die Projektrolle **Firebase Cloud Messaging API Admin**, Rollen-ID `roles/firebasecloudmessaging.admin`, erforderlich. Diese genaue Rolle ist diesem lokalen Test-Service-Account auf Projektebene im Projekt `metalcircle-30d9b` zugewiesen. Sie enthält `cloudmessaging.messages.create`. Nicht mit ähnlich benannten Firebase-Administrationsrollen verwechseln; OAuth-Tokenbezug allein beweist keine Versandberechtigung. Keine persönlichen oder Gitea-Zugänge verwenden. Siehe [Firebase IAM](https://firebase.google.com/docs/projects/iam/permissions) und [FCM-Rollen](https://docs.cloud.google.com/iam/docs/roles-permissions/firebasecloudmessaging).
4. Den privaten JSON-Schlüssel geschützt **außerhalb des Repositories und Docker-Buildkontexts** speichern. Der Betreiber verwaltet die Datei. [Firebase Admin Setup](https://firebase.google.com/docs/admin/setup) beschreibt Service-Account-Dateien.
5. Dateirechte einschränken, beispielsweise `chmod 600 /absoluter/pfad/firebase-service-account.json`. Keine Inhalte ausgeben.
6. In der lokalen `.env` die folgenden Werte selbst eintragen:
```dotenv
PUSH_ENABLED=true
FIREBASE_PROJECT_ID=<tatsaechliche-test-projekt-id>
FIREBASE_SERVICE_ACCOUNT_FILE=/absoluter/pfad/firebase-service-account.json
```
7. In der eigenen lokalen HTTP-Testumgebung starten:
```bash
COOKIE_SECURE=false docker compose -f compose.yml -f compose.push.yml up -d --build web
```
`compose.push.yml` liest `FIREBASE_SERVICE_ACCOUNT_FILE` als absoluten Host-Pfad, bindet diese Datei **read-only** unter `/run/secrets/firebase-service-account.json` in den `web`-Container ein und setzt `GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/firebase-service-account.json`. Die Quelldatei muss existieren. Der Sender prüft, dass `FIREBASE_PROJECT_ID` zur Projekt-ID des Credentials passt. Die Android-App muss dasselbe Firebase-Projekt verwenden. Schlüsseldatei, `.env` und Token gehören weder ins Repository noch in ein Image.
Cloud-Staging richtet der Betreiber separat ein; dort hinter HTTPS `COOKIE_SECURE=true` lassen. Codex auf PinguCore greift nicht automatisch darauf zu. Produktion bekommt später eigene Credentials, keine kopierten Testschlüssel.
## Verifizierter lokaler E2E-Stand
Am **15.09.2026** wurde der vollständige lokale Weg mit dem Android-Testgerät und dem lokalen Backend erfolgreich geprüft. Nach Zuweisung von `roles/firebasecloudmessaging.admin` verschwand der vorherige HTTP-403-Fehler `PERMISSION_DENIED`. Das Service-Account-Credential stimmte mit `FIREBASE_PROJECT_ID=metalcircle-30d9b` überein; der Firebase Admin SDK Versand wurde vom FCM HTTP-v1-Endpunkt angenommen.
Freundschaftsanfrage und Veranstaltungseinladung wurden im Android Notification Manager nachgewiesen. Die Direktnachrichten-Benachrichtigung wurde auf dem Gerät gesehen; Antippen öffnete den zugehörigen Chat. Alle drei Benachrichtigungen verwenden generische Vorschautexte ohne Nachrichtentext oder private Veranstaltungsdetails. Die installierte Test-App war `1.1.0-debug`, Package ID `de.pinguholic.concerts`.
Für eine Wiederholung lokale App und Backend verwenden; mit `adb reverse tcp:8080 tcp:8080` wird der Android-Testbuild an den lokalen Port 8080 weitergeleitet. Keine Cloud-Staging- oder Produktionsumgebung verwenden. In der Datenbank bedeutet `push_notifications.state='sent'`, dass der Firebase-Sendeaufruf angenommen wurde; für einen vollständigen E2E-PASS zusätzlich den tatsächlichen Android-Empfang über Notification Manager oder gleichwertige Gerätebeobachtung prüfen.
Automatisierte Backend-Tests simulieren Firebase und belegen nicht die Cloud-IAM-Berechtigung. Die lokale Suite mit 53 Tests und der Android-Debug-Build waren erfolgreich; der oben beschriebene Gerätetest hat zusätzlich den echten FCM-Versand und Empfang bestätigt. Bei erneutem `configuration`-/403-Fehler Projekt-ID, aktiven Service Account, dessen `roles/firebasecloudmessaging.admin`-Zuweisung im richtigen Firebase-Projekt, API-Aktivierung und Secret-Mount prüfen. Logs enthalten absichtlich keine vollständigen Firebase-Fehlerantworten oder Secrets. Keine Legacy-Server-Keys einsetzen.
## Pre-Production Deployment
### Bestand und Grenzen
Im Repository gibt es `compose.yml` für Web/DB, die optionale Secret-Einbindung `compose.push.yml` und eine lokale, ignorierte `compose.dev.yml` mit Reload/Quellcode-Mount. Die vom Betreiber bestätigte aktuelle Pre-Production-Adresse ist **https://konzerte.pinguholic.de/**; das Repository bleibt **`kai/pingu-concerts`**. Der absolute Server-Checkout-Pfad und zusätzliche Reverse-Proxy-/Compose-Overrides sind nicht dokumentiert. Die folgenden Befehle werden **vom Betreiber im bestehenden Pre-Production-Checkout** ausgeführt. Den vorhandenen Compose-Projektnamen und gegebenenfalls serverseitige Overrides beibehalten, damit dieselbe Pre-Production-Datenbank und dieselben Upload-Volumes verwendet werden. Keine zweite Installation mit denselben festen Containernamen auf demselben Docker-Host starten.
Neu ist `compose.preprod.yml`: Es übernimmt per `extends` die Einbindung aus `compose.push.yml`, aktiviert Push und erzwingt sichere Cookies. Start mit `compose.yml` + `compose.preprod.yml`; die dritte Datei muss nicht zusätzlich angegeben werden. Pre-Production benötigt HTTPS am vorhandenen Reverse Proxy. Die Basisdatei behält `PUSH_ENABLED=false` als Standard; lokale Konfiguration und spätere Produktion werden nicht automatisch aktiviert.
### Einmalig in Google Cloud: eigener Zugang
1. Projekt **`metalcircle-30d9b`** auswählen. Unter **APIs & Services → Enabled APIs** prüfen, dass **Firebase Cloud Messaging API** (`fcm.googleapis.com`, HTTP v1) aktiviert ist; nur falls nötig aktivieren.
2. Unter **IAM & Admin → Service Accounts → Create service account** den Namen und die ID **`metalcircle-push-preprod`** anlegen. Erwartete Adresse: `metalcircle-push-preprod@metalcircle-30d9b.iam.gserviceaccount.com`.
3. Diesem Service Account im genannten Projekt **Firebase Cloud Messaging API Admin**, exakt **`roles/firebasecloudmessaging.admin`**, zuweisen. Der Versand benötigt `cloudmessaging.messages.create`; die vordefinierte FCM-API-Rolle enthält diese Berechtigung. Keine Editor-/Owner-/Firebase-Admin-Rolle und keine zusätzliche Service Account Token Creator-Rolle für die hier verwendete JSON-Schlüssel-Authentifizierung vergeben. Nicht die ähnlich benannte Rolle `roles/firebasenotifications.admin` auswählen. [Offizielle Rollenübersicht](https://docs.cloud.google.com/iam/docs/roles-permissions/firebasecloudmessaging), [FCM HTTP v1](https://firebase.google.com/docs/cloud-messaging/send/v1-api).
4. Beim **neuen** Service Account unter **Keys → Add key → Create new key → JSON** einen Schlüssel erzeugen und über einen sicheren Übertragungsweg auf den Pre-Production-Host bringen. Der Betreiber führt diesen Schritt aus. Nicht die Admin-SDK-Schaltfläche für einen allgemeinen Standard-Service-Account verwenden und keine Organisationsrichtlinie für verbotene Schlüssel umgehen.
Der lokale Account `metalcircle-push-local@metalcircle-30d9b.iam.gserviceaccount.com` und sein Schlüssel bleiben ausschließlich lokal. Produktion bekommt später einen weiteren eigenen Account/Schlüssel. Unterschiedliche Service Accounts im selben Firebase-Projekt trennen Credentials, aber nicht die FCM-Token-Zielgruppe: Die separate Backend-Datenbank und frische Registrierung sind deshalb zwingend.
### A. Einmalig auf dem Pre-Production-Host
Es ist kein bestehendes Secret-Verzeichnis dokumentiert. **Vorschlag**, falls der Betreiber noch keine Konvention hat: `$HOME/.secrets/metalcircle/firebase-push-preprod.json` im Home-Verzeichnis des Deployment-Benutzers, außerhalb von Checkout und Docker-Buildkontext.
```bash
install -d -m 700 "$HOME/.secrets/metalcircle"
# Hier ausschließlich den NEUEN Pre-Production-Schlüssel sicher ablegen.
chmod 600 "$HOME/.secrets/metalcircle/firebase-push-preprod.json"
```
Die Datei muss dem Deployment-Benutzer gehören. Falls die sichere Übertragung einen anderen Eigentümer gesetzt hat, korrigiert der Betreiber ihn, z. B. mit `sudo chown "$(id -u):$(id -g)" "$HOME/.secrets/metalcircle/firebase-push-preprod.json"`. Das bestehende Dockerfile läuft als Container-root; bei Rootless-Docker oder zusätzlichen `user:`-Overrides muss die UID-Abbildung berücksichtigt werden. Nicht auf `chmod 644` ausweichen.
In der **bestehenden, nicht versionierten Server-`.env`** nur diese Werte ergänzen; DB-/Admin-/Gitea-Konfiguration beibehalten:
```dotenv
PUSH_ENABLED=true
FIREBASE_PROJECT_ID=metalcircle-30d9b
FIREBASE_SERVICE_ACCOUNT_FILE=/absoluter/hostpfad/zur/firebase-push-preprod.json
COOKIE_SECURE=true
```
Den Beispielpfad durch den tatsächlichen absoluten Pfad ersetzen, kein `~` in `.env`. `GOOGLE_APPLICATION_CREDENTIALS` wird durch Compose auf `/run/secrets/firebase-service-account.json` gesetzt. Der Bind-Mount ist `read_only: true`; `create_host_path: false` verhindert, dass eine fehlende Schlüsseldatei unbemerkt als Verzeichnis angelegt wird. Ein Host-Pfad ist kein Schlüsselinhalt und darf dokumentiert werden; JSON-Inhalt, private Schlüssel, OAuth- und FCM-Tokens niemals.
### B. Deployment im bestehenden Pre-Production-Checkout
Vor Aktivierung bestätigen: Die vorhandene DB ist die Pre-Production-DB, keine lokalen Daten oder Sessions wurden importiert. Übernommene `sessions`, `push_devices` und `push_notifications` aus einem anderen Umfeld müssen vor Aktivierung gezielt bereinigt werden; dies nicht durch Kopieren lokaler Daten lösen. Für den Smoke-Test frisch in Pre-Production anmelden und das Gerät dort registrieren.
```bash
git pull --ff-only
docker compose -f compose.yml -f compose.preprod.yml config --quiet
docker compose -f compose.yml -f compose.preprod.yml build web
docker compose -f compose.yml -f compose.preprod.yml run --rm --no-deps web \
python push_preflight.py \
--expected-service-account metalcircle-push-preprod@metalcircle-30d9b.iam.gserviceaccount.com
# Nur bei PASS fortsetzen. Das bestehende db-Service muss bereits laufen.
docker compose -f compose.yml -f compose.preprod.yml up -d --no-deps web
docker compose -f compose.yml -f compose.preprod.yml exec -T web \
python push_preflight.py \
--expected-service-account metalcircle-push-preprod@metalcircle-30d9b.iam.gserviceaccount.com
docker compose -f compose.yml -f compose.preprod.yml ps
docker compose -f compose.yml -f compose.preprod.yml logs --since=5m --tail=100 web
```
Zusätzliche bisher verwendete Server-Overrides bei diesen Befehlen beibehalten und ihre endgültigen Werte kontrollieren. Keine neue DB anlegen, kein `down -v`, kein Entwicklungs-Reload-Mount. `config --quiet` prüft ohne Ausgabe der interpolierten Secrets; vollständige `compose config`-/`docker inspect`-Ausgaben nicht teilen. Der normale FastAPI-Start führt die vorhandene Schema-Prüfung aus; diese Push-Konfiguration benötigt keine neue Migration.
`push_preflight.py` prüft nur lokal: Push aktiv, Pfad, reguläre Datei, restriktive Rechte, read-only Mount, syntaktisch lesbares Credential, Projekt und exakte Service-Account-Adresse. Damit fällt auch ein versehentlich eingesetzter Local-Key auf. Es gibt keine Netzwerkanfrage, keinen Push und keinen IAM-Nachweis. PASS ersetzt den echten Smoke-Test nicht.
Zum Deaktivieren in der Server-`.env` `PUSH_ENABLED=false` setzen und den Webcontainer **mit `compose.yml` + `compose.push.yml`** neu erstellen. Das Pre-Production-Override setzt explizit `true` und muss für diese Abschaltung entfallen. Der Mount kann bleiben. Bei erneutem Aktivieren können noch nicht abgelaufene Aufträge verarbeitet werden; während der Abschaltung entstehen keine neuen. Schlüsselrotation: sichere neue Datei am Host ersetzen und Webcontainer neu erstellen, damit Bind-Mount und gecachtes SDK-Credential erneuert werden.
### Android mit Pre-Production verbinden
`android/capacitor.config.ts` verwendet `METALCIRCLE_SERVER_URL`. Die Standardadresse **https://konzerte.pinguholic.de/** ist bereits eure bestätigte aktuelle Pre-Production. Eine App mit dieser Zieladresse benötigt für den neuen Backend-Zugang keine neue APK. Nur einen noch auf localhost zeigenden Testbuild oder eine App mit abweichender Adresse neu synchronisieren und bauen. Die Zieladresse wird beim Sync in die APK übernommen, nicht durch eine Backend-`.env` geändert:
```bash
# Im Verzeichnis android/ auf dem Build-Rechner:
METALCIRCLE_SERVER_URL=https://konzerte.pinguholic.de/ METALCIRCLE_LOCAL_TEST=0 npm run sync
npm run build
adb install -r android/app/build/outputs/apk/debug/app-debug.apk
```
Vor dem Wechsel am alten Backend abmelden. Package ID bleibt **`de.pinguholic.concerts`**; die APK ersetzt bei passender Signatur die bestehende Installation. Bei Signaturkonflikt nicht ungefragt App-Daten löschen. Kein `adb reverse` für das HTTPS-Pre-Production-Backend verwenden. Danach auf Pre-Production als Empfänger B anmelden, Benachrichtigungen erlauben und unter „Mein Profil“ die Registrierung bestätigen lassen. Für weitere Local-/Prod-Builds den gewünschten Ziel-URL erneut ausdrücklich setzen.
`native-push.js` verwendet ausschließlich relative `/api/push/...`-URLs und dieselbe authentifizierte Origin wie die geladene Web-App. `/api/push/session` liefert die aktuelle Session-Bindung, `POST /api/push/devices` registriert/aktualisiert nur den eingeloggten Benutzer. Geräte-ID und Token sind eindeutig; dieselbe Geräte-ID aktualisiert den Token. Ein Sitzungs-/Umgebungswechsel löscht im nativen Plugin den alten Token, leert eigene Notifications und registriert neu. Logout entfernt die serverseitige Session samt Geräte-/Outbox-Verknüpfungen; Kategorie-Abwahl und verweigerte Android-Berechtigung sind zusätzlich berücksichtigt. Datenbanken und aktive Geräte-/Session-Tabellen zwischen Umgebungen nicht kopieren.
### Smoke-Test: Backend bis zum Antippen
Zwei **Pre-Production-Testkonten** A/B verwenden. B ist auf dem Android-Testgerät angemeldet, alle drei Kategorien sind aktiv. Vor jedem Ereignis die App mit Home in den Hintergrund bringen (nicht per „Stopp erzwingen“ beenden). Den Browser von B ebenfalls nicht im Chat oder in Nachrichten/Einladungen offen lassen. Uhrzeit und IDs der Testereignisse notieren. Erst nach technischer Erfassung die Benachrichtigung antippen.
| Test | Aktion A / Voraussetzungen | Backend-Nachweis | Android-Vorschau und Ziel |
|---|---|---|---|
| 1 | Neue Freundschaftsanfrage an B, noch keine offene/bestätigte Verbindung | `friendships` mit `status='pending'`, Outbox `friend_request` | „Neue Freundschaftsanfrage“ / „New friend request“; Tap führt zu A's Profil |
| 2 | B zu einer privaten Testveranstaltung einladen | `event_invitations` mit B und `viewed_at IS NULL`, Outbox `event_invitation` | „Neue Veranstaltungseinladung“ / „New event invitation“; Tap öffnet Veranstaltung |
| 3 | Nach Annahme der Freundschaft eine neue DM an B senden | `direct_messages` mit B und `read_at IS NULL`, Outbox `direct_message` | „Neue Nachricht Du hast eine neue Nachricht.“ / „New message You have a new message.“; Tap öffnet Chat mit A |
Für die Backend-Prüfung können selektive SQL-Abfragen benutzt werden (IDs/Zeitpunkt eingrenzen; kein `SELECT *`):
```bash
docker compose -f compose.yml -f compose.preprod.yml exec -T db \
sh -c 'exec psql -U "$POSTGRES_USER" -d "$POSTGRES_DB"' <<'SQL'
SELECT user_id, platform, app_version, created_at, last_seen_at FROM push_devices ORDER BY last_seen_at DESC LIMIT 10;
SELECT user_id, language, friend_request, direct_message, event_invitation FROM notification_preferences ORDER BY user_id;
SELECT id, kind, object_id, state, attempts, created_at FROM push_notifications ORDER BY created_at DESC LIMIT 15;
SELECT id, requester_id, addressee_id, status FROM friendships ORDER BY id DESC LIMIT 5;
SELECT concert_id, user_id, viewed_at FROM event_invitations ORDER BY concert_id DESC LIMIT 5;
SELECT id, sender_id, recipient_id, read_at FROM direct_messages ORDER BY id DESC LIMIT 5;
SQL
```
Keiner dieser Befehle gibt Token, Session-Bindung, Schlüssel oder private Nachrichtentexte aus. `state='sent'` wird erst gesetzt, wenn `firebase_admin.messaging.send` erfolgreich zurückgekehrt ist. Dies belegt die FCM-Annahme, **nicht** die Zustellung. Der aktuelle Sender speichert die zurückgegebene Firebase-Message-ID nicht; stattdessen ist `push_notifications.id` als Android-Notification-Tag korrelierbar. Keine neuen SDK-Debuglogs einschalten.
Für einen technischen Nachweis direkt vor dem Tap auf dem ADB-Rechner die zur Outbox passende UUID im aktiven Notification-Manager-Bestand erfassen. Möglich ist die folgende Ausgabe mit ausschließlich UUID-Tags; keine vollständigen Notification-Dumps speichern oder teilen:
```bash
adb shell dumpsys notification | python3 -c '
import re, sys
active = False
for line in sys.stdin:
if line.strip() == "Notification List:":
active = True
continue
if active and line.strip() and len(line) - len(line.lstrip()) <= 2:
active = False
if active and "NotificationRecord(" in line and "pkg=de.pinguholic.concerts " in line:
tag = re.search(r"\btag=([a-f0-9-]{36})\b", line)
if tag:
print("active MetalCircle notification_id=" + tag.group(1))
'
```
Hersteller/Android-Versionen können den Dump anders gliedern. Leere Ausgabe ist deshalb kein alleiniger Nachweis fehlender Zustellung. Alternativ über die verbundene WebView nur `notifications.map(n => ({tag: n.tag, id: n.id}))` des Capacitor-Aufrufs `PushNotifications.getDeliveredNotifications()` betrachten, **nie** dessen komplettes Objekt (`data` enthält Session-Metadaten). Notification-Tag mit der Outbox-ID vergleichen, generische Vorschau am Gerät prüfen, dann Tap/Ziel prüfen. Ohne belastbaren Gerätebeleg bleibt der Android-Empfang **nicht verifiziert**.
Alle drei Tests zusätzlich mit B in EN wiederholen; ursprüngliche Sprache/Präferenzen danach wiederherstellen. Eine Kategorie deaktivieren: Ereignis muss entstehen, ein neuer Versandauftrag darf nicht entstehen. Im Vordergrund separat prüfen: lokale Banner-Aktion und native Anzeige; Verhalten siehe [Push Notifications](Push-Notifications.md). Testveranstaltung/DM/Anfragen anschließend gezielt entfernen bzw. ursprüngliche Verbindung wiederherstellen; keine fremden Daten pauschal löschen.
### Troubleshooting
- **Preflight abgebrochen:** `credential_service_account_mismatch` bedeutet falscher Account (z. B. Local-Key), `credential_project_mismatch` ein anderes Projekt. `credential_permissions_too_broad` verlangt engere Host-Rechte; `credential_mount_not_read_only` eine korrigierte Compose-Einbindung. Bei `credential_missing_unreadable_or_invalid` Dateiexistenz, Leserechte/UID-Mapping und Gültigkeit des neu erzeugten Schlüssels prüfen, keinen Inhalt ausgeben.
- **`configuration`/403 nach Preflight-PASS:** Preflight prüft kein IAM. Exakte Account-Zuweisung von `roles/firebasecloudmessaging.admin` auf `metalcircle-30d9b`, API-Aktivierung, nicht widerrufenen Schlüssel und Netzwerkzugang zu Google prüfen. IAM-Propagation abwarten. Der Worker loggt bewusst nur Fehlerkategorien, keine volle Google-Antwort. Für die drei aktuellen Push-Arten nicht `roles/firebasenotifications.admin` mit der HTTP-v1-Rolle verwechseln.
- **Kein Outbox-Eintrag:** `PUSH_ENABLED`, korrektes Backend in der APK, frische Registrierung von B, Kategorien, Selbstbenachrichtigung und tatsächliches neues Ereignis prüfen. Ein reines Backend-Update ändert die APK-Ziel-URL nicht.
- **`dropped`:** Auftrag kann gelesen, abgelaufen, abgewählt, blockiert oder durch Session-/Tokenwechsel ungültig sein. Direktnachrichten vor dem Versand nicht öffnen; der Chat-GET setzt `read_at`. Bereits erledigte Ziele führen beim Tap absichtlich zur Übersicht.
- **`sent`, aber nichts sichtbar:** FCM-Annahme getrennt vom Android-Empfang bewerten. Vor dem Tap erfassen; Android-App-/Kanalberechtigung, Benachrichtigungskanal, Energiesparen/Netzwerk, Force-Stop und Gerätewechsel prüfen. Identische Wiederholungs-Tags ersetzen dieselbe Meldung; neue Outbox-IDs sind verschieden. Die App löscht eigene Notifications bei Session-Wechsel.
- **Chat-Push im Vordergrund:** Capacitor 6.0.5 erhält den Payload im nativen Callback; `presentationOptions: ['sound', 'alert']` erlaubt eine native Anzeige, zusätzlich kommt der Web-Banner. Systemanzeige hängt weiterhin vom Android-Kanal ab. Es gibt keinen DM-spezifischen Kanal und keinen belegten DM-Payload-Fehler.
- **Keine Freigabe für Pre-Production ableiten:** Lokale historische Gerätetests und simulierte Tests ersetzen die drei echten Pre-Production-Smoke-Tests nicht. Aktuell liegt noch kein solcher Pre-Production-Nachweis vor.
### Lokal geprüfte Pre-Production-Vorbereitung
Die Repository-Vorbereitung wurde mit 64 erfolgreichen Backend-Tests (isolierte lokale PostgreSQL-Schemas, simuliertes Firebase) und 11 JavaScript-Tests für Push-Registrierung/-Navigation und Android-Zurück-Verhalten geprüft. Neue Prüfungen decken insbesondere falsche Service Accounts, Projekt-/Mount-/Dateirechte, relative authentifizierte Registrierungs-URLs, die gemeinsamen Payload-Eigenschaften aller drei Push-Arten und den Lesestatus von Direktnachrichten ab. Ein veralteter Sprach-Test wurde an das bereits bestehende DE/EN-Dropdown angepasst; keine Oberflächenänderung.
Compose-Konfigurationen für Basis, Local-Push und Pre-Production wurden mit synthetischen Konfigurationswerten geprüft, einschließlich Abbruch bei fehlendem Projekt/Secret-Pfad. Das Backend-Image wurde lokal gebaut und auf Credential-Dateien/Schlüssel geprüft; der einzige Schlüssel-Marker war ein kurzer synthetischer Text im bestehenden Gitea-Redaktionstest. `.env`, Android-Firebase-Konfiguration und die vorgesehenen Schlüsseldateinamen sind Git-ignoriert. Diese Prüfungen verwenden keinen Pre-Production-Key und belegen weder Server-IAM noch Zustellung auf dem Pre-Production-Gerät. Native Android-Dateien wurden nicht geändert; ein neuer APK-Build war dafür nicht erforderlich.