Skip to content

Teams Pipeline

Downloads team rosters from Sportlink, creates team posts in Rondo Club, and links members to teams via 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 (person 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.

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_dateComputedToday for new assignments, empty for backfill
end_dateComputedEmpty if current, today when removed
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 work history
  • 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)
  • 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/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