forked from 1b-agent/opencadstudio-upstream
206 lines
12 KiB
Markdown
206 lines
12 KiB
Markdown
# 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>`:
|
||
|
||
```html
|
||
<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). |
|
||
|
||
## 4. Deep link
|
||
|
||
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).
|