Enrolment — an ANUhub slice
The system that reliably ruins my week is ANUhub’s unit enrolment flow: a course you want has no places left, or adding it silently blows your credit-point load, and you find out after the click rather than before. This prototype is that one slice, wired end to end over the real ANU course catalogue — all 6,003 course offerings for 2026 and 2027, imported from Programs and Courses — with the constraints enforced server-side instead of left to a form that accepts anything.
Where the data comes from
src/data/courses.json is the ANU Programs and Courses catalogue, fetched by
pnpm courses:fetch (scripts/fetch-courses.ts). P&C renders its catalogue
search client-side from /data/CourseSearch/GetCourses, which returns a whole
commencement year in one response, so the import is two requests rather than a
crawl of six thousand pages. Course code, title, session, career and credit
points are ANU’s.
Places are not. P&C publishes no class sizes, so this app derives a capacity and a number of places already taken from a hash of the course code — deterministic, so it is stable across reseeds and redeploys, and deliberately shaped so about one course in sixteen is full and you actually run into the constraint while browsing. It is invented data. The page says so under the table, and it is the one thing here a marker should not read as ANU fact.
What good looks like here
-
A core flow that persists. Enrol in a course, reload the page (or come back after a redeploy) and it’s still there. Drop it, and it’s gone.
spec/enrolment.test.tsdrives the running app over HTTP to check that the database, not the client, is the source of truth. -
You can actually find your course. Six thousand rows is not a page, so the catalogue is a server-rendered search over code and title with career and year filters, capped at 50 results and showing the true match count. Plain GET form, no JavaScript required.
-
The credit cap is per session, which is the point. 24 credit points is a standard full-time load in a session, so four courses in First Semester and four more in Second is a normal year, not an overload. Enrolments are grouped by session, each with its own
n / 24 cp, and a course offered in several sessions makes you pick which one you’re enrolling in. -
The rules are enforced, not decorative. Six of them, all in one place —
enrol()insrc/lib/db.ts: unknown course, a course with no published session (in P&C that means it isn’t offered), a session the course doesn’t run in, a second enrol in the same course, no places left, and over that session’s cap. Each says which rule you hit and returns you to the search you were reading rather than dumping you at the top. -
Multi-tab live state. Places and the credit total are shared, so enrolling in one tab updates every other open tab over the SSE stream; a stale “12 places left” is worse than none.
-
Prerequisites, but only where they can be checked honestly. Requisites are imported from each COMP course page (
pnpm requisites:fetch), because unlike the catalogue they have no bulk endpoint. Of 245 fetched, 42 parse into a rule the app enforces; 203 are shown but not enforced — anything phrased as “6 units of 1000 level MATH” or with theCOMP1110 /1140shorthand is displayed verbatim and left alone. A wrong guess here tells a student they can’t take a course they’re eligible for, which is worse than not checking. Courses outside COMP have no requisite data at all, and say so with a dash rather than implying they have none. -
Permission codes, the way ANU actually uses them. When a convener grants an exception, you get a code. Here a code is bound to one course, grants exactly one exception —
full,over-caporprereq— and burns on use. It can’t be used on another course, can’t lift a rule it wasn’t issued for, and a spent or unknown code is refused in its own right rather than quietly falling through to the rule it failed to lift. A code handed over when nothing was blocking isn’t spent.Real codes come from a convener by email; these are seeded so the flow can be demonstrated. Each works once:
Code Course Lifts FULL-3600-A/B/CCOMP3600 (2026) no places left PREQ-1110-A/B/CCOMP1110 (2026) prerequisites LOAD-2310-A/B/CCOMP2310 (2026) the 24cp session cap To see the prerequisite one work: enrol in COMP1110 and it refuses; add COMP1100 under Completed courses and it lets you in; or use a
PREQ-1110code instead of completing anything. -
It looks like ANU, without pretending to be ANU. The palette and type are taken from ANU’s own published stylesheet — Public Sans, ANU gold
#be830e, black and the greys — rather than guessed. Gold is used for accents, rules and button fills only, never small text on white, where it lands around 4.0:1 and misses the 4.5:1 contrast floor. There is no ANU crest, logo or wordmark anywhere: this borrows the design language, and the page says in plain words that it’s a student prototype. -
What’s a judgement call, not a spec-enforced check: there’s no login — “my enrolments” is one shared list, so this models one student’s session, not a multi-user system. Prerequisites, timetable clashes and program rules are out of scope; the three constraints above are the ones that cost me time, and going further would be rebuilding ANUhub rather than fixing the one slice of it that hurts.
-
The accessibility floor (
spec/invariants.test.ts) and the deployed/readme/promise (spec/readme.test.ts) are shipped, always-on checks this app keeps green.