@condorinst/mobile-bridge 0.17.0 → 0.19.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 +120 -0
- package/CHANGELOG.md +93 -0
- package/README.md +75 -16
- package/contract/bridge-contract.json +198 -0
- package/dist/cli.cjs +230 -17
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +231 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +107 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +476 -373
- package/dist/index.d.ts +476 -373
- package/dist/index.js +105 -69
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +209 -0
- package/docs/bridge-contract.md +249 -0
- package/docs/client-guide.md +387 -0
- package/docs/limits.md +111 -0
- package/docs/pilot-onboarding.md +103 -0
- package/docs/troubleshooting.md +150 -0
- package/examples/minimal-home/AGENTS.md +28 -0
- package/examples/minimal-home/README.md +42 -0
- package/examples/minimal-home/build.mjs +31 -0
- package/examples/minimal-home/index.html +77 -0
- package/examples/minimal-home/package.json +23 -0
- package/examples/minimal-home/src/main.ts +161 -0
- package/examples/minimal-home/tsconfig.json +14 -0
- package/llms.txt +44 -0
- package/package.json +15 -2
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Agent guide — building a Canvas client
|
|
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.
|
|
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
|
+
```bash
|
|
23
|
+
mkdir my-home && cd my-home
|
|
24
|
+
npm init -y
|
|
25
|
+
npm install @condorinst/mobile-bridge
|
|
26
|
+
npm install -D esbuild typescript
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Copy `examples/minimal-home/` from the package as your starting point — it is a
|
|
30
|
+
complete, working client in about 160 lines, with a build script that produces
|
|
31
|
+
a single-file bundle:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cp -r node_modules/@condorinst/mobile-bridge/examples/minimal-home/. .
|
|
35
|
+
npm install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Pin the SDK to the minor you are building against. Below `1.0.0`, a minor can
|
|
39
|
+
change the contract — `^0.19.0` gives you patches only, which is what you want.
|
|
40
|
+
|
|
41
|
+
**Open `dist/index.html` in a browser.** With no native host detected, the SDK
|
|
42
|
+
activates mock mode and installs a dev panel in the corner that lets you emit
|
|
43
|
+
native events by hand. This is your entire development loop; you do not need
|
|
44
|
+
the app to build a Home.
|
|
45
|
+
|
|
46
|
+
Remember that in mock mode `nativeVersion` is `null`. That is not a bug in the
|
|
47
|
+
mock — it is the same value you get from a real app whose shell is older than
|
|
48
|
+
0.6.0, so your code has to handle it either way.
|
|
49
|
+
|
|
50
|
+
## Stage 1 — render before you connect
|
|
51
|
+
|
|
52
|
+
Write the screen so it paints correctly with **no data at all**: empty roster,
|
|
53
|
+
unknown status, default language. Then fill it as events arrive.
|
|
54
|
+
|
|
55
|
+
This is not a nicety. Events are emitted on attach, before your bundle exists,
|
|
56
|
+
and again after the handshake — so there is always a window where you have
|
|
57
|
+
nothing. A Home whose first paint depends on an event shows a blank screen on
|
|
58
|
+
every cold start.
|
|
59
|
+
|
|
60
|
+
## Stage 2 — handshake, and learn what this device can do
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
64
|
+
|
|
65
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
66
|
+
|
|
67
|
+
const native = ack.nativeVersion; // string | null
|
|
68
|
+
const has = (min: string) => native !== null && cmp(native, min) >= 0;
|
|
69
|
+
|
|
70
|
+
const canReadRawChannels = has('0.16.0');
|
|
71
|
+
const canQueryTimeRange = has('0.15.0');
|
|
72
|
+
const hasLanguageKey = has('0.14.0');
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Write `cmp` as a plain numeric comparison of the three segments. Do not compare
|
|
76
|
+
version strings lexically — `'0.9.0' > '0.16.0'` is `true` as strings and false
|
|
77
|
+
as versions.
|
|
78
|
+
|
|
79
|
+
**Do not branch on `ack.accepted`.** It is always `true`; the native side never
|
|
80
|
+
refuses a handshake. Anything you gate on it is ungated.
|
|
81
|
+
|
|
82
|
+
## Stage 3 — subscribe, then ask
|
|
83
|
+
|
|
84
|
+
Subscribe before sending the command that triggers the reply. The reply can
|
|
85
|
+
arrive before your next line runs.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
BridgeClient.on(BRIDGE_KEYS.userDevice, (p) => { setDevices(p); render(); });
|
|
89
|
+
BridgeClient.on(BRIDGE_KEYS.systemStatus, (s) => { setStatus(s); render(); });
|
|
90
|
+
BridgeClient.on(BRIDGE_KEYS.deviceConnection, (c) => { setConnected(c); render(); });
|
|
91
|
+
|
|
92
|
+
BridgeClient.send('userDevice', { getFromServer: false });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`on` returns an unsubscribe function. If your UI has views that mount and
|
|
96
|
+
unmount, call it — otherwise you accumulate handlers that all re-render.
|
|
97
|
+
|
|
98
|
+
## Stage 4 — handle the environment honestly
|
|
99
|
+
|
|
100
|
+
`systemStatus` tells you why things are not working. Surface it; do not swallow
|
|
101
|
+
it.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
if (status.missingEssentialPermissions.length > 0) {
|
|
105
|
+
// Background collection is OFF. This is blocking — say so.
|
|
106
|
+
renderBlockingCard(status.missingEssentialPermissions, (name) =>
|
|
107
|
+
BridgeClient.requestPermission(name));
|
|
108
|
+
} else if (!status.bluetooth) {
|
|
109
|
+
renderNotice('Bluetooth is off — devices cannot be reached.');
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
There is no permission-result event. After the person answers the OS prompt,
|
|
114
|
+
the native side re-emits `systemStatus`; your card disappears because the array
|
|
115
|
+
emptied. If you wait for a dedicated ack, you wait forever.
|
|
116
|
+
|
|
117
|
+
## Stage 5 — real-time, if you need it
|
|
118
|
+
|
|
119
|
+
Real-time is **off by default**, per device. Turn it on, wait for the ack, and
|
|
120
|
+
only then expect the stream.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
BridgeClient.on(BRIDGE_KEYS.realTime, (ack) => setRealTime(ack.serialNo, ack.value));
|
|
124
|
+
BridgeClient.on(BRIDGE_KEYS.deviceLightSensors, (p) => {
|
|
125
|
+
if (p.type === 'actlumus2') { /* 14 channels */ } else { /* 10 channels */ }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
BridgeClient.send('realTime', { serialNo, value: true });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`serialNo` is a **number**. A string is dropped in silence.
|
|
132
|
+
|
|
133
|
+
In a raw channel, `null` means the channel does not exist on that hardware
|
|
134
|
+
generation. It does not mean zero — `0.0` is a valid reading. Coercing `null`
|
|
135
|
+
to `0` invents data, and has previously led to a confident, wrong conclusion
|
|
136
|
+
about a device.
|
|
137
|
+
|
|
138
|
+
## Stage 6 — history, if you need it
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const page = await BridgeClient.fetchSensorLog({
|
|
142
|
+
requestId: crypto.randomUUID(),
|
|
143
|
+
serialNo,
|
|
144
|
+
limit: 200,
|
|
145
|
+
});
|
|
146
|
+
if (!page.success) return showError(page.error); // resolves, does not reject
|
|
147
|
+
renderLogs(page.logs ?? []);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Timestamps are **device-local**. Build ranges with local `Date` getters, never
|
|
151
|
+
`toISOString()` — converting to UTC shifts every series by the timezone offset
|
|
152
|
+
and produces data that looks entirely plausible.
|
|
153
|
+
|
|
154
|
+
Paginate with the cursors exactly as received. They are opaque.
|
|
155
|
+
|
|
156
|
+
## Stage 7 — back button
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
BridgeClient.onBack(() => goBackOnePage());
|
|
160
|
+
// call this on every internal navigation, not once at startup
|
|
161
|
+
BridgeClient.reportNavigation(hasInternalHistory);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
If you never report `true`, the OS back button stays with the app — which is
|
|
165
|
+
the correct behaviour for a single-screen Home.
|
|
166
|
+
|
|
167
|
+
## Stage 8 — build and validate
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run build # your bundler → dist/index.html, single chunk
|
|
171
|
+
npx condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
172
|
+
npx condor-mobile-bridge validate -i dist/bundle.zip --strict
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`validate` applies the gates the app applies plus a lint for APIs the sandbox
|
|
176
|
+
blocks. A non-zero exit is a build failure. Errors mean the app would reject
|
|
177
|
+
the bundle outright; warnings mean it installs and then misbehaves on the
|
|
178
|
+
device, which is worse to debug.
|
|
179
|
+
|
|
180
|
+
Validation is textual, so it cannot see through minification and it cannot know
|
|
181
|
+
whether you checked `nativeVersion` before using a capability. A clean run is
|
|
182
|
+
necessary, not sufficient.
|
|
183
|
+
|
|
184
|
+
## Stage 9 — before you call it done
|
|
185
|
+
|
|
186
|
+
- [ ] `validate --strict` exits 0.
|
|
187
|
+
- [ ] First paint works with no events received.
|
|
188
|
+
- [ ] Every capability past the baseline is guarded by a `nativeVersion` check,
|
|
189
|
+
and the `null` branch renders something sensible.
|
|
190
|
+
- [ ] No spinner can hang forever — the three confirmable commands
|
|
191
|
+
(`syncTime`, `deleteMemory`, `logout`) may never ack, because the person
|
|
192
|
+
can decline.
|
|
193
|
+
- [ ] No `localStorage`, no `fetch`, no `eval`. Not even behind a `try`.
|
|
194
|
+
- [ ] `serialNo` is a number everywhere.
|
|
195
|
+
- [ ] Errors are reported through `BridgeClient.send('jsError', …)`, the only
|
|
196
|
+
channel out.
|
|
197
|
+
- [ ] Tested in the browser mock with `nativeVersion === null`.
|
|
198
|
+
|
|
199
|
+
## Delivery
|
|
200
|
+
|
|
201
|
+
Hand Condor the `.zip` along with its size and `sha256-<hex>` hash, both printed
|
|
202
|
+
by the `build` command. The app verifies the hash against the manifest and
|
|
203
|
+
rejects any mismatch. `docs/pilot-onboarding.md` has the full checklist.
|
|
204
|
+
|
|
205
|
+
## When something does not work
|
|
206
|
+
|
|
207
|
+
`docs/troubleshooting.md`, which starts from the symptom. The most common one
|
|
208
|
+
is that a command did nothing and said nothing — there are six distinct causes
|
|
209
|
+
for that, and they are enumerated there.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Bridge contract
|
|
2
|
+
|
|
3
|
+
The normative reference for everything the client can observe and everything it
|
|
4
|
+
can ask for. Payload *shapes* are canonical in `dist/index.d.ts` — your editor
|
|
5
|
+
shows them on hover. This document states what a type declaration cannot: what
|
|
6
|
+
is reachable, what needs human consent, what fails in silence, and which native
|
|
7
|
+
version carries which capability.
|
|
8
|
+
|
|
9
|
+
A machine-readable form of this document lives at
|
|
10
|
+
`contract/bridge-contract.json`, importable as `@condorinst/mobile-bridge/contract`.
|
|
11
|
+
|
|
12
|
+
## Topology
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
your bundle (iframe, sandbox="allow-scripts", origin null)
|
|
16
|
+
| window.postMessage, schema-validated both ways
|
|
17
|
+
Condor shell (top-level document, opaque origin)
|
|
18
|
+
| native channel, nonce-stamped
|
|
19
|
+
native app (Flutter)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You never talk to the native side directly — the shell deletes the native
|
|
23
|
+
globals before your bundle runs. Use `BridgeClient`; it speaks the shell
|
|
24
|
+
protocol for you.
|
|
25
|
+
|
|
26
|
+
## Versions and capability detection
|
|
27
|
+
|
|
28
|
+
Four numbers coexist. Only one tells you what the device can do.
|
|
29
|
+
|
|
30
|
+
| Number | Current | What it means |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `nativeVersion` | `0.16.0` | **The capability signal.** The bridge schema the app on this device implements. |
|
|
33
|
+
| `shellVersion` | `0.6.0` | The Condor shell wrapping your iframe. |
|
|
34
|
+
| client protocol | `0.5.0` | The postMessage protocol `BridgeClient` speaks. |
|
|
35
|
+
| package | `0.19.0` | The npm version you compiled against. |
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
39
|
+
// { shellVersion: string, nativeVersion: string | null, accepted: boolean }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Two traps:
|
|
43
|
+
|
|
44
|
+
**`accepted` is always `true`.** There is no version negotiation and the native
|
|
45
|
+
side never refuses. Branching on it is branching on a constant.
|
|
46
|
+
|
|
47
|
+
**`nativeVersion` can be `null`.** In a plain browser (mock mode), and in
|
|
48
|
+
bundles compiled against SDK ≤ 0.16.x, where the shell did not relay it. Treat
|
|
49
|
+
`null` as "assume only the baseline" and keep a working default. Your browser
|
|
50
|
+
development environment always hits this path, so it is not a rare case — it is
|
|
51
|
+
the one you see most.
|
|
52
|
+
|
|
53
|
+
### Capability matrix
|
|
54
|
+
|
|
55
|
+
| Capability | Requires |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| Baseline: devices, real-time, acks, `systemStatus`, `sensorLog` | any |
|
|
58
|
+
| `language` key and the `language` pull command | `nativeVersion` ≥ 0.14.0 |
|
|
59
|
+
| `sensorLog` accepts `startTime` / `endTime` | `nativeVersion` ≥ 0.15.0 |
|
|
60
|
+
| `deviceLightSensors` carries raw `light_sensor_*` channels, and the `actlumus2` variant | `nativeVersion` ≥ 0.16.0 |
|
|
61
|
+
| `nativeVersion` reaches the client at all | `shellVersion` ≥ 0.6.0 |
|
|
62
|
+
|
|
63
|
+
`CHANGELOG.md` is the precise record of when each of these landed.
|
|
64
|
+
|
|
65
|
+
## Observable keys
|
|
66
|
+
|
|
67
|
+
Thirteen keys exist; **twelve are reachable** by a sandboxed client. Subscribe
|
|
68
|
+
with `BridgeClient.on(key, cb)`, which returns an unsubscribe function.
|
|
69
|
+
|
|
70
|
+
| Key | Payload type | Emitted when |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `pingPong` | `PingPongPayload` | reply to the `ping` command |
|
|
73
|
+
| `userDevice` | `UserDevicePayload` | reply to `userDevice`, and on roster changes |
|
|
74
|
+
| `deviceLightSensors` | `DeviceLightSensorsPayload` | real-time stream, only while `realTime` is on for that device |
|
|
75
|
+
| `deviceConnection` | `DeviceConnectionPayload` | a device connects or disconnects |
|
|
76
|
+
| `deviceNotification` | `DeviceNotificationPayload` | device-level notice, including errors |
|
|
77
|
+
| `realTime` | `RealTimeAckPayload` | ack of `realTime` |
|
|
78
|
+
| `syncTime` | `SyncTimeAckPayload` | ack of `syncTime` |
|
|
79
|
+
| `deleteMemory` | `DeleteMemoryAckPayload` | ack of `deleteMemory` |
|
|
80
|
+
| `sensorLog` | `SensorLogPayload` | reply to `sensorLog`; correlate by `requestId` |
|
|
81
|
+
| `systemStatus` | `SystemStatusPayload` | on attach, again right after handshake, and on every change |
|
|
82
|
+
| `language` | `LanguagePayload` | same cadence as `systemStatus`; needs native ≥ 0.14.0 |
|
|
83
|
+
| `navigationBack` | `NavigationBackPayload` (empty) | OS back pressed while you had reported `canGoBack: true` |
|
|
84
|
+
| `dfu` | — | **not reachable.** Firmware traffic. Subscribing is answered with `nack: not-read-only`. |
|
|
85
|
+
|
|
86
|
+
### Emission cadence you must design around
|
|
87
|
+
|
|
88
|
+
`systemStatus` and `language` are emitted **on attach, before your bundle
|
|
89
|
+
exists**, then again right after the handshake. The first emission is lost to
|
|
90
|
+
you; the second is the one you receive. This is why `language` also exists as a
|
|
91
|
+
pull command — a client that mounts late can ask.
|
|
92
|
+
|
|
93
|
+
Render a skeleton synchronously and fill it as events arrive. Do not make first
|
|
94
|
+
paint depend on any event.
|
|
95
|
+
|
|
96
|
+
### `deviceLightSensors` is a discriminated union
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
if (payload.type === 'actlumus2') {
|
|
100
|
+
// 14 wavelength channels, 395–800 nm, plus raw light_sensor_f1..f8 / clear / nir
|
|
101
|
+
} else {
|
|
102
|
+
// 'actlumus': 10 channels, 390–750 nm
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A `null` in a raw channel means **that channel does not exist on this hardware
|
|
107
|
+
generation** — never "the reading was zero". `0.0` is a valid reading. Do not
|
|
108
|
+
collapse `null` to `0`: doing so fabricates data, and it has produced wrong
|
|
109
|
+
conclusions before.
|
|
110
|
+
|
|
111
|
+
### `systemStatus` carries the permission state
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
{
|
|
115
|
+
bluetooth: boolean; internet: boolean; permission: boolean; hasDevices: boolean;
|
|
116
|
+
missingEssentialPermissions: string[]; // e.g. ["bluetoothScan", "bluetoothConnect"]
|
|
117
|
+
missingOptionalPermissions: string[]; // e.g. ["notification", "backgroundLocation"]
|
|
118
|
+
deviceSyncError: Record<number, string>; // serialNo → message, already localized
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`missingEssentialPermissions` being non-empty means background collection is
|
|
123
|
+
off. Render a blocking card; wire its button to
|
|
124
|
+
`BridgeClient.requestPermission(name)` with a name taken from that array.
|
|
125
|
+
|
|
126
|
+
## Commands
|
|
127
|
+
|
|
128
|
+
`BridgeClient.send(command, args)` is **fire-and-forget**: no return value, no
|
|
129
|
+
exception, no delivery confirmation. Replies arrive on the key in the last
|
|
130
|
+
column.
|
|
131
|
+
|
|
132
|
+
| Command | Arguments | Confirmation | Replies on |
|
|
133
|
+
| --- | --- | --- | --- |
|
|
134
|
+
| `ping` | — | no | `pingPong` |
|
|
135
|
+
| `userDevice` | `{ getFromServer?: boolean }` | no | `userDevice` |
|
|
136
|
+
| `realTime` | `{ serialNo: number, value: boolean }` | no | `realTime` |
|
|
137
|
+
| `deviceConnection` | `{ type: 'add' \| 'remove', serialNo: number }` | no | `deviceConnection` |
|
|
138
|
+
| `sensorLog` | see below | no | `sensorLog` |
|
|
139
|
+
| `openSettings` | — | no | — |
|
|
140
|
+
| `requestPermission` | `{ permission: string }` | no | — (re-emits `systemStatus`) |
|
|
141
|
+
| `setStatusBarColor` | `{ color: '#RRGGBB' }` | no | — |
|
|
142
|
+
| `navigationState` | `{ canGoBack: boolean }` | no | — |
|
|
143
|
+
| `language` | — | no | `language` |
|
|
144
|
+
| `jsError` | `{ message, stack?, source?, lineno?, colno? }` | no | — |
|
|
145
|
+
| `syncTime` | `{ serialNo: number }` | **yes** | `syncTime` |
|
|
146
|
+
| `deleteMemory` | `{ serialNo: number }` | **yes** | `deleteMemory` |
|
|
147
|
+
| `logout` | — | **yes** | — |
|
|
148
|
+
| `dfu` | — | denied | — |
|
|
149
|
+
|
|
150
|
+
`serialNo` is an integer. Passing a string makes the native side drop the
|
|
151
|
+
message in silence.
|
|
152
|
+
|
|
153
|
+
### Commands that need human confirmation
|
|
154
|
+
|
|
155
|
+
`syncTime`, `deleteMemory` and `logout` open a modal owned by the Condor shell.
|
|
156
|
+
If the person declines, the shell answers `nack` with reason `user-rejected`,
|
|
157
|
+
which reaches you only as a `console.warn`. **The ack simply never arrives.**
|
|
158
|
+
|
|
159
|
+
Design for that: show an optimistic state only if you can undo it, and put a
|
|
160
|
+
timeout on any spinner.
|
|
161
|
+
|
|
162
|
+
### Typed helpers
|
|
163
|
+
|
|
164
|
+
Prefer these over raw `send` — they add correlation, timeouts and types:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
BridgeClient.fetchSensorLog(query, { timeoutMs }) // correlates requestId, 10 s default
|
|
168
|
+
BridgeClient.requestPermission(name)
|
|
169
|
+
BridgeClient.reportNavigation(canGoBack)
|
|
170
|
+
BridgeClient.onBack(cb)
|
|
171
|
+
BridgeClient.openSettings()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### `sensorLog` in detail
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const page = await BridgeClient.fetchSensorLog({
|
|
178
|
+
requestId: crypto.randomUUID(),
|
|
179
|
+
serialNo,
|
|
180
|
+
bodyPart: 'wrist', // or 'necklace'; omit for no filter
|
|
181
|
+
limit: 200, // 1..1000, backend default 10
|
|
182
|
+
startTime: '2026-09-01 00:00:00', // native >= 0.15.0
|
|
183
|
+
endTime: '2026-09-02 00:00:00',
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
It **resolves** with `{ success: false, error }` on failure and only **rejects**
|
|
188
|
+
on timeout. Check `success` before reading `logs`.
|
|
189
|
+
|
|
190
|
+
Two things that have caused real bugs:
|
|
191
|
+
|
|
192
|
+
**Timestamps are device-local, not UTC.** `log_time` passes through from the
|
|
193
|
+
device RTC. Build your range with local `Date` getters — `getFullYear()`,
|
|
194
|
+
`getHours()` — and **never** with `toISOString()`. Converting shifts every
|
|
195
|
+
series by the timezone offset, producing plausible data and no error at all.
|
|
196
|
+
|
|
197
|
+
**Cursors are opaque.** `startingAfter` and `endingBefore` are mutually
|
|
198
|
+
exclusive; echo back exactly what you received and never construct one.
|
|
199
|
+
|
|
200
|
+
With a time range, `hasMore` and `hasPrevious` are computed *within* the
|
|
201
|
+
period, so `hasMore: false` means "this period is complete", not "no more data
|
|
202
|
+
exists".
|
|
203
|
+
|
|
204
|
+
## Error shapes
|
|
205
|
+
|
|
206
|
+
There are three, and none of them is a thrown exception.
|
|
207
|
+
|
|
208
|
+
**1. `nack`** — shell to client, reasons `not-read-only`, `denied`,
|
|
209
|
+
`no-confirmation-handler`, `confirmation-error`, `user-rejected`, `not-allowed`.
|
|
210
|
+
Visible only as `console.warn`.
|
|
211
|
+
|
|
212
|
+
**2. `sensorLog` failure** — `{ requestId, success: false, error }`. The codes
|
|
213
|
+
are `CONFLICTING_CURSORS`, `INVALID_BODY_PART`, `INVALID_LIMIT`,
|
|
214
|
+
`INVALID_START_TIME`, `INVALID_END_TIME`, `INVALID_TIME_RANGE`,
|
|
215
|
+
`MISSING_DEVICE_SERIAL_NO`, `invalid session`, `device not found for this user`.
|
|
216
|
+
`error` is a **diagnostic code, never user-facing text** — map it to your own
|
|
217
|
+
copy.
|
|
218
|
+
|
|
219
|
+
**3. Silence** — nothing reaches JavaScript. This is the dominant failure mode.
|
|
220
|
+
The six paths that produce it are enumerated in `docs/troubleshooting.md`.
|
|
221
|
+
|
|
222
|
+
## The back button
|
|
223
|
+
|
|
224
|
+
The app owns the OS back button and will delegate it to you only if you ask.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
BridgeClient.reportNavigation(true); // "I have internal history"
|
|
228
|
+
BridgeClient.onBack(() => history.back()); // go back exactly one page
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Report `false` when you are at your root, and the app handles back itself.
|
|
232
|
+
Report state on every internal navigation, not once at startup.
|
|
233
|
+
|
|
234
|
+
## Status bar
|
|
235
|
+
|
|
236
|
+
Two ways, in precedence order:
|
|
237
|
+
|
|
238
|
+
```html
|
|
239
|
+
<meta name="theme-color" content="#0A2540"> <!-- static, read from the bundle -->
|
|
240
|
+
```
|
|
241
|
+
```ts
|
|
242
|
+
BridgeClient.send('setStatusBarColor', { color: '#0A2540' }); // runtime, wins
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Session
|
|
246
|
+
|
|
247
|
+
When the session token dies, the app shows its own native banner above your
|
|
248
|
+
bundle and keeps rendering you. **You receive no event for this** — there is no
|
|
249
|
+
session key. Do not try to detect it; the app handles re-authentication.
|