@mxtommy/kip 4.4.0 → 4.5.0-beta.1

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.
Files changed (39) hide show
  1. package/.github/copilot-instructions.md +3 -0
  2. package/.github/instructions/best-practices.instructions.md +4 -0
  3. package/.github/instructions/project.instructions.md +6 -5
  4. package/package.json +1 -1
  5. package/plugin/index.js +128 -68
  6. package/plugin/openApi.json +8 -20
  7. package/public/chunk-2OB7ZJBR.js +3 -0
  8. package/public/{chunk-PSF2OKKT.js → chunk-5FEX27I4.js} +1 -1
  9. package/public/{chunk-MMIOUKLI.js → chunk-6GGJZDRE.js} +1 -1
  10. package/public/{chunk-XEJJOWK6.js → chunk-6V4GGGXE.js} +1 -1
  11. package/public/{chunk-7HLFWAA7.js → chunk-A5BW6BUM.js} +1 -1
  12. package/public/{chunk-IWK4FHBL.js → chunk-DGE5YFPU.js} +1 -1
  13. package/public/{chunk-3BGR52TW.js → chunk-G6M3Z3BY.js} +6 -6
  14. package/public/{chunk-5DLSQ773.js → chunk-GMGZLXY7.js} +1 -1
  15. package/public/{chunk-PESXPDBT.js → chunk-GUZ3BDVZ.js} +1 -1
  16. package/public/{chunk-BNIQFWQ6.js → chunk-HMOOTAEA.js} +1 -1
  17. package/public/{chunk-J4IGUIZA.js → chunk-ICDGHQFP.js} +2 -2
  18. package/public/{chunk-PDYVZHOK.js → chunk-IXQ7KIFY.js} +1 -1
  19. package/public/{chunk-TGGJAGV7.js → chunk-JCNE4QHQ.js} +12 -12
  20. package/public/chunk-K6XYUNG4.js +8 -0
  21. package/public/chunk-KFFAA7DL.js +10 -0
  22. package/public/{chunk-VH4ZIU4T.js → chunk-LGCQEN7V.js} +1 -1
  23. package/public/{chunk-UVAQADRE.js → chunk-M2B5OYGO.js} +1 -1
  24. package/public/{chunk-557F3J5T.js → chunk-O3JH7UTR.js} +1 -1
  25. package/public/{chunk-2KMYPGX4.js → chunk-Q3USFT4F.js} +1 -1
  26. package/public/{chunk-TKC7ROZ7.js → chunk-QVCLOCEC.js} +1 -1
  27. package/public/chunk-QZKCRH3H.js +1 -0
  28. package/public/{chunk-HJQQWPGC.js → chunk-T6TFVZVM.js} +1 -1
  29. package/public/{chunk-PKATAZA2.js → chunk-VIKU7BH7.js} +1 -1
  30. package/public/chunk-XMQPXXLW.js +8 -0
  31. package/public/{chunk-PGELIHBX.js → chunk-YIYYVDFO.js} +1 -1
  32. package/public/{chunk-KIR67PZ2.js → chunk-ZQER6AIQ.js} +1 -1
  33. package/public/index.html +1 -1
  34. package/public/main-4URMGBQS.js +1 -0
  35. package/public/chunk-2MWBAYPJ.js +0 -8
  36. package/public/chunk-4IHRH3BQ.js +0 -9
  37. package/public/chunk-JP7ZAJ6C.js +0 -3
  38. package/public/chunk-KKJXPB75.js +0 -8
  39. package/public/main-I7M3MAJT.js +0 -1
@@ -132,6 +132,7 @@ Template:
132
132
  - CommonJS deps are explicitly allowed (js-quantities). Avoid introducing new CJS without adding to allowedCommonJsDependencies.
133
133
  - Use standalone components, signals, @if/@for; follow .github/instructions/angular.instructions.md for style.
