# OneCAD Draft — Odoo Documents glue (`web/odoo/`) Integration contract for the addon assembler and future maintainers. This trio is the Odoo Documents host glue for the OpenCADStudio fork (`one_cad_draft` addon, wasm_app_framework iframe). It is a structural 1:1 port of the proven FieldCraft glue (`OneCAD-Simulation/apps/fieldcraft-web/src/odoo/` — see worklog tasks 4-c / 6-b), adapted to this app's iced/wasm seams. The host wire protocol is **unchanged** from the family contract. | File | Role | |---|---| | `index.ts` | Boot glue: bridge singleton + explorer wiring, host ping gate, `?document=` deep link, wasm-module resolver, `host-ready` signalling. | | `bridge.ts` | Bridge client (postMessage to the framework shell) + Rust facade (CustomEvents) + save air-gap hook + control-channel helpers. | | `explorer.js` | Vanilla-JS Documents explorer dialogs (`save` / `open` / `export`; `asset` kept for the family contract, not surfaced in v1). Plain JS — never compiled. | --- ## 1. Version constants (bump in LOCKSTEP) ``` ODOO_GLUE_VERSION (bridge.ts) = "20.0.1.0.0" ?v= literal in index.ts = "./bridge.js?v=20.0.1.0.0", "./explorer.js?v=20.0.1.0.0" ?v= literal in explorer.js = "./bridge.js?v=20.0.1.0.0" ``` Law (family precedent — the 7-day Odoo static max-age makes stale-bundle failures real): every build bumps **all** of 1. `ODOO_GLUE_VERSION` and the `?v=` literals inside `index.ts` / `explorer.js` (they cannot reference the const — import specifiers are static); 2. the `?v=` in the index.html injection snippet below; 3. the addon's `bundleVersion` (registration JS) and `__manifest__.py` version. The assembler owns the substitution (sed-style, one value everywhere). `bridge.ts` exports `ODOO_GLUE_VERSION` as the single source of truth for docs/tooling. ## 2. index.html injection snippet (assembler; exact) Inject into the app's `index.html` (built from `web-app.html`) **after the loading markup** (after the `#loading` div / splash scripts), inside ``: ```html ``` * The `window.parent !== window` guard keeps standalone/local runs glue-free (native downloads, native pickers — the glue is inert by design there too, but not loading it at all is cheaper). * Replace `` with the current lockstep version (§1). * The assembler also rewrites the built page for the addon bundle (strip the service-worker registration, rewrite `/app/`-absolute asset links to `/one_cad_draft/static/onecaddraft/`); the snippet must survive that rewrite as a relative `./odoo/index.js` import. * Compile step: `npx esbuild web/odoo/index.ts web/odoo/bridge.ts --format=esm --target=es2022` (erasable-syntax-only TS; no tsc needed) → ship `odoo/index.js`, `odoo/bridge.js`, `odoo/explorer.js` verbatim. **Mount-order tolerance (required):** the glue may load after the wasm boots (iced boots fast) or before it. The bridge handles glue-first (Rust polls `window.onecaddraftHostReady`); the deep-link resolver in `index.ts` handles wasm-first/late by polling the wasm entry points for up to 30 s (§5). ## 3. Window API surface ### 3.1 Glue ⇄ Odoo shell (postMessage, same-origin parent) ``` request : {source:"one-cad-draft", kind:"odoo-documents", id, method, payload} reply : {source:"one-cad-draft-host", kind:"odoo-documents", id, ok, result|error:{message}} ``` Host methods (implemented by `wasm_app_action.js`): `ping list read write delete usage folders createFolder listFolder saveExport stat`. Bulk methods (`read write saveExport`) use a 300 s timeout; all others 60 s. 128 MB raw-byte cap enforced client-side before base64 (host mirrors it). ### 3.2 Rust facade (window CustomEvents — the Rust odoo bridge module) | Event | Direction | Detail | |---|---|---| | `onecaddraft:host-request` | Rust → glue | `{id:number, method:string, payload?:object}` — the glue also accepts `params` (alias, kept from the FieldCraft wire). Known host methods pass through; orchestration methods: `saveAs`, `open`, `export`, `importAsset`; Rust-only methods: `odoo.pickOpen`, `odoo.pickProgram`, `odoo.pickSave`, `odoo.write` (`/odoo/doc//` in place, `/odoo/pending//` first save), `odoo.saveExport`, `odoo.import`. FIFO, one at a time, every id answered exactly once. | | `onecaddraft:host-response` | glue → Rust | `{id, ok, result?, error?{message}}` | | `onecaddraft:host-ready` | glue → Rust | `{host:boolean, documentId, name}` — fired once after the ping gate and the deep-link read. | | `onecaddraft:document-opened` | glue → Rust | `{documentId, name, dataB64}` — deep-link record read. | **Ready flag law (keep exactly):** the GLUE sets `window.onecaddraftHostReady = true` once its `host-response` listener is attached and the boot attempt finished (host answered or timed out) — BEFORE deep-link consumption. The Rust side POLLS/RETRIES until the flag is set; it must never set or clear it. Late-booting Rust reads the same state synchronously at `window.onecaddraftHost.{ready, host, documentId, name}` plus `consumePendingOpen()` (deep-link payload, once → null). `window.onecaddraft` (reserved): `request("odoo.document.bytes" | "odoo.document.info")` facade the bridge calls when a flow arrives without bytes. v1 does not install it — the bridge falls back to the control channel (§5). `window.onecaddraftOdoo.bootFile` (reserved): FieldCraft's Rust staging seam; not installed here, the resolver never depends on it. ### 3.3 Save air gap — `window.openCadStudioSaveFile(name, bytes)` Registered by `bridge.ts` (constructor, in-frame only). The wasm app already checks it first: `src/sys.rs::download_bytes` (line 723) calls `save_hook().call2(&window, &name, &Uint8Array)` and returns before the `` path, and `src/app/update/file.rs:4154` disables the File System Access picker while the hook exists. So while the glue is loaded in the iframe, **bytes can never reach a browser download** — Save, Save As, save-before-close and every export (PDF/CSV/PNG/ZIP/…) arrive here. Routing (bytes are ALWAYS consumed; failures are logged, never downloaded): | Case | Action | |---|---| | `.dxf`/`.dwg` name, bound record (`localStorage` bind) | in-place write-back `write {recordId, name}`; bind refreshed. | | `.dxf`/`.dwg` name, unbound | explorer SAVE dialog → `write {name, folderId}` (Projects default via the explorer) → bind → future saves take the write-back path. Cancel = bytes dropped with a console note. | | anything else (obj/stl/pdf/csv/png/zip/…) | `saveExport` → Documents **Exports** folder. | Hook saves are serialized through one promise chain (no stacked dialogs). Over-cap (>128 MB raw) saves are dropped with a warning (family cap law). ### 3.4 App entry points used by the glue (verified in src) | Seam | Where | Used for | |---|---|---| | `wasmBindings.ocs_web_drop_file(name, bytes)` — primary | `src/io/web_drop.rs:116` (`#[wasm_bindgen]`); Trunk publishes the module as `window.wasmBindings` (`docs/automation/web.md`) | deep-link open staging. Binary-safe; drained by the 50 ms `Message::PollWebControl` poll guarded by `opening.is_none()` (`src/app/update/mod.rs:877`). A plain `window.ocs_web_drop_file` global is accepted as an alias. | | `wasmBindings.ocs_control_submit/take` — fallback | `src/app/control/mod.rs:72/89` | deep-link fallback `{"op":"open","name","data_base64"}` (follows `accepted`/`running` via `{"op":"operation","request_id"}`, retries `code:"busy"`; 60 s) and the no-bytes serialization fallback `{"op":"save","allow_lossy":true}` → reply `data_base64` (`mod.rs:1243`). | | `window.openCadStudioSaveFile(name, bytes)` | `src/sys.rs:757 save_hook()` | save air gap (§3.3). | ## 4. Deep link The framework opens the iframe with `?document=` (plus its `?v=` cache buster). Boot order (ordering law, unchanged from FieldCraft): 1. `waitForHost()` (ping ×100 ms, 20 s) → host up or standalone; 2. `markHostReady()` — unblocks the Rust side first; 3. `bridge.openDeepLink()` reads the record (300 s), stores `pendingOpen`, fires `onecaddraft:document-opened`, refreshes the binding; 4. `host-ready` event + `window.onecaddraftHost` state; 5. the staged bytes go through the resolver (§5: drop entry, else control channel), then `?document=` is cleared from the iframe URL (`history.replaceState`; `?v=` and any other params survive). ## 5. Resolver / mount-order tolerance `stageDrawing(name, bytes)` polls for the wasm module (drop entry OR control pair) every 100 ms up to **30 s** — covers wasm-before-glue and glue-before-wasm — then: 1. `wasmBindings.ocs_web_drop_file(name, bytes)` (or the `window.` global alias) — primary, binary-safe; 2. else control `{"op":"open", name, data_base64}` — base64 path, `busy` retried until the deadline. Bytes are consumed by whichever path wins; the glue logs and gives up (never downloads) if neither appears within 30 s. ## 6. localStorage keys | Key | Meaning | |---|---| | `one-cad-draft:draft:bind:` | document name (`.dxf`/`.dwg` stripped) → `{recordId, name, updatedAt}` — write-back binding. | | `one-cad-draft:documents:last-folder:` | explorer last folder per mode (`save`/`open`/`asset`/`export`). | ## 7. Explorer Injected one-`