@condorinst/mobile-bridge 0.20.0 → 0.22.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.
- package/AGENTS.md +109 -42
- package/CHANGELOG.md +99 -0
- package/README.md +80 -14
- package/contract/bridge-contract.json +59 -14
- package/dist/cli.cjs +933 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +932 -3
- package/dist/cli.js.map +1 -1
- package/dist/dev-harness.global.js +2110 -0
- package/dist/dev.cjs +1662 -0
- package/dist/dev.cjs.map +1 -0
- package/dist/dev.d.cts +683 -0
- package/dist/dev.d.ts +683 -0
- package/dist/dev.js +1611 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.cjs +96 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -14
- package/dist/index.d.ts +236 -14
- package/dist/index.js +94 -3
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +75 -24
- package/docs/bridge-contract.md +121 -20
- package/docs/client-guide.md +42 -29
- package/docs/device-testing.md +114 -0
- package/docs/pilot-onboarding.md +31 -13
- package/docs/troubleshooting.md +79 -18
- package/examples/minimal-home/AGENTS.md +9 -6
- package/examples/minimal-home/README.md +10 -5
- package/examples/minimal-home/build.mjs +2 -1
- package/examples/minimal-home/index.html +2 -0
- package/examples/minimal-home/package.json +3 -2
- package/examples/minimal-home/src/main.ts +151 -73
- package/llms-full.txt +2086 -0
- package/llms.txt +15 -13
- package/package.json +9 -3
package/docs/agent-guide.md
CHANGED
|
@@ -19,33 +19,55 @@ in memory until the next reload.
|
|
|
19
19
|
|
|
20
20
|
## Stage 0 — set up so you can see what you are doing
|
|
21
21
|
|
|
22
|
+
Start from `examples/minimal-home/` — a complete, working client with the
|
|
23
|
+
reference boot sequence, and a build script that produces a single-file bundle.
|
|
24
|
+
Copy it first, then install, so its `package.json` is the one you keep:
|
|
25
|
+
|
|
22
26
|
```bash
|
|
23
27
|
mkdir my-home && cd my-home
|
|
24
|
-
npm
|
|
25
|
-
|
|
26
|
-
npm install
|
|
28
|
+
npm pack @condorinst/mobile-bridge@^0.22.0 && tar -xzf condorinst-mobile-bridge-*.tgz
|
|
29
|
+
cp -r package/examples/minimal-home/. . && rm -rf package condorinst-mobile-bridge-*.tgz
|
|
30
|
+
npm install
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
Or, in a project you already have: `npm install @condorinst/mobile-bridge@^0.22.0`
|
|
34
|
+
and copy only `src/main.ts`, `build.mjs` and `index.html` from
|
|
35
|
+
`node_modules/@condorinst/mobile-bridge/examples/minimal-home/`.
|
|
36
|
+
|
|
37
|
+
Pin the SDK to the minor you are building against. Below `1.0.0`, a minor can
|
|
38
|
+
change the contract — `^0.22.0` gives you patches only, which is what you want.
|
|
39
|
+
|
|
40
|
+
**Run it in the harness**, not in a bare browser tab:
|
|
32
41
|
|
|
33
42
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
npm run build
|
|
44
|
+
npx condor-mobile-bridge dev -i dist/index.html --scenario default
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
The harness serves your bundle inside the same sandboxed iframe, shell, policy
|
|
48
|
+
and CSP as the app, with a simulated native side behind it, and a dev panel to
|
|
49
|
+
emit events by hand. It re-reads the file on every reload. Opening
|
|
50
|
+
`dist/index.html` directly does **not** work: there is no shell, so
|
|
51
|
+
`BridgeClient.handshake()` rejects at once.
|
|
52
|
+
|
|
53
|
+
The scenarios reproduce what the device does to you — switch with the selector
|
|
54
|
+
at the top or `--scenario`:
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
the
|
|
56
|
+
| Scenario | Your bundle must… |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `default` | render two devices |
|
|
59
|
+
| `cold-start` | leave the loading screen although the first `userDevice` requests are lost |
|
|
60
|
+
| `no-devices` | show an empty state when `userDevice` never answers |
|
|
61
|
+
| `shared-serial` | keep two devices with the same `serialNo` apart |
|
|
62
|
+
| `offline` | explain that history is unavailable without internet |
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
Add `--native-version null` (or an older version such as `0.15.0`) to test
|
|
65
|
+
your capability branches, and use the **◀ OS back** button at the top to press
|
|
66
|
+
the OS back button.
|
|
67
|
+
|
|
68
|
+
If you already have a dev server (Vite), mount the same harness from your own
|
|
69
|
+
page with `createDevHarness()` from `@condorinst/mobile-bridge/dev` — in the
|
|
70
|
+
host page only, never in the bundle (`validate` rejects that).
|
|
49
71
|
|
|
50
72
|
## Stage 1 — render before you connect
|
|
51
73
|
|
|
@@ -72,9 +94,9 @@ const canQueryTimeRange = has('0.15.0');
|
|
|
72
94
|
const hasLanguageKey = has('0.14.0');
|
|
73
95
|
```
|
|
74
96
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
as versions.
|
|
97
|
+
Or use the SDK's `versionAtLeast(ack.nativeVersion, '0.16.0')`, which does
|
|
98
|
+
exactly that and returns `false` for `null`. Do not compare version strings
|
|
99
|
+
lexically — `'0.9.0' > '0.16.0'` is `true` as strings and false as versions.
|
|
78
100
|
|
|
79
101
|
**Do not branch on `ack.accepted`.** It is always `true`; the native side never
|
|
80
102
|
refuses a handshake. Anything you gate on it is ungated.
|
|
@@ -89,7 +111,27 @@ BridgeClient.on(BRIDGE_KEYS.userDevice, (p) => { setDevices(p); render(); });
|
|
|
89
111
|
BridgeClient.on(BRIDGE_KEYS.systemStatus, (s) => { setStatus(s); render(); });
|
|
90
112
|
BridgeClient.on(BRIDGE_KEYS.deviceConnection, (c) => { setConnected(c); render(); });
|
|
91
113
|
|
|
92
|
-
|
|
114
|
+
const result = await BridgeClient.requestUserDevices();
|
|
115
|
+
if (result.status === 'timeout') renderEmptyState(result.hasDevices === false);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Do not replace the helper with `send('userDevice')`.** The device list is
|
|
119
|
+
served by the app's background service, which starts after your bundle; a
|
|
120
|
+
request sent before it is up is lost, and an account with no device never gets
|
|
121
|
+
a reply. The helper resends with backoff for 30 s and then tells you to show the
|
|
122
|
+
empty state. Keep the `userDevice` subscription: roster changes arrive on it
|
|
123
|
+
later without a request. `docs/troubleshooting.md` has the details.
|
|
124
|
+
|
|
125
|
+
Read device data from the typed items: the list is `devicesConfig`, and the
|
|
126
|
+
name, battery and last sync are in `devicesTelemetry` — join them on
|
|
127
|
+
`deviceId`, never on `serialNo` alone.
|
|
128
|
+
|
|
129
|
+
Then follow the connection state of each device:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
for (const d of result.devices.devicesConfig) {
|
|
133
|
+
BridgeClient.send('deviceConnection', { type: 'add', serialNo: d.serialNo, deviceId: d.deviceId });
|
|
134
|
+
}
|
|
93
135
|
```
|
|
94
136
|
|
|
95
137
|
`on` returns an unsubscribe function. If your UI has views that mount and
|
|
@@ -157,7 +199,11 @@ Timestamps are **device-local**. Build ranges with local `Date` getters, never
|
|
|
157
199
|
`toISOString()` — converting to UTC shifts every series by the timezone offset
|
|
158
200
|
and produces data that looks entirely plausible.
|
|
159
201
|
|
|
160
|
-
Paginate with the cursors exactly as received. They are opaque.
|
|
202
|
+
Paginate with the cursors exactly as received. They are opaque. Without a range
|
|
203
|
+
the first page is the **most recent** one (go older with `endingBefore`); with
|
|
204
|
+
`startTime` it is the **oldest** of the range (go newer with `startingAfter`).
|
|
205
|
+
A device writes one log per period — programmable, 60 s by default — so a week
|
|
206
|
+
is about 10 000 logs: use `limit: 1000` and do not assume the cadence.
|
|
161
207
|
|
|
162
208
|
## Stage 7 — back button
|
|
163
209
|
|
|
@@ -201,13 +247,18 @@ necessary, not sufficient.
|
|
|
201
247
|
repeats on the account also carries its `deviceId` (`nativeVersion` ≥ 0.17.0).
|
|
202
248
|
- [ ] Errors are reported through `BridgeClient.send('jsError', …)`, the only
|
|
203
249
|
channel out.
|
|
204
|
-
- [ ]
|
|
250
|
+
- [ ] Every harness scenario passes: `default`, `cold-start`, `no-devices`,
|
|
251
|
+
`shared-serial`, `offline`.
|
|
252
|
+
- [ ] The `nativeVersion === null` branch renders something sensible.
|
|
205
253
|
|
|
206
254
|
## Delivery
|
|
207
255
|
|
|
208
256
|
Hand Condor the `.zip` along with its size and `sha256-<hex>` hash, both printed
|
|
209
257
|
by the `build` command. The app verifies the hash against the manifest and
|
|
210
|
-
rejects any mismatch.
|
|
258
|
+
rejects any mismatch. Condor publishes it for your company, which reaches every
|
|
259
|
+
user of that company — test on a development test company first.
|
|
260
|
+
`docs/device-testing.md` explains the path to the phone and how to inspect the
|
|
261
|
+
bundle there; `docs/pilot-onboarding.md` has the full checklist.
|
|
211
262
|
|
|
212
263
|
## When something does not work
|
|
213
264
|
|
package/docs/bridge-contract.md
CHANGED
|
@@ -32,7 +32,7 @@ Four numbers coexist. Only one tells you what the device can do.
|
|
|
32
32
|
| `nativeVersion` | `0.17.0` | **The capability signal.** The bridge schema the app on this device implements. |
|
|
33
33
|
| `shellVersion` | `0.6.0` | The Condor shell wrapping your iframe. |
|
|
34
34
|
| client protocol | `0.5.0` | The postMessage protocol `BridgeClient` speaks. |
|
|
35
|
-
| package | `0.
|
|
35
|
+
| package | `0.22.0` | The npm version you compiled against. |
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
38
|
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
@@ -44,11 +44,11 @@ Two traps:
|
|
|
44
44
|
**`accepted` is always `true`.** There is no version negotiation and the native
|
|
45
45
|
side never refuses. Branching on it is branching on a constant.
|
|
46
46
|
|
|
47
|
-
**`nativeVersion` can be `null`.**
|
|
48
|
-
bundles compiled against SDK ≤ 0.16.x, where the shell did not
|
|
49
|
-
`null` as "assume only the baseline" and keep a working
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
**`nativeVersion` can be `null`.** When the shell could not reach the native
|
|
48
|
+
side, and in bundles compiled against SDK ≤ 0.16.x, where the shell did not
|
|
49
|
+
relay it. Treat `null` as "assume only the baseline" and keep a working
|
|
50
|
+
default. The dev harness reports `0.17.0` by default; `--native-version null`
|
|
51
|
+
exercises this path.
|
|
52
52
|
|
|
53
53
|
### Capability matrix
|
|
54
54
|
|
|
@@ -70,8 +70,8 @@ with `BridgeClient.on(key, cb)`, which returns an unsubscribe function.
|
|
|
70
70
|
|
|
71
71
|
| Key | Payload type | Emitted when |
|
|
72
72
|
| --- | --- | --- |
|
|
73
|
-
| `pingPong` | `PingPongPayload` | reply to
|
|
74
|
-
| `userDevice` | `UserDevicePayload` | reply to `userDevice
|
|
73
|
+
| `pingPong` | `PingPongPayload` | reply to `ping`, once the app's background service is up |
|
|
74
|
+
| `userDevice` | `UserDevicePayload` | reply to `userDevice` **when the list loads** (not guaranteed — see below), and on roster changes |
|
|
75
75
|
| `deviceLightSensors` | `DeviceLightSensorsPayload` | real-time stream, only while `realTime` is on for that device |
|
|
76
76
|
| `deviceConnection` | `DeviceConnectionPayload` | a device connects or disconnects |
|
|
77
77
|
| `deviceNotification` | `DeviceNotificationPayload` | device-level notice, including errors |
|
|
@@ -125,6 +125,52 @@ conclusions before.
|
|
|
125
125
|
off. Render a blocking card; wire its button to
|
|
126
126
|
`BridgeClient.requestPermission(name)` with a name taken from that array.
|
|
127
127
|
|
|
128
|
+
**`hasDevices` is a hint.** It is `true` only after the app loaded a non-empty
|
|
129
|
+
device list from the server. `false` means "no linked device" **or** "the app
|
|
130
|
+
could not ask" (no internet, session problem). Do not show "you have no device"
|
|
131
|
+
on `false` alone; see the next section.
|
|
132
|
+
|
|
133
|
+
### `userDevice` in detail
|
|
134
|
+
|
|
135
|
+
**It is not guaranteed to answer.** `userDevice` (and `ping`) are handled by
|
|
136
|
+
the app's background service, which starts after your bundle is running; a
|
|
137
|
+
request sent before it is up is lost. And the app replies only when the list
|
|
138
|
+
loads: an account with no linked device, or a failed request, gets **no reply
|
|
139
|
+
at all** — never an empty list. Use the helper, which resends with backoff and
|
|
140
|
+
tells you when to give up:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const r = await BridgeClient.requestUserDevices(); // getFromServer: false
|
|
144
|
+
// { status: 'ok', devices } | { status: 'timeout', hasDevices: boolean | null }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**`getFromServer`.** `false` (default) answers from the app's memory or cache
|
|
148
|
+
while it is fresh, and asks the server otherwise — use it at boot. `true` asks
|
|
149
|
+
the server first and falls back to the cache if that fails — use it for a
|
|
150
|
+
"Refresh" button.
|
|
151
|
+
|
|
152
|
+
**Shape.** Three arrays, typed as `DeviceConfigItem`, `DeviceTelemetryItem` and
|
|
153
|
+
`DeviceUserConfigItem` in `dist/index.d.ts`:
|
|
154
|
+
|
|
155
|
+
| Array | Holds | Notable fields |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| `devicesConfig` | one item per linked device — **the list to render** | `deviceId`, `serialNo`, `modelId` (`'actlumus'`, `'actlumus_plus'`; absent = ActLumus), `deviceBodyPart` (`'wrist'` \| `'necklace'` \| `null`), `bleConnectionPeriod` (minutes) |
|
|
158
|
+
| `devicesTelemetry` | last state read from each device | `deviceName`, `batteryLevel` (0–100 %), `memoryDataUsage` (0–100 %), `updataDateTime` (epoch ms, last sync — the misspelling is the real name), `deviceVersion`, `isCharging`, `isOnDock`, `syncErrors[]` |
|
|
159
|
+
| `devicesUserConfig` | per-device toggles | `syncTime`, `realTime`, `deleteMemory`, `readAllMemory`, `readAllSystemLog` |
|
|
160
|
+
|
|
161
|
+
The name and battery of a device are in **telemetry**, not in the config. A
|
|
162
|
+
device that never synced may have no telemetry item, or one with every reading
|
|
163
|
+
`null`: render it anyway.
|
|
164
|
+
|
|
165
|
+
**Joining the arrays.** Join on `deviceId`. An item of `devicesTelemetry` or
|
|
166
|
+
`devicesUserConfig` without `deviceId` is legacy data: fall back to `serialNo`
|
|
167
|
+
only when that `serialNo` appears once in `devicesConfig`.
|
|
168
|
+
|
|
169
|
+
**Sync errors.** `devicesTelemetry[].syncErrors` is the per-device history
|
|
170
|
+
(`attemptDate`, `message`, `messageCode`); `systemStatus.deviceSyncErrorById`
|
|
171
|
+
is the current error per device, already localized. Show the current one; use
|
|
172
|
+
the history for detail screens.
|
|
173
|
+
|
|
128
174
|
## Commands
|
|
129
175
|
|
|
130
176
|
`BridgeClient.send(command, args)` is **fire-and-forget**: no return value, no
|
|
@@ -136,7 +182,7 @@ column.
|
|
|
136
182
|
| `ping` | — | no | `pingPong` |
|
|
137
183
|
| `userDevice` | `{ getFromServer?: boolean }` | no | `userDevice` |
|
|
138
184
|
| `realTime` | `{ serialNo: number, deviceId?: number, value: boolean }` | no | `realTime` |
|
|
139
|
-
| `deviceConnection` | `{ type: 'add' \| 'remove', serialNo: number, deviceId?: number }` | no | `deviceConnection` |
|
|
185
|
+
| `deviceConnection` | `{ type: 'add' \| 'remove', serialNo: number, deviceId?: number }` — subscribe to / stop following one device's connection state | no | `deviceConnection` |
|
|
140
186
|
| `sensorLog` | see below | no | `sensorLog` |
|
|
141
187
|
| `openSettings` | — | no | — |
|
|
142
188
|
| `requestPermission` | `{ permission: string }` | no | — (re-emits `systemStatus`) |
|
|
@@ -152,6 +198,11 @@ column.
|
|
|
152
198
|
`serialNo` is an integer. Passing a string makes the native side drop the
|
|
153
199
|
message in silence.
|
|
154
200
|
|
|
201
|
+
`deviceConnection` **pairs nothing.** `type: 'add'` asks the app to report the
|
|
202
|
+
BLE connection state of a device you already have from `userDevice`; the state
|
|
203
|
+
then arrives on the `deviceConnection` key whenever it changes. `'remove'` stops
|
|
204
|
+
it. Send `add` for each device after the list arrives.
|
|
205
|
+
|
|
155
206
|
### Two devices with the same `serialNo`
|
|
156
207
|
|
|
157
208
|
An ActLumus and an ActLumus Plus have separate serial sequences, so one account
|
|
@@ -192,6 +243,8 @@ timeout on any spinner.
|
|
|
192
243
|
Prefer these over raw `send` — they add correlation, timeouts and types:
|
|
193
244
|
|
|
194
245
|
```ts
|
|
246
|
+
BridgeClient.requestUserDevices({ getFromServer }) // resends until the list arrives, 30 s default
|
|
247
|
+
BridgeClient.waitForService() // resends ping until the background service answers
|
|
195
248
|
BridgeClient.fetchSensorLog(query, { timeoutMs }) // correlates requestId, 10 s default
|
|
196
249
|
BridgeClient.requestPermission(name)
|
|
197
250
|
BridgeClient.reportNavigation(canGoBack)
|
|
@@ -225,9 +278,48 @@ series by the timezone offset, producing plausible data and no error at all.
|
|
|
225
278
|
**Cursors are opaque.** `startingAfter` and `endingBefore` are mutually
|
|
226
279
|
exclusive; echo back exactly what you received and never construct one.
|
|
227
280
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
281
|
+
**Sweeping a period.** Keep `startTime` and `endTime` on **every** page and add
|
|
282
|
+
the cursor; the cursors are valid only inside the range they came from:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const logs: SensorLogItem[] = [];
|
|
286
|
+
let startingAfter: string | undefined;
|
|
287
|
+
for (;;) {
|
|
288
|
+
const page = await BridgeClient.fetchSensorLog({
|
|
289
|
+
requestId: crypto.randomUUID(), serialNo, limit: 1000,
|
|
290
|
+
startTime: '2026-10-13 00:00:00', endTime: '2026-10-20 00:00:00', // local time
|
|
291
|
+
...(startingAfter ? { startingAfter } : {}),
|
|
292
|
+
});
|
|
293
|
+
if (!page.success) break; // see the error codes below
|
|
294
|
+
logs.push(...(page.logs ?? []));
|
|
295
|
+
if (!page.hasMore || !page.startingAfter) break;
|
|
296
|
+
startingAfter = page.startingAfter;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Which page comes first.** `logs` is always chronological ascending inside a
|
|
301
|
+
page. Which page you get first depends on the query:
|
|
302
|
+
|
|
303
|
+
| Query | First page | Next page |
|
|
304
|
+
| --- | --- | --- |
|
|
305
|
+
| no cursor, no range | the **most recent** `limit` logs | older: `endingBefore` = the reply's `endingBefore`, while `hasPrevious` |
|
|
306
|
+
| `startTime` (with or without `endTime`) | the **oldest** logs of the range | newer: `startingAfter` = the reply's `startingAfter`, while `hasMore` |
|
|
307
|
+
| only `endTime` | the most recent logs before it | older, as in the first row |
|
|
308
|
+
|
|
309
|
+
`limit` defaults to 10. With a time range, `hasMore` and `hasPrevious` are
|
|
310
|
+
computed *within* the period, so `hasMore: false` means "this period is
|
|
311
|
+
complete", not "no more data exists".
|
|
312
|
+
|
|
313
|
+
**How many logs to expect.** One per log period, which is **programmable per
|
|
314
|
+
device** (default 60 s, so about 1 440 per day). Do not hard-code it: derive the
|
|
315
|
+
period from consecutive `log_time` values, and page with `limit` up to 1000.
|
|
316
|
+
Pages depend on the previous cursor, so they cannot be fetched in parallel.
|
|
317
|
+
|
|
318
|
+
**Fields.** Each log is a `SensorLogItem`: `log_time`, `body_part`,
|
|
319
|
+
`temperature` (°C), `lux_photopic` and `lux_melanopic` (lux), the raw light
|
|
320
|
+
channels and the activity counters. `null` on a channel means the hardware does
|
|
321
|
+
not have it, never zero. `device_log_time` and `app_log_time` mark the upload
|
|
322
|
+
batch, not the log — do not plot them.
|
|
231
323
|
|
|
232
324
|
## Error shapes
|
|
233
325
|
|
|
@@ -237,11 +329,15 @@ There are three, and none of them is a thrown exception.
|
|
|
237
329
|
`no-confirmation-handler`, `confirmation-error`, `user-rejected`, `not-allowed`.
|
|
238
330
|
Visible only as `console.warn`.
|
|
239
331
|
|
|
240
|
-
**2. `sensorLog` failure** — `{ requestId, success: false, error }`. The
|
|
241
|
-
are `CONFLICTING_CURSORS`, `INVALID_BODY_PART`, `INVALID_LIMIT`,
|
|
332
|
+
**2. `sensorLog` failure** — `{ requestId, success: false, error }`. The
|
|
333
|
+
contract codes are `CONFLICTING_CURSORS`, `INVALID_BODY_PART`, `INVALID_LIMIT`,
|
|
242
334
|
`INVALID_START_TIME`, `INVALID_END_TIME`, `INVALID_TIME_RANGE`,
|
|
243
|
-
`MISSING_DEVICE_SERIAL_NO`, `invalid session
|
|
244
|
-
`
|
|
335
|
+
`MISSING_DEVICE_SERIAL_NO`, `DEVICE_SERIAL_AMBIGUOUS`, `invalid session` and
|
|
336
|
+
`device not found for this user`. Some carry a suffix (`INVALID_LIMIT: 5000`),
|
|
337
|
+
so **match by prefix**, not equality. When the request never reached the
|
|
338
|
+
server (no internet, timeout), `error` is free text from the transport — so
|
|
339
|
+
treat any value outside the list as "history unavailable right now" and offer a
|
|
340
|
+
retry. `error` is a **diagnostic, never user-facing text** — map it to your own
|
|
245
341
|
copy.
|
|
246
342
|
|
|
247
343
|
**3. Silence** — nothing reaches JavaScript. This is the dominant failure mode.
|
|
@@ -252,12 +348,17 @@ The six paths that produce it are enumerated in `docs/troubleshooting.md`.
|
|
|
252
348
|
The app owns the OS back button and will delegate it to you only if you ask.
|
|
253
349
|
|
|
254
350
|
```ts
|
|
255
|
-
|
|
256
|
-
|
|
351
|
+
const stack: string[] = ['home']; // keep navigation in memory
|
|
352
|
+
function go(view: string) { stack.push(view); render(); BridgeClient.reportNavigation(stack.length > 1); }
|
|
353
|
+
BridgeClient.onBack(() => { stack.pop(); render(); BridgeClient.reportNavigation(stack.length > 1); });
|
|
257
354
|
```
|
|
258
355
|
|
|
259
|
-
|
|
260
|
-
|
|
356
|
+
Keep the navigation stack in memory rather than in the History API: the
|
|
357
|
+
bundle lives in a sandboxed `srcdoc` iframe, where `history.pushState` is not
|
|
358
|
+
something to rely on. Report `false` when you are at your root, and the app
|
|
359
|
+
handles back itself. Report state on every internal navigation, not once at
|
|
360
|
+
startup. In the harness, the **◀ OS back** button at the top presses the OS
|
|
361
|
+
back button for you.
|
|
261
362
|
|
|
262
363
|
## Status bar
|
|
263
364
|
|
package/docs/client-guide.md
CHANGED
|
@@ -61,13 +61,13 @@ Pin the version with a caret:
|
|
|
61
61
|
```jsonc
|
|
62
62
|
// package.json
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@condorinst/mobile-bridge": "^0.
|
|
64
|
+
"@condorinst/mobile-bridge": "^0.22.0"
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
**Updates.** The SDK is on `0.x` during the pilot. With `^0.
|
|
69
|
-
`npm install` (or your CI) auto-picks **patch** releases (`0.
|
|
70
|
-
no action needed. A `0.
|
|
68
|
+
**Updates.** The SDK is on `0.x` during the pilot. With `^0.22.0`, running
|
|
69
|
+
`npm install` (or your CI) auto-picks **patch** releases (`0.22.x`) — bug fixes,
|
|
70
|
+
no action needed. A `0.23.0` (a larger change during beta) is an intentional
|
|
71
71
|
opt-in: you bump the range when you're ready. `1.0.0` will mark the
|
|
72
72
|
production-stable contract.
|
|
73
73
|
|
|
@@ -112,7 +112,7 @@ const unsubscribe = BridgeClient.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
|
112
112
|
### Send commands (you → native)
|
|
113
113
|
|
|
114
114
|
```typescript
|
|
115
|
-
|
|
115
|
+
const devices = await BridgeClient.requestUserDevices(); // device list, resent until it arrives
|
|
116
116
|
BridgeClient.send('realTime', { serialNo: 1234, value: true }); // start real-time
|
|
117
117
|
BridgeClient.openSettings(); // open native Settings
|
|
118
118
|
BridgeClient.requestPermission('bluetoothScan'); // in-app permission dialog
|
|
@@ -120,6 +120,12 @@ BridgeClient.requestPermission('bluetoothScan'); // in-app permission
|
|
|
120
120
|
|
|
121
121
|
> Commands are gated by the app's policy. A command that isn't allowed is
|
|
122
122
|
> silently ignored — design your UI to not depend on a response you won't get.
|
|
123
|
+
>
|
|
124
|
+
> Ask for the device list with `requestUserDevices()`, not with
|
|
125
|
+
> `send('userDevice')`: the first request after the app starts is often lost,
|
|
126
|
+
> and an account with no device gets no reply at all. The helper resends with
|
|
127
|
+
> backoff and resolves `{ status: 'timeout', hasDevices }` after 30 s, so you can
|
|
128
|
+
> show an empty state. See [Troubleshooting](./troubleshooting.md).
|
|
123
129
|
|
|
124
130
|
### Handle the system back button
|
|
125
131
|
|
|
@@ -265,32 +271,34 @@ if you need to branch on it explicitly.
|
|
|
265
271
|
|
|
266
272
|
## 4. Develop in your browser (no app needed)
|
|
267
273
|
|
|
268
|
-
|
|
269
|
-
auto-activates **mock mode**: the bridge no longer expects a native side, and
|
|
270
|
-
you get tooling to simulate it.
|
|
274
|
+
Build your page, then serve it with the harness:
|
|
271
275
|
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (Bridge.isMock) {
|
|
276
|
-
// Floating dev panel: inspect sent messages and emit native events by hand.
|
|
277
|
-
Bridge.installDevPanel({ position: 'bottom-right', theme: 'dark' });
|
|
278
|
-
}
|
|
276
|
+
```bash
|
|
277
|
+
npx condor-mobile-bridge dev -i dist/index.html
|
|
279
278
|
```
|
|
280
279
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
280
|
+
Open `http://localhost:5173`. Your page runs inside the same sandboxed iframe,
|
|
281
|
+
shell, policy and CSP as in the app, and a simulated app answers your commands.
|
|
282
|
+
Rebuild and reload — the harness re-reads the file.
|
|
283
|
+
|
|
284
|
+
Pick a **scenario** in the selector at the top (or with `--scenario`) to see
|
|
285
|
+
what the real app does to your page:
|
|
286
|
+
|
|
287
|
+
- `cold-start` — the first device requests are lost while the app starts;
|
|
288
|
+
- `no-devices` — an account with no device: the device list never comes;
|
|
289
|
+
- `shared-serial` — two devices with the same serial number;
|
|
290
|
+
- `offline` — no internet, history unavailable.
|
|
291
|
+
|
|
292
|
+
Your page is ready for a phone when it behaves in all of them. The page's
|
|
293
|
+
console (`[condor dev]` lines) shows each command, reply and dropped request,
|
|
294
|
+
and the dev panel in the corner lets you emit any event by hand.
|
|
287
295
|
|
|
288
|
-
>
|
|
289
|
-
>
|
|
290
|
-
>
|
|
296
|
+
> Opening `dist/index.html` directly in a browser does not work: there is no
|
|
297
|
+
> shell around it, so `BridgeClient.handshake()` rejects at once with a message
|
|
298
|
+
> pointing to the harness.
|
|
291
299
|
|
|
292
|
-
The [`examples/minimal-home`](../examples/minimal-home) example
|
|
293
|
-
|
|
300
|
+
The [`examples/minimal-home`](../examples/minimal-home) example runs this way
|
|
301
|
+
out of the box.
|
|
294
302
|
|
|
295
303
|
---
|
|
296
304
|
|
|
@@ -361,10 +369,14 @@ bundles and falls back to a default screen, so keep under the limit.
|
|
|
361
369
|
|
|
362
370
|
## 7. Deliver
|
|
363
371
|
|
|
364
|
-
|
|
372
|
+
Publish it yourself with a manager user of your company —
|
|
373
|
+
`npx condor-mobile-bridge canvas publish dist/index.html --version 1.0.0` — or
|
|
374
|
+
send the resulting `dist/bundle.zip` through the publication channel Condor
|
|
365
375
|
gave you (the admin upload). The backend validates structure, self-containment,
|
|
366
376
|
and size on upload, and records the `bundleSize`/`bundleHash`. Once published,
|
|
367
|
-
the app downloads, verifies, and renders your Home
|
|
377
|
+
the app downloads, verifies, and renders your Home — for **every user of your
|
|
378
|
+
company**, so test on a development test company first. See
|
|
379
|
+
[Testing on a device](./device-testing.md).
|
|
368
380
|
|
|
369
381
|
---
|
|
370
382
|
|
|
@@ -376,7 +388,8 @@ the app downloads, verifies, and renders your Home.
|
|
|
376
388
|
| Images missing | Referenced by path instead of `data:` URI — the CLI inlines local images; remote images are blocked. |
|
|
377
389
|
| Works in browser, not in app | You relied on a network call or external asset at load time (CSP blocks it), or a code-split chunk didn't resolve. Go single-chunk + self-contained. |
|
|
378
390
|
| Bundle rejected on upload / falls back | Over 2 MB, multiple files at root, or not self-contained. |
|
|
379
|
-
| `handshake`
|
|
391
|
+
| `handshake` rejects in the browser | The page is not inside the shell. Serve it with `npx condor-mobile-bridge dev`. |
|
|
392
|
+
| Stuck on "loading devices" on the phone | The device request was lost or the account has no device. Use `requestUserDevices()` and test the `cold-start` and `no-devices` scenarios. |
|
|
380
393
|
| A command does nothing | The command may not be allowed by the app policy; it's dropped silently. |
|
|
381
394
|
|
|
382
395
|
---
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Testing on a device
|
|
2
|
+
|
|
3
|
+
The harness (`npx condor-mobile-bridge dev`) catches most problems in the
|
|
4
|
+
browser. This page covers the rest: how a bundle reaches a phone, what that
|
|
5
|
+
costs, and how to see what it is doing once it is there.
|
|
6
|
+
|
|
7
|
+
## How a bundle reaches the screen
|
|
8
|
+
|
|
9
|
+
You do not install a bundle on a phone. The app downloads it:
|
|
10
|
+
|
|
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`.
|
|
16
|
+
3. Each copy of the app asks for its company's manifest when the Home opens
|
|
17
|
+
and every time the app returns to the foreground. If the `bundleHash`
|
|
18
|
+
changed, it downloads the `.zip`, checks size and hash, and swaps the Home
|
|
19
|
+
without a restart. Otherwise it keeps the cached copy, also when offline.
|
|
20
|
+
|
|
21
|
+
Consequences:
|
|
22
|
+
|
|
23
|
+
- **Reinstalling the app changes nothing.** The bundle is not inside the app.
|
|
24
|
+
- **Publishing reaches every user of that company** the next time their app
|
|
25
|
+
comes to the foreground. There is no per-user or per-device preview today.
|
|
26
|
+
- **A rejected bundle is invisible to the person.** The app keeps its previous
|
|
27
|
+
bundle, or shows its default screen, if the download fails a gate — which is
|
|
28
|
+
why `validate --strict` must pass before you hand anything over.
|
|
29
|
+
|
|
30
|
+
## Test on a test company, never on your real one
|
|
31
|
+
|
|
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:
|
|
35
|
+
|
|
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.
|
|
39
|
+
|
|
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
|
+
|
|
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
|
+
## Seeing what the bundle does on the phone
|
|
78
|
+
|
|
79
|
+
**Release builds of the app cannot be inspected.** Ask Condor for a
|
|
80
|
+
development (debuggable) Android build. Then:
|
|
81
|
+
|
|
82
|
+
1. Connect the phone by USB with USB debugging on.
|
|
83
|
+
2. Open `chrome://inspect/#devices` in desktop Chrome and click **inspect** on
|
|
84
|
+
the app's WebView.
|
|
85
|
+
3. In the DevTools console, switch the context selector from `top` to the
|
|
86
|
+
frame `about:srcdoc` — that is your bundle. The `top` frame is the Condor
|
|
87
|
+
shell.
|
|
88
|
+
|
|
89
|
+
There you see your own `console.*` output, uncaught errors, and the shell's
|
|
90
|
+
refusals: a `nack` reaches you only as a `[BridgeClient] command nacked by
|
|
91
|
+
shell` warning, so this is the one place to find out that a command was denied
|
|
92
|
+
or declined.
|
|
93
|
+
|
|
94
|
+
What you will **not** see from the WebView: the native side. A command dropped
|
|
95
|
+
by the app (wrong argument type, unknown command, a request lost while the
|
|
96
|
+
background service starts) leaves no trace in the console — reproduce it in the
|
|
97
|
+
harness instead, whose console says what was dropped and why.
|
|
98
|
+
|
|
99
|
+
## Errors after the fact
|
|
100
|
+
|
|
101
|
+
`BridgeClient.send('jsError', { message, stack })` sends an error to Condor's
|
|
102
|
+
logging and crash reporting (rate limited: 20 per 10 s, 4 KB per field). Those
|
|
103
|
+
are Condor's tools: when you need the errors of a test session, ask your Condor
|
|
104
|
+
contact, with the test account and the time window.
|
|
105
|
+
|
|
106
|
+
## Before you publish
|
|
107
|
+
|
|
108
|
+
- [ ] `npx condor-mobile-bridge validate -i dist/bundle.zip --strict` exits 0.
|
|
109
|
+
- [ ] Every harness scenario passes (`default`, `cold-start`, `no-devices`,
|
|
110
|
+
`shared-serial`, `offline`).
|
|
111
|
+
- [ ] It is going to the **test company** first, with a version number not
|
|
112
|
+
used before in that company.
|
|
113
|
+
- [ ] After publishing, the `bundleHash` that `canvas publish` prints matches
|
|
114
|
+
the one `build` printed.
|