@condorinst/mobile-bridge 0.22.0 → 0.23.1

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.
@@ -74,6 +74,26 @@ You need a **manager** user of the company. The command needs a terminal (it
74
74
  prompts), so it does not run in CI; and an AI agent must not run it on its own
75
75
  — it publishes to real people and asks for your password.
76
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.
96
+
77
97
  ## Seeing what the bundle does on the phone
78
98
 
79
99
  **Release builds of the app cannot be inspected.** Ask Condor for a
@@ -96,6 +116,221 @@ by the app (wrong argument type, unknown command, a request lost while the
96
116
  background service starts) leaves no trace in the console — reproduce it in the
97
117
  harness instead, whose console says what was dropped and why.
98
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**; with `realTime` on it lasts until `realTime` goes off or 3 min after it started (`realTimeSessionMs`), whichever comes first |
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
+ "realTimeSessionMs": 30000,
204
+ "history": { "days": 14, "dockHours": [7], "gapHours": [2, 3], "gapDates": ["2026-10-18"] },
205
+ "sensorLogFailOnRequest": 3,
206
+ "userDeviceError": "NETWORK",
207
+ "sensorLogError": "INVALID_TIME_RANGE",
208
+ "systemStatus": { "bluetooth": false, "missingEssentialPermissions": ["bluetoothScan"] },
209
+ "devices": { "lastUpdate": 0, "devicesConfig": [], "devicesTelemetry": [], "devicesUserConfig": [] }
210
+ }
211
+ ```
212
+
213
+ `devices` is the `userDevice` payload to answer (`UserDevicePayload`).
214
+ `firstSessionAfterMs` is when the first device starts a BLE session: `3000` in
215
+ `default`, absent in every other scenario; `null` turns it off.
216
+ `systemStatus` is merged into the base scenario's. `sessionFailAt` makes every
217
+ simulated sync fail at that stage (`<stage>_error`, no
218
+ `pipeline_done_notification`); without internet syncs fail at
219
+ `send_data_log` anyway. A pending `syncTime`/`deleteMemory` shows up as its step
220
+ in the next session. A failed sync also puts an English text in
221
+ `systemStatus.deviceSyncErrorById` (and `deviceSyncError`) and emits it; the
222
+ next successful sync of that device clears it.
223
+
224
+ `history` shapes the simulated `sensorLog` (`HistoryOptions`): `days` before
225
+ now, `dockHours` (local hours with `off_wrist: 0` and no light), `gapHours`
226
+ (local hours with no log, every day) and `gapDates` (whole local dates with no
227
+ log). `sensorLogFailOnRequest: N` makes the Nth `sensorLog` request fail once
228
+ with `Connection failed` — a failure in the middle of a sweep.
229
+
230
+ **The data the harness serves.** Two devices (`twoDevices()`: ActLumus 4534 /
231
+ id 9001 set to the wrist, ActLumus Plus 5120 / id 9002 set to the necklace).
232
+ Their `sensorLog`: `history.days` (14 by default; set it to 30 for a 30-day
233
+ report) × 24 h back from **now** (not from midnight), one
234
+ log per `logPeriodS` (60 s by default); `body_part` is the device's
235
+ `deviceBodyPart`; worn (`off_wrist: 1`) all day except the dock hour 07:00–08:00
236
+ local (`off_wrist: 0`); light follows a daylight curve on the browser's local
237
+ clock — deterministic for a given timestamp, so a test can recompute what a
238
+ chart should show. The `bodyPart` filter of a query is not simulated.
239
+ `deviceLightSensors` arrives once per second.
240
+
241
+ **Other harness behaviour worth knowing in a test:**
242
+ - `language` is answered from the **browser's** `navigator.language`, with
243
+ `appLanguage: null`. Fix it in a test with
244
+ `condorDev.emit('language', …)` before asserting on text.
245
+ - A declined confirmation (`?confirm=decline`) produces nothing: no ack, no
246
+ event — as on the phone.
247
+ - With `nativeVersion` null the handshake resolves only after the shell's 2 s
248
+ relay timeout, so such a test starts 2 s later.
249
+ - The harness has no fake clock: a time-out of yours (45 s for a declined
250
+ confirmation, minutes for a lost sync) belongs in a unit test with your test
251
+ runner's fake timers, not in a browser test.
252
+ - Event order is not the phone's: no order between events is guaranteed there
253
+ either (`docs/bridge-contract.md`, "Emission cadence"), so don't let a test
254
+ depend on it.
255
+ - `condorDev.log` keeps the **last 500** entries: read it as you go in a long
256
+ test. The
257
+ fixtures are exported from `@condorinst/mobile-bridge/dev` (`twoDevices`,
258
+ `sharedSerial`, `systemStatus`, `sensorLogPage`, `lightSensorsReading`,
259
+ `formatLocalTimestamp`) for unit tests without a browser. Pass `sensorLogPage`
260
+ the device's `deviceId` (`{ deviceId }`); without it `device_id` is `0`.
261
+
262
+ **Like the phone, not like `localhost`.** The harness removes
263
+ `crypto.randomUUID` from your iframe: on `localhost` it would exist, and on the
264
+ phone it probably does not.
265
+
266
+ A test with `puppeteer-core` and the Chrome already installed (`puppeteer`
267
+ works the same and downloads its own browser):
268
+
269
+ ```js
270
+ import puppeteer from 'puppeteer-core';
271
+ import { spawn } from 'node:child_process';
272
+
273
+ const port = 5180;
274
+ const cli = 'node_modules/@condorinst/mobile-bridge/dist/cli.js';
275
+ const dev = spawn('node', [cli, 'dev', '-i', 'dist/index.html',
276
+ '--port', String(port), '--latency', '0', '--no-panel'], { stdio: 'inherit' });
277
+ for (let i = 0; i < 50; i++) { // wait for the port, not a fixed sleep
278
+ try { await fetch(`http://localhost:${port}/`); break; } catch { await new Promise((r) => setTimeout(r, 100)); }
279
+ }
280
+
281
+ const browser = await puppeteer.launch({
282
+ executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', // your Chrome
283
+ });
284
+ const page = await browser.newPage();
285
+ await page.emulateTimezone('Pacific/Kiritimati'); // log_time is local: test far from UTC
286
+ await page.goto(`http://localhost:${port}/?confirm=accept`); // the CLI's --scenario applies
287
+ await page.waitForFunction(() => window.condorDev?.ready);
288
+
289
+ const bundle = page.frames().find((f) => f !== page.mainFrame()); // your page
290
+ await bundle.waitForSelector('[data-device-id]'); // assert on your markup, not on text
291
+
292
+ await page.evaluate(() => window.condorDev.setStatus({ internet: false }));
293
+ await bundle.waitForSelector('[data-offline]');
294
+
295
+ const queries = await page.evaluate(() =>
296
+ window.condorDev.log.filter((e) => e.kind === 'sent' && e.command === 'sensorLog').map((e) => e.args));
297
+
298
+ await browser.close();
299
+ dev.kill(); // node, not npx: no orphan process
300
+ ```
301
+
302
+ The harness does nothing on its own after load, except one session (the first
303
+ device of the `default` scenario connects after 3 s, syncs, and disconnects
304
+ about 4 s later, or up to 3 min later with `realTime` on). Internet loss, a
305
+ device disconnecting, or a reading arriving are things your test does through
306
+ `window.condorDev`.
307
+
308
+ ### What `validate` checks
309
+
310
+ Errors make the app reject the bundle; warnings are failures with `--strict`.
311
+ The warnings are **textual**: they match strings and comments too, so do not
312
+ leave these names in your code even unused.
313
+
314
+ | Rule | Severity | Matches |
315
+ | --- | --- | --- |
316
+ | `not-self-contained` | error | `<script src>` or `<link rel="stylesheet">` that is not inline |
317
+ | `too-large` | error | a `.zip` over 2 MB |
318
+ | `unreadable-zip` | error | a file that is not a valid `.zip` |
319
+ | `entry-missing` | error | no HTML entry in the `.zip` |
320
+ | `entry-fallback` | warning | no `index.html` at the root of the `.zip` |
321
+ | `dev-harness-bundled` | error | the dev harness compiled into the bundle |
322
+ | `relative-asset` | warning | a relative `<img src>` (use a `data:` URI) |
323
+ | `web-storage` | warning | `localStorage`, `sessionStorage` |
324
+ | `indexed-db` | warning | `indexedDB` |
325
+ | `cookies` | warning | `document.cookie` |
326
+ | `dynamic-eval` | warning | `eval(`, `new Function(` |
327
+ | `service-worker` | warning | `serviceWorker` |
328
+ | `send-beacon` | warning | `sendBeacon` |
329
+ | `popups` | warning | `window.open(` |
330
+ | `crypto-randomuuid` | warning | `crypto.randomUUID` |
331
+ | `top-navigation` | warning | `window.top` |
332
+ | `external-network` | warning | `fetch(`, `XMLHttpRequest`, `WebSocket(` |
333
+
99
334
  ## Errors after the fact
100
335
 
101
336
  `BridgeClient.send('jsError', { message, stack })` sends an error to Condor's
@@ -1,121 +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.22.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. Publish
80
- With a manager user of your company:
81
- ```bash
82
- npx condor-mobile-bridge canvas publish dist/index.html --version 1.0.0
83
- ```
84
- It validates, shows what goes where, asks for confirmation and your password,
85
- and publishes. Publish to your **test company** first: a publish reaches every
86
- user of the company at once. (Without a manager user, send `dist/bundle.zip`
87
- with its `bundleSize` and `bundleHash` to your Condor contact.)
88
-
89
- ### 7. Verify on a device
90
- Condor publishes → the app downloads, validates and renders your Home the next
91
- time it comes to the foreground. [Testing on a device](./device-testing.md)
92
- explains the environments, how to inspect the WebView, and where errors go.
93
-
94
- ## Hard rules (or the app rejects the bundle)
95
-
96
- - ✅ Exactly **one `index.html`** at the zip root
97
- - ✅ JS/CSS **inline**, media as **`data:` URIs**, no external/relative refs loaded at runtime
98
- - ✅ **≤ 2 MB** (zipped)
99
- - ✅ **Single-chunk build** (no code-splitting): Vite → `vite-plugin-singlefile`; CRA → no `React.lazy` / dynamic `import()`
100
-
101
- The CLI handles the inlining/zipping for you — just keep your build single-file.
102
-
103
- ## Reference
104
-
105
- - [Client Guide](./client-guide.md) — full walkthrough (bridge API, key catalog, dev harness)
106
- - [Testing on a device](./device-testing.md) — from a published bundle to an inspected WebView
107
- - [Troubleshooting](./troubleshooting.md) — starts from the symptom
108
- - [`examples/minimal-home`](../examples/minimal-home) — runnable minimal client
109
- - [README](../README.md) — API reference
110
-
111
- ## Pilot checklist
112
-
113
- - [ ] SDK installed (`npm i @condorinst/mobile-bridge`)
114
- - [ ] Example runs in the harness (`npx condor-mobile-bridge dev`), in every scenario
115
- - [ ] Home built with `BridgeClient` (handshake + at least one key + one command)
116
- - [ ] Constraints respected (1 index.html, ≤2 MB, self-contained, single-chunk)
117
- - [ ] `bundle.zip` generated by the CLI, and `validate --strict` exits 0
118
- - [ ] `.zip` delivered to Condor
119
- - [ ] 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
120
45
  - [ ] Feedback shared with Condor
121
- </content>
46
+
47
+ When something does not work: [`docs/troubleshooting.md`](./troubleshooting.md).