134
134
  - Widget config UIs live under src/app/widget-config; path controls use custom validators (no Validators.required). Respect isPathConfigurable and pathRequired.
135
+ - Documentation standard: Every public property and public method in TypeScript code MUST include full JSDoc with: purpose, parameters, return value, and at least one usage example. Apply this by default for all generated/edited code unless a file explicitly cannot use comments.
135
136
 
136
137
  ## Widgets: do this, not that (Host2)
137
138
  - Do: Provide a complete `DEFAULT_CONFIG` with all paths & options. Don’t: Scatter defaults across lifecycle hooks.
@@ -143,10 +144,12 @@ Template:
143
144
 
144
145
  ## Key files/dirs
145
146
  - Core services: `src/app/core/services/` (DataService, SignalKConnectionService, SignalKDeltaService, AppNetworkInitService, UnitsService, DataSetService, NotificationsService)
147
+ - Plugin config foundation: `src/app/core/services/signalk-plugin-config.service.ts` (plugin-only detection, dependency validation, schema normalization metadata, and config persistence via `/plugins` endpoints)
146
148
  - Directives: `src/app/core/directives/` (widget-runtime, widget-streams, widget-metadata)
147
149
  - Widgets: `src/app/widgets/` (e.g., widget-numeric, widget-gauge-ng-*, widget-data-chart, widget-windtrends-chart, widget-autopilot)
148
150
  - Embedded host: `src/app/core/components/widget-embedded/`
149
151
  - Config UI: `src/app/widget-config/`
152
+ - Plugin management (server plugins) is handled separately through `SignalkPluginConfigService` and `/plugins` REST endpoints. Keep install/uninstall out of scope unless explicitly added.
150
153
  - Build: `angular.json`, `package.json` scripts
151
154
 
152
155
  ## Debugging
@@ -53,3 +53,7 @@ You are an expert in TypeScript, Angular, and scalable web application developme
53
53
  - Design services around a single responsibility
54
54
  - Use the `providedIn: 'root'` option for singleton services
55
55
  - Use the `inject()` function instead of constructor injection
56
+
57
+ ## Documentation
58
+
59
+ - Every public TypeScript property and public method MUST include full JSDoc with: purpose, parameters, return value, and at least one usage example.
@@ -53,6 +53,7 @@ KIP Instrument MFD is an advanced and versatile marine instrumentation package d
53
53
  ## 6. Documentation & Comments
54
54
  - **Document all custom validators and business rules.**
55
55
  - **Update this file and the README with any major changes or new patterns.**
56
+ - **JSDoc requirement:** Every public TypeScript property and public method must include full JSDoc containing: purpose, parameters, return value, and at least one usage example.
56
57
 
57
58
  ---
58
59
 
@@ -136,11 +137,11 @@ All major services in `src/app/core/services/` are summarized below for Copilot
136
137
  - Key methods: Data set CRUD, data source updates.
137
138
  - Dependencies: DataService, StorageService.
138
139
 
139
- - **SignalKPluginsService (`signalk-plugins.service.ts`)**
140
- - Purpose: Manages Signal K plugin discovery, configuration, and state.
141
- - Key methods: Plugin list management, config updates.
142
- - Dependencies: SignalKConnectionService, DataService.
143
- - Usage: Used to manage plugins and their configuration.
140
+ - **SignalkPluginConfigService (`signalk-plugin-config.service.ts`)**
141
+ - Purpose: Plugin configuration foundation service for dependency checks and plugin state/config persistence via Signal K `/plugins` endpoints.
142
+ - Key methods: `listPlugins()`, `getPlugin()`, `getPluginConfig()`, `savePluginConfig()`, `setPluginEnabled()`, `validateDependency()`, `normalizePluginSchema()`.
143
+ - Dependencies: HttpClient, SignalKConnectionService.
144
+ - Usage: Source of truth for plugin detection and config save flows; no plugin install/uninstall support.
144
145
 
145
146
  - **SignalKRequestsService (`signalk-requests.service.ts`)**
