signalk-container 0.1.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/.prettierignore +2 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/containers.d.ts +16 -0
- package/dist/containers.d.ts.map +1 -0
- package/dist/containers.js +205 -0
- package/dist/containers.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +3 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +75 -0
- package/dist/jobs.js.map +1 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +113 -0
- package/dist/runtime.js.map +1 -0
- package/dist/test/runtime.test.d.ts +2 -0
- package/dist/test/runtime.test.d.ts.map +1 -0
- package/dist/test/runtime.test.js +30 -0
- package/dist/test/runtime.test.js.map +1 -0
- package/dist/test/types.test.d.ts +2 -0
- package/dist/test/types.test.d.ts.map +1 -0
- package/dist/test/types.test.js +58 -0
- package/dist/test/types.test.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/doc/plugin-developer-guide.md +441 -0
- package/package.json +53 -0
- package/public/540.js +2 -0
- package/public/540.js.LICENSE.txt +9 -0
- package/public/805.js +1 -0
- package/public/main.js +1 -0
- package/public/remoteEntry.js +1 -0
- package/webpack.config.js +43 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# Plugin Developer Guide: Using signalk-container
|
|
2
|
+
|
|
3
|
+
How to use signalk-container from your Signal K plugin to manage Docker/Podman containers. This guide covers the integration patterns, pitfalls, and solutions discovered during real-world development.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// In your plugin's async startup function:
|
|
9
|
+
const containers = (globalThis as any).__signalk_containerManager;
|
|
10
|
+
if (!containers) {
|
|
11
|
+
app.setPluginError("signalk-container plugin is required");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
await containers.ensureRunning("my-service", {
|
|
16
|
+
image: "myorg/myimage",
|
|
17
|
+
tag: "latest",
|
|
18
|
+
ports: { "8080/tcp": "127.0.0.1:8080" },
|
|
19
|
+
volumes: { "/data": app.getDataDirPath() },
|
|
20
|
+
env: { MY_VAR: "value" },
|
|
21
|
+
restart: "unless-stopped",
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Critical: Signal K Plugin Lifecycle
|
|
28
|
+
|
|
29
|
+
### The server does NOT await `start()`
|
|
30
|
+
|
|
31
|
+
Signal K server calls `plugin.start(config, restart)` **synchronously**. If your `start()` is `async`, the returned Promise is ignored. Errors from rejected promises become unhandled rejections — no error status, no logs, silent failure.
|
|
32
|
+
|
|
33
|
+
**Wrong:**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// The server calls this but does NOT await it.
|
|
37
|
+
// If ensureRunning() rejects, no one catches it.
|
|
38
|
+
async start(config) {
|
|
39
|
+
await containers.ensureRunning(...) // unhandled rejection if this fails
|
|
40
|
+
app.setPluginStatus('Running') // never reached
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Correct:**
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
start(config) {
|
|
48
|
+
asyncStart(config).catch((err) => {
|
|
49
|
+
app.setPluginError(
|
|
50
|
+
`Startup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Extract all async logic into a separate function and call it from a synchronous `start()` with an explicit `.catch()`.
|
|
57
|
+
|
|
58
|
+
### `setPluginStatus` and `setPluginError` take ONE argument
|
|
59
|
+
|
|
60
|
+
The server wraps these methods per-plugin. The plugin id is pre-filled automatically.
|
|
61
|
+
|
|
62
|
+
**Wrong:**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
app.setPluginStatus(plugin.id, "Running"); // plugin.id becomes the message!
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Correct:**
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
app.setPluginStatus("Running");
|
|
72
|
+
app.setPluginError("Connection failed");
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The server internally calls `app.setPluginStatus(pluginId, msg)` with two args, but the version given to plugins via `appCopy` is already bound to the plugin id.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Critical: Cross-Plugin Communication
|
|
80
|
+
|
|
81
|
+
### Each plugin gets a shallow copy of `app`
|
|
82
|
+
|
|
83
|
+
Signal K server creates each plugin's `app` via `_.assign({}, app, {...})`. This is a **shallow copy**. Setting a property on one plugin's `app` does NOT propagate to other plugins.
|
|
84
|
+
|
|
85
|
+
**Wrong:**
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// In signalk-container:
|
|
89
|
+
(app as any).containerManager = api;
|
|
90
|
+
|
|
91
|
+
// In signalk-questdb:
|
|
92
|
+
const containers = (app as any).containerManager; // undefined!
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Correct — use `globalThis`:**
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// In signalk-container:
|
|
99
|
+
(globalThis as any).__signalk_containerManager = api;
|
|
100
|
+
|
|
101
|
+
// In signalk-questdb:
|
|
102
|
+
const containers = (globalThis as any).__signalk_containerManager;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Clean up in `stop()`:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
stop() {
|
|
109
|
+
delete (globalThis as any).__signalk_containerManager;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Startup order is not guaranteed
|
|
114
|
+
|
|
115
|
+
Plugins start in parallel. Your plugin may start before signalk-container has finished detecting the runtime. You must poll and wait:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
async function asyncStart(config) {
|
|
119
|
+
let containers;
|
|
120
|
+
const deadline = Date.now() + 30000;
|
|
121
|
+
while (Date.now() < deadline) {
|
|
122
|
+
containers = (globalThis as any).__signalk_containerManager;
|
|
123
|
+
// containerManager is exposed immediately, but runtime detection
|
|
124
|
+
// is async. Wait until getRuntime() returns non-null.
|
|
125
|
+
if (containers && containers.getRuntime()) break;
|
|
126
|
+
app.setPluginStatus("Waiting for container runtime...");
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!containers || !containers.getRuntime()) {
|
|
131
|
+
app.setPluginError("signalk-container plugin not available");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Now safe to call ensureRunning()
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The key insight: signalk-container exposes the API object on `globalThis` **synchronously** in `start()`, but `getRuntime()` returns `null` until the async runtime detection completes. Always check both.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Podman vs Docker Differences
|
|
144
|
+
|
|
145
|
+
### Image names must be fully qualified for Podman
|
|
146
|
+
|
|
147
|
+
Podman without `unqualified-search-registries` configured rejects short names like `questdb/questdb:latest`. signalk-container handles this automatically by prefixing `docker.io/` when needed. You don't need to worry about this in your plugin — just pass the normal Docker Hub image name.
|
|
148
|
+
|
|
149
|
+
### SELinux volume flags
|
|
150
|
+
|
|
151
|
+
signalk-container adds `:Z` to volume mounts when using Podman (required on Fedora/RHEL for SELinux relabelling). Docker ignores this flag harmlessly. Your plugin doesn't need to handle this.
|
|
152
|
+
|
|
153
|
+
### Container naming
|
|
154
|
+
|
|
155
|
+
All containers are prefixed with `sk-` (e.g., `sk-signalk-questdb`). This avoids conflicts with user containers and makes cleanup predictable. Pass just your plugin name to `ensureRunning()` — the prefix is added automatically.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Container Config Changes
|
|
160
|
+
|
|
161
|
+
When your plugin's configuration changes (compression, ports, image version, etc.), the container needs to be recreated because Docker/Podman env vars are set at container creation time.
|
|
162
|
+
|
|
163
|
+
**Pattern: hash-based recreation**
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const containerConfig = {
|
|
167
|
+
image: "questdb/questdb",
|
|
168
|
+
tag: config.version,
|
|
169
|
+
ports: { "9000/tcp": "127.0.0.1:9000" },
|
|
170
|
+
volumes: { "/data": app.getDataDirPath() },
|
|
171
|
+
env: { MY_COMPRESSION: config.compression },
|
|
172
|
+
restart: "unless-stopped",
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Hash the config to detect changes
|
|
176
|
+
const configHash = JSON.stringify({
|
|
177
|
+
tag: containerConfig.tag,
|
|
178
|
+
ports: containerConfig.ports,
|
|
179
|
+
env: containerConfig.env,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const hashFile = `${app.getDataDirPath()}/container-config-hash`;
|
|
183
|
+
let lastHash = "";
|
|
184
|
+
try {
|
|
185
|
+
lastHash = readFileSync(hashFile, "utf8");
|
|
186
|
+
} catch {
|
|
187
|
+
/* first run */
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const state = await containers.getState("my-service");
|
|
191
|
+
if (state !== "missing" && configHash !== lastHash) {
|
|
192
|
+
// Config changed — remove and recreate
|
|
193
|
+
await containers.remove("my-service");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await containers.ensureRunning("my-service", containerConfig);
|
|
197
|
+
writeFileSync(hashFile, configHash);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Data is safe because volumes live on the host filesystem, not inside the container.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Stopping Containers When Plugin is Disabled
|
|
205
|
+
|
|
206
|
+
When your plugin's `stop()` is called (user disables the plugin), you should stop the managed container. Otherwise it keeps running with no one managing it:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
async stop() {
|
|
210
|
+
// Clean up writer, timers, subscriptions...
|
|
211
|
+
|
|
212
|
+
// Stop the managed container
|
|
213
|
+
if (currentConfig?.managedContainer !== false) {
|
|
214
|
+
const containers = (globalThis as any).__signalk_containerManager;
|
|
215
|
+
if (containers) {
|
|
216
|
+
try {
|
|
217
|
+
await containers.stop('my-service');
|
|
218
|
+
} catch {
|
|
219
|
+
// may already be stopped
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The container is only stopped, not removed. Re-enabling the plugin will start it again instantly without pulling.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## API Reference
|
|
231
|
+
|
|
232
|
+
Access via `(globalThis as any).__signalk_containerManager`:
|
|
233
|
+
|
|
234
|
+
### `getRuntime(): RuntimeInfo | null`
|
|
235
|
+
|
|
236
|
+
Returns detected runtime info or `null` if detection hasn't completed.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
{ runtime: 'podman', version: '5.4.2', isPodmanDockerShim: false }
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### `ensureRunning(name, config, options?): Promise<void>`
|
|
243
|
+
|
|
244
|
+
Creates and starts a container if not already running. No-op if already running.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
await containers.ensureRunning("my-db", {
|
|
248
|
+
image: "postgres",
|
|
249
|
+
tag: "16",
|
|
250
|
+
ports: { "5432/tcp": "127.0.0.1:5432" },
|
|
251
|
+
volumes: { "/var/lib/postgresql/data": "/host/path" },
|
|
252
|
+
env: { POSTGRES_PASSWORD: "secret" },
|
|
253
|
+
restart: "unless-stopped",
|
|
254
|
+
command: ["-c", "shared_buffers=256MB"], // optional
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Use `networkMode: 'host'` for containers that need direct access to the host network (e.g. multicast/broadcast discovery). Port mappings are ignored when `networkMode` is set.
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
await containers.ensureRunning("mayara-server", {
|
|
262
|
+
image: "ghcr.io/marineyachtradar/mayara-server",
|
|
263
|
+
tag: "latest",
|
|
264
|
+
networkMode: "host",
|
|
265
|
+
restart: "unless-stopped",
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `start(name): Promise<void>`
|
|
270
|
+
|
|
271
|
+
Starts a stopped container. Throws if container doesn't exist.
|
|
272
|
+
|
|
273
|
+
### `stop(name): Promise<void>`
|
|
274
|
+
|
|
275
|
+
Stops a running container. Idempotent.
|
|
276
|
+
|
|
277
|
+
### `remove(name): Promise<void>`
|
|
278
|
+
|
|
279
|
+
Stops and removes a container. Idempotent.
|
|
280
|
+
|
|
281
|
+
### `getState(name): Promise<ContainerState>`
|
|
282
|
+
|
|
283
|
+
Returns `'running'`, `'stopped'`, `'missing'`, or `'no-runtime'`.
|
|
284
|
+
|
|
285
|
+
### `pullImage(image, onProgress?): Promise<void>`
|
|
286
|
+
|
|
287
|
+
Pulls an image. `onProgress` receives line-by-line pull output.
|
|
288
|
+
|
|
289
|
+
### `imageExists(image): Promise<boolean>`
|
|
290
|
+
|
|
291
|
+
Checks if an image exists locally.
|
|
292
|
+
|
|
293
|
+
### `runJob(config): Promise<ContainerJobResult>`
|
|
294
|
+
|
|
295
|
+
Runs a one-shot container (exits when done).
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const result = await containers.runJob({
|
|
299
|
+
image: "myorg/converter",
|
|
300
|
+
command: ["convert", "/in/data.csv", "/out/data.parquet"],
|
|
301
|
+
inputs: { "/in": "/host/input" }, // read-only mount
|
|
302
|
+
outputs: { "/out": "/host/output" }, // read-write mount
|
|
303
|
+
env: { FORMAT: "parquet" },
|
|
304
|
+
timeout: 120, // seconds
|
|
305
|
+
onProgress: (line) => console.log(line),
|
|
306
|
+
label: "parquet-export",
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (result.status === "completed") {
|
|
310
|
+
console.log("Exit code:", result.exitCode);
|
|
311
|
+
console.log("Output:", result.log);
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### `prune(): Promise<PruneResult>`
|
|
316
|
+
|
|
317
|
+
Removes dangling images.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
{ imagesRemoved: 3, spaceReclaimed: '1.2 GB' }
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### `listContainers(): Promise<ContainerInfo[]>`
|
|
324
|
+
|
|
325
|
+
Lists all `sk-` prefixed containers.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## TypeScript Types
|
|
330
|
+
|
|
331
|
+
If you want type safety, define a minimal interface in your plugin:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
interface ContainerManagerApi {
|
|
335
|
+
getRuntime: () => { runtime: string; version: string } | null;
|
|
336
|
+
ensureRunning: (name: string, config: unknown) => Promise<void>;
|
|
337
|
+
start: (name: string) => Promise<void>;
|
|
338
|
+
stop: (name: string) => Promise<void>;
|
|
339
|
+
remove: (name: string) => Promise<void>;
|
|
340
|
+
getState: (
|
|
341
|
+
name: string,
|
|
342
|
+
) => Promise<"running" | "stopped" | "missing" | "no-runtime">;
|
|
343
|
+
pullImage: (
|
|
344
|
+
image: string,
|
|
345
|
+
onProgress?: (msg: string) => void,
|
|
346
|
+
) => Promise<void>;
|
|
347
|
+
imageExists: (image: string) => Promise<boolean>;
|
|
348
|
+
runJob: (
|
|
349
|
+
config: unknown,
|
|
350
|
+
) => Promise<{ status: string; exitCode?: number; log: string[] }>;
|
|
351
|
+
prune: () => Promise<{ imagesRemoved: number; spaceReclaimed: string }>;
|
|
352
|
+
listContainers: () => Promise<unknown[]>;
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Plugin Config Panel (Module Federation)
|
|
359
|
+
|
|
360
|
+
If you want a custom config UI like signalk-container and signalk-questdb, use the `signalk-plugin-configurator` pattern:
|
|
361
|
+
|
|
362
|
+
### package.json
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"keywords": ["signalk-node-server-plugin", "signalk-plugin-configurator"]
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Webpack config
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
const { ModuleFederationPlugin } = require("webpack").container;
|
|
374
|
+
const pkg = require("./package.json");
|
|
375
|
+
|
|
376
|
+
module.exports = {
|
|
377
|
+
entry: "./src/configpanel/index",
|
|
378
|
+
mode: "production",
|
|
379
|
+
output: { path: path.resolve(__dirname, "public"), clean: false },
|
|
380
|
+
module: {
|
|
381
|
+
rules: [
|
|
382
|
+
{
|
|
383
|
+
test: /\.jsx?$/,
|
|
384
|
+
loader: "babel-loader",
|
|
385
|
+
exclude: /node_modules/,
|
|
386
|
+
options: { presets: ["@babel/preset-react"] },
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
plugins: [
|
|
391
|
+
new ModuleFederationPlugin({
|
|
392
|
+
name: pkg.name.replace(/[-@/]/g, "_"),
|
|
393
|
+
library: { type: "var", name: pkg.name.replace(/[-@/]/g, "_") },
|
|
394
|
+
filename: "remoteEntry.js",
|
|
395
|
+
exposes: {
|
|
396
|
+
"./PluginConfigurationPanel":
|
|
397
|
+
"./src/configpanel/PluginConfigurationPanel",
|
|
398
|
+
},
|
|
399
|
+
shared: {
|
|
400
|
+
react: { singleton: true, requiredVersion: "^19" },
|
|
401
|
+
"react-dom": { singleton: true, requiredVersion: "^19" },
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Component signature
|
|
409
|
+
|
|
410
|
+
```jsx
|
|
411
|
+
export default function PluginConfigurationPanel({ configuration, save }) {
|
|
412
|
+
// configuration = current plugin config object
|
|
413
|
+
// save(newConfig) = call to persist config and restart plugin
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The `save()` function provided by the Admin UI POSTs to `/plugins/{pluginId}/config` and triggers a plugin restart.
|
|
418
|
+
|
|
419
|
+
### Build output
|
|
420
|
+
|
|
421
|
+
Webpack outputs to `public/` which Signal K serves at `/{package-name}/`. The Admin UI loads `remoteEntry.js` and dynamically imports `PluginConfigurationPanel`.
|
|
422
|
+
|
|
423
|
+
**Do not commit `public/*.js` to git** — add them to `.gitignore`. They're built during `npm run build` (which CI and `npm publish` both run via `prepublishOnly`).
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Common Mistakes Summary
|
|
428
|
+
|
|
429
|
+
| Mistake | Symptom | Fix |
|
|
430
|
+
| ------------------------------------------ | -------------------------------------------- | ---------------------------------------------- |
|
|
431
|
+
| `async start()` without catch | Silent failure, no status | Sync `start()` + `asyncStart().catch()` |
|
|
432
|
+
| `app.setPluginStatus(id, msg)` | Status shows plugin id as message | `app.setPluginStatus(msg)` (one arg) |
|
|
433
|
+
| Setting property on `app` | Other plugins can't see it | Use `globalThis.__signalk_xxx` |
|
|
434
|
+
| Not waiting for runtime detection | `getRuntime()` returns null | Poll until `getRuntime()` is non-null |
|
|
435
|
+
| Short Docker image names with Podman | Pull fails with "short-name did not resolve" | signalk-container handles this automatically |
|
|
436
|
+
| `DEDUP ENABLED UPSERT KEYS` in QuestDB DDL | Table creation fails | `DEDUP UPSERT KEYS` (no ENABLED) |
|
|
437
|
+
| Committing webpack `public/` output | CI fails with "untracked files" | Add `public/*.js` to `.gitignore` |
|
|
438
|
+
| `engines.node` missing from package.json | CI validation error | Add `"engines": { "node": ">=22" }` |
|
|
439
|
+
| Not stopping container in `stop()` | Container runs after plugin disabled | Call `containers.stop()` in plugin `stop()` |
|
|
440
|
+
| `savePluginOptions` doesn't restart | Plugin stays stopped after config save | Don't rely on it for restart; do work directly |
|
|
441
|
+
| Config hash in QuestDB data volume | Hash file lost (QuestDB owns the dir) | Store hash file next to plugin JSON config |
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signalk-container",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared container runtime management (Podman/Docker) for Signal K plugins",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"signalk-node-server-plugin",
|
|
7
|
+
"signalk-plugin-configurator"
|
|
8
|
+
],
|
|
9
|
+
"signalk": {
|
|
10
|
+
"displayName": "Container Manager",
|
|
11
|
+
"appIcon": "./icon.svg"
|
|
12
|
+
},
|
|
13
|
+
"signalk-plugin-enabled-by-default": true,
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && webpack",
|
|
17
|
+
"build:config": "webpack",
|
|
18
|
+
"watch": "tsc --watch",
|
|
19
|
+
"prettier": "prettier --write .",
|
|
20
|
+
"lint": "eslint --fix",
|
|
21
|
+
"format": "npm run prettier && npm run lint",
|
|
22
|
+
"ci-lint": "eslint && prettier --check .",
|
|
23
|
+
"test": "node --test 'dist/test/**/*.test.js'",
|
|
24
|
+
"build:all": "npm run build && npm test",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/dirkwa/signalk-container"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@babel/core": "^7.29.0",
|
|
37
|
+
"@babel/preset-react": "^7.28.5",
|
|
38
|
+
"@eslint/js": "^10.0.1",
|
|
39
|
+
"@signalk/server-api": "latest",
|
|
40
|
+
"@types/express": "^5.0.6",
|
|
41
|
+
"@types/node": "^20.0.0",
|
|
42
|
+
"babel-loader": "^10.1.1",
|
|
43
|
+
"eslint": "^10.2.0",
|
|
44
|
+
"eslint-config-prettier": "^10.1.8",
|
|
45
|
+
"globals": "^17.4.0",
|
|
46
|
+
"prettier": "^3.8.1",
|
|
47
|
+
"react": "^19.2.4",
|
|
48
|
+
"typescript": "^5.0.0",
|
|
49
|
+
"typescript-eslint": "^8.58.0",
|
|
50
|
+
"webpack": "^5.105.4",
|
|
51
|
+
"webpack-cli": "^7.0.2"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/public/540.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! For license information please see 540.js.LICENSE.txt */
|
|
2
|
+
"use strict";(self.webpackChunksignalk_container=self.webpackChunksignalk_container||[]).push([[540],{869(e,t){var n=Symbol.for("react.transitional.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),u=Symbol.for("react.strict_mode"),c=Symbol.for("react.profiler"),i=Symbol.for("react.consumer"),s=Symbol.for("react.context"),a=Symbol.for("react.forward_ref"),f=Symbol.for("react.suspense"),l=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),y=Symbol.for("react.activity"),d=Symbol.iterator,h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,b={};function m(e,t,n){this.props=e,this.context=t,this.refs=b,this.updater=n||h}function v(){}function S(e,t,n){this.props=e,this.context=t,this.refs=b,this.updater=n||h}m.prototype.isReactComponent={},m.prototype.setState=function(e,t){if("object"!=typeof e&&"function"!=typeof e&&null!=e)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")},m.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")},v.prototype=m.prototype;var E=S.prototype=new v;E.constructor=S,_(E,m.prototype),E.isPureReactComponent=!0;var g=Array.isArray;function w(){}var k={H:null,A:null,T:null,S:null},H=Object.prototype.hasOwnProperty;function j(e,t,r){var o=r.ref;return{$$typeof:n,type:e,key:t,ref:void 0!==o?o:null,props:r}}function C(e){return"object"==typeof e&&null!==e&&e.$$typeof===n}var R=/\/+/g;function $(e,t){return"object"==typeof e&&null!==e&&null!=e.key?(n=""+e.key,r={"=":"=0",":":"=2"},"$"+n.replace(/[=:]/g,function(e){return r[e]})):t.toString(36);var n,r}function T(e,t,o,u,c){var i=typeof e;"undefined"!==i&&"boolean"!==i||(e=null);var s,a,f=!1;if(null===e)f=!0;else switch(i){case"bigint":case"string":case"number":f=!0;break;case"object":switch(e.$$typeof){case n:case r:f=!0;break;case p:return T((f=e._init)(e._payload),t,o,u,c)}}if(f)return c=c(e),f=""===u?"."+$(e,0):u,g(c)?(o="",null!=f&&(o=f.replace(R,"$&/")+"/"),T(c,t,o,"",function(e){return e})):null!=c&&(C(c)&&(s=c,a=o+(null==c.key||e&&e.key===c.key?"":(""+c.key).replace(R,"$&/")+"/")+f,c=j(s.type,a,s.props)),t.push(c)),1;f=0;var l,y=""===u?".":u+":";if(g(e))for(var h=0;h<e.length;h++)f+=T(u=e[h],t,o,i=y+$(u,h),c);else if("function"==typeof(h=null===(l=e)||"object"!=typeof l?null:"function"==typeof(l=d&&l[d]||l["@@iterator"])?l:null))for(e=h.call(e),h=0;!(u=e.next()).done;)f+=T(u=u.value,t,o,i=y+$(u,h++),c);else if("object"===i){if("function"==typeof e.then)return T(function(e){switch(e.status){case"fulfilled":return e.value;case"rejected":throw e.reason;default:switch("string"==typeof e.status?e.then(w,w):(e.status="pending",e.then(function(t){"pending"===e.status&&(e.status="fulfilled",e.value=t)},function(t){"pending"===e.status&&(e.status="rejected",e.reason=t)})),e.status){case"fulfilled":return e.value;case"rejected":throw e.reason}}throw e}(e),t,o,u,c);throw t=String(e),Error("Objects are not valid as a React child (found: "+("[object Object]"===t?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.")}return f}function x(e,t,n){if(null==e)return e;var r=[],o=0;return T(e,r,"","",function(e){return t.call(n,e,o++)}),r}function A(e){if(-1===e._status){var t=e._result;(t=t()).then(function(t){0!==e._status&&-1!==e._status||(e._status=1,e._result=t)},function(t){0!==e._status&&-1!==e._status||(e._status=2,e._result=t)}),-1===e._status&&(e._status=0,e._result=t)}if(1===e._status)return e._result.default;throw e._result}var O="function"==typeof reportError?reportError:function(e){if("object"==typeof window&&"function"==typeof window.ErrorEvent){var t=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:"object"==typeof e&&null!==e&&"string"==typeof e.message?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if("object"==typeof process&&"function"==typeof process.emit)return void process.emit("uncaughtException",e);console.error(e)},I={map:x,forEach:function(e,t,n){x(e,function(){t.apply(this,arguments)},n)},count:function(e){var t=0;return x(e,function(){t++}),t},toArray:function(e){return x(e,function(e){return e})||[]},only:function(e){if(!C(e))throw Error("React.Children.only expected to receive a single React element child.");return e}};t.Activity=y,t.Children=I,t.Component=m,t.Fragment=o,t.Profiler=c,t.PureComponent=S,t.StrictMode=u,t.Suspense=f,t.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=k,t.__COMPILER_RUNTIME={__proto__:null,c:function(e){return k.H.useMemoCache(e)}},t.cache=function(e){return function(){return e.apply(null,arguments)}},t.cacheSignal=function(){return null},t.cloneElement=function(e,t,n){if(null==e)throw Error("The argument must be a React element, but you passed "+e+".");var r=_({},e.props),o=e.key;if(null!=t)for(u in void 0!==t.key&&(o=""+t.key),t)!H.call(t,u)||"key"===u||"__self"===u||"__source"===u||"ref"===u&&void 0===t.ref||(r[u]=t[u]);var u=arguments.length-2;if(1===u)r.children=n;else if(1<u){for(var c=Array(u),i=0;i<u;i++)c[i]=arguments[i+2];r.children=c}return j(e.type,o,r)},t.createContext=function(e){return(e={$$typeof:s,_currentValue:e,_currentValue2:e,_threadCount:0,Provider:null,Consumer:null}).Provider=e,e.Consumer={$$typeof:i,_context:e},e},t.createElement=function(e,t,n){var r,o={},u=null;if(null!=t)for(r in void 0!==t.key&&(u=""+t.key),t)H.call(t,r)&&"key"!==r&&"__self"!==r&&"__source"!==r&&(o[r]=t[r]);var c=arguments.length-2;if(1===c)o.children=n;else if(1<c){for(var i=Array(c),s=0;s<c;s++)i[s]=arguments[s+2];o.children=i}if(e&&e.defaultProps)for(r in c=e.defaultProps)void 0===o[r]&&(o[r]=c[r]);return j(e,u,o)},t.createRef=function(){return{current:null}},t.forwardRef=function(e){return{$$typeof:a,render:e}},t.isValidElement=C,t.lazy=function(e){return{$$typeof:p,_payload:{_status:-1,_result:e},_init:A}},t.memo=function(e,t){return{$$typeof:l,type:e,compare:void 0===t?null:t}},t.startTransition=function(e){var t=k.T,n={};k.T=n;try{var r=e(),o=k.S;null!==o&&o(n,r),"object"==typeof r&&null!==r&&"function"==typeof r.then&&r.then(w,O)}catch(e){O(e)}finally{null!==t&&null!==n.types&&(t.types=n.types),k.T=t}},t.unstable_useCacheRefresh=function(){return k.H.useCacheRefresh()},t.use=function(e){return k.H.use(e)},t.useActionState=function(e,t,n){return k.H.useActionState(e,t,n)},t.useCallback=function(e,t){return k.H.useCallback(e,t)},t.useContext=function(e){return k.H.useContext(e)},t.useDebugValue=function(){},t.useDeferredValue=function(e,t){return k.H.useDeferredValue(e,t)},t.useEffect=function(e,t){return k.H.useEffect(e,t)},t.useEffectEvent=function(e){return k.H.useEffectEvent(e)},t.useId=function(){return k.H.useId()},t.useImperativeHandle=function(e,t,n){return k.H.useImperativeHandle(e,t,n)},t.useInsertionEffect=function(e,t){return k.H.useInsertionEffect(e,t)},t.useLayoutEffect=function(e,t){return k.H.useLayoutEffect(e,t)},t.useMemo=function(e,t){return k.H.useMemo(e,t)},t.useOptimistic=function(e,t){return k.H.useOptimistic(e,t)},t.useReducer=function(e,t,n){return k.H.useReducer(e,t,n)},t.useRef=function(e){return k.H.useRef(e)},t.useState=function(e){return k.H.useState(e)},t.useSyncExternalStore=function(e,t,n){return k.H.useSyncExternalStore(e,t,n)},t.useTransition=function(){return k.H.useTransition()},t.version="19.2.4"},540(e,t,n){e.exports=n(869)}}]);
|
package/public/805.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(self.webpackChunksignalk_container=self.webpackChunksignalk_container||[]).push([[805],{805(e,t,n){n.r(t),n.d(t,{default:()=>s});var a=n(231);const i={root:{fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',color:"#333",padding:"16px 0"},sectionTitle:{fontSize:13,fontWeight:600,color:"#888",textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:10,marginTop:24},btn:{display:"inline-flex",alignItems:"center",gap:8,padding:"8px 16px",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},btnPrimary:{background:"#3b82f6",color:"#fff"},btnDanger:{background:"#ef4444",color:"#fff",padding:"6px 12px",fontSize:12},btnWarning:{background:"#f59e0b",color:"#fff",padding:"6px 12px",fontSize:12},btnSave:{background:"#3b82f6",color:"#fff"},btnSuccess:{background:"#10b981",color:"#fff"},btnDisabled:{opacity:.5,cursor:"not-allowed"},status:{marginTop:8,fontSize:12,minHeight:18},runtimeCard:{display:"flex",alignItems:"center",gap:14,padding:"14px 18px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,marginBottom:12},runtimeIcon:{width:44,height:44,borderRadius:10,display:"flex",alignItems:"center",justifyContent:"center",fontSize:22,flexShrink:0},runtimeInfo:{flex:1},runtimeName:{fontSize:15,fontWeight:600,color:"#333"},runtimeVersion:{fontSize:12,color:"#888"},containerItem:{display:"flex",alignItems:"center",gap:12,padding:"10px 14px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,marginBottom:8},stateIndicator:{width:10,height:10,borderRadius:"50%",flexShrink:0},containerInfo:{flex:1,minWidth:0},containerName:{fontSize:14,fontWeight:600,color:"#333"},containerMeta:{fontSize:11,color:"#888",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},containerActions:{display:"flex",gap:6,flexShrink:0},empty:{textAlign:"center",padding:"30px 16px",color:"#999",fontSize:13},fieldRow:{display:"flex",alignItems:"center",gap:12,marginBottom:10},label:{fontSize:13,fontWeight:500,color:"#555",width:160,flexShrink:0},select:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333"},hint:{fontSize:11,color:"#aaa",marginLeft:8},pruneResult:{fontSize:12,color:"#10b981",marginTop:6}},o={running:"#10b981",stopped:"#f59e0b",missing:"#94a3b8","no-runtime":"#ef4444"},r={running:"Running",stopped:"Stopped",missing:"Not created","no-runtime":"No runtime"};function l({label:e,value:t,options:n,onChange:o,hint:r}){return a.createElement("div",{style:i.fieldRow},a.createElement("span",{style:i.label},e),a.createElement("select",{style:i.select,value:t,onChange:e=>o(e.target.value)},n.map(e=>a.createElement("option",{key:e.value,value:e.value},e.label))),r&&a.createElement("span",{style:i.hint},r))}function s({configuration:e,save:t}){const n=e||{},[s,c]=(0,a.useState)(n.runtime||"auto"),[m,d]=(0,a.useState)(n.pruneSchedule||"weekly"),[u,p]=(0,a.useState)(null),[f,g]=(0,a.useState)([]),[b,y]=(0,a.useState)(!0),[v,h]=(0,a.useState)(""),[S,k]=(0,a.useState)(!1),[E,x]=(0,a.useState)(null),w=(0,a.useCallback)(async()=>{try{const[e,t]=await Promise.all([fetch("/plugins/signalk-container/api/runtime"),fetch("/plugins/signalk-container/api/containers")]);e.ok?p(await e.json()):p(null),t.ok?g(await t.json()):g([])}catch{p(null),g([])}y(!1)},[]);return(0,a.useEffect)(()=>{w();const e=setInterval(w,5e3);return()=>clearInterval(e)},[w]),a.createElement("div",{style:i.root},a.createElement("div",{style:i.sectionTitle},"Runtime"),b?a.createElement("div",{style:i.empty},"Detecting container runtime..."):u?a.createElement("div",{style:i.runtimeCard},a.createElement("div",{style:{...i.runtimeIcon,background:"podman"===u.runtime?"#892ca0":"#2496ed",color:"#fff"}},"podman"===u.runtime?"P":"D"),a.createElement("div",{style:i.runtimeInfo},a.createElement("div",{style:i.runtimeName},u.runtime.charAt(0).toUpperCase()+u.runtime.slice(1),u.isPodmanDockerShim?" (via docker shim)":""),a.createElement("div",{style:i.runtimeVersion},"Version ",u.version)),a.createElement("div",{style:{...i.stateIndicator,background:"#10b981"},title:"Runtime available"})):a.createElement("div",{style:i.runtimeCard},a.createElement("div",{style:{...i.runtimeIcon,background:"#fef2f2",color:"#ef4444"}},"!"),a.createElement("div",{style:i.runtimeInfo},a.createElement("div",{style:i.runtimeName},"No container runtime found"),a.createElement("div",{style:i.runtimeVersion},"Install Podman: sudo apt install podman"))),a.createElement("div",{style:i.sectionTitle},"Settings"),a.createElement(l,{label:"Preferred runtime",value:s,onChange:c,options:[{value:"auto",label:"Auto-detect (Podman preferred)"},{value:"podman",label:"Podman"},{value:"docker",label:"Docker"}]}),a.createElement(l,{label:"Auto-prune images",value:m,onChange:d,options:[{value:"off",label:"Off"},{value:"weekly",label:"Weekly"},{value:"monthly",label:"Monthly"}]}),a.createElement("div",{style:i.sectionTitle},"Managed Containers"),0===f.length?a.createElement("div",{style:i.empty},b?"Loading...":"No managed containers. Other plugins will create them."):f.map(e=>a.createElement("div",{key:e.name,style:i.containerItem},a.createElement("div",{style:{...i.stateIndicator,background:o[e.state]||"#94a3b8"},title:r[e.state]||e.state}),a.createElement("div",{style:i.containerInfo},a.createElement("div",{style:i.containerName},e.name),a.createElement("div",{style:i.containerMeta},e.image," · ",r[e.state]||e.state,e.ports&&e.ports.length>0&&e.ports[0]?` · ${e.ports.join(", ")}`:"")),a.createElement("div",{style:i.containerActions},"stopped"===e.state&&a.createElement("button",{style:{...i.btn,...i.btnPrimary,padding:"6px 12px",fontSize:12},onClick:()=>(async e=>{h(`Starting ${e}...`),k(!1);try{const t=await fetch(`/plugins/signalk-container/api/containers/${encodeURIComponent(e)}/start`,{method:"POST"});if(t.ok)h(`${e} started.`),w();else{const e=await t.json();h(`Failed: ${e.error}`),k(!0)}}catch(e){h(`Error: ${e.message}`),k(!0)}})(e.name)},"Start"),"running"===e.state&&a.createElement("button",{style:{...i.btn,...i.btnWarning},onClick:()=>(async e=>{h(`Stopping ${e}...`),k(!1);try{const t=await fetch(`/plugins/signalk-container/api/containers/${encodeURIComponent(e)}/stop`,{method:"POST"});if(t.ok)h(`${e} stopped.`),w();else{const e=await t.json();h(`Failed: ${e.error}`),k(!0)}}catch(e){h(`Error: ${e.message}`),k(!0)}})(e.name)},"Stop"),a.createElement("button",{style:{...i.btn,...i.btnDanger},onClick:()=>(async(e,t)=>{if("running"!==t||window.confirm(`${e} is running. Stop and remove it?`)){h(`Removing ${e}...`),k(!1);try{const t=await fetch(`/plugins/signalk-container/api/containers/${encodeURIComponent(e)}/remove`,{method:"POST"});if(t.ok)h(`${e} removed.`),w();else{const e=await t.json();h(`Failed: ${e.error}`),k(!0)}}catch(e){h(`Error: ${e.message}`),k(!0)}}})(e.name,e.state)},"Remove")))),a.createElement("div",{style:i.sectionTitle},"Maintenance"),a.createElement("button",{style:{...i.btn,...i.btnSuccess},onClick:async()=>{h("Pruning dangling images..."),k(!1),x(null);try{const e=await fetch("/plugins/signalk-container/api/prune",{method:"POST"});if(e.ok){const t=await e.json();x(t),h(`Pruned ${t.imagesRemoved} image(s), reclaimed ${t.spaceReclaimed}.`)}else{const t=await e.json();h(`Prune failed: ${t.error}`),k(!0)}}catch(e){h(`Error: ${e.message}`),k(!0)}}},"Prune Dangling Images"),v&&a.createElement("div",{style:{...i.status,color:S?"#ef4444":"#10b981"}},v),a.createElement("div",{style:{...i.sectionTitle,marginTop:28}}," "),a.createElement("button",{style:{...i.btn,...i.btnSave},onClick:()=>{t({runtime:s,pruneSchedule:m,maxConcurrentJobs:n.maxConcurrentJobs||2}),h("Saved! Plugin will restart."),k(!1)}},"Save Configuration"))}}}]);
|
package/public/main.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{var e,r,t={316(){}},n={};function a(e){var r=n[e];if(void 0!==r)return r.exports;var o=n[e]={exports:{}};return t[e](o,o.exports,a),o.exports}a.m=t,a.c=n,a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((r,t)=>(a.f[t](e,r),r),[])),a.u=e=>e+".js",a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),e={},r="signalk-container:",a.l=(t,n,o,i)=>{if(e[t])e[t].push(n);else{var l,c;if(void 0!==o)for(var s=document.getElementsByTagName("script"),u=0;u<s.length;u++){var p=s[u];if(p.getAttribute("src")==t||p.getAttribute("data-webpack")==r+o){l=p;break}}l||(c=!0,(l=document.createElement("script")).charset="utf-8",a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",r+o),l.src=t),e[t]=[n];var d=(r,n)=>{l.onerror=l.onload=null,clearTimeout(f);var a=e[t];if(delete e[t],l.parentNode&&l.parentNode.removeChild(l),a&&a.forEach(e=>e(n)),r)return r(n)},f=setTimeout(d.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=d.bind(null,l.onerror),l.onload=d.bind(null,l.onload),c&&document.head.appendChild(l)}},(()=>{a.S={};var e={},r={};a.I=(t,n)=>{n||(n=[]);var o=r[t];if(o||(o=r[t]={}),!(n.indexOf(o)>=0)){if(n.push(o),e[t])return e[t];a.o(a.S,t)||(a.S[t]={});var i=a.S[t],l="signalk-container",c=[];return"default"===t&&((e,r,t,n)=>{var o=i[e]=i[e]||{},c=o[r];(!c||!c.loaded&&(1!=!c.eager?n:l>c.from))&&(o[r]={get:()=>a.e(540).then(()=>()=>a(540)),from:l,eager:!1})})("react","19.2.4"),e[t]=c.length?Promise.all(c).then(()=>e[t]=1):1}}})(),(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var n=t.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=t[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{var e={792:0};a.f.j=(r,t)=>{var n=a.o(e,r)?e[r]:void 0;if(0!==n)if(n)t.push(n[2]);else{var o=new Promise((t,a)=>n=e[r]=[t,a]);t.push(n[2]=o);var i=a.p+a.u(r),l=new Error;a.l(i,t=>{if(a.o(e,r)&&(0!==(n=e[r])&&(e[r]=void 0),n)){var o=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+o+": "+i+")",l.name="ChunkLoadError",l.type=o,l.request=i,n[1](l)}},"chunk-"+r,r)}};var r=(r,t)=>{var n,o,[i,l,c]=t,s=0;if(i.some(r=>0!==e[r])){for(n in l)a.o(l,n)&&(a.m[n]=l[n]);c&&c(a)}for(r&&r(t);s<i.length;s++)o=i[s],a.o(e,o)&&e[o]&&e[o][0](),e[o]=0},t=self.webpackChunksignalk_container=self.webpackChunksignalk_container||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a(316)})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var signalk_container;(()=>{"use strict";var e,r,t,n,o,a,i,u,l,s,f,c,p,d,h,v,g,m,b,y={623(e,r,t){var n={"./PluginConfigurationPanel":()=>t.e(805).then(()=>()=>t(805))},o=(e,r)=>(t.R=r,r=t.o(n,e)?n[e]():Promise.resolve().then(()=>{throw new Error('Module "'+e+'" does not exist in container.')}),t.R=void 0,r),a=(e,r)=>{if(t.S){var n="default",o=t.S[n];if(o&&o!==e)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return t.S[n]=e,t.I(n,r)}};t.d(r,{get:()=>o,init:()=>a})}},w={};function S(e){var r=w[e];if(void 0!==r)return r.exports;var t=w[e]={exports:{}};return y[e](t,t.exports,S),t.exports}S.m=y,S.c=w,S.d=(e,r)=>{for(var t in r)S.o(r,t)&&!S.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},S.f={},S.e=e=>Promise.all(Object.keys(S.f).reduce((r,t)=>(S.f[t](e,r),r),[])),S.u=e=>e+".js",S.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),S.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),e={},r="signalk-container:",S.l=(t,n,o,a)=>{if(e[t])e[t].push(n);else{var i,u;if(void 0!==o)for(var l=document.getElementsByTagName("script"),s=0;s<l.length;s++){var f=l[s];if(f.getAttribute("src")==t||f.getAttribute("data-webpack")==r+o){i=f;break}}i||(u=!0,(i=document.createElement("script")).charset="utf-8",S.nc&&i.setAttribute("nonce",S.nc),i.setAttribute("data-webpack",r+o),i.src=t),e[t]=[n];var c=(r,n)=>{i.onerror=i.onload=null,clearTimeout(p);var o=e[t];if(delete e[t],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(e=>e(n)),r)return r(n)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=c.bind(null,i.onerror),i.onload=c.bind(null,i.onload),u&&document.head.appendChild(i)}},S.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{S.S={};var e={},r={};S.I=(t,n)=>{n||(n=[]);var o=r[t];if(o||(o=r[t]={}),!(n.indexOf(o)>=0)){if(n.push(o),e[t])return e[t];S.o(S.S,t)||(S.S[t]={});var a=S.S[t],i="signalk-container",u=[];return"default"===t&&((e,r,t,n)=>{var o=a[e]=a[e]||{},u=o[r];(!u||!u.loaded&&(1!=!u.eager?n:i>u.from))&&(o[r]={get:()=>S.e(540).then(()=>()=>S(540)),from:i,eager:!1})})("react","19.2.4"),e[t]=u.length?Promise.all(u).then(()=>e[t]=1):1}}})(),(()=>{var e;S.g.importScripts&&(e=S.g.location+"");var r=S.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var n=t.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=t[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),S.p=e})(),t=e=>{var r=e=>e.split(".").map(e=>+e==e?+e:e),t=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(e),n=t[1]?r(t[1]):[];return t[2]&&(n.length++,n.push.apply(n,r(t[2]))),t[3]&&(n.push([]),n.push.apply(n,r(t[3]))),n},n=(e,r)=>{e=t(e),r=t(r);for(var n=0;;){if(n>=e.length)return n<r.length&&"u"!=(typeof r[n])[0];var o=e[n],a=(typeof o)[0];if(n>=r.length)return"u"==a;var i=r[n],u=(typeof i)[0];if(a!=u)return"o"==a&&"n"==u||"s"==u||"u"==a;if("o"!=a&&"u"!=a&&o!=i)return o<i;n++}},o=e=>{var r=e[0],t="";if(1===e.length)return"*";if(r+.5){t+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var n=1,a=1;a<e.length;a++)n--,t+="u"==(typeof(u=e[a]))[0]?"-":(n>0?".":"")+(n=2,u);return t}var i=[];for(a=1;a<e.length;a++){var u=e[a];i.push(0===u?"not("+l()+")":1===u?"("+l()+" || "+l()+")":2===u?i.pop()+" "+i.pop():o(u))}return l();function l(){return i.pop().replace(/^\((.+)\)$/,"$1")}},a=(e,r)=>{if(0 in e){r=t(r);var n=e[0],o=n<0;o&&(n=-n-1);for(var i=0,u=1,l=!0;;u++,i++){var s,f,c=u<e.length?(typeof e[u])[0]:"";if(i>=r.length||"o"==(f=(typeof(s=r[i]))[0]))return!l||("u"==c?u>n&&!o:""==c!=o);if("u"==f){if(!l||"u"!=c)return!1}else if(l)if(c==f)if(u<=n){if(s!=e[u])return!1}else{if(o?s>e[u]:s<e[u])return!1;s!=e[u]&&(l=!1)}else if("s"!=c&&"n"!=c){if(o||u<=n)return!1;l=!1,u--}else{if(u<=n||f<c!=o)return!1;l=!1}else"s"!=c&&"n"!=c&&(l=!1,u--)}}var p=[],d=p.pop.bind(p);for(i=1;i<e.length;i++){var h=e[i];p.push(1==h?d()|d():2==h?d()&d():h?a(h,r):!d())}return!!d()},i=(e,r)=>e&&S.o(e,r),u=e=>(e.loaded=1,e.get()),l=e=>Object.keys(e).reduce((r,t)=>(e[t].eager&&(r[t]=e[t]),r),{}),s=(e,r,t)=>{var o=t?l(e[r]):e[r];return Object.keys(o).reduce((e,r)=>!e||!o[e].loaded&&n(e,r)?r:e,0)},f=(e,r,t,n)=>"Unsatisfied version "+t+" from "+(t&&e[r][t].from)+" of shared singleton module "+r+" (required "+o(n)+")",c=e=>{throw new Error(e)},p=e=>{"undefined"!=typeof console&&console.warn&&console.warn(e)},d=(e,r,t)=>t?t():((e,r)=>c("Shared module "+r+" doesn't exist in shared scope "+e))(e,r),h=(e=>function(r,t,n,o,a){var i=S.I(r);return i&&i.then&&!n?i.then(e.bind(e,r,S.S[r],t,!1,o,a)):e(r,S.S[r],t,n,o,a)})((e,r,t,n,o,l)=>{if(!i(r,t))return d(e,t,l);var c=s(r,t,n);return a(o,c)||p(f(r,t,c,o)),u(r[t][c])}),v={},g={231:()=>h("default","react",!1,[1,19],()=>S.e(540).then(()=>()=>S(540)))},m={805:[231]},b={},S.f.consumes=(e,r)=>{S.o(m,e)&&m[e].forEach(e=>{if(S.o(v,e))return r.push(v[e]);if(!b[e]){var t=r=>{v[e]=0,S.m[e]=t=>{delete S.c[e],t.exports=r()}};b[e]=!0;var n=r=>{delete v[e],S.m[e]=t=>{throw delete S.c[e],r}};try{var o=g[e]();o.then?r.push(v[e]=o.then(t).catch(n)):t(o)}catch(e){n(e)}}})},(()=>{var e={362:0};S.f.j=(r,t)=>{var n=S.o(e,r)?e[r]:void 0;if(0!==n)if(n)t.push(n[2]);else{var o=new Promise((t,o)=>n=e[r]=[t,o]);t.push(n[2]=o);var a=S.p+S.u(r),i=new Error;S.l(a,t=>{if(S.o(e,r)&&(0!==(n=e[r])&&(e[r]=void 0),n)){var o=t&&("load"===t.type?"missing":t.type),a=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+o+": "+a+")",i.name="ChunkLoadError",i.type=o,i.request=a,n[1](i)}},"chunk-"+r,r)}};var r=(r,t)=>{var n,o,[a,i,u]=t,l=0;if(a.some(r=>0!==e[r])){for(n in i)S.o(i,n)&&(S.m[n]=i[n]);u&&u(S)}for(r&&r(t);l<a.length;l++)o=a[l],S.o(e,o)&&e[o]&&e[o][0](),e[o]=0},t=self.webpackChunksignalk_container=self.webpackChunksignalk_container||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})();var k=S(623);signalk_container=k})();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { ModuleFederationPlugin } = require("webpack").container;
|
|
3
|
+
const packageJson = require("./package.json");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
entry: "./src/configpanel/index",
|
|
7
|
+
mode: "production",
|
|
8
|
+
output: {
|
|
9
|
+
path: path.resolve(__dirname, "public"),
|
|
10
|
+
clean: false,
|
|
11
|
+
},
|
|
12
|
+
module: {
|
|
13
|
+
rules: [
|
|
14
|
+
{
|
|
15
|
+
test: /\.jsx?$/,
|
|
16
|
+
loader: "babel-loader",
|
|
17
|
+
exclude: /node_modules/,
|
|
18
|
+
options: { presets: ["@babel/preset-react"] },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
resolve: {
|
|
23
|
+
extensions: [".js", ".jsx"],
|
|
24
|
+
},
|
|
25
|
+
plugins: [
|
|
26
|
+
new ModuleFederationPlugin({
|
|
27
|
+
name: packageJson.name.replace(/[-@/]/g, "_"),
|
|
28
|
+
library: {
|
|
29
|
+
type: "var",
|
|
30
|
+
name: packageJson.name.replace(/[-@/]/g, "_"),
|
|
31
|
+
},
|
|
32
|
+
filename: "remoteEntry.js",
|
|
33
|
+
exposes: {
|
|
34
|
+
"./PluginConfigurationPanel":
|
|
35
|
+
"./src/configpanel/PluginConfigurationPanel",
|
|
36
|
+
},
|
|
37
|
+
shared: {
|
|
38
|
+
react: { singleton: true, requiredVersion: "^19" },
|
|
39
|
+
"react-dom": { singleton: true, requiredVersion: "^19" },
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
};
|