| index f5ce616..8ba8f1f 100644 |
| --- a/docs/superpowers/plans/2026-09-07-slice-1-live.md |
| +++ b/docs/superpowers/plans/2026-09-07-slice-1-live.md |
| @@ -13,7 +13,7 @@ |
| ## Global Constraints |
| |
| - `CGO_ENABLED=0` everywhere; never import `github.com/glebarez/*` or `gorm.io/driver/sqlite`. |
| -- Module path `amadan.net/oficina/slides`; app package `internal/slides`; the five-file shape plus one package per concern (`internal/core`, `internal/deck`, `internal/render`, `internal/present`). |
| +- Module path `amadan.net/oficina/slides`; app package `internal/slides`; the five-file shape plus one package per concern nested under the app package (`internal/slides/core`, `internal/slides/deck`, `internal/slides/render`, `internal/slides/present`), because `rastrillo migration check` requires exactly one directory under `internal/`. |
| - Every owned query goes through the `owned(r)` seam; not-found and not-yours both 404. |
| - Every state-changing route is a POST behind `csrf.Protect`; signed-in routes sit in a chi group with `authPlugin.RequireSession`; `/s/{token}`, `/p/*`, `/r/*` and `/api/*` are mounted outside that group. |
| - Under `rastrillo/auth` the Subject is the email. Never call `sessions.UserID`; map the address to the `users` row first. |
| @@ -44,11 +44,11 @@ |
| | `internal/slides/templates/*.html` | One file per page. | |
| | `internal/slides/static/slides.css` | Slide layout CSS: templates, canvas frame, phone reflow, themes. | |
| | `internal/slides/locales/en.toml` | Every string. | |
| -| `internal/core/core.go`, `internal/core/testdata/slides_core.wasm` | wazero host for the Rust core. | |
| +| `internal/slides/core/core.go`, `internal/slides/core/testdata/slides_core.wasm` | wazero host for the Rust core. | |
| | `core/` | Rust crate `slides-core`: schema, ops, JSON view, ABI. | |
| -| `internal/deck/deck.go` | Go types for the JSON view (`Deck`, `Slide`, `Block`, `Text`), `Parse([]byte)`. | |
| -| `internal/render/render.go`, `themes.go` | `Slide(d, i, vp) template.HTML`, `ThemeVars(id) template.CSS`. | |
| -| `internal/present/hub.go` | In-memory SSE hub keyed by session id. | |
| +| `internal/slides/deck/deck.go` | Go types for the JSON view (`Deck`, `Slide`, `Block`, `Text`), `Parse([]byte)`. | |
| +| `internal/slides/render/render.go`, `themes.go` | `Slide(d, i, vp) template.HTML`, `ThemeVars(id) template.CSS`. | |
| +| `internal/slides/present/hub.go` | In-memory SSE hub keyed by session id. | |
| | `scripts/build-wasm.sh`, `scripts/run-dev.sh` | Build the core; run locally with a logged magic link. | |
| | `.amadan/ci.d/10-gate` | The one gate: cargo test, wasm rebuild and `cmp`, go vet/test/fmt, migration check. | |
| |
| @@ -141,13 +141,13 @@ |
| ### Task 3: Decks: persistence, list, create, sample deck |
| |
| **Files:** |
| -- Create: `internal/slides/decks.go`, `internal/slides/sample.go`, `internal/slides/templates/decks.html`, `internal/slides/templates/deck_new.html`, `internal/slides/templates/deck_settings.html`, `internal/deck/deck.go`, `internal/deck/deck_test.go` |
| +- Create: `internal/slides/decks.go`, `internal/slides/sample.go`, `internal/slides/templates/decks.html`, `internal/slides/templates/deck_new.html`, `internal/slides/templates/deck_settings.html`, `internal/slides/deck/deck.go`, `internal/slides/deck/deck_test.go` |
| - Modify: `internal/slides/models.go`, `internal/slides/migrations/` (generate `0002_decks.sql`), `internal/slides/app.go`, `internal/slides/render.go`, `internal/slides/locales/en.toml` |
| - Test: `internal/slidestest/decks_test.go` |
| |
| **Interfaces:** |
| - Models: `Deck{ID int64; Sqid string (unique); Title string; OwnerID int64 (index); Rung string ("private"|"team"); Theme string; Seq int64; CreatedAt, UpdatedAt}`, `DeckUpdate{ID int64; DeckID int64 (index); Seq int64; Actor string; Bytes []byte; CreatedAt}` with unique `(deck_id, seq)`, `DeckGrant{ID; DeckID (index); UserID (index); Actor string; CreatedAt}`. |
| -- `internal/deck`: `type Deck struct{Meta Meta; Slides []Slide; Assets map[string]Asset}`, `Slide{ID, Layout, Template string; Background Background; Phone, Transition string; Notes Text; Blocks []Block}`, `Block{ID, Type string; Props map[string]any; Region string; Box *Box; Z int; Phone PhoneOverride; Text Text}`, `Text [][]Run`, `Run{T string; B, I, C, H bool; Href string}`, `Box{X, Y, W, H float64}`, `PhoneOverride{Hidden bool; Order *int; Size string; Box *Box}`; `Parse(b []byte) (*Deck, error)`; `(Text).Plain() string`. |
| +- `internal/slides/deck`: `type Deck struct{Meta Meta; Slides []Slide; Assets map[string]Asset}`, `Slide{ID, Layout, Template string; Background Background; Phone, Transition string; Notes Text; Blocks []Block}`, `Block{ID, Type string; Props map[string]any; Region string; Box *Box; Z int; Phone PhoneOverride; Text Text}`, `Text [][]Run`, `Run{T string; B, I, C, H bool; Href string}`, `Box{X, Y, W, H float64}`, `PhoneOverride{Hidden bool; Order *int; Size string; Box *Box}`; `Parse(b []byte) (*Deck, error)`; `(Text).Plain() string`. |
| - `decks.go`: `(*app).openDeck(ctx, deckID) (*core.Handle, *deck.Deck, error)` loads all `DeckUpdate` rows in seq order into a fresh handle (one wazero module per request, closed by the caller); `(*app).applyOps(ctx, d *Deck, actor string, ops []byte) (*deck.Deck, error)` runs `Ops`, inserts `DeckUpdate{Seq: d.Seq+1}` and bumps `Deck.Seq` in one transaction, and notifies the deck hub (Task 6 adds the hub; here a no-op func field `onUpdate func(deckID int64, seq int64)`). |
| - `visibleDecks(r) *gorm.DB` — decks where owner is me, or rung is `team`, or a grant names me. |
| - Sqids: use `github.com/sqids/sqids-go` with a fixed alphabet and min length 8, from the deck's `ID`, as Docs does (check Docs' `sqid.go` for the exact alphabet and reuse it). |
| @@ -163,8 +163,8 @@ |
| ### Task 4: The HTML renderer and themes |
| |
| **Files:** |
| -- Create: `internal/render/render.go`, `internal/render/themes.go`, `internal/render/render_test.go`, `internal/render/testdata/*.golden.html`, `internal/slides/static/slides.css` |
| -- Test: `internal/render/render_test.go` |
| +- Create: `internal/slides/render/render.go`, `internal/slides/render/themes.go`, `internal/slides/render/render_test.go`, `internal/slides/render/testdata/*.golden.html`, `internal/slides/static/slides.css` |
| +- Test: `internal/slides/render/render_test.go` |
| |
| **Interfaces:** |
| - `render.Viewport` is `Desktop` or `Phone`. `render.Slide(d *deck.Deck, index int, vp Viewport) (template.HTML, error)` returns one `<section slides-slide data-layout=.. data-template=.. data-index=..>` with the theme's variables inline as `style="--slides-bg:..;--slides-fg:..;--slides-accent:..;--slides-font-heading:..;--slides-font-body:.."`, blocks in order, canvas blocks positioned with `style="left:%;top:%;width:%;height:%;z-index"`, phone overrides as `data-phone-hidden`, `data-phone-order`, `data-phone-size`, `data-phone-box` and, when `vp == Phone` and the slide is `phone: reflow`, rendered as flow. |
| @@ -196,9 +196,9 @@ |
| ### Task 6: Presentation sessions, presenter view, phone remote |
| |
| **Files:** |
| -- Create: `internal/present/hub.go`, `internal/present/hub_test.go`, `internal/slides/presentation.go`, `internal/slides/templates/present.html`, `internal/slides/templates/present_notes.html`, `internal/slides/templates/remote.html`, `internal/slides/templates/remote_pair.html`, `internal/slides/static/present.js` (under 120 lines: keyboard, swipe, SSE apply; the page is complete without it) |
| +- Create: `internal/slides/present/hub.go`, `internal/slides/present/hub_test.go`, `internal/slides/presentation.go`, `internal/slides/templates/present.html`, `internal/slides/templates/present_notes.html`, `internal/slides/templates/remote.html`, `internal/slides/templates/remote_pair.html`, `internal/slides/static/present.js` (under 120 lines: keyboard, swipe, SSE apply; the page is complete without it) |
| - Modify: `models.go` (`Presentation{ID; Sqid string (unique); DeckID (index); Actor string; Slide int; Blackout bool; Code string (unique among live); RemoteTokenHash string; StartedAt; EndedAt *time.Time}`), migrations (`0004_presentations.sql`), `app.go`, `viewer.html` (a "Present" button), `decks.html` |
| -- Test: `internal/slidestest/present_test.go`, `internal/present/hub_test.go` |
| +- Test: `internal/slidestest/present_test.go`, `internal/slides/present/hub_test.go` |
| |
| **Interfaces:** |
| - `present.Hub`: `New() *Hub`, `(*Hub).Subscribe(key string) (ch <-chan Event, cancel func())`, `(*Hub).Publish(key string, e Event)`, `Event{Slide int; Blackout bool; Ended bool}`. Buffered channels of 8; a slow subscriber is dropped. Copy the ping/deadline/TTL recipe from Docs' `stream.go` (15 s `: ping`, 30 s write deadline, 5 minute TTL then the client reconnects). |