@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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<strategic-outlook>` — Screen 2: whole-passage summary (SPEC
|
|
3
|
+
* §6.3). ETA percentile table, macro sea-state warnings, convective
|
|
4
|
+
* warnings and the raw METAREA bulletin console.
|
|
5
|
+
*
|
|
6
|
+
* @file components/strategic-outlook.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { etaTable, sailWorkTimeline, splitSevere } from "./models.mjs";
|
|
10
|
+
import { SK_BASE_CSS } from "./sk-base-css.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The custom element (browser only).
|
|
14
|
+
*/
|
|
15
|
+
class StrategicOutlook extends HTMLElement {
|
|
16
|
+
connectedCallback() {
|
|
17
|
+
if (!this.shadowRoot) {
|
|
18
|
+
this.attachShadow({ mode: "open" });
|
|
19
|
+
}
|
|
20
|
+
this.shadowRoot.innerHTML = `
|
|
21
|
+
<style>
|
|
22
|
+
${SK_BASE_CSS}
|
|
23
|
+
:host { display: block; }
|
|
24
|
+
.sk-card { margin-bottom: 12px; }
|
|
25
|
+
pre.console { margin: 0; }
|
|
26
|
+
.warn {
|
|
27
|
+
color: var(--color-orange);
|
|
28
|
+
font-family: var(--font-data, ui-monospace, monospace);
|
|
29
|
+
font-size: 0.85rem;
|
|
30
|
+
padding: 4px 0;
|
|
31
|
+
}
|
|
32
|
+
.warn.severe { color: var(--color-red); }
|
|
33
|
+
.none { color: var(--text-muted); font-size: 0.85rem; }
|
|
34
|
+
strong.sev {
|
|
35
|
+
color: var(--color-orange);
|
|
36
|
+
text-transform: uppercase;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
<section class="sk-card theme-teal">
|
|
40
|
+
<h2>ETA & Motor Plan</h2>
|
|
41
|
+
<table class="data">
|
|
42
|
+
<thead>
|
|
43
|
+
<tr><th>Percentile</th><th>Arrival (UTC)</th></tr>
|
|
44
|
+
</thead>
|
|
45
|
+
<tbody id="eta-body"></tbody>
|
|
46
|
+
</table>
|
|
47
|
+
<div class="grid-auto" style="margin-top: 8px">
|
|
48
|
+
<div><div class="label">Motor hours</div><div class="value-small" id="motor">—</div></div>
|
|
49
|
+
<div><div class="label">Fuel</div><div class="value-small" id="fuel">—</div></div>
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
<section class="sk-card theme-orange">
|
|
53
|
+
<h2>Macro Sea State</h2>
|
|
54
|
+
<div id="sea"><span class="none">No anomalies</span></div>
|
|
55
|
+
</section>
|
|
56
|
+
<section class="sk-card theme-red">
|
|
57
|
+
<h2>Convective Risk</h2>
|
|
58
|
+
<div id="conv"><span class="none">No warnings</span></div>
|
|
59
|
+
</section>
|
|
60
|
+
<section class="sk-card theme-orange">
|
|
61
|
+
<h2>Sail Work</h2>
|
|
62
|
+
<div id="sailwork"><span class="none">No sail changes planned</span></div>
|
|
63
|
+
</section>
|
|
64
|
+
<section class="sk-card" id="sky-card" hidden>
|
|
65
|
+
<h2>Sky Notes</h2>
|
|
66
|
+
<div id="sky"></div>
|
|
67
|
+
</section>
|
|
68
|
+
<section class="sk-card theme-teal">
|
|
69
|
+
<h2>METAREA Bulletin</h2>
|
|
70
|
+
<pre class="console" id="bulletin">No bulletin cached</pre>
|
|
71
|
+
</section>
|
|
72
|
+
<section class="sk-card theme-red" id="blocks-card" hidden>
|
|
73
|
+
<h2>Warnings On Your Waters</h2>
|
|
74
|
+
<div class="console" id="blocks"></div>
|
|
75
|
+
</section>
|
|
76
|
+
<synoptic-chart hidden></synoptic-chart>
|
|
77
|
+
`;
|
|
78
|
+
this._etaBody = this.shadowRoot.getElementById("eta-body");
|
|
79
|
+
this._motorEl = this.shadowRoot.getElementById("motor");
|
|
80
|
+
this._fuelEl = this.shadowRoot.getElementById("fuel");
|
|
81
|
+
this._seaEl = this.shadowRoot.getElementById("sea");
|
|
82
|
+
this._convEl = this.shadowRoot.getElementById("conv");
|
|
83
|
+
this._sailWorkEl = this.shadowRoot.getElementById("sailwork");
|
|
84
|
+
this._skyCard = this.shadowRoot.getElementById("sky-card");
|
|
85
|
+
this._skyEl = this.shadowRoot.getElementById("sky");
|
|
86
|
+
this._bulletinEl = this.shadowRoot.getElementById("bulletin");
|
|
87
|
+
if (this._exceptions) {
|
|
88
|
+
this.setExceptions(this._exceptions);
|
|
89
|
+
}
|
|
90
|
+
if (this._bulletin !== undefined) {
|
|
91
|
+
this.setBulletin(this._bulletin);
|
|
92
|
+
}
|
|
93
|
+
if (this._spaceEvents !== undefined) {
|
|
94
|
+
this.setSpaceEvents(this._spaceEvents);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sky notes (work doc #3): comets and any other non-urgent space
|
|
100
|
+
* events for the passage summary. Hidden when there are none.
|
|
101
|
+
*
|
|
102
|
+
* @param {Array<{description: string, tactical: boolean}>|null} events
|
|
103
|
+
*/
|
|
104
|
+
setSpaceEvents(events) {
|
|
105
|
+
this._spaceEvents = events;
|
|
106
|
+
if (!this._skyEl) {
|
|
107
|
+
return; // Not yet connected
|
|
108
|
+
}
|
|
109
|
+
const list = (events ?? []).filter((e) => !e.tactical);
|
|
110
|
+
this._skyCard.hidden = list.length === 0;
|
|
111
|
+
this._skyEl.innerHTML = "";
|
|
112
|
+
for (const e of list) {
|
|
113
|
+
const el = document.createElement("div");
|
|
114
|
+
el.textContent = `✦ ${e.description}`;
|
|
115
|
+
this._skyEl.appendChild(el);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Renders from the worker's exception view.
|
|
121
|
+
*
|
|
122
|
+
* @param {object|null} exceptions
|
|
123
|
+
*/
|
|
124
|
+
setExceptions(exceptions) {
|
|
125
|
+
this._exceptions = exceptions;
|
|
126
|
+
if (!this._etaBody) {
|
|
127
|
+
return; // Not yet connected
|
|
128
|
+
}
|
|
129
|
+
const table = etaTable(exceptions);
|
|
130
|
+
this._etaBody.innerHTML = "";
|
|
131
|
+
for (const row of table.rows) {
|
|
132
|
+
const tr = document.createElement("tr");
|
|
133
|
+
const tdLabel = document.createElement("td");
|
|
134
|
+
tdLabel.className = "muted";
|
|
135
|
+
tdLabel.textContent = row.label;
|
|
136
|
+
const tdStamp = document.createElement("td");
|
|
137
|
+
tdStamp.textContent = row.stamp || "—";
|
|
138
|
+
tr.append(tdLabel, tdStamp);
|
|
139
|
+
this._etaBody.appendChild(tr);
|
|
140
|
+
}
|
|
141
|
+
this._motorEl.textContent = table.motorHours || "—";
|
|
142
|
+
this._fuelEl.textContent = table.fuel || "—";
|
|
143
|
+
|
|
144
|
+
// Macro sea-state anomalies
|
|
145
|
+
this._seaEl.innerHTML = "";
|
|
146
|
+
const sea = exceptions?.passageSummary?.macroSeaAnomalies ?? [];
|
|
147
|
+
for (const a of sea) {
|
|
148
|
+
const el = document.createElement("div");
|
|
149
|
+
el.className = "warn";
|
|
150
|
+
el.textContent = `+${a.hoursFromNow}h steep sea (ratio ${a.steepnessRatio})`;
|
|
151
|
+
this._seaEl.appendChild(el);
|
|
152
|
+
}
|
|
153
|
+
if (sea.length === 0) {
|
|
154
|
+
const el = document.createElement("span");
|
|
155
|
+
el.className = "none";
|
|
156
|
+
el.textContent = "No anomalies";
|
|
157
|
+
this._seaEl.appendChild(el);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Sail work: recommendations plus the planned tacks/gybes (doc #5)
|
|
161
|
+
this._sailWorkEl.innerHTML = "";
|
|
162
|
+
const sailWork = sailWorkTimeline(exceptions);
|
|
163
|
+
for (const item of sailWork) {
|
|
164
|
+
const el = document.createElement("div");
|
|
165
|
+
el.className =
|
|
166
|
+
item.label.startsWith("Tack") || item.label.startsWith("Gybe")
|
|
167
|
+
? "warn"
|
|
168
|
+
: "";
|
|
169
|
+
el.textContent = `+${item.hoursFromNow}h ${item.stamp} · ${item.label}${item.detail ? ` · ${item.detail}` : ""}`;
|
|
170
|
+
this._sailWorkEl.appendChild(el);
|
|
171
|
+
}
|
|
172
|
+
if (sailWork.length === 0) {
|
|
173
|
+
const el = document.createElement("span");
|
|
174
|
+
el.className = "none";
|
|
175
|
+
el.textContent = "No sail changes planned";
|
|
176
|
+
this._sailWorkEl.appendChild(el);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Convective warnings
|
|
180
|
+
this._convEl.innerHTML = "";
|
|
181
|
+
const conv = exceptions?.passageSummary?.convectiveWarnings ?? [];
|
|
182
|
+
for (const a of conv) {
|
|
183
|
+
const el = document.createElement("div");
|
|
184
|
+
el.className = "warn severe";
|
|
185
|
+
const parts = [];
|
|
186
|
+
if (a.cape != null) {
|
|
187
|
+
parts.push(`CAPE ${a.cape}`);
|
|
188
|
+
}
|
|
189
|
+
if (a.kIndex != null) {
|
|
190
|
+
parts.push(`K ${a.kIndex}`);
|
|
191
|
+
}
|
|
192
|
+
el.textContent = `+${a.hoursFromNow}h convection (${parts.join(", ")})`;
|
|
193
|
+
this._convEl.appendChild(el);
|
|
194
|
+
}
|
|
195
|
+
if (conv.length === 0) {
|
|
196
|
+
const el = document.createElement("span");
|
|
197
|
+
el.className = "none";
|
|
198
|
+
el.textContent = "No warnings";
|
|
199
|
+
this._convEl.appendChild(el);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Raw METAREA bulletin (spool-sourced when available).
|
|
205
|
+
*
|
|
206
|
+
* @param {string|{bulletinText: string, blocks: Array<{text: string}>}|null}
|
|
207
|
+
* bulletin - The payload's metareaBulletin, or raw text
|
|
208
|
+
*/
|
|
209
|
+
setBulletin(bulletin) {
|
|
210
|
+
this._bulletin = bulletin;
|
|
211
|
+
if (!this._bulletinEl) {
|
|
212
|
+
return; // Not yet connected
|
|
213
|
+
}
|
|
214
|
+
const normalized =
|
|
215
|
+
typeof bulletin === "string" ? { bulletinText: bulletin } : bulletin;
|
|
216
|
+
this._bulletinEl.textContent =
|
|
217
|
+
normalized?.bulletinText || "No bulletin cached";
|
|
218
|
+
|
|
219
|
+
// Filtered blocks replace the raw wall of text (work doc #4 §5):
|
|
220
|
+
// only geographically relevant paragraphs, severe keywords lit
|
|
221
|
+
const blocksCard = this.shadowRoot.getElementById("blocks-card");
|
|
222
|
+
const blocksEl = this.shadowRoot.getElementById("blocks");
|
|
223
|
+
const blocks = normalized?.blocks ?? [];
|
|
224
|
+
blocksCard.hidden = blocks.length === 0;
|
|
225
|
+
blocksEl.innerHTML = "";
|
|
226
|
+
for (const block of blocks) {
|
|
227
|
+
const pre = document.createElement("div");
|
|
228
|
+
pre.style.marginBottom = "8px";
|
|
229
|
+
for (const token of splitSevere(block.text)) {
|
|
230
|
+
if (token.severe) {
|
|
231
|
+
const strong = document.createElement("strong");
|
|
232
|
+
strong.className = "sev";
|
|
233
|
+
strong.textContent = token.text;
|
|
234
|
+
pre.appendChild(strong);
|
|
235
|
+
} else {
|
|
236
|
+
pre.appendChild(document.createTextNode(token.text));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
blocksEl.appendChild(pre);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (typeof customElements !== "undefined") {
|
|
245
|
+
customElements.define("strategic-outlook", StrategicOutlook);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { StrategicOutlook };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The synoptic surface-analysis figure (work doc #11): loads the
|
|
3
|
+
* cached chart for the vessel's position zone from the plugin API
|
|
4
|
+
* and shows it inverted under the night palette. Stays hidden until
|
|
5
|
+
* the image actually loads, so zones without a chart render nothing
|
|
6
|
+
* — same graceful handling as the bulletin console. Shared by the
|
|
7
|
+
* strategic screen and the conditions-here view.
|
|
8
|
+
*
|
|
9
|
+
* @file components/synoptic-chart.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const PLUGIN_API = "/plugins/signalk-passage-briefing/api";
|
|
13
|
+
|
|
14
|
+
class SynopticChart extends HTMLElement {
|
|
15
|
+
connectedCallback() {
|
|
16
|
+
if (!this.shadowRoot) {
|
|
17
|
+
this.attachShadow({ mode: "open" });
|
|
18
|
+
}
|
|
19
|
+
this.shadowRoot.innerHTML = `
|
|
20
|
+
<style>
|
|
21
|
+
:host { display: block; }
|
|
22
|
+
:host([hidden]) { display: none; }
|
|
23
|
+
figure { margin: 0; }
|
|
24
|
+
img { width: 100%; height: auto; display: block; }
|
|
25
|
+
:host([data-mode="night"]) img {
|
|
26
|
+
filter: invert(0.93) hue-rotate(180deg);
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
<figure>
|
|
30
|
+
<img id="img" alt="Synoptic surface analysis for the current zone">
|
|
31
|
+
</figure>
|
|
32
|
+
`;
|
|
33
|
+
const img = this.shadowRoot.getElementById("img");
|
|
34
|
+
img.addEventListener("load", () => {
|
|
35
|
+
this.hidden = false;
|
|
36
|
+
});
|
|
37
|
+
img.addEventListener("error", () => {
|
|
38
|
+
this.hidden = true;
|
|
39
|
+
});
|
|
40
|
+
img.src = `${PLUGIN_API}/synoptic`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof customElements !== "undefined") {
|
|
45
|
+
customElements.define("synoptic-chart", SynopticChart);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { SynopticChart };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<tactical-dashboard>` — Screen 1: the next 24 hours (SPEC §6.3).
|
|
3
|
+
*
|
|
4
|
+
* Exception-driven: sail action cards render only when there are
|
|
5
|
+
* changes, the energy warning only on deficit, hazard banners only
|
|
6
|
+
* on proximity alerts. Data arrives via `setExceptions()` from the
|
|
7
|
+
* root component; updates touch cached DOM nodes only.
|
|
8
|
+
*
|
|
9
|
+
* @file components/tactical-dashboard.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { sailActionCards, tacticalNow } from "./models.mjs";
|
|
13
|
+
import { SK_BASE_CSS } from "./sk-base-css.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The custom element (browser only).
|
|
17
|
+
*/
|
|
18
|
+
class TacticalDashboard extends HTMLElement {
|
|
19
|
+
connectedCallback() {
|
|
20
|
+
if (!this.shadowRoot) {
|
|
21
|
+
this.attachShadow({ mode: "open" });
|
|
22
|
+
}
|
|
23
|
+
this.shadowRoot.innerHTML = `
|
|
24
|
+
<style>
|
|
25
|
+
${SK_BASE_CSS}
|
|
26
|
+
:host { display: block; }
|
|
27
|
+
.now { display: flex; align-items: baseline; gap: 12px; }
|
|
28
|
+
.tier {
|
|
29
|
+
font-family: var(--font-data, ui-monospace, monospace);
|
|
30
|
+
font-size: 1rem; font-weight: 700;
|
|
31
|
+
letter-spacing: 0.1em; text-transform: uppercase;
|
|
32
|
+
}
|
|
33
|
+
.cards { display: grid; gap: 8px; }
|
|
34
|
+
.card {
|
|
35
|
+
border: 1px solid var(--theme-color, var(--color-teal));
|
|
36
|
+
padding: 8px 12px; display: flex; justify-content: space-between;
|
|
37
|
+
gap: 8px; align-items: baseline;
|
|
38
|
+
}
|
|
39
|
+
.banner {
|
|
40
|
+
border: 1px solid var(--color-red);
|
|
41
|
+
color: var(--color-red);
|
|
42
|
+
font-family: var(--font-data, ui-monospace, monospace);
|
|
43
|
+
padding: 8px 12px; margin-top: 8px;
|
|
44
|
+
text-transform: uppercase; letter-spacing: 0.1em;
|
|
45
|
+
font-size: 0.85rem; font-weight: 700;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
<section class="sk-card theme-teal">
|
|
49
|
+
<h2>Next 24 Hours</h2>
|
|
50
|
+
<div class="now">
|
|
51
|
+
<span class="value" id="aws">—</span>
|
|
52
|
+
<span class="tier" id="tier">no data</span>
|
|
53
|
+
<comfort-info id="cinfo"></comfort-info>
|
|
54
|
+
</div>
|
|
55
|
+
<horizon-sparkline id="spark"></horizon-sparkline>
|
|
56
|
+
<div id="actions"></div>
|
|
57
|
+
<div id="energy"></div>
|
|
58
|
+
<div id="hazards"></div>
|
|
59
|
+
<div id="space"></div>
|
|
60
|
+
</section>
|
|
61
|
+
`;
|
|
62
|
+
this._awsEl = this.shadowRoot.getElementById("aws");
|
|
63
|
+
this._tierEl = this.shadowRoot.getElementById("tier");
|
|
64
|
+
this._cinfo = this.shadowRoot.getElementById("cinfo");
|
|
65
|
+
this._spark = this.shadowRoot.getElementById("spark");
|
|
66
|
+
this._actionsEl = this.shadowRoot.getElementById("actions");
|
|
67
|
+
this._energyEl = this.shadowRoot.getElementById("energy");
|
|
68
|
+
this._hazardsEl = this.shadowRoot.getElementById("hazards");
|
|
69
|
+
this._spaceEl = this.shadowRoot.getElementById("space");
|
|
70
|
+
if (this._exceptions) {
|
|
71
|
+
this.setExceptions(this._exceptions);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Renders from the worker's exception view.
|
|
77
|
+
*
|
|
78
|
+
* @param {object|null} exceptions
|
|
79
|
+
*/
|
|
80
|
+
setExceptions(exceptions) {
|
|
81
|
+
this._exceptions = exceptions;
|
|
82
|
+
if (!this._awsEl) {
|
|
83
|
+
return; // Not yet connected
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Now readout
|
|
87
|
+
const now = tacticalNow(exceptions);
|
|
88
|
+
this._awsEl.textContent =
|
|
89
|
+
now.awsKnots != null ? `${now.awsKnots.toFixed(1)}` : "—";
|
|
90
|
+
this._tierEl.textContent = now.comfortLevel ?? "no data";
|
|
91
|
+
this._tierEl.style.color = now.color;
|
|
92
|
+
this._cinfo?.setAttribute("tier", now.comfortLevel ?? "");
|
|
93
|
+
|
|
94
|
+
this._spark.setColumns(exceptions?.next24h?.comfortBlocks ?? []);
|
|
95
|
+
|
|
96
|
+
// Sail action cards — only when there are changes; maneuver
|
|
97
|
+
// events read as the sail work they demand (work doc #5)
|
|
98
|
+
const cards = sailActionCards(exceptions);
|
|
99
|
+
this._actionsEl.innerHTML = "";
|
|
100
|
+
if (cards.length > 0) {
|
|
101
|
+
const wrap = document.createElement("div");
|
|
102
|
+
wrap.className = "cards";
|
|
103
|
+
for (const c of cards) {
|
|
104
|
+
const el = document.createElement("div");
|
|
105
|
+
el.className = "card";
|
|
106
|
+
el.style.setProperty("--theme-color", "var(--color-orange)");
|
|
107
|
+
const name = document.createElement("span");
|
|
108
|
+
name.className = "value-small";
|
|
109
|
+
name.textContent = c.maneuver
|
|
110
|
+
? `${c.maneuver === "tack" ? "Tack" : "Gybe"} to ${c.toTack ?? "?"}`
|
|
111
|
+
: c.sailState;
|
|
112
|
+
const when = document.createElement("span");
|
|
113
|
+
when.className = "muted";
|
|
114
|
+
when.textContent = c.maneuver
|
|
115
|
+
? `~${c.stamp}${c.twsKnots != null ? `, ${c.twsKnots.toFixed(0)} kt` : ""}`
|
|
116
|
+
: `${c.night ? "☾ " : ""}+${c.hoursFromNow}h ${c.stamp}`;
|
|
117
|
+
el.append(name, when);
|
|
118
|
+
wrap.appendChild(el);
|
|
119
|
+
}
|
|
120
|
+
this._actionsEl.appendChild(wrap);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Energy deficit — only when true
|
|
124
|
+
const deficit = exceptions?.next24h?.energyDeficitAlert === true;
|
|
125
|
+
this._energyEl.innerHTML = "";
|
|
126
|
+
if (deficit) {
|
|
127
|
+
const el = document.createElement("div");
|
|
128
|
+
el.className = "banner";
|
|
129
|
+
el.textContent = "Energy deficit next 24h";
|
|
130
|
+
this._energyEl.appendChild(el);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Hazard banners — only when present
|
|
134
|
+
const hazards = exceptions?.next24h?.hazards ?? [];
|
|
135
|
+
this._hazardsEl.innerHTML = "";
|
|
136
|
+
for (const h of hazards) {
|
|
137
|
+
const el = document.createElement("div");
|
|
138
|
+
el.className = "banner";
|
|
139
|
+
el.textContent = `⚠ ${h.description ?? h.noteId ?? "hazard"} +${h.hoursFromNow}h`;
|
|
140
|
+
this._hazardsEl.appendChild(el);
|
|
141
|
+
}
|
|
142
|
+
if (this._spaceEvents) {
|
|
143
|
+
this.setSpaceEvents(this._spaceEvents);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Tactical space-weather banners (work doc #3): aurora-class
|
|
149
|
+
* alerts only; comet items belong to the strategic outlook.
|
|
150
|
+
*
|
|
151
|
+
* @param {Array<{description: string}>|null} events
|
|
152
|
+
*/
|
|
153
|
+
setSpaceEvents(events) {
|
|
154
|
+
this._spaceEvents = events;
|
|
155
|
+
if (!this._spaceEl) {
|
|
156
|
+
return; // Not yet connected
|
|
157
|
+
}
|
|
158
|
+
this._spaceEl.innerHTML = "";
|
|
159
|
+
for (const e of events ?? []) {
|
|
160
|
+
const el = document.createElement("div");
|
|
161
|
+
el.className = "banner";
|
|
162
|
+
el.textContent = `✦ ${e.description}`;
|
|
163
|
+
this._spaceEl.appendChild(el);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof customElements !== "undefined") {
|
|
169
|
+
customElements.define("tactical-dashboard", TacticalDashboard);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { TacticalDashboard };
|