WHS Scheduler
May 2021TLDR: I built a React Native mobile app with >10,000 downloads for my high school classmates to track their schedules, instead of relying on paper or a janky legacy website. It involved REST API reverse engineering, bug-testing in production/lessons of maintainability, and working with admin for 5 years. You can view the code here.
Since this project is pretty old I no longer have all the documentation but I still remember the process I went through which I still think is pretty interesting!
The Problem
Westside High School has a unique “modular scheduling” system1: classes didn’t happen in fixed periods. Instead they occurred for 1 or 1.5 multiples of 14 total “mods,” aka 35- or 52.5-minute increments at unrememberable times depending on if they were “large group” – lectures, or “small group” – individual sections. Oh–and Wednesdays ran on a 22-minute-shorter schedule. It was a dedicated admin’s job to figure this all out for every student with Excel!2
The idea was to prep students for college life. If you didn’t have a class during a mod, it was “open” and you could go to various “IMCs,” instructional material centers, to get help from teachers in specific departments including social studies, science, math, art, etc similar to office hours (I always hung around the band IMC, very little work was done). You could also be “cross-sectioned” if you were lucky enough to have two classes during the same mod.
Just starting high school, this was pretty overwhelming. Your only options were either the paper schedule your counselor printed on the first day, or using an old .NET webapp hosted on an obscure Azure domain designed for a desktop monitor (UX?!) that was down half the time anyways. I distinctly remember the first week pulling out my tattered paper schedule on my way to Science 9–or was it Honors English? Stuffing it in my backpack wasn’t the best idea.
Live footage of me holding my schedule and remembering I can code
I had been messing around with React Native the summer before and wanted to make something real, not Todo App #2488. I started doing some research.
Inspect Element
I needed a way to programmatically pull my schedule from the school website. Having learned about REST APIs recently, I figured there was some endpoint I could POST to return a JSON version of my schedule. I wasn’t so lucky. It seemed like everything was rendered server-side and command-Fs for $.ajax
or XMLHttpRequest
were moot. But inspecting-element like a l33t-hax0r I was lucky enough to find a hidden gem buried in the rendered page on login:
<script>
window._pageDataJson = '{"schedule":[{"sourceId":26559,"sourceType":"course","title":"English 1 S2","body":"Rm. 113","roomNumber":"113","sectionNumber":5,"phaseNumber":1,"day":2,"startMod":14,"length":1,"endMod":15,"data":{"courseId":"476"}}],"semesterId":6,"teacherId":"523","action":"Student","controller":"Student","userId":"3175"}';
</script>
We’re in! I now had a JSON representation of the schedule with oodles of information. And after wrestling with react-native-fetch
’s cookie quirks, I was able to programmatically send a POST to the login page, request a redirect, then use Cheerio to parse the page and extract what I needed. I also could extract school photos and ID numbers.
An App Takes Shape
Once I knew the idea was feasible, I had a few design requirements:
- Check your next class
- How long of passing period you had left
- View my schedule for every day in a form that actually fit my screen
With a few Node.js scripts, I wrote some spaghetti code (with then-blazing-new ES6 functional array reduce
, forEach
, and map
) to
- Group the list of “class items” in the JSON into days
- Query the current, next classes, passing period duration given a certain datetime
- Handle different schedules (Wednesdays, 1PM dismissals on holidays, etc.)
With a few notecards I drew a few mockups (my sister had a few thoughts). Over the next week, I worked in parallel to turn the mockups into React component and soon I was able to do an ad-hoc build on a few of my friends’ phones as a “market test.”
First version of the app, teacher names redacted
Word spread like wildfire, so I got started on researching how to release a build to the Android and iOS app store. After negotiating a $99 check from the school administration and many countless nights spent configuring certificates, painfully waiting for Gradle builds, I released the app about a month later in early October.
Crisis Strikes
After a feature in the school newspaper, usage really began to ramp up. And so did the cracks of my brittle-untested code. The list included:
- A school-wide announcement to “ignore the app” during our first assembly. I didn’t know these were a thing, and they threw off the entire day’s mod schedules
- “★☆☆☆☆ I missed my cross-sectioned class!” – my app tracked this overlap but I’d forgot to make any UI 🤦
- Edge cases for my schedule search algorithm that involved people sending me their raw website HTMLs for me to debug
So to handle this I devised a few solutions:
- Wrote a quick-and-dirty CRUD admin backend in Vue.js3 backed by Express hosted on Heroku using MongoDB’s then free service to keep a list of “special dates” (assemblies, early dismissals, the works) so my app could dynamically change its behavior. Every time you opened the app, it would poll to see updates. Later I integrated snow days as well so it would give you a nice celebratory message.
- Wrestled with React Native’s new support for flexbox to properly display cross-sectioned classes, and notify on the Dashboard that you had to decide which one to go to.
- Added in-app bug and crash reporting with Bugsnag! This would send the entire internal state of the app (Redux) with passwords and IDs redacted. No more long debug email chains.
- Added in-app feature requests. A lot of people who wanted ads? Or a paid version they could flex on their classmates with??
But perhaps most exciting of all was over-the-air updates with Codepush. No more begging the admin to make an announcement over the PA to go to the app store and please-please-please update for a minor off-by-one. It was a rocky start, but I learned how to manage rollouts over different app versions, create migrations for Redux state when I added new features, and practice good deployment etiquette.
Sharing with QR Codes with $0 Budget
There was a feature I wanted to implement next: sharing schedules with your friends. This posed kind of a challenge though: the school’s REST API didn’t allow you to view other students’ schedules with your login cookie. Something something principle of least privilege. So I needed a method only via my app, when you and your friend had both authenticated separately.
QR codes became the natural answer. I’d generate one with your local schedule data if you logged in and you could only scan a friends’ via my app. But I ran into a problem pretty fast: QR codes can only hold a max of 2953 bytes and they look monstrous at that size. My bug reports contained schedule JSON blobs upwards of 30k bytes.
Biblically accurate QR code (~1852 ASCII chars)
I also couldn’t just store everyone’s schedules in my CRUD app: neither MongoDB’s nor Heroku’s free tier were forgiving and I was cheap cheap. So I came up with this really really scuffed scheme (you can see it here)
/**
* QR Generation pipeline:
* Convoluted to allow for large schedules
*
* schedule (JS object) --> compressed (JS object) --> binary (deflated after JSON stringified)
* --> base64 --> POSTed to API/shorten?d={base64} (d for 'data')
*
* At endpoint /shorten, a URL with schedule data is shortened via bit.ly. The id of the generated
* link is returned as JSON. The id (which is a bit.ly link without a protocol) is encoded as the QR code
*
* on scan --> /expand?id={id from QR code} --> bit.ly lookup retrieves the originally shortened URL with data
* --> decodes base64 --> uncompresses data --> returns schedule
*/
The idea was first to compress the schedule JSON as much as possible. I didn’t want to go through the trouble of creating a true binary representation, so I opted to shorten key names, remove unwanted properties, turn objects into arrays with well-known indices. Then I’d turn the JSON into binary, base64 encode (in retrospect, this definitely didn’t help my case), then slap on a URL stub so that bit.ly would actually shorten it.
Using the bit.ly’s shortened URL (and hopefully not exhausting my usage limit, I think I did a back of the napkin fully-connected graph type calculation) I’d encode that in the QR code. This would give me something at most a few tens of bytes! I was using bit.ly as my datastore instead of MongoDB! In hindsight, I definitely could’ve shelled out a few bucks (or asked the school admin to reimburse me), or even learned how to make a proper shortened encoding. But this was quick and dirty over winter break, and it worked.
I piloted the feature with a few friends, but after the principal caught wind I unfortunately had to leave the feature unreleased. Maybe there was a reason4 to leave other students’ schedules inaccessible after all… in it’s place I added the ability to add teacher schedules. It was a fun engineering challenge while it lasted.
Maintenance and Inspiring Others
I spent the new few years maintaining the app, giving it a new makeover internally and externally. I simplified a lot of the code, rewrote much of it in TypeScript and added extensive unit testing with Jest for all the edge cases I’d encountered over the years. Now, the code lives on GitHub with ~90% coverage. During the COVID years, I updated the app to handle an interesting E-learning change, where students were staggered to ensure 50% occupancy of the building.
App Store screenshots from v3 – with dark mode!
On the less technical side, I got to meet a ton of new people from the project: freshman to seniors, teachers, school administration. It turned out, the project had inspired a few folks to pursure coding and create similar apps for our finals scheduling system. That, even more than all the technical stuff I learned along the way, was what made it worth it! It started a lot of conversation with friends I would’ve never talked to otherwise, and got me out of my shell.