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.
@@ -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)}}]);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @license React
3
+ * react.production.js
4
+ *
5
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */
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
+ };