signalk-questdb 1.8.1 → 1.8.3
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/README.md +2 -0
- package/dist/api-contract.d.ts +17 -0
- package/dist/console-proxy.d.ts +8 -0
- package/dist/console-proxy.js +81 -8
- package/dist/console-proxy.js.map +1 -1
- package/dist/index.js +81 -12
- package/dist/index.js.map +1 -1
- package/dist/test/console-proxy.test.js +328 -1
- package/dist/test/console-proxy.test.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/{PluginConfigurationPanel-w6DPiiey.js → PluginConfigurationPanel-u1H_dS78.js} +1 -1
- package/public/assets/{index-Dqj6oJzX.js → index-C6iuilck.js} +1 -1
- package/public/assets/{virtualExposes-DcAiR6tF.js → virtualExposes-Cj0hoMce.js} +1 -1
- package/public/remoteEntry.js +1 -1
package/README.md
CHANGED
|
@@ -201,6 +201,8 @@ with no position in the window is skipped, as is one that has already transmitte
|
|
|
201
201
|
since startup. Restore honours the recording toggles: with **Record AIS targets**
|
|
202
202
|
off, no AIS target is restored.
|
|
203
203
|
|
|
204
|
+
The config panel reports the outcome once startup finishes: **Vessels Restored** with the count, or **Restore failed** if the replay could not run. Neither appears when the option is off.
|
|
205
|
+
|
|
204
206
|
## Connectivity
|
|
205
207
|
|
|
206
208
|
In **managed mode** the plugin no longer needs `QuestDB host` to be correct for
|
package/dist/api-contract.d.ts
CHANGED
|
@@ -43,6 +43,23 @@ export interface DbStatus extends ApiError {
|
|
|
43
43
|
ulimitClamp?: UlimitClampStatus | null;
|
|
44
44
|
hostMaxMapCount?: MaxMapCountStatus | null;
|
|
45
45
|
endpoint?: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Outcome of the startup restore, or null when it did not run (disabled, or
|
|
48
|
+
* still in flight). Surfaced here rather than only through
|
|
49
|
+
* `setPluginStatus`, because Signal K stores plugin status under
|
|
50
|
+
* `plugin.id` but reads it back by `plugin.name` — so any plugin with a
|
|
51
|
+
* display name gets an empty statusMessage and the count would be invisible.
|
|
52
|
+
*/
|
|
53
|
+
restore?: RestoreStatus | null;
|
|
54
|
+
}
|
|
55
|
+
/** Startup-restore outcome, shown on the config panel. */
|
|
56
|
+
export interface RestoreStatus {
|
|
57
|
+
/** Vessels replayed into the Signal K model. */
|
|
58
|
+
contexts: number;
|
|
59
|
+
/** Contexts skipped because the vessel had already transmitted. */
|
|
60
|
+
skippedLive: number;
|
|
61
|
+
/** True when the restore threw; `contexts` is then not meaningful. */
|
|
62
|
+
failed: boolean;
|
|
46
63
|
}
|
|
47
64
|
/** GET /api/versions — QuestDB releases, drafts filtered out. */
|
|
48
65
|
export interface QuestdbVersion {
|
package/dist/console-proxy.d.ts
CHANGED
|
@@ -29,5 +29,13 @@ export interface ConsoleProxyDeps {
|
|
|
29
29
|
/** Base URL of QuestDB's HTTP interface, e.g. `http://127.0.0.1:9000`. */
|
|
30
30
|
baseUrl: () => string | null;
|
|
31
31
|
debug: (msg: string) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Where this proxy is mounted, e.g. `/plugins/signalk-questdb/console`.
|
|
34
|
+
* Needed to rewrite upstream redirects: QuestDB answers `/` with
|
|
35
|
+
* `301 Location: /index.html`, an ABSOLUTE path that escapes the mount and
|
|
36
|
+
* lands on the Signal K root — a 404, and the reason the webapp reported
|
|
37
|
+
* "console unavailable" while the console itself was fine.
|
|
38
|
+
*/
|
|
39
|
+
mountPath: string;
|
|
32
40
|
}
|
|
33
41
|
export declare function createConsoleProxy(deps: ConsoleProxyDeps): (req: IncomingMessage, res: ServerResponse) => void;
|
package/dist/console-proxy.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Rewrite an upstream redirect so it stays inside the mount.
|
|
4
|
+
*
|
|
5
|
+
* Only root-relative targets need it: a relative `Location` already resolves
|
|
6
|
+
* against the proxied URL, and an absolute one pointing elsewhere is not ours
|
|
7
|
+
* to rewrite (and is left alone rather than silently retargeted).
|
|
8
|
+
*/
|
|
9
|
+
function rewriteLocation(location, mountPath) {
|
|
10
|
+
if (typeof location !== "string")
|
|
11
|
+
return undefined;
|
|
12
|
+
if (!location.startsWith("/") || location.startsWith("//"))
|
|
13
|
+
return undefined;
|
|
14
|
+
return `${mountPath.replace(/\/$/, "")}${location}`;
|
|
15
|
+
}
|
|
2
16
|
// Hop-by-hop headers must not be forwarded (RFC 7230 §6.1). `host` is dropped
|
|
3
17
|
// separately because it has to name the upstream, not the Signal K server.
|
|
4
18
|
const HOP_BY_HOP = new Set([
|
|
@@ -102,6 +116,9 @@ export function createConsoleProxy(deps) {
|
|
|
102
116
|
headers: forwardableHeaders(req.headers),
|
|
103
117
|
}, (proxied) => {
|
|
104
118
|
const headers = returnableHeaders(proxied.headers);
|
|
119
|
+
const rewritten = rewriteLocation(proxied.headers.location, deps.mountPath);
|
|
120
|
+
if (rewritten)
|
|
121
|
+
headers.location = rewritten;
|
|
105
122
|
// The console is served under the Signal K origin, so its scripts run
|
|
106
123
|
// with the admin's session. Signal K's auth cookie is httpOnly, so
|
|
107
124
|
// script cannot read it — but it IS attached to same-origin requests,
|
|
@@ -125,18 +142,74 @@ export function createConsoleProxy(deps) {
|
|
|
125
142
|
"form-action 'self'; frame-ancestors 'self'; base-uri 'self'";
|
|
126
143
|
// The console has no reason to be sniffed into a different type.
|
|
127
144
|
headers["x-content-type-options"] = "nosniff";
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
145
|
+
// QuestDB's redirect states NEITHER content-length nor
|
|
146
|
+
// transfer-encoding, so Node cannot know the response is complete and
|
|
147
|
+
// falls back to `Transfer-Encoding: chunked` — announcing a body that
|
|
148
|
+
// never arrives. The browser then waits on a 301 that never finishes,
|
|
149
|
+
// which is exactly how a working console still reported "console
|
|
150
|
+
// unavailable" after the Location rewrite was already correct.
|
|
151
|
+
//
|
|
152
|
+
// Declaring the length explicitly is what stops Node guessing. Applied
|
|
153
|
+
// to every status that carries no body by definition, not just 3xx.
|
|
154
|
+
// Deliberately keyed on the STATUS, not on the upstream's framing
|
|
155
|
+
// headers: an origin that omits both (QuestDB) and one that sends
|
|
156
|
+
// `chunked` for an empty body (Node's own server does this) must be
|
|
157
|
+
// handled identically, and only the status tells us a body is absent
|
|
158
|
+
// by definition.
|
|
159
|
+
const status = proxied.statusCode;
|
|
160
|
+
// 204 and 304 carry no body by protocol, whatever the origin sends.
|
|
161
|
+
// Node already frames these correctly, so they only need the upstream
|
|
162
|
+
// drained.
|
|
163
|
+
const neverHasBody = status === 204 || status === 304;
|
|
164
|
+
// A 3xx MAY carry a body (RFC 9110 §15.4) and browsers render it, so
|
|
165
|
+
// this cannot assume every redirect is empty — an earlier version did,
|
|
166
|
+
// and silently dropped the body.
|
|
167
|
+
//
|
|
168
|
+
// Bodyless means the upstream declared NEITHER framing header. Testing
|
|
169
|
+
// content-length alone is not enough: a redirect body sent with
|
|
170
|
+
// chunked encoding carries no length, and was also being dropped.
|
|
171
|
+
const declaresBody = proxied.headers["content-length"] !== undefined ||
|
|
172
|
+
proxied.headers["transfer-encoding"] !== undefined;
|
|
173
|
+
const redirectWithoutBody = status !== undefined &&
|
|
174
|
+
status >= 300 &&
|
|
175
|
+
status < 400 &&
|
|
176
|
+
!declaresBody;
|
|
177
|
+
if (req.method === "HEAD" || neverHasBody || redirectWithoutBody) {
|
|
178
|
+
// Synthesize `content-length: 0` only when the upstream declared no
|
|
179
|
+
// framing at all — that is the case where Node would otherwise guess
|
|
180
|
+
// and fall back to chunked for a body that never arrives.
|
|
181
|
+
//
|
|
182
|
+
// Do NOT synthesize it when the upstream stated a length. A HEAD
|
|
183
|
+
// response must carry the same headers a GET would (RFC 9110
|
|
184
|
+
// §9.3.2), so overwriting a real `content-length: 12345` with 0
|
|
185
|
+
// misreports the resource's size to the client.
|
|
186
|
+
//
|
|
187
|
+
// 204/304 must not carry content-length at all (RFC 9110 §6.4.1).
|
|
188
|
+
if (!neverHasBody && !declaresBody) {
|
|
189
|
+
headers["content-length"] = "0";
|
|
190
|
+
}
|
|
191
|
+
res.writeHead(status ?? 502, headers);
|
|
192
|
+
// Drain whatever the upstream sent regardless: QuestDB answers HEAD
|
|
193
|
+
// with a body under chunked framing, which is protocol-illegal, and
|
|
194
|
+
// leaving it unread holds the socket open.
|
|
136
195
|
proxied.resume();
|
|
137
196
|
res.end();
|
|
138
197
|
return;
|
|
139
198
|
}
|
|
199
|
+
res.writeHead(status ?? 502, headers);
|
|
200
|
+
// If the upstream dies mid-body, `pipe` alone never ends `res` — the
|
|
201
|
+
// browser sits on a response that will never complete. Node signals
|
|
202
|
+
// truncation to a direct client by aborting the socket; destroying our
|
|
203
|
+
// response does the same for ours, so a truncated console query fails
|
|
204
|
+
// visibly instead of hanging the tab.
|
|
205
|
+
proxied.on("aborted", () => {
|
|
206
|
+
deps.debug("console proxy: upstream aborted mid-response");
|
|
207
|
+
res.destroy();
|
|
208
|
+
});
|
|
209
|
+
proxied.on("error", (err) => {
|
|
210
|
+
deps.debug(`console proxy: upstream stream error: ${err.message}`);
|
|
211
|
+
res.destroy();
|
|
212
|
+
});
|
|
140
213
|
proxied.pipe(res);
|
|
141
214
|
});
|
|
142
215
|
upstream.on("error", (err) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"console-proxy.js","sourceRoot":"","sources":["../src/console-proxy.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"console-proxy.js","sourceRoot":"","sources":["../src/console-proxy.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -137,6 +137,33 @@ export default (app) => {
|
|
|
137
137
|
// (health-wait exhausted, ensureTables/connect threw) leaves them non-null
|
|
138
138
|
// on a plugin that never came up.
|
|
139
139
|
let pluginRunning = false;
|
|
140
|
+
// Summary of the startup restore, appended to the plugin status so the
|
|
141
|
+
// result is visible on the plugin card. Without this the only report was an
|
|
142
|
+
// app.debug() line, invisible unless DEBUG is set — so a user had no way to
|
|
143
|
+
// tell whether a feature whose entire point is "did it repopulate?" had run.
|
|
144
|
+
let restoreSummary = null;
|
|
145
|
+
// Structured form of the same result, served on /api/status. The status
|
|
146
|
+
// string above is kept because it is correct and will start working if
|
|
147
|
+
// Signal K's plugin.id/plugin.name status mismatch is ever fixed, but the
|
|
148
|
+
// config panel reads THIS — it is the only path that is visible today.
|
|
149
|
+
let restoreStatus = null;
|
|
150
|
+
const recordingStatus = (host, port) => `Recording to QuestDB at ${host}:${port}` +
|
|
151
|
+
(restoreSummary ? ` — ${restoreSummary}` : "");
|
|
152
|
+
// True while a writer/WAL failure is on the status line. Signal K keeps ONE
|
|
153
|
+
// status entry per plugin, so publishing "Recording…" over an active error
|
|
154
|
+
// hides it — and the restore result, which lands seconds after startup on a
|
|
155
|
+
// timer of its own, is exactly the kind of late writer that would do it.
|
|
156
|
+
let pluginErrorActive = false;
|
|
157
|
+
const publishError = (msg) => {
|
|
158
|
+
pluginErrorActive = true;
|
|
159
|
+
app.setPluginError(msg);
|
|
160
|
+
};
|
|
161
|
+
// Republish the recording line, but never at the cost of an active error.
|
|
162
|
+
const publishRecordingStatus = (host, port) => {
|
|
163
|
+
if (pluginErrorActive)
|
|
164
|
+
return;
|
|
165
|
+
app.setPluginStatus(recordingStatus(host, port));
|
|
166
|
+
};
|
|
140
167
|
// Contexts seen live while a startup restore is in flight. A stored fix
|
|
141
168
|
// must never overwrite a vessel that has already transmitted since boot —
|
|
142
169
|
// that would move it backwards on the chart. Populated by the recorder
|
|
@@ -249,6 +276,7 @@ export default (app) => {
|
|
|
249
276
|
const consoleProxy = createConsoleProxy({
|
|
250
277
|
baseUrl: () => (currentConfig ? questdbHttpBaseUrl() : null),
|
|
251
278
|
debug: (msg) => app.debug(msg),
|
|
279
|
+
mountPath: "/plugins/signalk-questdb/console",
|
|
252
280
|
});
|
|
253
281
|
/**
|
|
254
282
|
* Compute the bind-mount source for QuestDB's /var/lib/questdb volume.
|
|
@@ -575,7 +603,7 @@ export default (app) => {
|
|
|
575
603
|
const containers = globalThis.__signalk_containerManager;
|
|
576
604
|
if (!containers) {
|
|
577
605
|
app.debug("containerManager not found");
|
|
578
|
-
|
|
606
|
+
publishError("signalk-container plugin required for managed mode. Install it or set managedContainer=false.");
|
|
579
607
|
return;
|
|
580
608
|
}
|
|
581
609
|
app.setPluginStatus("Waiting for container runtime detection...");
|
|
@@ -586,7 +614,7 @@ export default (app) => {
|
|
|
586
614
|
return;
|
|
587
615
|
if (!containers.getRuntime()) {
|
|
588
616
|
app.debug("container runtime not detected");
|
|
589
|
-
|
|
617
|
+
publishError("No container runtime detected. Check signalk-container plugin.");
|
|
590
618
|
return;
|
|
591
619
|
}
|
|
592
620
|
void (async () => {
|
|
@@ -643,7 +671,7 @@ export default (app) => {
|
|
|
643
671
|
}
|
|
644
672
|
catch (err) {
|
|
645
673
|
app.debug("ensureRunning failed:", err);
|
|
646
|
-
|
|
674
|
+
publishError(`Failed to start QuestDB container: ${err instanceof Error ? err.message : String(err)}`);
|
|
647
675
|
return;
|
|
648
676
|
}
|
|
649
677
|
}
|
|
@@ -667,7 +695,7 @@ export default (app) => {
|
|
|
667
695
|
return;
|
|
668
696
|
}
|
|
669
697
|
if (!(await queryClient.isHealthy())) {
|
|
670
|
-
|
|
698
|
+
publishError(`QuestDB not responding at ${httpHost}:${httpPort}`);
|
|
671
699
|
return;
|
|
672
700
|
}
|
|
673
701
|
app.setPluginStatus("Creating tables...");
|
|
@@ -679,8 +707,13 @@ export default (app) => {
|
|
|
679
707
|
writer = new ILPWriter(ilpHost, ilpPort, (msg) => app.debug(msg), {
|
|
680
708
|
// Surface a flapping ILP connection instead of leaving the status line
|
|
681
709
|
// stuck on a cheerful "Recording" while every sample is dropped.
|
|
682
|
-
onUnhealthy: (msg) =>
|
|
683
|
-
onHealthy: () =>
|
|
710
|
+
onUnhealthy: (msg) => publishError(msg),
|
|
711
|
+
onHealthy: () => {
|
|
712
|
+
// The writer recovered: the error is no longer current, so the
|
|
713
|
+
// recording line may take the status entry back.
|
|
714
|
+
pluginErrorActive = false;
|
|
715
|
+
app.setPluginStatus(recordingStatus(ilpHost, ilpPort));
|
|
716
|
+
},
|
|
684
717
|
flushIntervalMs: config.ilpFlushIntervalMs,
|
|
685
718
|
});
|
|
686
719
|
await writer.connect();
|
|
@@ -846,7 +879,7 @@ export default (app) => {
|
|
|
846
879
|
const summary = tables
|
|
847
880
|
.map((t) => `${t.name} (${t.txnLag} txns behind)`)
|
|
848
881
|
.join(", ");
|
|
849
|
-
|
|
882
|
+
publishError(`QuestDB WAL suspended: ${summary}`);
|
|
850
883
|
const key = "alert:" +
|
|
851
884
|
tables
|
|
852
885
|
.map((t) => `${t.name}@${t.writerTxn}`)
|
|
@@ -862,7 +895,7 @@ export default (app) => {
|
|
|
862
895
|
},
|
|
863
896
|
onResolved: () => {
|
|
864
897
|
emitWalNotification("normal", "QuestDB history recording has recovered.", "normal:recovered");
|
|
865
|
-
app.setPluginStatus(
|
|
898
|
+
app.setPluginStatus(recordingStatus(ilpHost, ilpPort));
|
|
866
899
|
},
|
|
867
900
|
debug: (msg) => app.debug(msg),
|
|
868
901
|
error: (msg) => app.error(msg),
|
|
@@ -886,7 +919,7 @@ export default (app) => {
|
|
|
886
919
|
`/etc/sysctl.d/99-questdb.conf && sudo sysctl --system`);
|
|
887
920
|
}
|
|
888
921
|
}
|
|
889
|
-
app.setPluginStatus(
|
|
922
|
+
app.setPluginStatus(recordingStatus(ilpHost, ilpPort));
|
|
890
923
|
// Everything is registered — only now does the plugin count as running.
|
|
891
924
|
// Any earlier return/throw leaves the flag false, so a half-started
|
|
892
925
|
// plugin rejects lifecycle-dependent requests like /api/update/apply.
|
|
@@ -919,11 +952,41 @@ export default (app) => {
|
|
|
919
952
|
maxAgeMs: config.restoreMaxAgeMinutes * 60_000,
|
|
920
953
|
restoreSelf: config.recordSelf,
|
|
921
954
|
restoreOthers: config.recordOthers,
|
|
955
|
+
})
|
|
956
|
+
.then((result) => {
|
|
957
|
+
if (lifecycleGeneration !== restoreGeneration)
|
|
958
|
+
return;
|
|
959
|
+
restoreSummary =
|
|
960
|
+
result.contexts > 0
|
|
961
|
+
? `restored ${result.contexts} vessel${result.contexts === 1 ? "" : "s"} at startup`
|
|
962
|
+
: "no vessels to restore at startup";
|
|
963
|
+
restoreStatus = {
|
|
964
|
+
contexts: result.contexts,
|
|
965
|
+
skippedLive: result.skippedLive,
|
|
966
|
+
failed: false,
|
|
967
|
+
};
|
|
968
|
+
// Re-publish so the summary lands on the card even when the status
|
|
969
|
+
// was already set before the (slow) restore query finished.
|
|
970
|
+
if (questdbEndpoints) {
|
|
971
|
+
const { host, port } = questdbEndpoints.ilp;
|
|
972
|
+
publishRecordingStatus(host, port);
|
|
973
|
+
}
|
|
922
974
|
})
|
|
923
975
|
.catch((err) => {
|
|
924
976
|
// A failed restore is a cosmetic loss — targets still arrive live —
|
|
925
|
-
// so it must never take the plugin down with it.
|
|
926
|
-
|
|
977
|
+
// so it must never take the plugin down with it. Surfaced on the
|
|
978
|
+
// card rather than only in debug: silence is indistinguishable from
|
|
979
|
+
// "restore is off".
|
|
980
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
981
|
+
app.debug(`restore failed: ${message}`);
|
|
982
|
+
if (lifecycleGeneration !== restoreGeneration)
|
|
983
|
+
return;
|
|
984
|
+
restoreSummary = "startup restore failed";
|
|
985
|
+
restoreStatus = { contexts: 0, skippedLive: 0, failed: true };
|
|
986
|
+
if (questdbEndpoints) {
|
|
987
|
+
const { host, port } = questdbEndpoints.ilp;
|
|
988
|
+
publishRecordingStatus(host, port);
|
|
989
|
+
}
|
|
927
990
|
})
|
|
928
991
|
.finally(() => {
|
|
929
992
|
// Only needed to shield the restore; tracking every context for the
|
|
@@ -960,7 +1023,7 @@ export default (app) => {
|
|
|
960
1023
|
// Server does not await start(), so run async init in a
|
|
961
1024
|
// self-contained promise that handles its own errors.
|
|
962
1025
|
asyncStart(config).catch((err) => {
|
|
963
|
-
|
|
1026
|
+
publishError(`Startup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
964
1027
|
});
|
|
965
1028
|
},
|
|
966
1029
|
async stop() {
|
|
@@ -977,6 +1040,11 @@ export default (app) => {
|
|
|
977
1040
|
// armed, accumulating contexts for a restore that never happens.
|
|
978
1041
|
trackLiveContexts = false;
|
|
979
1042
|
liveContexts.clear();
|
|
1043
|
+
// Stale across a restart otherwise: the card would still claim a
|
|
1044
|
+
// restore count from the previous run.
|
|
1045
|
+
restoreSummary = null;
|
|
1046
|
+
restoreStatus = null;
|
|
1047
|
+
pluginErrorActive = false;
|
|
980
1048
|
for (const unsub of unsubscribes) {
|
|
981
1049
|
try {
|
|
982
1050
|
unsub();
|
|
@@ -1176,6 +1244,7 @@ export default (app) => {
|
|
|
1176
1244
|
endpoint: questdbEndpoints
|
|
1177
1245
|
? `${questdbEndpoints.http.host}:${questdbEndpoints.http.port}`
|
|
1178
1246
|
: null,
|
|
1247
|
+
restore: restoreStatus,
|
|
1179
1248
|
});
|
|
1180
1249
|
}
|
|
1181
1250
|
catch (err) {
|