Skip to content

Repository files navigation

Rondo Sync

Node.js 18+ License: GPL v2 Version

Automated member data synchronization for Dutch sports clubs. Extracts data from Sportlink Club (KNVB's member administration — no API) via headless browser automation and syncs it to Laposta, Rondo Club (WordPress), and FreeScout. Club volunteers never enter the same data twice.

The people import also refreshes Sportlink parent field labels for existing relationships. To backfill labels without changing contact data, run node tools/sync-parent-slot-labels.js --apply on the sync server; omit --apply to preview source coverage.

Laposta receives three current-season volunteer counters from Rondo: vrijwilligersplicht (total required duties, -1 for exempt/not applicable), vrijwilligersingepland (planned), and vrijwilligersafgerond (completed/credited). Progress follows Rondo's shared-family attribution. Planning or completing a shift never lowers the total requirement.

System Architecture

graph LR
    SL[Sportlink Club]
    SYNC[Rondo Sync<br>+ SQLite databases]
    ST[Rondo Club WordPress]
    LP[Laposta]
    FS[FreeScout]

    SL -->|Members, teams,<br>functions, discipline| SYNC
    SYNC -->|Members, custom fields| LP
    SYNC -->|Members, parents, teams,<br>commissies, work history,<br>photos| ST
    SYNC -->|Customers| FS
    ST -->|Field changes| SYNC
    SYNC -->|Reverse sync| SL
Loading

How It Works

  • Browser automation — Playwright (headless Chromium) with TOTP 2FA navigates Sportlink's UI to extract data from a system that has no API
  • Hash-based change detection — SHA-256 diffing ensures only records that actually changed get synced, minimizing API calls and avoiding unnecessary updates
  • State tracking — SQLite databases maintain ID mappings, sync history, and photo upload state across systems
  • Pipeline locking — flock-based concurrency prevention ensures parallel cron jobs don't collide
  • Operations dashboard — Manual starts confirm that the sync wrapper stayed alive and show immediate lock or launch failures inline
  • Email reports — HTML summaries via Lettermint after every sync run
  • Photo sync — Downloads member photos from Sportlink, uploads to WordPress with a state machine tracking each photo's lifecycle
  • Reverse sync — Pushes Rondo Club field changes back to Sportlink via browser automation, retries transient Rondo API failures, normalizes Sportlink's localized date display during read-back verification, and supersedes queued intermediate values when a newer edit corrects or reverts them

Sync Pipelines

Dutch name prefixes stay in Rondo's separate infix field for Sportlink members. Sponsit may include them in the surname instead. Matching compares the complete surname plus first name and email, retains stable source-ID precedence, and quarantines ambiguous matches. Equivalent names keep their existing Rondo field layout. FreeScout and the sponsor Laposta list receive the complete surname; the regular member Laposta lists retain their separate tussenvoegsel field.

After a person merge, sponsor aliases with different emails share one primary pass. Sync preserves an existing eligible primary relation, including a manually managed Businessclub pass.

Member sync checks the surviving person's KNVB ID before following a merge. When a merged source has a different KNVB ID, its original tracking row is retained with retired_into_knvb_id. Reimports cannot reactivate that source, even after the old WordPress post is permanently removed. Former-member cleanup also leaves the surviving membership intact.

Pipeline Schedule What it syncs
People 4x daily Members, parents, photos → Laposta + Rondo Club
Functions 4x daily + weekly full Commissies, free fields, work history → Rondo Club
FreeScout Daily Customer names and email addresses → FreeScout helpdesk
Teams Weekly Team rosters + work history with Sportlink relation dates → Rondo Club
Discipline Weekly Discipline cases → Rondo Club

Sportlink-owned member fields are synchronized as desired state: when a complete Sportlink response explicitly contains an empty game activity, the sync sends spelactiviteit: null so Rondo Club clears the previous value. The individual sync with --fetch (also used by Rondo Club) searches Sportlink by the exact KNVB ID before overlaying its partial /general response, so game activity and age class are refreshed immediately. Former members are searched with the inactive status filter if needed. Missing, ambiguous, or incomplete search results stop the sync before person updates; an explicitly empty activity still clears the old value. Runs without --fetch continue to use the stored full member snapshot. After a successful individual sync, its locally computed source hash is recorded as synchronized so the next People run does not process the same payload again.

Team history uses the Sportlink season when a historical relation has no end date: a closed season ends on June 30. Explicit end dates and continuing current-season relations take precedence, so importing old history cannot reactivate an old role.

Daily Timeline

All times in Europe/Amsterdam timezone.

 07:30         Functions sync (recent) → 08:00 People sync (1st) + FreeScout sync
 10:30         Functions sync (recent) → 11:00 People sync (2nd)
 13:30         Functions sync (recent) → 14:00 People sync (3rd)
 16:30         Functions sync (recent) → 17:00 People sync (4th)

 Sunday  01:00  Functions sync (full --all)
 Sunday  06:00  Teams sync
 Monday  23:30  Discipline sync

Quick Start

Prerequisites: Node.js 18+, a Sportlink Club account with TOTP 2FA configured.

npm install
npx playwright install chromium
cp .env.example .env  # Fill in your credentials

Run a pipeline:

scripts/sync.sh people           # Members, parents, photos
scripts/sync.sh functions        # Commissies + free fields (recent)
scripts/sync.sh functions --all  # Full commissie sync (all members)
scripts/sync.sh freescout        # FreeScout customers
scripts/sync.sh teams            # Team rosters + dated work history
scripts/sync.sh discipline       # Discipline cases
scripts/sync.sh all              # Everything

See the Installation Guide for full setup instructions including server deployment and cron configuration.

Documentation

Document Contents
Installation Prerequisites, server setup, initial sync, cron
Architecture System overview, schedules, field mappings, data flow
People Pipeline 7-step flow, Laposta + Rondo Club field mappings
Teams Pipeline Team download + work history
Functions Pipeline Commissies, free fields, daily vs full mode
FreeScout Pipeline Minimal customer identity sync
Discipline Pipeline Discipline cases + season taxonomy
Reverse Sync Rondo Club → Sportlink browser automation with parent e-mail propagation
Database Schema All 4 databases, 21 tables
Operations Server ops, monitoring, deploys
Troubleshooting Common issues and solutions
Utility Scripts Cleanup, validation, inspection tools

Tech Stack

Node.js 18+ · Playwright · better-sqlite3 · otplib · Lettermint · dotenv

License

GPL v2 or later

About

Automated member data sync from Sportlink Club to Laposta, Rondo Club, and FreeScout

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages