@meri-imperiumi/signalk-passage-briefing 0.2.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.
Files changed (86) hide show
  1. package/.editorconfig +5 -0
  2. package/.github/workflows/publish.yml +34 -0
  3. package/.github/workflows/signalk-ci.yml +11 -0
  4. package/.github/workflows/test.yml +21 -0
  5. package/CHANGELOG.md +423 -0
  6. package/README.md +86 -0
  7. package/SPEC.md +488 -0
  8. package/bin/backfill-report.js +218 -0
  9. package/bin/backtest-cli.js +132 -0
  10. package/biome.json +6 -0
  11. package/doc/here-brief.png +0 -0
  12. package/package.json +48 -0
  13. package/plugin/backtest.js +742 -0
  14. package/plugin/brief-ext.js +173 -0
  15. package/plugin/bulletin-engine.js +746 -0
  16. package/plugin/bulletin-source.js +150 -0
  17. package/plugin/celestial-source.js +417 -0
  18. package/plugin/fetch-engine.js +717 -0
  19. package/plugin/history-backfill.js +540 -0
  20. package/plugin/index.js +1479 -0
  21. package/plugin/logbook-source.js +463 -0
  22. package/plugin/notes-publisher.js +228 -0
  23. package/plugin/notes-store.js +241 -0
  24. package/plugin/raster-convert.js +168 -0
  25. package/plugin/sails-configuration.js +111 -0
  26. package/plugin/spool-watcher.js +218 -0
  27. package/plugin/sqlite-db.js +378 -0
  28. package/plugin/state-machine.js +249 -0
  29. package/plugin/statustilesexamples.js +101 -0
  30. package/plugin/synoptic-map.json +45 -0
  31. package/plugin/synoptic-source.js +227 -0
  32. package/plugin/zone-source.js +339 -0
  33. package/public/app.js +18 -0
  34. package/public/brief-ext-model.js +64 -0
  35. package/public/brief-ext-widget.html +14 -0
  36. package/public/brief-ext-widget.js +294 -0
  37. package/public/components/backfill-controls.js +100 -0
  38. package/public/components/comfort-info.js +94 -0
  39. package/public/components/conditions-here.js +234 -0
  40. package/public/components/horizon-sparkline.js +77 -0
  41. package/public/components/models.mjs +372 -0
  42. package/public/components/passage-outlook.js +368 -0
  43. package/public/components/sk-api.js +146 -0
  44. package/public/components/sk-base-css.js +177 -0
  45. package/public/components/strategic-outlook.js +248 -0
  46. package/public/components/synoptic-chart.js +48 -0
  47. package/public/components/tactical-dashboard.js +172 -0
  48. package/public/css/visuals.css +292 -0
  49. package/public/gmdss-zones-min.json +287 -0
  50. package/public/icon.png +0 -0
  51. package/public/index.html +13 -0
  52. package/public/polar.mjs +303 -0
  53. package/public/route-sim.mjs +750 -0
  54. package/public/sereno-physics.mjs +592 -0
  55. package/public/tack-gybe.js +174 -0
  56. package/public/vendor/plotterext-bus/LICENSE +21 -0
  57. package/public/vendor/plotterext-bus/README.md +17 -0
  58. package/public/vendor/plotterext-bus/chunk-4W6N34SD.js +333 -0
  59. package/public/vendor/plotterext-bus/chunk-7XRFPDQL.js +263 -0
  60. package/public/vendor/plotterext-bus/chunk-RED55KML.js +117 -0
  61. package/public/vendor/plotterext-bus/extension.js +29 -0
  62. package/public/vendor/plotterext-bus/host.js +28 -0
  63. package/public/vendor/utif/LICENSE +21 -0
  64. package/public/vendor/utif/UTIF.js +1171 -0
  65. package/public/worker.js +35 -0
  66. package/status-tiles-examples.json +55 -0
  67. package/tests/backtest.test.js +461 -0
  68. package/tests/brief-ext.test.js +194 -0
  69. package/tests/bulletin-engine.test.js +361 -0
  70. package/tests/celestial-source.test.js +265 -0
  71. package/tests/fetch-engine.test.js +297 -0
  72. package/tests/history-backfill.test.js +341 -0
  73. package/tests/logbook-source.test.js +324 -0
  74. package/tests/notes-publisher.test.js +161 -0
  75. package/tests/notes-store.test.js +138 -0
  76. package/tests/openmeteo-mock.js +100 -0
  77. package/tests/plugin.test.js +1344 -0
  78. package/tests/route-sim.test.js +533 -0
  79. package/tests/sereno-physics.test.js +333 -0
  80. package/tests/sqlite-db.test.js +164 -0
  81. package/tests/state-machine.test.js +182 -0
  82. package/tests/statustilesexamples.test.js +86 -0
  83. package/tests/synoptic-source.test.js +195 -0
  84. package/tests/tack-gybe.test.js +211 -0
  85. package/tests/webapp.test.js +257 -0
  86. package/tests/zone-source.test.js +226 -0
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backtest CLI (SPEC §7.1): replays the vessel's history through the
4
+ * Sereno motion model, tunes (k_heel, k_pitch) with Nelder-Mead and
5
+ * prints the comfort confusion matrix.
6
+ *
7
+ * Usage:
8
+ * node bin/backtest-cli.js \
9
+ * --history-url http://localhost:3000 \
10
+ * --start 2026-08-01T00:00:00Z \
11
+ * --end 2026-08-03T12:00:00Z \
12
+ * --output ./backtest-report.json
13
+ *
14
+ * Options beyond the SPEC example:
15
+ * --resolution <sec> History bucket size (default 10)
16
+ * --window-minutes <m> Sliding window span (default 15)
17
+ * --chunk-hours <h> History query chunk (default 24)
18
+ * --provider <id> History provider ID
19
+ * --k-heel / --k-pitch Starting guesses (default plugin values)
20
+ * --waterline <m> Waterline length override
21
+ *
22
+ * The auth token is read from SIGNALK_TOKEN. The JSON report is
23
+ * written to --output (default ./backtest-report.json).
24
+ *
25
+ * @file backtest-cli.js
26
+ */
27
+
28
+ const { writeFile } = require("node:fs/promises");
29
+ const { runBacktest } = require("../plugin/backtest.js");
30
+
31
+ /**
32
+ * Parses the command line into options.
33
+ *
34
+ * @param {string[]} argv - process.argv.slice(2)
35
+ * @returns {object} Parsed flags
36
+ */
37
+ function parseArgs(argv) {
38
+ const args = {};
39
+ for (let i = 0; i < argv.length; i++) {
40
+ if (!argv[i].startsWith("--")) {
41
+ continue;
42
+ }
43
+ const key = argv[i].slice(2);
44
+ const next = argv[i + 1];
45
+ if (next === undefined || next.startsWith("--")) {
46
+ args[key] = true;
47
+ } else {
48
+ args[key] = next;
49
+ i++;
50
+ }
51
+ }
52
+ return args;
53
+ }
54
+
55
+ /**
56
+ * Renders the confusion matrix as an ASCII table.
57
+ *
58
+ * @param {{tiers: string[], matrix: number[][]}} confusion
59
+ * @returns {string}
60
+ */
61
+ function renderMatrix(confusion) {
62
+ const { tiers, matrix } = confusion;
63
+ if (tiers.length === 0) {
64
+ return "No windows evaluated — nothing to classify.";
65
+ }
66
+ const header =
67
+ "pred\\meas ".padEnd(11) + tiers.map((t) => t.padStart(8)).join("");
68
+ const rows = matrix.map(
69
+ (row, i) =>
70
+ tiers[i].padEnd(10) + row.map((n) => String(n).padStart(8)).join(""),
71
+ );
72
+ return [header, ...rows].join("\n");
73
+ }
74
+
75
+ async function main() {
76
+ const args = parseArgs(process.argv.slice(2));
77
+ const baseUrl = args["history-url"] ?? args.url;
78
+ const start = args.start;
79
+ const end = args.end;
80
+ if (!baseUrl || !start || !end) {
81
+ console.error(
82
+ "Usage: node bin/backtest-cli.js --history-url <server> " +
83
+ "--start <ISO> --end <ISO> [--output <file>]",
84
+ );
85
+ process.exit(1);
86
+ }
87
+
88
+ console.error(`Backtesting ${start} → ${end} against ${baseUrl} …`);
89
+ const report = await runBacktest({
90
+ baseUrl,
91
+ from: new Date(start),
92
+ to: new Date(end),
93
+ options: {
94
+ resolution: args.resolution ? Number(args.resolution) : undefined,
95
+ windowMinutes: args["window-minutes"]
96
+ ? Number(args["window-minutes"])
97
+ : undefined,
98
+ chunkHours: args["chunk-hours"] ? Number(args["chunk-hours"]) : undefined,
99
+ provider: args.provider,
100
+ waterline_length_m: args.waterline ? Number(args.waterline) : undefined,
101
+ kHeel: args["k-heel"] ? Number(args["k-heel"]) : undefined,
102
+ kPitch: args["k-pitch"] ? Number(args["k-pitch"]) : undefined,
103
+ },
104
+ });
105
+
106
+ const output = args.output ?? "./backtest-report.json";
107
+ await writeFile(output, `${JSON.stringify(report, null, 2)}\n`);
108
+
109
+ console.error(
110
+ `${report.samples} samples → ${report.windowsEvaluated} windows evaluated`,
111
+ );
112
+ if (report.tuned.mae != null) {
113
+ console.error(
114
+ `k_heel ${report.start.kHeel} → ${report.tuned.kHeel}, ` +
115
+ `k_pitch ${report.start.kPitch} → ${report.tuned.kPitch}`,
116
+ );
117
+ console.error(
118
+ `MAE ${(report.start.mae ?? 0).toFixed(4)} → ${report.tuned.mae.toFixed(4)} m/s²`,
119
+ );
120
+ }
121
+ console.log(renderMatrix(report.confusionMatrix));
122
+ console.error(`Report written to ${output}`);
123
+ }
124
+
125
+ module.exports = { parseArgs, renderMatrix };
126
+
127
+ if (require.main === module) {
128
+ main().catch((error) => {
129
+ console.error(`Backtest failed: ${error.message}`);
130
+ process.exit(1);
131
+ });
132
+ }
package/biome.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
3
+ "files": {
4
+ "includes": ["plugin/**", "public/**", "tests/**", "bin/**", "!public/vendor/**"]
5
+ }
6
+ }
Binary file
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@meri-imperiumi/signalk-passage-briefing",
3
+ "version": "0.2.0",
4
+ "description": "Offshore passage daily briefing webapp for Signal K",
5
+ "main": "plugin/index.js",
6
+ "scripts": {
7
+ "format": "npx @biomejs/biome check --use-editorconfig=true --write plugin/ tests/ bin/ public/",
8
+ "lint": "npx @biomejs/biome check --use-editorconfig=true plugin/ public/",
9
+ "test": "node --test --test-force-exit \"tests/*.test.js\""
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git://github.com/meri-imperiumi/signalk-passage-briefing.git"
14
+ },
15
+ "keywords": [
16
+ "signalk-node-server-plugin",
17
+ "signalk-category-utility",
18
+ "signalk-webapp"
19
+ ],
20
+ "author": "Henri Bergius <henri.bergius@iki.fi>",
21
+ "license": "EUPL-1.2",
22
+ "bugs": {
23
+ "url": "https://github.com/meri-imperiumi/signalk-passage-briefing/issues"
24
+ },
25
+ "homepage": "https://github.com/meri-imperiumi/signalk-passage-briefing#readme",
26
+ "signalk-plugin-enabled-by-default": false,
27
+ "signalk": {
28
+ "displayName": "Passage Briefing",
29
+ "appIcon": "./icon.png",
30
+ "screenshots": [
31
+ "doc/here-brief.png"
32
+ ],
33
+ "requires": [
34
+ "@meri-imperiumi/signalk-energy-predictor",
35
+ "@meri-imperiumi/signalk-internet",
36
+ "signalk-polar-management"
37
+ ]
38
+ },
39
+ "engines": {
40
+ "node": ">=22.5.0"
41
+ },
42
+ "dependencies": {
43
+ "yaml": "^2.9.0"
44
+ },
45
+ "devDependencies": {
46
+ "@signalk/server-api": "^2.30.0"
47
+ }
48
+ }