@leumas/consoles-web
consoles.leumas.tech — Leumas Consoles on the public web. A free, prerendered reference with a page per console — what the hardware was, which emulator core runs it in a browser and which native...
@leumas/consoles-web — Leumas Consoles at consoles.leumas.tech
A public, prerendered console reference — a page per machine, saying what it was, which emulator core runs it and whether that happens in a tab or on your desktop — and behind a Leumas membership, the deck itself: bring games you already own, play them, and keep your save states.
Leumas supplies no games. There is no ROM catalogue here, nothing to download and no links to one. That is not a caveat bolted onto the pitch; it is the shape of the product.
The two halves, and why they are two
| Path | Who | What serves it | |
|---|---|---|---|
| The reference | /, /systems, /systems/:id, /download, /legal | anyone, no account | prerendered HTML, one file per page |
| The deck | /consoles, /consoles/:system, /consoles/:system/play | signed in + membership | the SPA fallback; no file exists |
[critical] consolesRoutes() from @leumas/features already owns /consoles and wraps the whole subtree in RequireAuth, with eighteen hardcoded /consoles/... links inside its five screens. The public reference therefore lives at /systems, not at /consoles. Giving the two the same prefix would either collide with the deck's routes or put RequireAuth in front of the pages whose entire job is to be readable by a stranger — a crawler bounced to /login, on every indexable URL the site has.
This product renders no console UI of its own. The carousel, the library, the uploader, the player and the discovery search are all @leumas/features' — the same code Leumas Studio mounts. src/screens/Deck.jsx is thirty lines and adds nothing but the router and a lazy() boundary. A change to the deck belongs in shared/packages/features/src/surfaces/consoles/, where Studio gets it too.
Where the shelf comes from
Nothing on this site names a console by hand. scripts/systems.mjs reads BROWSER_EMULATORS and NATIVE_EMULATORS out of @leumas/a-consoles at build time and writes the result to dist/consoles-systems.json; src/content/pages.js turns each row into a page, and the browser fetches the same file back.
Two consequences worth stating:
- The site cannot advertise a console the engine does not have, and a core added to the engine
gets a page on the next build with no edit here.
- Every count in the copy is computed, not typed. "Six console cores", "the eighteen that need a
real emulator" and "twenty-seven native emulators" were all written by hand once and one of them was already wrong. A number in prose has no guard on it and no test that can fail: it is simply read by a customer and quietly disbelieved.
src/content/consoles.js holds the one thing that genuinely has no other source — a paragraph of what each machine was, and the year. It owns no emulator mapping and must not start to.
Six things that will bite whoever edits this next
- **
/api/consoles/is behindrequireAuth+consoles.core.* A signed-out visitor cannot
reach the engine at all, which is why the shelf and the logos are served as static files from this build (consoles-systems.json, consoles-art/**) rather than fetched from the API. Both are public on purpose and carry nothing about anybody's library.
- With
spa: true, any unproxied path answersindex.htmlat 200text/html. Every fetch
helper here checks the content type, never the status alone — useSystems and Download.jsx both do, and a caller reading the status sees success with a truthy body. The same trap hits the share card and all 23 console logos: a missing image "loads", and only the bytes tell.
- Never set
VITE_API_BASE. The session cookie is httpOnly and same-origin; an absolute API
origin drops it on every authenticated call and the deck renders permanently signed out with nothing in the console to say why.
- The prerendered copy and the React screens must say the same words. They do, because both
read content/pages.js. A screen that renders different prose means the crawler and the reader are on two different pages and only one is being ranked — a bug with no symptom.
- No root-level filename may collide with the apex build. imperium-server mounts leumas.tech's
own dist as static before this site's dispatcher on every host, so a file called logo.svg or favicon.ico here is silently shadowed. Everything this build emits at the root is consoles--namespaced, and check:consoles-public refuses a build that adds a colliding name.
- The port is in two files and nothing reconciles them.
5050invite.config.jsand in
SERVICES.consoles in ops/infra/scripts/dev.mjs.
Running it
pnpm dev:consoles # http://localhost:5050, API proxied from :3000
pnpm --filter @leumas/consoles-web build
pnpm check:consoles-public # the build's own SEO + asset guard (runs inside build too)
pnpm smoke:consoles-site # the real Imperium dispatcher over dist/
pnpm smoke:consoles-isolation # [critical] one member's library is unreachable to another
pnpm deploy:consoles ships it; dist is excluded from the deploy, so the server builds it.
The engine behind it
@leumas/a-consoles (shared/engines/adapters/domain/a-consoles) — 6 EmulatorJS browser cores, 27 native desktop launchers, the library catalogue, uploads, save states, game-art lookup and link-out discovery, plus 8 agent tools. Mounted at /api/consoles.
[critical] Every library route is owner-scoped. Uploads, saves, listings and byte-serving all resolve through pathsFor(ownerId), so a member's games live in data/consoles/u/<owner>/ and another member cannot list them or fetch one by knowing its exact filename. smoke:consoles-isolation asserts the 404 on the byte route, not merely the absence from the listing — filtering the listings is cosmetic while /files/roms/* still walks every root.