signalk-polar-performance-plugin 1.2.0 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.2.1] - 2026-07-25
8
+
9
+ ### Fixed
10
+ - `openApi.json` was missing from the npm tarball (`files` field in `package.json` did not list it), causing the plugin to crash on install with `Cannot find module '../openApi.json'` (#20).
11
+
12
+ ## [1.2.0] - 2026-07-25
13
+
7
14
  ## [1.1.0] - 2026-07-21
8
15
 
9
16
  ### Added
package/openApi.json ADDED
@@ -0,0 +1,912 @@
1
+ {
2
+ "openapi": "3.0.3",
3
+ "info": {
4
+ "title": "Polar Performance Plugin API",
5
+ "version": "1.0.0",
6
+ "description": "REST API for polar library management and polar performance queries.\nAll speed values are in m/s, all angles in radians."
7
+ },
8
+ "servers": [
9
+ { "url": "/plugins/signalk-polar-performance-plugin" }
10
+ ],
11
+ "tags": [
12
+ {
13
+ "name": "polar-manager",
14
+ "description": "Polar library management: list, replace, inspect, delete, and select canonical polars by id."
15
+ },
16
+ {
17
+ "name": "polar-query",
18
+ "description": "Polar performance queries: inspect axes and derive speed, targets, curves, and performance ratios for any stored polar."
19
+ },
20
+ {
21
+ "name": "polar-import",
22
+ "description": "Optional polar ingestion extension: discover supported external sources and text formats, convert them to canonical polar resources, and store them in the library."
23
+ }
24
+ ],
25
+ "paths": {
26
+
27
+ "/polars": {
28
+ "get": {
29
+ "tags": ["polar-manager"],
30
+ "operationId": "listPolars",
31
+ "summary": "List all stored polars",
32
+ "description": "Returns a summary of every polar in the library.",
33
+ "responses": {
34
+ "200": {
35
+ "description": "Array of polar summaries",
36
+ "content": {
37
+ "application/json": {
38
+ "schema": {
39
+ "type": "array",
40
+ "items": { "$ref": "#/components/schemas/PolarSummary" }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ },
47
+ "post": {
48
+ "tags": ["polar-manager"],
49
+ "operationId": "createPolar",
50
+ "summary": "Create a canonical polar with an auto-generated id",
51
+ "description": "Stores the supplied canonical PolarTable resource under an auto-generated id and returns that id.",
52
+ "requestBody": {
53
+ "required": true,
54
+ "content": {
55
+ "application/json": {
56
+ "schema": { "$ref": "#/components/schemas/PolarResourceBody" }
57
+ }
58
+ }
59
+ },
60
+ "responses": {
61
+ "201": {
62
+ "description": "Polar stored with an auto-generated id",
63
+ "content": {
64
+ "application/json": {
65
+ "schema": { "$ref": "#/components/schemas/IdResponse" }
66
+ }
67
+ }
68
+ },
69
+ "400": {
70
+ "description": "Invalid canonical polar resource",
71
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
72
+ }
73
+ }
74
+ }
75
+ },
76
+
77
+ "/imports/formats": {
78
+ "get": {
79
+ "tags": ["polar-import"],
80
+ "operationId": "listImportFormats",
81
+ "summary": "List supported text import formats",
82
+ "description": "Returns the format identifiers accepted by the text import endpoint. These formats are conversion inputs only; imported data is stored as canonical polar resources.",
83
+ "responses": {
84
+ "200": {
85
+ "description": "Supported text import formats",
86
+ "content": {
87
+ "application/json": {
88
+ "schema": {
89
+ "type": "array",
90
+ "items": { "$ref": "#/components/schemas/ImportFormatDescriptor" }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ }
97
+ },
98
+
99
+ "/imports/text/{format}": {
100
+ "post": {
101
+ "tags": ["polar-import"],
102
+ "operationId": "importPolarFromText",
103
+ "summary": "Import a polar from text content",
104
+ "description": "Parses the supplied text payload in the specified format, converts it to the canonical polar resource model, stores it in the local library, and returns the assigned id.",
105
+ "parameters": [
106
+ {
107
+ "name": "format",
108
+ "in": "path",
109
+ "required": true,
110
+ "description": "Text format identifier returned by GET /imports/formats.",
111
+ "schema": { "type": "string" }
112
+ }
113
+ ],
114
+ "requestBody": {
115
+ "required": true,
116
+ "content": {
117
+ "application/json": {
118
+ "schema": { "$ref": "#/components/schemas/TextPolarImportRequest" }
119
+ }
120
+ }
121
+ },
122
+ "responses": {
123
+ "201": {
124
+ "description": "Polar imported and stored",
125
+ "content": {
126
+ "application/json": {
127
+ "schema": { "$ref": "#/components/schemas/IdResponse" }
128
+ }
129
+ }
130
+ },
131
+ "400": {
132
+ "description": "Unsupported format or invalid text payload",
133
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
134
+ },
135
+ "409": {
136
+ "description": "Requested id already exists",
137
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
138
+ }
139
+ }
140
+ }
141
+ },
142
+
143
+ "/imports/sources": {
144
+ "get": {
145
+ "tags": ["polar-import"],
146
+ "operationId": "listImportSources",
147
+ "summary": "List supported external import sources",
148
+ "description": "Returns the external sources from which polars can be searched and imported. The current backend exposes the official ORC active certificate source as `orc`.",
149
+ "responses": {
150
+ "200": {
151
+ "description": "Supported external sources",
152
+ "content": {
153
+ "application/json": {
154
+ "schema": {
155
+ "type": "array",
156
+ "items": { "$ref": "#/components/schemas/ImportSourceDescriptor" }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+ },
164
+
165
+ "/imports/sources/{source}/search": {
166
+ "get": {
167
+ "tags": ["polar-import"],
168
+ "operationId": "searchImportSource",
169
+ "summary": "Search an external polar source",
170
+ "description": "Queries the specified external source for polar candidates matching the search term. For the `orc` source this searches the official active certificate index by certificate RefNo, boat name, sail number, and class.",
171
+ "parameters": [
172
+ {
173
+ "name": "source",
174
+ "in": "path",
175
+ "required": true,
176
+ "description": "External source identifier returned by GET /imports/sources. The current backend exposes `orc` for the official ORC active certificate source.",
177
+ "schema": { "type": "string" }
178
+ },
179
+ {
180
+ "name": "q",
181
+ "in": "query",
182
+ "required": false,
183
+ "description": "Free-text search string matched against source-specific polar metadata.",
184
+ "schema": { "type": "string" }
185
+ }
186
+ ],
187
+ "responses": {
188
+ "200": {
189
+ "description": "Matching external polar candidates",
190
+ "content": {
191
+ "application/json": {
192
+ "schema": {
193
+ "type": "array",
194
+ "items": { "$ref": "#/components/schemas/ExternalPolarSummary" }
195
+ }
196
+ }
197
+ }
198
+ },
199
+ "404": {
200
+ "description": "Import source not found",
201
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
202
+ },
203
+ "502": {
204
+ "description": "Failed to reach the external source",
205
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
206
+ }
207
+ }
208
+ }
209
+ },
210
+
211
+ "/imports/sources/{source}/items/{externalId}": {
212
+ "post": {
213
+ "tags": ["polar-import"],
214
+ "operationId": "importPolarFromSource",
215
+ "summary": "Import a polar from an external source",
216
+ "description": "Fetches the specified external polar, converts it to the canonical resource model, stores it in the local library, and returns the assigned id. For the `orc` source, `externalId` is the certificate RefNo from the official active certificate index.",
217
+ "parameters": [
218
+ {
219
+ "name": "source",
220
+ "in": "path",
221
+ "required": true,
222
+ "description": "External source identifier returned by GET /imports/sources. The current backend exposes `orc` for the official ORC active certificate source.",
223
+ "schema": { "type": "string" }
224
+ },
225
+ {
226
+ "name": "externalId",
227
+ "in": "path",
228
+ "required": true,
229
+ "description": "External polar identifier returned by GET /imports/sources/{source}/search. For the `orc` source, this is the certificate RefNo.",
230
+ "schema": { "type": "string" }
231
+ }
232
+ ],
233
+ "responses": {
234
+ "201": {
235
+ "description": "Polar imported and stored",
236
+ "content": {
237
+ "application/json": {
238
+ "schema": { "$ref": "#/components/schemas/IdResponse" }
239
+ }
240
+ }
241
+ },
242
+ "404": {
243
+ "description": "Import source or external polar not found",
244
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
245
+ },
246
+ "409": {
247
+ "description": "Requested id already exists",
248
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
249
+ },
250
+ "502": {
251
+ "description": "Failed to reach the external source",
252
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
253
+ }
254
+ }
255
+ }
256
+ },
257
+
258
+ "/polars/active": {
259
+ "get": {
260
+ "tags": ["polar-manager"],
261
+ "operationId": "getActivePolar",
262
+ "summary": "Get the active polar id",
263
+ "description": "Returns the id of the currently active polar.",
264
+ "responses": {
265
+ "200": {
266
+ "description": "Active polar id",
267
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IdResponse" } } }
268
+ },
269
+ "404": {
270
+ "description": "No polar is currently active",
271
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
272
+ }
273
+ }
274
+ },
275
+ "put": {
276
+ "tags": ["polar-manager"],
277
+ "operationId": "setActivePolar",
278
+ "summary": "Set the active polar",
279
+ "description": "Makes the identified polar the active polar for live computation.",
280
+ "requestBody": {
281
+ "required": true,
282
+ "content": {
283
+ "application/json": {
284
+ "schema": { "$ref": "#/components/schemas/IdResponse" }
285
+ }
286
+ }
287
+ },
288
+ "responses": {
289
+ "200": {
290
+ "description": "Active polar updated",
291
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IdResponse" } } }
292
+ },
293
+ "404": {
294
+ "description": "Polar not found",
295
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
296
+ }
297
+ }
298
+ },
299
+ "delete": {
300
+ "tags": ["polar-manager"],
301
+ "operationId": "clearActivePolar",
302
+ "summary": "Clear the active polar",
303
+ "description": "Clears the active polar so no stored polar is used for live computation.",
304
+ "responses": {
305
+ "200": {
306
+ "description": "Active polar cleared",
307
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IdResponse" } } }
308
+ }
309
+ }
310
+ }
311
+ },
312
+
313
+ "/polars/{id}": {
314
+ "parameters": [
315
+ { "$ref": "#/components/parameters/polarId" }
316
+ ],
317
+ "get": {
318
+ "tags": ["polar-manager"],
319
+ "operationId": "getPolar",
320
+ "summary": "Get a canonical polar resource",
321
+ "description": "Returns the full canonical PolarTable resource, including axes, values, units, symmetry assumptions, and derived metadata.",
322
+ "responses": {
323
+ "200": {
324
+ "description": "Canonical polar resource",
325
+ "content": {
326
+ "application/json": {
327
+ "schema": { "$ref": "#/components/schemas/PolarResource" }
328
+ }
329
+ }
330
+ },
331
+ "404": {
332
+ "description": "Polar not found",
333
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
334
+ }
335
+ }
336
+ },
337
+ "put": {
338
+ "tags": ["polar-manager"],
339
+ "operationId": "replacePolar",
340
+ "summary": "Create or replace a canonical polar resource",
341
+ "description": "Stores the supplied canonical PolarTable resource under the given id. If the polar exists it is replaced; otherwise it is created.",
342
+ "requestBody": {
343
+ "required": true,
344
+ "content": {
345
+ "application/json": {
346
+ "schema": { "$ref": "#/components/schemas/PolarResourceBody" }
347
+ }
348
+ }
349
+ },
350
+ "responses": {
351
+ "200": {
352
+ "description": "Polar stored",
353
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IdResponse" } } }
354
+ },
355
+ "400": {
356
+ "description": "Invalid canonical resource body",
357
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
358
+ }
359
+ }
360
+ },
361
+ "delete": {
362
+ "tags": ["polar-manager"],
363
+ "operationId": "deletePolar",
364
+ "summary": "Delete a polar",
365
+ "description": "Permanently removes the identified polar from the library.",
366
+ "responses": {
367
+ "200": {
368
+ "description": "Polar deleted",
369
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IdResponse" } } }
370
+ },
371
+ "404": {
372
+ "description": "Polar not found",
373
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
374
+ }
375
+ }
376
+ }
377
+ },
378
+
379
+ "/polars/{id}/meta": {
380
+ "get": {
381
+ "tags": ["polar-manager"],
382
+ "operationId": "getPolarMeta",
383
+ "summary": "Get polar metadata",
384
+ "description": "Returns the stored metadata for the identified polar: name, sailnumber, boatType, year, source, notes, and TWS range.",
385
+ "parameters": [
386
+ { "$ref": "#/components/parameters/polarId" }
387
+ ],
388
+ "responses": {
389
+ "200": {
390
+ "description": "Polar metadata",
391
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PolarMeta" } } }
392
+ },
393
+ "404": {
394
+ "description": "Polar not found",
395
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
396
+ }
397
+ }
398
+ }
399
+ },
400
+
401
+ "/polars/{id}/axes/tws": {
402
+ "get": {
403
+ "tags": ["polar-query"],
404
+ "operationId": "getPolarTws",
405
+ "summary": "TWS values available in a polar",
406
+ "description": "Returns the array of true wind speed values (m/s) for which the polar has tabulated data.",
407
+ "parameters": [
408
+ { "$ref": "#/components/parameters/polarId" }
409
+ ],
410
+ "responses": {
411
+ "200": {
412
+ "description": "TWS values in m/s",
413
+ "content": {
414
+ "application/json": {
415
+ "schema": { "type": "array", "items": { "type": "number" } }
416
+ }
417
+ }
418
+ },
419
+ "404": {
420
+ "description": "Polar not found",
421
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
422
+ }
423
+ }
424
+ }
425
+ },
426
+
427
+ "/polars/{id}/queries/curve": {
428
+ "get": {
429
+ "tags": ["polar-query"],
430
+ "operationId": "getPolarCurve",
431
+ "summary": "Interpolated polar curve at a given TWS",
432
+ "description": "Returns a sampled polar curve at the requested TWS, including beat and run target markers.",
433
+ "parameters": [
434
+ { "$ref": "#/components/parameters/polarId" },
435
+ {
436
+ "name": "tws",
437
+ "in": "query",
438
+ "required": true,
439
+ "description": "True wind speed in m/s",
440
+ "schema": { "type": "number", "minimum": 0 }
441
+ },
442
+ {
443
+ "name": "step",
444
+ "in": "query",
445
+ "required": false,
446
+ "description": "Angular sampling step in radians (default ~0.035 rad = 2 deg)",
447
+ "schema": { "type": "number", "minimum": 0 }
448
+ }
449
+ ],
450
+ "responses": {
451
+ "200": {
452
+ "description": "Polar curve data",
453
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PolarCurveResult" } } }
454
+ },
455
+ "400": {
456
+ "description": "Missing or invalid query parameters",
457
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
458
+ },
459
+ "404": {
460
+ "description": "Polar not found",
461
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
462
+ }
463
+ }
464
+ }
465
+ },
466
+
467
+ "/polars/{id}/queries/speed": {
468
+ "get": {
469
+ "tags": ["polar-query"],
470
+ "operationId": "getPolarSpeed",
471
+ "summary": "Interpolated boat speed at a TWS/TWA point",
472
+ "description": "Returns the polar boat speed at the given true wind speed and angle, plus interpolation quality indicators.",
473
+ "parameters": [
474
+ { "$ref": "#/components/parameters/polarId" },
475
+ {
476
+ "name": "tws",
477
+ "in": "query",
478
+ "required": true,
479
+ "description": "True wind speed in m/s",
480
+ "schema": { "type": "number", "minimum": 0 }
481
+ },
482
+ {
483
+ "name": "twa",
484
+ "in": "query",
485
+ "required": true,
486
+ "description": "True wind angle in radians (positive, 0 to pi)",
487
+ "schema": { "type": "number", "minimum": 0 }
488
+ }
489
+ ],
490
+ "responses": {
491
+ "200": {
492
+ "description": "Interpolated speed result",
493
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PolarSpeedResult" } } }
494
+ },
495
+ "400": {
496
+ "description": "Missing or invalid query parameters",
497
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
498
+ },
499
+ "404": {
500
+ "description": "Polar not found",
501
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
502
+ }
503
+ }
504
+ }
505
+ },
506
+
507
+ "/polars/{id}/queries/targets": {
508
+ "get": {
509
+ "tags": ["polar-query"],
510
+ "operationId": "getPolarTargets",
511
+ "summary": "Optimal beat and run targets at a given TWS",
512
+ "description": "Returns the optimal beat and run angles, boat speeds, and VMG values at the given true wind speed.",
513
+ "parameters": [
514
+ { "$ref": "#/components/parameters/polarId" },
515
+ {
516
+ "name": "tws",
517
+ "in": "query",
518
+ "required": true,
519
+ "description": "True wind speed in m/s",
520
+ "schema": { "type": "number", "minimum": 0 }
521
+ }
522
+ ],
523
+ "responses": {
524
+ "200": {
525
+ "description": "Beat and run targets",
526
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PolarTargetsResult" } } }
527
+ },
528
+ "400": {
529
+ "description": "Missing or invalid query parameters",
530
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
531
+ },
532
+ "404": {
533
+ "description": "Polar not found",
534
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
535
+ }
536
+ }
537
+ }
538
+ },
539
+
540
+ "/polars/{id}/queries/performance": {
541
+ "get": {
542
+ "tags": ["polar-query"],
543
+ "operationId": "getPolarPerformance",
544
+ "summary": "Performance ratio against the polar",
545
+ "description": "Given actual TWS, TWA, and BSP, returns the polar target speed and performance ratio, plus VMG performance. VMG is upwind when TWA < 60 deg (1.047 rad), downwind when TWA > 120 deg (2.094 rad), and null for reaching in between.",
546
+ "parameters": [
547
+ { "$ref": "#/components/parameters/polarId" },
548
+ {
549
+ "name": "tws",
550
+ "in": "query",
551
+ "required": true,
552
+ "description": "True wind speed in m/s",
553
+ "schema": { "type": "number", "minimum": 0 }
554
+ },
555
+ {
556
+ "name": "twa",
557
+ "in": "query",
558
+ "required": true,
559
+ "description": "True wind angle in radians (positive, 0 to pi)",
560
+ "schema": { "type": "number", "minimum": 0 }
561
+ },
562
+ {
563
+ "name": "bsp",
564
+ "in": "query",
565
+ "required": true,
566
+ "description": "Actual boat speed in m/s",
567
+ "schema": { "type": "number", "minimum": 0 }
568
+ }
569
+ ],
570
+ "responses": {
571
+ "200": {
572
+ "description": "Performance result",
573
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PolarPerformanceResult" } } }
574
+ },
575
+ "400": {
576
+ "description": "Missing or invalid query parameters",
577
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
578
+ },
579
+ "404": {
580
+ "description": "Polar not found",
581
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
582
+ }
583
+ }
584
+ }
585
+ }
586
+
587
+ },
588
+
589
+ "components": {
590
+ "parameters": {
591
+ "polarId": {
592
+ "name": "id",
593
+ "in": "path",
594
+ "required": true,
595
+ "description": "Polar identifier",
596
+ "schema": { "type": "string" }
597
+ }
598
+ },
599
+ "schemas": {
600
+
601
+ "Error": {
602
+ "type": "object",
603
+ "required": ["error"],
604
+ "properties": {
605
+ "error": { "type": "string" }
606
+ }
607
+ },
608
+
609
+ "IdResponse": {
610
+ "type": "object",
611
+ "required": ["id"],
612
+ "properties": {
613
+ "id": { "type": "string" }
614
+ }
615
+ },
616
+
617
+ "ImportFormatDescriptor": {
618
+ "type": "object",
619
+ "required": ["id", "name"],
620
+ "properties": {
621
+ "id": { "type": "string", "description": "Stable text format identifier used in POST /imports/text/{format}." },
622
+ "name": { "type": "string", "description": "Human-readable format name." },
623
+ "description": { "type": "string", "description": "Short description of the source text format." }
624
+ }
625
+ },
626
+
627
+ "ImportSourceDescriptor": {
628
+ "type": "object",
629
+ "required": ["id", "name"],
630
+ "properties": {
631
+ "id": { "type": "string", "description": "Stable source identifier used in /imports/sources/{source}/... endpoints." },
632
+ "name": { "type": "string", "description": "Human-readable source name." },
633
+ "description": { "type": "string", "description": "Short description of the external source." },
634
+ "url": { "type": "string", "description": "Optional source home page or API URL." },
635
+ "available": { "type": "boolean", "description": "Whether the external source is currently reachable for live imports." },
636
+ "availabilityMessage": { "type": "string", "description": "Human-readable reason when the source is currently unavailable." }
637
+ }
638
+ },
639
+
640
+ "ExternalPolarSummary": {
641
+ "type": "object",
642
+ "required": ["externalId"],
643
+ "properties": {
644
+ "externalId": { "type": "string", "description": "Source-specific polar identifier used when importing a result. For the `orc` source, this is the certificate RefNo." },
645
+ "name": { "type": "string", "description": "Human-readable boat or polar name." },
646
+ "sailnumber": { "type": "string" },
647
+ "boatType": { "type": "string" },
648
+ "year": { "type": "integer", "description": "Certificate or measurement year if known." },
649
+ "source": { "type": "string", "description": "Source identifier that produced this result." },
650
+ "countryId": { "type": "string", "description": "Source-specific country identifier if available." },
651
+ "certificateName": { "type": "string", "description": "Certificate variant label if available." },
652
+ "familyName": { "type": "string", "description": "Source-specific certificate family label if available." }
653
+ }
654
+ },
655
+
656
+ "TextPolarImportRequest": {
657
+ "type": "object",
658
+ "required": ["content"],
659
+ "properties": {
660
+ "content": { "type": "string", "description": "Raw source text to parse and import." },
661
+ "name": { "type": "string", "description": "Optional human-readable label override." },
662
+ "sailnumber": { "type": "string" },
663
+ "boatType": { "type": "string" },
664
+ "year": { "type": "integer", "description": "Certificate or measurement year override." },
665
+ "source": { "type": "string", "description": "Source label override stored in the canonical metadata." },
666
+ "notes": { "type": "string", "description": "Optional notes stored in the canonical metadata." }
667
+ }
668
+ },
669
+
670
+ "PolarSummary": {
671
+ "type": "object",
672
+ "required": ["id"],
673
+ "properties": {
674
+ "id": { "type": "string", "description": "Opaque stable identifier for this polar" },
675
+ "name": { "type": "string", "description": "Human-readable label" },
676
+ "sailnumber": { "type": "string" },
677
+ "boatType": { "type": "string" },
678
+ "year": { "type": "integer", "description": "Certificate or measurement year" },
679
+ "source": { "type": "string", "description": "Data origin, e.g. orc, irc, measured, custom" }
680
+ }
681
+ },
682
+
683
+ "PolarMeta": {
684
+ "allOf": [
685
+ { "$ref": "#/components/schemas/PolarSummary" },
686
+ {
687
+ "type": "object",
688
+ "properties": {
689
+ "notes": { "type": "string" },
690
+ "twsMin": { "type": "number", "description": "Lowest available TWS in the table (m/s)" },
691
+ "twsMax": { "type": "number", "description": "Highest available TWS in the table (m/s)" }
692
+ }
693
+ }
694
+ ]
695
+ },
696
+
697
+ "PolarUnits": {
698
+ "type": "object",
699
+ "required": ["tws", "twa", "boatSpeed"],
700
+ "properties": {
701
+ "tws": { "type": "string", "enum": ["m/s"] },
702
+ "twa": { "type": "string", "enum": ["rad"] },
703
+ "boatSpeed": { "type": "string", "enum": ["m/s"] }
704
+ }
705
+ },
706
+
707
+ "PolarSymmetry": {
708
+ "type": "object",
709
+ "required": ["portStarboardSymmetric"],
710
+ "properties": {
711
+ "portStarboardSymmetric": {
712
+ "type": "boolean",
713
+ "description": "Whether the polar assumes the same performance on port and starboard tack."
714
+ }
715
+ }
716
+ },
717
+
718
+ "PolarAxes": {
719
+ "type": "object",
720
+ "required": ["tws", "twa"],
721
+ "properties": {
722
+ "tws": {
723
+ "type": "array",
724
+ "description": "Sorted true wind speed axis in m/s.",
725
+ "items": { "type": "number" }
726
+ },
727
+ "twa": {
728
+ "type": "array",
729
+ "description": "Sorted true wind angle axis in radians, expressed over 0 to pi.",
730
+ "items": { "type": "number" }
731
+ }
732
+ }
733
+ },
734
+
735
+ "PolarValues": {
736
+ "type": "object",
737
+ "required": ["boatSpeedMatrix"],
738
+ "properties": {
739
+ "boatSpeedMatrix": {
740
+ "type": "array",
741
+ "description": "Boat speed matrix in m/s indexed by TWS row then TWA column.",
742
+ "items": {
743
+ "type": "array",
744
+ "items": { "type": "number" }
745
+ }
746
+ }
747
+ }
748
+ },
749
+
750
+ "PolarDerivedRow": {
751
+ "type": "object",
752
+ "required": ["tws"],
753
+ "properties": {
754
+ "tws": { "type": "number", "description": "True wind speed in m/s for the derived row." },
755
+ "beat": { "nullable": true, "$ref": "#/components/schemas/PolarTargetPoint" },
756
+ "run": { "nullable": true, "$ref": "#/components/schemas/PolarTargetPoint" },
757
+ "maxSpeed": { "type": "number", "description": "Maximum boat speed in m/s at this TWS." },
758
+ "maxSpeedAngle": { "type": "number", "description": "True wind angle in radians at which maxSpeed occurs." }
759
+ }
760
+ },
761
+
762
+ "PolarDerived": {
763
+ "type": "object",
764
+ "properties": {
765
+ "rows": {
766
+ "type": "array",
767
+ "items": { "$ref": "#/components/schemas/PolarDerivedRow" }
768
+ }
769
+ }
770
+ },
771
+
772
+ "PolarResource": {
773
+ "allOf": [
774
+ { "$ref": "#/components/schemas/PolarMeta" },
775
+ {
776
+ "type": "object",
777
+ "required": ["id", "kind", "schemaVersion", "units", "symmetry", "axes", "values"],
778
+ "properties": {
779
+ "kind": {
780
+ "type": "string",
781
+ "enum": ["polarTable"],
782
+ "description": "Canonical resource type identifier."
783
+ },
784
+ "schemaVersion": {
785
+ "type": "string",
786
+ "description": "Version of the canonical PolarTable resource schema."
787
+ },
788
+ "units": { "$ref": "#/components/schemas/PolarUnits" },
789
+ "symmetry": { "$ref": "#/components/schemas/PolarSymmetry" },
790
+ "axes": { "$ref": "#/components/schemas/PolarAxes" },
791
+ "values": { "$ref": "#/components/schemas/PolarValues" },
792
+ "derived": { "$ref": "#/components/schemas/PolarDerived" }
793
+ }
794
+ }
795
+ ]
796
+ },
797
+
798
+ "PolarResourceBody": {
799
+ "type": "object",
800
+ "required": ["kind", "schemaVersion", "units", "symmetry", "axes", "values"],
801
+ "properties": {
802
+ "name": { "type": "string", "description": "Human-readable label" },
803
+ "sailnumber": { "type": "string" },
804
+ "boatType": { "type": "string" },
805
+ "year": { "type": "integer", "description": "Certificate or measurement year" },
806
+ "source": { "type": "string", "description": "Data origin, e.g. orc, irc, measured, custom" },
807
+ "notes": { "type": "string" },
808
+ "kind": {
809
+ "type": "string",
810
+ "enum": ["polarTable"],
811
+ "description": "Canonical resource type identifier."
812
+ },
813
+ "schemaVersion": {
814
+ "type": "string",
815
+ "description": "Version of the canonical PolarTable resource schema."
816
+ },
817
+ "units": { "$ref": "#/components/schemas/PolarUnits" },
818
+ "symmetry": { "$ref": "#/components/schemas/PolarSymmetry" },
819
+ "axes": { "$ref": "#/components/schemas/PolarAxes" },
820
+ "values": { "$ref": "#/components/schemas/PolarValues" },
821
+ "derived": { "$ref": "#/components/schemas/PolarDerived" }
822
+ }
823
+ },
824
+
825
+ "PolarTargetPoint": {
826
+ "type": "object",
827
+ "description": "Optimal sailing target (beat or run).",
828
+ "properties": {
829
+ "twa": { "type": "number", "description": "Optimal true wind angle (rad)" },
830
+ "tbs": { "type": "number", "description": "Boat speed at optimal angle (m/s)" },
831
+ "vmg": { "type": "number", "description": "Velocity made good at optimal angle (m/s)" }
832
+ }
833
+ },
834
+
835
+ "PolarCurveResult": {
836
+ "type": "object",
837
+ "properties": {
838
+ "tws": { "type": "number", "description": "Requested TWS (m/s)" },
839
+ "points": {
840
+ "type": "array",
841
+ "description": "Sampled points from 0 to pi",
842
+ "items": {
843
+ "type": "object",
844
+ "properties": {
845
+ "twa": { "type": "number", "description": "True wind angle (rad)" },
846
+ "tbs": { "type": "number", "description": "Boat speed (m/s)" }
847
+ }
848
+ }
849
+ },
850
+ "beat": { "nullable": true, "$ref": "#/components/schemas/PolarTargetPoint" },
851
+ "run": { "nullable": true, "$ref": "#/components/schemas/PolarTargetPoint" }
852
+ }
853
+ },
854
+
855
+ "PolarSpeedResult": {
856
+ "type": "object",
857
+ "properties": {
858
+ "tws": { "type": "number", "description": "True wind speed (m/s)" },
859
+ "twa": { "type": "number", "description": "True wind angle (rad)" },
860
+ "tbs": { "type": "number", "nullable": true, "description": "Interpolated boat speed (m/s), null if outside polar range" },
861
+ "state": {
862
+ "type": "object",
863
+ "description": "Interpolation quality indicators.",
864
+ "properties": {
865
+ "tws": { "type": "string", "enum": ["below_range", "in_range", "above_range"] },
866
+ "twa": { "type": "string", "enum": ["in_irons", "pinching", "in_range", "extrapolated", "above_range"] }
867
+ }
868
+ }
869
+ }
870
+ },
871
+
872
+ "PolarTargetsResult": {
873
+ "type": "object",
874
+ "properties": {
875
+ "tws": { "type": "number", "description": "True wind speed (m/s)" },
876
+ "beat": { "nullable": true, "$ref": "#/components/schemas/PolarTargetPoint" },
877
+ "run": { "nullable": true, "$ref": "#/components/schemas/PolarTargetPoint" }
878
+ }
879
+ },
880
+
881
+ "PerformanceMetric": {
882
+ "type": "object",
883
+ "properties": {
884
+ "polar": { "type": "number", "description": "Polar reference value (m/s)" },
885
+ "actual": { "type": "number", "description": "Actual measured value (m/s)" },
886
+ "ratio": { "type": "number", "description": "actual / polar" }
887
+ }
888
+ },
889
+
890
+ "PolarPerformanceResult": {
891
+ "type": "object",
892
+ "properties": {
893
+ "tws": { "type": "number", "description": "True wind speed (m/s)" },
894
+ "twa": { "type": "number", "description": "True wind angle (rad)" },
895
+ "bsp": { "type": "number", "description": "Actual boat speed (m/s)" },
896
+ "direction": {
897
+ "type": "string",
898
+ "enum": ["upwind", "reaching", "downwind"],
899
+ "description": "Point of sail. upwind: TWA < 60 deg (1.047 rad); downwind: TWA > 120 deg (2.094 rad); reaching: between."
900
+ },
901
+ "speed": { "$ref": "#/components/schemas/PerformanceMetric" },
902
+ "vmg": {
903
+ "nullable": true,
904
+ "$ref": "#/components/schemas/PerformanceMetric",
905
+ "description": "null when direction is reaching"
906
+ }
907
+ }
908
+ }
909
+
910
+ }
911
+ }
912
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-polar-performance-plugin",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Calculates live sailing performance from polar data, with built-in polar management and import tools.",
5
5
  "main": "plugin/index.js",
6
6
  "exports": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "icon.png",
11
+ "openApi.json",
11
12
  "plugin/",
12
13
  "public/",
13
14
  "test/",