@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.
@@ -1,267 +1,11 @@
1
- # Agent guide — building a Canvas client
1
+ # Agent guide — merged into AGENTS.md
2
2
 
3
- A working procedure for an AI agent (or a developer) building the client bundle
4
- that renders as the Home of a Condor Canvas app.
3
+ This page used to repeat `AGENTS.md` as a staged procedure. Two copies of the
4
+ same rules drifted apart, so there is now one of each:
5
5
 
6
- Read `AGENTS.md` first — it is the contract in one screen. This document is the
7
- procedure: what to do, in what order, and what to check at each stage.
8
-
9
- ## What you are building
10
-
11
- One `.zip` containing one self-contained `index.html`. Condor's app downloads
12
- it, verifies its SHA-256, and renders it inside a sandboxed iframe as the Home
13
- screen. Login, settings, Bluetooth and background collection are the app's job.
14
- Yours is what the person sees when the app opens.
15
-
16
- You are not building a website. You have no storage, no network, and no origin.
17
- Everything you display comes through the bridge, and everything you hold lives
18
- in memory until the next reload.
19
-
20
- ## Stage 0 — set up so you can see what you are doing
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
-
26
- ```bash
27
- mkdir my-home && cd my-home
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
31
- ```
32
-
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:
41
-
42
- ```bash
43
- npm run build
44
- npx condor-mobile-bridge dev -i dist/index.html --scenario default
45
- ```
46
-
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`:
55
-
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 |
63
-
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).
71
-
72
- ## Stage 1 — render before you connect
73
-
74
- Write the screen so it paints correctly with **no data at all**: empty roster,
75
- unknown status, default language. Then fill it as events arrive.
76
-
77
- This is not a nicety. Events are emitted on attach, before your bundle exists,
78
- and again after the handshake — so there is always a window where you have
79
- nothing. A Home whose first paint depends on an event shows a blank screen on
80
- every cold start.
81
-
82
- ## Stage 2 — handshake, and learn what this device can do
83
-
84
- ```ts
85
- import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
86
-
87
- const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
88
-
89
- const native = ack.nativeVersion; // string | null
90
- const has = (min: string) => native !== null && cmp(native, min) >= 0;
91
-
92
- const canReadRawChannels = has('0.16.0');
93
- const canQueryTimeRange = has('0.15.0');
94
- const hasLanguageKey = has('0.14.0');
95
- ```
96
-
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.
100
-
101
- **Do not branch on `ack.accepted`.** It is always `true`; the native side never
102
- refuses a handshake. Anything you gate on it is ungated.
103
-
104
- ## Stage 3 — subscribe, then ask
105
-
106
- Subscribe before sending the command that triggers the reply. The reply can
107
- arrive before your next line runs.
108
-
109
- ```ts
110
- BridgeClient.on(BRIDGE_KEYS.userDevice, (p) => { setDevices(p); render(); });
111
- BridgeClient.on(BRIDGE_KEYS.systemStatus, (s) => { setStatus(s); render(); });
112
- BridgeClient.on(BRIDGE_KEYS.deviceConnection, (c) => { setConnected(c); render(); });
113
-
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
- }
135
- ```
136
-
137
- `on` returns an unsubscribe function. If your UI has views that mount and
138
- unmount, call it — otherwise you accumulate handlers that all re-render.
139
-
140
- ## Stage 4 — handle the environment honestly
141
-
142
- `systemStatus` tells you why things are not working. Surface it; do not swallow
143
- it.
144
-
145
- ```ts
146
- if (status.missingEssentialPermissions.length > 0) {
147
- // Background collection is OFF. This is blocking — say so.
148
- renderBlockingCard(status.missingEssentialPermissions, (name) =>
149
- BridgeClient.requestPermission(name));
150
- } else if (!status.bluetooth) {
151
- renderNotice('Bluetooth is off — devices cannot be reached.');
152
- }
153
- ```
154
-
155
- There is no permission-result event. After the person answers the OS prompt,
156
- the native side re-emits `systemStatus`; your card disappears because the array
157
- emptied. If you wait for a dedicated ack, you wait forever.
158
-
159
- ## Stage 5 — real-time, if you need it
160
-
161
- Real-time is **off by default**, per device. Turn it on, wait for the ack, and
162
- only then expect the stream.
163
-
164
- ```ts
165
- BridgeClient.on(BRIDGE_KEYS.realTime, (ack) => setRealTime(ack.serialNo, ack.value));
166
- BridgeClient.on(BRIDGE_KEYS.deviceLightSensors, (p) => {
167
- if (p.type === 'actlumus2') { /* 14 channels */ } else { /* 10 channels */ }
168
- });
169
-
170
- BridgeClient.send('realTime', { serialNo, value: true });
171
- ```
172
-
173
- `serialNo` is a **number**. A string is dropped in silence.
174
-
175
- Two devices can share a `serialNo` (an ActLumus and an ActLumus Plus). To act on
176
- one of them, send its `deviceId` **together with** `serialNo`, taken from the
177
- `userDevice` list: `BridgeClient.send('realTime', { serialNo, deviceId, value })`.
178
- A command with only the shared `serialNo` is ignored on purpose. Gate this on
179
- `nativeVersion` ≥ 0.17.0; details in `docs/bridge-contract.md`.
180
-
181
- In a raw channel, `null` means the channel does not exist on that hardware
182
- generation. It does not mean zero — `0.0` is a valid reading. Coercing `null`
183
- to `0` invents data, and has previously led to a confident, wrong conclusion
184
- about a device.
185
-
186
- ## Stage 6 — history, if you need it
187
-
188
- ```ts
189
- const page = await BridgeClient.fetchSensorLog({
190
- requestId: crypto.randomUUID(),
191
- serialNo,
192
- limit: 200,
193
- });
194
- if (!page.success) return showError(page.error); // resolves, does not reject
195
- renderLogs(page.logs ?? []);
196
- ```
197
-
198
- Timestamps are **device-local**. Build ranges with local `Date` getters, never
199
- `toISOString()` — converting to UTC shifts every series by the timezone offset
200
- and produces data that looks entirely plausible.
201
-
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.
207
-
208
- ## Stage 7 — back button
209
-
210
- ```ts
211
- BridgeClient.onBack(() => goBackOnePage());
212
- // call this on every internal navigation, not once at startup
213
- BridgeClient.reportNavigation(hasInternalHistory);
214
- ```
215
-
216
- If you never report `true`, the OS back button stays with the app — which is
217
- the correct behaviour for a single-screen Home.
218
-
219
- ## Stage 8 — build and validate
220
-
221
- ```bash
222
- npm run build # your bundler → dist/index.html, single chunk
223
- npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
224
- npx condor-mobile-bridge validate -i dist/bundle.zip --strict
225
- ```
226
-
227
- `validate` applies the gates the app applies plus a lint for APIs the sandbox
228
- blocks. A non-zero exit is a build failure. Errors mean the app would reject
229
- the bundle outright; warnings mean it installs and then misbehaves on the
230
- device, which is worse to debug.
231
-
232
- Validation is textual, so it cannot see through minification and it cannot know
233
- whether you checked `nativeVersion` before using a capability. A clean run is
234
- necessary, not sufficient.
235
-
236
- ## Stage 9 — before you call it done
237
-
238
- - [ ] `validate --strict` exits 0.
239
- - [ ] First paint works with no events received.
240
- - [ ] Every capability past the baseline is guarded by a `nativeVersion` check,
241
- and the `null` branch renders something sensible.
242
- - [ ] No spinner can hang forever — the three confirmable commands
243
- (`syncTime`, `deleteMemory`, `logout`) may never ack, because the person
244
- can decline.
245
- - [ ] No `localStorage`, no `fetch`, no `eval`. Not even behind a `try`.
246
- - [ ] `serialNo` is a number everywhere, and a command for a device whose `serialNo`
247
- repeats on the account also carries its `deviceId` (`nativeVersion` ≥ 0.17.0).
248
- - [ ] Errors are reported through `BridgeClient.send('jsError', …)`, the only
249
- channel out.
250
- - [ ] Every harness scenario passes: `default`, `cold-start`, `no-devices`,
251
- `shared-serial`, `offline`.
252
- - [ ] The `nativeVersion === null` branch renders something sensible.
253
-
254
- ## Delivery
255
-
256
- Hand Condor the `.zip` along with its size and `sha256-<hex>` hash, both printed
257
- by the `build` command. The app verifies the hash against the manifest and
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.
262
-
263
- ## When something does not work
264
-
265
- `docs/troubleshooting.md`, which starts from the symptom. The most common one
266
- is that a command did nothing and said nothing — there are six distinct causes
267
- for that, and they are enumerated there.
6
+ - **The procedure** — setup, boot sequence, harness, validation, definition of
7
+ done: [`AGENTS.md`](../AGENTS.md).
8
+ - **What every key, command, ack and error means**:
9
+ [`docs/bridge-contract.md`](./bridge-contract.md).
10
+ - **Harness, scripted tests, `validate`, publishing, the phone**:
11
+ [`docs/device-testing.md`](./device-testing.md).