@condorinst/mobile-bridge 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,10 +8,11 @@ costs, and how to see what it is doing once it is there.
8
8
 
9
9
  You do not install a bundle on a phone. The app downloads it:
10
10
 
11
- 1. You deliver `bundle.zip`, its `bundleSize` and its `bundleHash` (printed by
12
- `condor-mobile-bridge build`) to your Condor contact.
13
- 2. Condor publishes it for **your company**. There is exactly one current
14
- bundle per company.
11
+ 1. The bundle is published for **your company** — by you, with
12
+ `npx condor-mobile-bridge canvas publish` (below), or by your Condor
13
+ contact. There is exactly one current bundle per company.
14
+ 2. The server checks it on upload (2 MB, `index.html` at the root,
15
+ self-contained) and records its `bundleSize` and `bundleHash`.
15
16
  3. Each copy of the app asks for its company's manifest when the Home opens
16
17
  and every time the app returns to the foreground. If the `bundleHash`
17
18
  changed, it downloads the `.zip`, checks size and hash, and swaps the Home
@@ -26,24 +27,72 @@ Consequences:
26
27
  bundle, or shows its default screen, if the download fails a gate — which is
27
28
  why `validate --strict` must pass before you hand anything over.
28
29
 
29
- ## Development and production are separate worlds
30
+ ## Test on a test company, never on your real one
30
31
 
31
- Condor runs a **development** environment next to production. An app build
32
- talks to exactly one of them, and accounts, devices and bundles do not cross
33
- over:
32
+ You work against production — the store app and the production Condor system.
33
+ Since a publish reaches every user of the company at once, ask Condor for a
34
+ **test company** with:
34
35
 
35
- - a development build of the app sees only development accounts and bundles;
36
- - the store app sees only production.
36
+ - a **manager** user for you, to publish (see below);
37
+ - one or more **test accounts** with devices, to log in to the store app on
38
+ your phone.
37
39
 
38
- So, to test on a phone, ask Condor for three things that match: a **test
39
- account** in a **test company** in the development environment, a
40
- **development build** of the app (Android), and to publish your bundle **to that
41
- test company**. Never iterate on a production company: every publish reaches
42
- real people.
40
+ Each iteration on the device is then: build → `canvas publish` to the test
41
+ company → bring the app to the foreground. Do every iteration you can in the
42
+ harness first, and publish to your real company only what passed there.
43
43
 
44
- Each iteration on the device is therefore: build → validate → hand over →
45
- Condor publishes → bring the app to the foreground. That is slow, so do every
46
- iteration you can in the harness first.
44
+ (Condor also runs a development environment for its own staff; a client does
45
+ not need it.)
46
+
47
+ ## Publishing from the command line
48
+
49
+ ```bash
50
+ npx condor-mobile-bridge canvas publish dist/index.html --version 1.4.0
51
+ ```
52
+
53
+ It does what the Condor web admin does, with the same login:
54
+
55
+ 1. It validates the bundle locally (the same gates as `validate`) and refuses
56
+ anything the app would reject — before sending anything.
57
+ 2. It asks for your **company id** and **user id** — the numbers you type in
58
+ the Condor web login — unless you pass `--company` / `--user` or set
59
+ `CONDOR_CANVAS_COMPANY` / `CONDOR_CANVAS_USER`.
60
+ 3. It shows the company, version, size and hash, warns that the bundle goes
61
+ **live at once for every user of the company**, and asks `y/N`.
62
+ 4. It asks for your **password** without echo — there is no flag or variable
63
+ for it, and it is never stored — and, when your company uses two-factor
64
+ login, for the code sent to your e-mail.
65
+ 5. It publishes and prints the result.
66
+
67
+ Each version can be published once per company: publish a fix as a new
68
+ version. `canvas list` shows the bundles of the company, and
69
+ `canvas revoke <id> --fallback-version <v>` takes one back and serves another;
70
+ revoking the current bundle without a fallback leaves the company on the app's
71
+ default screen.
72
+
73
+ You need a **manager** user of the company. The command needs a terminal (it
74
+ prompts), so it does not run in CI; and an AI agent must not run it on its own
75
+ — it publishes to real people and asks for your password.
76
+
77
+ ## Loading the bundle from your machine (development builds)
78
+
79
+ From `nativeVersion` 0.18.0 a **development** build of the app can skip the
80
+ published bundle and load yours straight from your computer, on every launch
81
+ and every return to the foreground — no publish, no other user affected.
82
+
83
+ The address is fixed when Condor builds the app, so use one that is the same on
84
+ every machine: the harness on `localhost`, forwarded over USB (Android).
85
+
86
+ 1. Ask Condor for a development build made with
87
+ `--dart-define=IS_DEV=true --dart-define=WL_BUNDLE_DEV_URL=http://localhost:5173/__condor/client.html`.
88
+ 2. Serve your bundle with the harness — it inlines it like `build` does:
89
+ `npx condor-mobile-bridge dev -i dist/index.html`.
90
+ 3. Forward the port to the phone: `adb reverse tcp:5173 tcp:5173`.
91
+ 4. Rebuild your bundle, bring the app to the foreground: the new bundle loads.
92
+
93
+ The app still applies the 2 MB and self-contained checks; it never caches this
94
+ bundle, so a normal build shows the published one again. Store builds refuse
95
+ the flag.
47
96
 
