Skip to content

Teams Pipeline

Downloads team rosters from Sportlink, creates team posts in Rondo Club, and links members to teams via dated work history.

Runs weekly on Sunday at 6:00 AM (Amsterdam time).

Terminal window
scripts/sync.sh teams # Production (with locking + email report)
node pipelines/sync-teams.js --verbose # Direct execution (verbose)
pipelines/sync-teams.js
├── Step 1: steps/download-teams-from-sportlink.js → data/rondo-sync.sqlite
├── Step 2: steps/submit-rondo-club-teams.js → Rondo Club API (teams)
├── Step 3: steps/submit-rondo-club-work-history.js → Rondo Club API (quick work_history)
└── Step 4: steps/submit-rondo-club-player-history.js → Sportlink details + Rondo Club API (dated work_history)

Script: steps/download-teams-from-sportlink.js Function: runTeamDownload({ logger, verbose })

  1. Launches headless Chromium via Playwright
  2. Logs into Sportlink Club
  3. Calls Sportlink API to fetch team data:
    • UnionTeams (KNVB-assigned teams, preferred source)
    • ClubTeams (club-assigned teams, fallback)
  4. For each team, fetches the team roster:
    • Players with their roles (Speler, Keeper, etc.)
    • Staff members with their roles (Trainer, Leider, etc.)
  5. Stores team metadata in data/rondo-sync.sqliterondo_club_teams:
    • team_name, sportlink_id, game_activity, gender, player_count, staff_count
  6. Stores team membership in data/rondo-sync.sqlitesportlink_team_members:
    • sportlink_team_id, sportlink_person_id, role_description

Output: { success, teamCount, memberCount }

Rate limiting: 500ms-1.5s random jitter between member scrapes.

Script: steps/submit-rondo-club-teams.js Function: runSync({ logger, verbose, force, currentSportlinkIds })

  1. Reads all teams from data/rondo-sync.sqliterondo_club_teams
  2. For each team where source_hash != last_synced_hash:
    • No rondo_club_id: POST /wp/v2/teams (create new team)
    • Has rondo_club_id: PUT /wp/v2/teams/{rondo_club_id} (update existing)
  3. Stores returned WordPress post ID as rondo_club_id
  4. Updates last_synced_hash on success
  5. Detects orphan teams (teams in Rondo Club DB but not in current Sportlink download) and optionally removes them

Output: { total, synced, created, updated, skipped, deleted, errors }

Team renames: Uses sportlink_id as the conflict key, so renamed teams update the existing WordPress post instead of creating duplicates.

Script: steps/submit-rondo-club-work-history.js Function: runSync({ logger, verbose, force })

  1. Reads team membership from sportlink_team_members joined with rondo_club_teams and rondo_club_members
  2. Compares current team assignments against rondo_club_work_history table
  3. For each member with changes:
    • Fetches current work_history ACF repeater from Rondo Club
    • Adds new team assignments (creates new rows in the repeater)
    • Ends removed assignments (sets is_current: false, end_date: today)
    • Only modifies sync-created entries (manual entries are preserved)
  4. Sends PUT /wp/v2/people/{rondo_club_id} with updated work_history repeater
  5. Skips members without a rondo_club_id

Output: { total, synced, created, ended, skipped, errors }

Important: The work history sync only touches entries it previously created (tracked via rondo_club_work_history table). Manually added work history entries in Rondo Club are left untouched.

Section titled “Step 4: Enrich Work History with Sportlink Dates”

Script: steps/submit-rondo-club-player-history.js Function: runSync({ logger, verbose, force })

The fast team-roster response used in step 1 does not expose the start and end dates of a team relation. This step immediately follows the quick work-history sync and:

  1. Compares each member’s current team signature with the last successful detail sync
  2. Skips members whose team data is unchanged
  3. Fetches changed memberships from Sportlink’s member-details endpoint
  4. Maps RelationStart and RelationEnd to the corresponding work-history row
  5. Reconciles the existing row instead of creating a dated duplicate

Before mapping, lib/team-membership-periods.js interprets an empty RelationEnd using SeasonDescription. A recognized closed season such as 2025/'26 gets June 30, 2026 as its inferred end date. The cutoff follows Europe/Amsterdam. Explicit source end dates remain authoritative; current/future seasons, unrecognized seasons, and contradictory start dates are left unchanged. A continuing current-season copy with the same team, role, and start date takes precedence over an inferred historical end. Multiple historical copies retain the latest season. The normal reconciliation closes the existing current row and keeps unrelated and already ended history.

The standalone monthly player-history run remains a safety net, but new team assignments no longer wait for it before their dates appear in Rondo Club.

Output: { total, downloaded, synced, created, reconciled, skippedUnchanged, skippedQuarantined, errors }

Rondo Club FieldSportlink SourceNotes
titleTeamName / NamePost title
acf.publicteamidPublicTeamIdSportlink team identifier
acf.activiteitGameActivityDescription”Veld” or “Zaal”
acf.genderGenderMannen→male, Vrouwen→female, Gemengd→skipped

The ACF work_history is a repeater field on person posts:

Repeater FieldSourceNotes
teamrondo_club_teams.rondo_club_idWordPress post ID of the team
job_titlerole_description or fallback”Speler”, “Keeper”, “Trainer”, “Staflid”
is_currentComputedtrue if currently on team
start_dateRelationStart from member detailsThe quick roster step may temporarily use today; the detail pass immediately replaces it with Sportlink’s date
end_dateRelationEnd from member detailsEmpty while current
DatabaseTableUsage
rondo-sync.sqliterondo_club_teamsTeam → WordPress ID mapping + metadata
rondo-sync.sqlitesportlink_team_membersRaw team roster data from Sportlink
rondo-sync.sqliterondo_club_work_historyTracks which work_history entries sync created
rondo-sync.sqliterondo_club_membersKNVB ID → Rondo Club ID lookup (for work history)
FlagEffect
--verboseDetailed per-team/per-member logging
--forceSkip change detection, sync all teams and fetch player history for every member
  • Team download failure doesn’t prevent team sync (uses cached data)
  • Individual team sync failures don’t stop the pipeline
  • Work history sync skips members not yet in Rondo Club (counted as skipped)
  • Player-history detail sync skips unchanged members and reports quarantined or failed Sportlink detail records
  • All errors collected in summary report
FilePurpose
pipelines/sync-teams.jsPipeline orchestrator
steps/download-teams-from-sportlink.jsSportlink team scraping (Playwright)
steps/submit-rondo-club-teams.jsRondo Club team API sync
steps/submit-rondo-club-work-history.jsRondo Club work history API sync
steps/submit-rondo-club-player-history.jsSportlink relation-date enrichment and reconciliation
steps/prepare-rondo-club-teams.jsTeam data preparation
lib/rondo-club-db.jsSQLite operations
lib/rondo-club-client.jsRondo Club HTTP client
lib/sportlink-login.jsSportlink authentication