Teams Pipeline
Downloads team rosters from Sportlink, creates team posts in Rondo Club, and links members to teams via dated work history.
Schedule
Section titled “Schedule”Runs weekly on Sunday at 6:00 AM (Amsterdam time).
scripts/sync.sh teams # Production (with locking + email report)node pipelines/sync-teams.js --verbose # Direct execution (verbose)Pipeline Flow
Section titled “Pipeline Flow”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)Step-by-Step Details
Section titled “Step-by-Step Details”Step 1: Download Teams from Sportlink
Section titled “Step 1: Download Teams from Sportlink”Script: steps/download-teams-from-sportlink.js
Function: runTeamDownload({ logger, verbose })
- Launches headless Chromium via Playwright
- Logs into Sportlink Club
- Calls Sportlink API to fetch team data:
UnionTeams(KNVB-assigned teams, preferred source)ClubTeams(club-assigned teams, fallback)
- For each team, fetches the team roster:
- Players with their roles (Speler, Keeper, etc.)
- Staff members with their roles (Trainer, Leider, etc.)
- Stores team metadata in
data/rondo-sync.sqlite→rondo_club_teams:team_name,sportlink_id,game_activity,gender,player_count,staff_count
- Stores team membership in
data/rondo-sync.sqlite→sportlink_team_members:sportlink_team_id,sportlink_person_id,role_description
Output: { success, teamCount, memberCount }
Rate limiting: 500ms-1.5s random jitter between member scrapes.
Step 2: Sync Teams to Rondo Club
Section titled “Step 2: Sync Teams to Rondo Club”Script: steps/submit-rondo-club-teams.js
Function: runSync({ logger, verbose, force, currentSportlinkIds })
- Reads all teams from
data/rondo-sync.sqlite→rondo_club_teams - 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)
- No
- Stores returned WordPress post ID as
rondo_club_id - Updates
last_synced_hashon success - 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.
Step 3: Sync Work History
Section titled “Step 3: Sync Work History”Script: steps/submit-rondo-club-work-history.js
Function: runSync({ logger, verbose, force })
- Reads team membership from
sportlink_team_membersjoined withrondo_club_teamsandrondo_club_members - Compares current team assignments against
rondo_club_work_historytable - For each member with changes:
- Fetches current
work_historyACF 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)
- Fetches current
- Sends
PUT /wp/v2/people/{rondo_club_id}with updatedwork_historyrepeater - 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.
Step 4: Enrich Work History with Sportlink Dates
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:
- Compares each member’s current team signature with the last successful detail sync
- Skips members whose team data is unchanged
- Fetches changed memberships from Sportlink’s member-details endpoint
- Maps
RelationStartandRelationEndto the corresponding work-history row - 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 }
Field Mappings
Section titled “Field Mappings”Sportlink → Rondo Club Teams
Section titled “Sportlink → Rondo Club Teams”| Rondo Club Field | Sportlink Source | Notes |
|---|---|---|
title | TeamName / Name | Post title |
acf.publicteamid | PublicTeamId | Sportlink team identifier |
acf.activiteit | GameActivityDescription | ”Veld” or “Zaal” |
acf.gender | Gender | Mannen→male, Vrouwen→female, Gemengd→skipped |
Sportlink → Rondo Club Work History
Section titled “Sportlink → Rondo Club Work History”The ACF work_history is a repeater field on person posts:
| Repeater Field | Source | Notes |
|---|---|---|
team | rondo_club_teams.rondo_club_id | WordPress post ID of the team |
job_title | role_description or fallback | ”Speler”, “Keeper”, “Trainer”, “Staflid” |
is_current | Computed | true if currently on team |
start_date | RelationStart from member details | The quick roster step may temporarily use today; the detail pass immediately replaces it with Sportlink’s date |
end_date | RelationEnd from member details | Empty while current |
Database Tables Used
Section titled “Database Tables Used”| Database | Table | Usage |
|---|---|---|
rondo-sync.sqlite | rondo_club_teams | Team → WordPress ID mapping + metadata |
rondo-sync.sqlite | sportlink_team_members | Raw team roster data from Sportlink |
rondo-sync.sqlite | rondo_club_work_history | Tracks which work_history entries sync created |
rondo-sync.sqlite | rondo_club_members | KNVB ID → Rondo Club ID lookup (for work history) |
CLI Flags
Section titled “CLI Flags”| Flag | Effect |
|---|---|
--verbose | Detailed per-team/per-member logging |
--force | Skip change detection, sync all teams and fetch player history for every member |
Error Handling
Section titled “Error Handling”- 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
Source Files
Section titled “Source Files”| File | Purpose |
|---|---|
pipelines/sync-teams.js | Pipeline orchestrator |
steps/download-teams-from-sportlink.js | Sportlink team scraping (Playwright) |
steps/submit-rondo-club-teams.js | Rondo Club team API sync |
steps/submit-rondo-club-work-history.js | Rondo Club work history API sync |
steps/submit-rondo-club-player-history.js | Sportlink relation-date enrichment and reconciliation |
steps/prepare-rondo-club-teams.js | Team data preparation |
lib/rondo-club-db.js | SQLite operations |
lib/rondo-club-client.js | Rondo Club HTTP client |
lib/sportlink-login.js | Sportlink authentication |