Files
OneCAD-Draft/web/odoo/README-odoo.md
T

12 KiB
Raw Blame History

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=<VERSION> 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 <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 !== 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 <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.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/<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).

The framework opens the iframe with ?document=<recordId> (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:<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

  1. esbuild-compile index.ts/bridge.ts (esm, es2022) next to explorer.js.
  2. Bump ODOO_GLUE_VERSION + ?v= literals + injection snippet + bundleVersion + manifest version (§1).
  3. Rewrite the built index.html (strip SW registration, /app/ links) and inject the §2 snippet after the loading markup.
  4. Verify gate: no absolute refs, no SW, odoo/index.js + odoo/bridge.js + odoo/explorer.js present in static/onecaddraft/odoo/.
  5. Rust side (main agent's odoo bridge module) must: dispatch onecaddraft:host-request (payload or params), listen for onecaddraft:host-response, poll window.onecaddraftHostReady until true before dispatching odoo.* methods, and never set the flag itself.

9. Deviations from the FieldCraft glue (all intentional)

  1. Save flow replaces services_odoo.rs's take_writes drain: this app has no Rust services layer; sys.rs::download_bytes already honors a page hook, so the air gap is the openCadStudioSaveFile bridge hook (§3.3).
  2. Deep-link staging replaces onecadsimOdoo.bootFile + ui.file: no such Rust seams exist here; the resolver uses ocs_web_drop_file (primary) / control open (fallback) and clears ?document= afterwards.
  3. appSerialize control-channel fallback: window.onecaddraft is not installed in v1, so the no-bytes save/export path serializes via control {"op":"save","allow_lossy":true} (reply data_base64).
  4. Extensions/identity: .fieldcraft→.dxf/.dwg ownership, RUST_ASSET_EXTS = ["obj","landxml"] (landxml import is wired in src), bind prefix one-cad-draft:draft:bind:, tags one-cad-draft(-host), events onecaddraft:*, style prefix ocd-.
  5. ODOO_GLUE_VERSION const added (docs/tooling anchor; import specifiers stay literal for the assembler's lockstep sed).