Benjamin ThaiSoftware Engineer · ML
← Projects
Web2026Complete

Cypress Scheduler

A course-scheduling tool that turns a list of desired classes into conflict-free weekly schedules.

Next.js 16TypeScriptPostgreSQLPrismaLeaflet
01THE PROBLEM

Every term starts with a spreadsheet nobody wants to build.

Cypress College has no visual course scheduler. Planning a term means cross-referencing the published section list against the myGateway portal by hand. You copy meeting times into a spreadsheet, then check every pair of classes for an overlap before your registration window opens.

The work is mechanical, but the stakes aren't: one missed overlap and a student registers into a week they can't actually attend, or gives up a section they needed while re-planning around it.

02SCOPE

What the tool has to get right.

Complete

Search the real catalog, not a sample of it. Three terms of Cypress College sections, with live seat and waitlist counts refreshed on a schedule rather than typed in by hand.

Correct

No two meeting blocks in a generated schedule may overlap, including courses that meet on several days or split into separate lecture and lab meeting patterns.

Comparable

A list of valid schedules is not an answer. Options render as a week and are ranked against the student's own constraints, so they can be judged at a glance instead of decoded.

Registration itself stays in myGateway. The app plans a term, it doesn't enroll you in one. Instructor quality is a link out to RateMyProfessors rather than something I model.

03THE APPROACH

Add the courses you want, get the weeks that work.

Students search the catalog, add the courses they want, and the app generates conflict-free schedule options out of every section combination available. Those render on a weekly calendar they can step through, save to their account, and share by link.

Everything lives on one screen: search on one side, the week on the other, so adding a section and seeing where it lands is a single glance rather than a page transition. Plans sit side by side, because comparing “the Tuesday-heavy one” against “the one with no Fridays” is the decision students are actually making.

04HOW IT WORKS

Three stages, one pass per change.

01 · INPUT

Scraped catalog

A Python scraper pulls the district's public course and section JSON for three terms, joins titles and units onto meeting times and seat counts, and seeds Postgres through Prisma. GitHub Actions keeps it current.

02 · GENERATE

Conflict-free permutations

Each added course expands into a pool of its sections. A depth-first walk takes one section per course and abandons a branch the moment two meeting blocks overlap.

03 · RENDER

Ranked weekly calendar

Surviving combinations are scored against the student's constraints (no Fridays, no early mornings, a cap per day) and the best ones render on the calendar.

05INSIDE THE GENERATOR

From a list of courses to a set of weeks.

The generator re-runs whenever the course list or the constraints change. Each step below is one stage of that pass.

01

Normalize the catalog

Every section becomes a set of meeting blocks: days, start, end, building, room. Times and dates are stored as plain strings in Postgres, so no timezone can ever be applied to a class time by accident.

02

Expand each course into a pool

A course the student added is matched against every section sharing its subject and number, deduplicated by CRN. The search therefore covers alternate sections the student never explicitly clicked on.

03

Walk the pools, reject on first conflict

One section is chosen per course, depth-first. Meeting blocks are projected onto a fixed dummy week so an overlap is a single interval test — starts before the other ends, ends after it starts — and a candidate is dropped the moment it collides, pruning that whole branch instead of scoring a dead schedule. A visit cap keeps a wide course list from exploding the cross-product.

04

Score and rank the survivors

Each complete schedule is scored on Friday classes, how far it starts before the preferred hour, the busiest day's load, and how many days it puts the student on campus. Lowest score wins, and the top options are the ones the student steps through.

06INTERFACE DECISIONS

Choices in the calendar view.

A week grid, not a results list
Students judge a schedule by its shape: gaps, early mornings, back-to-backs. A grid shows that instantly; rows of times don't.
Courses keep a stable color
A fixed six-color palette assigns each course the same block color everywhere, so switching between options never resets the student's mental map of the week.
The campus map sits beside the calendar
A building code means nothing to a new student. Meeting locations plot on a Leaflet map with an estimated walking time between back-to-back classes.
Share links carry the schedule itself
A shared schedule is compressed and HMAC-signed into the URL, so a link opens read-only for anyone. No account, no database row, nothing for the recipient to edit.
07SCREENS
▶cypress.mp4
The scheduler, with the calendar on the left and search and plan on the right. Press play for the full walkthrough.
Signed-out landing: what the tool does before you commit an account to it
Signed-out landing: what the tool does before you commit an account to it
Mobile: one pane at a time, with a bottom tab bar
Mobile: one pane at a time, with a bottom tab bar
A shared schedule, opened from a link
A shared schedule, opened from a link
08TRADEOFFS

What I chose, and what it cost.

Chose

Enumerate combinations in the browser

Cost

Work grows with the cross-product of sections, so a visit cap stands in for real pruning. That's ample for one student's term and would need rethinking for anything wider.

Chose

A scheduled catalog snapshot, not a live feed

Cost

Seat and waitlist counts are only as fresh as the last scraper run, so the tool plans a term rather than tracking one minute to minute, in exchange for never depending on a portal session.

The hardest part wasn't the scheduler

It was the nightly data job. The GitHub Actions run scraped and seeded but never synced the schema, so when the production database drifted the seed threw, the courses API swallowed the error, and the site quietly served a stale committed JSON fallback that looked completely normal. Nothing was broken on screen, which is exactly why it went unnoticed. The fix was making every stage fail loudly instead of degrading: assert the scrape returned sections before touching the database, push the schema first, seed row by row with inserted/skipped/failed counts, and commit the refreshed snapshot back so the fallback is never older than the last successful run.

09WHAT'S NEXT

Where I'd take it.

→

Unit tests around conflict detection, starting with sections that carry two meeting patterns, like a lecture on Monday and Wednesday with a lab on Friday. It's the one place a silent bug produces a wrong schedule that still looks right.

→

Alerting on the data pipeline. The courses API falls back silently, so a failed nightly run is currently invisible; a post-deploy assertion on the course count would surface it.

→

Versioned Prisma migrations instead of a schema push, plus an incremental upsert keyed on (CRN, term) so the course table is never briefly empty mid-run.