# INKBOARD — master specification (authoritative for every phase)

You are building a minimalistic single-user whiteboard (think a tiny Miro / diagrams.net) as a Rust workspace:
a WebAssembly whiteboard ENGINE with a minimal browser frontend, and a Rust + tokio BACKEND on Postgres. Both are
built test-first: every phase that adds behaviour first writes the interfaces and a failing test suite, then makes
it pass. Files from earlier phases are on disk: build on them; do not rewrite what already passes its tests.

## Workspace layout (fixed)
```
Cargo.toml                 # [workspace] members = ["model", "server", "engine"]; resolver = "2"
model/                     # PURE Rust library, no IO, compiles natively AND for wasm32: the domain
model/src/lib.rs           #   pub mod board, element, ops, geometry, history, codec;
model/src/element.rs       #   Element { id: Uuid, kind: Kind, x, y, w, h, z, style, text }  Kind: Rect|Ellipse|Line|Arrow|Text|Sticky
model/src/board.rs         #   Board { id, name, version: u64, elements: Vec<Element> } + queries (by id, bbox, hit-test)
model/src/ops.rs           #   Op { Add(Element) | Move{id,dx,dy} | Resize{id,x,y,w,h} | SetText{id,text} | SetStyle{id,style} | Delete{id} | Reorder{id,z} }
                           #   fn apply(&mut Board, &Op) -> Result<Op /*inverse*/, OpError>; ops are the ONLY way to change a board
model/src/history.rs       #   History { undo: Vec<Op>, redo: Vec<Op> } with push/undo/redo over apply
model/src/geometry.rs      #   Point, Rect, hit-testing (point in rect/ellipse/segment tolerance), union bbox, snapping to a grid
model/src/codec.rs         #   serde JSON of Board and Op (versioned: {"v":1,...}); import/export round-trip
server/                    # BINARY: axum 0.7 + tokio + sqlx 0.8 (postgres, runtime-tokio) ; serves the API and the built frontend
server/src/main.rs         #   reads DATABASE_URL, PORT, STATIC_DIR; runs migrations; serves
server/src/lib.rs          #   pub fn app(pool) -> axum::Router  (so tests can drive it with tower::ServiceExt::oneshot)
server/src/db.rs           #   boards table (id uuid pk, name text, version bigint, doc jsonb, updated_at timestamptz)
                           #   ops table (board_id, seq bigint, op jsonb, created_at)  — the op log; doc is the materialised board
server/src/routes.rs       #   GET /api/boards  · POST /api/boards {name} · GET /api/boards/:id · PUT /api/boards/:id {doc}
                           #   POST /api/boards/:id/ops {ops:[Op], base_version} -> {version} (409 on stale base_version)
                           #   GET /api/boards/:id/export (JSON download) · DELETE /api/boards/:id · GET /healthz
server/migrations/*.sql    #   sqlx migrations, embedded with sqlx::migrate!()
server/tests/api.rs        #   integration tests against the real Postgres in DATABASE_URL (each test uses its own board ids)
engine/                    # LIBRARY crate (crate-type = ["cdylib", "rlib"], NO main.rs) compiled to wasm32 with wasm-bindgen; ALSO compiles natively for tests
engine/src/lib.rs          #   pub mod scene, viewport, tools, input, render(wasm32 only), bridge(wasm32 only)
engine/src/scene.rs        #   Scene { board: Board, history, selection: Vec<Uuid> } — all edits go through model::ops
engine/src/viewport.rs     #   Viewport { pan: Point, zoom: f64 } world<->screen, zoom-at-cursor, fit-to-content
engine/src/tools.rs        #   Tool: Select | Rect | Ellipse | Line | Arrow | Text | Sticky ; drag state machine (press/move/release)
engine/src/input.rs        #   pointer/keyboard events (plain structs, no web-sys) -> tool state machine -> Ops
engine/src/render.rs       #   wasm32: draw the scene onto a CanvasRenderingContext2d (web-sys) incl. selection handles
engine/src/bridge.rs       #   wasm32: `#[wasm_bindgen(start)] pub fn start()` mounts the canvas, wires DOM events and the /api calls (gloo-net);
                           #   #[wasm_bindgen] exports: wb_init(canvas_id), wb_pointer(kind,x,y,buttons), wb_key(code),
                           #   wb_set_tool(name), wb_set_text(id,text), wb_state() -> JSON, wb_load(json), wb_apply(op_json),
                           #   wb_undo(), wb_redo(), wb_dirty_ops() -> JSON (and clears), wb_fit()
engine/index.html          #   trunk entry: `<link data-trunk rel="rust" />` (the lib is the only artifact) + the minimal frontend:
                           #   toolbar (tools, undo/redo, delete), sticky note text area, board list + New/Save/Load/Export
                           #   (plain HTML + a few lines of inline JS that call the wasm exports; NO frontend framework)
