| 1 | # Slides |
| 2 | |
| 3 | Slides is the presentation app for the Oficina suite. A deck looks like a |
| 4 | Keynote deck on a desktop and reads like a document on a phone; you share |
| 5 | it as a read-only link, present it fullscreen with speaker notes, and drive |
| 6 | the talk from your phone while the room follows along. One instance per |
| 7 | team, on the CARLOS platform. |
| 8 | |
| 9 | You edit a deck in the browser. Press **Edit** on a deck and |
| 10 | `/d/{sqid}/edit` gives you a slide strip down the side, the stage in the |
| 11 | middle, and an inspector for the deck's theme, the slide's layout and |
| 12 | whichever block you have selected. You type into the slide itself. Canvas |
| 13 | blocks drag and resize, snapping to the thirds and to each other. A |
| 14 | switcher flips the stage between desktop and phone widths, and what you |
| 15 | change in phone view becomes an override for phones rather than a change |
| 16 | everyone sees. Several people can be in a deck at once: you see where they |
| 17 | are, and undo only ever takes back your own work. Pictures upload straight |
| 18 | to the object store when one is bound. |
| 19 | |
| 20 | The stage is not a second renderer. It shows the HTML the viewer would |
| 21 | serve for that slide, fetched from the server, with the text blocks bound |
| 22 | for editing in place — which is what keeps a deck the same deck wherever |
| 23 | it is seen. |
| 24 | |
| 25 | A deck comes back out as a PDF. Press **Export PDF** on a deck, say |
| 26 | whether the speaker notes should come too, and the export runs as a job |
| 27 | you watch on a page of its own — one 16:9 page per slide, the text as |
| 28 | real text you can search and copy, the pictures embedded. It is the same |
| 29 | paint list the thumbnails are drawn from, set in the same faces the |
| 30 | browser sets a slide in, so the file is the deck rather than a photograph |
| 31 | of it. With no object store bound the file is held for an hour and served |
| 32 | from the instance; with a bucket it goes there and the download is a |
| 33 | signed URL. |
| 34 | |
| 35 | The editor cannot hold a comment or show you an older version. And the |
| 36 | first deployment has no object store bound, so uploads there answer `503` |
| 37 | and the image field says so on the page. |
| 38 | |
| 39 | A deck can also be built entirely by a program. The drive API in |
| 40 | [`docs/api.md`](docs/api.md) has every endpoint and every operation, and an |
| 41 | agent is a principal of its own with its own token — its writes are badged |
| 42 | as the agent's, never laundered into a person's name. Every screen but the |
| 43 | editor works with JavaScript switched off, and every string a person can |
| 44 | read is a catalog key. |
| 45 | |
| 46 | Sign-in is a magic link, and the instance only mails one to an address in |
| 47 | `SLIDES_ALLOWED_ADDRESSES`. |
| 48 | |
| 49 | ## Running it |
| 50 | |
| 51 | ```sh |
| 52 | ./scripts/run-dev.sh # http://localhost:8091 |
| 53 | ``` |
| 54 | |
| 55 | That builds the binary and runs it against a scratch database that starts |
| 56 | empty every time. Sign in with any address you like: the magic link is |
| 57 | logged to the terminal rather than mailed, but it is the real flow, not a |
| 58 | stand-in. |
| 59 | |
| 60 | Images go in an object store, never in the database and never on the |
| 61 | instance's own disk. Development uses a directory beside the database, so |
| 62 | uploading works out of the box; a real deployment gets its bucket from |
| 63 | `CARLOS_STORE_*`, and with no bucket bound the upload endpoints answer |
| 64 | `503` and the editor says so rather than half-working. |
| 65 | |
| 66 | **Deploying with a bucket needs one thing this app cannot do for you:** |
| 67 | uploads are presigned PUTs made by the browser, straight to the store, so |
| 68 | the bucket's CORS policy has to allow `PUT` from the instance's origin |
| 69 | with the `Content-Type` header. Without it uploads fail in the browser |
| 70 | with nothing in this app's log — the request never reaches it. |
| 71 | [`docs/api.md`](docs/api.md) has the details. |
| 72 | |
| 73 | ## The editor's bundle |
| 74 | |
| 75 | The editor is the one screen that needs JavaScript. Its bundle is built by |
| 76 | hand and committed: |
| 77 | |
| 78 | ```sh |
| 79 | cd fixtures && npm ci && node build-bundles.mjs |
| 80 | ``` |
| 81 | |
| 82 | That writes `internal/slides/static/editor.<hash>.js`, deletes the |
| 83 | previous hashed file, and the Go binary embeds whatever it finds there. |
| 84 | Nothing builds it at run time and CI does not commit it for you, so the |
| 85 | moment you change a `.mjs` under `fixtures/` the committed bundle is out |
| 86 | of date until you rebuild and commit it. The gate rebuilds and diffs, so |
| 87 | you find out before the runner does. |
| 88 | |
| 89 | Uploads need `https` (or `localhost`): the browser hashes the file with |
| 90 | `crypto.subtle` before asking for a signed PUT, and that API only exists |
| 91 | in a secure context. On a plain `http` host that is not localhost the |
| 92 | image field cannot upload, and nothing in this app's log will say why. |
| 93 | |
| 94 | ## The fixtures rule |
| 95 | |
| 96 | `fixtures/` also holds a second implementation of the deck's operations, |
| 97 | in JavaScript, because the browser has to write the same document the Rust |
| 98 | core reads. `fixtures/data/*.json` are op batches; `node generate.mjs` |
| 99 | runs each one through the JS ops and writes the resulting Yjs update and |
| 100 | the JS view of it into `fixtures/vectors/`. |
| 101 | `internal/slides/core/vectors_test.go` loads those same bytes through the |
| 102 | core and requires the same view. |
| 103 | |
| 104 | So change an op and you change both sides, or neither: |
| 105 | |
| 106 | ```sh |
| 107 | node fixtures/generate.mjs # then commit what moves |
| 108 | ``` |
| 109 | |
| 110 | Never edit a vector to make a test pass. A vector that was quietly |
| 111 | adjusted leaves the Go test comparing the core against a JS implementation |
| 112 | that no longer exists — it passes, and the two sides have already drifted. |
| 113 | |
| 114 | ## The gate |
| 115 | |
| 116 | ```sh |
| 117 | ./.amadan/ci.d/10-gate |
| 118 | ``` |
| 119 | |
| 120 | One script, run by CI and by you: rustfmt, clippy, the wasm build and a |
| 121 | `cmp` against the committed artifact, the Rust tests, then the JS half |
| 122 | (`npm ci`, `node --test`, the vectors regenerated and the bundle rebuilt, |
| 123 | both diffed against what is committed), then `make ci` (vet, gofmt, Go |
| 124 | tests, migration check). `make ci` on its own is the fast loop while you |
| 125 | work; run the whole gate before you push. |
| 126 | |
| 127 | It needs Node 20 or newer with `npm`, and `npm ci` wants the registry |
| 128 | reachable while it runs. [`.amadan/README.md`](.amadan/README.md) says why |
| 129 | that trade is worth making and what each step catches. |
| 130 | |
| 131 | ## The browser drive |
| 132 | |
| 133 | `internal/slidestest/browser_test.go` walks the app in a real Chromium and |
| 134 | is loud about any console error, failed request, or JS value that leaked to |
| 135 | the screen: |
| 136 | |
| 137 | ```sh |
| 138 | go test -tags browser ./... |
| 139 | ``` |
| 140 | |
| 141 | It needs a Chromium on `PATH`, at `$RASTRILLO_CHROME`, or in a Playwright |
| 142 | cache, and **a skip is not a pass**: with no browser it fails, unless |
| 143 | `RASTRILLO_BROWSER_OPTIONAL=1` makes the skip a deliberate, visible choice. |
| 144 | It is not part of the plain suite or of the gate. |
| 145 | |
| 146 | ## Where the reasoning lives |
| 147 | |
| 148 | The design is `docs/superpowers/specs/2026-09-07-slides-design.md`. The |
| 149 | plans are `docs/superpowers/plans/2026-09-07-slice-1-live.md` (sign-in, |
| 150 | viewer, sharing, presenting), `2026-09-07-slice-2-editor.md` (the editor |
| 151 | and collaboration) and `2026-09-08-slice-3-export.md` (the layout engine, |
| 152 | thumbnails and PDF export). `AGENTS.md` is the working file: the seams, the |
| 153 | traps, how to deploy. Read it before you change anything here. |
| 154 | |