48
97
  ## Seeing what the bundle does on the phone
49
98
 
@@ -67,6 +116,220 @@ by the app (wrong argument type, unknown command, a request lost while the
67
116
  background service starts) leaves no trace in the console — reproduce it in the
68
117
  harness instead, whose console says what was dropped and why.
69
118
 
119
+ ## What `build` does
120
+
121
+ Generates the **self-contained `.zip` bundle** that the white-label app
122
+ consumes. The app injects the client HTML via `iframe.srcdoc` (origin `null`,
123
+ no base URL) → **relative references do not resolve** and the CSP blocks
124
+ external fetches; therefore the `.zip` must contain **a single `index.html`**
125
+ with inline JS/CSS and media as `data:` URIs.
126
+
127
+ The CLI does this from your build's `dist/`: it reads `index.html`, **inlines**
128
+ `<script src>` (escaping `</script>`) and `<link rel=stylesheet>`, resolves CSS
129
+ `url()` and local `<img>`/icons to `data:` URIs, and packs a `.zip` with
130
+ `index.html` at the root. External refs (`https://…`) are **preserved** (and
131
+ blocked by the app's CSP at runtime).
132
+
133
+ ```bash
134
+ npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
135
+ ```
136
+
137
+ | Option | Description |
138
+ |---|---|
139
+ | `-i, --html <path>` | Input HTML (e.g. `dist/index.html`). Required. |
140
+ | `-o, --out <path>` | Output `.zip` (default: `dist/whitelabel-bundle.zip`). |
141
+ | `--max-bytes <n>` | **Warning** threshold in bytes (default: `2097152` = 2 MB). |
142
+
143
+ Output (for registering the bundle with the backend — `bundleSize`/`bundleHash`):
144
+
145
+ ```
146
+ Built dist/bundle.zip
147
+ entry: index.html (123.4 KB self-contained)
148
+ bundleSize (zip): 45678 bytes
149
+ bundleHash (zip): sha256-<hex>
150
+ ```
151
+
152
+ The hash is **deterministic** (fixed mtime on the zip entry) — the same input
153
+ produces the same `bundleHash`. Above the threshold, the CLI **warns** (does
154
+ not block): remember that **the app rejects** a `.zip` larger than 2 MB and
155
+ falls back to the mock.
156
+
157
+ > ⚠️ **Constraint: single-chunk build.** The inliner is post-build and assumes
158
+ > a `dist` with **one JS + one CSS + assets**. Builds with **code-splitting /
159
+ > ES modules** that `import` across chunks **break** in `srcdoc` (the imports
160
+ > do not resolve). Configure your build to emit a single file:
161
+ > - **Vite:** `vite-plugin-singlefile` (or `build.rollupOptions.output.inlineDynamicImports: true` + `manualChunks: undefined`).
162
+ > - **CRA:** build without code-splitting (no `React.lazy`/dynamic `import()` in routes), single bundle.
163
+ >
164
+ > Inlining inflates `data:` media by ~33% (base64) — size against the 2 MB limit.
165
+
166
+ ## Scripted tests
167
+
168
+ `npx condor-mobile-bridge dev` is also a test target. On the harness page,
169
+ `window.condorDev` drives the simulated app; your bundle runs in the iframe
170
+ under it, as on the phone.
171
+
172
+ | `window.condorDev.…` | Does |
173
+ | --- | --- |
174
+ | `ready` | `true` once the bundle is mounted **and** its handshake was answered — wait for it |
175
+ | `scenario` | the scenario in use, after every override |
176
+ | `log` | everything sent, answered and dropped: `{ at, kind, command?, key?, detail, args?, payload? }[]`. `detail` is a shortened line; `args` (sent) and `payload` (reply) are complete |
177
+ | `setStatus({ internet: false })` | changes `systemStatus` and emits it, as the app does; without internet `sensorLog` fails, and it recovers with `{ internet: true }` |
178
+ | `setConnected(deviceId, true \| false)` | starts or ends a device's BLE session: `deviceConnection` (if you sent `add`), the `deviceLightSensors` stream (if `realTime` is on) and the sync's `deviceNotification` steps, 200 ms apart, ending in `pipeline_done_notification`. As on the phone, the session then **ends by itself** — unless `realTime` is on for that device, which keeps it open until `realTime` goes off |
179
+ | `emit(key, payload)` | any native event, typed as in `docs/bridge-contract.md` — e.g. `emit('language', { deviceLocale: 'pt-BR', appLanguage: 'pt', resolvedLanguage: 'pt' })` |
180
+ | `setConfirmations('accept' \| 'decline' \| 'ask')` | answers the app's confirmation dialog for you |
181
+ | `pressBack()` | the OS back button |
182
+
183
+ URL parameters set the same things before the bundle loads:
184
+ `?scenario=offline`, `?nativeVersion=0.17.0`, `?confirm=accept`,
185
+ `?latency=0`, `?panel=0`. A `?scenario=` in the URL **replaces** a
186
+ `--scenario file.json` given to the CLI (the file is not loaded at all), so
187
+ with a file leave `scenario` out of the URL. The CLI equivalents are `--latency <ms>` (`0` makes
188
+ a test suite several times faster) and `--no-panel` (the floating MOCK panel
189
+ covers the bottom of your page and can swallow clicks).
190
+
191
+ **Your own scenario.** `--scenario edge.json` takes a file that extends a named
192
+ scenario; every field is optional:
193
+
194
+ ```json
195
+ {
196
+ "extends": "default",
197
+ "nativeVersion": "0.17.0",
198
+ "latencyMs": 0,
199
+ "serviceStartMs": 4000,
200
+ "logPeriodS": 300,
201
+ "firstSessionAfterMs": null,
202
+ "sessionFailAt": "get_data_log",
203
+ "history": { "days": 14, "dockHours": [7], "gapHours": [2, 3], "gapDates": ["2026-10-18"] },
204
+ "sensorLogFailOnRequest": 3,
205
+ "userDeviceError": "NETWORK",
206
+ "sensorLogError": "INVALID_TIME_RANGE",
207
+ "systemStatus": { "bluetooth": false, "missingEssentialPermissions": ["bluetoothScan"] },
208
+ "devices": { "lastUpdate": 0, "devicesConfig": [], "devicesTelemetry": [], "devicesUserConfig": [] }
209
+ }
210
+ ```
211
+
212
+ `devices` is the `userDevice` payload to answer (`UserDevicePayload`).
213
+ `firstSessionAfterMs` is when the first device starts a BLE session: `3000` in
214
+ `default`, absent in every other scenario; `null` turns it off.
215
+ `systemStatus` is merged into the base scenario's. `sessionFailAt` makes every
216
+ simulated sync fail at that stage (`<stage>_error`, no
217
+ `pipeline_done_notification`); without internet syncs fail at
218
+ `send_data_log` anyway. A pending `syncTime`/`deleteMemory` shows up as its step
219
+ in the next session. A failed sync also puts an English text in
220
+ `systemStatus.deviceSyncErrorById` (and `deviceSyncError`) and emits it; the
221
+ next successful sync of that device clears it.
222
+
223
+ `history` shapes the simulated `sensorLog` (`HistoryOptions`): `days` before
224
+ now, `dockHours` (local hours with `off_wrist: 0` and no light), `gapHours`
225
+ (local hours with no log, every day) and `gapDates` (whole local dates with no
226
+ log). `sensorLogFailOnRequest: N` makes the Nth `sensorLog` request fail once
227
+ with `Connection failed` — a failure in the middle of a sweep.
228
+
229
+ **The data the harness serves.** Two devices (`twoDevices()`: ActLumus 4534 /
230
+ id 9001 set to the wrist, ActLumus Plus 5120 / id 9002 set to the necklace).
231
+ Their `sensorLog`: `history.days` (14 by default; set it to 30 for a 30-day
232
+ report) × 24 h back from **now** (not from midnight), one
233
+ log per `logPeriodS` (60 s by default); `body_part` is the device's
234
+ `deviceBodyPart`; worn (`off_wrist: 1`) all day except the dock hour 07:00–08:00
235
+ local (`off_wrist: 0`); light follows a daylight curve on the browser's local
236
+ clock — deterministic for a given timestamp, so a test can recompute what a
237
+ chart should show. The `bodyPart` filter of a query is not simulated.
238
+ `deviceLightSensors` arrives once per second.
239
+
240
+ **Other harness behaviour worth knowing in a test:**
241
+ - `language` is answered from the **browser's** `navigator.language`, with
242
+ `appLanguage: null`. Fix it in a test with
243
+ `condorDev.emit('language', …)` before asserting on text.
244
+ - A declined confirmation (`?confirm=decline`) produces nothing: no ack, no
245
+ event — as on the phone.
246
+ - With `nativeVersion` null the handshake resolves only after the shell's 2 s
247
+ relay timeout, so such a test starts 2 s later.
248
+ - The harness has no fake clock: a time-out of yours (45 s for a declined
249
+ confirmation, minutes for a lost sync) belongs in a unit test with your test
250
+ runner's fake timers, not in a browser test.
251
+ - Event order is not the phone's: no order between events is guaranteed there
252
+ either (`docs/bridge-contract.md`, "Emission cadence"), so don't let a test
253
+ depend on it.
254
+ - `condorDev.log` keeps the **last 500** entries: read it as you go in a long
255
+ test. The
256
+ fixtures are exported from `@condorinst/mobile-bridge/dev` (`twoDevices`,
257
+ `sharedSerial`, `systemStatus`, `sensorLogPage`, `lightSensorsReading`,
258
+ `formatLocalTimestamp`) for unit tests without a browser. Pass `sensorLogPage`
259
+ the device's `deviceId` (`{ deviceId }`); without it `device_id` is `0`.
260
+
261
+ **Like the phone, not like `localhost`.** The harness removes
262
+ `crypto.randomUUID` from your iframe: on `localhost` it would exist, and on the
263
+ phone it probably does not.
264
+
265
+ A test with `puppeteer-core` and the Chrome already installed (`puppeteer`
266
+ works the same and downloads its own browser):
267
+
268
+ ```js
269
+ import puppeteer from 'puppeteer-core';
270
+ import { spawn } from 'node:child_process';
271
+
272
+ const port = 5180;
273
+ const cli = 'node_modules/@condorinst/mobile-bridge/dist/cli.js';
274
+ const dev = spawn('node', [cli, 'dev', '-i', 'dist/index.html',
275
+ '--port', String(port), '--latency', '0', '--no-panel'], { stdio: 'inherit' });
276
+ for (let i = 0; i < 50; i++) { // wait for the port, not a fixed sleep
277
+ try { await fetch(`http://localhost:${port}/`); break; } catch { await new Promise((r) => setTimeout(r, 100)); }
278
+ }
279
+
280
+ const browser = await puppeteer.launch({
281
+ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', // your Chrome
282
+ });
283
+ const page = await browser.newPage();
284
+ await page.emulateTimezone('Pacific/Kiritimati'); // log_time is local: test far from UTC
285
+ await page.goto(`http://localhost:${port}/?confirm=accept`); // the CLI's --scenario applies
286
+ await page.waitForFunction(() => window.condorDev?.ready);
287
+
288
+ const bundle = page.frames().find((f) => f !== page.mainFrame()); // your page
289
+ await bundle.waitForSelector('[data-device-id]'); // assert on your markup, not on text
290
+
291
+ await page.evaluate(() => window.condorDev.setStatus({ internet: false }));
292
+ await bundle.waitForSelector('[data-offline]');
293
+
294
+ const queries = await page.evaluate(() =>
295
+ window.condorDev.log.filter((e) => e.kind === 'sent' && e.command === 'sensorLog').map((e) => e.args));
296
+
297
+ await browser.close();
298
+ dev.kill(); // node, not npx: no orphan process
299
+ ```
300
+
301
+ The harness does nothing on its own after load, except one session (the first
302
+ device of the `default` scenario connects after 3 s, syncs, and disconnects
303
+ about 4 s later unless `realTime` is on). Internet loss, a
304
+ device disconnecting, or a reading arriving are things your test does through
305
+ `window.condorDev`.
306
+
307
+ ### What `validate` checks
308
+
309
+ Errors make the app reject the bundle; warnings are failures with `--strict`.
310
+ The warnings are **textual**: they match strings and comments too, so do not
311
+ leave these names in your code even unused.
312
+
313
+ | Rule | Severity | Matches |
314
+ | --- | --- | --- |
315
+ | `not-self-contained` | error | `<script src>` or `<link rel="stylesheet">` that is not inline |
316
+ | `too-large` | error | a `.zip` over 2 MB |
317
+ | `unreadable-zip` | error | a file that is not a valid `.zip` |
318
+ | `entry-missing` | error | no HTML entry in the `.zip` |
319
+ | `entry-fallback` | warning | no `index.html` at the root of the `.zip` |
320
+ | `dev-harness-bundled` | error | the dev harness compiled into the bundle |
321
+ | `relative-asset` | warning | a relative `<img src>` (use a `data:` URI) |
322
+ | `web-storage` | warning | `localStorage`, `sessionStorage` |
323
+ | `indexed-db` | warning | `indexedDB` |
324
+ | `cookies` | warning | `document.cookie` |
325
+ | `dynamic-eval` | warning | `eval(`, `new Function(` |
326
+ | `service-worker` | warning | `serviceWorker` |
327
+ | `send-beacon` | warning | `sendBeacon` |
328
+ | `popups` | warning | `window.open(` |
329
+ | `crypto-randomuuid` | warning | `crypto.randomUUID` |
330
+ | `top-navigation` | warning | `window.top` |
331
+ | `external-network` | warning | `fetch(`, `XMLHttpRequest`, `WebSocket(` |
332
+
70
333
  ## Errors after the fact
71
334
 
72
335
  `BridgeClient.send('jsError', { message, stack })` sends an error to Condor's
@@ -74,11 +337,12 @@ logging and crash reporting (rate limited: 20 per 10 s, 4 KB per field). Those
74
337
  are Condor's tools: when you need the errors of a test session, ask your Condor
75
338
  contact, with the test account and the time window.
76
339
 
77
- ## Before you ask for a publish
340
+ ## Before you publish
78
341
 
79
342
  - [ ] `npx condor-mobile-bridge validate -i dist/bundle.zip --strict` exits 0.
80
343
  - [ ] Every harness scenario passes (`default`, `cold-start`, `no-devices`,
81
344
  `shared-serial`, `offline`).
82
- - [ ] You know which environment and which company the bundle is for.
83
- - [ ] You sent `bundleSize` and `bundleHash` with the `.zip`, so the published
84
- bundle can be checked against what you built.
345
+ - [ ] It is going to the **test company** first, with a version number not
346
+ used before in that company.
347
+ - [ ] After publishing, the `bundleHash` that `canvas publish` prints matches
348
+ the one `build` printed.
@@ -1,117 +1,47 @@
1
- # White Label — Pilot Onboarding
1
+ # Canvas — pilot onboarding
2
2
 
3
- Welcome! This is the fast path to building and shipping your **White Label
4
- Home** — the screen that runs inside the Condor app. You build a normal web app;
5
- we package and render it. No Flutter / Android / iOS needed.
6
-
7
- For the full reference, see the [Client Guide](./client-guide.md). This page is
8
- the quick checklist to get a pilot through the whole loop.
9
-
10
- ## What you'll end up with
11
-
12
- A self-contained `bundle.zip` that Condor publishes to your tenant — your Home
13
- renders inside the app, talking to the device through the Condor bridge.
3
+ The steps from zero to your Home live for your company, and who does what. The
4
+ how of each step lives in one place, linked from it.
14
5
 
15
6
  ## Who does what
16
7
 
17
- | Condor provides | You build |
8
+ | Condor provides | You build and run |
18
9
  |---|---|
19
- | The SDK (`@condorinst/mobile-bridge`), guide & example | Your Home web app (HTML/CSS/JS or React) |
20
- | The app that renders your bundle + the device bridge | The `bundle.zip` (via our CLI) |
21
- | The publish channel (admin upload) | Delivering the `.zip` to us |
22
-
23
- ## Prerequisites
24
-
25
- - Node.js 18+
26
- - npm access (the package is public — no credentials needed)
27
-
28
- ## Step-by-step
29
-
30
- ### 1. Install the SDK
31
- ```bash
32
- npm install @condorinst/mobile-bridge
33
- ```
34
- Pin `"@condorinst/mobile-bridge": "^0.21.0"` (beta; auto-updates patches).
35
-
36
- ### 2. Start from the example
37
- Copy [`examples/minimal-home`](../examples/minimal-home) — it already wires the
38
- bridge, renders devices, and builds a bundle.
39
-
40
- ### 3. Build your Home
41
- Talk to the app via `BridgeClient`:
42
- ```ts
43
- import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
44
-
45
- BridgeClient.on(BRIDGE_KEYS.userDevice, (d) => render(d)); // listen first
46
- const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' }); // ack.nativeVersion = capabilities
47
- const r = await BridgeClient.requestUserDevices(); // resends until the list arrives
48
- if (r.status === 'timeout') renderEmptyState(); // no reply in 30 s
49
- BridgeClient.openSettings(); // open native settings
50
- ```
51
- Never ask for the device list with a single `send('userDevice')`: the first
52
- request after the app starts is often lost, and an account with no device gets
53
- no reply. `AGENTS.md` has the full boot sequence.
54
-
55
- ### 4. Test in your browser (no app needed)
56
- ```bash
57
- npx condor-mobile-bridge dev -i dist/index.html
58
- ```
59
- Serves your page inside the same sandbox, shell and policy as the app, with a
60
- simulated app behind it. Check every scenario in the selector at the top:
61
- `default`, `cold-start`, `no-devices`, `shared-serial`, `offline`.
62
-
63
- ### 5. Build the bundle
64
- ```bash
65
- npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
66
- ```
67
- This inlines everything into a single self-contained `index.html` and zips it.
68
- It prints `bundleSize` + `bundleHash`.
69
-
70
- ### 5b. Validate before you deliver
71
- ```bash
72
- npx condor-mobile-bridge validate -i dist/bundle.zip --strict
73
- ```
74
- Applies the same gates the app applies on download, plus a lint for the APIs the
75
- sandbox and the CSP block at runtime. **A non-zero exit means do not deliver.**
76
- Errors are outright rejections by the app; warnings install fine and then
77
- misbehave on the device, which is harder to diagnose.
78
-
79
- ### 6. Deliver
80
- Send `dist/bundle.zip`, with the `bundleSize` and `bundleHash` printed by
81
- `build`, through the channel Condor gave you. We publish it to your company —
82
- first to a **development test company**, because a publish reaches every user
83
- of the company.
84
-
85
- ### 7. Verify on a device
86
- Condor publishes → the app downloads, validates and renders your Home the next
87
- time it comes to the foreground. [Testing on a device](./device-testing.md)
88
- explains the environments, how to inspect the WebView, and where errors go.
89
-
90
- ## Hard rules (or the app rejects the bundle)
91
-
92
- - ✅ Exactly **one `index.html`** at the zip root
93
- - ✅ JS/CSS **inline**, media as **`data:` URIs**, no external/relative refs loaded at runtime
94
- - ✅ **≤ 2 MB** (zipped)
95
- - ✅ **Single-chunk build** (no code-splitting): Vite → `vite-plugin-singlefile`; CRA → no `React.lazy` / dynamic `import()`
96
-
97
- The CLI handles the inlining/zipping for you — just keep your build single-file.
98
-
99
- ## Reference
100
-
101
- - [Client Guide](./client-guide.md) — full walkthrough (bridge API, key catalog, dev harness)
102
- - [Testing on a device](./device-testing.md) — from a published bundle to an inspected WebView
103
- - [Troubleshooting](./troubleshooting.md) — starts from the symptom
104
- - [`examples/minimal-home`](../examples/minimal-home) — runnable minimal client
105
- - [README](../README.md) — API reference
106
-
107
- ## Pilot checklist
108
-
109
- - [ ] SDK installed (`npm i @condorinst/mobile-bridge`)
110
- - [ ] Example runs in the harness (`npx condor-mobile-bridge dev`), in every scenario
111
- - [ ] Home built with `BridgeClient` (handshake + at least one key + one command)
112
- - [ ] Constraints respected (1 index.html, ≤2 MB, self-contained, single-chunk)
113
- - [ ] `bundle.zip` generated by the CLI, and `validate --strict` exits 0
114
- - [ ] `.zip` delivered to Condor
115
- - [ ] Home verified on a device
10
+ | The SDK (`@condorinst/mobile-bridge`), its documentation and example | Your Home: one self-contained web page |
11
+ | The Canvas app that shows your page, and the device bridge | The `.zip` bundle, built and checked with the SDK CLI |
12
+ | A **test company** in production, and a manager user for it | Publishing to the test company, then to your real one |
13
+
14
+ You need Node.js 18+. The package is public on npm: no credentials.
15
+
16
+ ## Steps
17
+
18
+ 1. **Start from the example and the procedure.** Follow
19
+ [`AGENTS.md`](../AGENTS.md), "The golden path": copy
20
+ `examples/minimal-home`, install, build.
21
+ 2. **Build your Home in the harness.**
22
+ `npx condor-mobile-bridge dev -i dist/index.html` runs your page as the app
23
+ does. It must work in every scenario — `AGENTS.md`, "Develop in the harness".
24
+ Remember that your page is the whole screen: navigation, logout and the way
25
+ to the app settings are yours (`AGENTS.md`).
26
+ 3. **Build and validate.** `build` writes the `.zip` and prints its size and
27
+ hash; `validate --strict` must exit 0 — `AGENTS.md`, "The validation loop".
28
+ 4. **Publish to your test company.**
29
+ `npx condor-mobile-bridge canvas publish dist/index.html --version 1.0.0`
30
+ with the manager user of the **test** company. A publish reaches every user
31
+ of that company at once — [`docs/device-testing.md`](./device-testing.md),
32
+ "Publishing from the command line".
33
+ 5. **Check it on a phone** logged in with a test user —
34
+ [`docs/device-testing.md`](./device-testing.md).
35
+ 6. **Publish to your company** with a new version number, the same way.
36
+
37
+ ## Checklist
38
+
39
+ - [ ] The example runs in the harness, in every scenario
40
+ - [ ] Your Home works in every scenario, with `--native-version null` too
41
+ - [ ] Back navigation, logout and the way to the app settings are in your screens
42
+ - [ ] `validate --strict` exits 0 on the `.zip`
43
+ - [ ] Published to the test company and checked on a phone
44
+ - [ ] Published to your company
116
45
  - [ ] Feedback shared with Condor
117
- </content>
46
+
47
+ When something does not work: [`docs/troubleshooting.md`](./troubleshooting.md).