F2 — Starterlisten-Stammdaten-Kanal (Spec)¶
Ziel: Eine Starterliste erscheint in der Vereinseinstellungen-Zuordnung, sobald sie in der Vereinssoftware angelegt ist — unabhängig davon, ob ein (eingewilligter) Schütze darin schon geschossen hat. Der Vorstand kann dann vorab klassifizieren; kommen später Scores, sind sie bereits zugeordnet.
Warum ein eigener Kanal¶
Der Starterlisten-Katalog wird heute rein aus ClubDataScoreSheet abgeleitet
(GROUP BY starterliste in loadStarterlistCatalogRows). Diese Score-Zeilen
sind personenbezogen und laufen durch den ConsentGate (1C). Folge: eine Liste
ohne eingewilligte, geschossene Teilnehmer existiert für die App nicht.
Der Header einer Starterliste (ID + Name + Disziplin) ist kein
personenbezogenes Datum → braucht keinen Consent und kann als eigener
Stammdaten-Kanal fließen (analog zum Roster ClubDataShooter).
App-Seite — FERTIG (dieser Commit)¶
- Tabelle
ClubDataStarterlist(PK(clubId, starterlistenId)): nur Header —name,disziplin,disziplinId,lastSeenAt, Source-Provenance,sourceDeleted. Keine Teilnehmer. Migration20260713141000_add_clubdata_starterlist_master. - Ingest-Route
POST /api/edge/starterlists/sync(edgeAuth, per-Club-Token): upsert der Header, idempotent prostarterlistenId, reaktiviert soft-gelöschte Header.upsertStarterlistMasterRows()instarterlistUtils.ts. - Katalog-Merge:
loadStarterlistCatalogRows(prisma, clubIds, includeMaster)hängt beiincludeMaster=trueperUNIONNUR die Master-Header an, die in den Scores weder perstarterlistenIdNOCH per normalisiertem Namen vorkommen (NOT EXISTS) — alssessionCount 0. So kann keine Liste doppelt erscheinen (auch nicht bei Legacy-Score-Zeilen mitstarterlistenId = NULL). Bei leerer Master-Tabelle identisch zum Alt-Verhalten. - Kill-Switch: Env
TS_STARTERLIST_MASTER_ENABLED(defaultfalse). Ersttruesetzen, wenn die Edge Header pusht.
Request-Contract¶
POST /api/edge/starterlists/sync
Authorization: Bearer tse_<per-club-token>
Content-Type: application/json
{
"sourceConnectorName": "ts-connect-<host>", // optional
"ingestTraceId": "<uuid>", // optional, Trace-Korrelation
"starterlists": [
{ "starterlistenId": 4711, "name": "Frühjahrs-Cup 2026", "disziplin": "LG", "disziplinId": 10 },
{ "starterlistenId": 4712, "name": "Vereinsmeisterschaft LP", "disziplin": "LP" }
]
}
Antwort: { clubId, syncedAt, received, upserted }. Max. 5000 Header/Request
(sonst 413). Grösse per Batching/Pagination auf der Edge lösen.
Connector-Seite — OFFEN (ts-connect Repo, separat)¶
Analog zu consent_sync.py (1B) einen Push-Worker ergänzen:
- Quelle: Starterlisten-Header aus der Range-DB lesen (Meyton/Disag kennt
die Startliste vor dem Schießen). Nur
(starterlistenId, name, disziplin, disziplinId)— nicht die Teilnehmer. - Worker: periodisch (z. B. gleicher Takt wie der Consent-Pull) die aktuelle
Header-Menge an
POST /api/edge/starterlists/syncschicken, mit dem vorhandenen per-Club Edge-Token (tse_…).ingestTraceIdaus dem Operation-Marker-Schema stempeln, falls vorhanden. - Compose/.env: kein neues Secret nötig (Edge-Token existiert bereits aus
1B/1C). Ggf. ein Intervall-Env
TS_CONNECT_STARTERLIST_SYNC_INTERVAL_S. - Soft-Delete (später): Wenn eine Liste in der Range-DB verschwindet, kann
ein
full-Modus (alle IDs eines Clubs mitsenden) serverseitig die fehlenden aufsourceDeleted=truesetzen. MVP: nur upsert, kein Delete.
Rollout-Reihenfolge (wie 1B/1C)¶
- App-Seite deployen (Tabelle + Route + Flag aus). ✅ dieser Commit.
- Connector-Worker bauen + auf einer echten Box gegen Staging E2E testen:
Header pushen →
ClubDataStarterlistgefüllt prüfen. TS_STARTERLIST_MASTER_ENABLED=trueauf Staging → Katalog zeigt Header-only- Listen mit 0 Sitzungen → Vorstand kann vorab zuordnen.- Nach Abnahme auf Prod scharfschalten.
DSGVO¶
Nur Listen-Header (Organisationsdatum) fliessen. Teilnahme/Score bleibt
consent-gated in ClubDataScoreSheet. Einziger Randfall: Vereine, die eine
Liste nach einer einzelnen Person benennen — dann könnte der Name
personenbezogen sein. In der Praxis selten; falls relevant, Namens-Whitelist
oder Hashing erwägen.