Flute Atlas & Piano Atlas
Personal · Competition researchFind the right opportunity. Keep the source.
Compare competition requirements side by side, inspect a field’s provenance and see how verified values stay protected.
Interactive explanation · not the live service
Loading interactive explanation…
Watch the original recordingsFlute Atlas · Piano Atlas · Recording notes included
Use the left and right arrow keys to switch slides.
Recorded from a local build with the repository's public notices and a fictional account.
Searching for Telemann narrows the notices, one is saved, its required pieces open in the detail view, and a private calendar feed is created.
1 / 6Explore the architectureSystem diagrams, design decisions and detailed flows
System overview
- GITHUB ACTIONSPython collectordaily + on demandStep 1, fetch + parseOfficial notice sitesHTML, PDF, DOCX
- Python collectorStep 2, sync API, both waysCLOUDFLARE (ONE PER SITE)Workerpages + APICLOUDFLARE (ONE PER SITE)D1 databasesnapshots, changes, users
- CLOUDFLARE (ONE PER SITE)Daily cron triggerdeadline remindersStep 3, scheduled()WorkerVAPID pushWeb Push servicebrowser push endpointsreminderBrowser PWAapp + service worker
- Calendar appsGoogle, Apple, TickTickprivate .icsWorker
Other connections
- Browser PWAtoWorker
View the diagram as text
Parts
- Python collector (daily + on demand): Background job
- Official notice sites (HTML, PDF, DOCX): External service
- Daily cron trigger (deadline reminders): Background job
- Worker (pages + API): Owned service
- D1 database (snapshots, changes, users): Data store / broker
- Web Push service (browser push endpoints): External service
- Browser PWA (app + service worker): Client / UI
- Calendar apps (Google, Apple, TickTick): External service
Connections
- Step 1: Python collector → Official notice sites: fetch + parse
- Step 2: Python collector ↔ Worker: sync API
- Worker → D1 database
- Step 3: Daily cron trigger → Worker: scheduled()
- Worker → Web Push service: VAPID push
- Web Push service → Browser PWA: reminder
- Calendar apps → Worker: private .ics
- Browser PWA → Worker
One codebase, two sites
- Shared codebasepages + Worker modulesGITHUB ACTIONSbuild.mjsSITE=flute | pianodist/ per siteGITHUB ACTIONSTests per sitechecks + Playwrightpass on mainGITHUB ACTIONSDeploy per sitemigrate, deploy, checkCLOUDFLAREFlute Worker + D1SITE_ID=flute
- Deploy per site--env pianoCLOUDFLAREPiano Worker + D1SITE_ID=piano
Other connections
- sites/piano/tobuild.mjscounted swaps
View the diagram as text
Parts
- Shared codebase (pages + Worker modules): Hosting / CI
- sites/piano/ (site.json, data, icons): Data store / broker
- build.mjs (SITE=flute | piano): Background job
- Tests per site (checks + Playwright): Hosting / CI
- Deploy per site (migrate, deploy, check): Hosting / CI
- Flute Worker + D1 (SITE_ID=flute): Owned service
- Piano Worker + D1 (SITE_ID=piano): Owned service
Connections
- Shared codebase → build.mjs
- sites/piano/ → build.mjs: counted swaps
- build.mjs → Tests per site: dist/ per site
- Tests per site → Deploy per site: pass on main
- Deploy per site → Flute Worker + D1
- Deploy per site → Piano Worker + D1: --env piano
How a daily sync run updates a dashboard
Run starts
A daily cron in the collector repository starts one job per site, and each job reads only its own site's secret. The dashboard's update button starts the same workflow through the GitHub API, at most once every 30 minutes.
Export current state
The collector fetches the site's current notices with a bearer token and refuses redirects, so the token cannot leak elsewhere. It stops if the site id does not match, picks up notices readers added by URL, and opens a run.
Collect official pages
Six threads walk the catalog, fetching only public HTTPS hosts on each notice's allow-list, extracting PDF and DOCX text, using headless Chromium for dynamic pages and following linked rules documents.
Parse conservatively
Each document yields the edition year, opening, deadline, fee, age rules and repertoire, each stamped with its source URL. Documents from different editions are never merged, and ambiguous age rules stay 'needs checking'.
Ingest and diff
Each notice is posted to the ingest API. The Worker validates it, keeps human-verified fields (a differing value waits as an observation), records per-field changes tagged by kind and updates the notice snapshot.
Finalize and notify
Closing the run makes the Worker look for new or changed deadline and opening dates, store a message per account, leaving out notices that account hid, and send Web Push.
The project & my contribution
- Problem
- Competition deadlines, fees, age limits and required pieces are scattered across official pages, PDFs and Word files.
- Solution
- Two dashboards from one codebase, fed by a daily collector that reads the official sources and keeps field-level change history.
- Result
- Every field links to its official source, and the collector never overwrites a field a person has verified.
Live site: The interface is in Korean.
The source code is private. I'm happy to walk through it on request.
How it's built
- A daily Python collector in GitHub Actions reads official pages, PDFs and Word files
- Results sync through a token-protected API into each site's Cloudflare Worker and D1
- One build script produces either site; each Worker refuses the other site's bundle
Highlights
- Field-level change history that filters out noise like formatting-only edits
- The collector never overwrites a field a person has verified
- Calendar feeds, Web Push written on WebCrypto, and offline-capable PWAs