@bobfrankston/msgapidefs 0.1.29 → 0.1.30

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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @bobfrankston/msgapidefs
2
2
 
3
- TypeScript definitions for the **msgapi** JavaScript API used in msgview and msger.
3
+ TypeScript definitions for the **msgapi** JavaScript API used in msgview and msger. See the Typescript definitions in
4
+ [msgdpidefs.ts](msgapidefs.ts) for more info
4
5
 
5
6
  ## Installation
6
7
 
@@ -34,8 +35,69 @@ localStorage.setItem('key', 'value'); // Instead of msgapi.saveData()
34
35
  window.close(); // Instead of msgapi.close() if not passing result
35
36
  ```
36
37
 
38
+ ## Implementation Status
39
+
40
+ ✅ = Implemented | ⚠️ = Partial | ❌ = Not implemented
41
+
42
+ | API | msgview | msger | msga | Notes |
43
+ |-----|:-------:|:-----:|:----:|-------|
44
+ | **Configuration** | | | | |
45
+ | `version` | ❌ | ❌ | ✅ | `window.msgapi.version` string |
46
+ | `setLogging(value?)` | ❌ | ❌ | ✅ | Returns previous state |
47
+ | `setAutoUpdate(value?)` | ❌ | ❌ | ✅ | `'auto'`/`'check'`/`'off'`; msga Android only |
48
+ | **Window Control** | | | | **`window.msgapi.*`** |
49
+ | `toggleFullscreen()` | ❌ | ⚠️ | ✅ | msger: via IPC, not on window.msgapi |
50
+ | `setFullscreen(bool)` | ✅ | ⚠️ | ✅ | msger: via IPC, not on window.msgapi |
51
+ | `minimize()` | ✅ | ⚠️ | ✅ | msga: Windows only; msger: via IPC |
52
+ | `maximize()` | ✅ | ⚠️ | ✅ | msga: Windows only; msger: via IPC |
53
+ | `setSize(w, h)` | ✅ | ⚠️ | ✅ | msga: Windows only; msger: via IPC |
54
+ | `setPosition(x, y)` | ✅ | ⚠️ | ✅ | msga: Windows only; msger: via IPC |
55
+ | `setAlwaysOnTop(bool)` | ✅ | ⚠️ | ❌ | msger: via IPC |
56
+ | `close(result?)` | ✅ | ✅ | ✅ | msger: on window.msger |
57
+ | `navigate(url)` | ❌ | ❌ | ✅ | msga only |
58
+ | `reload()` | ❌ | ❌ | ✅ | msga only |
59
+ | **File System** | | | | **`window.msgapi.fs.*`** |
60
+ | `fs.selectFile()` | ✅ | ✅ | ⚠️ | msga: C# exists, not wired to JS |
61
+ | `fs.selectFiles()` | ✅ | ✅ | ⚠️ | msga: C# exists, not wired to JS |
62
+ | `fs.saveFileAs()` | ✅ | ✅ | ❌ | |
63
+ | `fs.selectFolder()` | ✅ | ✅ | ❌ | |
64
+ | `fs.read(path)` | ✅ | ✅ | ❌ | Requires allowFs flag |
65
+ | `fs.write(path, content)` | ✅ | ✅ | ❌ | Requires allowFs flag |
66
+ | `fs.list(path)` | ✅ | ✅ | ❌ | Requires allowFs flag |
67
+ | `fs.exists(path)` | ✅ | ✅ | ❌ | Requires allowFs flag |
68
+ | `fs.delete(path)` | ✅ | ✅ | ❌ | Requires allowFs flag |
69
+ | **UDP Networking** | | | | **`window.msgapi.udp.*`** |
70
+ | `udp.send()` | ❌ | ❌ | ✅ | msga: C# UdpClient |
71
+ | `udp.listen()` | ❌ | ❌ | ✅ | msga: background receive loop |
72
+ | `udp.sendReceive()` | ❌ | ❌ | ✅ | msga: send + wait with timeout |
73
+ | `udp.broadcast()` | ❌ | ❌ | ✅ | msga: EnableBroadcast=true |
74
+
75
+ > **msger note:** Window control functionality exists in Rust but is exposed via `window.msger` + IPC actions (`window.ipc.postMessage({_action: ...})`), not as `window.msgapi` methods. Alignment to `window.msgapi` is needed.
76
+
77
+ [httpudp-client](../../homecontrol/utils/udp/httpudp-client/README.md) auto-detects `window.msgapi?.udp` and uses native UDP when available, falling back to the httpudp WebSocket proxy otherwise.
78
+
79
+ See [`msgapi-plan.md`](./msgapi-plan.md) for the full implementation plan and roadmap.
80
+
37
81
  ## API Reference
38
82
 
83
+ ### Configuration
84
+
85
+ - `version` - Host app version string (e.g., `"1.2.0"`)
86
+ - `setLogging(value?)` - Control debug logging. Returns previous state as string.
87
+ - `'msga'` - C# host logging only (logit-msga)
88
+ - `'msgapi'` - JS bridge logging only (logit-msgapi)
89
+ - `'on'` - Both streams
90
+ - `'off'` / `'none'` - All off
91
+ - `null` / `''` - Query current state without changing
92
+ - Returns: `'msga'`, `'msgapi'`, `'msga,msgapi'`, or `'none'`
93
+ - `setAutoUpdate(value?)` - Control APK auto-update behavior. Returns previous state as string. msga Android only.
94
+ - `'auto'` - Check versions.json, download + install silently (no prompt)
95
+ - `'check'` - Check versions.json, prompt "Install now?" before downloading
96
+ - `'off'` - No update checking
97
+ - `null` / `undefined` - Query current state without changing
98
+ - Default: `'auto'` (configurable via `AUTO_UPDATE` in msga.env)
99
+ - Checks on startup then every `UPDATE_INTERVAL_MINUTES` (default 60)
100
+
39
101
  ### Window Control
40
102
 
41
103
  - `toggleFullscreen()` - Toggle fullscreen mode
@@ -49,15 +111,29 @@ window.close(); // Instead of msgapi.close() if not passing result
49
111
 
50
112
  ### File System
51
113
 
114
+ File dialogs (user picks interactively — safe without special permissions):
52
115
  - `fs.selectFile(options?)` - Open file picker
53
116
  - `fs.selectFiles(options?)` - Open multi-file picker
54
117
  - `fs.saveFileAs(content, filename?, options?)` - Save file dialog
55
118
  - `fs.selectFolder(options?)` - Folder picker
56
- - `fs.read(path)` - Read file (requires allowFs flag)
57
- - `fs.write(path, content)` - Write file (requires allowFs flag)
58
- - `fs.list(path)` - List directory (requires allowFs flag)
59
- - `fs.exists(path)` - Check if file exists (requires allowFs flag)
60
- - `fs.delete(path)` - Delete file (requires allowFs flag)
119
+
120
+ Direct path operations (give the webview full filesystem access):
121
+ - `fs.read(path)` - Read file
122
+ - `fs.write(path, content)` - Write file
123
+ - `fs.list(path)` - List directory
124
+ - `fs.exists(path)` - Check if file exists
125
+ - `fs.delete(path)` - Delete file
126
+
127
+ > **`allowFs` flag:** Direct path operations are intended to require an explicit `--allowFs` CLI flag (e.g., `msger -url "..." --allowFs`) since they grant unrestricted filesystem access. This flag is documented in the interface but **not yet enforced** — the methods currently work without it in both msger and msgview. Enforcement is planned.
128
+
129
+ ### UDP Networking
130
+
131
+ Native UDP socket access — no WebSocket proxy needed when running inside a msgapi host.
132
+
133
+ - `udp.send(host, port, data)` - Send UDP packet to specific host
134
+ - `udp.listen(port, callback)` - Listen for incoming UDP packets (returns `{close()}` handle)
135
+ - `udp.sendReceive(host, port, data, timeoutMs?)` - Send and wait for response
136
+ - `udp.broadcast(port, data)` - Broadcast to local network
61
137
 
62
138
  ## Examples
63
139
 
@@ -86,10 +162,49 @@ The samples demonstrate:
86
162
  - And more!
87
163
 
88
164
 
165
+ ## Cross-Platform Implementation
166
+
167
+ `msgapidefs` defines the TypeScript interface only. Each host implements the `window.msgapi` bridge differently:
168
+
169
+ ### msgview (Electron)
170
+ - **Bridge:** Electron `contextBridge.exposeInMainWorld('msgapi', ...)` in `preload/preload.ts`
171
+ - **Mechanism:** `ipcRenderer.invoke()` calls to main process IPC handlers
172
+ - **Window control:** Electron `BrowserWindow` API
173
+ - **File system:** Node.js `fs` module via IPC, file dialogs via Electron `dialog`
174
+
175
+ ### msger (Rust/wry)
176
+ - **Bridge:** JavaScript injected via wry `with_initialization_script()` in `msger-native/src/main.rs`
177
+ - **JS API:** `msger-native/src/msger-api.js` creates `window.msger` object
178
+ - **Mechanism:** `window.ipc.postMessage(JSON.stringify(...))` to Rust IPC handler
179
+ - **Note:** Currently exposes `window.msger` (not `window.msgapi`) - alignment needed
180
+ - **Limitations:** wry 0.47.2 has IPC issues; `close()` uses `window.close()` workaround
181
+
182
+ ### msga (.NET MAUI)
183
+ - **Bridge:** JavaScript injected via `WebViewControl.EvaluateJavaScriptAsync()` in `MainPage.xaml.cs`
184
+ - **JS API:** `GetMsgApiScript()` creates `window.msgapi` object inline
185
+ - **Mechanism:** `msgapi://` URL interception — JS creates hidden iframes, C# intercepts in `OnNavigating`
186
+ - **Async:** Promise callbacks via `callNativeAsync()` → `window._msgapiResolve()` / `_msgapiReject()`
187
+ - **Window control:** Platform-conditional (`#if WINDOWS` uses WinUI AppWindow APIs)
188
+ - **File system:** MAUI `FilePicker`, Windows-specific `FileSavePicker`/`FolderPicker`
189
+ - **Platforms:** Android (primary target), Windows (development/testing)
190
+
191
+ ### Adding New Sub-Objects
192
+
193
+ Each capability is an optional sub-object on `MsgAPI` (e.g., `fs?`, `udp?`). To add a new one:
194
+ 1. Define the interface in [`msgapidefs.ts`](./msgapidefs.ts) as an optional sub-object on `MsgAPI`
195
+ 2. Implement the native side in each host:
196
+ - **msgview:** Add IPC handlers in `main.ts`, expose in `preload/preload.ts`
197
+ - **msger:** Add Rust handler in `main.rs`, expose in `msger-api.js`
198
+ - **msga:** Add C# methods + JS injection in `GetMsgApiScript()` (see [msga readme](../msga/readme.md))
199
+ 3. Browser code uses optional chaining: `window.msgapi?.newFeature?.method()` — works everywhere, undefined when unavailable
200
+
201
+ See [`msgapi-plan.md`](./msgapi-plan.md) for the full feature matrix and implementation status across all three hosts.
202
+
89
203
  ## Related Packages
