@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.
- package/.editorconfig +5 -0
- package/.github/workflows/publish.yml +34 -0
- package/.github/workflows/signalk-ci.yml +11 -0
- package/.github/workflows/test.yml +21 -0
- package/CHANGELOG.md +423 -0
- package/README.md +86 -0
- package/SPEC.md +488 -0
- package/bin/backfill-report.js +218 -0
- package/bin/backtest-cli.js +132 -0
- package/biome.json +6 -0
- package/doc/here-brief.png +0 -0
- package/package.json +48 -0
- package/plugin/backtest.js +742 -0
- package/plugin/brief-ext.js +173 -0
- package/plugin/bulletin-engine.js +746 -0
- package/plugin/bulletin-source.js +150 -0
- package/plugin/celestial-source.js +417 -0
- package/plugin/fetch-engine.js +717 -0
- package/plugin/history-backfill.js +540 -0
- package/plugin/index.js +1479 -0
- package/plugin/logbook-source.js +463 -0
- package/plugin/notes-publisher.js +228 -0
- package/plugin/notes-store.js +241 -0
- package/plugin/raster-convert.js +168 -0
- package/plugin/sails-configuration.js +111 -0
- package/plugin/spool-watcher.js +218 -0
- package/plugin/sqlite-db.js +378 -0
- package/plugin/state-machine.js +249 -0
- package/plugin/statustilesexamples.js +101 -0
- package/plugin/synoptic-map.json +45 -0
- package/plugin/synoptic-source.js +227 -0
- package/plugin/zone-source.js +339 -0
- package/public/app.js +18 -0
- package/public/brief-ext-model.js +64 -0
- package/public/brief-ext-widget.html +14 -0
- package/public/brief-ext-widget.js +294 -0
- package/public/components/backfill-controls.js +100 -0
- package/public/components/comfort-info.js +94 -0
- package/public/components/conditions-here.js +234 -0
- package/public/components/horizon-sparkline.js +77 -0
- package/public/components/models.mjs +372 -0
- package/public/components/passage-outlook.js +368 -0
- package/public/components/sk-api.js +146 -0
- package/public/components/sk-base-css.js +177 -0
- package/public/components/strategic-outlook.js +248 -0
- package/public/components/synoptic-chart.js +48 -0
- package/public/components/tactical-dashboard.js +172 -0
- package/public/css/visuals.css +292 -0
- package/public/gmdss-zones-min.json +287 -0
- package/public/icon.png +0 -0
- package/public/index.html +13 -0
- package/public/polar.mjs +303 -0
- package/public/route-sim.mjs +750 -0
- package/public/sereno-physics.mjs +592 -0
- package/public/tack-gybe.js +174 -0
- package/public/vendor/plotterext-bus/LICENSE +21 -0
- package/public/vendor/plotterext-bus/README.md +17 -0
- package/public/vendor/plotterext-bus/chunk-4W6N34SD.js +333 -0
- package/public/vendor/plotterext-bus/chunk-7XRFPDQL.js +263 -0
- package/public/vendor/plotterext-bus/chunk-RED55KML.js +117 -0
- package/public/vendor/plotterext-bus/extension.js +29 -0
- package/public/vendor/plotterext-bus/host.js +28 -0
- package/public/vendor/utif/LICENSE +21 -0
- package/public/vendor/utif/UTIF.js +1171 -0
- package/public/worker.js +35 -0
- package/status-tiles-examples.json +55 -0
- package/tests/backtest.test.js +461 -0
- package/tests/brief-ext.test.js +194 -0
- package/tests/bulletin-engine.test.js +361 -0
- package/tests/celestial-source.test.js +265 -0
- package/tests/fetch-engine.test.js +297 -0
- package/tests/history-backfill.test.js +341 -0
- package/tests/logbook-source.test.js +324 -0
- package/tests/notes-publisher.test.js +161 -0
- package/tests/notes-store.test.js +138 -0
- package/tests/openmeteo-mock.js +100 -0
- package/tests/plugin.test.js +1344 -0
- package/tests/route-sim.test.js +533 -0
- package/tests/sereno-physics.test.js +333 -0
- package/tests/sqlite-db.test.js +164 -0
- package/tests/state-machine.test.js +182 -0
- package/tests/statustilesexamples.test.js +86 -0
- package/tests/synoptic-source.test.js +195 -0
- package/tests/tack-gybe.test.js +211 -0
- package/tests/webapp.test.js +257 -0
- 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
|
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
|
+
}
|