12 KiB
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
ODOO_GLUE_VERSIONand the?v=literals insideindex.ts/explorer.js(they cannot reference the const — import specifiers are static);- the
?v=<VERSION>in the index.html injection snippet below; - the addon's
bundleVersion(registration JS) and__manifest__.pyversion.
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 <body>:
<script type="module">
try { if (window.parent !== window) { import("./odoo/index.js?v=<VERSION>").catch((e) => console.warn("OneCAD Draft odoo bridge:", e)); } } catch (e) { /* standalone */ }
</script>
- The
window.parent !== windowguard 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
<VERSION>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.jsimport. - Compile step:
npx esbuild web/odoo/index.ts web/odoo/bridge.ts --format=esm --target=es2022(erasable-syntax-only TS; no tsc needed) → shipodoo/index.js,odoo/bridge.js,odoo/explorer.jsverbatim.
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/<rid>/<name> in place, /odoo/pending/<token>/<name> 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
<a download> 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=<recordId> (plus its ?v=
cache buster). Boot order (ordering law, unchanged from FieldCraft):
waitForHost()(ping ×100 ms, 20 s) → host up or standalone;markHostReady()— unblocks the Rust side first;bridge.openDeepLink()reads the record (300 s), storespendingOpen, firesonecaddraft:document-opened, refreshes the binding;host-readyevent +window.onecaddraftHoststate;- 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:
wasmBindings.ocs_web_drop_file(name, bytes)(or thewindow.global alias) — primary, binary-safe;- else control
{"op":"open", name, data_base64}— base64 path,busyretried 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:<stem> |
document name (.dxf/.dwg stripped) → {recordId, name, updatedAt} — write-back binding. |
one-cad-draft:documents:last-folder:<mode> |
explorer last folder per mode (save/open/asset/export). |
7. Explorer
Injected one-<style> dark CAD palette (ocd-explorer-style, --ocd-*
vars, ocd-* classes), dialog over the canvas. Modes: save (tree + name,
.dxf chip), open (whole drive, files filtered to dxf/dwg),
export (name → Exports). asset stays in code for the family contract but
is not surfaced in v1 (its filter set mirrors the Rust asset exts:
obj, landxml — LANDXMLIMPORT is wired in src/app/commands/display.rs).
8. Assembler checklist
- esbuild-compile
index.ts/bridge.ts(esm, es2022) next toexplorer.js. - Bump
ODOO_GLUE_VERSION+?v=literals + injection snippet +bundleVersion+ manifest version (§1). - Rewrite the built
index.html(strip SW registration,/app/links) and inject the §2 snippet after the loading markup. - Verify gate: no absolute refs, no SW,
odoo/index.js+odoo/bridge.js+odoo/explorer.jspresent instatic/onecaddraft/odoo/. - Rust side (main agent's odoo bridge module) must: dispatch
onecaddraft:host-request(payload or params), listen foronecaddraft:host-response, pollwindow.onecaddraftHostReadyuntil true before dispatchingodoo.*methods, and never set the flag itself.
9. Deviations from the FieldCraft glue (all intentional)
- Save flow replaces
services_odoo.rs's take_writes drain: this app has no Rust services layer;sys.rs::download_bytesalready honors a page hook, so the air gap is theopenCadStudioSaveFilebridge hook (§3.3). - Deep-link staging replaces
onecadsimOdoo.bootFile+ui.file: no such Rust seams exist here; the resolver usesocs_web_drop_file(primary) / controlopen(fallback) and clears?document=afterwards. appSerializecontrol-channel fallback:window.onecaddraftis not installed in v1, so the no-bytes save/export path serializes via control{"op":"save","allow_lossy":true}(replydata_base64).- Extensions/identity:
.fieldcraft→.dxf/.dwgownership,RUST_ASSET_EXTS = ["obj","landxml"](landxml import is wired in src), bind prefixone-cad-draft:draft:bind:, tagsone-cad-draft(-host), eventsonecaddraft:*, style prefixocd-. ODOO_GLUE_VERSIONconst added (docs/tooling anchor; import specifiers stay literal for the assembler's lockstep sed).