90
204
 
91
205
  - [@bobfrankston/msgview](https://www.npmjs.com/package/@bobfrankston/msgview) - Electron-based message box (slower, works on Pi)
92
206
  - [@bobfrankston/msger](https://www.npmjs.com/package/@bobfrankston/msger) - Rust/wry-based message box (fast, lightweight)
207
+ - [msga](https://github.com/BobFrankston/msga) - .NET MAUI Android/Windows wrapper app
93
208
 
94
209
  ## License
95
210
 
package/msgapi-plan.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # msgapi Implementation Plan
2
2
 
3
+ > **This is the authoritative plan for msgapi.** Other TODOs (md/todo.md) reference here for msgapi-related items.
3
4
  Shared planning document for msgapi across implementations:
4
- - **TypeScript**: msgapidefs (this repo) - types/definitions
5
- - **TypeScript/Electron**: msgview
6
- - **Rust/wry**: msger
7
- - **C#/.NET MAUI**: msga (y:\dev\utils\msga)
5
+ - **TypeScript**: [msgapidefs](../msgapidefs/) (this repo) - types/definitions ([README](../msgapidefs/README.md))
6
+ - **TypeScript/Electron**: [msgview](../msgview/)
7
+ - **Rust/wry**: [msger](../msger/)
8
+ - **C#/.NET MAUI**: [msga](../msga/) ([readme](../msga/readme.md))
8
9
 
9
10
  ## Current Status
10
11
 
@@ -13,14 +14,16 @@ Shared planning document for msgapi across implementations:
13
14
  | API | msgview | msger | msga | Notes |
14
15
  |-----|---------|-------|------|-------|
15
16
  | **Window Control** |
16
- | toggleFullscreen() | ✅ | ✅ | ⚠️ | msga: C# exists, not wired |
17
- | setFullscreen(bool) | ✅ | ✅ | ⚠️ | msga: C# exists, not wired |
18
- | minimize() | ✅ | ✅ | ⚠️ | msga: Windows only in C# |
19
- | maximize() | ✅ | ✅ | ⚠️ | msga: Windows only in C# |
20
- | setSize(w,h) | ✅ | ✅ | ⚠️ | msga: Windows only in C# |
21
- | setPosition(x,y) | ✅ | ✅ | ⚠️ | msga: Windows only in C# |
17
+ | toggleFullscreen() | ✅ | ✅ | | msga: fire-and-forget JS→C# |
18
+ | setFullscreen(bool) | ✅ | ✅ | | |
19
+ | minimize() | ✅ | ✅ | | msga: Windows only |
20
+ | maximize() | ✅ | ✅ | | msga: Windows only |
21
+ | setSize(w,h) | ✅ | ✅ | | msga: Windows only |
22
+ | setPosition(x,y) | ✅ | ✅ | | msga: Windows only |
22
23
  | setAlwaysOnTop(bool) | ✅ | ✅ | ❌ | |
23
- | close(result?) | ✅ | ✅ | ⚠️ | msga: basic close only |
24
+ | close(result?) | ✅ | ✅ | | |
25
+ | navigate(url) | ❌ | ❌ | ✅ | msga only |
26
+ | reload() | ❌ | ❌ | ✅ | msga only |
24
27
  | **File System** |
25
28
  | fs.selectFile() | ✅ | ✅ | ⚠️ | msga: C# exists, not wired |
26
29
  | fs.selectFiles() | ✅ | ✅ | ⚠️ | msga: C# exists, not wired |
@@ -31,54 +34,109 @@ Shared planning document for msgapi across implementations:
31
34
  | fs.list(path) | ✅ | ✅ | ❌ | Requires allowFs flag |
32
35
  | fs.exists(path) | ✅ | ✅ | ❌ | Requires allowFs flag |
33
36
  | fs.delete(path) | ✅ | ✅ | ❌ | Requires allowFs flag |
37
+ | **UDP** |
38
+ | udp.send() | ❌ | ❌ | ✅ | msga: C# UdpClient |
39
+ | udp.listen() | ❌ | ❌ | ✅ | msga: background receive loop |
40
+ | udp.sendReceive() | ❌ | ❌ | ✅ | msga: send + wait with timeout |
41
+ | udp.broadcast() | ❌ | ❌ | ✅ | msga: EnableBroadcast=true |
42
+ | **Configuration** |
43
+ | setLogging(value?) | ❌ | ❌ | ✅ | Returns old state; 'msga'/'msgapi'/'on'/'off'/null |
44
+ | setAutoUpdate(value?) | ❌ | ❌ | ✅ | 'auto'/'check'/'off'; defaults to 'off' (web app owns policy) |
45
+ | installUpdate(url) | ❌ | ❌ | ✅ | Download + install APK; Android only. Web app calls this. |
46
+ | getNetworkInfo() | ❌ | ❌ | ✅ | Fresh local IP + subnet mask; used by httpudp-client isAddressReachable() |
47
+ | info | ❌ | ❌ | ✅ | `MsgHostInfo`: host, version, platform, updateUrl |
48
+ | updateUrl | ❌ | ❌ | ✅ | URL to versions.json — install source for update checks |
49
+ | version | ❌ | ❌ | ✅ | `window.msgapi.version` = app version string |
34
50
 
35
51
  Legend: ✅ Implemented | ⚠️ Partial/Not wired | ❌ Not implemented
36
52
 
37
- ### msga Wiring Status
53
+ ### msga Logging
38
54
 
39
- The MsgApiBridge.cs file in msga has C# implementations but the JavaScript bridge is not connected:
40
- - JavaScript injection exists in MainPage.xaml.cs
41
- - C# handler methods exist in Services/MsgApiBridge.cs
42
- - Missing: WebView JavaScript-to-C# message handler wiring
55
+ Two independent log streams, both via `https://rmf39.aaz.lt/logit/`:
43
56
 
44
- ---
57
+ | Stream | Log name | Controlled by | Content |
58
+ |--------|----------|---------------|---------|
59
+ | **C# (MsgALog)** | `logit-msga` | `LOGGING=true` in msga.env | OnNavigated, msgapi:// dispatch, SendAsync, UDP recv |
60
+ | **JS (_logit)** | `logit-msgapi` | `JS_LOGGING=true` in msga.env | msgapi inject, msgapi object creation |
45
61
 
46
- ## Planned Features
62
+ **Startup defaults** are set in `msga.env`. At **runtime**:
63
+ ```javascript
64
+ msgapi.setLogging() // → 'none' (query current, no change)
65
+ msgapi.setLogging('msga') // → 'none' (old); enables C# only
66
+ msgapi.setLogging('msgapi') // → 'msga' (old); enables JS only
67
+ msgapi.setLogging('on') // → 'msgapi' (old); enables both
68
+ msgapi.setLogging('off') // → 'msga,msgapi' (old); disables both
69
+ ```
70
+ Always returns previous state as string: `'msga'`, `'msgapi'`, `'msga,msgapi'`, or `'none'`.
71
+
72
+ ### msga Implementation Status
47
73
 
48
- ### 1. UDP Communication
74
+ msga has the most complete msgapi.udp implementation. The JS bridge is fully wired:
75
+ - `GetMsgApiScript()` in MainPage.xaml.cs injects `window.msgapi` with all sub-objects
76
+ - Window control: fire-and-forget calls via `MsgABridge.invokeMethod()`
77
+ - UDP: async Promise-based calls via `msgapi://udp/*` URL routing → `UdpService.cs`
78
+ - Data transport: base64 encoding for binary data over the bridge
79
+ - Listener callbacks: pushed via `window._msgapiUdpReceive()` from C#
49
80
 
50
- **Use case**: Local device control, IoT messaging, low-latency communication
81
+ ---
82
+
83
+ ## UDP Interface
84
+
85
+ Defined in [`msgapidefs.ts`](./msgapidefs.ts) and implemented in [msga](../msga/). Types available via `@bobfrankston/msgapidefs`.
51
86
 
52
87
  ```typescript
53
88
  interface MsgAPI {
54
- udp: {
55
- // Send UDP datagram
89
+ udp?: {
56
90
  send(host: string, port: number, data: string | Uint8Array): Promise<void>;
57
-
58
- // Listen for UDP datagrams on a port
59
91
  listen(port: number, callback: (data: Uint8Array, sender: {host: string, port: number}) => void): Promise<{close: () => void}>;
60
-
61
- // Send and wait for response (with timeout)
62
92
  sendReceive(host: string, port: number, data: string | Uint8Array, timeoutMs?: number): Promise<Uint8Array>;
63
-
64
- // Broadcast to subnet
65
93
  broadcast(port: number, data: string | Uint8Array): Promise<void>;
66
94
  }
67
95
  }
68
96
  ```
69
97
 
70
- **Implementation notes**:
71
- - TypeScript: Define types only (browser cannot do UDP)
72
- - Electron (msgview): Use dgram module
73
- - Rust (msger): Use std::net::UdpSocket or tokio UDP
74
- - C#/MAUI (msga): Use System.Net.Sockets.UdpClient
98
+ **Platform implementations**:
99
+ - **msga** (C#/MAUI): `System.Net.Sockets.UdpClient` via `UdpService.cs`
100
+ - **msgview** (Electron): Planned — Node.js `dgram` via IPC/preload
101
+ - **msger** (Rust/wry): Planned — `std::net::UdpSocket` or tokio
75
102
 
76
- **Security considerations**:
77
- - Require explicit allowUdp flag or permission prompt
78
- - Consider port restrictions (no well-known ports < 1024?)
79
- - Localhost-only mode option
103
+ **httpudp-client integration**: `HttpUdpClient.connect()` detects `window.msgapi?.udp` and uses it directly when available, falling back to the httpudp WebSocket proxy otherwise. Callers never know which path was taken.
104
+
105
+ ---
106
+
107
+ ## Web-App-Driven APK Updates
108
+
109
+ Update policy is owned by the web app (houser), not msga. msga provides the `installUpdate(url)` primitive; the web app decides when/whether to use it.
110
+
111
+ **How it works:**
112
+ 1. Web app fetches `versions.json` from `https://rmf39.aaz.lt/msga/versions.json`
113
+ 2. Compares remote version with `window.msgapi.version`
114
+ 3. If newer, calls `await window.msgapi.installUpdate(apkUrl)` to download + install
115
+ 4. Optionally, UDP broadcast can notify about available updates (no polling needed)
116
+
117
+ **`setAutoUpdate` defaults to `'off'`** — the C#-side timer-based auto-update is disabled by default. Web app calls `installUpdate()` directly when it decides an update is needed. `setAutoUpdate('auto')` can still be used to re-enable C#-side silent updates if desired.
118
+
119
+ ```javascript
120
+ // Web app update check example (types: MsgVersionInfo, MsgApkInfo from msgapidefs)
121
+ // Versions are strings like "1.5.21" — compare numerically, not lexicographically
122
+ const isNewer = (remote, current) => {
123
+ const c = current.split('.').map(Number);
124
+ return remote.split('.').map(Number).map((v, i) => v - c[i]).reduce((r, d) => r || d, 0) > 0;
125
+ };
126
+ const resp = await fetch(window.msgapi.updateUrl);
127
+ const info = await resp.json(); // MsgVersionInfo
128
+ if (isNewer(info.version, window.msgapi.version)) {
129
+ const apk = info.apks.find(a => a.name === 'houser45');
130
+ const baseUrl = window.msgapi.updateUrl.replace(/\/[^/]*$/, '/');
131
+ await window.msgapi.installUpdate(baseUrl + apk.file);
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Planned Features
80
138
 
81
- ### 2. Enhanced File System
139
+ ### 1. Enhanced File System
82
140
 
83
141
  **Use case**: Full file system access for local apps, document management
84
142
 
@@ -160,16 +218,16 @@ interface MsgAPI {
160
218
 
161
219
  ## Implementation Priorities
162
220
 
163
- ### Phase 1: Wire Up msga (Current)
164
- 1. Connect JavaScript bridge to C# handlers in msga
165
- 2. Test existing window control APIs on Android/Windows
166
- 3. Ensure parity with msgview/msger for basic features
221
+ ### Phase 1: msga msgapi + UDP ✅ Done
222
+ 1. JavaScript bridge wired in msga (GetMsgApiScript → msgapi:// URL routing)
223
+ 2. Window control working on Android/Windows
224
+ 3. UDP types added to msgapidefs
225
+ 4. ✅ UDP fully implemented in msga (UdpService.cs)
226
+ 5. ✅ httpudp-client detects msgapi.udp and uses it natively
167
227
 
168
- ### Phase 2: UDP Support
169
- 1. Add UDP types to msgapidefs
170
- 2. Implement in msger (Rust) - fastest iteration
171
- 3. Port to msgview (Electron)
172
- 4. Port to msga (C#/MAUI)
228
+ ### Phase 2: UDP in msger/msgview
229
+ 1. Implement in msger (Rust) — `std::net::UdpSocket` or tokio
230
+ 2. Port to msgview (Electron) Node.js `dgram` via IPC/preload
173
231
 
174
232
  ### Phase 3: Enhanced File System
175
233
  1. Add new fs types to msgapidefs
@@ -200,6 +258,24 @@ Test categories:
200
258
 
201
259
  ---
202
260
 
261
+ ## Known Issues (Resolved)
262
+
263
+ ### msga: hash navigations killed UDP listener (fixed v1.4.0)
264
+
265
+ MAUI fires OnNavigated for in-page hash changes (#Floor3, #Office, etc.) even though it's a SPA. OnNavigated was disposing UdpService on every such event, killing the listener that httpudp-client set up via msgapi.udp.listen(). Sends still worked (ephemeral sockets) but responses were lost. Symptom: "works once then stops" as the first room switch killed the listener.
266
+
267
+ **Fix:** OnNavigated now compares the base URL (without hash) and skips disposal + re-injection for hash-only changes.
268
+
269
+ ### msga: httpudp://start caused stuck loading overlay (fixed v1.5.3)
270
+
271
+ httpudp-client does `location.href = 'httpudp://start'` to try launching the httpudp proxy app on Android. This custom URL scheme triggered MAUI navigation events in unexpected order (OnNavigated before OnNavigating), causing the loading overlay to get stuck and — in some cases — preventing msgapi injection entirely.
272
+
273
+ **Fix (v1.5.3):** OnNavigating and OnNavigated now skip non-http/https URLs via `IsHttpUrl()` check. LinkNavigationWebViewClient blocks non-http schemes (`return true` = "handled") so the Android WebView doesn't try to load them.
274
+
275
+ **Note:** httpudp-client's `ensureHttpUdpProxy()` should NOT fire when running inside msga (msgapi.udp is available natively). If `httpudp://start` navigations appear in logs, it means httpudp-client isn't detecting msgapi.udp — check that `msgapiready` event fires before httpudp-client's `connect()` runs.
276
+
277
+ ---
278
+
203
279
  ## Notes
204
280
 
205
281
  - msgapi is injected as `window.msgapi` global
package/msgapidefs.ts CHANGED
@@ -1,263 +1,121 @@
1
- /**
2
- * TypeScript definitions for the msgapi JavaScript API
3
- *
4
- * This module provides type-safe access to msgapi's window control,
5
- * data persistence, and file system features (used by msgview and msger).
6
- *
7
- * See README.md for complete usage documentation and examples.
8
- */
9
-
10
- /**
11
- * Result object returned when the window closes
12
- */
13
- export interface MsgResult {
14
- /** Button that was clicked */
15
- button: string;
16
- /** Optional input field value */
17
- value?: string;
18
- /** Optional form data */
19
- form?: Record<string, any>;
20
- /** True if window was closed via close button */
21
- closed?: boolean;
22
- /** True if window was dismissed via Escape key */
23
- dismissed?: boolean;
24
- /** True if window was closed due to timeout */
25
- timeout?: boolean;
1
+ // TypeScript definitions for the msgapi JavaScript API
2
+ // Type-safe access to msgapi's window control, file system, and UDP features
3
+ // Used by msgview, msger, and msga. See README.md for docs.
4
+
5
+ export interface MsgResult { // Result object returned when the window closes
6
+ button: string; // Button that was clicked
7
+ value?: string; // Optional input field value
8
+ form?: Record<string, any>; // Optional form data
9
+ closed?: boolean; // True if closed via close button
10
+ dismissed?: boolean; // True if dismissed via Escape key
11
+ timeout?: boolean; // True if closed due to timeout
26
12
  }
27
13
 
28
- /**
29
- * File information returned by file system operations
30
- */
31
- export interface FileInfo {
32
- /** File name */
33
- name: string;
34
- /** Full file path */
35
- path: string;
36
- /** True if this is a directory */
37
- isDir: boolean;
38
- /** File size in bytes (0 for directories) */
39
- size: number;
40
- /** Last modified timestamp (ISO 8601 string) */
41
- modified?: string;
14
+ export interface FileInfo { // File information from file system operations
15
+ name: string; // File name
16
+ path: string; // Full file path
17
+ isDir: boolean; // True if this is a directory
18
+ size: number; // File size in bytes (0 for directories)
19
+ modified?: string; // Last modified timestamp (ISO 8601 string)
42
20
  }
43
21
 
44
- /**
45
- * Options for file selection dialogs
46
- */
47
- export interface FileDialogOptions {
48
- /** Dialog title */
49
- title?: string;
50
- /** Default filename for save dialogs */
51
- defaultFilename?: string;
52
- /** File filters (e.g., [{name: "Text", extensions: ["txt", "md"]}]) */
53
- filters?: Array<{name: string; extensions: string[]}>;
54
- /** Default directory to open */
55
- defaultPath?: string;
22
+ export interface FileDialogOptions { // Options for file selection dialogs
23
+ title?: string; // Dialog title
24
+ defaultFilename?: string; // Default filename for save dialogs
25
+ filters?: Array<{name: string; extensions: string[]}>; // File filters
26
+ defaultPath?: string; // Default directory to open
56
27
  }
57
28
 
58
- /**
59
- * Selected file with content
60
- */
61
- export interface SelectedFile {
62
- /** File name */
63
- name: string;
64
- /** Full file path */
65
- path: string;
66
- /** File content as string */
67
- content: string;
29
+ export interface SelectedFile { // Selected file with content
30
+ name: string; // File name
31
+ path: string; // Full file path
32
+ content: string; // File content as string
68
33
  }
69
34
 
70
- /**
71
- * msgapi JavaScript API
72
- *
73
- * Available when running inside msgview/msger via window.msgapi
74
- */
75
- export interface MsgAPI {
76
- // ========================================
77
- // Window Control
78
- // ========================================
35
+ export type MsgHost = 'msga' | 'msger' | 'msgview';
36
+ export type MsgPlatform = 'android' | 'windows' | 'linux' | 'macos';
37
+ export type AutoUpdateMode = 'auto' | 'check' | 'off';
38
+ export type LoggingMode = 'msga' | 'msgapi' | 'on' | 'off' | 'none';
39
+
40
+ export interface MsgHostInfo { // Host application identity and capabilities
41
+ host: MsgHost; // Which host app is running
42
+ version: string; // App version (e.g. "1.5.21") — semver string, compare numerically not lexicographically
43
+ platform: MsgPlatform; // Runtime platform
44
+ appName?: string; // APK variant name (e.g. "houser46") — derived from package ID, matches versions.json apks[].name
45
+ updateUrl?: string; // URL to versions.json — install source for update checks (msga only)
46
+ apkUrl?: string; // Direct URL to this APK (e.g. "https://rmf39.aaz.lt/msga/apks/houser46.apk")
47
+ localIp?: string; // Device IP on current network (e.g. "172.20.1.42")
48
+ localMask?: string; // Subnet mask (e.g. "255.255.0.0") — msga uses ip+mask to reject udp.send() when destination is off-network
49
+ }
79
50
 
80
- /**
81
- * Toggle fullscreen mode on/off
82
- * @example
83
- * msgapi.toggleFullscreen();
84
- */
85
- toggleFullscreen(): void;
51
+ export interface MsgApkInfo { // One APK entry in versions.json
52
+ name: string; // App variant name (e.g. "houser", "houser45")
53
+ url: string; // Default URL the APK loads
54
+ description: string; // Human-readable description
55
+ icon: string; // Icon URL
56
+ file: string; // APK file path relative to versions.json (e.g. "apks/houser.apk")
57
+ sizeMB: number; // Approximate size in MB
58
+ built: string; // Build timestamp (ISO 8601)
59
+ default?: boolean; // True if this is the default APK
60
+ }
86
61
 
87
- /**
88
- * Set fullscreen mode
89
- * @param enabled - true to enter fullscreen, false to exit
90
- * @example
91
- * msgapi.setFullscreen(true);
92
- */
93
- setFullscreen(enabled: boolean): void;
62
+ export interface MsgVersionInfo { // Structure of versions.json
63
+ version: string; // Latest available version (e.g. "1.5.21") — semver string, compare numerically not lexicographically
64
+ buildDate: string; // Build timestamp (ISO 8601)
65
+ apks: MsgApkInfo[]; // Available APK variants
66
+ }
94
67
 
95
- /**
96
- * Minimize the window
97
- * @example
98
- * msgapi.minimize();
99
- */
100
- minimize(): void;
68
+ // msgapi JavaScript API — available via window.msgapi inside msgview/msger/msga
69
+ export interface MsgAPI {
101
70
 
102
- /**
103
- * Toggle maximize/restore window
104
- * @example
105
- * msgapi.maximize();
106
- */
107
- maximize(): void;
71
+ version?: string; // Host app version (e.g. "1.3.0") — semver string, compare numerically not lexicographically
72
+ updateUrl?: string; // URL to versions.json — install source for update checks
73
+ info?: MsgHostInfo; // Host identity: host name, version, platform, updateUrl
108
74
 
109
- /**
110
- * Set window size
111
- * @param width - Window width in pixels
112
- * @param height - Window height in pixels
113
- * @example
114
- * msgapi.setSize(800, 600);
115
- */
116
- setSize(width: number, height: number): void;
75
+ // ── Configuration ────────────────────────────────────────────
117
76
 
118
- /**
119
- * Set window position
120
- * @param x - X coordinate in pixels
121
- * @param y - Y coordinate in pixels
122
- * @example
123
- * msgapi.setPosition(100, 100);
124
- */
125
- setPosition(x: number, y: number): void;
77
+ setLogging?(value?: LoggingMode): LoggingMode; // null/undefined = query only. Returns previous state.
78
+ setAutoUpdate?(value?: AutoUpdateMode): AutoUpdateMode; // null/undefined = query only. Returns previous state. msga Android only.
79
+ installUpdate?(url: string): Promise<void>; // Download + install APK from url. msga Android only; web app owns update policy.
80
+ getNetworkInfo?(): Promise<{ localIp: string; localMask: string }>; // Current device IP + subnet mask (fresh, not cached)
126
81
 
127
- /**
128
- * Set always-on-top behavior
129
- * @param enabled - true to keep window on top
130
- * @example
131
- * msgapi.setAlwaysOnTop(true);
132
- */
133
- setAlwaysOnTop(enabled: boolean): void;
82
+ // ── Window Control ──────────────────────────────────────────
134
83
 
135
- /**
136
- * Close the window and return result to parent process
137
- * @param result - Optional result to return to parent process
138
- * @example
139
- * // Close with default dismissed result
140
- * msgapi.close();
141
- *
142
- * // Close with custom result
143
- * msgapi.close({button: 'OK', value: 'user data'});
144
- */
145
- close(result?: Partial<MsgResult>): void;
84
+ toggleFullscreen(): void; // Toggle fullscreen mode on/off
85
+ setFullscreen(enabled: boolean): void; // Set fullscreen mode
86
+ minimize(): void; // Minimize the window
87
+ maximize(): void; // Toggle maximize/restore
88
+ setSize(width: number, height: number): void; // Set window size in pixels
89
+ setPosition(x: number, y: number): void; // Set window position in pixels
90
+ setAlwaysOnTop(enabled: boolean): void; // Keep window on top
91
+ close(result?: Partial<MsgResult>): void; // Close window, return result to parent
146
92
 
147
- // ========================================
148
- // File System Access
149
- // ========================================
93
+ // ── File System Access ──────────────────────────────────────
150
94
 
151
- /**
152
- * File system operations namespace
153
- */
154
95
  fs?: {
155
- /**
156
- * Select a file using native file picker
157
- * @param options - File dialog options
158
- * @returns Selected file with content, or null if cancelled
159
- * @example
160
- * const file = await msgapi.fs.selectFile({
161
- * title: 'Open File',
162
- * filters: [{name: 'Text', extensions: ['txt', 'md']}]
163
- * });
164
- * if (file) {
165
- * console.log(file.content);
166
- * }
167
- */
168
- selectFile(options?: FileDialogOptions): Promise<SelectedFile | null>;
169
-
170
- /**
171
- * Select multiple files using native file picker
172
- * @param options - File dialog options
173
- * @returns Array of selected files with content
174
- * @example
175
- * const files = await msgapi.fs.selectFiles({
176
- * title: 'Open Files'
177
- * });
178
- */
179
- selectFiles(options?: FileDialogOptions): Promise<SelectedFile[]>;
180
-
181
- /**
182
- * Save file using native save dialog
183
- * @param content - File content to save
184
- * @param defaultFilename - Default filename
185
- * @param options - Additional dialog options
186
- * @returns Path where file was saved, or null if cancelled
187
- * @example
188
- * const path = await msgapi.fs.saveFileAs('Hello World', 'greeting.txt');
189
- */
190
- saveFileAs(
191
- content: string,
192
- defaultFilename?: string,
193
- options?: FileDialogOptions
194
- ): Promise<string | null>;
195
-
196
- /**
197
- * Select a directory using native folder picker
198
- * @param options - Dialog options
199
- * @returns Selected directory path, or null if cancelled
200
- * @example
201
- * const dir = await msgapi.fs.selectFolder({
202
- * title: 'Select Output Folder'
203
- * });
204
- */
205
- selectFolder(options?: FileDialogOptions): Promise<string | null>;
206
-
207
- /**
208
- * Read file contents
209
- * Note: Requires allowFs: true flag when launching msgview/msger
210
- * @param path - File path to read
211
- * @returns File content as string
212
- * @example
213
- * const content = await msgapi.fs.read('/path/to/file.txt');
214
- */
215
- read(path: string): Promise<string>;
216
-
217
- /**
218
- * Write file contents
219
- * Note: Requires allowFs: true flag when launching msgview/msger
220
- * @param path - File path to write
221
- * @param content - Content to write
222
- * @example
223
- * await msgapi.fs.write('/path/to/file.txt', 'Hello World');
224
- */
225
- write(path: string, content: string): Promise<void>;
226
-
227
- /**
228
- * List directory contents
229
- * Note: Requires allowFs: true flag when launching msgview/msger
230
- * @param path - Directory path to list
231
- * @returns Array of file information
232
- * @example
233
- * const files = await msgapi.fs.list('/path/to/dir');
234
- */
235
- list(path: string): Promise<FileInfo[]>;
96
+ selectFile(options?: FileDialogOptions): Promise<SelectedFile | null>; // Native file picker (null if cancelled)
97
+ selectFiles(options?: FileDialogOptions): Promise<SelectedFile[]>; // Multi-file picker
98
+ saveFileAs(content: string, defaultFilename?: string, options?: FileDialogOptions): Promise<string | null>; // Save dialog (returns path or null)
99
+ selectFolder(options?: FileDialogOptions): Promise<string | null>; // Folder picker (null if cancelled)
100
+ read(path: string): Promise<string>; // Read file contents (requires allowFs)
101
+ write(path: string, content: string): Promise<void>; // Write file contents (requires allowFs)
102
+ list(path: string): Promise<FileInfo[]>; // List directory contents (requires allowFs)
103
+ exists(path: string): Promise<boolean>; // Check if file/directory exists (requires allowFs)
104
+ delete(path: string): Promise<void>; // Delete file or directory (requires allowFs)
105
+ };
236
106
 
237
- /**
238
- * Check if file or directory exists
239
- * Note: Requires allowFs: true flag when launching msgview/msger
240
- * @param path - Path to check
241
- * @returns true if exists
242
- * @example
243
- * const exists = await msgapi.fs.exists('/path/to/file.txt');
244
- */
245
- exists(path: string): Promise<boolean>;
107
+ // ── UDP Networking ──────────────────────────────────────────
108
+ // Native UDP no httpudp WebSocket proxy needed
246
109
 
247
- /**
248
- * Delete file or directory
249
- * Note: Requires allowFs: true flag when launching msgview/msger
250
- * @param path - Path to delete
251
- * @example
252
- * await msgapi.fs.delete('/path/to/file.txt');
253
- */
254
- delete(path: string): Promise<void>;
110
+ udp?: {
111
+ send(host: string, port: number, data: string | Uint8Array): Promise<void>; // Send UDP packet to host:port
112
+ listen(port: number, callback: (data: Uint8Array, sender: { host: string; port: number }) => void): Promise<{ close: () => void }>; // Listen on port (0=ephemeral), returns {close()}
113
+ sendReceive(host: string, port: number, data: string | Uint8Array, timeoutMs?: number): Promise<Uint8Array>; // Send and wait for response
114
+ broadcast(port: number, data: string | Uint8Array): Promise<void>; // Broadcast to local network
255
115
  };
256
116
  }
257
117
 
258
- /**
259
- * Declare global window.msgapi
260
- */
118
+ // Global window.msgapi declaration
261
119
  declare global {
262
120
  interface Window {
263
121
  msgapi?: MsgAPI;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/msgapidefs",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "TypeScript definitions for msgapi JavaScript API (msgview/msger)",
5
5
  "type": "module",
6
6
  "main": "./msgapidefs.js",
@@ -34,5 +34,8 @@
34
34
  "preversion": "git add -A",
35
35
  "postversion": "git push && git push --tags",
36
36
  "release": "npm run prerelease:local && npm version patch && npm publish --quiet"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.2.1"
37
40
  }
38
41
  }