status — built and tested, not yet in use. self-initiated.
I work in the Entertainment Technology department I studied in, so I have been on both sides of this counter.
most of what I know here I found by accident. I had no idea microcontrollers were something a person could buy and program themselves until one crossed my path. the curiosity was there. the encounter wasn't.
a catalogue can't make anyone curious. it can be one more way to run into something. it helps the staff too: what the department owns lives in whoever has worked here longest, and a page can be checked at eleven at night.

what it does
- browse everything the department owns, with photos and live counts
- request an item for a date — as a student, or for a class
- approve or turn down a student request, with the reason shown to them
- hand over and check in at the desk, item by item
- walk-in: several items for somebody at the counter with no request and no account
- manage stock — equipment, storage locations, retiring, who is on the desk which day
- history on any request: who did what, when, and which item came back
that is the whole feature list. the rest is the reasoning underneath.
permissions live in the database, not in the pages
there is no server of my own here. the browser talks to Supabase directly, with a key that is public by design — so anyone signed in could query any table themselves, whatever the interface chose to show them.
that means the page cannot be where permission is decided. every table carries row level security policies, so an account that should not see a row does not get it, even asking the API directly. the pages are left with the cosmetic half of the job: which tabs are worth showing.

policies gate on capabilities rather than role names — can_desk(), can_approve(), can_stock(), is_admin() — and roles stack, so one person can be a student and desk staff at once. adding a role later is one function body, not a search for every place a name was spelled out.
the sign-up form is the smallest version of the same split. type an address and it tells you, before you press anything, which kind of account that domain will create. that line is a courtesy. the domain is read again inside the database as the account row arrives, and that reading is the one that decides — including for anyone who skips the form and talks to the API directly.

who decides, and who executes
a student's request waits for a senior technician. a teacher's does not — helping with class equipment is the desk's job rather than something to be granted, and a queue in the middle only delays a class that is already scheduled.
so working the desk and deciding are separate capabilities, and holding the first does not give you the second: there is no approve button unless you may approve. a teacher's request has no rejection path at all. the desk answers it line by line instead — have it, don't, or here is what there is instead — which also closed a quieter bug, where a hand-written line for something nobody had catalogued simply vanished at hand over, because nothing in the system had moved.
the trade is two flows to build and to explain rather than one. collapsing them would have meant either making teachers wait, or letting students through without approval.
what is written is what runs
no framework, and no build step. the files in the repository are the files the browser runs, so any one of them can be opened and read as the thing that is actually happening.
that is a judgement about circumstances rather than about tools. a framework and a build step are an investment paid up front — setup, tooling, a layer between what is written and what runs — and what they return is spread over a team, a long life, and many screens. this is a project I started myself, that nobody has committed to, and one person maintains it. so the cheapest thing that works is also the right thing here, and when that stops being the situation the arithmetic changes with it.
the two outside libraries are copied in rather than fetched from someone else's server as the page loads: a compromise of that server would be a compromise of every session here. the cost is that updating one is a deliberate act instead of something that happens on its own, which is why the version sits in the filename.
stock moves when equipment moves
filing a request reserves nothing. the count changes at hand over and at check-in, so the number on screen is the number on the shelf.
a count that lags behind the shelf does not stay a small problem. it gets wrong, people notice it is wrong, they stop consulting it, and nothing corrects it after that — so it drifts further, which is the state most equipment records end up in. the only way out is for the record to move at the moment the equipment does, done by the person doing the moving, in the seconds they have at the counter. reserving on request is the other direction: holds nobody collects, and a number that means intentions rather than shelves.
and because two people can work the desk at once, that count is never changed from JavaScript. it moves inside database functions that lock the row first, so two clicks at the same moment cannot both subtract from the same stale number.
history is a log, not a status
two moments need it. the count is short and somebody has to find out who is holding one. or you come on shift to something half-finished, and need to know who was dealing with it before touching it.
a request has a status, but a status only says where that request is now — it is overwritten every time the thing moves. left at that, a docket leaving the desk erases the record of it having left.
so every step is written as its own event, at the time it happens, and an item's own record reads the same way: how many are out, who is holding them, and everything that has been and come back. the desk and the person who filed the request read that same log — what happened to something should not depend on who is asking, and the policies decide what each account may see.
an action nobody has named yet shows its raw database name rather than being left out. a step with no label is still a step that happened, and dropping it would make the log lie by omission — nobody goes looking for a line that isn't there.