Banner
A Rust system that scrapes UTSA course data from Banner 9 and serves it through a SvelteKit web app and a Discord bot, with an adaptive scraper and live availability tracking.
Banner is a Rust system that scrapes UTSA course data from Ellucian Banner 9 and serves it through a SvelteKit web app and a Discord bot. One binary runs three coordinated services: an Axum web server, a Discord bot built on Serenity and Poise, and a background scraper that keeps a PostgreSQL copy of the catalog current. The web app does fast course search, instructor profiles, and schedule building; the bot answers the same queries from inside Discord.
The adaptive scraper
Banner is a live registration system, not a data export. Scraping it too hard is antisocial and risks a block; scraping it too slowly means stale seat counts during the registration rush, when they change by the minute. The resolution is to make the rate a per-subject decision rather than a global one. Every subject in every active term carries its own scrape interval, recomputed each cycle from how its data has actually been behaving.
Intervals run from a three-minute floor to a four-hour ceiling. A subject whose sections churn earns the floor; one that has gone twenty consecutive scrapes without a single change drifts to the ceiling. A time-of-day multiplier then stretches every interval when nobody is registering: weekday evenings double it, nights and weekends quadruple it. Subjects that come back empty or fail several times in a row are paused outright, with a probe every six hours to catch recovery.
How the interval is computed
Change ratio sets the base interval: 3 minutes at or above a 10 percent change ratio, 5 minutes at 5 percent, 15 minutes at 1 percent, 30 minutes below that.
A run of unchanged scrapes overrides the base: 30 minutes for up to five in a row, 1 hour by five, 2 hours by ten, the 4-hour ceiling by twenty.
Time of day multiplies the result: 1x on weekday daytimes, 2x on weekday evenings, 4x on nights and weekends.
Three consecutive empty fetches or five straight failures pause a subject, with a probe every 6 hours to detect recovery.
Past and archived terms bypass all of this and refresh once every three weeks.
Ranking instructors with sparse reviews
Each instructor can carry ratings from two sources: RateMyProfessors and BlueBook, the UTSA course-evaluation system. They sit on different scales and arrive in very different volumes. An instructor with three glowing reviews should not outrank one with two hundred steady ones, but a plain average treats them as equal.
Scoring runs through a small Bayesian pipeline. BlueBook scores are first regression-calibrated onto the RateMyProfessors scale. Both sources then update a prior built from the population mean of every RMP professor, weighted so each RMP review counts as roughly twice as informative as a BlueBook one. Instructors sort not by the posterior mean but by the lower bound of its eighty-percent credible interval, which holds thinly-reviewed instructors down until the evidence earns them a higher rank.
One binary, three services
The web server, bot, and scraper share a process, a connection pool, and one set of domain types. The frontend is compiled and embedded into the Rust binary at build time, so a deploy is a single self-contained artifact with no separate static host to manage.
Types cross the language boundary on their own. ts-rs generates TypeScript definitions from the Rust structs behind every API response, so changing a backend model surfaces as a frontend type error instead of a runtime surprise. The admin dashboard watches the scrape queue live over a WebSocket, receiving job events the moment they fire.
Rough edges
The scraper is only as right as its assumptions about Banner, which is sparsely documented and occasionally inconsistent. Session lifetimes, null semantics, and the meaning of some metadata fields were reverse-engineered from real responses and remain best-effort. Course-evaluation coverage is uneven, so some instructors are scored from RateMyProfessors alone.