@condorinst/mobile-bridge 0.19.2 → 0.21.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 +100 -42
- package/CHANGELOG.md +106 -0
- package/README.md +64 -14
- package/contract/bridge-contract.json +70 -25
- package/dist/cli.cjs +436 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +433 -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 +265 -19
- package/dist/index.d.ts +265 -19
- package/dist/index.js +94 -3
- package/dist/index.js.map +1 -1
- package/docs/agent-guide.md +83 -25
- package/docs/bridge-contract.md +154 -25
- package/docs/client-guide.md +39 -28
- package/docs/device-testing.md +84 -0
- package/docs/pilot-onboarding.md +26 -12
- package/docs/troubleshooting.md +83 -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 +2041 -0
- package/llms.txt +15 -13
- package/package.json +9 -3
package/docs/pilot-onboarding.md
CHANGED
|
@@ -31,7 +31,7 @@ renders inside the app, talking to the device through the Condor bridge.
|
|
|
31
31
|
```bash
|
|
32
32
|
npm install @condorinst/mobile-bridge
|
|
33
33
|
```
|
|
34
|
-
Pin `"@condorinst/mobile-bridge": "^0.
|
|
34
|
+
Pin `"@condorinst/mobile-bridge": "^0.21.0"` (beta; auto-updates patches).
|
|
35
35
|
|
|
36
36
|
### 2. Start from the example
|
|
37
37
|
Copy [`examples/minimal-home`](../examples/minimal-home) — it already wires the
|
|
@@ -42,15 +42,23 @@ Talk to the app via `BridgeClient`:
|
|
|
42
42
|
```ts
|
|
43
43
|
import { BridgeClient, BRIDGE_KEYS } from '@condorinst/mobile-bridge';
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
BridgeClient.
|
|
47
|
-
|
|
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
|
|
48
49
|
BridgeClient.openSettings(); // open native settings
|
|
49
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.
|
|
50
54
|
|
|
51
55
|
### 4. Test in your browser (no app needed)
|
|
52
|
-
|
|
53
|
-
|
|
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`.
|
|
54
62
|
|
|
55
63
|
### 5. Build the bundle
|
|
56
64
|
```bash
|
|
@@ -69,11 +77,15 @@ Errors are outright rejections by the app; warnings install fine and then
|
|
|
69
77
|
misbehave on the device, which is harder to diagnose.
|
|
70
78
|
|
|
71
79
|
### 6. Deliver
|
|
72
|
-
Send `dist/bundle.zip
|
|
73
|
-
|
|
80
|
+
Send `dist/bundle.zip`, with the `bundleSize` and `bundleHash` printed by
|
|
81
|
+
`build`, through the channel Condor gave you. We publish it to your company —
|
|
82
|
+
first to a **development test company**, because a publish reaches every user
|
|
83
|
+
of the company.
|
|
74
84
|
|
|
75
85
|
### 7. Verify on a device
|
|
76
|
-
Condor publishes → the app downloads, validates and renders your Home
|
|
86
|
+
Condor publishes → the app downloads, validates and renders your Home the next
|
|
87
|
+
time it comes to the foreground. [Testing on a device](./device-testing.md)
|
|
88
|
+
explains the environments, how to inspect the WebView, and where errors go.
|
|
77
89
|
|
|
78
90
|
## Hard rules (or the app rejects the bundle)
|
|
79
91
|
|
|
@@ -86,17 +98,19 @@ The CLI handles the inlining/zipping for you — just keep your build single-fil
|
|
|
86
98
|
|
|
87
99
|
## Reference
|
|
88
100
|
|
|
89
|
-
- [Client Guide](./client-guide.md) — full walkthrough (bridge API, key catalog,
|
|
101
|
+
- [Client Guide](./client-guide.md) — full walkthrough (bridge API, key catalog, dev harness)
|
|
102
|
+
- [Testing on a device](./device-testing.md) — from a published bundle to an inspected WebView
|
|
103
|
+
- [Troubleshooting](./troubleshooting.md) — starts from the symptom
|
|
90
104
|
- [`examples/minimal-home`](../examples/minimal-home) — runnable minimal client
|
|
91
105
|
- [README](../README.md) — API reference
|
|
92
106
|
|
|
93
107
|
## Pilot checklist
|
|
94
108
|
|
|
95
109
|
- [ ] SDK installed (`npm i @condorinst/mobile-bridge`)
|
|
96
|
-
- [ ] Example runs in the
|
|
110
|
+
- [ ] Example runs in the harness (`npx condor-mobile-bridge dev`), in every scenario
|
|
97
111
|
- [ ] Home built with `BridgeClient` (handshake + at least one key + one command)
|
|
98
112
|
- [ ] Constraints respected (1 index.html, ≤2 MB, self-contained, single-chunk)
|
|
99
|
-
- [ ] `bundle.zip` generated by the CLI
|
|
113
|
+
- [ ] `bundle.zip` generated by the CLI, and `validate --strict` exits 0
|
|
100
114
|
- [ ] `.zip` delivered to Condor
|
|
101
115
|
- [ ] Home verified on a device
|
|
102
116
|
- [ ] Feedback shared with Condor
|
package/docs/troubleshooting.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Troubleshooting
|
|
2
2
|
|
|
3
|
+
## "The device list never came" (stuck on loading)
|
|
4
|
+
|
|
5
|
+
The handshake worked, `systemStatus` arrived, and `userDevice` never answered.
|
|
6
|
+
Two different causes produce exactly this symptom, and the bundle cannot tell
|
|
7
|
+
them apart.
|
|
8
|
+
|
|
9
|
+
### Cause 1: the request was sent before the app's background service was up
|
|
10
|
+
|
|
11
|
+
`userDevice` and `ping` are not answered by the page that hosts you: the app
|
|
12
|
+
forwards them to a background service that reads the devices. On a cold start
|
|
13
|
+
the app creates your WebView first and starts that service afterwards, so a
|
|
14
|
+
request sent at boot is often **lost without a reply**. Nothing re-sends the
|
|
15
|
+
list when the service comes up. Meanwhile the handshake, `systemStatus` and
|
|
16
|
+
`language` work from the start, which makes the bridge look ready.
|
|
17
|
+
|
|
18
|
+
### Cause 2: the account has no linked device
|
|
19
|
+
|
|
20
|
+
The app replies to `userDevice` only when the list loads. When the server
|
|
21
|
+
answers that the account has no device — or the request fails for any other
|
|
22
|
+
reason (no internet, expired session) — nothing is sent to the bundle. A
|
|
23
|
+
`userDevice` with empty arrays never arrives today.
|
|
24
|
+
|
|
25
|
+
### What to do
|
|
26
|
+
|
|
27
|
+
Use the helper, which handles both:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const result = await BridgeClient.requestUserDevices(); // resends with backoff for 30 s
|
|
31
|
+
if (result.status === 'ok') render(result.devices);
|
|
32
|
+
else renderEmpty({ probablyNoDevices: result.hasDevices === false }); // + a "Try again" button
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- It resends at 1.5 s, 3 s, 6 s and then every 10 s, and on every
|
|
36
|
+
`systemStatus` while it waits. `BridgeClient.waitForService()` does the same
|
|
37
|
+
with `ping` if you only need to know the service is up.
|
|
38
|
+
- **`systemStatus.hasDevices` is a hint, not a verdict.** It is `true` only
|
|
39
|
+
after the app loaded a non-empty list from the server. `false` means either
|
|
40
|
+
"no device" or "the app could not ask". Word the empty state so it is true in
|
|
41
|
+
both cases, and always offer a retry with `getFromServer: true`.
|
|
42
|
+
- Keep the `userDevice` subscription for the life of the page: roster changes
|
|
43
|
+
arrive on it without a request.
|
|
44
|
+
|
|
45
|
+
Reproduce both causes in the browser before going to a device:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx condor-mobile-bridge dev -i dist/index.html --scenario cold-start
|
|
49
|
+
npx condor-mobile-bridge dev -i dist/index.html --scenario no-devices
|
|
50
|
+
```
|
|
51
|
+
|
|
3
52
|
## "My command did nothing"
|
|
4
53
|
|
|
5
54
|
This is the most common report, and it is usually correct: the command did
|
|
@@ -30,6 +79,10 @@ in the field can be several versions behind your development device.
|
|
|
30
79
|
payload natively and drop it on mismatch. `serialNo` is an **integer**, not a
|
|
31
80
|
string; `value` and `canGoBack` are **booleans**, not strings.
|
|
32
81
|
|
|
82
|
+
**Also check:** if the account holds an ActLumus and an ActLumus Plus with the same
|
|
83
|
+
`serialNo`, a command without `deviceId` is ignored (never guessed). Send both
|
|
84
|
+
(`nativeVersion` ≥ 0.17.0).
|
|
85
|
+
|
|
33
86
|
**Check:** `typeof` at the call site. `serialNo: "1234"` fails; `serialNo: 1234`
|
|
34
87
|
works.
|
|
35
88
|
|
|
@@ -38,7 +91,10 @@ works.
|
|
|
38
91
|
`requestPermission` takes a name the native side recognizes. Anything else is
|
|
39
92
|
dropped.
|
|
40
93
|
|
|
41
|
-
**Check:** the
|
|
94
|
+
**Check:** that the name came from `systemStatus.missingEssentialPermissions`
|
|
95
|
+
or `missingOptionalPermissions` — those arrays are the authoritative list of
|
|
96
|
+
names, so pass them through unchanged and never type one by hand. Show a
|
|
97
|
+
generic label for a name you do not recognise. And note there is
|
|
42
98
|
no permission-result event at all — the outcome reaches you as a fresh
|
|
43
99
|
`systemStatus` emission. If you were waiting for a dedicated ack, you will wait
|
|
44
100
|
forever.
|
|
@@ -52,7 +108,9 @@ surfaces as a `console.warn` and nothing else — `send` is fire-and-forget.
|
|
|
52
108
|
`dfu` is denied outright. Subscribing to a key outside the allowed set produces
|
|
53
109
|
`nack` with reason `not-read-only`.
|
|
54
110
|
|
|
55
|
-
**Check:** the browser console for a warning starting with `BridgeClient
|
|
111
|
+
**Check:** the browser console for a warning starting with `[BridgeClient]`. On
|
|
112
|
+
a device that console is only visible through the WebView inspector
|
|
113
|
+
(`docs/device-testing.md`).
|
|
56
114
|
Then re-read whether that command needs confirmation.
|
|
57
115
|
|
|
58
116
|
### 6. You bypassed `BridgeClient`
|
|
@@ -88,25 +146,30 @@ the ones that cause rejection:
|
|
|
88
146
|
|
|
89
147
|
If `validate` passes and the app still falls back, the bundle never arrived:
|
|
90
148
|
check the manifest, the `bundleHash` (it must match the SHA-256 of the exact
|
|
91
|
-
`.zip` bytes you uploaded), and `minAppVersion`
|
|
149
|
+
`.zip` bytes you uploaded), and `minAppVersion` — the minimum app build Condor
|
|
150
|
+
sets when publishing, for a bundle that needs a newer app — against the app build on the
|
|
92
151
|
device.
|
|
93
152
|
|
|
94
153
|
## "It works in the browser but not on the device"
|
|
95
154
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
**`nativeVersion` is `null` in mock mode.** Code that reads it without a null
|
|
99
|
-
branch behaves differently in the two environments. Test both.
|
|
155
|
+
First question: **which browser setup?**
|
|
100
156
|
|
|
101
|
-
**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
- **A plain tab or your own dev server** (`vite dev`, opening `dist/index.html`):
|
|
158
|
+
`BridgeClient.handshake()` rejects at once, because there is no shell. If you
|
|
159
|
+
built a mock of your own to get past that, it is more permissive than the
|
|
160
|
+
device in every way that matters: real origin (storage works), no CSP (CDN
|
|
161
|
+
fonts and `fetch` work), instant replies (nothing is lost).
|
|
162
|
+
- **The harness** (`npx condor-mobile-bridge dev`): same sandbox, shell, policy
|
|
163
|
+
and CSP as the app. What it cannot reproduce is timing you did not ask for —
|
|
164
|
+
run every scenario, not just `default`. `cold-start` and `no-devices` are the
|
|
165
|
+
ones that catch real bugs.
|
|
105
166
|
|
|
106
|
-
|
|
107
|
-
|
|
167
|
+
In the harness `nativeVersion` is `0.17.0` by default. Run it with
|
|
168
|
+
`--native-version null` to reproduce a shell that could not reach the native
|
|
169
|
+
side; keep a branch that renders in that case too.
|
|
108
170
|
|
|
109
|
-
Run `validate --strict` before concluding that something works
|
|
171
|
+
Run `validate --strict` before concluding that something works, and see
|
|
172
|
+
`docs/device-testing.md` to inspect the bundle on a phone.
|
|
110
173
|
|
|
111
174
|
## "The screen is blank"
|
|
112
175
|
|
|
@@ -144,7 +207,9 @@ Use the bridge:
|
|
|
144
207
|
BridgeClient.send('jsError', { message: String(err), stack: err?.stack });
|
|
145
208
|
```
|
|
146
209
|
|
|
147
|
-
That reaches Condor's logging and Crashlytics
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
210
|
+
That reaches Condor's logging and Crashlytics — **Condor reads them, you do
|
|
211
|
+
not**; ask your Condor contact for the errors of a test account. It is rate
|
|
212
|
+
limited to 20 per 10 seconds and truncated at 4 KB per field, so do not use it
|
|
213
|
+
as a general telemetry channel — it will drop exactly when something is going
|
|
214
|
+
wrong in a loop. To watch errors live, inspect the WebView: see
|
|
215
|
+
`docs/device-testing.md`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AGENTS.md — minimal Canvas home
|
|
2
2
|
|
|
3
|
-
This is a working Condor Canvas client
|
|
4
|
-
one.
|
|
3
|
+
This is a working Condor Canvas client with the reference boot sequence. Copy it
|
|
4
|
+
as the starting point for a new one.
|
|
5
5
|
|
|
6
6
|
The full contract you must respect is in the SDK package:
|
|
7
7
|
`node_modules/@condorinst/mobile-bridge/AGENTS.md`. Read it before changing
|
|
@@ -12,17 +12,20 @@ anything here. The short version:
|
|
|
12
12
|
- Everything inline: one self-contained `index.html`, built single-chunk.
|
|
13
13
|
- Failures are silent. Drive the UI from events that arrive; never infer success
|
|
14
14
|
from the absence of an error.
|
|
15
|
-
-
|
|
15
|
+
- Ask for devices with `BridgeClient.requestUserDevices()`, never with a single
|
|
16
|
+
`send('userDevice')`, and always render an empty state for its timeout.
|
|
17
|
+
- Identify devices by `deviceId`; `serialNo` can repeat. Name and battery are in
|
|
18
|
+
`devicesTelemetry`, not in `devicesConfig`.
|
|
16
19
|
|
|
17
20
|
## Commands
|
|
18
21
|
|
|
19
22
|
```bash
|
|
20
23
|
npm run build # esbuild → dist/index.html (single chunk)
|
|
24
|
+
npm run dev # harness at http://localhost:5173 (scenario cold-start; switch at the top)
|
|
21
25
|
npm run bundle # → dist/bundle.zip
|
|
22
26
|
npm run typecheck
|
|
23
27
|
npx condor-mobile-bridge validate -i dist/bundle.zip --strict # must exit 0
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
there — the same value a real but older app reports, so handle it.
|
|
30
|
+
Done means: `validate --strict` exits 0 and the page behaves in every harness
|
|
31
|
+
scenario — `default`, `cold-start`, `no-devices`, `shared-serial`, `offline`.
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
A runnable, standalone example of a White Label Home built on
|
|
4
4
|
[`@condorinst/mobile-bridge`](../../README.md). It renders a device list,
|
|
5
5
|
handshakes with the native side, listens for connection/real-time events, and
|
|
6
|
-
sends commands back — all through `BridgeClient
|
|
6
|
+
sends commands back — all through `BridgeClient`, with the boot sequence that
|
|
7
|
+
survives a cold start and an account with no device.
|
|
7
8
|
|
|
8
9
|
Use it as a starting skeleton for your own bundle. See the
|
|
9
10
|
[Client Guide](../../docs/client-guide.md) for the full walkthrough.
|
|
@@ -13,12 +14,16 @@ Use it as a starting skeleton for your own bundle. See the
|
|
|
13
14
|
```bash
|
|
14
15
|
npm install
|
|
15
16
|
npm run build # bundles src/main.ts → dist/main.js + dist/index.html
|
|
17
|
+
npm run dev # serves it in the harness at http://localhost:5173
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
The harness runs the page inside the same sandboxed iframe, shell and policy as
|
|
21
|
+
the app, with a simulated app behind it. Switch scenarios at the top:
|
|
22
|
+
`cold-start` loses the first device requests, `no-devices` never answers —
|
|
23
|
+
this example handles both, which is the point of copying it.
|
|
24
|
+
|
|
25
|
+
Opening `dist/index.html` directly does not work: outside the shell,
|
|
26
|
+
`BridgeClient.handshake()` rejects at once.
|
|
22
27
|
|
|
23
28
|
## Produce the app bundle
|
|
24
29
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* into `index.html` and pack the self-contained `dist/bundle.zip` the app
|
|
9
9
|
* consumes.
|
|
10
10
|
*
|
|
11
|
-
* For browser dev: `npm run build` then
|
|
11
|
+
* For browser dev: `npm run build` then `npm run dev` (the harness, which runs
|
|
12
|
+
* the page under the same sandbox and shell as the app).
|
|
12
13
|
*/
|
|
13
14
|
import * as esbuild from 'esbuild';
|
|
14
15
|
import { copyFileSync, mkdirSync } from 'node:fs';
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
.toolbar button:active { background: #334155; }
|
|
39
39
|
main { padding: 12px 16px 24px; }
|
|
40
40
|
.empty { text-align: center; color: #94a3b8; padding: 32px 16px; }
|
|
41
|
+
.empty button { background: #0d9488; color: #fff; border: none; border-radius: 9px; padding: 10px 16px; font-weight: 600; }
|
|
42
|
+
.meta { margin: 8px 0 0; font-size: 13px; color: #94a3b8; }
|
|
41
43
|
.card {
|
|
42
44
|
background: #1e293b;
|
|
43
45
|
border: 1px solid #334155;
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "node build.mjs",
|
|
9
9
|
"bundle": "condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip",
|
|
10
|
-
"typecheck": "tsc --noEmit"
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"dev": "condor-mobile-bridge dev -i dist/index.html --scenario cold-start"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=18"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"@condorinst/mobile-bridge": "^0.
|
|
17
|
+
"@condorinst/mobile-bridge": "^0.21.0"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
19
20
|
"@types/node": "^22.0.0",
|
|
@@ -1,80 +1,155 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Minimal
|
|
2
|
+
* Minimal Canvas Home — the reference boot sequence for @condorinst/mobile-bridge.
|
|
3
3
|
*
|
|
4
|
-
* This is the kind of code a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (postMessage → shell → native bridge).
|
|
4
|
+
* This is the kind of code a Canvas client writes: it runs inside the app's
|
|
5
|
+
* sandboxed iframe (origin `null`, no network, no storage) and talks to the
|
|
6
|
+
* native side only through `BridgeClient`.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Develop it in the browser with the harness, which runs it under the same
|
|
9
|
+
* sandbox and shell as the app:
|
|
10
|
+
*
|
|
11
|
+
* npm run build
|
|
12
|
+
* npx condor-mobile-bridge dev -i dist/index.html --scenario cold-start
|
|
13
|
+
*
|
|
14
|
+
* It must work in every scenario (`default`, `cold-start`, `no-devices`,
|
|
15
|
+
* `shared-serial`, `offline`) before it goes to a device.
|
|
12
16
|
*/
|
|
13
17
|
import {
|
|
14
18
|
BRIDGE_KEYS,
|
|
15
19
|
BridgeClient,
|
|
20
|
+
type DeviceConfigItem,
|
|
21
|
+
type DeviceTelemetryItem,
|
|
16
22
|
type UserDevicePayload,
|
|
23
|
+
versionAtLeast,
|
|
17
24
|
} from '@condorinst/mobile-bridge';
|
|
18
25
|
|
|
19
26
|
interface DeviceRow {
|
|
27
|
+
deviceId: number;
|
|
20
28
|
serialNo: number;
|
|
21
29
|
name: string;
|
|
30
|
+
battery: number | null;
|
|
22
31
|
connected: boolean;
|
|
23
32
|
realTimeOn: boolean;
|
|
24
33
|
}
|
|
25
34
|
|
|
35
|
+
type Phase = 'loading' | 'ready' | 'empty' | 'outside-shell';
|
|
36
|
+
|
|
26
37
|
const state = {
|
|
38
|
+
phase: 'loading' as Phase,
|
|
39
|
+
/** `null` = unknown (older shell, or the native side did not answer). */
|
|
40
|
+
nativeVersion: null as string | null,
|
|
41
|
+
/** Whether the last timeout came with `systemStatus.hasDevices === false`. */
|
|
42
|
+
probablyNoDevices: false,
|
|
27
43
|
devices: new Map<number, DeviceRow>(),
|
|
28
|
-
shellVersion: '—',
|
|
29
|
-
connected: false,
|
|
30
44
|
};
|
|
31
45
|
|
|
32
46
|
declare global {
|
|
33
47
|
interface Window {
|
|
34
48
|
__home: {
|
|
35
|
-
|
|
49
|
+
retry: () => void;
|
|
36
50
|
settings: () => void;
|
|
37
|
-
toggle: (
|
|
51
|
+
toggle: (deviceId: number) => void;
|
|
38
52
|
};
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
55
|
|
|
56
|
+
// ── Data ───────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Joins the three arrays of `userDevice`. `deviceId` is the key: an ActLumus
|
|
60
|
+
* and an ActLumus Plus may share a `serialNo`. Fall back to `serialNo` only for
|
|
61
|
+
* an item without `deviceId` whose `serialNo` is unique.
|
|
62
|
+
*/
|
|
63
|
+
function telemetryFor(
|
|
64
|
+
cfg: DeviceConfigItem,
|
|
65
|
+
payload: UserDevicePayload,
|
|
66
|
+
): DeviceTelemetryItem | undefined {
|
|
67
|
+
const byId = payload.devicesTelemetry.find((t) => t.deviceId === cfg.deviceId);
|
|
68
|
+
if (byId) return byId;
|
|
69
|
+
const sameSerial = payload.devicesConfig.filter((c) => c.serialNo === cfg.serialNo);
|
|
70
|
+
if (sameSerial.length !== 1) return undefined;
|
|
71
|
+
return payload.devicesTelemetry.find(
|
|
72
|
+
(t) => t.deviceId === undefined && t.serialNo === cfg.serialNo,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function applyDevices(payload: UserDevicePayload): void {
|
|
77
|
+
const next = new Map<number, DeviceRow>();
|
|
78
|
+
for (const cfg of payload.devicesConfig) {
|
|
79
|
+
const telemetry = telemetryFor(cfg, payload);
|
|
80
|
+
const previous = state.devices.get(cfg.deviceId);
|
|
81
|
+
next.set(cfg.deviceId, {
|
|
82
|
+
deviceId: cfg.deviceId,
|
|
83
|
+
serialNo: cfg.serialNo,
|
|
84
|
+
name: telemetry?.deviceName ?? `Device ${cfg.serialNo}`,
|
|
85
|
+
battery: telemetry?.batteryLevel ?? null,
|
|
86
|
+
connected: previous?.connected ?? false,
|
|
87
|
+
realTimeOn: previous?.realTimeOn ?? false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
state.devices = next;
|
|
91
|
+
state.phase = next.size > 0 ? 'ready' : 'empty';
|
|
92
|
+
render();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `deviceId` in commands needs nativeVersion >= 0.17.0; `serialNo` always goes.
|
|
97
|
+
* Below that, a shared serial cannot be addressed at all.
|
|
98
|
+
*/
|
|
99
|
+
function address(d: DeviceRow): { serialNo: number; deviceId?: number } {
|
|
100
|
+
return versionAtLeast(state.nativeVersion, '0.17.0')
|
|
101
|
+
? { serialNo: d.serialNo, deviceId: d.deviceId }
|
|
102
|
+
: { serialNo: d.serialNo };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Events carry `deviceId` only when the serial is shared; resolve both ways. */
|
|
106
|
+
function findRow(serialNo: number, deviceId?: number): DeviceRow | undefined {
|
|
107
|
+
if (deviceId !== undefined) return state.devices.get(deviceId);
|
|
108
|
+
return [...state.devices.values()].find((d) => d.serialNo === serialNo);
|
|
109
|
+
}
|
|
110
|
+
|
|
42
111
|
// ── Render ─────────────────────────────────────────────────────────────
|
|
43
112
|
|
|
44
113
|
function esc(value: string): string {
|
|
45
114
|
return value.replace(
|
|
46
115
|
/[&<>"]/g,
|
|
47
|
-
(c) =>
|
|
48
|
-
({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] as string,
|
|
116
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] as string,
|
|
49
117
|
);
|
|
50
118
|
}
|
|
51
119
|
|
|
52
120
|
function render(): void {
|
|
53
121
|
const el = document.getElementById('app');
|
|
54
122
|
if (!el) return;
|
|
55
|
-
|
|
56
|
-
const rows = [...state.devices.values()];
|
|
57
|
-
const cards = rows.length
|
|
58
|
-
? rows.map(renderCard).join('')
|
|
59
|
-
: '<p class="empty">No devices for this user yet.</p>';
|
|
60
|
-
|
|
61
123
|
el.innerHTML = `
|
|
62
124
|
<header class="hero">
|
|
63
|
-
<h1>My
|
|
64
|
-
<p class="status
|
|
65
|
-
state.connected
|
|
66
|
-
? `bridge connected • shell ${esc(state.shellVersion)}`
|
|
67
|
-
: 'connecting to the native bridge…'
|
|
68
|
-
}</p>
|
|
125
|
+
<h1>My Canvas Home</h1>
|
|
126
|
+
<p class="status">${state.nativeVersion ? `app ${esc(state.nativeVersion)}` : ''}</p>
|
|
69
127
|
</header>
|
|
70
128
|
<div class="toolbar">
|
|
71
|
-
<button onclick="__home.
|
|
129
|
+
<button onclick="__home.retry()">Refresh</button>
|
|
72
130
|
<button onclick="__home.settings()">Settings</button>
|
|
73
131
|
</div>
|
|
74
|
-
<main>${
|
|
132
|
+
<main>${body()}</main>
|
|
75
133
|
`;
|
|
76
134
|
}
|
|
77
135
|
|
|
136
|
+
function body(): string {
|
|
137
|
+
switch (state.phase) {
|
|
138
|
+
case 'outside-shell':
|
|
139
|
+
return '<p class="empty">Open this page with <code>npx condor-mobile-bridge dev</code>.</p>';
|
|
140
|
+
case 'loading':
|
|
141
|
+
return '<p class="empty">Loading your devices…</p>';
|
|
142
|
+
case 'empty':
|
|
143
|
+
return `<p class="empty">${
|
|
144
|
+
state.probablyNoDevices
|
|
145
|
+
? 'No device is linked to your account yet.'
|
|
146
|
+
: 'Could not load your devices.'
|
|
147
|
+
}</p><p class="empty"><button onclick="__home.retry()">Try again</button></p>`;
|
|
148
|
+
case 'ready':
|
|
149
|
+
return [...state.devices.values()].map(renderCard).join('');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
78
153
|
function renderCard(d: DeviceRow): string {
|
|
79
154
|
return `
|
|
80
155
|
<section class="card">
|
|
@@ -84,8 +159,9 @@ function renderCard(d: DeviceRow): string {
|
|
|
84
159
|
d.connected ? '● connected' : '○ disconnected'
|
|
85
160
|
}</span>
|
|
86
161
|
</div>
|
|
162
|
+
<p class="meta">Battery: ${d.battery === null ? '—' : `${Math.round(d.battery)}%`}</p>
|
|
87
163
|
<div class="actions">
|
|
88
|
-
<button onclick="__home.toggle(${d.
|
|
164
|
+
<button onclick="__home.toggle(${d.deviceId})">${
|
|
89
165
|
d.realTimeOn ? 'Stop real-time' : 'Start real-time'
|
|
90
166
|
}</button>
|
|
91
167
|
</div>
|
|
@@ -93,69 +169,71 @@ function renderCard(d: DeviceRow): string {
|
|
|
93
169
|
`;
|
|
94
170
|
}
|
|
95
171
|
|
|
96
|
-
// ── Subscribers (native → iframe)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const serialNo = Number((cfg as { serialNo?: number }).serialNo);
|
|
101
|
-
if (Number.isNaN(serialNo)) continue;
|
|
102
|
-
const existing = state.devices.get(serialNo);
|
|
103
|
-
state.devices.set(serialNo, {
|
|
104
|
-
serialNo,
|
|
105
|
-
name: String((cfg as { deviceName?: string }).deviceName ?? 'Device'),
|
|
106
|
-
connected: existing?.connected ?? false,
|
|
107
|
-
realTimeOn: existing?.realTimeOn ?? false,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
render();
|
|
111
|
-
});
|
|
172
|
+
// ── Subscribers (native → iframe) — before any command ─────────────────
|
|
173
|
+
|
|
174
|
+
// Roster changes arrive here too, not only replies to requestUserDevices().
|
|
175
|
+
BridgeClient.on(BRIDGE_KEYS.userDevice, applyDevices);
|
|
112
176
|
|
|
113
177
|
BridgeClient.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
114
|
-
const d =
|
|
115
|
-
if (d)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
178
|
+
const d = findRow(payload.serialNo, payload.deviceId);
|
|
179
|
+
if (!d) return;
|
|
180
|
+
d.connected = payload.isConnect;
|
|
181
|
+
render();
|
|
119
182
|
});
|
|
120
183
|
|
|
121
184
|
BridgeClient.on(BRIDGE_KEYS.realTime, (payload) => {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
render();
|
|
127
|
-
}
|
|
185
|
+
const d = findRow(payload.serialNo, payload.deviceId);
|
|
186
|
+
if (!d || !payload.success) return;
|
|
187
|
+
d.realTimeOn = payload.value;
|
|
188
|
+
render();
|
|
128
189
|
});
|
|
129
190
|
|
|
130
191
|
// ── Actions (iframe → native) ───────────────────────────────────────────
|
|
131
192
|
|
|
193
|
+
async function loadDevices(getFromServer: boolean): Promise<void> {
|
|
194
|
+
const result = await BridgeClient.requestUserDevices({ getFromServer });
|
|
195
|
+
if (result.status === 'ok') return; // applyDevices already rendered it
|
|
196
|
+
state.probablyNoDevices = result.hasDevices === false;
|
|
197
|
+
state.phase = 'empty';
|
|
198
|
+
render();
|
|
199
|
+
}
|
|
200
|
+
|
|
132
201
|
window.__home = {
|
|
133
|
-
|
|
202
|
+
retry: () => {
|
|
203
|
+
state.phase = 'loading';
|
|
204
|
+
render();
|
|
205
|
+
void loadDevices(true);
|
|
206
|
+
},
|
|
134
207
|
settings: () => BridgeClient.openSettings(),
|
|
135
|
-
toggle: (
|
|
136
|
-
const d = state.devices.get(
|
|
137
|
-
|
|
208
|
+
toggle: (deviceId) => {
|
|
209
|
+
const d = state.devices.get(deviceId);
|
|
210
|
+
if (!d) return;
|
|
211
|
+
BridgeClient.send('realTime', { ...address(d), value: !d.realTimeOn });
|
|
138
212
|
},
|
|
139
213
|
};
|
|
140
214
|
|
|
141
215
|
// ── Boot ────────────────────────────────────────────────────────────────
|
|
142
216
|
|
|
143
|
-
function boot(): void {
|
|
217
|
+
async function boot(): Promise<void> {
|
|
144
218
|
render();
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
219
|
+
try {
|
|
220
|
+
const ack = await BridgeClient.handshake({ clientVersion: '1.0.0' });
|
|
221
|
+
state.nativeVersion = ack.nativeVersion; // branch on this; the accepted flag is always true
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error('[home] handshake failed:', err);
|
|
224
|
+
state.phase = 'outside-shell';
|
|
225
|
+
render();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await loadDevices(false);
|
|
229
|
+
for (const d of state.devices.values()) {
|
|
230
|
+
// `add` subscribes to the connection state of one device; it pairs nothing.
|
|
231
|
+
BridgeClient.send('deviceConnection', { type: 'add', ...address(d) });
|
|
232
|
+
}
|
|
155
233
|
}
|
|
156
234
|
|
|
157
235
|
if (document.readyState === 'loading') {
|
|
158
|
-
document.addEventListener('DOMContentLoaded', boot);
|
|
236
|
+
document.addEventListener('DOMContentLoaded', () => void boot());
|
|
159
237
|
} else {
|
|
160
|
-
boot();
|
|
238
|
+
void boot();
|
|
161
239
|
}
|