@meri-imperiumi/signalk-passage-briefing 0.5.0 → 0.5.2
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/CHANGELOG.md +21 -0
- package/SPEC.md +7 -0
- package/package.json +1 -1
- package/plugin/bulletin-engine.js +111 -19
- package/plugin/bulletin-source.js +6 -1
- package/plugin/hazard-source.js +10 -1
- package/plugin/index.js +433 -42
- package/plugin/satellite-source.js +23 -1
- package/plugin/source-status.js +288 -0
- package/plugin/zone-source.js +115 -7
- package/public/components/models.mjs +54 -1
- package/public/components/source-status.js +247 -0
- package/public/components/strategic-outlook.js +2 -0
- package/tests/bulletin-engine.test.js +230 -4
- package/tests/fixtures/bulletins/hsfep2-knhc-2026-10-03.txt +85 -0
- package/tests/fixtures/ukho-rnw-sample.html +131 -0
- package/tests/plugin.test.js +251 -33
- package/tests/satellite-source.test.js +16 -0
- package/tests/source-status.test.js +343 -0
- package/tests/zone-source.test.js +53 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.5.2] - 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Cached briefings kept serving bulletin blocks computed at compile time, so filtering and parsing fixes only reached the crew after a re-fetch — on the Tactical route view the console still showed the pre-fix boilerplate while the freshly compiled conditions-here view was already clean. The `/api/briefing` serve path now recomputes the merged bulletin console from the bulletin cache against the payload's track on every serve (route and here mode alike, subsuming the old splice-when-missing logic): parsing and filtering improvements apply to cached briefings on the next page load, a payload compiled before a fresher bulletin arrived picks the newer merged view, and a payload whose bulletin cache is gone keeps its own compile-time console.
|
|
10
|
+
- Residual bulletin-console noise after the boilerplate rule went live: the EP1 seasonal coverage note ("FROM OCTOBER 15 TO APRIL 1, DUE TO THE CLIMATOLOGY…") survived by enumerating the newly added severity keywords (freezing spray, dense fog, volcanic ash), and the NWS area-header line ("NORTH PACIFIC EQUATOR TO 30N BETWEEN 140W AND 180W") parsed to a pole-to-pole box on its longitude band, intersecting every water on it — visible as far away as Fiji. Fixed both ways: the fixed NWS disclaimer paragraphs (seasonal note, sea-state definition, 1-minute-winds note, tropical-cyclone pointers, ice-free note, outreach appeal) are now stripped whole by their stable opening lines at cruft-cut time, and "EQUATOR" parses as a real southern bound (lat 0) in the cardinal-bounds reader, so "EQUATOR TO 30N" clips to the bulletin's actual hemisphere.
|
|
11
|
+
- Severity keyword false positives on product and office names: "HIGH SEAS FORECAST" (the masthead) no longer trips the HIGH SEAS keyword and "NATIONAL HURRICANE CENTER" no longer trips the HURRICANE keyword in the boilerplate classifier, so mastheads and forecaster footers cannot masquerade as warnings. HURRICANE and CYCLONE are now severity keywords (kept in sync with the webapp highlighter, which also gained freezing spray, dense fog and volcanic ash) — hurricane and tropical-cyclone paragraphs highlight and survive the console filter, as the crew intended: a cyclone near our waters is on our radar even when the exact bounding phrase parses loosely.
|
|
12
|
+
- The "Warnings On Your Waters" console drowned in bulletin boilerplate (day-one finding from the data-source checklist): the conservative keep for geometry-less blocks meant every NWS masthead, disclaimer ("SEAS GIVEN AS…", "SUPERSEDED BY…", "SECURITE"), section heading and forecaster footer from all four merged bulletins rendered as "warnings" while the actual warning paragraphs were correctly filtered out as off-waters. The block filter now drops blocks that carry neither parseable geography nor a severity keyword — transmission boilerplate is not a warning — with the product name "HIGH SEAS FORECAST" excluded from the severity match so the masthead cannot masquerade as a HIGH SEAS warning. Structured UKHO warnings are unaffected (they are warnings by definition). The severity keyword list gains freezing spray, dense fog and volcanic ash, synced to the webapp highlighter; a warning whose geography failed to parse still survives on its severity alone.
|
|
13
|
+
- NWS High Seas Forecast texts via the api.weather.gov product feed began with a `000` frame line and a bare WMO routing header (`FZPN03 KNHC 030838`), which became the bulletin header ("000") and a junk block in the console. The cruft cutter strips digit-only frame lines and routing headers without the trailing `Z`; the product code line (`HSFEP2`) survives and now serves as the bulletin header.
|
|
14
|
+
- The NWS api.weather.gov product resolver (configured HSF bulletin feeds) failed with `400 Bad Request`: the API started rejecting the `?limit=1` query parameter the resolver appended ("Query parameter limit is not recognized"), and the second resolution step fetched the graph's `id` field — a bare UUID, not a URL. The resolver now requests the bare locations URL, picks the newest iteration by `issuanceTime`, and follows `@id` (falling back to the UUID in the products URL). Fetch failures now carry the failing URL in the message so the source status checklist row detail names the exact request. Verified live: the NP feed returns the FZPN40 High Seas Forecast text.
|
|
15
|
+
- The UKHO structured-warnings source was dead twice over: the `/api/Warnings/Area/{XIV}` JSON endpoint no longer exists (the MSI site is server-rendered HTML now), and it was being queried for every resolved zone although the UKHO only coordinates NAVAREA I — other zones 404 forever. `ukhoWarningsUrl` now returns the Radio Navigational Warnings page for zone 1 and null otherwise, and the page's warning sections (reference, date-time group, full ANMB text in `<pre class="warning-description">`) parse into the same structured warning shape the old JSON fed, so the serve-time filtering pipeline is unchanged. Verified live: 28 in-force warnings parsed with references and timestamps.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Data source status checklist (work doc #23): every ingest path — the weather fetch, the per-zone bulletin and UKHO pulls, the configured extra bulletin feeds, the synoptic chart rasters, the GDACS hazard feed and the local celestial ephemeris — now records the outcome of its latest cycle in a persistent per-source registry (`plugin/source-status.js`, survives restarts in `source-status.json`), and the webapp renders it as a collapsed-by-default checklist card in the strategic outlook. Error classification is coarse and stable (`http-404`, `http-429`, `http-4xx`, `http-5xx`, `timeout`, `network`, `parse`, `unavailable`) so a moved URL reads differently from a dead host; cycles skipped while the boat is offline record as `offline-skipped` and never count as failures — an offline boat shows a wall of `[ SKIP ]`, which is correct information, not an error state. The checklist also covers the Signal K side: the subscribed paths the state machine and payload compile consume (internet state, navigation state, house SoC required; active route and the energy outlook optional) and the resource sources (route resources, polar, ship's time, logbook) record availability every ticker minute — a required path with no value since startup reads `[ FAIL ]`, an absent optional source renders muted `[ N/A ]`. Brackets: `[ OK ]` green, `[ WARN ]` orange (429/parse failures, or success stale beyond the source's expected refresh interval), `[ FAIL ]` red, `[ SKIP ]`/`[ N/A ]` muted; the summary line ("12 sources, 1 FAIL") stays visible while collapsed, rows expand to URL, last success, classified error and consecutive-failure count. Served at `GET /plugins/signalk-passage-briefing/sources` for external diagnostics; polling at a minute rate, rows update in place.
|
|
20
|
+
|
|
21
|
+
## [0.5.1] - 2026-10-03
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Duplicate satellite-pass lines (ISS ×2, Tiangong ×3): CelesTrak's stations group lists several catalog entries per crewed station — the docked modules ISS (ZARYA), ISS (NAUKA) and CSS (TIANHE), CSS (WENTIAN), CSS (MENGTIAN) — all mapped to the same display name, so every pass was propagated and reported once per module. `parseTLEs` now keeps one element set per station, preferring the core module's (ZARYA, TIANHE).
|
|
25
|
+
|
|
5
26
|
## [0.5.0] - 2026-10-03
|
|
6
27
|
|
|
7
28
|
### Added
|
package/SPEC.md
CHANGED
|
@@ -164,6 +164,12 @@ The plugin monitors `network.internet.state`, `navigation.state`, and `electrica
|
|
|
164
164
|
* **Cron Schedule Rules:** Runs at `02:15`, `08:15`, `14:15`, and `20:15` UTC (15 minutes after major global ensemble publication windows).
|
|
165
165
|
* **Execution Guard:** Checks if `navigation.state === 'moored' | 'anchored'`. If `navigation.state === 'sailing'`, cron timers are disabled and data fetches are strictly tied to single explicit transitions of `network.internet.state` to `online` or `metered`.
|
|
166
166
|
|
|
167
|
+
### 2.3 Data Source Status Registry (work doc #23)
|
|
168
|
+
|
|
169
|
+
Every ingest path records the outcome of its latest cycle in a persistent per-source registry (`plugin/source-status.js`, persisted in `source-status.json`) so a moved URL or dead host is diagnosable mid-passage instead of surfacing only as absence of data. Covered sources: the weather fetch, per-zone bulletin/UKHO pulls, the configured extra bulletin feeds, the synoptic chart rasters, the GDACS hazard feed, the local celestial ephemeris, and — checked on the one-minute ticker — the Signal K sources themselves (required subscribed paths: internet state, navigation state, house SoC; optional: active route, energy outlook, route resources, polar, ship's time, logbook).
|
|
170
|
+
|
|
171
|
+
Each entry carries `id`, `label`, `kind`, `url`, `lastAttemptAt`, `lastSuccessAt`, `lastError` (`{class, message}`), `consecutiveFailures`, `expectedRefreshMs` and `lastStatus` (`ok` / `fail` / `skip` / `absent`). Error classes are coarse and stable: `http-404`, `http-429`, `http-4xx`, `http-5xx`, `timeout`, `network`, `parse`, `unavailable`. Cycles the online gate skips record `offline-skipped` behavior via `lastStatus: "skip"` — never counted as failures. The registry serves at `GET /plugins/signalk-passage-briefing/sources` (diagnostics, not briefing content) and survives restarts.
|
|
172
|
+
|
|
167
173
|
---
|
|
168
174
|
|
|
169
175
|
## 3. Data Schemas & Structural Interfaces
|
|
@@ -563,6 +569,7 @@ Displays:
|
|
|
563
569
|
2. Macro Sea State warnings block.
|
|
564
570
|
3. Convective Warning block (CAPE / K-Index).
|
|
565
571
|
4. Raw METAREA text bulletin container.
|
|
572
|
+
5. `<source-status>` checklist (work doc #23): collapsed by default, summary line always visible ("12 sources, 1 FAIL"). Rows: last-attempt stamp (muted) | source label + kind | right-aligned monospace status bracket mapped by the pure `statusVerdict` view model — `[ OK ]` green, `[ WARN ]` orange (429/parse failures, or success stale beyond the source's expected refresh interval), `[ FAIL ]` red, `[ SKIP ]`/`[ N/A ]` muted. Tap expands the row's detail (URL, last success, classified error, consecutive failures). The component polls `GET /plugins/signalk-passage-briefing/sources` at a minute rate and mutates only `textContent`/class attributes; expanded state persists in `localStorage`.
|
|
566
573
|
|
|
567
574
|
#### `<horizon-sparkline>` (SVG Renderer)
|
|
568
575
|
|
package/package.json
CHANGED
|
@@ -26,15 +26,40 @@
|
|
|
26
26
|
const SEVERE_KEYWORDS = [
|
|
27
27
|
"GALE",
|
|
28
28
|
"STORM",
|
|
29
|
+
"HURRICANE",
|
|
29
30
|
"HURRICANE FORCE",
|
|
31
|
+
"CYCLONE",
|
|
30
32
|
"SQUALL",
|
|
31
33
|
"VIOLENT STORM",
|
|
32
34
|
"ROUGH SEAS",
|
|
33
35
|
"VERY ROUGH SEAS",
|
|
34
36
|
"HIGH SEAS",
|
|
35
37
|
"PHENOMENAL SEAS",
|
|
38
|
+
"FREEZING SPRAY",
|
|
39
|
+
"DENSE FOG",
|
|
40
|
+
"VOLCANIC ASH",
|
|
36
41
|
];
|
|
37
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Whether a block carries a severity keyword (kept in sync with the
|
|
45
|
+
* webapp's highlighter copy in `public/components/models.mjs`).
|
|
46
|
+
*
|
|
47
|
+
* Product and office names must not trip the keywords, or every NWS
|
|
48
|
+
* masthead survives the boilerplate filter: "HIGH SEAS FORECAST"
|
|
49
|
+
* would read as a HIGH SEAS warning, "NATIONAL HURRICANE CENTER"
|
|
50
|
+
* as a hurricane warning.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} text
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
function hasSevereKeyword(text) {
|
|
56
|
+
const upper = String(text ?? "")
|
|
57
|
+
.toUpperCase()
|
|
58
|
+
.replace(/HIGH SEAS FORECAST/g, "")
|
|
59
|
+
.replace(/HURRICANE CENTER/g, "");
|
|
60
|
+
return SEVERE_KEYWORDS.some((keyword) => upper.includes(keyword));
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
/**
|
|
39
64
|
* Retained NAVTEX B_2 subject indicators (work doc #4): A navigational
|
|
40
65
|
* warnings, B gale warnings, E meteorological forecasts. The rest
|
|
@@ -61,19 +86,46 @@ const SECTION_ANCHORS =
|
|
|
61
86
|
* @param {string} text - Raw bulletin text
|
|
62
87
|
* @returns {string} Cleaned text
|
|
63
88
|
*/
|
|
89
|
+
/**
|
|
90
|
+
* NWS fixed disclaimer paragraphs, stripped whole by their stable
|
|
91
|
+
* opening line. These frame every High Seas Forecast (seasonal
|
|
92
|
+
* coverage note, sea-state definitions, tropical-cyclone pointers);
|
|
93
|
+
* they are transmission framing, not bulletin content — and with
|
|
94
|
+
* HURRICANE/CYCLONE in the severity keywords they would otherwise
|
|
95
|
+
* survive the boilerplate filter by enumeration.
|
|
96
|
+
*/
|
|
97
|
+
const DISCLAIMER_START =
|
|
98
|
+
/^(SEAS GIVEN AS SIGNIFICANT|SUPERSEDED BY NEXT ISSUANCE|THIS HIGH SEAS FORECAST USES|FORECAST WINDS IN AND NEAR ACTIVE TROPICAL CYCLONES|ONLY YOU KNOW THE WEATHER|FOR ANY TROPICAL CYCLONE INFORMATION|ALL FORECASTS VALID OVER ICE FREE|FROM\s+[A-Z]+\s+\d+\s+TO\s+[A-Z]+\s+\d+\s*,?\s+DUE TO THE CLIMATOLOGY)/i;
|
|
99
|
+
|
|
64
100
|
function stripBoilerplate(text) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
101
|
+
const kept = [];
|
|
102
|
+
let skipping = false;
|
|
103
|
+
for (const line of text.split(/\r?\n/)) {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (trimmed.length === 0) {
|
|
106
|
+
skipping = false;
|
|
107
|
+
kept.push(line);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (/^NNNN$/.test(trimmed)) {
|
|
111
|
+
continue; // End-of-message frame
|
|
112
|
+
}
|
|
113
|
+
if (/^\d{3,4}$/.test(trimmed)) {
|
|
114
|
+
continue; // NWS product frame line ("000")
|
|
115
|
+
}
|
|
116
|
+
if (/^[A-Z]{4}\d{2}\s+[A-Z]{4}\s+\d{6}Z?/.test(trimmed)) {
|
|
117
|
+
continue; // Routing header (FQPS01 NFFN 011200Z AUG 26,
|
|
118
|
+
// or the NWS API's bare "FZPN03 KNHC 030838")
|
|
119
|
+
}
|
|
120
|
+
if (!skipping && DISCLAIMER_START.test(trimmed)) {
|
|
121
|
+
skipping = true; // Fixed disclaimer paragraph: drop to the blank line
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!skipping) {
|
|
125
|
+
kept.push(line);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return kept
|
|
77
129
|
.join("\n")
|
|
78
130
|
.replace(/\bGULF OF AMERICA\b/gi, "GULF OF MEXICO")
|
|
79
131
|
.replace(/\n{3,}/g, "\n\n")
|
|
@@ -280,6 +332,22 @@ function parseCardinalBounds(text) {
|
|
|
280
332
|
minLat = hemisphereDegrees(northOf[1], northOf[2].toUpperCase());
|
|
281
333
|
found = true;
|
|
282
334
|
}
|
|
335
|
+
// The equator is a real bound in NWS area headers ("NORTH PACIFIC
|
|
336
|
+
// EQUATOR TO 30N BETWEEN 140W AND 180W"), not a poleward default —
|
|
337
|
+
// without this the area line parses to a pole-to-pole box that
|
|
338
|
+
// intersects every water on the longitude band
|
|
339
|
+
const equatorTo = text.match(/EQUATOR\s+TO\s+(\d+(?:\.\d+)?)\s*([NS])/i);
|
|
340
|
+
if (equatorTo) {
|
|
341
|
+
const deg = hemisphereDegrees(equatorTo[1], equatorTo[2].toUpperCase());
|
|
342
|
+
if (deg >= 0) {
|
|
343
|
+
minLat = Math.max(minLat, 0);
|
|
344
|
+
maxLat = Math.min(maxLat, deg);
|
|
345
|
+
} else {
|
|
346
|
+
minLat = Math.max(minLat, deg);
|
|
347
|
+
maxLat = Math.min(maxLat, 0);
|
|
348
|
+
}
|
|
349
|
+
found = true;
|
|
350
|
+
}
|
|
283
351
|
|
|
284
352
|
// EAST OF x means lon ≥ x; WEST OF y means lon ≤ y. Both present:
|
|
285
353
|
// the box runs from x eastwards to y (unwrapping y across the seam
|
|
@@ -829,6 +897,13 @@ function filterBulletin({
|
|
|
829
897
|
if (!intersectsTrack(geometry, track)) {
|
|
830
898
|
return null; // Discard rule: not on our waters
|
|
831
899
|
}
|
|
900
|
+
// The warnings console is for relevant paragraphs only: a block
|
|
901
|
+
// with neither geography nor severity is transmission
|
|
902
|
+
// boilerplate (preamble, disclaimers, footers) — dropping it
|
|
903
|
+
// keeps real warnings from drowning in NWS preamble text
|
|
904
|
+
if (!geometry && !hasSevereKeyword(blockText)) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
832
907
|
return {
|
|
833
908
|
text: blockText,
|
|
834
909
|
subject: subject ?? null,
|
|
@@ -914,7 +989,11 @@ async function resolveBulletinSource(
|
|
|
914
989
|
headers: { Accept: "text/plain" },
|
|
915
990
|
});
|
|
916
991
|
if (!response.ok) {
|
|
917
|
-
|
|
992
|
+
// The URL rides in the message: the source status checklist
|
|
993
|
+
// shows it verbatim in the row detail (work doc #23)
|
|
994
|
+
throw new Error(
|
|
995
|
+
`${url} returned ${response.status} ${response.statusText}`,
|
|
996
|
+
);
|
|
918
997
|
}
|
|
919
998
|
return { text: await response.text(), source: "api" };
|
|
920
999
|
} finally {
|
|
@@ -922,8 +1001,11 @@ async function resolveBulletinSource(
|
|
|
922
1001
|
}
|
|
923
1002
|
}
|
|
924
1003
|
|
|
925
|
-
// api.weather.gov: latest iteration of the product type/location
|
|
926
|
-
|
|
1004
|
+
// api.weather.gov: latest iteration of the product type/location.
|
|
1005
|
+
// No query parameters — the API now rejects unknown ones (limit
|
|
1006
|
+
// included) with a 400, so "latest" is resolved client-side by
|
|
1007
|
+
// sorting the returned graph on issuanceTime
|
|
1008
|
+
const listUrl = apiMatch[0];
|
|
927
1009
|
const get = async (u) => {
|
|
928
1010
|
const controller = new AbortController();
|
|
929
1011
|
const timer = setTimeout(() => controller.abort(), timeoutMs ?? 15000);
|
|
@@ -933,7 +1015,9 @@ async function resolveBulletinSource(
|
|
|
933
1015
|
headers: { Accept: "application/geo+json, application/json" },
|
|
934
1016
|
});
|
|
935
1017
|
if (!response.ok) {
|
|
936
|
-
throw new Error(
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
`${u} returned ${response.status} ${response.statusText}`,
|
|
1020
|
+
);
|
|
937
1021
|
}
|
|
938
1022
|
return response.json();
|
|
939
1023
|
} finally {
|
|
@@ -941,11 +1025,18 @@ async function resolveBulletinSource(
|
|
|
941
1025
|
}
|
|
942
1026
|
};
|
|
943
1027
|
const list = await get(listUrl);
|
|
944
|
-
|
|
945
|
-
|
|
1028
|
+
// "id" is a bare UUID; the fetchable product URL lives in "@id"
|
|
1029
|
+
const latest = [...(list?.["@graph"] ?? [])]
|
|
1030
|
+
.filter((product) => product?.id)
|
|
1031
|
+
.sort((a, b) =>
|
|
1032
|
+
String(b.issuanceTime ?? "").localeCompare(String(a.issuanceTime ?? "")),
|
|
1033
|
+
)[0];
|
|
1034
|
+
if (!latest) {
|
|
946
1035
|
throw new Error("No product iterations available");
|
|
947
1036
|
}
|
|
948
|
-
const
|
|
1037
|
+
const productUrl =
|
|
1038
|
+
latest["@id"] ?? `https://api.weather.gov/products/${latest.id}`;
|
|
1039
|
+
const product = await get(productUrl);
|
|
949
1040
|
const text = product?.productText;
|
|
950
1041
|
if (typeof text !== "string" || text.length === 0) {
|
|
951
1042
|
throw new Error("Product has no text");
|
|
@@ -1018,6 +1109,7 @@ module.exports = {
|
|
|
1018
1109
|
SEVERE_KEYWORDS,
|
|
1019
1110
|
SECTION_ANCHORS,
|
|
1020
1111
|
stripBoilerplate,
|
|
1112
|
+
hasSevereKeyword,
|
|
1021
1113
|
navtexSubject,
|
|
1022
1114
|
shouldRetainSubject,
|
|
1023
1115
|
segmentBlocks,
|
|
@@ -95,6 +95,9 @@ async function saveBulletinCache(dataDir, entries) {
|
|
|
95
95
|
* [params.zoneBulletins] - Already-fetched zone bulletins
|
|
96
96
|
* @param {typeof fetch} [params.fetchImpl]
|
|
97
97
|
* @param {number} [params.timeoutMs]
|
|
98
|
+
* @param {Function} [params.onFailure] - Called with `(url, error)`
|
|
99
|
+
* for every URL that failed this cycle (source status registry,
|
|
100
|
+
* work doc #23)
|
|
98
101
|
* @returns {Promise<{fetched: string[], failed: string[], entries: BulletinEntry[]}>}
|
|
99
102
|
* Newly fetched URLs (failures listed separately) and the merged
|
|
100
103
|
* cache
|
|
@@ -105,6 +108,7 @@ async function refreshBulletins({
|
|
|
105
108
|
zoneBulletins = [],
|
|
106
109
|
fetchImpl = fetch,
|
|
107
110
|
timeoutMs,
|
|
111
|
+
onFailure,
|
|
108
112
|
}) {
|
|
109
113
|
const cached = await loadBulletinCache(dataDir);
|
|
110
114
|
const fetched = [];
|
|
@@ -133,7 +137,8 @@ async function refreshBulletins({
|
|
|
133
137
|
source,
|
|
134
138
|
});
|
|
135
139
|
fetched.push(url);
|
|
136
|
-
} catch {
|
|
140
|
+
} catch (error) {
|
|
141
|
+
onFailure?.(url, error);
|
|
137
142
|
failed.push(url);
|
|
138
143
|
}
|
|
139
144
|
}
|
package/plugin/hazard-source.js
CHANGED
|
@@ -225,12 +225,16 @@ async function saveHazards(dataDir, events) {
|
|
|
225
225
|
* @param {string} params.dataDir - Plugin data directory
|
|
226
226
|
* @param {typeof fetch} [params.fetchImpl]
|
|
227
227
|
* @param {number} [params.timeoutMs]
|
|
228
|
+
* @param {Function} [params.onFailure] - Called with the failure
|
|
229
|
+
* (transport error or HTTP status error) when the feed could not
|
|
230
|
+
* be fetched this cycle (source status registry, work doc #23)
|
|
228
231
|
* @returns {Promise<{fetched: boolean, events: Array<object>}>}
|
|
229
232
|
*/
|
|
230
233
|
async function refreshHazards({
|
|
231
234
|
dataDir,
|
|
232
235
|
fetchImpl = fetch,
|
|
233
236
|
timeoutMs = 15000,
|
|
237
|
+
onFailure,
|
|
234
238
|
}) {
|
|
235
239
|
const cached = await loadHazards(dataDir);
|
|
236
240
|
let incoming = null;
|
|
@@ -242,8 +246,13 @@ async function refreshHazards({
|
|
|
242
246
|
});
|
|
243
247
|
if (response.ok) {
|
|
244
248
|
incoming = parseHazardsXml(await response.text());
|
|
249
|
+
} else {
|
|
250
|
+
onFailure?.(
|
|
251
|
+
new Error(`${response.status} ${response.statusText}`.trim()),
|
|
252
|
+
);
|
|
245
253
|
}
|
|
246
|
-
} catch {
|
|
254
|
+
} catch (error) {
|
|
255
|
+
onFailure?.(error);
|
|
247
256
|
incoming = null;
|
|
248
257
|
} finally {
|
|
249
258
|
clearTimeout(timer);
|