I built an attendance tool in 2023 to escape a paper register. I rewrote it last week, and past me made four real mistakes.
In 2022 I started teaching programming to schoolchildren. On the first day I was handed the curriculum for the web track. Nobody could tell me who had written it. After a full year of lessons, the students were supposed to produce an HTML page with a background image and some pictures on it.
I threw it out and wrote a new one.
The other half of the job was the register. Paper, one sheet per child. On every sheet you marked attendance, wrote in the topic of the lesson and the number of hours. That was every group, every week, by hand.
At some point I had enough and built something. It was 2023, I knew PHP, and I wrote it the way you write things at eleven at night: procedural, one file per action. Some colleagues started using it too. Five commits in a single day, two of them named "Add files via upload".
Last week I opened that repository for the first time in three years.
What past me got right
More than I expected. Passwords went through password_hash and password_verify. Eighty-nine prepared statements, and not one place where $_GET was pasted into SQL. For something written to stop filling in paper, that is not bad.
What past me got wrong
htmlspecialchars appears zero times in the whole codebase. Every value from the database goes straight into the page.
The upload handler takes the extension from the filename the browser sent and writes the file into a served directory. You can upload a .php file and then request it. That is remote code execution.
Attendance was stored as (student_id, date, status). No group. A student who came to two different tracks was one row per day, and there was no way to say which of the two they had missed.
A student had a single group_id, so anyone attending two tracks could not be recorded at all.
Teachers and students lived in two tables with two separate login handlers. Anything that concerned both had to be written twice.
The rewrite
Laravel 13 and Postgres. The domain survived and nothing else did.
The interesting part is what the 2023 version never had: tasks that grade themselves.
I do not run student code. It goes to Wandbox, which has been running untrusted code for years and does nothing else. On a project this size, running my own sandbox would mean one person keeping isolation correct in their spare time, and I would rather that person were not me.
I expected to stitch several backends together, the way these things usually go: Wandbox for most languages, the Go playground for Go, the Rust playground for Rust. Then I read the Go playground's compile endpoint. It takes the program body and a version. There is no field for standard input. I posted stdin and input alongside the program anyway, in case the field simply went undocumented, and the program read EOF both times.
Without stdin there is no test case with input data, and without that there is no assessment worth the name. Every task collapses into "print this constant". So I use one backend. Wandbox covers fourteen languages, Rust, Pascal and SQL included.
Grading runs on a queue, because I measured it. One Go build on Wandbox takes about twenty seconds. Five test cases one after another would be a minute and a half. I send them in parallel, four at a time, which got it down to sixty-two seconds. Still far too long to hold an HTTP request open, so submitting queues the work and the page polls for the result.
Some test cases are hidden. For open cases the student sees the input and the expected answer. For hidden ones they see only whether it passed. Show everything and the solution gets fitted to the known answers instead of made to work.
Three findings that cost me an afternoon each
Wandbox's Rust compiler rejects the warning option set, and does not say so. It returns exit status 1 with empty output, which is indistinguishable from a program that printed nothing.
Mono prints non-ASCII as question marks until you set Console.OutputEncoding, and once you do, it prefixes the output with a byte order mark. Comparing that against the expected answer fails on an invisible character. The dotnetcore image on Wandbox does not build at all, so mono is the only option.
Laravel's trans_choice() falls back to the fallback locale in cases where __() returns the key unchanged. My source language is Russian and my fallback was English, so Russian pages rendered English text in exactly the places where a count was involved. No test caught it; I saw it in a screenshot.
What I am not sure about
Leaning on a free public service at the moment a student submits an exam is the obvious weak point, and I do not have a good answer for the day Wandbox is down. My reasoning is that the alternative is worse for a project this size. If you have run the other way and maintained your own isolation, I would like to hear how that went.
MIT, Laravel 13, PHP 8.3+, Postgres, no keys or accounts needed to try it. The seed builds a demo school with four tracks and a month of attendance.
[link] [comments]