oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

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