Skip to content

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. Migration 20260713141000_add_clubdata_starterlist_master.
  • Ingest-Route POST /api/edge/starterlists/sync (edgeAuth, per-Club-Token): upsert der Header, idempotent pro starterlistenId, reaktiviert soft-gelöschte Header. upsertStarterlistMasterRows() in starterlistUtils.ts.
  • Katalog-Merge: loadStarterlistCatalogRows(prisma, clubIds, includeMaster) hängt bei includeMaster=true per UNION NUR die Master-Header an, die in den Scores weder per starterlistenId NOCH per normalisiertem Namen vorkommen (NOT EXISTS) — als sessionCount 0. So kann keine Liste doppelt erscheinen (auch nicht bei Legacy-Score-Zeilen mit starterlistenId = NULL). Bei leerer Master-Tabelle identisch zum Alt-Verhalten.
  • Kill-Switch: Env TS_STARTERLIST_MASTER_ENABLED (default false). Erst true setzen, 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:

  1. 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.
  2. Worker: periodisch (z. B. gleicher Takt wie der Consent-Pull) die aktuelle Header-Menge an POST /api/edge/starterlists/sync schicken, mit dem vorhandenen per-Club Edge-Token (tse_…). ingestTraceId aus dem Operation-Marker-Schema stempeln, falls vorhanden.
  3. Compose/.env: kein neues Secret nötig (Edge-Token existiert bereits aus 1B/1C). Ggf. ein Intervall-Env TS_CONNECT_STARTERLIST_SYNC_INTERVAL_S.
  4. Soft-Delete (später): Wenn eine Liste in der Range-DB verschwindet, kann ein full-Modus (alle IDs eines Clubs mitsenden) serverseitig die fehlenden auf sourceDeleted=true setzen. MVP: nur upsert, kein Delete.

Rollout-Reihenfolge (wie 1B/1C)

  1. App-Seite deployen (Tabelle + Route + Flag aus). ✅ dieser Commit.
  2. Connector-Worker bauen + auf einer echten Box gegen Staging E2E testen: Header pushen → ClubDataStarterlist gefüllt prüfen.
  3. TS_STARTERLIST_MASTER_ENABLED=true auf Staging → Katalog zeigt Header-only- Listen mit 0 Sitzungen → Vorstand kann vorab zuordnen.
  4. 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.