# Slides

Slides is the presentation app for the Oficina suite. A deck looks like a
Keynote deck on a desktop and reads like a document on a phone; you share
it as a read-only link, present it fullscreen with speaker notes, and drive
the talk from your phone while the room follows along. One instance per
team, on the CARLOS platform.

You edit a deck in the browser. Press **Edit** on a deck and
`/d/{sqid}/edit` gives you a slide strip down the side, the stage in the
middle, and an inspector for the deck's theme, the slide's layout and
whichever block you have selected. You type into the slide itself. Canvas
blocks drag and resize, snapping to the thirds and to each other. A
switcher flips the stage between desktop and phone widths, and what you
change in phone view becomes an override for phones rather than a change
everyone sees. Several people can be in a deck at once: you see where they
are, and undo only ever takes back your own work. Pictures upload straight
to the object store when one is bound.

The stage is not a second renderer. It shows the HTML the viewer would
serve for that slide, fetched from the server, with the text blocks bound
for editing in place — which is what keeps a deck the same deck wherever
it is seen.

A deck comes back out as a PDF. Press **Export PDF** on a deck, say
whether the speaker notes should come too, and the export runs as a job
you watch on a page of its own — one 16:9 page per slide, the text as
real text you can search and copy, the pictures embedded. It is the same
paint list the thumbnails are drawn from, set in the same faces the
browser sets a slide in, so the file is the deck rather than a photograph
of it. With no object store bound the file is held for an hour and served
from the instance; with a bucket it goes there and the download is a
signed URL.

The editor cannot hold a comment or show you an older version. And the
first deployment has no object store bound, so uploads there answer `503`
and the image field says so on the page.

A deck can also be built entirely by a program. The drive API in
[`docs/api.md`](docs/api.md) has every endpoint and every operation, and an
agent is a principal of its own with its own token — its writes are badged
as the agent's, never laundered into a person's name. Every screen but the
editor works with JavaScript switched off, and every string a person can
read is a catalog key.

Sign-in is a magic link, and the instance only mails one to an address in
`SLIDES_ALLOWED_ADDRESSES`.

## Running it

```sh
./scripts/run-dev.sh          # http://localhost:8091
```

That builds the binary and runs it against a scratch database that starts
empty every time. Sign in with any address you like: the magic link is
logged to the terminal rather than mailed, but it is the real flow, not a
stand-in.

Images go in an object store, never in the database and never on the
instance's own disk. Development uses a directory beside the database, so
uploading works out of the box; a real deployment gets its bucket from
`CARLOS_STORE_*`, and with no bucket bound the upload endpoints answer
`503` and the editor says so rather than half-working.

**Deploying with a bucket needs one thing this app cannot do for you:**
uploads are presigned PUTs made by the browser, straight to the store, so
the bucket's CORS policy has to allow `PUT` from the instance's origin
with the `Content-Type` header. Without it uploads fail in the browser
with nothing in this app's log — the request never reaches it.
[`docs/api.md`](docs/api.md) has the details.

## The editor's bundle

The editor is the one screen that needs JavaScript. Its bundle is built by
hand and committed:

```sh
cd fixtures && npm ci && node build-bundles.mjs
```

That writes `internal/slides/static/editor.<hash>.js`, deletes the
previous hashed file, and the Go binary embeds whatever it finds there.
Nothing builds it at run time and CI does not commit it for you, so the
moment you change a `.mjs` under `fixtures/` the committed bundle is out
of date until you rebuild and commit it. The gate rebuilds and diffs, so
you find out before the runner does.

Uploads need `https` (or `localhost`): the browser hashes the file with
`crypto.subtle` before asking for a signed PUT, and that API only exists
in a secure context. On a plain `http` host that is not localhost the
image field cannot upload, and nothing in this app's log will say why.

## The fixtures rule

`fixtures/` also holds a second implementation of the deck's operations,
in JavaScript, because the browser has to write the same document the Rust
core reads. `fixtures/data/*.json` are op batches; `node generate.mjs`
runs each one through the JS ops and writes the resulting Yjs update and
the JS view of it into `fixtures/vectors/`.
`internal/slides/core/vectors_test.go` loads those same bytes through the
core and requires the same view.

So change an op and you change both sides, or neither:

```sh
node fixtures/generate.mjs     # then commit what moves
```

Never edit a vector to make a test pass. A vector that was quietly
adjusted leaves the Go test comparing the core against a JS implementation
that no longer exists — it passes, and the two sides have already drifted.

## The gate

```sh
./.amadan/ci.d/10-gate
```

One script, run by CI and by you: rustfmt, clippy, the wasm build and a
`cmp` against the committed artifact, the Rust tests, then the JS half
(`npm ci`, `node --test`, the vectors regenerated and the bundle rebuilt,
both diffed against what is committed), then `make ci` (vet, gofmt, Go
tests, migration check). `make ci` on its own is the fast loop while you
work; run the whole gate before you push.

It needs Node 20 or newer with `npm`, and `npm ci` wants the registry
reachable while it runs. [`.amadan/README.md`](.amadan/README.md) says why
that trade is worth making and what each step catches.

## The browser drive

`internal/slidestest/browser_test.go` walks the app in a real Chromium and
is loud about any console error, failed request, or JS value that leaked to
the screen:

```sh
go test -tags browser ./...
```

It needs a Chromium on `PATH`, at `$RASTRILLO_CHROME`, or in a Playwright
cache, and **a skip is not a pass**: with no browser it fails, unless
`RASTRILLO_BROWSER_OPTIONAL=1` makes the skip a deliberate, visible choice.
It is not part of the plain suite or of the gate.

## Where the reasoning lives

The design is `docs/superpowers/specs/2026-09-07-slides-design.md`. The
plans are `docs/superpowers/plans/2026-09-07-slice-1-live.md` (sign-in,
viewer, sharing, presenting), `2026-09-07-slice-2-editor.md` (the editor
and collaboration) and `2026-09-08-slice-3-export.md` (the layout engine,
thumbnails and PDF export). `AGENTS.md` is the working file: the seams, the
traps, how to deploy. Read it before you change anything here.
