liquidbrain

Things I did to make a typing test

Around a week ago, I made a typing test as part of a workshop on bad art. You can find it here. I'd encourage you to try it now! It'll make the rest make sense.

In the post below, I go through in detail about the process making it, and what I learned along the way. It's a bit of an info-dump, so feel free to skip it.

tl;dr If you have questions about using disco, let me know!

1. Inspiration and static hosting:

The original typing test was exclusively made in html, JavaScript and a little bit of CSS. Within 45 minutes, I had made a minimal viable product. But ... this is evil enough that I need to share it with the world ... yes yes yes.

I started with GitHub pages. I was wanting to make a personal website anyways, and this was good excuse. I learned from the process, but mostly about how DNS caching is both an enemy and a friend.

One small detail I learned: I had not realized that index.html has special behavior1

2. Backend

This all was good, but there was a problem: I wanted a leaderboard. There's nothing quite like spending 6 minutes on a typing test and wondering whether other people can do it faster.

I decided to use FastAPI to implement the backend in python. This was sort of an arbitrary choice; I recently did a small project with Tywen Kelly where we used FastAPI.2 Other than that, I had never used a backend before, so there was a lot of learning to do.

The initial implementation was quite simple. All I needed to do was a) create leaderboard entries b) store them and c) return them to a website. Implementing these three functions (initially using a python list within main.py) took about an hour and 40 lines of code. This was mostly good to solidify what I had learned pairing with Python.

HTML Leaderboard

However, it wasn't enough to create the APIs. I needed to then pair that up with the front-end. Because my operations (get and set) repeated much of the same information, I structured the code to have a single function which performed the appropriate fetch request. Through this process, I learned quite a bit about javascript syntax (I think python ternary operators look better), but it was ultimately a straightfoward process. This felt like a straightforward feature to add.3

SQL and persistence

There was one problem with my implementation: the data was being stored in an incredibly fragile way. Any time I restarted main.py, the leaderboard was initialized to an empty list. While this might not matter in ideal conditions where the script never goes down, I want to set up a more permanent database. Given that I haven't worked with SQL before, I thought this would be a great chance to do exactly that!

And then, I chose the wrong package to use. I used sqlmodel, which was made by the same people as FastAPI. The two work together very well, but sqlmodel feels very very abstracted and I had a tough time getting a good feel for what was going on under the hood.

The primary challenge I faced (with help from Saleh Alghusson, thanks!) was in storing the data correctly. The data was very simple -- all I'm storing is the name and time -- but we ended up needing to introduce an id field to allow for duplicate names. However, with a little work, we got this running.

3. Hosting on Disco

Now that I had it running locally, I needed to figure out a place to host the website. Github pages only will host static pages, so I needed someplace else. I heard about disco from a talk that Gred Sadetsky (one of the creators, and an RC alum!) gave. The Recurse Center has a shared raspberry pi running disco, and I figured that I would give it a shot. It was ... a lot. A lot of fun.

Downloading Disco

The installation instructions online were simple: download a .tar file, extract it, and then add it to PATH. What could go wrong? I turns out, there were some windows symlink errors, and the default extractor just gave up.

With the help of Iain McDonald and Sebastian Messier, I was able to figure out a working solution, which I then wrote up. It required downloading 7-zip, and extracting the file twice. It ended up being fairly straightforward to figure out, once I calmed down enough to approach it with a straight head.

Lessons learned:

  • Writing documentation is hard (I spent like 45 minutes trying to type out what I had done in the 30 minutes previous. I probably still failed)

Starting server from FastAPI

Once I got disco downloaded, it was smooth sailing: I added my account to the shared server, added my typing test repo to the Disco App, and created a new project. I copied the boilerplate from the tutorial example, and built the code. And then ... I got errors.

