@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,173 @@
1
+ /**
2
+ * Plotter-extension integration (work doc #8): exposes the passage
3
+ * briefing as a Plotter Extensions API v1 extension so a fresh brief
4
+ * shows up as a tile on the chart plotter. Follows the proven
5
+ * in-family pattern from signalk-dead-reckoning's `plotterext.js`.
6
+ *
7
+ * Two server-side responsibilities:
8
+ *
9
+ * 1. A read-only `plotterExtensions` resource provider whose single
10
+ * entry is the manifest — presence is the enablement signal, and
11
+ * the provider goes empty on plugin stop so hosts tear the
12
+ * contexts down.
13
+ * 2. The extension's iframe assets served at a publicly readable,
14
+ * non-admin-gated prefix (`/plugins/*` is admin-only and `express`
15
+ * is not resolvable from a plugin's own tree, hence the minimal
16
+ * static handler).
17
+ *
18
+ * @file brief-ext.js
19
+ */
20
+
21
+ const path = require("node:path");
22
+ const fs = require("node:fs");
23
+ const pkg = require("../package.json");
24
+
25
+ const PUBLIC_DIR = path.join(__dirname, "..", "public");
26
+
27
+ /** Content types for the extension's asset kinds (inert UI code). */
28
+ const MIME = {
29
+ ".html": "text/html; charset=utf-8",
30
+ ".js": "text/javascript; charset=utf-8",
31
+ ".mjs": "text/javascript; charset=utf-8",
32
+ ".css": "text/css; charset=utf-8",
33
+ ".png": "image/png",
34
+ ".svg": "image/svg+xml",
35
+ ".ico": "image/x-icon",
36
+ ".json": "application/json; charset=utf-8",
37
+ ".txt": "text/plain; charset=utf-8",
38
+ };
39
+
40
+ /**
41
+ * Builds the extension manifest (Plotter Extensions API v1). The
42
+ * `panels.iframe` capability is declared optional pending the v1
43
+ * widget→host open-panel settlement; without it the tile falls back
44
+ * to opening the brief webapp URL in a new browser context.
45
+ *
46
+ * @param {string} assetBase - server-relative URL prefix for assets
47
+ * @param {string} version - plugin version for display metadata
48
+ * @returns {object}
49
+ */
50
+ function buildManifest(assetBase, version) {
51
+ return {
52
+ name: "Passage Briefing",
53
+ description:
54
+ "Passage brief tile: freshness of the latest compiled brief at a glance, one tap to the full briefing.",
55
+ version,
56
+ apiVersion: "1",
57
+ requires: ["widgets", "signalk.stream"],
58
+ optional: ["nightMode", "panels.iframe", "signalk.put"],
59
+ widgets: [
60
+ {
61
+ id: "passage-brief-tile",
62
+ title: "Passage Brief",
63
+ type: "iframe",
64
+ // ?v= cache-busts the host's iframe when the plugin updates
65
+ url: `${assetBase}/brief-ext-widget.html?v=${version}`,
66
+ size: "1x1",
67
+ lifecycle: "whileEnabled",
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Minimal static-asset middleware: only regular files directly under
75
+ * `root` are served (traversal-guarded; anything else falls through).
76
+ *
77
+ * @param {string} root - absolute directory to serve
78
+ * @param {string} assetBase - mount prefix, stripped when present
79
+ * @returns {(req: object, res: object, next: Function) => void}
80
+ */
81
+ function staticAssetHandler(root, assetBase) {
82
+ const rootPrefix = root.endsWith(path.sep) ? root : root + path.sep;
83
+ return (req, res, next) => {
84
+ let urlPath = req.path ?? req.url ?? "/";
85
+ const q = urlPath.indexOf("?");
86
+ if (q >= 0) urlPath = urlPath.slice(0, q);
87
+ if (urlPath.startsWith(assetBase)) {
88
+ urlPath = urlPath.slice(assetBase.length);
89
+ }
90
+ if (!urlPath.startsWith("/")) urlPath = `/${urlPath}`;
91
+
92
+ let resolved;
93
+ try {
94
+ resolved = path.normalize(
95
+ path.join(rootPrefix, decodeURIComponent(urlPath)),
96
+ );
97
+ } catch {
98
+ next(); // malformed percent-encoding
99
+ return;
100
+ }
101
+ if (!resolved.startsWith(rootPrefix)) {
102
+ next(); // traversal attempt
103
+ return;
104
+ }
105
+
106
+ fs.stat(resolved, (err, st) => {
107
+ if (err || !st.isFile()) {
108
+ next();
109
+ return;
110
+ }
111
+ res.statusCode = 200;
112
+ res.setHeader(
113
+ "Content-Type",
114
+ MIME[path.extname(resolved)] ?? "application/octet-stream",
115
+ );
116
+ res.setHeader("Cache-Control", "public, max-age=3600");
117
+ fs.readFile(resolved, (readErr, data) => {
118
+ if (readErr) {
119
+ next();
120
+ return;
121
+ }
122
+ res.end(data);
123
+ });
124
+ });
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Registers the plotter-extension provider and mounts the asset
130
+ * route.
131
+ *
132
+ * @param {import("@signalk/server-api").ServerAPI} app
133
+ * @param {{id: string, version?: string}} opts
134
+ * @returns {() => void} teardown — empties the provider listing
135
+ */
136
+ function registerPlotterExtension(app, opts) {
137
+ const { id } = opts;
138
+ const version = opts.version ?? pkg.version;
139
+ const assetBase = `/plotterext/${id}`;
140
+ const manifest = buildManifest(assetBase, version);
141
+ let running = true;
142
+
143
+ app.registerResourceProvider({
144
+ type: "plotterExtensions",
145
+ methods: {
146
+ listResources: async () => (running ? { [id]: manifest } : {}),
147
+ getResource: async (resourceId) => {
148
+ if (!running || resourceId !== id) {
149
+ throw new Error(`No such plotterExtensions resource: ${resourceId}`);
150
+ }
151
+ return manifest;
152
+ },
153
+ setResource: async () => {
154
+ throw new Error(`${id} is a read-only provider`);
155
+ },
156
+ deleteResource: async () => {
157
+ throw new Error(`${id} is a read-only provider`);
158
+ },
159
+ },
160
+ });
161
+
162
+ app.use(assetBase, staticAssetHandler(PUBLIC_DIR, assetBase));
163
+
164
+ return () => {
165
+ running = false;
166
+ };
167
+ }
168
+
169
+ module.exports = {
170
+ buildManifest,
171
+ registerPlotterExtension,
172
+ staticAssetHandler,
173
+ };