The lab
The interfaces from my projects, running for real in your browser. Press things.
- Experiment
- EXP-01
- From
- desk-companion
- Format
- 800 × 480, scaled to fit
The desk screen
The interface the Rust application draws on the Pi's panel: a dock of plugins down the left, a status bar along the top, and a grid of widgets that becomes a full page when you open one.
Try Press the dock slots to switch plugins, open the switcher and settings, then press play on the Spotify page.
No plugin loaded
Drop a manifest and two files into plugins/ to fill it
Beat 1284
every 2s
beat 1283
4 min ago
0 · 0
pointer, in a 0×0 viewport
What’s real here
Implemented
- Geometry, radii, type sizes and colours taken from the Slint sources
- Icon outlines redrawn from the same path commands the device uses
- Page routing between home, each plugin, the switcher and settings
- A clock and date read from your own device
- A host readout that prints your actual pointer and window size
Simulated
- There is no Pi, no Slint renderer and no framebuffer: this is an HTML rebuild
- Track, progress and the Beacon counter are fixtures, not plugin output
- No plugin code runs, and nothing is fetched from any service
- Experiment
- EXP-02
- From
- desk-companion
- Format
- Two ends of one wire
The serial link
How the host and the device stay in step. They open with a version and baud handshake, then every change travels as a length-prefixed, checksummed frame, and the device repaints only once a frame has arrived intact.
Try Toggle a module on the host, flip a bit to corrupt the next frame, or pull the cable and watch the link come back.
What’s real here
Implemented
- Message names and fields lifted from the protocol crate
- Frames encoded byte for byte: length, type, sequence, payload, CRC-8
- Version and baud negotiation before any traffic, refused on a mismatch
- A corrupted frame fails its CRC, is NAKed and resent
- The device state is a pure reduction of the frames it has received
Simulated
- The wire is an in-memory queue; there is no serial port in a browser
- Travel is slowed to 700ms so a frame can be watched crossing
- The host is a handful of controls, not the Rust host process
- Experiment
- EXP-03
- Format
- Four panels, one passphrase
Deriving the key
How the file store decides what a request may do. Keys are derived from a passphrase in the browser, and a trust score is compared with a threshold that each action sets for itself.
Try Type a passphrase, run a low-risk action, then a high-risk one, then pass the challenge and run it again.
1 · Type a passphrase
Anything at all. It is not stored or sent anywhere. Watch what the browser builds from it, and watch the other column stay empty.
In this browser
- Saltnot derived yet
- PBKDF2not derived yet
- Derived keynot derived yet
- RSA-OAEP keypairnot derived yet
- RSA-PSS keypairnot derived yet
Sent to the server
Nothing. Not the passphrase, not a hash of it, not the key.
Which is also why a forgotten passphrase is unrecoverable here. There is no one else to ask.
2 · Move the trust score
Trust is a number, not a yes or no. Every request is scored against a threshold of its own, so the same action flips as this number moves.
40/ 100
3 · Attempt something
Derive the keys first. Type a passphrase above.
- needs 20 · low riskallowed now
- needs 40 · medium riskallowed now
- needs 60 · high riskneeds step-up
- needs 85 · severe riskneeds step-up
- no threshold grants this · impossible risknever
No request attempted yet.
4 · What the server holds, right now
- Ciphertext blobs3
- Saltpublic
- Encryption public keynone yet
- Signing public keynone yet
- Encryption private keynever uploaded
- Signing private keynever uploaded
- Derived keynever transmitted
- Passphrasenever transmitted
- Plaintext bytes seen0
Requests it saw
Nothing yet.
Every row above is either ciphertext or a public key. Own this server completely and that is still all you get.
What’s real here
Implemented
- The structure of the system: key material built client-side, a server that holds ciphertext and public keys only
- Per-action thresholds, step-up that is refused rather than queued, and an action no score unlocks
- Everything stays in this tab: no storage, no requests
Simulated
- No cryptography runs: there is no PBKDF2, no RSA and no WebCrypto call
- Nothing on screen is a secret, and the masked keys are decoration
- The server and its log are modelled in the page, not a running service
- Experiment
- EXP-04
- From
- neobank
- Format
- Phone, recipient's view
Requesting money
The request flow from the banking app. A request is a claim made by one person and decided by another, so the irreversible step belongs to whoever holds the money.
Try Decline the request, reset it, then settle it.
Nadia asks you for
RM 48.00
Dinner, Thursday
Pending your decision
The request is a claim. Until you settle it, no money has moved and nothing needs reversing; declining costs the sender a decline, not a refund.
What’s real here
Implemented
- The state model: pending, settled and declined are the only outcomes
- Settling and declining are the recipient's actions, never the sender's
Simulated
- No money, account or ledger exists behind it
- The names and amount are placeholders, and nothing is signed
Each of these has a longer write-up on its project page.