Skip to content
<Hyun/>
All projects

Flute Atlas & Piano Atlas

Personal · Competition research

Find 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 / 6
Explore the architectureSystem diagrams, design decisions and detailed flows

System overview

  1. GITHUB ACTIONSPython collectordaily + on demand
    Step 1, fetch + parse
    Official notice sitesHTML, PDF, DOCX
  2. Python collector
    Step 2, sync API, both ways
    CLOUDFLARE (ONE PER SITE)Workerpages + API
    CLOUDFLARE (ONE PER SITE)D1 databasesnapshots, changes, users
  3. CLOUDFLARE (ONE PER SITE)Daily cron triggerdeadline reminders
    Step 3, scheduled()
    Worker
    VAPID push
    Web Push servicebrowser push endpoints
    reminder
    Browser PWAapp + service worker
  4. Calendar appsGoogle, Apple, TickTick
    private .ics
    Worker

Other connections

  • Browser PWAtoWorker
Each site is one Cloudflare Worker with its own D1. (1) A daily Python collector in GitHub Actions fetches and parses the official notices, (2) syncs the results into D1 through the Worker's token-protected API, and (3) a daily cron sends deadline reminders by Web Push, next to private calendar feeds.
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

  1. Shared codebasepages + Worker modules
    GITHUB ACTIONSbuild.mjsSITE=flute | piano
    dist/ per site
    GITHUB ACTIONSTests per sitechecks + Playwright
    pass on main
    GITHUB ACTIONSDeploy per sitemigrate, deploy, check
    CLOUDFLAREFlute Worker + D1SITE_ID=flute
  2. Deploy per site
    --env piano
    CLOUDFLAREPiano Worker + D1SITE_ID=piano

Other connections

  • sites/piano/tobuild.mjscounted swaps
build.mjs turns the shared source into a flute or a piano bundle. GitHub Actions tests both, then deploys each to its own Worker and D1 and checks that the live site serves the new build.
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

  1. 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.

  2. 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.

  3. 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.

  4. 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'.

  5. 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.

  6. 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