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

23 KiB
Raw Blame History

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 und FCM-Rollen.
  4. Den privaten JSON-Schlüssel geschützt außerhalb des Repositories und Docker-Buildkontexts speichern. Der Betreiber verwaltet die Datei. Firebase 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:
PUSH_ENABLED=true
FIREBASE_PROJECT_ID=<tatsaechliche-test-projekt-id>
FIREBASE_SERVICE_ACCOUNT_FILE=/absoluter/pfad/firebase-service-account.json
  1. In der eigenen lokalen HTTP-Testumgebung starten:
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, FCM HTTP v1.
  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.

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:

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.

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:

# 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 *):

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:

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. 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.