Skip to content

Saisonpunkte (SP): ein globales Konto, Buchung beim Training

Stand 05.10.2026 (Entscheide von Max: 30.09. „SP beim Training buchen“, 04.10. „Training und Punktezählung soll in jedem Verein funktionieren“, „Saison soll irgendwann vereinsübergreifend funktionieren“).

Modell

  • Ein SP-Konto je Nutzer und Periode. Die Tabelle Season hat je (Jahr, Quartal/Sentinel) genau eine Zeile mit clubId = 'GLOBAL' (Sentinel, kein Fremdschlüssel auf Club). SpLedgerEntry, SeasonPassProgress und BadgeAward.seasonId hängen daran. resolveCurrentSeason(prisma, now) legt die Zeile faul an (Upsert, Rennen werden über P2002 abgefangen).
  • Die Vereinsdimension steckt nur noch im Filter der Ranglisten. GET /api/challenges/season/leaderboard, /season/honours und /season/member/:id nehmen optional ?clubId= (Pflicht: aktive Mitgliedschaft in ClubMember, sonst 403 club_forbidden); ohne Parameter gilt der erste Verein des Nutzers. Die Liste zeigt die Mitglieder dieses Vereins (Pro, Einwilligung) mit ihrem globalen Stand: ein Mehrvereins-Mitglied steht in jeder Vereinsrangliste mit denselben Punkten. Keine Scheibendaten fremder Vereine.
  • ?scope=global ist vorbereitet, aber gesperrt (403 ranking_scope_not_enabled, Schalter GLOBAL_RANKING_ENABLED in routes/challenges.ts), bis die Datenschutzentscheidung zur globalen Rangliste vorliegt (eigenes Opt-in, Minderjährige, Pseudonymisierung).
  • Scheiben zählen vereinsübergreifend (Regel foreignRangeScoring, unverändert); die Wochenfrequenz ist die Vereinigung der gepflegten Trainingsabende aller Vereine des Nutzers (userTrainingWeekdays).
  • Idempotenz: awardSp über SpLedgerEntry.idempotencyKey; die Schlüssel (week:/month:/season:) tragen die seasonId, die Vorab-Lookups arbeiten über reason + refId und alle Season-Zeilen gleicher Periode (resolveEquivalentSeasonIds).

Buchungswege (alle idempotent)

  1. GET /api/challenges/season/pass beim Öffnen der App (laufende Woche, laufender Monat, Saison-Slots, Nachlauf).
  2. Live-Buchung runLiveSeasonPointsBooking im 60-s-Tick des Schedulers (Abschalten: SP_LIVE_BOOKING=off). Liest die seit dem letzten Lauf geänderten Scheiben (ClubDataScoreSheet.updatedAt > Cursor in Setting 'sp-live:cursor', Ruhephase 90 s, Compare-and-Set), bildet die betroffenen Nutzer (DISTINCT) und bewertet jeden einmal; Abkühlzeit 2 min je Nutzer, höchstens 1000 Scheiben und 200 Nutzer je Tick, 3 Wiederholungen bei Fehlern. Der Ingest wird nicht berührt (ein Wächtertest verbietet Importe des SP-Kontos im Ingest-Code). Nummer → Konto über User.sportpassId ODER UserSetting.data.sportpassId; im LIVE-Job bucht eine Nummer mit mehr als einem Konto niemand (Sweep und GET buchen jedes Konto mit seinem eigenen Saatgut, wie schon vor dem Umbau); nie über die E-Mail.
  3. Sweep runSeasonChallengeSettleSweep alle 6 h: geschlossene Perioden im Horizont und Saison-Challenges für alle Pro-Konten, seitenweise (500 je Seite).

Migration 20261005120000_sp_globales_konto

Je (Jahr, Quartal) wird die Zeile mit clubId = 'GLOBAL' (sonst die älteste) kanonisch; Ledger und Badges werden umgehängt, der Pass-Cache (SeasonPassProgress) behält je Nutzer die Zeile mit den meisten SP, die übrigen Season-Zeilen werden gelöscht, clubId wird NOT NULL DEFAULT 'GLOBAL'. Die Idempotenzschlüssel behalten ihre alte seasonId (Zeichenkette ohne Fremdschlüssel). Prod 04.10.2026: 4 Season-Zeilen, 56 Ledger-Zeilen, kein Konto mit Ledger in mehreren Zeilen derselben Periode. Test: spGlobalesKontoMigration.db.test.ts.

