@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,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
|
+
};
|