146
147
  - Purpose: Handles requests to the Signal K server, such as PUT/POST operations and custom actions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxtommy/kip",
3
- "version": "4.4.0",
3
+ "version": "4.5.0-beta.1",
4
4
  "description": "An advanced and versatile marine instrumentation package to display Signal K data.",
5
5
  "license": "MIT",
6
6
  "author": {
package/plugin/index.js CHANGED
@@ -36,11 +36,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const server_api_1 = require("@signalk/server-api");
37
37
  const openapi = __importStar(require("./openApi.json"));
38
38
  const start = (server) => {
39
+ const mutableOpenApi = JSON.parse(JSON.stringify(openapi.default ?? openapi));
39
40
  const API_PATHS = {
40
41
  DISPLAYS: `/displays`,
41
42
  INSTANCE: `/displays/:displayId`,
42
- ACTIVE_SCREEN: `/displays/:displayId/screenIndex`,
43
- CHANGE_SCREEN: `/displays/:displayId/activeScreen`
43
+ SCREEN_INDEX: `/displays/:displayId/screenIndex`,
44
+ ACTIVATE_SCREEN: `/displays/:displayId/activeScreen`
45
+ };
46
+ const PUT_CONTEXT = 'vessels.self';
47
+ const COMMAND_PATHS = {
48
+ SET_DISPLAY: 'kip.remote.setDisplay',
49
+ SET_SCREEN_INDEX: 'kip.remote.setScreenIndex',
50
+ REQUEST_ACTIVE_SCREEN: 'kip.remote.requestActiveScreen'
44
51
  };
45
52
  const CONFIG_SCHEMA = {
46
53
  properties: {
@@ -64,11 +71,6 @@ const start = (server) => {
64
71
  server.debug(`getAvailableDisplays: fullPath=${JSON.stringify(fullPath)}`);
65
72
  return typeof fullPath === 'object' && fullPath !== null ? fullPath : undefined;
66
73
  }
67
- function pathToDotNotation(path) {
68
- const dottedPath = path.replace(/\//g, '.').replace(/^\./, '');
69
- server.debug(`pathToDotNotation: input path=${path}, dottedPath=${dottedPath}`);
70
- return dottedPath;
71
- }
72
74
  function sendOk(res, body) {
73
75
  if (body === undefined)
74
76
  return res.status(204).end();
@@ -77,12 +79,91 @@ const start = (server) => {
77
79
  function sendFail(res, statusCode, message) {
78
80
  return res.status(statusCode).json({ state: 'FAILED', statusCode, message });
79
81
  }
82
+ function completed(statusCode, message) {
83
+ return { state: 'COMPLETED', statusCode, message };
84
+ }
85
+ function isValidDisplayId(displayId) {
86
+ return typeof displayId === 'string' && /^[A-Za-z0-9-]+$/.test(displayId);
87
+ }
88
+ function applyDisplayWrite(displayId, suffix, value) {
89
+ const path = suffix ? `displays.${displayId}.${suffix}` : `displays.${displayId}`;
90
+ try {
91
+ server.handleMessage(plugin.id, {
92
+ updates: [
93
+ {
94
+ values: [
95
+ {
96
+ path: path,
97
+ value: value ?? null
98
+ }
99
+ ]
100
+ }
101
+ ]
102
+ }, server_api_1.SKVersion.v1);
103
+ return completed(200);
104
+ }
105
+ catch (error) {
106
+ const message = error?.message ?? 'Unable to write display path';
107
+ return completed(400, message);
108
+ }
109
+ }
110
+ function handleSetDisplay(value) {
111
+ const command = value;
112
+ if (!command || typeof command !== 'object') {
113
+ return completed(400, 'Command payload is required');
114
+ }
115
+ if (!isValidDisplayId(command.displayId)) {
116
+ return completed(400, 'Invalid displayId format');
117
+ }
118
+ const displayValue = command.display ?? null;
119
+ if (displayValue !== null && typeof displayValue !== 'object') {
120
+ return completed(400, 'display must be an object or null');
121
+ }
122
+ return applyDisplayWrite(command.displayId, null, displayValue);
123
+ }
124
+ function handleScreenWrite(value, suffix) {
125
+ const command = value;
126
+ if (!command || typeof command !== 'object') {
127
+ return completed(400, 'Command payload is required');
128
+ }
129
+ if (!isValidDisplayId(command.displayId)) {
130
+ return completed(400, 'Invalid displayId format');
131
+ }
132
+ const screenIdxValue = command.screenIdx ?? null;
133
+ if (screenIdxValue !== null && typeof screenIdxValue !== 'number') {
134
+ return completed(400, 'screenIdx must be a number or null');
135
+ }
136
+ return applyDisplayWrite(command.displayId, suffix, screenIdxValue);
137
+ }
138
+ function sendActionAsRest(res, result) {
139
+ if (result.statusCode === 200) {
140
+ return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
141
+ }
142
+ return sendFail(res, result.statusCode || 400, result.message || 'Command failed');
143
+ }
80
144
  const plugin = {
81
145
  id: 'kip',
82
146
  name: 'KIP',
83
147
  description: 'KIP server plugin',
84
148
  start: (settings) => {
85
149
  server.debug(`Starting plugin with settings: ${JSON.stringify(settings)}`);
150
+ if (server.registerPutHandler) {
151
+ server.registerPutHandler(PUT_CONTEXT, COMMAND_PATHS.SET_DISPLAY, (context, path, value) => {
152
+ void context;
153
+ void path;
154
+ return handleSetDisplay(value);
155
+ }, plugin.id);
156
+ server.registerPutHandler(PUT_CONTEXT, COMMAND_PATHS.SET_SCREEN_INDEX, (context, path, value) => {
157
+ void context;
158
+ void path;
159
+ return handleScreenWrite(value, 'screenIndex');
160
+ }, plugin.id);
161
+ server.registerPutHandler(PUT_CONTEXT, COMMAND_PATHS.REQUEST_ACTIVE_SCREEN, (context, path, value) => {
162
+ void context;
163
+ void path;
164
+ return handleScreenWrite(value, 'activeScreen');
165
+ }, plugin.id);
166
+ }
86
167
  server.setPluginStatus(`Starting...`);
87
168
  },
88
169
  stop: () => {
@@ -92,7 +173,7 @@ const start = (server) => {
92
173
  },
93
174
  schema: () => CONFIG_SCHEMA,
94
175
  registerWithRouter(router) {
95
- server.debug(`Registering plugin routes: ${API_PATHS.DISPLAYS}, ${API_PATHS.INSTANCE}, ${API_PATHS.ACTIVE_SCREEN}, ${API_PATHS.CHANGE_SCREEN}`);
176
+ server.debug(`Registering plugin routes: ${API_PATHS.DISPLAYS}, ${API_PATHS.INSTANCE}, ${API_PATHS.SCREEN_INDEX}, ${API_PATHS.ACTIVATE_SCREEN}`);
96
177
  // Validate/normalize :displayId where present
97
178
  router.param('displayId', (req, res, next, displayId) => {
98
179
  if (displayId == null)
@@ -135,21 +216,12 @@ const start = (server) => {
135
216
  router.put(`${API_PATHS.INSTANCE}`, async (req, res) => {
136
217
  server.debug(`** PUT ${API_PATHS.INSTANCE}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
137
218
  try {
138
- const dottedPath = pathToDotNotation(req.path);
139
- server.debug(`Updating SK path ${dottedPath}`);
140
- server.handleMessage(plugin.id, {
141
- updates: [
142
- {
143
- values: [
144
- {
145
- path: dottedPath,
146
- value: req.body ?? null
147
- }
148
- ]
149
- }
150
- ]
151
- }, server_api_1.SKVersion.v1);
152
- return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
219
+ const displayId = req.displayId;
220
+ if (!displayId) {
221
+ return sendFail(res, 400, 'Missing displayId parameter');
222
+ }
223
+ const result = handleSetDisplay({ displayId, display: req.body ?? null });
224
+ return sendActionAsRest(res, result);
153
225
  }
154
226
  catch (error) {
155
227
  const msg = `HandleMessage failed with errors!`;
@@ -158,24 +230,18 @@ const start = (server) => {
158
230
  return sendFail(res, 400, error.message);
159
231
  }
160
232
  });
161
- router.put(`${API_PATHS.ACTIVE_SCREEN}`, async (req, res) => {
162
- server.debug(`** PUT ${API_PATHS.ACTIVE_SCREEN}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
233
+ router.put(`${API_PATHS.SCREEN_INDEX}`, async (req, res) => {
234
+ server.debug(`** PUT ${API_PATHS.SCREEN_INDEX}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
163
235
  try {
164
- const dottedPath = pathToDotNotation(req.path);
165
- server.debug(`Updating SK path ${dottedPath} with body.screenIdx`);
166
- server.handleMessage(plugin.id, {
167
- updates: [
168
- {
169
- values: [
170
- {
171
- path: dottedPath,
172
- value: req.body.screenIdx !== undefined ? req.body.screenIdx : null
173
- }
174
- ]
175
- }
176
- ]
177
- }, server_api_1.SKVersion.v1);
178
- return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
236
+ const displayId = req.displayId;
237
+ if (!displayId) {
238
+ return sendFail(res, 400, 'Missing displayId parameter');
239
+ }
240
+ const result = handleScreenWrite({
241
+ displayId,
242
+ screenIdx: req.body?.screenIdx !== undefined ? req.body.screenIdx : null
243
+ }, 'screenIndex');
244
+ return sendActionAsRest(res, result);
179
245
  }
180
246
  catch (error) {
181
247
  const msg = `HandleMessage failed with errors!`;
@@ -184,24 +250,18 @@ const start = (server) => {
184
250
  return sendFail(res, 400, error.message);
185
251
  }
186
252
  });
187
- router.put(`${API_PATHS.CHANGE_SCREEN}`, async (req, res) => {
188
- server.debug(`** PUT ${API_PATHS.CHANGE_SCREEN}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
253
+ router.put(`${API_PATHS.ACTIVATE_SCREEN}`, async (req, res) => {
254
+ server.debug(`** PUT ${API_PATHS.ACTIVATE_SCREEN}. Params: ${JSON.stringify(req.params)} Body: ${JSON.stringify(req.body)}`);
189
255
  try {
190
- const dottedPath = pathToDotNotation(req.path);
191
- server.debug(`Updating SK path ${dottedPath} with body.screenIdx`);
192
- server.handleMessage(plugin.id, {
193
- updates: [
194
- {
195
- values: [
196
- {
197
- path: dottedPath,
198
- value: req.body.screenIdx !== undefined ? req.body.screenIdx : null
199
- }
200
- ]
201
- }
202
- ]
203
- }, server_api_1.SKVersion.v1);
204
- return res.status(200).json({ state: 'SUCCESS', statusCode: 200 });
256
+ const displayId = req.displayId;
257
+ if (!displayId) {
258
+ return sendFail(res, 400, 'Missing displayId parameter');
259
+ }
260
+ const result = handleScreenWrite({
261
+ displayId,
262
+ screenIdx: req.body?.screenIdx !== undefined ? req.body.screenIdx : null
263
+ }, 'activeScreen');
264
+ return sendActionAsRest(res, result);
205
265
  }
206
266
  catch (error) {
207
267
  const msg = `HandleMessage failed with errors!`;
@@ -252,8 +312,8 @@ const start = (server) => {
252
312
  return sendFail(res, 400, error.message);
253
313
  }
254
314
  });
255
- router.get(`${API_PATHS.ACTIVE_SCREEN}`, (req, res) => {
256
- server.debug(`*** GET ACTIVE_SCREEN ${API_PATHS.ACTIVE_SCREEN}. Params: ${JSON.stringify(req.params)}`);
315
+ router.get(`${API_PATHS.SCREEN_INDEX}`, (req, res) => {
316
+ server.debug(`*** GET SCREEN_INDEX ${API_PATHS.SCREEN_INDEX}. Params: ${JSON.stringify(req.params)}`);
257
317
  try {
258
318
  const displayId = req.displayId;
259
319
  if (!displayId) {
@@ -272,23 +332,23 @@ const start = (server) => {
272
332
  return sendFail(res, 400, error.message);
273
333
  }
274
334
  });
275
- router.get(`${API_PATHS.CHANGE_SCREEN}`, (req, res) => {
276
- server.debug(`*** GET CHANGE_SCREEN ${API_PATHS.CHANGE_SCREEN}. Params: ${JSON.stringify(req.params)}`);
335
+ router.get(`${API_PATHS.ACTIVATE_SCREEN}`, (req, res) => {
336
+ server.debug(`*** GET ACTIVATE_SCREEN ${API_PATHS.ACTIVATE_SCREEN}. Params: ${JSON.stringify(req.params)}`);
277
337
  try {
278
- const changeId = req.changeId;
279
- if (!changeId) {
280
- return sendFail(res, 400, 'Missing changeId parameter');
338
+ const displayId = req.displayId;
339
+ if (!displayId) {
340
+ return sendFail(res, 400, 'Missing displayId parameter');
281
341
  }
282
- const node = getDisplaySelfPath(changeId, 'activeScreen');
342
+ const node = getDisplaySelfPath(displayId, 'activeScreen');
283
343
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
284
344
  const idx = node?.value ?? null;
285
345
  if (idx === undefined) {
286
- return sendFail(res, 404, `Change display screen Id ${changeId} not found in path`);
346
+ return sendFail(res, 404, `Change display screen Id ${displayId} not found in path`);
287
347
  }
288
348
  return sendOk(res, idx);
289
349
  }
290
350
  catch (error) {
291
- server.error(`Error reading activeScreen for ${req.params?.changeId}: ${String(error.message || error)}`);
351
+ server.error(`Error reading activeScreen for ${req.params?.displayId}: ${String(error.message || error)}`);
292
352
  return sendFail(res, 400, error.message);
293
353
  }
294
354
  });
@@ -302,7 +362,7 @@ const start = (server) => {
302
362
  }
303
363
  server.setPluginStatus(`Providing remote display screen control`);
304
364
  },
305
- getOpenApi: () => openapi
365
+ getOpenApi: () => mutableOpenApi
306
366
  };
307
367
  return plugin;
308
368
  };
@@ -63,17 +63,11 @@
63
63
  ]
64
64
  },
65
65
  "ScreenListOrNull": {
66
- "oneOf": [
67
- {
68
- "type": "array",
69
- "items": {
70
- "$ref": "#/components/schemas/ScreenItem"
71
- }
72
- },
73
- {
74
- "type": "null"
75
- }
76
- ]
66
+ "type": "array",
67
+ "nullable": true,
68
+ "items": {
69
+ "$ref": "#/components/schemas/ScreenItem"
70
+ }
77
71
  },
78
72
  "SuccessResponse": {
79
73
  "type": "object",
@@ -129,15 +123,9 @@
129
123
  }
130
124
  },
131
125
  "AnyObjectOrNull": {
132
- "oneOf": [
133
- {
134
- "type": "object",
135
- "additionalProperties": true
136
- },
137
- {
138
- "type": "null"
139
- }
140
- ]
126
+ "type": "object",
127
+ "nullable": true,
128
+ "additionalProperties": true
141
129
  }
142
130
  },
143
131
  "parameters": {