Files

206 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).