LANVAR
← The shelf

stagiaire-bot

Escaped the lab

A zero-cost watchdog that logs into UdeM’s medical placement portal every 15 minutes and emails the moment a stage slot opens up.

PythonPlaywrightGitHub Actions 2026
How it actually works
( idle ) CAS SSO stagiaire_01 •••••••• LOG IN PLACEMENT SLOTS 0 0 0 0 PLACEMENT SLOTS 0 0 2 0 PLACEMENT SLOTS 0 0 2 0 SHA256 FINGERPRINT last: a3f2 hashing… a3f2 = a3f2 — same a3f2 ≠ 9c41 — changed! a3f2 ≠ 9c41 — changed! vs last run, from state.json → zzz, nothing to say → wake the human! → wake the human! SLOTS OPEN! via Gmail SMTP SLOTS OPEN! via Gmail SMTP 9 SECONDS ≈ 45 MINUTES GitHub Actions cron fires every 15 min Playwright CAS login → scrape the table silence while the hash matches — one email the run it doesn't
FIG 1 · the stakeout, animated — three runs, two shrugs, one email
$0 INFRASTRUCTURE GitHub Actions cron every ~15 min, free forever Playwright login CAS SSO, with retries scrape capacity table every row, every page SHA256 fingerprint of the available slots, vs last run back to sleep nothing changed — silence state.json so next run remembers email — slots open! Gmail SMTP, alert template changed + available the whole architecture: a scheduler, a browser, a hash, an inbox
FIG 2 · the machinery — cron → login → scrape → hash → (only sometimes) email
The story

Medical students refresh the Université de Montréal placement portal by hand, hoping a clinical stage slot opens before someone else grabs it. That is a job for a robot.

A GitHub Actions cron wakes the bot every ~15 minutes — free, forever, no server. Playwright logs in through the university’s CAS single sign-on, navigates to the capacity table, and scrapes every row across every page. The available slots get serialized and hashed; if the SHA256 fingerprint matches last run, the bot goes back to sleep without a peep.

Only when slots exist and the fingerprint changed does an email go out over Gmail SMTP. State persists to a JSON file so the next run remembers. The entire architecture is a scheduler, a browser, a hash, and an inbox — which is exactly as much architecture as the problem deserves.