The first error was super simple: I had called my python file a different name than in the tutorial; quick 10 second fix. After asking how to reload a server manually (I can't read documentation, apparently),4 I was ready to display my website! It worked: and showed a blank screen.

After a bit of trial and error, I realized I needed to simplify things.

Playing around with the ~Flask~FastAPI Tutorial

There is helpfully a tutorial on the disco apps for a simple flask app. Luckily, the code worked as written.

I started to modify it for FastAPI. The first thing I realized was that I needed to start the server from within the main python file. The FastAPI tutorial recommend using fastapi run file_name.py from the command line to start a server, but the Docker file setup called python file_name.py instead. I know less about Docker than Python, so I figured that I would just stick with modifying python for the moment. The result was simple:

import uvicorn if __name__ == "__main__": uvicorn.run("main:app", host="0.0.0.0", port=8000)

Take note of the port number; that will be important below.

Adding HTML and Javascript

Now that I could call the server, I needed some way to show the HTML. I made the decision that I wanted all of the code together. I learned how FastAPI can work with HTML and javascript. Usually, FastAPI functions return a JSON object. However, using a special call (response_call = HTMLContent), they can return an HTML object which the browser then renders!

I iterated with that concept. It was a little difficult to add a separate javascript file, but I learned that one can mount "static" content in FastAPI, which did the trick. I made a little "hello world" template for FastAPI with disco here in case anyone is curious.

Ports

I was able to get the toy example running, but couldn't get my typing test to display for the life of me. I tried a bunch of things: I moved the directories around, I modified the docker file, I even changed which version of python I was using. All no dice.

And then, I tried changing the port I was hosting the server on to 8080. It worked! And then I realized that I had been foolish, in my disco.json file, I was setting the server to function on port 8080, so I needed to fix that in my code. Suddenly, the front end displayed! But I couldn't make requests that went through?

After some more debugging, I asked Claude, who noted that I had hardcoded my front end to make requests to port 8000, and I needed to change that. That fixed this issue, but definitely illustrated bad coding hygeine. Is the correct approach to call both values from a configuration file?

Running on the Raspberry Pi

I was able to get this running locally, but there was an issue: my requests from the front-end were still throwing CORS errors when I hosted it on Disco. (For understandable reasons, servers won't usually accept requests from unknown endpoints; you have to configure this behavior to change it). I couldn't quite figure out how to do this locally on the Raspberry Pi, but I realized I didn't need to! I could just have the front-end call the public API, and all would be right with the world.

And this worked! Except for the bit where I forgot to add kai-williams.com to the CORS setting until after I had publicly released it. But now it's done. Yay!

4. Thinking about next steps

It's now public at , and I encourage you to give it a try. It's a little difficult, so don't lose heart too quickly. I have a couple issues that I might get around to solving (but likely not).

  1. The way I currently have it set up is that when I push a new update to Disco, the SQL file is wiped. I need to figure out how to

  2. There should be some sort of cheating prevention -- one of the people at RC did it in 2 seconds ... which really isn't physically possible.

Lessons learned:

  • Rome wasn't built in a day; it often takes a bit to understand and build something new. Even if the final project isn't particularly impressive, the learning is what matters! (That, and infuriating your friends)

  • Starting with a small project is good when trying to learn something completely new. I'm gonna make some questionable decisions (using FastAPI; not keeping my front-end completely separate; etc.), and it's easier for that not to be a problem in a small project.

  • Writing things is hard!

  • I would like to start writing down a debugging journal; I learned quite a bit from going through my git commits to see what I tried, and how I approached problems.

Acknowledgements:

Thanks to Greg Sadetsky for answering my questions about disco!

Thanks to Saleh Alghusson for pairing with me on the SQL database stuff!

Thanks to Tywen Kelly for showing me FastAPI and being a good sounding board.

Thanks to Iain McDonald and Sebastian Messier for helping me figure out how to install Disco!

Thanks to everyone at RC for the encouragement! 


  1. As far as I was able to figure out: when a website goes to a URL that is not the path of an HTML file, it must find an HTML file. index.html is the default name the browser looks for; if you use another name of the HTML file, nothing might display. 

  2. In hindsight, Flask might have made my life easier, but I think I learned more using FastAPI. 

  3. The only mildly controversial decision I made was to use a HTML table. I feel like I've heard out in the ether that "one shouldn't use HTML tables," but after perusing some blog posts (like this one, I figured it made the most sense for my application. 

  4. For future reference, it is disco reload --project-name {my-project} where one replace {my-project} with the name of their project. 

Thoughts? Leave a comment