engine/Trunk.toml          #   [build] target="index.html" release=true public_url="./"  dist -> engine/dist
docker-compose.yml         #   postgres:16 + server (STATIC_DIR=engine/dist)
docs/design.md             #   the formalised domain model and the API contract (phase 1)
```
`cargo test --workspace` must run NATIVELY (no wasm, no browser): engine tests exercise scene/viewport/tools/input
with plain structs; everything that touches web-sys is behind `#[cfg(target_arch = "wasm32")]`.
`cargo check -p engine --target wasm32-unknown-unknown` must pass. `trunk build --release` in `engine/` produces
`engine/dist/`. Server tests need `DATABASE_URL` (the harness provides a live Postgres; tests create what they need).

## Pinned dependencies (copy exactly; the cargo cache is warm for these)
model:   serde = { version = "1", features = ["derive"] }, serde_json = "1", uuid = { version = "1", features = ["v4", "serde", "js"] }, thiserror = "1"
server:  axum = "0.7", tokio = { version = "1", features = ["full"] }, sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "json", "migrate"] },
         serde/serde_json as above, uuid as above, tower = "0.5", tower-http = { version = "0.5", features = ["fs", "cors", "trace"] },
         tracing = "0.1", tracing-subscriber = "0.3", chrono = { version = "0.4", features = ["serde"] }, anyhow = "1"
         [dev-dependencies] reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }, http-body-util = "0.1"   (rustls: there is no OpenSSL dev package on the box)
engine:  [lib] crate-type = ["cdylib", "rlib"];  model = { path = "../model" }, serde/serde_json/uuid as above, wasm-bindgen = "0.2", js-sys = "0.3",
         web-sys = { version = "0.3", features = ["Window","Document","Element","HtmlCanvasElement","CanvasRenderingContext2d","HtmlElement","HtmlTextAreaElement","HtmlInputElement","HtmlSelectElement","PointerEvent","KeyboardEvent","Event","EventTarget","TextMetrics","DomRect","console"] },
         gloo-net = { version = "0.6", features = ["http"] }, gloo-events = "0.2", wasm-bindgen-futures = "0.4", console_error_panic_hook = "0.1"
Use `sqlx::query(...)`/`query_as` with runtime binding (NOT the compile-time `query!` macros — they need a DB at build time).

## Product scope (single user, no auth, no realtime)
- Shapes: rectangle, ellipse, line, arrow (line with head), text label, STICKY NOTE (a coloured note with multi-line text).
- Create by choosing a tool and dragging on the canvas; select by click / marquee; move by drag; resize by 8 handles;
  edit text of Text/Sticky by double-click (a textarea overlay); delete key; undo/redo (Ctrl+Z / Ctrl+Y); z-order
  (bring to front / send to back); pan by space+drag or middle button; zoom by wheel at cursor; fit-to-content button;
  snap to a 10px grid while dragging.
- Boards: list, new (name), open, save (PUT whole doc, version check), autosave every 5 s of dirty ops via POST ops,
  export JSON, delete. Reloading the page and opening the board shows exactly what was saved.
- Rendering: canvas 2D, sticky = filled rounded rect + wrapped text; selection = blue outline + handles; a light grid.

## Determinism and testing
- IDs are uuid v4 in the app, but tests build elements with fixed ids. Every model/engine behaviour has a native
  test; the server has integration tests through `app(pool)` (tower oneshot) and one end-to-end test that spawns the
  server on a random port and uses reqwest. The TDD gate per phase is: tests_total grows, the new tests are red on
  `todo!()` first, then green — the harness verifies test counts and pass/fail after every phase.
- No `unsafe`; `cargo clippy` errors 0; f64 for geometry; version numbers are u64.
FOCUS_FILES: server/src/lib.rs, server/src/db.rs, server/src/routes.rs
SCOPE: server/src/*, server/migrations/*

# PHASE 4 (E3) — implement the backend, make the API tests green (budget ~60 min)

Implement db.rs with `sqlx::query`/`query_as` (runtime binding, no macros), then the handlers, one route per edit,
running `cargo test -p server <name>` after each. The op log: `POST /api/boards/:id/ops` loads the doc, checks
`base_version == version` (else 409), applies each op through `model::ops::apply`, stores the ops with increasing
`seq`, writes the new doc and `version + ops.len()`, all in one transaction. Static files: `tower_http::services::
ServeDir` at `/` from STATIC_DIR with index fallback (used in phase 8). Do NOT weaken tests. A compiling workspace at
the end is REQUIRED. Acceptance (`verify`): every server test passing, clippy errors 0. Summary: what each route does.
