@condorinst/mobile-bridge 0.18.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 +57 -0
- package/README.md +66 -11
- 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 +64 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +442 -396
- package/dist/index.d.ts +442 -396
- package/dist/index.js +64 -64
- 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,150 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## "My command did nothing"
|
|
4
|
+
|
|
5
|
+
This is the most common report, and it is usually correct: the command did
|
|
6
|
+
nothing, and nothing told you. The native side drops malformed or unauthorized
|
|
7
|
+
traffic with a log on the device and no reply to JavaScript.
|
|
8
|
+
|
|
9
|
+
There are six paths to silence. Work down the list.
|
|
10
|
+
|
|
11
|
+
### 1. The payload was over 64 KB
|
|
12
|
+
|
|
13
|
+
The whole JSON message is measured, not just your arguments. Over the limit it
|
|
14
|
+
is discarded before dispatch.
|
|
15
|
+
|
|
16
|
+
**Check:** `JSON.stringify(args).length` at the call site.
|
|
17
|
+
|
|
18
|
+
### 2. The command name is unknown to this device
|
|
19
|
+
|
|
20
|
+
A typo, or — more often — a command that exists in a newer native version than
|
|
21
|
+
the device runs. The native switch has no default branch that reports back.
|
|
22
|
+
|
|
23
|
+
**Check:** the command against `docs/bridge-contract.md`, then check
|
|
24
|
+
`ack.nativeVersion` from your handshake against the capability matrix. A device
|
|
25
|
+
in the field can be several versions behind your development device.
|
|
26
|
+
|
|
27
|
+
### 3. The arguments had the wrong shape
|
|
28
|
+
|
|
29
|
+
`realTime`, `syncTime`, `deleteMemory` and `deviceConnection` validate their
|
|
30
|
+
payload natively and drop it on mismatch. `serialNo` is an **integer**, not a
|
|
31
|
+
string; `value` and `canGoBack` are **booleans**, not strings.
|
|
32
|
+
|
|
33
|
+
**Check:** `typeof` at the call site. `serialNo: "1234"` fails; `serialNo: 1234`
|
|
34
|
+
works.
|
|
35
|
+
|
|
36
|
+
### 4. The permission name is not in the native enum
|
|
37
|
+
|
|
38
|
+
`requestPermission` takes a name the native side recognizes. Anything else is
|
|
39
|
+
dropped.
|
|
40
|
+
|
|
41
|
+
**Check:** the permission list in `docs/bridge-contract.md`. And note there is
|
|
42
|
+
no permission-result event at all — the outcome reaches you as a fresh
|
|
43
|
+
`systemStatus` emission. If you were waiting for a dedicated ack, you will wait
|
|
44
|
+
forever.
|
|
45
|
+
|
|
46
|
+
### 5. The command was denied, or the person declined
|
|
47
|
+
|
|
48
|
+
`syncTime`, `deleteMemory` and `logout` open a Condor-owned confirmation modal.
|
|
49
|
+
Declining produces a `nack` with reason `user-rejected`, which `BridgeClient`
|
|
50
|
+
surfaces as a `console.warn` and nothing else — `send` is fire-and-forget.
|
|
51
|
+
|
|
52
|
+
`dfu` is denied outright. Subscribing to a key outside the allowed set produces
|
|
53
|
+
`nack` with reason `not-read-only`.
|
|
54
|
+
|
|
55
|
+
**Check:** the browser console for a warning starting with `BridgeClient:`.
|
|
56
|
+
Then re-read whether that command needs confirmation.
|
|
57
|
+
|
|
58
|
+
### 6. You bypassed `BridgeClient`
|
|
59
|
+
|
|
60
|
+
A hand-rolled `window.parent.postMessage` almost certainly fails the Valibot
|
|
61
|
+
schema at the shell boundary and is dropped without a reply. The shell validates
|
|
62
|
+
every message before it touches the native bridge.
|
|
63
|
+
|
|
64
|
+
**Check:** that every message goes through `BridgeClient`.
|
|
65
|
+
|
|
66
|
+
There is a seventh path you cannot cause by accident but should know about: a
|
|
67
|
+
message that reaches the native channel without the shell's nonce is dropped
|
|
68
|
+
and logged as a security event. This is what happens if you create a nested
|
|
69
|
+
iframe and try to talk to the native side from inside it.
|
|
70
|
+
|
|
71
|
+
## "My bundle was rejected and the app shows its default screen"
|
|
72
|
+
|
|
73
|
+
The app validates the bundle on download and keeps the previous version — or
|
|
74
|
+
falls back — on any failure.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx condor-mobile-bridge validate -i dist/bundle.zip --strict
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This applies the same gates. The failures it reports as **errors** are exactly
|
|
81
|
+
the ones that cause rejection:
|
|
82
|
+
|
|
83
|
+
| Finding | Cause |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| `too-large` | The `.zip` is over 2 MB. |
|
|
86
|
+
| `entry-missing` | No `.html` document in the archive. |
|
|
87
|
+
| `not-self-contained` | A `<script src>` or `<link rel="stylesheet">` pointing outside a `data:` URI. |
|
|
88
|
+
|
|
89
|
+
If `validate` passes and the app still falls back, the bundle never arrived:
|
|
90
|
+
check the manifest, the `bundleHash` (it must match the SHA-256 of the exact
|
|
91
|
+
`.zip` bytes you uploaded), and `minAppVersion` against the app build on the
|
|
92
|
+
device.
|
|
93
|
+
|
|
94
|
+
## "It works in the browser but not on the device"
|
|
95
|
+
|
|
96
|
+
The browser mock is more permissive than the device in three ways that matter.
|
|
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.
|
|
100
|
+
|
|
101
|
+
**Storage works in your browser and does not on the device.** Your dev server
|
|
102
|
+
serves from a real origin; the device gives you a `null` one. Anything that
|
|
103
|
+
touches `localStorage` works locally and throws in production. `validate` warns
|
|
104
|
+
about this — do not dismiss the warning.
|
|
105
|
+
|
|
106
|
+
**The CSP is not applied by your dev server.** External fonts, CDN scripts and
|
|
107
|
+
API calls all work locally and are blocked on the device.
|
|
108
|
+
|
|
109
|
+
Run `validate --strict` before concluding that something works.
|
|
110
|
+
|
|
111
|
+
## "The screen is blank"
|
|
112
|
+
|
|
113
|
+
Almost always the bundle build, not the bridge.
|
|
114
|
+
|
|
115
|
+
**Code splitting.** If your bundler emitted more than one chunk, or ES module
|
|
116
|
+
imports between chunks, `srcdoc` cannot resolve them and nothing executes. Build
|
|
117
|
+
single-file: Vite with `vite-plugin-singlefile`, or an equivalent.
|
|
118
|
+
|
|
119
|
+
**A script that failed to parse.** Inlining a script that contains markup inside
|
|
120
|
+
JavaScript strings can break if done by a naive tool — use
|
|
121
|
+
`npx condor-mobile-bridge build`, which handles the escaping.
|
|
122
|
+
|
|
123
|
+
**First paint depends on the bridge.** If your render only runs inside an event
|
|
124
|
+
handler and no event has arrived yet, you get a blank screen. Render a skeleton
|
|
125
|
+
synchronously; fill it as events arrive.
|
|
126
|
+
|
|
127
|
+
## "An ack never arrived"
|
|
128
|
+
|
|
129
|
+
Check, in order: was the command one of the three that need confirmation
|
|
130
|
+
(the person may have declined); did the command reach the native side at all
|
|
131
|
+
(the six paths above); and is your `requestId` correlation correct for
|
|
132
|
+
`sensorLog` — the reply carries back the id you sent, and a mismatch looks
|
|
133
|
+
exactly like silence.
|
|
134
|
+
|
|
135
|
+
Always have a timeout. `BridgeClient.fetchSensorLog` has one built in; anything
|
|
136
|
+
you correlate by hand needs your own.
|
|
137
|
+
|
|
138
|
+
## Getting an error out of the device
|
|
139
|
+
|
|
140
|
+
You cannot reach an external logging service — `connect-src 'self'` blocks it.
|
|
141
|
+
Use the bridge:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
BridgeClient.send('jsError', { message: String(err), stack: err?.stack });
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
That reaches Condor's logging and Crashlytics. It is rate limited to 20 per
|
|
148
|
+
10 seconds and truncated at 4 KB per field, so do not use it as a general
|
|
149
|
+
telemetry channel — it will drop exactly when something is going wrong in a
|
|
150
|
+
loop.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# AGENTS.md — minimal Canvas home
|
|
2
|
+
|
|
3
|
+
This is a working Condor Canvas client. Copy it as the starting point for a new
|
|
4
|
+
one.
|
|
5
|
+
|
|
6
|
+
The full contract you must respect is in the SDK package:
|
|
7
|
+
`node_modules/@condorinst/mobile-bridge/AGENTS.md`. Read it before changing
|
|
8
|
+
anything here. The short version:
|
|
9
|
+
|
|
10
|
+
- No storage, no network, no `eval` — the iframe has a `null` origin and
|
|
11
|
+
inherits a CSP that blocks all three.
|
|
12
|
+
- Everything inline: one self-contained `index.html`, built single-chunk.
|
|
13
|
+
- Failures are silent. Drive the UI from events that arrive; never infer success
|
|
14
|
+
from the absence of an error.
|
|
15
|
+
- `serialNo` is a number, never a string.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run build # esbuild → dist/index.html (single chunk)
|
|
21
|
+
npm run bundle # → dist/bundle.zip
|
|
22
|
+
npm run typecheck
|
|
23
|
+
npx condor-mobile-bridge validate -i dist/bundle.zip --strict # must exit 0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Open `dist/index.html` in a browser to develop against the mock, including the
|
|
27
|
+
dev panel that emits native events by hand. Note that `nativeVersion` is `null`
|
|
28
|
+
there — the same value a real but older app reports, so handle it.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Minimal White Label Home — example
|
|
2
|
+
|
|
3
|
+
A runnable, standalone example of a White Label Home built on
|
|
4
|
+
[`@condorinst/mobile-bridge`](../../README.md). It renders a device list,
|
|
5
|
+
handshakes with the native side, listens for connection/real-time events, and
|
|
6
|
+
sends commands back — all through `BridgeClient`.
|
|
7
|
+
|
|
8
|
+
Use it as a starting skeleton for your own bundle. See the
|
|
9
|
+
[Client Guide](../../docs/client-guide.md) for the full walkthrough.
|
|
10
|
+
|
|
11
|
+
## Run it
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npm run build # bundles src/main.ts → dist/main.js + dist/index.html
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Open `dist/index.html` in any browser. Outside the app, the SDK activates
|
|
19
|
+
**mock mode** automatically: a dev panel appears (bottom-right) where you can
|
|
20
|
+
emit native events (e.g. `userDevice`, `deviceConnection`) by hand or enable
|
|
21
|
+
auto-respond to close the loop.
|
|
22
|
+
|
|
23
|
+
## Produce the app bundle
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run bundle # condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This inlines everything into a single self-contained `index.html` and packs
|
|
30
|
+
`dist/bundle.zip` — the artifact the app consumes. The command prints the
|
|
31
|
+
`bundleSize` and `bundleHash` to register with the backend.
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
minimal-home/
|
|
37
|
+
├── index.html # markup + styles; references main.js
|
|
38
|
+
├── src/main.ts # the client logic (BridgeClient)
|
|
39
|
+
├── build.mjs # esbuild bundle + copy index.html into dist/
|
|
40
|
+
├── package.json
|
|
41
|
+
└── tsconfig.json
|
|
42
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build.mjs — bundles the example into a `dist/` ready for the bundle CLI.
|
|
3
|
+
*
|
|
4
|
+
* 1. esbuild bundles `src/main.ts` (and the SDK) into `dist/main.js` (IIFE).
|
|
5
|
+
* 2. copies `index.html` into `dist/` (it references `main.js`).
|
|
6
|
+
*
|
|
7
|
+
* Then `npm run bundle` calls `condor-mobile-bridge build` to inline `main.js`
|
|
8
|
+
* into `index.html` and pack the self-contained `dist/bundle.zip` the app
|
|
9
|
+
* consumes.
|
|
10
|
+
*
|
|
11
|
+
* For browser dev: `npm run build` then open `dist/index.html` (mock mode).
|
|
12
|
+
*/
|
|
13
|
+
import * as esbuild from 'esbuild';
|
|
14
|
+
import { copyFileSync, mkdirSync } from 'node:fs';
|
|
15
|
+
|
|
16
|
+
mkdirSync('dist', { recursive: true });
|
|
17
|
+
|
|
18
|
+
await esbuild.build({
|
|
19
|
+
entryPoints: ['src/main.ts'],
|
|
20
|
+
outfile: 'dist/main.js',
|
|
21
|
+
bundle: true,
|
|
22
|
+
format: 'iife',
|
|
23
|
+
target: 'es2018',
|
|
24
|
+
platform: 'browser',
|
|
25
|
+
minify: true,
|
|
26
|
+
sourcemap: false,
|
|
27
|
+
logLevel: 'info',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
copyFileSync('index.html', 'dist/index.html');
|
|
31
|
+
console.log('Built dist/main.js + dist/index.html — now run `npm run bundle`.');
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
6
|
+
<!-- Status bar color (Layer A): the native app reads this from the bundle.
|
|
7
|
+
Keep it in sync with the top of the .hero gradient below. -->
|
|
8
|
+
<meta name="theme-color" content="#0d9488">
|
|
9
|
+
<title>My White Label Home</title>
|
|
10
|
+
<style>
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
font-family: -apple-system, system-ui, "Segoe UI", sans-serif;
|
|
15
|
+
background: #0f172a;
|
|
16
|
+
color: #e2e8f0;
|
|
17
|
+
-webkit-font-smoothing: antialiased;
|
|
18
|
+
}
|
|
19
|
+
.hero {
|
|
20
|
+
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
|
|
21
|
+
padding: 24px 20px 28px;
|
|
22
|
+
text-align: center;
|
|
23
|
+
}
|
|
24
|
+
.hero h1 { margin: 4px 0; font-size: 22px; font-weight: 700; color: #fff; }
|
|
25
|
+
.hero .status { margin: 8px 0 0; font-size: 12px; color: #fcd34d; }
|
|
26
|
+
.hero .status.ok { color: #bbf7d0; }
|
|
27
|
+
.toolbar { display: flex; gap: 10px; padding: 16px 20px 4px; }
|
|
28
|
+
.toolbar button {
|
|
29
|
+
flex: 1;
|
|
30
|
+
background: #1e293b;
|
|
31
|
+
color: #e2e8f0;
|
|
32
|
+
border: 1px solid #334155;
|
|
33
|
+
border-radius: 10px;
|
|
34
|
+
padding: 11px;
|
|
35
|
+
font-size: 14px;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
}
|
|
38
|
+
.toolbar button:active { background: #334155; }
|
|
39
|
+
main { padding: 12px 16px 24px; }
|
|
40
|
+
.empty { text-align: center; color: #94a3b8; padding: 32px 16px; }
|
|
41
|
+
.card {
|
|
42
|
+
background: #1e293b;
|
|
43
|
+
border: 1px solid #334155;
|
|
44
|
+
border-radius: 14px;
|
|
45
|
+
padding: 16px;
|
|
46
|
+
margin-bottom: 12px;
|
|
47
|
+
}
|
|
48
|
+
.card-head {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: space-between;
|
|
52
|
+
gap: 10px;
|
|
53
|
+
}
|
|
54
|
+
.card-head .name { font-size: 16px; font-weight: 700; color: #f8fafc; }
|
|
55
|
+
.card-head .name small { color: #64748b; font-weight: 500; }
|
|
56
|
+
.badge { font-size: 12px; font-weight: 600; padding: 3px 9px; border-radius: 999px; }
|
|
57
|
+
.badge.on { background: #064e3b; color: #6ee7b7; }
|
|
58
|
+
.badge.off { background: #3f1d1d; color: #fca5a5; }
|
|
59
|
+
.actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
60
|
+
.actions button {
|
|
61
|
+
flex: 1;
|
|
62
|
+
background: #0d9488;
|
|
63
|
+
color: #fff;
|
|
64
|
+
border: none;
|
|
65
|
+
border-radius: 9px;
|
|
66
|
+
padding: 10px;
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
}
|
|
70
|
+
.actions button:active { background: #0f766e; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div id="app"></div>
|
|
75
|
+
<script src="main.js"></script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whitelabel-minimal-home",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Minimal runnable White Label Home example for @condorinst/mobile-bridge.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "node build.mjs",
|
|
9
|
+
"bundle": "condor-mobile-bridge build -i dist/index.html -o dist/bundle.zip",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@condorinst/mobile-bridge": "^0.19.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"esbuild": "^0.25.0",
|
|
21
|
+
"typescript": "^5.6.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal White Label Home — runnable example for @condorinst/mobile-bridge.
|
|
3
|
+
*
|
|
4
|
+
* This is the kind of code a White Label client writes: it runs inside the
|
|
5
|
+
* app's sandboxed iframe (origin `null`, no `window.flutter_inappwebview`) and
|
|
6
|
+
* talks to the native side exclusively through `BridgeClient`
|
|
7
|
+
* (postMessage → shell → native bridge).
|
|
8
|
+
*
|
|
9
|
+
* Open `dist/index.html` in a browser and the SDK auto-activates **mock mode**,
|
|
10
|
+
* so you can develop the whole flow without building the app. Use the dev panel
|
|
11
|
+
* (bottom-right) to emit native events by hand or enable auto-respond.
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
BRIDGE_KEYS,
|
|
15
|
+
BridgeClient,
|
|
16
|
+
type UserDevicePayload,
|
|
17
|
+
} from '@condorinst/mobile-bridge';
|
|
18
|
+
|
|
19
|
+
interface DeviceRow {
|
|
20
|
+
serialNo: number;
|
|
21
|
+
name: string;
|
|
22
|
+
connected: boolean;
|
|
23
|
+
realTimeOn: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const state = {
|
|
27
|
+
devices: new Map<number, DeviceRow>(),
|
|
28
|
+
shellVersion: '—',
|
|
29
|
+
connected: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
declare global {
|
|
33
|
+
interface Window {
|
|
34
|
+
__home: {
|
|
35
|
+
refresh: () => void;
|
|
36
|
+
settings: () => void;
|
|
37
|
+
toggle: (serialNo: number) => void;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Render ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function esc(value: string): string {
|
|
45
|
+
return value.replace(
|
|
46
|
+
/[&<>"]/g,
|
|
47
|
+
(c) =>
|
|
48
|
+
({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] as string,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function render(): void {
|
|
53
|
+
const el = document.getElementById('app');
|
|
54
|
+
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
|
+
el.innerHTML = `
|
|
62
|
+
<header class="hero">
|
|
63
|
+
<h1>My White Label Home</h1>
|
|
64
|
+
<p class="status ${state.connected ? 'ok' : ''}">${
|
|
65
|
+
state.connected
|
|
66
|
+
? `bridge connected • shell ${esc(state.shellVersion)}`
|
|
67
|
+
: 'connecting to the native bridge…'
|
|
68
|
+
}</p>
|
|
69
|
+
</header>
|
|
70
|
+
<div class="toolbar">
|
|
71
|
+
<button onclick="__home.refresh()">Refresh devices</button>
|
|
72
|
+
<button onclick="__home.settings()">Settings</button>
|
|
73
|
+
</div>
|
|
74
|
+
<main>${cards}</main>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderCard(d: DeviceRow): string {
|
|
79
|
+
return `
|
|
80
|
+
<section class="card">
|
|
81
|
+
<div class="card-head">
|
|
82
|
+
<span class="name">${esc(d.name)} <small>#${d.serialNo}</small></span>
|
|
83
|
+
<span class="badge ${d.connected ? 'on' : 'off'}">${
|
|
84
|
+
d.connected ? '● connected' : '○ disconnected'
|
|
85
|
+
}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="actions">
|
|
88
|
+
<button onclick="__home.toggle(${d.serialNo})">${
|
|
89
|
+
d.realTimeOn ? 'Stop real-time' : 'Start real-time'
|
|
90
|
+
}</button>
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Subscribers (native → iframe) ───────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
BridgeClient.on(BRIDGE_KEYS.userDevice, (payload: UserDevicePayload) => {
|
|
99
|
+
for (const cfg of payload.devicesConfig ?? []) {
|
|
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
|
+
});
|
|
112
|
+
|
|
113
|
+
BridgeClient.on(BRIDGE_KEYS.deviceConnection, (payload) => {
|
|
114
|
+
const d = state.devices.get(payload.serialNo);
|
|
115
|
+
if (d) {
|
|
116
|
+
d.connected = payload.isConnect;
|
|
117
|
+
render();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
BridgeClient.on(BRIDGE_KEYS.realTime, (payload) => {
|
|
122
|
+
const { serialNo, success, value } = payload;
|
|
123
|
+
const d = state.devices.get(serialNo);
|
|
124
|
+
if (d && success) {
|
|
125
|
+
d.realTimeOn = value;
|
|
126
|
+
render();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── Actions (iframe → native) ───────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
window.__home = {
|
|
133
|
+
refresh: () => BridgeClient.send('userDevice', { getFromServer: true }),
|
|
134
|
+
settings: () => BridgeClient.openSettings(),
|
|
135
|
+
toggle: (serialNo) => {
|
|
136
|
+
const d = state.devices.get(serialNo);
|
|
137
|
+
BridgeClient.send('realTime', { serialNo, value: !(d?.realTimeOn ?? false) });
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// ── Boot ────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function boot(): void {
|
|
144
|
+
render();
|
|
145
|
+
BridgeClient.handshake({ clientVersion: '1.0.0' })
|
|
146
|
+
.then((ack) => {
|
|
147
|
+
state.connected = ack.accepted;
|
|
148
|
+
state.shellVersion = ack.shellVersion;
|
|
149
|
+
render();
|
|
150
|
+
})
|
|
151
|
+
.catch((err: unknown) => console.error('[home] handshake failed:', err));
|
|
152
|
+
|
|
153
|
+
// Ask the native side for the current device list.
|
|
154
|
+
BridgeClient.send('userDevice', { getFromServer: false });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (document.readyState === 'loading') {
|
|
158
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
159
|
+
} else {
|
|
160
|
+
boot();
|
|
161
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2018",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2018", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"types": ["node"]
|
|
12
|
+
},
|
|
13
|
+
"include": ["src", "build.mjs"]
|
|
14
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Condor Mobile Bridge
|
|
2
|
+
|
|
3
|
+
> TypeScript SDK for the JS↔Native bridge of the Condor Canvas white-label
|
|
4
|
+
> mobile app. A client builds a self-contained web bundle that renders as the
|
|
5
|
+
> app's Home inside a sandboxed iframe; this SDK is how that bundle talks to
|
|
6
|
+
> the native side — device roster, real-time light-sensor data, sensor log
|
|
7
|
+
> pagination, permissions, system status and language.
|
|
8
|
+
|
|
9
|
+
Every URL below is plain text over HTTPS, pinned to a released version, and
|
|
10
|
+
needs no authentication. Replace `0.19.0` with the version you depend on; the
|
|
11
|
+
contract changes between minors while the package is below 1.0.0.
|
|
12
|
+
|
|
13
|
+
The same files ship inside the npm tarball, so if you have run
|
|
14
|
+
`npm install @condorinst/mobile-bridge` you can read them from
|
|
15
|
+
`node_modules/@condorinst/mobile-bridge/` with no network access at all.
|
|
16
|
+
|
|
17
|
+
## Start here
|
|
18
|
+
|
|
19
|
+
- [AGENTS.md](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/AGENTS.md): The contract in one screen — hard rules, what is unreachable, and the validation loop. Read before writing code.
|
|
20
|
+
- [Agent guide](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/docs/agent-guide.md): Step-by-step construction of a client Home, with the checks to run at each stage.
|
|
21
|
+
|
|
22
|
+
## Reference
|
|
23
|
+
|
|
24
|
+
- [Bridge contract](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/docs/bridge-contract.md): Every observable key, every command, the three error shapes, and the capability matrix by native version.
|
|
25
|
+
- [Machine-readable contract](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/contract/bridge-contract.json): The same contract as JSON. Also importable as `@condorinst/mobile-bridge/contract`.
|
|
26
|
+
- [Limits](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/docs/limits.md): Every hard limit — payload size, rate limits, bundle size, timeouts — with what happens when you cross it.
|
|
27
|
+
- [Troubleshooting](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/docs/troubleshooting.md): Why a command did nothing. Silence is the platform's dominant failure mode.
|
|
28
|
+
- [TypeScript declarations](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/dist/index.d.ts): Authoritative payload shapes, with documentation on every member.
|
|
29
|
+
|
|
30
|
+
## Guides
|
|
31
|
+
|
|
32
|
+
- [Client guide](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/docs/client-guide.md): Narrative walkthrough for a developer building the Home by hand.
|
|
33
|
+
- [Pilot onboarding](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/docs/pilot-onboarding.md): Checklist from zero to a bundle delivered to Condor.
|
|
34
|
+
- [README](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/README.md): Package overview, install, quickstart.
|
|
35
|
+
- [CHANGELOG](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/CHANGELOG.md): When each capability was introduced. The most precise record of what a given native version supports.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
- [Minimal home](https://cdn.jsdelivr.net/npm/@condorinst/mobile-bridge@0.19.0/examples/minimal-home/src/main.ts): A complete, working client in ~160 lines. Copy this as your starting point.
|
|
40
|
+
|
|
41
|
+
## Optional
|
|
42
|
+
|
|
43
|
+
- [Public repository](https://bitbucket.org/condorinstprogdev/mobile-bridge): Full source, tests, and the larger `client_demo` example. Note that the repository landing page is a JavaScript application and returns no readable text to a fetcher — use the raw file URLs, or prefer the jsDelivr links above.
|
|
44
|
+
- [Package on npm](https://www.npmjs.com/package/@condorinst/mobile-bridge)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@condorinst/mobile-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "TypeScript SDK for the JS↔Native bridge of the Condor white-label mobile app.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"private": false,
|
|
@@ -16,13 +16,26 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"import": "./dist/index.js",
|
|
18
18
|
"require": "./dist/index.cjs"
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
|
+
"./contract": "./contract/bridge-contract.json",
|
|
21
|
+
"./package.json": "./package.json"
|
|
20
22
|
},
|
|
21
23
|
"bin": {
|
|
22
24
|
"condor-mobile-bridge": "dist/cli.js"
|
|
23
25
|
},
|
|
24
26
|
"files": [
|
|
25
27
|
"dist",
|
|
28
|
+
"docs",
|
|
29
|
+
"contract",
|
|
30
|
+
"examples/minimal-home/src",
|
|
31
|
+
"examples/minimal-home/index.html",
|
|
32
|
+
"examples/minimal-home/build.mjs",
|
|
33
|
+
"examples/minimal-home/package.json",
|
|
34
|
+
"examples/minimal-home/tsconfig.json",
|
|
35
|
+
"examples/minimal-home/README.md",
|
|
36
|
+
"examples/minimal-home/AGENTS.md",
|
|
37
|
+
"AGENTS.md",
|
|
38
|
+
"llms.txt",
|
|
26
39
|
"README.md",
|
|
27
40
|
"CHANGELOG.md",
|
|
28
41
|
"LICENSE"
|