Moderation: Vertrag für Melden und Blockieren¶
Stand 09.10.2026, eingefroren für B1 (Melden, Admin) und B2 (Blockieren). Korrekturrunden 1 und 2 und der Abschluss-Commit von B1 eingearbeitet (Prüfbefunde B1-D…/B1-K…, B1-D2-…/B1-K2-…, B1-D3-…/B1-K3-…, Recht-Nachtrag 07.10. bis Abschnitt r, Antwort Recht 08.10.); jede Abweichung vom ersten Stand steht unten unter „Änderungen", die der zweiten Runde mit „(R2)", die des Abschluss-Commits mit „(R3)".
Änderungen nur mit Vermerk unten. Web, Mobile und Admin-UI bauen gegen diesen Vertrag.
Grundlagen
- Plan: Vault Design/melden-blockieren-2026-10-06/plan.md
- Recht: Vault Store/Release-Vorbereitung-2026-09-29/recht/melden-blockieren-2026-10-07.md, mit Wortlaut, DSA Art. 16/17 und Fristen
- Recht-Nachtrag: Vault Design/melden-blockieren-2026-10-06/recht-nachtrag-2026-10-07.md (c–r), alle Texte wörtlich
- Antwort Recht 08.10.: Vault Design/melden-blockieren-2026-10-06/ui-texte-gegenlesen.md, Abschnitt „Antwort Recht" (Sperrende, Satz „ab dem {Datum}")
- Entscheide der Koordination vom 07.10.: O1–O7
Grundsätze¶
- Der Server filtert.
- Ein ausgeblendetes Bild kommt in jeder fremden Antwort als
avatarUrl: null. - Das gilt für blockierte Personen und für von mir gemeldete Bildversionen.
- Die Form der Antwort bleibt gleich. Es gibt keinen Hinweis auf den Grund.
- Einzige Ausnahme ist
blockedByMeim Mitgliedsprofil, siehe B2. - Die blockierte Person erfährt nie etwas.
- Kein Feld
blockedMe. - Neutrale Fehlercodes.
- Kein Push und keine Mail an sie.
- Die meldende Person bleibt gegenüber der gemeldeten anonym. Ihre Identität steht nur in der Admin-API.
- Das Bild geht nie per Mail weiter.
- Die Mail an contact@ enthält nur Meldungs-ID, Konto-IDs, Ziel und Grund.
- Kein Bild, kein Bildlink, kein Freitext.
- Die tägliche Sammelmail enthält nur Anzahlen, Meldungs-IDs, Datum und Dauer; Konto-IDs nur in den Abschnitten zu fail-closed (Recht r).
- Ein gemeldetes Bild verschwindet nicht spurlos, solange die Meldung offen ist. Muss es aus der App, ohne gelöscht werden zu dürfen, liegt es in einem privaten Quarantäne-Container (siehe „Quarantäne"). Nach der Entscheidung wird es sofort endgültig gelöscht, ausser bei einem laufenden Verfahren — oder es kommt zurück, wenn der Admin es gesperrt hatte und kein Verstoß vorlag (Nachtrag k). Nie zurück kommt es, wenn an der Bildversion je „kein Aufdecken" oder ein Verfahren hing oder die Person es löschen wollte (R3, Recht q und r).
- Kinder dürfen melden und blockieren. Freitext ist nie Pflicht.
- Code-Namen:
moderationbzw.block. Die Wörter „Meldung“ und „report“ sind im Code schon für den StoreKit-Melder bzw. die Trainingsberichte vergeben.
Datenmodell (additive Migration)¶
ModerationReport¶
| Feld | Typ / Werte | Bedeutung |
|---|---|---|
id |
uuid | |
reporterUserId |
FK User, SetNull |
meldende Person |
reportedUserId |
FK User, SetNull |
gemeldete Person; leer nach deren Kontolöschung |
reportedAccountDeletedAt |
DateTime? | Kennzeichen „Konto gelöscht" |
reason |
avatar \| name \| harassment \| other \| rights |
genau ein Grund (Recht §1, Nachtrag g) |
note |
String?, höchstens 500 Zeichen | freiwilliger Text |
avatarKey |
String? | gemeldete Bildversion (avatarUrl bzw. Alt-Datei), bei avatar und rights |
status |
open \| removed \| dismissed \| action_taken \| withdrawn |
|
decisionReason |
sexualised \| offensive \| violence \| rights \| other, nullable |
|
decidedAt, decidedBy |
decidedBy = Admin-Subjekt |
|
outcomeNotifiedAt |
DateTime? | |
legalHoldUntil |
DateTime? | laufendes Verfahren: kein Löschlauf, keine Pseudonymisierung, Quarantäne bleibt |
legalHoldIdentity |
Json? | nur unter legalHold und nur nach Kontolöschung: { reported?, reporter? } je { userId, email, sportpassId }; fällt mit dem Ende des Verfahrens |
quarantineBlob |
String? | Name im privaten Quarantäne-Container (geteilt je Bildversion) |
quarantineOrigin |
admin \| self_delete \| replace \| account_deletion \| club_purge |
wie das Bild in die Quarantäne kam; nur admin wird nach „kein Verstoß" wiederhergestellt |
pendingPublicDeletion |
String[] | öffentliche Objekte (Blob-Adresse bzw. /uploads/avatars/<Datei>), deren endgültige Löschung scheiterte |
pendingQuarantineDeletion |
String? | Quarantäne-Kopie, deren endgültige Löschung aussteht |
blobDeletionFailedAt |
DateTime? | letzter Fehlschlag einer der beiden Löschungen; der tägliche Lauf holt beide nach |
messagesWithheld |
Boolean | „Nachricht zurückhalten" (Nachtrag n-1) |
art17Pending |
Json? | zurückgehaltene Art.-17-Nachrichten, beim Lösen nachgeholt |
noReveal |
Boolean | „kein Aufdecken" (Nachtrag n-Fortsetzung 5) |
noRevealSetAt |
DateTime? | „kein Aufdecken" wurde je gesetzt; bleibt nach police-handover stehen (R3, D3-1) |
legalHoldSetAt |
DateTime? | ein Verfahren wurde je vermerkt; bleibt nach Aufheben bzw. Ablauf stehen (R3, D3-1) |
ownerDeletionRequestedAt |
DateTime? | „Löschwunsch der Person" an einer Admin-Quarantäne (R3, K3-3) |
ownerReplacedAt |
DateTime? | „ersetzt": Upload während einer Admin-Quarantäne (R3a, K4-2) — kein Wiederherstellen, q-3 bleibt möglich |
messagesWithheldAt |
DateTime? | seit wann „Nachricht zurückhalten" gesetzt ist; fällt mit dem Flag (R3, D3-6) |
exportFollowUpDueAt |
DateTime? | offene Aufgabe „Auskunft zu Meldung X nachreichen", seit (R3, Recht p-3) |
reporterFollowUpDueAt |
DateTime? | dieselbe Aufgabe für die meldende Person (R3a, Recht s-3) |
hiddenNoticeDeliveredAt |
DateTime? | die Quarantäne-Nachricht n-1 ging an die Person (R3, D3-7) |
reporterOutcomeWithheld |
Boolean | „entfernt" an die meldende Person zurückgehalten (R3, q-Fortsetzung 1) |
reporterOutcomeHeldBy |
String[] | Kennungen der Meldungen, deren Verfahren das Ergebnis an die meldende Person halten (R3d): geschrieben beim Zurückhalten, beim Setzen eines Verfahrens und bei der Kontolöschung der gemeldeten Person; die EINZIGE Quelle für „Verfahren hält" |
operatorNotifiedAt, operatorMailFailedAt |
DateTime? | Betreiber-Mail zugestellt bzw. zuletzt gescheitert |
createdAt |
DateTime |
- Indizes:
(status, createdAt, id),(reportedUserId, status),(reporterUserId, status). - Eindeutigkeit: höchstens eine offene Meldung je Melder, Gemeldetem und Grund, durchgesetzt in der Anwendung (siehe Melden).
UserBlock¶
blockerUserIdundblockedUserId, beide FKCascade.createdAt.@@unique([blockerUserId, blockedUserId])und@@index([blockedUserId]).
UserAvatarHide¶
viewerUserIdundtargetUserId, beide FKCascade.avatarKey,createdAt.@@unique([viewerUserId, targetUserId, avatarKey]).
User.avatarUploadBlockedUntil DateTime?¶
User.moderationAvatarChangeRejectedAt DateTime?, User.moderationDeletionDeferredAt DateTime? (R3)¶
Seit wann eine Bildänderung der Person wegen fail-closed mit 503 abgelehnt bzw. ihre Konto- oder Vereinslöschung zurückgestellt ist (erster Zeitpunkt). Fällt mit der nächsten gelungenen Bildänderung bzw. mit dem gelungenen Sichern; die Zeile selbst geht mit der Löschung. Nur für die Sammelmail, nicht in der Datenauskunft (sie verriete eine offene Meldung).
Pflichten für alle neuen Tabellen¶
userDataPurgemit FK-Map, Text-/JSON-Map und Kaskaden-Listen.dataExport:- eigene Blocks mit Platzhalter für die fremde Person,
- eigene Meldungen ohne fremde Identität,
- Meldungen gegen mich nur mit Grund, Datum und Entscheidung, nie mit der meldenden Person; eine Meldung mit „Nachricht zurückhalten" und die übrigen ihrer Bildgruppe (gleiche Person und
avatarKey, ohne Bild nur die eine) fehlen, bis es gelöst ist (R3/R3a, Recht p-3, s-2). - eigene Meldungen, deren Ergebnis zurückgehalten ist oder die
meldendenErgebnisGehaltenhält: Statusin_review, Grund und Datum, ohne Ergebnis undentschiedenAm(R3a/R3d, Recht s-3). seedFullUser.
Täglicher Lauf (Scheduler, runModerationDaily)¶
Jeder Schritt für sich; ein Fehler in einem hält die anderen nicht auf.
- ModerationReport mit status != open und decidedAt älter als 6 Monate löschen. Die Grenze rechnet Kalendermonate und begrenzt den Tag auf das Monatsende (nie zu früh). Ausgenommen: legalHoldUntil in der Zukunft, quarantineBlob gesetzt, ein Löschvermerk offen (pendingPublicDeletion, pendingQuarantineDeletion), messagesWithheld, offene Aufgabe exportFollowUpDueAt, reporterOutcomeWithheld, geparkte Nachrichten (art17Pending) — diese aber nur, bis der jüngste geparkte Eintrag 6 Monate plus 60 Tage alt ist (PARK_OBERGRENZE_TAGE, ab dem Parken, B1-M3-D-1/B1-M4-D-2), ein Verfahren an der Meldung ebenso lange vorbei ist und gegen die Person weder ein Verfahren noch „Nachricht zurückhalten“ läuft. Die Löschfrist läuft im täglichen Lauf nach dem Nachholen geparkter Nachrichten. messagesWithheld und legalHoldUntil sind getrennt: fällt das Zurückhalten (Lösen oder Kontolöschung), gilt die Frist nur ohne laufendes Verfahren (Recht q-4). Speicherbegrenzung (B1-M5-D-3, B1-M6-K-4/D-2; Bedingung Recht, DSE Fassung 4): Bleibt eine entschiedene Meldung über die Frist hinaus stehen, aus welchem Grund auch immer (geparkter Text, „Nachricht zurückhalten“ an ihr selbst oder an einer anderen Meldung, offene Aufgabe „Auskunft nachreichen“, offener Löschvermerk, Quarantäne-Kopie), fallen nach Löschfrist plus 60 Tagen note und reporterUserId. Nur ein laufendes Verfahren an dieser Meldung hält beides länger. Ausnahme (M6-D-3, Recht 10.10.): Ein noch zurückgehaltenes Ergebnis an die meldende Person (reporterOutcomeWithheld) oder die Aufgabe „Auskunft an die meldende Person nachreichen“ (reporterFollowUpDueAt) hält nur reporterUserId länger, sonst wäre das Ergebnis nie zustellbar (DSA Art. 16 Abs. 5). Den Freitext note hält sie nie; bei einer offenen Aufgabe „Auskunft nachreichen“ (eine der beiden Seiten) fällt er schon mit der 6-Monats-Grenze statt erst 60 Tage danach (B1-M8-1). Die Aufgabe hält reporterUserId höchstens einen Monat ab ihrem Anlegen (frühestens fällt sie mit der 6-Monats-Grenze), also ab der Zustellung des Ergebnisses, ab der Kontolöschung der gemeldeten Person bzw. ab dem Lösen von „Nachricht zurückhalten“ (reporterFollowUpDueAt, MELDENDE_AUFGABE_MONATE, B1-M7-D-1). Danach fällt reporterUserId, die Aufgabe bleibt ohne Kennung stehen, bis sie quittiert ist. Ist das Ergebnis zugestellt und keine Aufgabe mehr im Monat offen bzw. das Verfahren beendet, fällt reporterUserId beim nächsten Lauf. Die geparkten Texte an die gemeldete Person bleiben zustellbar.
- UserAvatarHide, deren avatarKey nicht mehr die aktuelle Version des Ziels ist, löschen; ebenso die einer Alt-Datei, die es nicht mehr gibt. Ausgenommen Versionen, die gerade in der Quarantäne liegen (das Profil ist dann leer, die Version aber nicht veraltet).
- Gescheiterte endgültige Löschungen nachholen, öffentliche und Quarantäne-Kopien je mit eigenem Vermerk; Quarantäne-Bilder ohne offene oder gehaltene Meldung freigeben (endgültig löschen bzw. nach Admin-Quarantäne ohne Verstoß wiederherstellen); unverknüpfte Quarantäne-Kopien nach 24 h wegräumen. Fehlt der Container noch (vor der ersten Quarantäne), ist das kein Fehler.
- Abgelaufene Verfahren: legalHoldIdentity leeren; Moderations-Audits gelöschter Konten verlieren dann die Kennung, sofern kein anderes Verfahren die Person hält.
- Zurückgehaltene „entfernt"-Ergebnisse an meldende Personen zustellen, die nichts mehr zurückhält — weder holdNotice noch ein laufendes Verfahren an der Meldung oder gegen die Person (R3a, D4-3).
- Geparkte Abschlusstexte t-F6 zustellen, sobald gegen die Person weder holdNotice noch ein Verfahren läuft (R3a).
- AdminAuditEvent mit action moderation_* nach 6 Monaten pseudonymisieren: entityId (Konto und Meldung) und detail.targetUserId fallen, ebenso Freitext und Meldungs-Kennungen; Aufzählungswerte und Zahlen bleiben, summary wird die feste Bezeichnung der Aktion. Ausnahme: laufendes Verfahren (auch über die gesicherte Zuordnung eines gelöschten Kontos) und (R3a, D4-5, § 34 Abs. 2 BDSG) Meldungen mit holdNotice oder offener Aufgabe „Auskunft nachreichen" (beide Seiten).
- Betreiber-Mails nachsenden (offene Meldung, nicht zugestellt, Versand vor mindestens 10 Minuten gescheitert oder seit 1 h ausstehend). Der Anspruch ist ein bedingter Schreibvorgang mit derselben Fälligkeit (Frist 10 Minuten), damit zwei Läufe nie doppelt senden.
- Sammelmail „Moderation: tägliche Übersicht {Datum}" an contact@, je Berliner Kalendertag höchstens einmal, nur wenn ein Abschnitt etwas enthält (R3):
- offene Meldungen älter als 24 Stunden (Meldungs-IDs);
- „Nachricht zurückhalten" seit mindestens 14 Tagen, täglich, ohne Automatik (Meldungs-ID, seit, Dauer; D3-6);
- Aufgaben „Auskunft zu Meldung X nachreichen" bzw. „Auskunft an die meldende Person zu Meldung X nachreichen" (Meldungs-ID, fällig seit; Recht p-3, s-3);
- wegen fail-closed zurückgestellte Kontolöschungen und abgelehnte Bildänderungen (503): Anzahl, älteste offen seit, Dauer, je Konto-ID und seit (Recht r).
Kein Freitext, kein Bild, kein Link, kein Name, keine E-Mail-Adresse.
Quarantäne¶
- Privater Blob-Container
<Foto-Container>-quarantineüber die vorhandene Verbindung. Der Code legt ihn beim ersten Gebrauch selbst an (createIfNotExistsohneaccess) und liest vor jedem Schreiben die Zugriffsregel; ist sie öffentlich, wird nichts hineingelegt. Nie von Hand in Azure anlegen. - Gelesen wird nur über eine SAS-Adresse: nur Lesen, höchstens 5 Minuten von
startsOnbisexpiresOn(der Speicher kürzt jede längere Anforderung),rscc=no-store. Jeder Abruf mit Audit. - Ein Bild kommt hinein durch
quarantine-avatar(Admin, Herkunftadmin) oder automatisch, wenn die gemeldete Person bei einer offenenavatar/rights-Meldung auf genau dieser Version — oder einer entschiedenen mit laufendem Verfahren — ihr Bild löscht (self_delete) oder per Upload ersetzt (replace), ihr Konto löscht (account_deletion) oder der Verein es entfernt (club_purge). Gilt auch für Alt-Dateien (/uploads/avatars/…,<sportpassId>.webp). - Scheitert das Sichern, wird nichts geändert: Bild löschen und Bild hochladen antworten 503
avatar_change_unavailable(fail-closed, auch beim Upload bleibt das alte Bild), Konto- und Vereinslöschung werden zurückgestellt, bevor eine Betreiber-Aufgabe zum Abo hinausgeht. Beides wird am Konto vermerkt (moderationAvatarChangeRejectedAt,moderationDeletionDeferredAt) und steht in der täglichen Sammelmail (R3, Recht r). - Beim eigenen Löschen wird die Kopie vor dem Löschen der öffentlichen Version verknüpft; beim Upload in derselben Transaktion wie das Umschalten des Profils.
- Ohne verdrahteten Blob-Speicher (nur Entwicklung) wird nicht gesichert; die Meldung lässt sich dann als
withdrawnschließen. - Nach der Entscheidung der ganzen Versionsgruppe ohne laufendes Verfahren (R3: erst die Riegel der Kopie, dann die Meldungen lesen und entscheiden, K3-11):
- Herkunft
admin, die ganze Gruppe ohne Verstoß entschieden (dismissed; nie nachaction_taken), und an der Bildversion hing nie „kein Aufdecken" oder ein Verfahren (noReveal/noRevealSetAt,legalHoldUntil/legalHoldSetAt) und kein Löschwunsch der Person (ownerDeletionRequestedAt): wiederherstellen — öffentlich unter neuer Kennung, nur wenn das Profil noch leer ist (bedingter Schreibvorgang, unter dem Riegel neu geprüft). Die Ausblendungen der meldenden Personen ziehen auf die neue Version um. In-App-Info „Dein Profilbild ist wieder sichtbar" (Nachtrag l), keine Mail. - Hat die Person inzwischen ein neues Bild und hat sie die Quarantäne-Nachricht n-1 bekommen: In-App-Text „Ergebnis zu deinem früheren Profilbild … bleibt das neue Bild sichtbar" (q-Fortsetzung 3,
moderation_earlier_picture_no_breach), keine Mail; die Kopie wird gelöscht. Ein Upload während der Quarantäne setztownerReplacedAt(R3a, K4-2): kein Wiederherstellen, auch wenn das neue Bild wieder gelöscht wird. - Wird wegen „kein Aufdecken" oder Verfahren gelöscht statt wiederhergestellt (ohne Löschwunsch): Text t-F6 „Ergebnis zu deinem Profilbild" (
moderation_not_restored), ohne Grund, in App und Mail, immer — auch ohne erhaltenes n-1, weil das Bild faktisch entfernt ist und die Person sonst nie erführe, warum es fehlt. Er wartet auf das Ende von holdNotice und jedes laufenden Verfahrens gegen die Person (geparkt inart17Pendingder gehaltenen Meldung, beim Lösen bzw. im täglichen Lauf nachgeholt; R3a, Recht t-F6). Gilt bei dismiss, close-behaviour dismissed, Aufheben, Ablauf und im täglichen Lauf. - Sonst endgültig löschen (mit Vorab-Vermerk
pendingQuarantineDeletion). - Mit Verfahren: erst beim Aufheben bzw. Ablauf — und dann nie wiederherstellen.
- Person im Papierkorb (B1-M2-D1: vom Verein entfernt — kanonisch
imVereinsweg, alsoUser.deletedAtUND alle Mitgliedschaften gelöscht —, Frist läuft noch; der Halbzustand mitUser.deletedAtund aktiver Mitgliedschaft gilt als aktiv, B1-M3-D-1): dieselbe Entscheidung wie bei einer aktiven Person, also auch wiederherstellen. „Wieder sichtbar“ und q-3 (nur In-App) werden an der Meldung geparkt; bei t-F6 und R-4 geht die Mail sofort, nur das In-App-Exemplar wird geparkt (nächste Zeile). Geparktes holt der tägliche Lauf nach der Wiederherstellung durch den Verein genau einmal nach. Beim Purge fällt es mit dem Konto weg, und das Bild geht über den normalen Bildlöschweg des Purge. Nach dem Fristende (nicht mehr wiederherstellbar) wird die Kopie wie bisher endgültig gelöscht. - Zustellweg im Papierkorb (B1-M3-D-2, Recht 07.10., DSA Art. 17 Abs. 2; gilt ebenso bei eigener Löschung ohne Vereinsweg, solange sie rücknehmbar ist — nach der Rücknahme holt der tägliche Lauf das In-App-Exemplar nach wie nach einer Wiederherstellung durch den Verein, B1-M5-D-1): t-F6 und R-4 gehen als Mail sofort hinaus (es sei denn, „Nachricht zurückhalten“ oder ein Verfahren hält, dann wie bei einer aktiven Person). Statt „du kannst jederzeit ein neues hochladen“ bzw. statt des Sperrdatums steht dann: „Ein neues Bild kannst du hochladen, sobald dein Konto wieder freigegeben ist.“ / „You can upload a new picture once your account is unlocked again.“ Nur das In-App-Exemplar wird geparkt (
mailSofort). Es wird erst bei der Zustellung nach der Wiederherstellung gebaut, also mit dem normalen Satz, und ohne zweite Mail. Beim Purge entfällt nur das geparkte In-App-Exemplar. „Wieder sichtbar“ und q-3 sind reine In-App-Texte und bleiben geparkt. Das KennzeichenmailSofortwird erst gesetzt, wenn die Mail nachweislich hinausging (B1-M4-K-2); bis dahin versucht der tägliche Lauf sie erneut, und die Sammelmail an contact@ nennt sie als „Art.-17-Mail nicht zustellbar“ (nur Meldungs-Kennung). Im Zustandabgelaufen(Fristende erreicht, auch bei fälliger eigener Löschung ohne Vereinsweg, B1-M5-D-1) fällt der Upload-Satz ganz weg, der übrige Text bleibt vollständig (B1-M3-D-6, Koordination 10.10.). - Rechtsbehelf (B1-M5-D-2, Recht 10.10., DSA Art. 17 Abs. 3 lit. f), in App und Mail und in jeder Fassung des Upload-Satzes: t-F6 schließt mit „Hast du Fragen dazu oder bist du mit dem Ergebnis nicht einverstanden, schreib uns an [email protected]; wir helfen dir weiter und prüfen es auf Wunsch noch einmal. Du kannst dich außerdem an ein Gericht wenden.“ / „If you have questions or disagree with the outcome, write to [email protected]; we will help and, if you wish, review it again. You can also go to court.“ An R-4 werden „Bist du mit dem Ergebnis nicht einverstanden, schreib uns an [email protected]; wir prüfen es dann noch einmal. Du kannst dich außerdem an ein Gericht wenden.“ / „If you disagree with the outcome, write to [email protected] and we will review it again. You can also go to court.“ angehängt.
- Versand-Anspruch und bekannte Grenzen (B1-M5): Vor dem Versand setzt der Lauf unter dem Riegel einen Anspruch mit Ablaufzeit (
mailAnspruchBis, Versand-Zeitlimit plus 5 Minuten). Solange er frisch ist, fasst kein anderer Lauf den Eintrag an; nach einem Absturz läuft er ab, und der nächste Lauf versucht es erneut. Im Zustandabgelaufen(auch fällige eigene Löschung) gibt es vor dem Purge (Fristende plus 1 Stunde) praktisch keinen Neuversuch, und mit dem Purge verschwindet der Eintrag. Deshalb geht ein Fehlschlag dort sofort an contact@ (nur Meldungs-Kennung). Danach versucht das System es nicht mehr (mailAnBetreiber), damit eine Mail von Hand keinen Doppelversand auslöst. Scheitert auch diese Betreibermail, bleibt es beim Neuversuch. In der Sammelmail steht ein Fehlschlag als „nicht zustellbar“. Hält inzwischen „Nachricht zurückhalten“ oder ein Verfahren, steht er stattdessen als „zurückgehalten“ (nicht von Hand schicken).mailFehlerSeit,mailAnspruchBisundmailAnBetreiberstehen bewusst nicht in der Datenauskunft: Es sind kurzlebige Betriebsvermerke zum Zustellversuch, die mit der Zustellung, mit der Obergrenze fürs Parken oder mit der Kontolöschung fallen. Das Ergebnis selbst erreicht die Person als Nachricht (Mail, In-App), und die In-App-Nachricht steht in der Auskunft (B1-M5-D-7, B1-M6). Der Versand-Anspruch rechnet mit der Echtzeit der Datenbank (clock_timestamp()), nicht mit der Laufuhr des täglichen Laufs (B1-M6-K-1). - Löschwunsch während einer Admin-Quarantäne (R3, K3-3):
DELETE /api/profile/avatarbei leerem Profil setzt an jeder noch verknüpften Admin-Quarantäne der PersonownerDeletionRequestedAt; Antwort weiter200. Danach kein Wiederherstellen, kein „wieder sichtbar"; gelöscht wird nach der Entscheidung, bei Verfahren nach dessen Ende. - Sperrreihenfolge (R3, K3-6): Jede Transaktion nimmt ZUERST den Moderations-Riegel der Person, DANN Zeilen (Kontozeile, Meldungen, Ausblendungen) — Upload, Quarantäne, Entfernen, Wiederherstellen, Upload-Sperre, Konto- und Vereinslöschung. Ein Quelltext-Wächter hält das fest (
test/moderationLockOrder.guard.test.ts). - Der Aufräumer verwaister Fotos (
cleanupOrphanPhotos) schont jede Bildversion, die an einer Meldung steht (services/photoInUse.ts), und listet den Quarantäne-Container nie.
B1: Nutzer-API (App-Realm, angemeldet)¶
POST /api/moderation/reports¶
- Rate-Limit
nutzerRateLimitConfig, 10 je Stunde. - Body:
json { "reportedUserId": "uuid", "reason": "avatar|name|harassment|other|rights", "note": "≤500, optional" } - Antworten:
201mit{ "id": "uuid", "avatarHidden": true|false }.avatarHiddenist nurtruebeireason=avataroderrights, wenn ein Bild vorhanden war.200, gleiche Form, bei einer bereits offenen gleichen Meldung (idempotent, keine zweite Mail).- Fehler:
| Status | Code | Wann |
|---|---|---|
| 400 | invalid_reason |
unbekannter Grund |
| 400 | note_too_long |
Text über 500 Zeichen |
| 400 | cannot_report_self |
Meldung gegen sich selbst |
| 404 | report_target_unavailable |
Konto unbekannt oder kein gemeinsamer Verein mit der meldenden Person |
| 429 | Rate-Limit |
- Nebenwirkungen:
- Bei
reason=avataroderrightsmit Bild entsteht sofortUserAvatarHidefür die aktuelle Version. - In-App-Nachricht an die meldende Person, Typ
moderation_report_received. Text nach Recht §1, Bestätigung ohne Bildsatz, wenn nichts ausgeblendet wurde. - Mail an contact@ (Betreiberadresse wie
OPERATOR_EMAIL):- Betreff:
Neue Meldung <id> - Inhalt: Meldungs-ID,
reportedUserId,reporterUserId, Grund, Zeit - ohne Text und Bild
- Ein Fehler beim Versand blockiert die Meldung nicht. Er wird an der Meldung vermerkt (
operatorMailFailedAt), der tägliche Lauf sendet nach.
- Betreff:
Bild-Upload¶
POST /api/profile/avatar liefert bei aktiver Sperre 409 avatar_upload_blocked mit { "blockedUntil": "ISO" }.
In-App-Nachrichten (UserNotification)¶
- Titel und Text rendert der Server in der Sprache der Person,
appearance.language, Rückfall Deutsch. - Typen:
moderation_report_receivedmoderation_report_outcome: an die meldende Person, entfernt oder nicht entfernt (Recht §1)moderation_content_removed: an die gemeldete Person (Art. 17), zusätzlich per Mail an die Kontoadresse, mit{Grund}ausdecisionReason(beiotherder Satz des Admins)moderation_upload_blocked: an die gesperrte Person (Art. 17, Nachtrag c bzw. l-5), zusätzlich per Mailmoderation_avatar_hidden: Beginn einer Quarantäne durch den Admin (Art. 17, Nachtrag n-1), zusätzlich per Mailmoderation_earlier_picture: Verstoß bei einem selbst gelöschten bzw. ersetzten Bild (Nachtrag n-4b), zusätzlich per Mailmoderation_avatar_restored: Wiederherstellung nach Admin-Quarantäne ohne Verstoß (Nachtrag l), nur In-Appmoderation_earlier_picture_no_breach: „kein Verstoß", aber inzwischen ein neues Bild (q-Fortsetzung 3), nur In-App, nur nach erhaltenem n-1 (R3)moderation_content_removedmit dem Text „Verdacht … Behörden" (q-Fortsetzung 1), wenn an der Bildversion „kein Aufdecken" gesetzt ist oder war (R3), in App und Mail- Nachricht zurückhalten (Nachtrag n-1): Ist an irgendeiner Meldung gegen die Person
messagesWithheldgesetzt, geht keine Art.-17-Nachricht an sie hinaus (Beginn, Entscheidung, Sperre, wieder sichtbar, früheres Bild). Beim Lösen werden sie nachgeholt, soweit sie noch stimmen. Auch das Ergebnis „entfernt" an die meldenden Personen wird dann zurückgehalten und geht beim Lösen bzw. im täglichen Lauf hinaus (R3, q-Fortsetzung 1). Dasselbe Flag hält die Meldung aus der Datenauskunft der gemeldeten Person (R3, Recht p-3). - Ergebnistexte an die meldende Person:
removed, „nicht entfernt",action_taken(Nachtrag d), jeweils mit dem Hinweis auf Rechtsbehelf (j-2);withdrawn(j-3) ohne ihn. Der Satz zum Blockieren lautet überall nach j-6. Ob ein Ergebnis zurückgehalten wird, entscheidet eine Funktion (meldendenErgebnisGehalten, R3d): die gespeicherte ListereporterOutcomeHeldBy(eine Meldung daraus hat ein aktives Verfahren) oder holdNotice (an der Meldung bzw. ihrer Bildgruppe; bei einem noch nicht zugestellten „entfernt" an irgendeiner Meldung gegen die Person). Zurückgehalten heißtreporterOutcomeWithheld; zugestellt wird beim Lösen bzw. im täglichen Lauf, sobald dieselbe Funktion nichts mehr hält. moderation_replaced_not_restored(R3c, Recht w R-4): „kein Verstoß" nach Admin-Quarantäne, während der die Person ein anderes Bild hochgeladen und wieder entfernt hat (Profil leer); nur nach erhaltenem n-1; App und Mail; Sperrsatz bei laufender Upload-Sperre; zurückgehalten wie t-F6.- Kein Push.
B1: Admin-API (Admin-Realm, createTargetshotAdminAuthHook, nur platform_admin)¶
Jede schreibende Aktion und jedes Aufdecken des Bildes erzeugt ein AdminAuditEvent mit entityType: 'moderation_report' bzw. 'user'.
Lesen¶
GET /api/admin/moderation/reports?status=open|removed|dismissed|action_taken|withdrawn&reason=…&cursor=liefert{ items, nextCursor }.cursorist ein undurchsichtiger Keyset-Wert (createdAt,id);400 invalid_cursor,400 invalid_reason.task=export_follow_up(R3) liefert stattdessen alle Meldungen mit offener Aufgabe „Auskunft nachreichen", gleich welcher Status, älteste zuerst;400 invalid_task. Jeder Eintrag enthält:id,createdAt,status,reason,illegalContentNotice(reason=rights, Art. 16 DSA),hasNotereportedUser { id, name, clubIds, language }(nach Kontolöschung{ null, null, [], null }),reasonTextLanguageHint: "bitte in dieser Sprache begründen"(Nachtrag l-3;reasonTextgibt es nur einmal),reportedAccountDeletedreporterUserId(nullable)avatarStillCurrent: ob die gemeldete Version noch aktuell ist (nullohne Bildversion)quarantined,quarantineOrigin,legalHoldUntil,messagesWithheld,messagesWithheldSince(R3),noRevealexportFollowUpDueSince(R3): Aufgabe „Auskunft zu Meldung X nachreichen" offen seit, sonstnull;reporterFollowUpDueSince(R3a): dieselbe Aufgabe für die meldende PersonpriorRemovedCount6m: entfernte Bilder dieser Person in den letzten 6 MonatenuploadBlockedUntilGET /api/admin/moderation/reports/:idliefert dasselbe plusnote,decisionReason,decidedAt,decidedBy,outcomeNotifiedAt,operatorNotifiedAt,operatorMailFailedAt,reportedAccountDeletedAt,blobDeletion { pending, publicPending, quarantinePending, failedAt },art17PendingCountund (R3)reporterOutcomeWithheld,noRevealSetAt,legalHoldSetAt,ownerDeletionRequestedAt,hiddenNoticeDeliveredAt. Ohne Bild-URL.GET /api/admin/moderation/reports/:id/avatarliefert{ avatarUrl, avatarKey, quarantined, expiresAt }.avatarKeyist die undurchsichtige Kennung der gesehenen Version (v1:<Hash>); Entfernen und Quarantäne verlangen sie zurück.- Liegt das Bild in der Quarantäne: SAS-Adresse, höchstens 5 Minuten,
no-store. - Audit
moderation_avatar_revealed(mitquelle). 410 avatar_gone, wenn die gemeldete Version nicht mehr aktuell ist und keine Quarantäne-Kopie besteht.409 reveal_blocked, wenn an einer Meldung derselben Bildversion „kein Aufdecken" gesetzt ist;409 reveal_blocked_permanently, wenn es je gesetzt war (auch nachpolice-handover, R3a, Recht t-F4).503 quarantine_unavailable | reveal_unavailable.- Auflage für das Admin-UI (admin-portal, später): das Bild nur im UI anzeigen, kein Download-Knopf, kein Kopieren der Adresse, Abruf mit
Cache-Control: no-store(die SAS setztrscc=no-store).
Entscheiden¶
POST /api/admin/moderation/reports/:id/remove-avatarmit{ decisionReason, avatarKey, reasonText?, secondRemoval?, uploadBanDays?, note? }. Fürreason=avatarundrights.noteist nur intern und geht ins Audit.avatarKeyaus dem Aufdecken ist Pflicht (400 avatar_key_required); weicht er von der gemeldeten Version ab:409 avatar_changed. Ausnahme (R3, D3-1): Ist bzw. war an der Bildversion „kein Aufdecken" gesetzt, geht es ohneavatarKey(entschieden wird die gemeldete Version, ohne Ansehen; wirdavatarKeytrotzdem mitgegeben, muss er passen).- Bei
decisionReason=otheristreasonTextPflicht (konkreter Satz, 10–300 Zeichen), sonst422 reason_text_required. Der Satz ist der{Grund}der Art.-17-Nachricht. - Ist das Bild nicht in der Quarantäne: nur wenn die gemeldete Version noch aktuell ist, sonst
409 avatar_changed; liegt ein laufendes Verfahren auf einer Meldung der Version:409 quarantine_required. Gelöscht wird Blob plus Alt-Datei unteruploadssowie<sportpassId>.webp;avatarUrlundavatarUpdatedAtwerdennull. - Liegt das Bild in der Quarantäne, wird die Kopie endgültig gelöscht (ausser bei Verfahren:
quarantine: "kept"). - Alle offenen
avatar- undrights-Meldungen zu dieser Version werden mitremovedgeschlossen. Jede meldende Person erhältmoderation_report_outcome. - Die gemeldete Person erhält einmal
moderation_content_removedin der App und per Mail; nach Kontolöschung niemand (art17Mail: "no_account"). Läuft eine Upload-Sperre, steht darin „Ein neues Bild kannst du ab dem {Datum} wieder hochladen." (n-4a). - Ist bzw. war „kein Aufdecken" gesetzt (R3): stattdessen der Text „Verdacht … Behörden" (q-Fortsetzung 1), ohne {Grund};
art17Text: "suspicion". Liegt dann keine Quarantäne-Kopie vor:409 quarantine_required— erstquarantine-avatar(ohne Kennung), dann entfernen (R3a, K4-4). Ist an der Bildversion kein Verfahren aktiv:409 legal_hold_required— erstlegal-hold, dann entfernen (R3c, R3a-2). - Selbst gelöschtes/ersetztes Bild mit „kein Aufdecken": Text t-F5 schon bei der ERSTEN Entfernung (R3c, Recht w, R-1). Bei der zweiten (R3a, Recht t-F5) ist
secondRemovalfreiwillig; ohne Wahl bzw. mitno_upload_bander Text „Ergebnis zu deinem früheren Profilbild" in der Verdachts-Fassung (art17Text: "earlier_picture_suspicion", App und Mail), mitupload_banSperre und Text (c). holdNotice gilt. - Hatte die Person das Bild selbst gelöscht oder ersetzt (Herkunft
self_delete/replace): stattdessenmoderation_earlier_picture(n-4b mit p-2: „selbst ersetzt oder gelöscht");art17Text: "earlier_picture". - Zweite Entfernung in 6 Monaten eines selbst gelöschten oder ersetzten Bildes (R3, Recht p-1):
secondRemovalist Pflicht, sonst422 second_removal_choice_required(nichts geändert).upload_ban: Upload-Sperre (uploadBanDays, Vorgabe 30, 1–365; Ende wie beiupload-ban), Text (c) in App und Mail, Auditmoderation_upload_banned;art17Text: "upload_blocked",uploadBlockedUntil.no_upload_ban: keine Sperre, Text n-4b. In jedem anderen Fall istsecondRemoval409 second_removal_choice_not_applicable; ein unbekannter Wert400 invalid_second_removal.upload_ban_dueentfällt. - Mit „Nachricht zurückhalten":
art17Mail: "withheld", und das „entfernt" an die meldenden Personen wird ebenfalls zurückgehalten (reporterOutcome: "withheld", R3, q-Fortsetzung 1). - Liegt die Version trotz Quarantäne noch öffentlich (abgebrochenes eigenes Löschen), wird sie ebenfalls entfernt.
- Scheitert die endgültige Löschung, wird sie vermerkt (
pendingPublicDeletionbzw.pendingQuarantineDeletion) und nachgeholt. - Antwort
{ ok, closedReports, reportersNotified, reporterOutcome, files, quarantine, art17Mail, art17Text, uploadBlockedUntil }. Auditmoderation_avatar_removed(mitohneAnsehen,secondRemoval). POST /api/admin/moderation/reports/:id/quarantine-avatarmit{ avatarKey, note? }. Füravatarundrights. Ist bzw. war „kein Aufdecken" gesetzt, geht es ohneavatarKey(gesperrt wird die gemeldete Version, solange sie aktuell ist; R3a: auch nach der Übergabe, weil das Ansehen dauerhaft gesperrt ist).- Verschiebt die aktuelle Version in die Quarantäne (Herkunft
admin) und leertavatarUrl/avatarUpdatedAt. Alle offenen Bildmeldungen dieser Version bleiben offen. - Die gemeldete Person erhält die Art.-17-Nachricht
moderation_avatar_hidden(n-1) in App und Mail, außer „Nachricht zurückhalten". Antwort{ quarantinedReports, files, art17Mail }. - Liegt die öffentliche Version danach noch da, wird sie vermerkt (
pendingPublicDeletion). - Fehler:
409 avatar_changed | already_quarantined | report_already_decided,502 quarantine_failed,503 quarantine_unavailable. Auditmoderation_avatar_quarantined. POST /api/admin/moderation/reports/:id/withdrawmit{ note? }. Füravatarundrights, wenn die gemeldete Version vor der Prüfung geändert oder gelöscht wurde.- Nur ohne Quarantäne-Kopie (
409 avatar_quarantined) und wenn die Version nicht mehr aktuell ist (409 avatar_still_current,409 no_picture_version). - Status
withdrawnfür alle offenen Bildmeldungen dieser Version; Text j-3 ohne Rechtsbehelf. Auditmoderation_report_withdrawn. POST /api/admin/moderation/reports/:id/dismissmit{ note? }.- Status
dismissed, die meldende Person erhältoutcome. Nur diese Meldung. - Die Ausblendung für die meldende Person bleibt (Recht). Eine Quarantäne-Kopie, die keine andere offene Meldung braucht, wird nach Admin-Quarantäne wiederhergestellt (
quarantine: "restored") — außer bei „kein Aufdecken", Verfahren oder Löschwunsch (R3) —, sonst endgültig gelöscht. - Audit
moderation_report_dismissed. POST /api/admin/moderation/reports/:id/close-namemit{ outcome: "action_taken"|"dismissed", note? }.- Gilt für
reason=name. Wir ändern keine Namen (Nachtrag h); bei einem Verstoß bittet der Admin den Verein, den Namen an der Quelle zu korrigieren. action_takenschließt alle offenen Namensmeldungen gegen die Person, Text (d).dismissednur diese, Text „nicht entfernt" ohne Bildsatz.- Die gemeldete Person erhält keine Nachricht. Audit
moderation_name_closed. POST /api/admin/moderation/reports/:id/close-behaviourmit{ outcome: "action_taken"|"dismissed", note? }.- Gilt für
harassment,otherundrights(Nachtrag k, vor allemrightsohne Bild), nur diese Meldung. Texte wie beiclose-name. Auditmoderation_behaviour_closed. - Liegt das Bild der Meldung in der Quarantäne (R3, D3-3/K3-2, Recht q-Fortsetzung 2):
action_takenist409 avatar_quarantined(entfernt wird überremove-avatarmitdecisionReason: rights);dismissedgilt als „kein Verstoß": Wiederherstellung wie bei dismiss (sofern nichts nach D3-1 entgegensteht) und „nicht entfernt" an die meldende Person. Antwort mitquarantine. POST /api/admin/moderation/reports/:id/legal-holdmit{ until, reason }(ISO-Datum in der Zukunft, höchstens 5 Jahre; Grund nur im Audit).DELETE …/legal-holdmit{ reason }. Auditsmoderation_legal_hold_set/_lifted. Beim Aufheben einer entschiedenen Meldung wird die Quarantäne-Kopie freigegeben (gelöscht, nie wiederhergestellt —legalHoldSetAtbleibt, R3), undlegalHoldIdentityfällt.POST /api/admin/moderation/reports/:id/withhold-messagesmit{ reason }setzt „Nachricht zurückhalten";DELETE …/withhold-messagesmit{ reason }löst es und holt die fälligen Nachrichten nach, wenn keine andere Meldung gegen die Person mehr zurückhält, dazu zurückgehaltene „entfernt"-Ergebnisse an meldende Personen. Beim Lösen entsteht an dieser Meldung die Aufgabe „Auskunft nachreichen" (exportFollowUpDueAt, R3, Recht p-3). Antwort{ messagesWithheld, delivered, mailOnly, skipped, mails, reporterOutcomes, exportFollowUp };deliveredzählt In-App-Zustellungen,mailOnlyreine Mails an ein Konto im Papierkorb (B1-M4-K-6). Grund nur im Audit (moderation_messages_withheld/_released).409 no_accountnach Kontolöschung. Keine Automatik: das Flag fällt nur von Hand oder mit der Kontolöschung der gemeldeten Person; ab 14 Tagen erinnert die Sammelmail täglich.POST /api/admin/moderation/reports/:id/export-follow-up-donemit{ party?: "reported"|"reporter", note? }(R3, R3a, Recht p-3, s-3) quittiert die Aufgabe „Auskunft zu Meldung X nachreichen" für die gemeldete bzw. die meldende Person (nachgereicht, bzw. während des Zurückhaltens wurde keine Auskunft erteilt). Ohnepartydie eine offene; sind beide offen400 party_required;400 invalid_party;409 no_export_follow_up, wenn keine offen ist. Antwort{ party, exportFollowUpDueSince, reporterFollowUpDueSince }. Auditmoderation_export_follow_up_done(mitparty).POST /api/admin/moderation/reports/:id/no-revealmit{ reason }setzt „kein Aufdecken" (Auditmoderation_reveal_blocked). Danach409 reveal_blockedfür jede Meldung derselben Bildversion, auch für den Admin.POST /api/admin/moderation/reports/:id/police-handovermit{ reason }(„Polizei hat übernommen") ist die einzige Aktion, die „kein Aufdecken" löst, für alle Meldungen derselben Version. Auditmoderation_police_handover. Die SpurnoRevealSetAtbleibt (R3): kein Wiederherstellen,remove-avatarweiter ohne Ansehen möglich. „Nachricht zurückhalten" bleibt gesetzt, bis die Polizei freigibt (Recht q-Fortsetzung 1).
Upload-Sperre¶
POST /api/admin/moderation/users/:userId/upload-banmit{ days: 30, reason, reasonText? }setztavatarUploadBlockedUntilauf den ersten freien Tag, 00:00 Uhr Berliner Zeit (Tag des Setzens = Tag 1; bei 30 Tagen der Beginn von Tag 31, über Sommer-/Winterzeit in Kalendertagen gerechnet). Auditmoderation_upload_banned.- Gibt es zwei entfernte Bildversionen in 6 Monaten (auch selbst gelöschte, später als Verstoß entschiedene, n-4b), erhält die Person die Begründung nach Art. 17 (Nachtrag c) in der App (
moderation_upload_blocked) und per Mail, mit{Datum der letzten Entfernung}aus den Daten. Antwortart17: sent|failed|no_email|withheld,text: "c". - Sonst mit
reasonText(Pflichtsatz des Admins, 10–300 Zeichen, sonst422 reason_text_required): Einzelfall-Text l-5,text: "l5". - Sonst
art17: manual_required(Rückfall; Begründung von Hand). - {Datum} ist in beiden Texten der erste freie Tag; der Satzanfang lautet nach Recht 08.10. „Wir haben dir das Hochladen von Profilbildern gesperrt; ab dem {Datum} kannst du wieder Bilder hochladen."
- Der Upload ist bis 23:59 des letzten gesperrten Tags
409, ab 00:00 des ersten freien Tags wieder möglich. DELETE /api/admin/moderation/users/:userId/upload-ban. Auditmoderation_upload_ban_lifted.- Keine Automatik.
priorRemovedCount6m >= 2ist nur ein Hinweis (Recht §3).
Fehler¶
404 report_not_found, 409 report_already_decided. Weitere Codes stehen bei den Routen.
Kontosperre¶
In v1 keine Funktion: Sperre von Hand im Keycloak-Admin, Begründungsmail von Hand nach der Vorlage von Recht (Vault Runbooks/Meldungen-bearbeiten.md, „Kontosperre"). Danach die Meldung mit action_taken schließen.
B2: Blockieren (nach #478)¶
Routen¶
POST /api/moderation/blocksmit{ "userId": "uuid" }oder{ "reportRef": "…" }(genau eines, B2). Rate-Limit 30 je Stunde und Person.201bzw. bei bestehendem Block200, jeweils{ "unblockRef": "…", "endedDuels": 0 }, dazuuserIdNUR, wenn die Anfrage sie selbst genannt hat — überreportRefnie (B2 R1, K4).endedDuelszählt die beendeten laufenden Duelle, nicht die zurückgezogenen Einladungen.- Fehler:
400 cannot_block_self,400 ambiguous_target(beides angegeben) und404 block_target_unavailable(unbekannt, kein gemeinsamer Verein, kein Ziel angegeben,reportRefungültig, abgelaufen oder für eine andere Person).503 block_unavailableohne SchlüsselTS_REPORTREF_HMAC_KEY— vor jeder Prüfung des Ziels, also neutral; sonst entstünde eine Blockierung, die sich nie aufheben ließe (B2 R2, D-5). - Aufräumen scheitert (B2 R2, K3 angepasst; B2 R3: je Vermerk und je Paar mit eigenem Fehlerfang, ein scheiternder Vermerk zählt
fehlversucheund rückt nach hinten; Aufheben der Blockierung löscht den Vermerk in derselben Transaktion, ein Duell läuft dann weiter): Die Blockierung steht, die Antwort ist trotzdem201/200. Der Server holt das Aufräumen selbst nach: über einen Aufräum-Vermerk (moderation_block_cleanup_pending) im täglichen Lauf UND sofort beim nächsten Duell-Zugriff zwischen den beiden (Duell-Liste, -Detail, Anlegen, Annehmen, Abschluss). Zusätzlich beendet der tägliche Lauf jedes laufende Duell zwischen Personen mit Blockierung, auch ohne Vermerk — ein laufendes Duell zwischen Blockierten bleibt nicht dauerhaft bestehen. Nur wenn auch der Vermerk scheitert:500 block_cleanup_failedmiterrorId. - Sichtbarkeit: gemeinsamer Verein wie beim Melden (eigene aktive Mitgliedschaften plus Token-Vereine; die Zielperson nur mit aktiver Mitgliedschaft) oder ein gültiges
reportRef. - Eine bestehende Blockierung in Gegenrichtung spielt keine Rolle (Recht o).
- Nebenwirkungen, erst NACH dem Anlegen der Zeile, bei jedem
POSTerneut (idempotent):- Offene Einladungen in beide Richtungen werden zurückgezogen, laufende Duelle zwischen beiden enden ohne Wertung — beides über den internen Abbruchpfad (Zeilensperren überall erst auf die Challenge, dann auf die Teilnahmen — auch Widerruf und Kontolöschung sperren ihre Duell-Challenges vor dem Löschen der Teilnahmen, B2 R3; Zustand und Zeitpunkt unter der Sperre; inaktiv, Teilnahmen
CANCELLED, MarkerabortedAtohne Kennung; bei einem Verklemmen ein Wiederholungsversuch, danach holt das Aufräumen nach — B2 R2, K-6; WächterduelLockOrder.guard.test.ts). „Laufend" heißt angenommen, noch kein Ergebnis und Fenster bei der Blockierung noch offen; ein vor der Blockierung abgelaufenes Duell behält seinen Ausgang. - Die andere Person bekommt je beendetem Duell denselben Push wie bei einem Plattform-Abbruch: „Duell beendet" / „Dieses Duell wurde ohne Wertung beendet." (Ereignis
aborted, Kategorie Duell), ohne Grund, ohne Urheber, keine Mail (B2 R1, K5). Offene, nie angenommene Einladungen werden zurückgezogen und bekommen DENSELBEN neutralen Push (B2 R3, R2-D-4; ersetzt D-7 „still, ohne Push“). Offene Einladungs-Aufgaben beider werden geschlossen. Ihre Duellansicht: das Duell verschwindet aus der Liste, die Detailseite antwortet404 duel_not_found— wie nach einem Plattform-Abbruch (B2 R1, D2). - Offene Tauschanfragen der Standaufsicht zwischen beiden (beide Richtungen, jeder gemeinsame Verein) werden still zurückgezogen (
cancelled), die offene Aufgabe geschlossen; Anfragen der Vereinsleitung bleiben (B2 R1, D1). - Wer so laufende Duelle beendet, wird im Moderations-Audit vermerkt (
moderation_block_duels_ended, nur intern) — je Duell in DERSELBEN Transaktion wie der Abbruch, auch beim Nachholen (B2 R2, D-2/K-1).
- Offene Einladungen in beide Richtungen werden zurückgezogen, laufende Duelle zwischen beiden enden ohne Wertung — beides über den internen Abbruchpfad (Zeilensperren überall erst auf die Challenge, dann auf die Teilnahmen — auch Widerruf und Kontolöschung sperren ihre Duell-Challenges vor dem Löschen der Teilnahmen, B2 R3; Zustand und Zeitpunkt unter der Sperre; inaktiv, Teilnahmen
DELETE /api/moderation/blocks/:unblockRefliefert204, Rate-Limit 30 je Stunde und Person. Die Zeile wird gelöscht.unblockRef(ausGETbzw. derPOST-Antwort) ist ein opakes Token wiereportRef: Zielkonto verschlüsselt, per HMAC an die betrachtende Person gebunden, eigener Verwendungszweck (einreportRefgilt hier nicht), läuft nach 7 Tagen ab (nie kürzer als 24 h). Kompakt, weil es im Pfad steht. Annahme: Konto-Kennungen sind UUIDs (36 Zeichen) — dann istunblockRefhöchstens 100 Zeichen lang; ab 43 Byte Kennung würde es für den Pfad zu lang (B2 R2, K-8). SchlüsselTS_REPORTREF_HMAC_KEY; ohne Schlüssel istunblockRefnullund das Aufheben nicht möglich (fail-closed, auf Prod Pflicht, D3).- Abgelaufene, unbekannte, gefälschte oder für eine andere betrachtende Person ausgestellte Kennung (auch eine Kontokennung): neutral
404 block_not_found, nie204— die Blockierung besteht weiter, die Clients laden die Liste neu. Die Antwort verrät nichts über die Person. Ein Pfadteil über 100 Zeichen erreicht die Route nicht (Fastify404 Not Found, ebenfalls ohne Auskunft). - Gültige Kennung, Blockierung schon aufgehoben: idempotent
204(B2 R1, K4, Entscheid der Koordination zu #496-D-7). - Sperrfrist (B2 R1, K1): Wird die Blockierung binnen 24 h nach dem Anlegen aufgehoben, bleiben Duelle (Anlegen, Annehmen, Gegnerliste) und Tauschanfragen zwischen beiden bis 24 h nach dem Anlegen gesperrt, mit denselben neutralen Antworten wie bei einer Blockierung. Bild und
blockedByMefolgen sofort dem Aufheben. Vermerkt als Moderations-Auditmoderation_block_lifted_early(Paar und Fristende), keine neue Tabelle. GET /api/moderation/blocksliefert{ "items": [{ "unblockRef": "…", "name": "…" | null, "nameKind": "name" | "unknown" | "otherClub", "blockedAt": "ISO" }] }, neueste zuerst, ohne Bild, ohne Kontokennung, nur die eigenen (B2 R1, K4).- Kein Rohtext vom Server (B2 R2): bei gemeinsamem aktiven Verein
name= der gespeicherte Kontoname ohne Nummer (#487),nameKind: "name"; ohne verwertbaren Namenname: null,nameKind: "unknown"; ohne gemeinsamen Vereinname: null,nameKind: "otherClub". Die Clients zeigen ihre übersetzten Platzhalter („Unbekannt" bzw. „Mitglied eines anderen Vereins"). Ersetzt das FeldotherClub. - Der Name kommt nie direkt aus E-Mail oder Benutzername; wie der gespeicherte Kontoname entsteht, regelt ein eigener PR („E-Mail als Name").
Moderations-Audits aus B2 (B2 R1, K1; B2 R2, D-1 bis D-4)¶
moderation_block_duels_ended(je beendetem Duell;detail:targetUserId,endedDuels= 1,challengeIds= [die Duell-Kennung], keine Punktwerte),moderation_block_lifted_early(Sperrfrist;detail:targetUserId,gesperrtBis) undmoderation_block_cleanup_pending(Aufräum-Vermerk;detail:targetUserId). Handelnde Person undentityId= die blockierende Person, ohne Name und Adresse.- Aufbewahrung (D-3, Entscheid Recht 07.10.), eine Konstante
BLOCK_AUDIT_RETENTION(userBlocks.ts): Sperrfrist-Vermerk gelöscht, sobaldgesperrtBisvorbei ist; „Duelle beendet" 6 Monate (Art. 6 Abs. 1 lit. f, Missbrauchserkennung), dann gelöscht; Aufräum-Vermerk bis erledigt. Bei Löschung des Kontos EINER der beiden Seiten werden alle B2-Audits des Paares GELÖSCHT, nicht nur anonymisiert. Verfahren (B2 R3, R2-D-2; Paarregel B2-R3-D-1, Recht 10.10.): Ein laufendeslegalHoldhält ein B2-Audit nur, wenn das ungeordnete Paar {blockierende, blockierte Person} genau das Paar {meldende, gemeldete Person} einer Meldung mit laufendemlegalHoldUntilist; ist eine Seite gelöscht, zählt die gesicherte ZuordnunglegalHoldIdentity.reporter/reported.userId. Ein solches Audit bleibt stehen — über die Fristen, die Pseudonymisierung und eine Kontolöschung hinaus, unverändert. B2-Audits einer der beiden Personen mit Dritten hält das Verfahren nicht; sie laufen regulär ab und fallen bei einer Kontolöschung (eine FunktiongehaltenePaare/blockAuditPaarfür alle drei Wege). Braucht eine Behörde weitere Einträge, setzt das der Admin von Hand fest (Runbook „Moderation-Admin-API"). Endet das Verfahren, gelten die Fristen sofort: der nächste tägliche Lauf löscht, was überfällig ist oder ein inzwischen gelöschtes Konto betrifft. Den Satz für die Datenschutzerklärung (Fassung 4) setzt Recht. - Pseudonymisierung (D-1): Erreicht ein B2-Audit die 6-Monats-Pseudonymisierung, fallen auch
actorId,actorName,actorEmailundactorRoles(die Handelnde ist dort die blockierende Person selbst). - Datenauskunft (D-4): Die blockierende Person bekommt ihre eigenen B2-Audits als
moderation.eigeneBlockierVermerke(art:duell_ohne_wertung_beendet|sperrfrist_nach_fruehem_aufheben|aufraeumen_ausstehend;personals Platzhalter; nur Datum; ohne Duell-Kennungen). Für die blockierte Person bleiben sie ausgeschlossen (Recht f). - Nie an Mitglieder ausgeliefert außer in der eigenen Datenauskunft.
reportRef (B2, D11)¶
- Opakes Token je fremder Zeile in
GET /api/leaderboard(alle Listen:bestTeiler,bestTotalRing,bestTotalRingTenths,bestAverage,mostTrainings, jede Kategorie) und inGET /api/members(jede Sicht, auchdetail=manage), FeldreportRef: string | null. nullfür die eigene Zeile, für Zeilen ohne App-Konto und ohne Schlüssel. In der Bestenliste gehört die Zeile dem Konto, dem auch ihr Bild gehört (Mitgliedschaft im Listenverein, sonst im Verein der Zeile; Spalte vor Einstellungen).- Inhalt: Zielkonto und Ablauf (24 h) verschlüsselt (AES-256-CTR, zufälliger IV), HMAC-SHA256 über Zweck, betrachtende Person, IV und Geheimtext. Keine Kennung und keine Nummer im Klartext; ein Token für eine andere betrachtende Person ist ungültig. Ein gültiges Token ist der Nachweis der Sichtbarkeit: Melden und Blockieren prüfen dann keinen gemeinsamen Verein.
POST /api/moderation/reportsnimmtreportRefalternativ zureportedUserId(genau eines; beides400 ambiguous_target).- Schlüssel:
TS_REPORTREF_HMAC_KEY(Key Vaultreportref-hmac-key), 32 Byte Zufall, eigener Wert. Der Melden-Release setzt den Schlüssel auf Prod voraus (Pflicht im Prod-Secret-Check vonbuild-prod-images.yml, B2 R1, D3). Fail-closed: fehlt er, ist er kürzer als 32 Byte oder ein nicht aufgelöster Key-Vault-Verweis, startet die App trotzdem; die Listen liefernreportRef: null, jedes Token ergibt das neutrale 404, die Wege über die Kontokennung laufen weiter. Beim Start einmal eine Warnung ohne Wert. Der Preflight warnt (kein Fehlschlag). Schlüsselwechsel macht alle alten Tokens ungültig.
Wirkung, beidseitig¶
POST /api/challenges/duelsund…/:id/respond(annehmen) liefern403 duel_opponent_unavailable— derselbe neutrale Code UND Status wie die Duell-Sperre für Kinder aus #478 (B2 R1, D4). Der Status steht an einer Stelle (DUEL_OPPONENT_UNAVAILABLE_STATUSinchallenges.ts) und gilt für Blockierung und die neutrale Antwort der Kinder-Sperre gleich; das Kind selbst bekommt weiter403 minor_duel_not_covered. Die Texte legt die Session „App-Kleinkram“ an (O5). Gilt auch in der Sperrfrist.- Reihenfolge: nach der Duell-Sperre für Kinder (
MINORS_DUEL_BLOCK, #478), in beiden Routen. Bei einem Kind bleibt die Antwort dort (403 minor_duel_not_coveredbzw.403 duel_opponent_unavailable) mit und ohne Blockierung gleich. POST /duelsprüft nach dem Anlegen erneut und zieht das Duell bei einer inzwischen entstandenen Blockierung still zurück (403, keine Nachricht). Die Annahme schreibt nur, solange das Duell aktiv ist.- Ablehnen bleibt möglich, ohne Nachricht an die andere Person.
- Der Abschluss eines Duells (Ergebnis, Teilnahmen, SP, Moment, Ergebnis-Nachricht): das Ergebnis wird nur auf ein aktives Duell geschrieben, Teilnahmen nur, solange sie nicht abgesagt sind, und SP, Moment und Nachricht entfallen, wenn das Duell inzwischen den Abbruch-Marker
abortedAtträgt (B2 R1, K2; B2 R2, K-2/K-7). Maßgeblich für die Schlussprüfung istabortedAt, nichtactive: neutralisierte Hüllen aus Widerruf und Kontolöschung sind inaktiv, ihr Ergebnis zählt weiter (Sieg incountWonDuelsInPeriod). - Vor dem ERSTEN Werten prüft der Abschluss die Blockierung (B2 R2, D-2): Bestand zwischen beiden eine Blockierung schon vor dem Fensterende, wird das Duell ohne Wertung beendet (nachgeholtes Aufräumen) statt gewertet. Eine Blockierung nach dem Fensterende ändert am Ausgang nichts. Wurde die Blockierung vor dem Fensterende wieder aufgehoben (und das Duell war noch nicht beendet), wird das Duell normal gewertet (B2 R3). Scheitert die Prüfung selbst, wird für diese Antwort nicht gewertet (kein 500); der nächste Zugriff bzw. Lauf versucht es erneut.
POST /api/challenges/:id/joinantwortet für Duelle seit #478 immer404 challenge_not_found; eine Blockierung ändert daran nichts.GET /api/challenges/duels/opponentsblendet die Person aus.nameist nur der Kontoname, ohne Sportpass-Nummer (B2 R1, N1) und ohne Rückfall auf die E-Mail oder den Keycloak-Benutzernamen. Konten ohne erkennbaren Namen erscheinen nicht, wie in der Bestenliste.- Die Tauschanfrage der Standaufsicht (
club.ts) liefert409 swap_target_unavailable(O3, neutral; KonstanteSWAP_TARGET_UNAVAILABLE_STATUSinclub.ts), aber nur, wenn die eingeteilte Person die Anfrage SELBST stellt; Anfragen der Vereinsleitung bleiben unberührt (Recht bestätigt 07.10., B2 R1, D1; die Anfrage trägt dannbyManager). Die Antwort auf eine solche Anfrage: Übernehmen409 swap_target_unavailable, Ablehnen ohne Nachricht an die anfragende Person. - Nach dem Speichern einer Anfrage wird die Blockierung erneut geprüft; eine inzwischen entstandene zieht die Anfrage still zurück (409, keine Nachricht). Die Schreibwege des Plans in
club.ts(Abgleich, Anfrage, Antwort, Zurücksetzen) laufen ganz unter der Zeilensperre der Plan-Zeile: sperren, lesen, abgleichen, ändern, schreiben in EINER Transaktion; ein zweiter Schreiber wartet und liest den neuen Stand. Lesezugriffe darin (die Blockprüfung) laufen über dieselbe Transaktion (R3-K-1). Status und Zuweisung kommen so immer aus demselben Stand, gleichzeitige Anfragen gehen nicht verloren (B2 R3, R2-K-1; ersetzt die R2-Überlagerung entschiedener Anfragen). Bekannte Ausnahme (R3-K-4, Nachliste): Die Kontolöschung bzw. der Vereins-Purge (userDataPurge.ts, Bereinigung der Plan-Zeilen) liest den Plan ohne diese Sperre und schreibt ihn unbedingt zurück; eine gleichzeitige Anfrage oder Antwort kann dort verloren gehen.
Wirkung nur für die blockierende Person¶
- In allen fremden Antworten ist
avatarUrl: null. Namen bleiben sichtbar. GET /api/challenges/season/member/:memberIdenthält zusätzlichblockedByMe: boolean. Das Profil bleibt erreichbar, aber ohne Bild.
Clients (Web, Mobile)¶
- Menü „Melden/Blockieren“
- Web: Duell-Detail, Duellkarten, Bestenliste, Saison-Rangliste, Challenge-Teilnehmer. Ausnahme: die Mitgliederverwaltung (
Members.tsx) bekommt kein Menü (Entscheid Koordination 07.10.) — sie sehen nur Verwaltende (canManageMembers), und dieselben Personen lassen sich über Bestenliste und Profil melden. Am Server ändert sich nichts:reportRefinGET /api/membersbleibt. - Mobile:
MemberProfileScreen, Duell-Detail, Ringe-Bestenliste. - Liste „Blockierte Personen“
- Web:
/settings, eigenes Panel ohne Sammel-Speichern. - Mobile: Profil-Tab → Verwaltung.
- Texte: wörtlich aus Recht §1 und Nachtrag 07.10., de/en. Fünf Gründe (Nachtrag g), Auswahltexte wie
MODERATION_REASON_LABELSinbackend/src/services/moderationTexte.ts. Den Satz zum Blockieren nach j-6 verwenden, nirgends „Möchtest du die Person nicht mehr sehen …". - Meldeknopf in Bestenliste und Mitgliederliste (
reportRef, D11): folgt mit B2 auf #487. In der Web-Mitgliederverwaltung kein Menü (Ausnahme oben);reportRefsteht trotzdem in jeder Sicht vonGET /api/members. - Melden trotz Blockierung (Recht 08.10., Neutralität): Melden prüft keine Blockierung und muss auch gehen, wenn die andere Person einen selbst blockiert hat; sonst verriete die Fehlermeldung die Blockierung. Blockieren geht in beide Richtungen unabhängig. B2 darf
POST /api/moderation/reportsnicht anUserBlockbinden. - Nach Melden oder Blockieren die betroffenen Daten neu laden. Lokal wird nichts ausgeblendet, der Server liefert das richtige Ergebnis.
Änderungen¶
| Datum | Änderung | Grund |
|---|---|---|
| 07.10. | angelegt | |
| 07.10. | In-App-Nachrichten: Der Typ (moderation_report_received, moderation_report_outcome, moderation_content_removed) steht in payload.kind. Die Spalte type trägt info bzw. warning. |
Die Spalte UserNotification.type kennt nur info\|success\|warning\|error und wird beim Lesen darauf normalisiert; ein eigener Wert ginge dort verloren. |
| 07.10. | avatarStillCurrent ist null bei reason != avatar. |
Nur Bildmeldungen haben eine Version; false hieße „Bild inzwischen geändert“. |
| 07.10. | Das Detail enthält zusätzlich outcomeNotifiedAt. |
Zeigt, ob die Art.-17-Mail zugestellt wurde (leer = Versand gescheitert, im Runbook nachzuholen). |
| 07.10. | close-name mit outcome: "changed" schließt alle offenen Namensmeldungen gegen die Person (wie remove-avatar je Bildversion); decisionReason ist dann Pflicht. dismissed betrifft nur die eine Meldung. |
Sonst bekäme die gemeldete Person je Meldung eine eigene Art.-17-Nachricht; der {Grund} steht im Pflichttext. |
| 07.10. | Weitere Admin-Fehlercodes: 400 invalid_decision_reason, 400 invalid_outcome, 400 report_reason_mismatch (z. B. remove-avatar auf eine Namensmeldung, Aufdecken bei reason != avatar), 400 invalid_days, 400 reason_required, 404 user_not_found. Schreibende Admin-Antworten: 200 { ok: true, … } mit Zählern (closedReports, reportersNotified, art17Mail: sent\|failed\|no_email). |
Der Vertrag nannte nur die gemeinsamen Codes. |
| 07.10. | upload-ban: days ist optional (Vorgabe 30, erlaubt 1–365), reason Pflicht (3–500 Zeichen, nur im Audit). |
Präzisierung; der Grund dient der Nachvollziehbarkeit wie bei den anderen Admin-Quittungen. |
| 07.10. | Idempotentes Melden bei neuer Bildversion: Ist die offene Bildmeldung auf eine alte Version gerichtet und hat die Person inzwischen ein neues Bild, wird die neue Version sofort ausgeblendet und die offene Meldung auf sie umgestellt (weiter 200, keine zweite Mail). |
Recht: das gemeldete Bild ist für die meldende Person sofort aus. Ohne Umstellung bliebe die Meldung nur mit 409 avatar_changed zu beantworten. |
| 07.10. | Gemeinsamer Verein: Für die meldende Person zählen aktive Mitgliedschaften und die Vereine aus dem Token, für die gemeldete Person nur aktive Mitgliedschaften (nie UserSetting.data). |
Identität nie allein aus clientseitig schreibbaren Daten. |
| 07.10. | Bild aus der Alt-Datei (/uploads/avatars/<sportpassId>.webp, Konto ohne avatarUrl): avatarKey ist dann dieser Pfad. Ausblenden, Aufdecken und Entfernen funktionieren gleich. |
Der Alt-Datei-Rückfall der Mitgliederliste zeigt sonst ein Bild, das nicht meldbar wäre. |
| 07.10. | Admin-Liste: offene Meldungen älteste zuerst, entschiedene neueste zuerst; 50 je Seite; unbekannter status gilt als open. Zusätzlicher Index (reporterUserId, status). |
Bearbeitungsreihenfolge; der Index trägt die Idempotenzprüfung und den Löschlauf der meldenden Person. |
| 07.10. | Abgeleitete Texte (Titel der drei In-App-Nachrichten, {Grund} für decisionReason=other, die Fassung für einen geänderten Anzeigenamen) stehen in backend/src/services/moderationTexte.ts. Bestätigung durch Recht offen. |
Recht §1 nennt nur vier Bild-Gründe und „Text entsprechend“ für Namen. |
| 07.10. | Datenauskunft: Ob andere Personen mein Bild ausgeblendet oder mich blockiert haben, ist nicht enthalten (allgemeiner Hinweis in bewussteAuslassungen). Meldungen gegen mich: nur Grund, Datum, Entscheidung (auch offene). Rechtliche Bestätigung offen. |
Grundsatz 2 (die blockierte Person erfährt nichts) gegen Art. 15 DSGVO; Abwägung nach Art. 15 Abs. 4. |
| 07.10. | Löschung der meldenden Person: Ihre Meldungen bleiben bis zur Frist ohne sie (SET NULL); Kennung und E-Mail werden aus dem Freitext entfernt, ein Klarname im Freitext bleibt (bekannter Rest, wie AdminAuditEvent.summary). |
Prüfverfahren und erneute Prüfung (Recht §2) brauchen die Meldung. |
| 07.10. (R1) | Quarantäne (privater Container, quarantine-avatar, Aufdecken per SAS, automatisches Sichern beim eigenen Löschen/Ersetzen, bei Kontolöschung und Vereins-Purge, endgültige Löschung nach der Entscheidung). Neue Felder quarantineBlob, pendingBlobDeletion, blobDeletionFailedAt. |
B1-D1, Recht-Nachtrag i und j-0: „sperren, sichern, nicht löschen"; Kontolöschung scharf geschaltet. |
| 07.10. (R1) | reportedUserId ist SetNull statt Cascade, mit Kennzeichen reportedAccountDeletedAt. Bei der Löschung fallen Kennung und E-Mail aus note, ein Alt-Datei-Pfad in avatarKey wird entfernt:<Hash>. Ersetzt die Zeile „Löschung der meldenden Person" nicht, sondern ergänzt sie um die gemeldete Person. |
B1-D1: die Meldung und ihr Beweisstück müssen die Kontolöschung überleben (Art. 17 Abs. 3 lit. e DSGVO, Recht j-0). |
| 07.10. (R1) | legalHoldUntil plus POST/DELETE …/legal-hold; Löschlauf, Audit-Pseudonymisierung und Quarantäne-Löschung überspringen gehaltene Meldungen; remove-avatar ohne Quarantäne bei Verfahren: 409 quarantine_required. |
B1-D2, Recht §2 „es sei denn, wir brauchen sie für ein laufendes Verfahren". |
| 07.10. (R1) | Datenauskunft: Meldungen über mich nur entschiedene (removed, dismissed, action_taken, withdrawn), alle Moderationsdaten nur mit Datum (Berlin), keine moderation_*-Audits im Verwaltungsprotokoll. Ersetzt „Meldungen gegen mich … (auch offene)". |
B1-D3, Recht-Nachtrag f. Rechtliche Bestätigung damit erteilt. |
| 07.10. (R1) | Moderations-Audits nach 6 Monaten pseudonymisiert (täglicher Lauf), Ausnahme legalHold. Audit-Detail nutzt reportReason für den Meldegrund und reason nur für Freitext. |
B1-D4, Recht j-1. |
| 07.10. (R1) | Neuer Status withdrawn mit Route POST …/withdraw (vom Admin ausgelöst, nicht automatisch). |
B1-D5, Recht j-3. Bewusst eine Admin-Aktion: ob eine Version „vor der Prüfung" verschwand, entscheidet ein Mensch, und eine Automatik beim Upload liefe gegen die Quarantäne. |
| 07.10. (R1) | Upload-Sperre sendet die Art.-17-Begründung (Nachtrag c) in App und Mail, wenn zwei entfernte Versionen in 6 Monaten vorliegen; sonst art17: manual_required. Neuer Nachrichtentyp moderation_upload_blocked. |
B1-D6. Der Text sagt „zweimal entfernt"; ohne zwei Entfernungen wäre er falsch. |
| 07.10. (R1) | Betreiber-Mail: Erfolg/Fehlschlag dauerhaft an der Meldung (operatorNotifiedAt, operatorMailFailedAt), täglicher Nachversand, tägliche Sammelmail „älter als 24 h" (nur Anzahl und IDs, einmal je Tag über einen Merker in Setting). |
B1-D7. |
| 07.10. (R1) | Ausblendungen einer Alt-Datei fallen, wenn die Datei nicht mehr existiert. | B1-D8/K6, Recht j-4. |
| 07.10. (R1) | Hinweis auf Rechtsbehelf an alle Ergebnisnachrichten an die meldende Person ausser withdrawn; Satz zum Blockieren nach j-6. |
B1-D9, Recht j-2 und j-6. |
| 07.10. (R1) | close-name nur noch action_taken \| dismissed, keine Art.-17-Nachricht bei Namen; NAME_REASONS und „Dein Anzeigename wurde geändert" entfernt. Ersetzt die Zeile zu close-name … changed. Neue Route close-behaviour für harassment und other. |
Recht-Nachtrag d und h. |
| 07.10. (R1) | decisionReason=other verlangt reasonText (10–300 Zeichen), sonst 422 reason_text_required; der abgeleitete {Grund} für other entfällt. |
Recht-Nachtrag e. |
| 07.10. (R1) | Meldegrund rights (Auswahltext wörtlich), Kennzeichen illegalContentNotice, Filter reason= in der Liste; blendet sofort aus wie avatar; remove-avatar und quarantine-avatar gelten für beide. |
Recht-Nachtrag g. |
| 07.10. (R1) | Abgeleitete Titel: „Deine Meldung ist erledigt" / „Your report has been closed" für withdrawn. Bestätigung durch Recht offen (wie die übrigen Titel). |
Nachtrag j-3 liefert nur den Text, keinen Titel. |
| 07.10. (R1) | Aufdecken liefert avatarKey (undurchsichtige Versionskennung v1:<Hash>), quarantined, expiresAt; remove-avatar und quarantine-avatar verlangen avatarKey, sonst 400 avatar_key_required bzw. 409 avatar_changed. |
B1-K1: entschieden wird nur über die gesehene Version. Ein Hash statt der Adresse, damit die Bildadresse nicht in Befehlszeilen landet. |
| 07.10. (R1) | Gescheiterte endgültige Löschung nach removed bzw. Quarantäne-Freigabe wird vermerkt und täglich nachgeholt; das Detail zeigt blobDeletion { pending, failedAt }. |
B1-K2. |
| 07.10. (R1) | Admin-Liste mit Keyset-Cursor (createdAt, id), 400 invalid_cursor. Ersetzt den Prisma-Cursor. |
B1-K3: eine entschiedene oder gelöschte Cursor-Zeile brach die Folgeseite ab. |
| 07.10. (R1) | 429-Meldung nennt das konfigurierte Fenster (Route oder global), die Restzeit steht nur in retryAfterSeconds. |
B1-K4: context.after ist bei @fastify/rate-limit die Restzeit. |
| 07.10. (R1) | 6-Monats-Grenze: Monat abziehen, Tag auf das Monatsende begrenzen. | B1-K5: setUTCMonth lief am 31.08. auf den 03.03. über und hätte zu früh gelöscht. |
| 07.10. (R1) | reportRef für Bestenliste und Mitgliederliste (D11) folgt mit B2 auf #487; B1 geht nur zusammen mit B2 live (D10). |
Entscheid Koordination 07.10. |
| 07.10. (R1) | Idempotentes Melden: eine offene Meldung, deren Bild schon in der Quarantäne liegt, zählt nicht als „gleiche offene Meldung"; eine erneute Bildmeldung gegen ein NEUES Bild legt dann eine neue Meldung an. | Die gesicherte Meldung darf nicht auf eine andere Version umgebogen werden (Beweisstück). |
| 08.10. (R2) | Wiederherstellen nach Admin-Quarantäne: neues Feld quarantineOrigin; ist die ganze Versionsgruppe ohne removed entschieden (dismiss, close-behaviour dismissed/action_taken), kommt das Bild unter neuer Kennung zurück, bedingt auf ein leeres Profil; die Ausblendungen ziehen um; In-App-Info (l). Antwortwert quarantine: "restored". Ersetzt „nach dismissed sofort endgültig gelöscht" für Herkunft admin. |
B1-K2-1/B1-D2-1, Recht-Nachtrag k und l. |
| 08.10. (R2) | Der tägliche Lauf löscht Ausblendungen einer Version nicht, solange sie in der Quarantäne liegt. | B1-K2-1: sonst wäre die Ausblendung nach der Wiederherstellung weg. |
| 08.10. (R2) | Art.-17-Nachricht zu Beginn der Admin-Quarantäne (moderation_avatar_hidden, n-1). Ersetzt „Keine Nachricht an irgendwen" bei quarantine-avatar. |
Recht-Nachtrag n-1. |
| 08.10. (R2) | „Nachricht zurückhalten": messagesWithheld, art17Pending, Routen POST/DELETE …/withhold-messages; gilt für alle vier Art.-17-Arten an die gemeldete Person, Nachholen beim Lösen (nur, was dann noch stimmt). Neuer Antwortwert withheld. |
Recht-Nachtrag n-1. |
| 08.10. (R2) | Getrennte Löschvermerke pendingPublicDeletion (Liste) und pendingQuarantineDeletion. Ersetzt pendingBlobDeletion; das Detail zeigt blobDeletion { pending, publicPending, quarantinePending, failedAt }. Nicht löschbare öffentliche Dateien werden vermerkt statt verschluckt. |
B1-K2-2, B1-K2-6. |
| 08.10. (R2) | Bild löschen und Bild hochladen sind fail-closed: scheitert das Sichern, 503 avatar_change_unavailable und nichts geändert. Ersetzt „Bildlöschung antwortet 500" und „beim Ersetzen bleibt die alte Version liegen". Der Aufräumer verwaister Fotos schont jede gemeldete Version. |
B1-K2-3/B1-D2-5, Nachtrag n (D2-5). |
| 08.10. (R2) | Beim eigenen Löschen wird erst verknüpft, dann gelöscht; beim Upload in derselben Transaktion wie das Umschalten. Bei Konto- und Vereinslöschung wird gesichert, bevor die Betreiber-Aufgabe zum Abo hinausgeht. | B1-K2-4/B1-D2-8, B1-K2-5. |
| 08.10. (R2) | Kontolöschung unter legalHold: legalHoldIdentity (Konto-ID, E-Mail, Sportpass-Nr.) für gemeldete und meldende Person, nur unter Hold; die Moderations-Audits der Person bleiben zuordenbar. Mit dem Ende des Holds (Aufheben bzw. täglicher Lauf) fällt die Zuordnung, und die Audits verlieren die Kennung. Die Datenauskunft enthält die Zuordnung nie. |
B1-D2-2, Nachtrag n-Fortsetzung 2. |
| 08.10. (R2) | Ein aktives Verfahren auf einer entschiedenen Meldung löst die Sicherung ebenfalls aus (Löschen, Ersetzen, Konto-, Vereinslöschung). | B1-D2-6, Nachtrag n (D2-6). |
| 08.10. (R2) | close-behaviour gilt auch für rights. Ersetzt „Gilt für harassment und other". |
B1-D2-3, Nachtrag k. |
| 08.10. (R2) | Liste und Detail tragen reportedUser.language und reasonTextLanguageHint. |
B1-D2-4, Nachtrag l-3. |
| 08.10. (R2) | „Kein Aufdecken": noReveal, Routen no-reveal und police-handover (nur diese löst), 409 reveal_blocked je Bildversion; quarantine-avatar ohne avatarKey, solange es gesetzt ist. SAS höchstens 5 Minuten von startsOn bis expiresOn, vom Speicher begrenzt, rscc=no-store. Zugriffsregel vor jedem put. Fehlender Container ist im Sweep kein Fehler. Auflage für das Admin-UI (nur anzeigen, kein Download, no-store). |
Nachtrag m und n-Fortsetzung 5. |
| 08.10. (R2) | Titel bei withdrawn: „Deine Meldung hat sich erledigt". Ersetzt den abgeleiteten Titel „Deine Meldung ist erledigt"; die übrigen Titel hat Recht bestätigt (n-6). |
Recht-Nachtrag l-4, n-6. |
| 08.10. (R2) | Upload-Sperre: Einzelfall-Text l-5 mit reasonText; manual_required bleibt Rückfall. Antwort mit text: "c"\|"l5"\|null. |
Recht-Nachtrag l-5. |
| 08.10. (R2) | n-4a: bei laufender Sperre „Ein neues Bild kannst du ab dem {Datum} wieder hochladen." statt „…jederzeit…" (de/en). n-4b: Verstoß bei selbst gelöschtem bzw. ersetztem Bild → moderation_earlier_picture; bei der zweiten Entfernung keine eigene Nachricht (art17Mail: "upload_ban_due"), der Fall zählt für die Sperre und Text (c). |
Recht-Nachtrag n-4a, n-4b. |
| 08.10. (R2) | avatarUploadBlockedUntil ist der erste freie Tag, 00:00 Uhr Berliner Zeit (Kalendertage über die Zeitumstellung). Satzanfang von (c) und l-5: „Wir haben dir das Hochladen von Profilbildern gesperrt; ab dem {Datum} kannst du wieder Bilder hochladen." bzw. „We have blocked you from uploading profile pictures; you can upload pictures again from {date}." Ersetzt „bis zum {Datum} gesperrt" und „jetzt + N × 24 h". |
Antwort Recht 08.10. (ui-texte-gegenlesen.md). |
| 08.10. (R2) | Pseudonymisierung leert entityId auch bei Meldungen (außer unter legalHold). Ersetzt „entityId bei Konten". |
B1-K2-9/B1-D2-7, Nachtrag n (D2-7). |
| 08.10. (R2) | Melden prüft keine Blockierung (für B2 festgehalten). | Antwort Recht 08.10., Neutralität. |
| 08.10. (R2) | Nachsenden der Betreiber-Mail mit Anspruchsfrist von 10 Minuten: ein eben beanspruchter oder eben gescheiterter Eintrag ist für andere Läufe nicht fällig. | Gegenprobe M11 (B1-K2-7): ein Lauf, der nach dem Anspruch eines anderen las, sandte doppelt. |
| 09.10. (R3) | Ersetzt die R2-Zeile „Wiederherstellen nach Admin-Quarantäne … (dismiss, close-behaviour dismissed/action_taken) …": Auslöser der Wiederherstellung ist nur „kein Verstoß" (dismiss, close-behaviour dismissed), nie action_taken. |
B1-D3-3/B1-K3-2: die R2-Zeile nannte action_taken fälschlich als Auslöser; Nachtrag k nennt nur dismiss bzw. withdraw. |
| 09.10. (R3) | Nie wiederherstellen, wenn an der Bildversion je „kein Aufdecken" oder ein Verfahren hing — auch nach police-handover, Aufheben oder Ablauf; geprüft unter dem Riegel bei dismiss, close-behaviour, Aufheben, Ablauf und im täglichen Lauf. Neue Spuren noRevealSetAt, legalHoldSetAt (nie zurückgesetzt). |
B1-D3-1/B1-K3-1, Recht q (D3-1): ein mutmaßliches Missbrauchsbild darf nie wieder öffentlich werden. |
| 09.10. (R3) | remove-avatar ohne avatarKey, wenn „kein Aufdecken" gesetzt ist oder war; Art.-17-Text „Verdacht … Behörden" (q-Fortsetzung 1). holdNotice hält auch das „entfernt" an die meldende Person zurück (reporterOutcomeWithheld), bis es gelöst ist bzw. die gemeldete Person ihr Konto löscht. |
B1-D3-1 zweite Hälfte; Recht q-Fortsetzung 1: nach der Übergabe ohne Ansehen schließen. |
| 09.10. (R3) | close-behaviour action_taken auf eine Meldung mit Quarantäne-Bild: 409 avatar_quarantined; Entfernen über remove-avatar mit rights. dismissed bleibt „kein Verstoß" mit Wiederherstellung. |
B1-D3-3/B1-K3-2, Recht q-Fortsetzung 2: nur die widersprüchliche Kombination wird abgelehnt. |
| 09.10. (R3) | Zweite Entfernung eines selbst gelöschten oder ersetzten Bildes: Pflichtwahl secondRemoval: upload_ban \| no_upload_ban, sonst 422 second_removal_choice_required. Ersetzt art17Mail: "upload_ban_due" (entfällt). Neue Antwortfelder art17Text, uploadBlockedUntil, reporterOutcome. |
B1-D3-4, Recht p-1: „keine Begründung" ist unzulässig. |
| 09.10. (R3) | n-4b gilt auch für ersetzte Bilder: „…selbst ersetzt oder gelöscht hast…" / „…since replaced or deleted yourself…". | B1-D3-5, Recht p-2. |
| 09.10. (R3) | Datenauskunft: eine Meldung mit „Nachricht zurückhalten" fehlt in „Meldungen über mich", bis es gelöst ist (nur diese). Beim Lösen entsteht die Aufgabe „Auskunft zu Meldung X nachreichen" (exportFollowUpDueAt): Admin-Liste task=export_follow_up, Zeile in der Sammelmail, Quittung export-follow-up-done. Ergänzt die R1-Zeile zur Datenauskunft. |
B1-D3-2, Recht p-3 (Art. 23 DSGVO, §§ 33, 34 BDSG): ein Flag steuert Nachricht und Auskunft. |
| 09.10. (R3) | „Kein Verstoß", aber inzwischen ein neues Bild: Text q-Fortsetzung 3 an die gemeldete Person (nur In-App, nur nach erhaltenem n-1, hiddenNoticeDeliveredAt), die meldende Person bekommt das normale „kein Verstoß". |
B1-D3-7: n-1 verspricht „sagen dir dann Bescheid". |
| 09.10. (R3) | „Nachricht zurückhalten": messagesWithheldAt, tägliche Erinnerung ab 14 Tagen in der Sammelmail, keine Automatik; das Flag fällt bei der Kontolöschung der gemeldeten Person (auch userDataPurge). holdNotice und legalHold getrennt: danach gilt die 6-Monats-Frist nur ohne laufendes Verfahren; ein Hold bleibt mit legalHoldIdentity. |
B1-D3-6/B1-K3-4, Recht q-4. |
| 09.10. (R3) | Löschwunsch während einer Admin-Quarantäne (DELETE /api/profile/avatar bei leerem Profil): Vermerk ownerDeletionRequestedAt, kein Wiederherstellen, kein „wieder sichtbar", Antwort weiter 200. |
B1-K3-3, Entscheid Koordination r. |
| 09.10. (R3) | Einheitliche Sperrreihenfolge: Riegel zuerst, dann Zeilen — auch im Upload (vorher Kontozeile zuerst) und in der Kontolöschung (purgeUserOwnedRows nimmt den Riegel als Erstes). Quelltext-Wächter moderationLockOrder.guard.test.ts. |
B1-K3-6: umgekehrte Reihenfolge führte zum Deadlock. |
| 09.10. (R3) | releaseQuarantine nimmt die Riegel der Kopie, BEVOR es die Meldungen liest und entscheidet; ein gleichzeitig gesetztes Verfahren hält die Kopie. Wiederherstellen und Löschen prüfen unter dem Riegel neu. |
B1-K3-11. |
| 09.10. (R3) | Fail-closed sichtbar: abgelehnte Bildänderungen (503) und zurückgestellte Konto-/Vereinslöschungen werden am Konto vermerkt (moderationAvatarChangeRejectedAt, moderationDeletionDeferredAt) und stehen in der Sammelmail mit Anzahl, „älteste offen seit" und Dauer — nur Konto-IDs, keine Bilder, keine Links. Die Sammelmail heißt jetzt „Moderation: tägliche Übersicht {Datum}". Ersetzt „Sammelmail „Offene Meldungen älter als 24 Stunden: N"" und „Sammelmail nur Anzahl und Meldungs-IDs". |
Recht r (Bedingung zu fail-closed): keine Löschfrist darf unbemerkt verstreichen. |
| 09.10. (R3a) | Datenauskunft unter „Nachricht zurückhalten": Bei der gemeldeten Person fehlen auch die übrigen Meldungen derselben Bildgruppe (gleiche Person und avatarKey; ohne Bild nur die eine). Bei der meldenden Person steht die eigene Meldung bis zum Lösen als in_review (Grund und Datum, ohne Ergebnis und entschiedenAm), auch solange ihr „entfernt" zurückgehalten ist. Aufgaben „Auskunft nachreichen" entstehen erst, wenn die Gruppe frei ist (das letzte Lösen), für die gemeldete Person an der gelösten Meldung und jeder entschiedenen der Gruppe, für die meldende Person (reporterFollowUpDueAt) an jeder entschiedenen, sobald ihr Ergebnis sichtbar ist — auch beim Zustellen und wenn das Zurückhalten mit der Kontolöschung der gemeldeten Person endet. Je Meldung und Seite höchstens eine Aufgabe, der erste Zeitpunkt bleibt. Für die meldende Person IMMER, ohne Prüfung einer Anforderung: die einzige Spur (User.dataExportRequestedAt) kennt nur den App-Export, hält nur den letzten Zeitpunkt und fällt bei einem gescheiterten Export zurück; Anfragen per Mail hinterlassen keine. Quittung mit party. Löscht die meldende Person ihr Konto, fällt ihre Aufgabe. Ergänzt die R3-Zeile zur Datenauskunft. |
Recht s-2, s-3 (Koordination, Nachtrag s). |
| 09.10. (R3a) | „Entfernt" an die meldende Person bleibt zurückgehalten, solange ein Verfahren an der Meldung oder gegen die gemeldete Person läuft — unabhängig von holdNotice; nach deren Kontolöschung über reporterOutcomeHeldBy (nur Meldungs-Kennungen). Endet das Verfahren, holt der tägliche Lauf die Zustellung nach. Ersetzt die Zustellung allein nach dem Ende von holdNotice (R3). |
D4-3, Recht t-F3. |
| 09.10. (R3a) | Pseudonymisierung der Moderations-Audits nimmt Meldungen mit holdNotice oder offener Aufgabe „Auskunft nachreichen" (beide Seiten) aus. | D4-5, § 34 Abs. 2 BDSG: der Grund der beschränkten Auskunft bleibt dokumentiert. |
| 09.10. (R3a) | Aufdecken ist dauerhaft gesperrt, wenn „kein Aufdecken" je gesetzt war: 409 reveal_blocked_permanently, auch nach police-handover. police-handover löst noReveal weiter für Abschluss und Nachrichten; quarantine-avatar geht ohne Kennung. |
Recht t-F4. |
| 09.10. (R3a) | Zweite Entfernung eines selbst gelöschten/ersetzten Bildes mit „kein Aufdecken": keine Pflichtwahl; ohne Wahl Text t-F5 (wörtlich, App und Mail, holdNotice gilt), mit upload_ban Sperrtext (c). Ergänzt die R3-Zeile zur Pflichtwahl. Bei der ERSTEN Entfernung eines selbst gelöschten Bildes mit „kein Aufdecken" bleibt es beim Text q-Fortsetzung 1 (Recht hat dafür nichts anderes festgelegt). |
Recht t-F5. |
| 09.10. (R3a) | Abschlusstext t-F6 bei „kein Verstoß" ohne Wiederherstellung wegen „kein Aufdecken"/Verfahren: wörtlich, ohne Grund, App und Mail, immer (auch ohne n-1, das Bild ist faktisch entfernt), nicht bei Löschwunsch; wartet auf holdNotice und jedes laufende Verfahren gegen die Person. Neuer Nachrichtentyp moderation_not_restored. Der Satz „du kannst jederzeit ein neues hochladen" bleibt auch bei laufender Upload-Sperre unverändert (an die Koordination gemeldet: der n-4a-Satz ist ein eigener Satz, hier steht ein Satzteil nach Semikolon — ohne Eingriff in den Wortlaut von Recht nicht sauber einsetzbar). |
Recht t-F6. |
| 09.10. (R3a) | remove-avatar unter „kein Aufdecken" ohne Quarantäne-Kopie: 409 quarantine_required — erst sperren, dann entfernen. |
K4-4: sonst ginge mit dem Entfernen die einzige Spur verloren. |
| 09.10. (R3a) | Ein gelungener Upload während einer Admin-Quarantäne setzt unter dem Riegel ownerReplacedAt: kein Wiederherstellen, kein „wieder sichtbar"; gelöscht nach der Entscheidung bzw. nach dem Verfahren; q-3 bleibt an „nach n-1" gebunden. Jeder Upload schreibt dafür in einer Transaktion unter dem Riegel. |
K4-2: Upload, dann Löschen, dann dismiss stellte das alte Bild wieder her. |
| 09.10. (R3b) | t-F6 bei laufender Upload-Sperre: der Satzteil „; du kannst jederzeit ein neues hochladen." wird zu „. Ein neues Bild kannst du ab dem {Datum} wieder hochladen." bzw. „; you can upload a new one at any time." zu „. You can upload a new picture again from {date}." (Wortlaut n-4a); {Datum} ist der erste freie Tag in Berliner Zeit, wie bei n-4a zum Zeitpunkt der Entscheidung. Der übrige Text bleibt. Ersetzt den Vermerk der R3a-Zeile, der Satz bleibe bei Sperre unverändert. | Recht (über die Koordination): Ersetzungsregel n-4a gilt auch für t-F6, Formulierung mit Punkt bestätigt. |
| 09.10. (R3c) | „Entfernt" an Meldende wird zurückgehalten, sobald ein Verfahren an der Meldung bzw. Gruppe oder gegen die Person läuft — auch OHNE holdNotice. Behebt die R3a-Zeile zu D4-3, die nur zusammen mit holdNotice griff. | B1-R3a-1 (hoch), Recht w. |
| 09.10. (R3c) | Unter laufendem Verfahren werden ALLE Ergebnistexte an Meldende zurückgehalten: „nicht entfernt" (dismiss, close-behaviour, close-name), „Maßnahmen ergriffen" (close-behaviour, close-name) und withdrawn (gleiche Begründung: „alle Ergebnisnachrichten"). Zustellung je Ausgang mit seinem Text; outcomeNotifiedAt erst bei Zustellung. holdNotice allein hält weiter nur „entfernt" zurück (q-Fortsetzung 1). |
Recht w, R-3. |
| 09.10. (R3c) | remove-avatar unter „kein Aufdecken" ohne aktives Verfahren an der Bildversion: 409 legal_hold_required. |
R3a-2, Recht w: die Kopie muss bis zum Ende des Verfahrens bleiben. |
| 09.10. (R3c) | Text t-F5 auch bei der ERSTEN Entfernung eines selbst gelöschten bzw. ersetzten Bildes unter „kein Aufdecken". Ersetzt den R3a-Vermerk, dort bleibe es beim Text q-Fortsetzung 1. | Recht w, R-1. |
| 09.10. (R3c) | Neuer Text R-4 (wörtlich aus Nachtrag w) bei „kein Verstoß", wenn die Person während der Admin-Quarantäne ein anderes Bild hochgeladen und wieder entfernt hat: nur nach n-1, App und Mail, Sperrsatz wie t-F6, zurückgehalten wie t-F6. Abgrenzung über die Vermerke: ownerReplacedAt (nur ein gelungener Upload setzt ihn) bei leerem Profil → R-4; nur ownerDeletionRequestedAt (Löschen bei leerem Profil, kein Upload) → keine Nachricht (Nachtrag l); Profil mit neuem Bild → q-3. Grund: Nur der Upload-Vermerk belegt, dass die Person selbst ein anderes Bild hochgeladen hat, wie es der Text sagt. |
Recht w, R-4. |
| 09.10. (R3c) | Audit moderation_police_handover: „Polizei hat übernommen; Aufdecken bleibt gesperrt". |
R3a-4: seit t-F4 war „Aufdecken wieder möglich" falsch. |
| 09.10. (R3d) | Neues Modell für Ergebnisse an Meldende (Strukturfix). (1) Ob zurückgehalten wird, entscheidet NUR die gespeicherte Liste reporterOutcomeHeldBy plus holdNotice, in genau EINER Funktion meldendenErgebnisGehalten (moderationQuarantine.ts); Entscheidung (ergebnisAnMeldende), Lösen und täglicher Lauf (zurueckgehalteneErgebnisseZustellen) und Datenauskunft („in Bearbeitung") nutzen sie; ein Wächter (moderationReporterHold.guard.test.ts) verbietet eine zweite Berechnung. (2) Bei jeder Entscheidung wird die Liste um ALLE gerade haltenden Verfahren ergänzt (an den Meldungen, an Kopie bzw. Bildversion, gegen die Person, solange es sie gibt; haltendeVerfahren), nie ersetzt. (3) legal-hold ergänzt die Liste noch zurückgehaltener Ergebnisse (diese Meldung, ihre Kopie, Meldungen gegen die Person). (4) Die Kontolöschung der gemeldeten Person ergänzt die Liste an ALLEN offenen und noch zurückgehaltenen Meldungen gegen sie, solange ein Verfahren gegen sie läuft. (5) Zustellung nie über die Person, nur über Liste und holdNotice. holdNotice person-weit hält nur ein noch nicht zugestelltes „entfernt"; holdNotice an Meldung bzw. Bildgruppe hält jeden Ausgang (auch „nicht entfernt"), damit Zustellung und Auskunft demselben Kriterium folgen. Ersetzt die R3a/R3c-Zeilen zu D4-3 und R-3 im Mechanismus (Wirkung unverändert, Lücken geschlossen). |
B1-R3c-1 (hoch, Fälle A–C), Anordnung der Koordination. |
| 09.10. (R3d) | outcomeNotifiedAt wird beim Zustellen nur gesetzt, wenn es die meldende Person noch gibt. |
R3c-3. |
| 09.10. (R3d) | 409 legal_hold_required nur, solange „kein Aufdecken" AKTIV ist; nach police-handover und Ende des Verfahrens braucht das Entfernen kein künstliches Verfahren. Ergänzt die R3c-Zeile zu R3a-2. |
R3c-4. |
| 07.10. (B2) | POST /api/moderation/blocks nimmt userId oder reportRef (genau eines); beides 400 ambiguous_target. Dasselbe für POST /api/moderation/reports (reportedUserId oder reportRef). |
Vorgabe B2 (D11); ein eigener Code, weil „beides" ein Fehler der Anfrage ist, kein unbekanntes Ziel. |
| 07.10. (B2) | reportRef: Format, Lieferstellen, Ablauf 24 h, Schlüssel TS_REPORTREF_HMAC_KEY (Key Vault reportref-hmac-key), fail-closed (siehe Abschnitt „reportRef"). Ein nicht aufgelöster Key-Vault-Verweis gilt als fehlender Schlüssel. Verschlüsselt (AES-256-CTR) und per HMAC an die betrachtende Person gebunden. |
Koordination 08.10.; „keine userId im Klartext" verlangt Verschlüsselung, ein HMAC allein trüge die Kennung offen. Ein fehlendes Key-Vault-Geheimnis stünde sonst wörtlich (und öffentlich bekannt) als Schlüssel im App-Setting. |
| 07.10. (B2) | Die blockierte Person sieht das beendete Duell nicht als „ohne Wertung beendet": es verschwindet aus ihrer Liste, die Detailseite antwortet 404 duel_not_found — derselbe Zustand wie nach dem Plattform-Abbruch. Abweichung, Entscheid offen (eigener Zustand in der API oder so lassen). |
Der vorhandene Abbruchpfad macht das Duell inaktiv; die API kennt keinen sichtbaren Zustand „beendet ohne Wertung". |
| 07.10. (B2) | POST /api/challenges/:id/join bekommt keine Prüfung: für Duelle seit #478 immer 404 challenge_not_found. Ersetzt „…sowie POST /api/challenges/:id/join liefern 409 duel_opponent_unavailable". |
Ein Beitritt zu einem Duell ist seit #478 gar nicht möglich; eine Blockier-Prüfung wäre unerreichbar. |
| 07.10. (B2) | Status bei Blockierung 409, die Duell-Sperre für Kinder aus #478 bleibt 403, mit Vorrang. |
Vertrag nennt 409; #478 nutzt 403. Mit Vorrang bleibt jede Antwort zu einem Kind von der Blockierung unberührt (kein Leck über Alter oder Blockierung). |
| 07.10. (B2) | „Laufend" = angenommen, Fenster offen, kein Ergebnis. Abgelaufene, noch nicht abgeschlossene Duelle bleiben; endedDuels zählt nur laufende. |
Eine Blockierung darf kein feststehendes Ergebnis auslöschen. |
| 07.10. (B2) | Tauschanfrage: Prüfung zwischen eingeteilter Person und Ziel (auch für die Vereinsleitung). Antwort auf eine ältere Anfrage: Übernehmen 409 swap_target_unavailable, Ablehnen ohne Nachricht. Duell-Absage zwischen blockierten Personen ohne Nachricht. |
„Push und Mail gehen nie an die blockierte Person"; die offene Aufgabe muss sich trotzdem erledigen lassen. |
| 07.10. (B2) | Blockliste: neueste zuerst, höchstens 500, name leer bei Nummer als Namen. Aufheben löscht die Zeile. Markierungen LOESCHJOURNAL an Entblocken, Ausblendungs- und Meldungs-Löschlauf (nur Kommentare, Teil B der Widerruf-Session). |
#487; Datenschutzerklärung „beim Aufheben gelöscht"; Vorgabe der Koordination. |
| 07.10. (B2) | Purge (beide Fremdschlüssel Cascade), Datenauskunft (eigene Blockierungen mit Platzhalter, fremde nicht) und seedFullUser aus B1 geprüft, unverändert. |
Keine neue Tabelle, kein neues Feld. |
| 07.10. (B2 R1) | K1: Moderations-Audit moderation_block_duels_ended; Rate-Limit 30/h auch für DELETE; Sperrfrist 24 h ab Anlegen bei frühem Aufheben (Audit moderation_block_lifted_early, keine Migration). Die Sperrfrist gilt für Duelle und Tauschanfragen gleich. |
Missbrauch „blockieren und sofort aufheben", um ein laufendes Duell ohne Wertung zu beenden. |
| 07.10. (B2 R1) | K2: Abbruch (Plattform und Blockierung) unter Zeilensperre mit frischem Zeitpunkt; der Abschluss schreibt Ergebnis und Teilnahmen nur bedingt auf active: true und bucht SP, Moment und Ergebnis-Nachricht nur danach. |
Wettlauf Abbruch/Abschluss. |
| 07.10. (B2 R1) | K3: Scheitert das Aufräumen, 500 block_cleanup_failed mit errorId; die Blockierung bleibt, eine Wiederholung holt nach. |
Kein verschluckter Fehler; die Wiederholung ist idempotent und nachweisbar, ein täglicher Lauf über alle Blockierungen wäre schwerer zu belegen. |
| 07.10. (B2 R1) | K4/D5: unblockRef (opakes, an die Person gebundenes Token, siehe unten) statt userId in Liste und DELETE; über reportRef keine userId in der POST-Antwort; Name als Projektion mit otherClub. Ersetzt DELETE …/:userId, items[].userId und den leeren Namen bei Nummer. |
Keine Kontokennung an die Clients, wo sie sie nicht schon kennen. Eine vereinsübergreifende Projektion nach Teil 3 gibt es im Code noch nicht; bis dahin der Platzhalter. |
| 07.10. (B2 R1) | K5/D2: Plattform- und Block-Abbruch schicken denselben Push „Duell beendet" / „Dieses Duell wurde ohne Wertung beendet." an die andere Person. Ersetzt „Kein Push und keine Mail an die andere Person" beim Beenden von Duellen und Einladungen sowie den Admin-Text „Ein Admin hat dieses Duell abgebrochen." | Entscheid der Koordination: die Person erfährt, dass das Duell endete, nicht warum. |
| 07.10. (B2 R1) | D1: Tauschanfrage nur gesperrt, wenn die eingeteilte Person selbst anfragt; Anfragen der Leitung unberührt; offene Anfragen zwischen beiden werden beim Blockieren zurückgezogen. Ersetzt „auch wenn die Vereinsleitung die Anfrage stellt". | Recht bestätigt (07.10.). |
| 07.10. (B2 R1) | D3: reportref-hmac-key in der Pflichtliste des Prod-Secret-Checks. Der Melden-Release setzt den Schlüssel auf Prod voraus. |
Ohne Schlüssel fehlt der Meldeknopf in Bestenliste und Mitgliederliste. |
| 07.10. (B2 R1) | D4: Blockierung an Duell-Wegen 403 statt 409. Tauschanfrage bleibt 409 swap_target_unavailable. Vorbehalt der Koordination aufgelöst (07.10.): Duell 403 gilt, Tausch unverändert 409. Je eine Konstante, Tests beziehen sich auf sie, ein Wächter verbietet fest eingetragene Status (duelOpponentStatus.guard.test.ts). Ersetzt die B2-Zeile „Status bei Blockierung 409". |
Duell: der Status darf Blockierung und Sperre für Kinder nicht unterscheiden. Tausch: dort gibt es keine zweite neutrale Ablehnung mit anderem Status; 409 steht neben swap_request_pending für „gerade nicht möglich", 403 ist dort für fehlende Rechte vergeben. |
| 07.10. (B2 R1) | N1: Gegnerliste ohne Sportpass-Nummer im Namen (auch nicht über den E-Mail-Rückfall). | #487. |
| 07.10. (B2 R1) | DELETE /api/moderation/blocks/:unblockRef: unblockRef ist ein an die betrachtende Person gebundenes Token (7 Tage, eigener Zweck, kompakt für den Pfad); abgelaufen, unbekannt, gefälscht oder fremd → 404 block_not_found; gültig und schon aufgehoben → 204. Ohne Schlüssel kein Aufheben. Ersetzt „204, idempotent" für jede Kennung und den Feldnamen blockRef (jetzt unblockRef). |
Entscheid der Koordination zu #496-D-7: Mit 204 meldete die App „aufgehoben", obwohl die Blockierung bei einer ungültigen Kennung weiter besteht. „Gültig, schon aufgehoben" lässt sich nur mit einem signierten Token unterscheiden; die Zeilen-Kennung allein gibt es nach dem Löschen nicht mehr. |
| 07.10. (B2 R1) | Web: kein Melden/Blockieren-Menü in der Mitgliederverwaltung (Members.tsx); reportRef in GET /api/members bleibt. |
Entscheid Koordination 07.10.: nur Verwaltende sehen die Seite, dieselben Personen sind über Bestenliste und Profil meldbar. |
| 07.10. (B2 R1) | N1: GET /api/challenges/duels/opponents ohne Rückfall auf den E-Mail-Anfang; ohne Kontonamen fehlt der Eintrag. Wächter duelOpponentsName.guard.test.ts. Ergänzt die N1-Zeile. |
Entscheid Koordination 07.10.: der E-Mail-Anfang ist ein Stück Adresse, das andere Mitglieder sehen — dieselbe Familie wie die Nummer. Die Bestenliste liefert für fehlende Namen keinen Platzhalter-Text (Zeilen ohne Namen fallen dort weg, hasIdentifiableName), die Clients lassen Gegner ohne Namen fallen; also derselbe Weg: leer = nicht gelistet. |
| 10.10. (B2 R2) | K-2/K-7: Schlussprüfung im Abschluss an abortedAt statt active; Teilnahmen nur noch an status ≠ CANCELLED gebunden. Neutralisierte Hüllen zählen wieder als Sieg. |
Rückschritt aus R1 (Hüllen aus Widerruf/Kontolöschung sind inaktiv). |
| 10.10. (B2 R2) | D-2/K-1: Audit moderation_block_duels_ended je Duell in der Transaktion des Abbruchs; der Abschluss prüft die Blockierung (vor dem Fensterende angelegt → ohne Wertung beenden). Ersetzt das eine Sammel-Audit nach dem Aufräumen. |
Das Audit ging verloren, wenn nach den Duellen etwas scheiterte. |
| 10.10. (B2 R2) | K3: Aufräumen holt der Server nach (Vermerk moderation_block_cleanup_pending, täglicher Lauf runBlockDaily, jeder Duell-Zugriff zwischen den beiden); POST antwortet trotzdem 201/200, 500 nur, wenn auch der Vermerk scheitert. Ersetzt „500, die Anfrage wiederholen holt nach". |
Entscheid Koordination: nach dem Neuladen zeigt die App „blockiert", ein erneutes Blockieren als Nachholweg fehlt. |
| 10.10. (B2 R2) | D-1/D-3/D-4: Pseudonymisierung leert bei B2-Audits auch die Handelnde; Aufbewahrung je Art (BLOCK_AUDIT_RETENTION), Löschung bei Kontolöschung einer Seite; eigene B2-Audits in der Auskunft der Blockierenden. Ersetzt „beim Löschen eines Kontos anonymisiert, nach 6 Monaten pseudonymisiert" und „nicht in der Datenauskunft der einen oder anderen Seite". |
Prüfer Daten/Recht; Recht 07.10. |
| 10.10. (B2 R2) | D-5: POST /blocks ohne Schlüssel 503 block_unavailable; Prod-Workflow prüft den NAMEN des App-Settings (nie den Wert); Admin-Weg im Runbook Moderation-Admin-API.md. |
Ohne Schlüssel ließ sich blockieren, aber nie aufheben. Eigener Code statt 404 block_target_unavailable: die Ablehnung liegt nicht am Ziel, und sie kommt vor jeder Zielprüfung, verrät also nichts. |
| 10.10. (B2 R2) | D-7: offene, nie angenommene Einladungen beim Blockieren still zurückgezogen, ohne Push. Ersetzt den K5-Teil „bzw. zurückgezogener Einladung". | Plan: still zurückziehen. |
| 10.10. (B2 R2) | GET /blocks: name: null plus nameKind statt Rohtext „Unbekannt"/„Mitglied eines anderen Vereins"; Feld otherClub entfällt. |
Entscheid Koordination: die Clients übersetzen. |
| 10.10. (B2 R2) | K-4: Plan der Standaufsicht unter Zeilensperre gespeichert, entschiedene Anfragen bleiben entschieden, Nachprüfung nach dem Speichern. K-6: Sperr-Reihenfolge Teilnahmen vor Challenge, ein Wiederholungsversuch bei Verklemmen. K-8: Annahme UUID-Kennungen für unblockRef; Vertragssätze zu Blockliste und Gegnerliste präzisiert. |
Prüfer Korrektheit. |
| 10.10. (B2 R3) | R2-K-1: Plan der Standaufsicht ganz unter der Zeilensperre geändert (aenderePlanUnterSperre). Ersetzt die R2-Überlagerung entschiedener Anfragen. |
Annahme ohne Umteilen und verlorene gleichzeitige Anfragen (reproduziert). |
| 10.10. (B2 R3) | R2-K-4: Sperr-Reihenfolge überall „Challenge, dann Teilnahmen", auch Widerruf und Kontolöschung (sperreDuellChallenges), Wächter. Ersetzt die R2-Zeile „Teilnahmen vor Challenge". |
Rückschritt aus R2. In der Kontolöschung nach dem Moderations-Riegel (B1-K3-6), davor kein Duell-Lock. |
| 10.10. (B2 R3) | R2-D-4: zurückgezogene Einladungen wieder mit dem neutralen Push. Ersetzt D-7. | Entscheid Koordination, Recht zugestimmt. |
| 10.10. (B2 R3) | R2-D-2: laufendes Verfahren hält B2-Audits (Fristen, Pseudonymisierung, Kontolöschung); nach dem Ende sofort die normalen Fristen, auch für gelöschte Konten. | Entscheid Koordination, Bedingung Recht. |
| 10.10. (B2 R3) | R2-D-3: Aufheben löscht den Aufräum-Vermerk; R2-D-1/K-5: Fehler je Vermerk/Paar/Duell abgefangen; R2-K-2/K-3: gewertete und abgebrochene Duelle ungesperrt ausgefiltert, zweite Sicherung in SQL gegen UserBlock. Aufhebung vor dem Fensterende → normal gewertet. |
Prüfer R2. |
| 10.10. (B2 R3, Abschluss) | B2-R3-D-1: Verfahren hält B2-Audits nur am selben Paar wie die Meldung (ungeordnet; gelöschte Seite über legalHoldIdentity), in Fristen, Pseudonymisierung und Kontolöschung. Ersetzt die R2-D-2-Regel „eine der beiden Seiten". |
Prüfer R3, Recht bestätigt 10.10.: die breite Regel hielt Einträge mit Dritten ohne Anlass. Erweiterung auf Einträge der gemeldeten Person mit Dritten entscheidet Recht. |
| 10.10. (B2 R3, Abschluss) | Täglicher Lauf: runModerationDaily und runBlockDaily laufen als getrennte Schritte mit eigenem Fehlerfang (taeglicheModerationsLaeufe). |
Ein Fehler im Moderationslauf hielt sonst das Aufräumen der Blockierungen an. |
| 10.10. (B2 R3, Abschluss) | R3-K-1: Blockprüfung in Anfrage und Antwort der Standaufsicht über die Transaktion der Plan-Sperre. R3-K-4: Satz „jeder Schreibweg unter der Zeilensperre“ korrigiert, Kontolöschung/Vereins-Purge als bekannte Ausnahme (Nachliste, Code unverändert). | Prüfer R3 (Korrektheit): mit connection_limit=2 hielt die Sperre die einzige zweite Verbindung fest, nach 5 s 500 P2028. |