Migration 20261005110000_sp_moments_auf_globale_saison (läuft VOR sp_globales_konto)

Der Moment-Feed (#471) liegt vor dem SP-Umbau auf Prod. Seine Zeilen tragen Vereins-Saison-IDs in den Schlüsseln level:, challenge: (bei Saison-Slots auch als Periode), certificate: und in MomentEvent.seasonId. Die Migration schreibt sie auf die kanonische Saison um und läuft bewusst VOR sp_globales_konto, solange die nicht kanonischen Season-Zeilen noch existieren (nur dann ist die Zuordnung alt → kanonisch bekannt). Dieselbe Kanon-Regel (GLOBAL zuerst, dann createdAt, dann id); die kanonische Zeile ist immer eine bestehende, ihre id bleibt. Bei Schlüssel-Kollision (@@unique [userId, key]) bleibt die älteste Zeile (der Seen-Cursor wandert nur vorwärts: ist ein Moment gesehen, dann der älteste), nichts wird neu angelegt. Test: spMomentMigration.db.test.ts fährt beide Migrationen nacheinander und prüft, dass keine Zeile auf eine gelöschte Saison zeigt. Der Index idx_scoresheet_updated_at (ClubDataScoreSheet.updatedAt) trägt den Live-Job (Migration 20261005130000).

Nachlauf seasonConsolidation.ts (Rollout-Fenster)

Prod migriert vor dem Abbild-Tausch; im Fenster legt der alte Code wieder Vereins-Seasons an. Im 60-s-Tick fährt consolidateStrayClubSeasons dieselben Anweisungen wie die beiden Migrationen (wortgleich, Test), sobald eine Season mit clubId <> 'GLOBAL' existiert. Die Transaktion hält einen Advisory-Lock und sperrt Season/Ledger/Pass/Badge/Moment gegen Schreiber (SHARE ROW EXCLUSIVE, lock_timeout 5 s: bei Zeitüberschreitung kein Lauf, nächster Tick). Danach wird der Pass-Cache je betroffenem Konto aus dem Ledger neu berechnet (Meilenstein-Badges) und die Stufen-Momente der überschrittenen Stufen werden nachgeschrieben. Die Liste der Konten steht DAUERHAFT in der Setting-Zeile sp-stray:recompute (im selben Commit wie die Zusammenführung): ein Neustart oder Fehler danach verliert nichts, der nächste Tick arbeitet den Rest ab. Ein Deadlock mit der Kontolöschung (sie sperrt BadgeAward → Pass → Ledger, der Nachlauf in anderer Reihenfolge) bricht den Nachlauf ab, die Löschung läuft weiter, der nächste Tick holt nach; das Log nennt nur den SQLSTATE (40P01, 55P03). Bekannte Restgrenze: Wurden im Fenster vom alten Code Stufen-Momente einer frischen Vereinssaison geschrieben, deren Stufe das Konto schon vor dem Feed-Start erreicht hatte oder deren globaler Moment durch die Aufbewahrung (120 Tage) schon gelöscht ist, bleibt ein solcher Moment als level:{GLOBAL}:k stehen (setzt mindestens 150 SP im Fenster voraus). Betriebsprüfung nach dem Rollout: SELECT count(*) FROM "Season" WHERE "clubId" <> 'GLOBAL' muss 0 sein.

Sichtbarkeit von Buchungstagen (Entscheid 05.10.2026, Recht/Koordination)

SpLedgerEntry hat keine clubId; "Buchungstage aus dem eigenen Verein" lassen sich deshalb nicht abgrenzen. Im Mitgliedsprofil (/api/challenges/season/member/:memberId) sieht deshalb NUR die Person selbst spEntries (Grund, SP, Zeitpunkt) und die Zeitpunkte ihrer Abzeichen. Fremde bekommen die SP-Summe (Rangliste, Ehrentafel), die Abzeichen ohne Zeitpunkt (awardedAt: null) und kein spEntries-Feld. Test: spLiveBooking.db.test.ts.

Widerruf der Einwilligung (Teil 1) löscht auch Punkte, Stufen, Auszeichnungen und Urkunde (Entscheid Max 06.10.2026)

Ausnahme: Widerrufe von alten oder unbekannten Clients (Abschnitt „Frist für die Folgedaten nach dem Widerruf“ unten): dort bleiben die Folgedaten je Widerruf bis zum späteren von RELEASE + 30 Tage und Widerrufstag + 30 Tage gesperrt und werden erst danach gelöscht. Alles Folgende gilt ohne diese Frist sofort.

Der Widerrufs-Purge (shootingDataRevocationPurge.ts, Takt ca. 15 Minuten) löscht für jedes Konto, dessen Einwilligung in die Trainingsauswertung (SHOOTING_DATA_VISIBILITY) REVOKED ist, zusätzlich zu den Schießdaten und Momenten in EINER Transaktion: SpLedgerEntry (Punkte), SeasonPassProgress (Stufen), BadgeAward (Auszeichnungen) und die Urkunde (MomentEvent certificate:<saison>). Das steht so im Elternfreigabe-Formular Fassung 2. Es läuft bei jedem Lauf (wie die Momente): eine Nachbuchung, die nach dem Widerruf noch ankommt, geht im nächsten Lauf wieder. /season/honours liest die Urkunde live aus dem Punktekonto und nennt ohne Punkte keine mehr. Nicht gelöscht werden die Saison-Zeilen selbst (Season, global). Zusätzlich gehen die Posteingangs-Hinweise „Training ausgewertet“ (UserNotification mit dedupeKey session-evaluated:…, sie tragen Bestwerte und Ringzahlen). Damit nichts nachwächst, bucht awardSp für ein Konto mit widerrufener Einwilligung nichts mehr, recomputeSeasonPassProgress legt keine Stufen-Zeile mehr an, und evaluateAndAwardBadges vergibt keine Auszeichnungen: der Duell-Abruf, die Live-Buchung, der Sweep und der Pass-Abruf laufen über diese Funktionen (hasRevokedShootingDataConsent in seasonPass.ts). Wird die Einwilligung später neu erteilt, beginnt das Konto bei null.

Duell- und Herausforderungsergebnisse (Recht 07.10.2026, revocationChallengeCleanup.ts): Beim selben Lauf werden die Teilnahmen des Kontos entfernt (Werte progressJson/resultJson und Kontobezug; bei Herausforderungen mit mehreren fällt nur seine Zeile, Plätze und Punkte der anderen werden nicht neu berechnet), seine Duellrunden, sein Rekordbuch und seine App-Sessions samt Schüssen. Beim Gegner eines Duells bleiben Ergebnis, Ausgang, Punkte und Auszeichnungen; die Hülle wird wie bei der Kontolöschung neutralisiert (settleDuelShells: Titel „Duell: … vs. Teilnehmer entfernt“, Kennung durch einen Platzhalter ersetzt), ein Duell ohne Gegenüber oder ohne Ergebnis fällt ganz, und die Duell-Meldungen im Posteingang des Gegners mit dem Klarnamen gehen weg. Damit buchen auch beendete Duelle bei einer erneuten Einwilligung nichts mehr nach.

Saison-Rangliste, Ehrentafel, Urkunde: Ränge nur über Sichtbare, kein hiddenCount (Entscheid 08.10.2026)

Die Ränge der Saison-Rangliste (buildSeasonLeaderboard) laufen nur über die sichtbaren Zeilen plus die eigene. Über alle Mitglieder zu ranken ließ Lücken in den Rangnummern („1, 2, 4“): ein verborgenes Mitglied (etwa ein Kind ohne Teil 2 der Elternfreigabe) lag erkennbar zwischen zwei sichtbaren SP-Werten, in einem kleinen Verein zuordenbar. hiddenCount (Saison-Rangliste und Ehrentafel) ist immer 0 und bleibt nur als Feld stehen, weil ältere Clients den Hinweis „n weitere Mitglieder haben nicht freigegeben“ nur bei > 0 zeigen. Die Urkunde ist damit konsistent: place und outOf zählen beide über Sichtbare plus selbst („Platz 5 von 4“ ist unmöglich). Tests: seasonLeaderboard.test.ts, seasonHonours.test.ts, memberProfileVisibility.test.ts.

Frist für die Folgedaten nach dem Widerruf (Markierung nach App-Kennung, Recht/Koordination 08.10./11.10.2026)

Wer Teil 1 widerrief, bevor der neue Dialogtext ausgeliefert war, hat nicht in eine endgültige Löschung der Folgedaten eingewilligt. Den alten Text („… sofort wieder ausgeblendet, bis du die Freigabe erneut aktiv erteilst“) zeigten die TestFlight-Builds 33/34 UND die Web-App auf Prod bis zum Release. Für Widerrufe, die nicht von einem eindeutig neuen Client kamen (blockedReason = self_revoked), bleiben die Folgedaten je Widerruf bis zum späteren von RELEASE + 30 Tage (nur Altbestand, in Stunden) und dem Ablauf (24:00 Berliner Zeit) des 30. Kalendertags nach dem Widerruf gesperrt (Recht 11.10.2026, F5: auch ein alter Client, der nach dem 08.11. widerruft, behält seine 30 Tage; die Wirkung des Widerrufs selbst, z. B. die Vereinsdaten, ist sofort), danach löscht der Purge sie automatisch (services/revocationGrace.ts). Folgedaten sind alles im Block „Folgedaten“ (Saisonpunkte, Stufen, Auszeichnungen, Momente samt Urkunde, Hinweise „Training ausgewertet“) und im Block „Duell- und Herausforderungsergebnisse“ (Teilnahmen, Duellrunden, Rekordbuch, App-Sessions, Neutralisieren der Duell-Hülle) in shootingDataRevocationPurge.ts, beide hinter !inGrace. Die Vereins-Schießdaten (Blätter, Schützensatz, Serien, Treffer) gehen weiter sofort. Erteilt die Person die Freigabe in der Frist erneut, ist die Zeile nicht mehr REVOKED, und die Löschung entfällt von selbst. - Gesperrt heißt: nicht ausgewertet (hasRevokedShootingDataConsent bucht nichts mehr) und für andere nicht sichtbar in Ranglisten, Ehrentafel, Mitgliedsprofil, Duell-Gegnersuche und Teilnehmerlisten (sie hängen an der Einwilligung). Der Moment-Feed liefert nur die Momente des angemeldeten Kontos selbst. Grenzen: (1) Beendete Duelle sind beim Gegner und für Vereinsbeobachter mit Name, Titel und Sieger sichtbar (die Duell-Hülle wird in der Frist nicht neutralisiert; der Dialogtext sagt „Teilnehmer entfernt“ und gilt erst nach der Frist; der Code-Fix zum 16.10.2026 betrifft Widerruf UND Kontolöschung). (2) Ein laufendes Duell mit der widerrufenen Person kann beim Abruf nach dem Fensterende ohne Einwilligungsprüfung gewertet werden (Wert 0, Sieg des Gegners); das war auf Prod schon so und wird erst mit dem Duell-Fix behandelt. (3) Hinweise „Training ausgewertet“ und App-Sessions bleiben in der Frist liegen und sind nur für die Person selbst sichtbar. - Prod-Bestand: Der bisherige Purge auf Prod löscht bei jedem Lauf schon alle Momente außer den Urkunden. Für Widerrufe VOR dem Release sind die Momente also bereits weg; die Frist schützt dort Punkte, Stufen, Auszeichnungen, Urkunden und die Duell-/Challenge-Ergebnisse. Eine Mail darf keine Momente versprechen. - Bekannter Rest (Vermutung, nicht reproduziert): revokeSelfShootingDataConsent liest die Zeile außerhalb der Transaktion. Ein genau gleichzeitiger erneuter Widerruf und eine Neu-Erteilung derselben Person könnten eine REVOKED-Zeile ohne revokedAt/blockedReason hinterlassen; sie fällt dann fail-safe in die Frist (sichere Richtung). Bei einem wiederholten Widerruf tragen policyVersion/policyLocale der Zeile die Werte des letzten Aufrufs, revokedAt den ersten Zeitpunkt; der Verlauf steht vollständig in den Ereignissen (ShootingDataConsentEvent). - Ausnahme, sofort voll: Papier-Widerruf (blockedReason = guardian_paper_revocation). Admin-Korrekturen legen keinen Widerruf an (sie ändern nur subjectType einer schon widerrufenen Zeile und erben die Frist des Widerrufs). Fehlt revokedAt (Altzeile), gilt fail-safe die Frist. Ein wiederholter Selbst-Widerruf verschiebt weder revokedAt noch den Grund (revokeSelfShootingDataConsent). - Kinder (Konten mit Elternfreigabe: subjectType MINOR, oder Teil 1 mit Eltern-Weg, d. h. guardianVerifiedAt/guardianVerificationMethod; auch nach einer Korrektur der Einstufung auf UNKNOWN): Ihre Frist endet NICHT bei RELEASE + 30 Tage, sondern beim späteren von revocation:grace-minor-until (Vorgabe 31.12.2026 23:59 +01:00) und dem Fristende dieses Widerrufs (Erwachsenen-Regel unten). Grund (Recht 08.10.2026): Nach einem Widerruf gibt es für ein Kind heute keinen Weg zurück auf GRANTED (Selbstfreigabe guardian_consent_required, neue Eltern-Anfrage consent_revoked, Papier verlangt GRANTED, Korrektur verlangt einen Geburtsdatumsbeleg, den der Purge löscht). Der Wert wird später ohne Deploy auf „Nachricht über Stufe 2b + 14 Tage“ gesetzt (Stufe 2b = Rückweg REACCEPTED, geplant bis 06.11.2026). Ist er unlesbar, gilt die Vorgabe 31.12., nie früher; ein bewusst früher gesetzter lesbarer Wert gilt, aber nie vor dem Ende der Erwachsenen-Frist (Frist des Kindes = max(minor-until, RELEASE + 30 Tage, Widerrufstag + 30 Tage); ein früherer Wert, etwa ein Jahres-Tippfehler, wird im Log als minor_until_before_adult_end gemeldet und nicht befolgt). Papier-Widerruf und Widerruf eines eindeutig neuen Clients (self_revoked_full) gehen auch bei Kindern sofort voll. - Welcher Client? Der Server liest beim Selbst-Widerruf den Kopf X-TargetShot-App (ios 1.0.7 (35), android 1.0.7 (23), web 0.23.0; services/appKennung.ts) und schreibt blockedReason = self_revoked_full (eindeutig neuer Dialogtext: iOS-Build ≥ 35, Android-versionCode ≥ 23, Web ≥ 0.23.0 – Folgedaten voll und sofort) oder self_revoked (alt, unbekannt, fehlend oder unlesbar: Frist). Im Zweifel gilt „alt“. Das Ereignis trägt meta = { app, einstufung, neuerDialog, wiederholt } mit der geprüften Kennung (nie der rohe Kopf) und dem Grund der Einstufung (neu, kein_kopf, unter_schwelle, schwelle_unlesbar, notbremse). Bei einem wiederholten Widerruf beschreibt einstufung den aktuellen Aufruf, neuerDialog die gespeicherte Markierung des ersten (z. B. erst Build 34, dann 35: einstufung: neu, neuerDialog: false, wiederholt: true). Keine Rückwirkung: ein schon widerrufenes Konto behält Markierung und Zeitpunkt des ersten Widerrufs. Einen Stichtag gibt es nicht mehr (revocation:grace-cutoff wird nicht gelesen). - Konfiguration in der Tabelle Setting, bei jedem Lauf neu gelesen (ein Anpassen braucht keinen Deploy): revocation:app-min-build-ios (Vorgabe 35), revocation:app-min-build-android (Vorgabe 23), revocation:app-min-version-web (Vorgabe 0.23.0) – fehlt die Zeile, gilt die Vorgabe, steht sie da und ist unlesbar, gilt „alt“; revocation:grace-release (RELEASE) und revocation:grace-minor-until (Ende der Frist für Kinder), Wert = ISO-8601-Zeichenkette mit Datum, Uhrzeit und Zeitzone (2026-10-13T00:00:00+02:00; alles andere gilt als unlesbar); Notbremse revocation:grace-force-all (true/false): true schickt auch self_revoked_full in die Frist (nie umgekehrt), eine vorhandene, aber unlesbare Zeile gilt als AN (sichere Seite) und wird gemeldet. Fail-safe: fehlt RELEASE oder ist es unlesbar, gilt die Vorgabe 10.10.2026 00:00 Berlin; ein Lesefehler heißt Notbremse AN. Unlesbare Zeilen, ein nicht lesbarer Zugriff, ein RELEASE vor der Vorgabe (release_before_default) und ein zu frühes minor-until (minor_until_before_adult_end) stehen als revocation_grace_config_unreadable (nur Schlüsselnamen, keine Werte) im Protokoll; jeder Lauf mit aufgeschobenen Widerrufen schreibt revocation_purge_grace (Anzahl, Notbremse, RELEASE und minor-until). Runbook: Vault Runbooks/Widerruf-App-Kennung.md. - Fristende (Mitternacht-Regel, Koordination 11.10.2026, B-01): Gelöscht wird nie vor 00:00 Berlin nach dem angezeigten Tag. Ein Fristende mitten am Tag (Altbestand RELEASE + 720 h = 08.11. 23:00; minor-until 31.12. 23:59; ein später gesetzter Wert) rückt im Code (aufMitternachtBerlin in revocationGrace.ts) auf die nächste Mitternacht Berlin: Altbestand 09.11. 00:00, Kinder 01.01. 00:00; ein Ende genau um Mitternacht bleibt. Settings auf Prod ändern sich dafür nicht. Roh gerechnet: - Fristende: das spätere von RELEASE + 30 × 24 Stunden (Altbestand: Widerrufe vor dem Release behalten genau diesen Wert) und fristEnde(Widerruf, 30) aus utils/berlinerKalender.ts (Ablauf des 30. Kalendertags nach dem Widerruf, 24:00 Berlin; dieselbe Quelle wie Mail, Sperre und Purge, §§ 187 Abs. 1, 188 Abs. 1 BGB – der Hinweis „nach Ablauf des {Datum}“ nennt diesen Tag). Fehlt revokedAt (Altzeile, Wettlauf), gilt nur RELEASE + 30 Tage; updatedAt ist KEIN Bezug (es wandert mit jedem Schreiben auf die Zeile). Ein wiederholter Selbst-Widerruf einer Zeile ohne revokedAt schreibt den Zeitpunkt einmal fest (Status und Grund bleiben; die Frist dieser Altzeile beginnt dann am Tag dieses Aufrufs, sie wird nie kürzer als RELEASE + 30 Tage), weitere Wiederholungen verschieben ihn nicht. Ein Widerrufsdatum in der Zukunft verlängert die Frist ohne Kappung und wird als revocation_revoked_at_in_future (nur Anzahl) gemeldet; ein absurdes Datum (Jahr > 9999) hält den Purge-Lauf nicht an, die Sperre bleibt. Die Oberflächen nennen den LETZTEN Tag der Frist („nach Ablauf des {Tag}“): das Fristende (00:00 Berlin am Tag danach) minus eine Millisekunde, Berliner Tag. Durch die Zeitumstellung am 25.10.2026 endet die Frist des Altbestands roh am 08.11.2026 um 23:00 Uhr Berlin (22:00 UTC) und wird auf 09.11. 00:00 Berlin normalisiert. Nach dem Ende löscht der nächste Lauf automatisch, ohne Kopplung an einen Mailversand: ist die Information noch nicht raus, RELEASE im Setting nach hinten schieben. - Setzen (Prod nur mit Max' Ja): je Schlüssel ein Eintrag in Setting; der Stichtag revocation:grace-cutoff fehlt absichtlich (ein auf Prod noch gesetzter Wert bleibt wirkungslos stehen). - Die betroffenen Konten liefern zwei nur lesende Abfragen (Vault, Runbooks): Regel8-Widerruf-Frist-Anzahlen.sql (NUR Anzahlen und die Konfigurationszeilen, Kinder und Erwachsene getrennt, vor/nach Deploy; durch Regel 8 gedeckt) und Widerruf-Frist-Konten-je-Konto.sql (eine Zeile je Konto: Kennung, Widerrufstag, Art, Folgedaten ja/nein, Eltern-Adresse ja/nein; läuft auf Prod NUR mit Max' eigenem Ja). Grundlage für die Richtigstellungs-Mail (die Mail ist nicht gebaut, sie geht nur mit Max' Ja). - Tests: revocationGrace.test.ts (Regel, strenge Zeitpunkte, Konfiguration), revocationGrace.db.test.ts (Purge gegen echte Postgres, auch der Produktionsweg ohne now und der wiederholte Selbst-Widerruf). Die Tests, die den sofortigen Umfang prüfen, stellen über test/helpers/revocationGraceSetup.ts die widerrufenen Zeilen für den Aufruf auf self_revoked_full (und danach zurück). Dazu appKennung.test.ts und appKennungWiderruf.db.test.ts (Kopf → Markierung über die echte Route).