# @leumas/consoles-web — Leumas Consoles at consoles.leumas.tech

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

1. **`/api/consoles/*` is behind `requireAuth` + `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.

2. **With `spa: true`, any unproxied path answers `index.html` at 200 `text/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.

3. **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.

4. **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.

5. **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.

6. **The port is in two files and nothing reconciles them.** `5050` in `vite.config.js` and in
   `SERVICES.consoles` in `ops/infra/scripts/dev.mjs`.

---

## Running it

```sh
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.


---
Source: products/leumas-consoles/README.md
Canonical: https://docs.leumas.tech/p/products/consoles-web
