signalk-questdb 1.7.1 → 1.8.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/CLAUDE.md +4 -0
- package/README.md +34 -1
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +9 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/console-proxy.d.ts +33 -0
- package/dist/console-proxy.js +162 -0
- package/dist/console-proxy.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -1
- package/dist/test/config.test.js +19 -0
- package/dist/test/config.test.js.map +1 -1
- package/dist/test/console-proxy.test.d.ts +1 -0
- package/dist/test/console-proxy.test.js +337 -0
- package/dist/test/console-proxy.test.js.map +1 -0
- package/dist/test/package-e2e.test.js +22 -6
- package/dist/test/package-e2e.test.js.map +1 -1
- package/package.json +2 -1
- package/public/assets/{PluginConfigurationPanel-BiADSRcq.js → PluginConfigurationPanel-w6DPiiey.js} +7 -7
- package/public/assets/{index-Cv0SCFTN.js → index-3YxZffm5.js} +1 -1
- package/public/assets/{virtualExposes-BT6Pf_MF.js → virtualExposes-CzZANPwy.js} +1 -1
- package/public/index.html +96 -0
- package/public/remoteEntry.js +1 -1
package/CLAUDE.md
CHANGED
|
@@ -36,6 +36,8 @@ Both build outputs are shipped in the npm package; `prepublishOnly` rebuilds the
|
|
|
36
36
|
- **v2** (`src/history-v2.ts`) — `app.registerHistoryApiProvider()`, REST under `/signalk/v2/api/history/`. Aggregates map directly to QuestDB SQL except `sma`/`ema` (computed client-side).
|
|
37
37
|
- **v1** (`src/history-v1.ts`) — `app.registerHistoryProvider()`, WebSocket playback with chunked reads.
|
|
38
38
|
- Retention (`src/retention.ts`) drops old daily partitions on a timer.
|
|
39
|
+
- Startup restore (`src/restore.ts`) optionally replays each vessel's last recorded position back into the live model after a server restart.
|
|
40
|
+
- Console proxy (`src/console-proxy.ts`) reverse-proxies QuestDB's own web console so the embeddable webapp (`public/index.html`) can host it in the admin UI.
|
|
39
41
|
|
|
40
42
|
### Container integration
|
|
41
43
|
|
|
@@ -53,6 +55,8 @@ The package is the unscoped `typebox` (1.x), not `@sinclair/typebox` (which stop
|
|
|
53
55
|
|
|
54
56
|
All extra plugin endpoints live under `/plugins/signalk-questdb/api/` and are wired in `src/index.ts` via the `IRouter` Signal K passes to `registerWithRouter`. The `/query` endpoint is gated by `isReadOnlySQL` in `src/query-client.ts` — DDL/DML must remain blocked.
|
|
55
57
|
|
|
58
|
+
⚠️ **`/console` is deliberately NOT gated that way.** It proxies QuestDB's own console, which is a full SQL client and can drop tables. It is safe only because signalk-server gives plugin routes an **admin-only default**, and routes are downgraded to readwrite/readonly _only_ by registering them through `router.access(...)`. Never wrap the console mount in `.access()` — doing so hands non-admins the ability to delete recorded history. The mount is also gated on the `enableConsole` config flag, so turning it off removes the route rather than leaving it to refuse.
|
|
59
|
+
|
|
56
60
|
Their wire shapes live in `src/api-contract.ts` and are shared by both surfaces: handlers assert responses with `satisfies`, and the panel casts fetch results to the same types. Add or rename a response field there, not inline — that is what stops the server and panel drifting apart silently.
|
|
57
61
|
|
|
58
62
|
### Tests
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Stores all vessel data in QuestDB running as a managed container (via [signalk-c
|
|
|
20
20
|
- **CSV export** -- download historical data via REST endpoint
|
|
21
21
|
- **InfluxDB migration** -- auto-detect InfluxDB 1.x/2.x on localhost or remote URL
|
|
22
22
|
- **One-click updates** -- check for new QuestDB releases and update from the config panel
|
|
23
|
+
- **Console webapp** -- QuestDB's own SQL console embedded in the Signal K admin UI (admin only)
|
|
23
24
|
- **Config panel** -- status dashboard with row counts, version picker, update check, collapsible compression/migration/export sections
|
|
24
25
|
- **SQL injection protection** -- strict input validation on all query endpoints
|
|
25
26
|
- **Container lifecycle** -- container stops when plugin is disabled, starts on enable
|
|
@@ -32,7 +33,7 @@ The plugin embeds a React config panel in the Signal K Admin UI showing:
|
|
|
32
33
|
- **Update check** -- compares running version against latest GitHub release, one-click update
|
|
33
34
|
- **Image Version** -- dropdown with latest, pre-releases, and last 3 stable releases
|
|
34
35
|
- **Connection** -- managed container toggle, host/ports, PostgreSQL port for Grafana
|
|
35
|
-
- **Recording** -- record self, record AIS targets, retention days
|
|
36
|
+
- **Recording** -- record self, record AIS targets, startup restore, console webapp, retention days
|
|
36
37
|
- **Path filtering** (collapsible) -- exclude or include-only paths with glob patterns (e.g. exclude `navigation.position`)
|
|
37
38
|
- **Compression** (collapsible) -- LZ4/ZSTD codec selection for on-disk storage
|
|
38
39
|
- **InfluxDB Migration** (collapsible) -- auto-detect with manual URL for remote instances
|
|
@@ -129,6 +130,7 @@ wants to slice it into kopia-dedup-friendly shards. Allowed tables:
|
|
|
129
130
|
| Record AIS targets | `true` | Record other vessels |
|
|
130
131
|
| Restore vessels on startup | `false` | Replay each vessel's last recorded position after a restart (see Startup restore) |
|
|
131
132
|
| Restore max age (minutes) | `9` | Only replay values recorded within this window |
|
|
133
|
+
| QuestDB console webapp | `true` | Serve QuestDB's console in the Signal K admin UI, admin only (see Console webapp) |
|
|
132
134
|
| Retention (days) | `0` | Auto-delete old partitions (0 = keep forever) |
|
|
133
135
|
| Path filter mode | `exclude` | `exclude` matching paths, or `include` only matching paths |
|
|
134
136
|
| Path filter paths | _(empty)_ | Glob patterns, one per line (e.g. `navigation.position`); empty = record everything |
|
|
@@ -137,6 +139,37 @@ wants to slice it into kopia-dedup-friendly shards. Allowed tables:
|
|
|
137
139
|
| Container network | `sk-network` | Shared network for QuestDB (only applied when binding to 0.0.0.0) |
|
|
138
140
|
| Bind to 0.0.0.0 | `false` | Expose QuestDB's ports on the LAN (see Connectivity below) |
|
|
139
141
|
|
|
142
|
+
## Console webapp
|
|
143
|
+
|
|
144
|
+
QuestDB ships its own web console — a full SQL workbench with schema browsing,
|
|
145
|
+
query history and CSV import. It normally lives on QuestDB's HTTP port (9000),
|
|
146
|
+
which is bound to loopback unless you turn on **Bind to 0.0.0.0**, so from any
|
|
147
|
+
other device on the boat it is effectively unreachable.
|
|
148
|
+
|
|
149
|
+
With **QuestDB console webapp** enabled (the default) the plugin appears in the
|
|
150
|
+
Signal K webapp list and serves that console inside the admin UI, with Signal
|
|
151
|
+
K's own navigation panel still in place.
|
|
152
|
+
|
|
153
|
+
**It is admin only.** The console is proxied at
|
|
154
|
+
`/plugins/signalk-questdb/console/`, and Signal K gives plugin routes an
|
|
155
|
+
admin-only default — the plugin deliberately does not relax that. A non-admin
|
|
156
|
+
who opens the webapp sees the page frame and an authorization error, never the
|
|
157
|
+
console.
|
|
158
|
+
|
|
159
|
+
**It can modify data.** Unlike the read-only `/query` endpoint, which rejects
|
|
160
|
+
anything that is not a `SELECT`, the console is a real SQL client: it can drop
|
|
161
|
+
tables and delete recorded history. That is the point of having it, and it is
|
|
162
|
+
what an administrator would use it for — but it is worth knowing before handing
|
|
163
|
+
someone an admin account.
|
|
164
|
+
|
|
165
|
+
If you would rather not serve it at all, switch the option off and the route is
|
|
166
|
+
not registered.
|
|
167
|
+
|
|
168
|
+
> Note this is _safer_ than the alternative it replaces. Reaching the console
|
|
169
|
+
> from another device previously meant enabling **Bind to 0.0.0.0**, which
|
|
170
|
+
> publishes an unauthenticated SQL endpoint on your network. The webapp route
|
|
171
|
+
> requires a Signal K admin session.
|
|
172
|
+
|
|
140
173
|
## Startup restore
|
|
141
174
|
|
|
142
175
|
Signal K's data model lives in memory only. After a server restart every vessel
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const ConfigSchema: Type.TObject<{
|
|
|
19
19
|
recordOthers: Type.TBoolean;
|
|
20
20
|
restoreOnStart: Type.TBoolean;
|
|
21
21
|
restoreMaxAgeMinutes: Type.TNumber;
|
|
22
|
+
enableConsole: Type.TBoolean;
|
|
22
23
|
retentionDays: Type.TNumber;
|
|
23
24
|
networkName: Type.TString;
|
|
24
25
|
exposeToContainers: Type.TBoolean;
|
package/dist/config/schema.js
CHANGED
|
@@ -86,6 +86,11 @@ export const ConfigSchema = Type.Object({
|
|
|
86
86
|
title: "Restore max age (minutes)",
|
|
87
87
|
description: "Only replay values recorded within this many minutes. Matches Freeboard's default AIS expiry (9 minutes), so a restored target is one that would still have been on the chart. Raising this puts progressively staler positions on the chart.",
|
|
88
88
|
}),
|
|
89
|
+
enableConsole: Type.Boolean({
|
|
90
|
+
default: true,
|
|
91
|
+
title: "QuestDB console webapp",
|
|
92
|
+
description: "Serve QuestDB's own web console inside the Signal K admin UI (admin only). The console is a full SQL client — unlike the read-only query endpoint it can modify and delete recorded data. Turn this off to remove the route entirely.",
|
|
93
|
+
}),
|
|
89
94
|
retentionDays: Type.Number({
|
|
90
95
|
default: 0,
|
|
91
96
|
title: "Retention (days, 0 = keep forever)",
|
|
@@ -142,6 +147,10 @@ export function normalizeConfig(config) {
|
|
|
142
147
|
restoreMaxAgeMinutes: config.restoreMaxAgeMinutes && config.restoreMaxAgeMinutes > 0
|
|
143
148
|
? config.restoreMaxAgeMinutes
|
|
144
149
|
: 9,
|
|
150
|
+
// On unless explicitly disabled, matching the recording toggles rather
|
|
151
|
+
// than restoreOnStart: the console only ever does what an authenticated
|
|
152
|
+
// admin types, so a missing key should not hide the tool.
|
|
153
|
+
enableConsole: config.enableConsole !== false,
|
|
145
154
|
// Missing resource limits get the schema defaults. Config-panel saves
|
|
146
155
|
// used to drop these keys entirely (issue #98), which ran QuestDB with
|
|
147
156
|
// NO memory cap instead of the promised 768m — this backfill repairs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Reverse proxy for QuestDB's own web console.
|
|
4
|
+
*
|
|
5
|
+
* The console is a separate app served by the QuestDB container on its HTTP
|
|
6
|
+
* port. Rather than rebuild it, this forwards requests to it so the Signal K
|
|
7
|
+
* admin UI can embed it — keeping Signal K's nav panel in place instead of
|
|
8
|
+
* sending the user off to `:9000`, which is unreachable from another device
|
|
9
|
+
* unless the operator publishes the port on the LAN.
|
|
10
|
+
*
|
|
11
|
+
* Plain `node:http` piping rather than a proxy library: the plugin's dependency
|
|
12
|
+
* tree is deliberately lean, and what is needed here (method, path, query,
|
|
13
|
+
* headers, body, streamed response) is a few lines.
|
|
14
|
+
*
|
|
15
|
+
* Path handling is why this works without patching QuestDB's bundle: the
|
|
16
|
+
* console requests its assets and its API relatively (`./assets/…`, `exec?…`),
|
|
17
|
+
* so serving it under a prefix needs no rewriting. If a future QuestDB image
|
|
18
|
+
* switches to absolute paths that breaks visibly as 404s — see the plan's
|
|
19
|
+
* manual-verification step — rather than silently.
|
|
20
|
+
*
|
|
21
|
+
* SECURITY: the console is a full SQL client. Unlike `/api/query`, which is
|
|
22
|
+
* gated by isReadOnlySQL, it can drop tables. It is reachable only because
|
|
23
|
+
* routes registered directly on a plugin router are admin-only by default in
|
|
24
|
+
* signalk-server. Do NOT register the mount through `router.access(...)` —
|
|
25
|
+
* that would downgrade it to readwrite/readonly and hand non-admins a way to
|
|
26
|
+
* delete the vessel's recorded history.
|
|
27
|
+
*/
|
|
28
|
+
export interface ConsoleProxyDeps {
|
|
29
|
+
/** Base URL of QuestDB's HTTP interface, e.g. `http://127.0.0.1:9000`. */
|
|
30
|
+
baseUrl: () => string | null;
|
|
31
|
+
debug: (msg: string) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function createConsoleProxy(deps: ConsoleProxyDeps): (req: IncomingMessage, res: ServerResponse) => void;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
// Hop-by-hop headers must not be forwarded (RFC 7230 §6.1). `host` is dropped
|
|
3
|
+
// separately because it has to name the upstream, not the Signal K server.
|
|
4
|
+
const HOP_BY_HOP = new Set([
|
|
5
|
+
"connection",
|
|
6
|
+
"keep-alive",
|
|
7
|
+
"proxy-authenticate",
|
|
8
|
+
"proxy-authorization",
|
|
9
|
+
"te",
|
|
10
|
+
"trailer",
|
|
11
|
+
"transfer-encoding",
|
|
12
|
+
"upgrade",
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Header names `Connection` nominates for removal. RFC 7230 §6.1 lets a hop
|
|
16
|
+
* list additional single-hop headers there, so the fixed set above is not the
|
|
17
|
+
* whole story — forwarding a nominated header leaks connection-scoped state
|
|
18
|
+
* across the hop.
|
|
19
|
+
*/
|
|
20
|
+
function connectionNominated(headers) {
|
|
21
|
+
const raw = Array.isArray(headers.connection)
|
|
22
|
+
? headers.connection.join(",")
|
|
23
|
+
: (headers.connection ?? "");
|
|
24
|
+
return new Set(raw
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((v) => v.trim().toLowerCase())
|
|
27
|
+
.filter(Boolean));
|
|
28
|
+
}
|
|
29
|
+
function stripHopByHop(headers) {
|
|
30
|
+
const nominated = connectionNominated(headers);
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
continue;
|
|
35
|
+
const lower = key.toLowerCase();
|
|
36
|
+
if (HOP_BY_HOP.has(lower) || nominated.has(lower))
|
|
37
|
+
continue;
|
|
38
|
+
out[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function forwardableHeaders(headers) {
|
|
43
|
+
const out = stripHopByHop(headers);
|
|
44
|
+
// `host` must name the upstream, not the Signal K server.
|
|
45
|
+
delete out.host;
|
|
46
|
+
// Signal K's session cookie is not QuestDB's business, and forwarding it
|
|
47
|
+
// would leak an admin credential to a process that has no use for it.
|
|
48
|
+
delete out.cookie;
|
|
49
|
+
delete out.authorization;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Response headers must be filtered too. The upstream's own hop-by-hop
|
|
54
|
+
* headers describe ITS connection to us, not ours to the browser; relaying
|
|
55
|
+
* them lets QuestDB's framing decisions override the ones Node is making for
|
|
56
|
+
* the client's connection.
|
|
57
|
+
*/
|
|
58
|
+
function returnableHeaders(headers) {
|
|
59
|
+
const out = stripHopByHop(headers);
|
|
60
|
+
// The console shares the Signal K origin, so a proxied Set-Cookie would be
|
|
61
|
+
// applied to it — letting a misconfigured or compromised QuestDB write
|
|
62
|
+
// cookies that collide with Signal K's own session handling. Nothing is
|
|
63
|
+
// lost by dropping it: the request side already strips `cookie`, so no
|
|
64
|
+
// QuestDB cookie could ever have completed a round trip anyway.
|
|
65
|
+
delete out["set-cookie"];
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
export function createConsoleProxy(deps) {
|
|
69
|
+
return function consoleProxy(req, res) {
|
|
70
|
+
const base = deps.baseUrl();
|
|
71
|
+
if (!base) {
|
|
72
|
+
res.writeHead(503, { "content-type": "text/plain" });
|
|
73
|
+
res.end("QuestDB is not running.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let target;
|
|
77
|
+
try {
|
|
78
|
+
// `req.url` here is already relative to the mount point (Express strips
|
|
79
|
+
// the matched prefix for `router.use`), so "/" is the console root.
|
|
80
|
+
target = new URL(req.url ?? "/", base);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
84
|
+
res.end("Bad request path.");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Confine the proxy to the resolved QuestDB origin. `new URL` would
|
|
88
|
+
// otherwise let a path like `//evil.example.com/` re-target the request,
|
|
89
|
+
// turning an admin-only route into an open relay.
|
|
90
|
+
const origin = new URL(base);
|
|
91
|
+
if (target.origin !== origin.origin) {
|
|
92
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
93
|
+
res.end("Bad request path.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const upstream = http.request({
|
|
97
|
+
protocol: origin.protocol,
|
|
98
|
+
hostname: origin.hostname,
|
|
99
|
+
port: origin.port,
|
|
100
|
+
method: req.method,
|
|
101
|
+
path: target.pathname + target.search,
|
|
102
|
+
headers: forwardableHeaders(req.headers),
|
|
103
|
+
}, (proxied) => {
|
|
104
|
+
const headers = returnableHeaders(proxied.headers);
|
|
105
|
+
// The console is served under the Signal K origin, so its scripts run
|
|
106
|
+
// with the admin's session. Signal K's auth cookie is httpOnly, so
|
|
107
|
+
// script cannot read it — but it IS attached to same-origin requests,
|
|
108
|
+
// which would let a tampered console drive Signal K's admin API as the
|
|
109
|
+
// logged-in user. A plugin cannot mint a separate origin, so narrow
|
|
110
|
+
// what the console may do instead.
|
|
111
|
+
//
|
|
112
|
+
// Deliberately NOT `connect-src 'self'`: the console's SQL editor is
|
|
113
|
+
// Monaco, and the bundle carries a jsdelivr loader path for it. Static
|
|
114
|
+
// analysis could not settle whether that path is actually taken, and
|
|
115
|
+
// shipping a policy that blanks out the query editor would be a worse
|
|
116
|
+
// outcome than the risk it mitigates. `form-action` and `base-uri`
|
|
117
|
+
// still block the two cheapest ways to redirect an admin's submission
|
|
118
|
+
// or rewrite relative URLs, and `frame-ancestors` blocks clickjacking
|
|
119
|
+
// from another site.
|
|
120
|
+
//
|
|
121
|
+
// Revisit if QuestDB ever bundles Monaco locally — then `connect-src`
|
|
122
|
+
// and `default-src 'self'` become safe to tighten, which is the real
|
|
123
|
+
// fix for a same-origin console.
|
|
124
|
+
headers["content-security-policy"] =
|
|
125
|
+
"form-action 'self'; frame-ancestors 'self'; base-uri 'self'";
|
|
126
|
+
// The console has no reason to be sniffed into a different type.
|
|
127
|
+
headers["x-content-type-options"] = "nosniff";
|
|
128
|
+
res.writeHead(proxied.statusCode ?? 502, headers);
|
|
129
|
+
// A HEAD response carries no body by definition. QuestDB nonetheless
|
|
130
|
+
// answers HEAD with `405 Transfer-Encoding: chunked` AND a body, which
|
|
131
|
+
// is protocol-illegal; piping that upstream response through leaves
|
|
132
|
+
// the client waiting for chunks that never legally arrive and the
|
|
133
|
+
// connection dies with an empty reply. Terminate the response
|
|
134
|
+
// ourselves and discard whatever the upstream sent.
|
|
135
|
+
if (req.method === "HEAD") {
|
|
136
|
+
proxied.resume();
|
|
137
|
+
res.end();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
proxied.pipe(res);
|
|
141
|
+
});
|
|
142
|
+
upstream.on("error", (err) => {
|
|
143
|
+
deps.debug(`console proxy error: ${err.message}`);
|
|
144
|
+
if (res.headersSent) {
|
|
145
|
+
res.destroy();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// The overwhelmingly common cause is the container being stopped, so say
|
|
149
|
+
// that rather than surfacing an ECONNREFUSED stack to the browser.
|
|
150
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
151
|
+
res.end("QuestDB is not reachable. Is the container running?");
|
|
152
|
+
});
|
|
153
|
+
// A client that goes away mid-request must not leave the upstream socket
|
|
154
|
+
// open — the console streams large query results.
|
|
155
|
+
res.on("close", () => {
|
|
156
|
+
if (!upstream.destroyed)
|
|
157
|
+
upstream.destroy();
|
|
158
|
+
});
|
|
159
|
+
req.pipe(upstream);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=console-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-proxy.js","sourceRoot":"","sources":["../src/console-proxy.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ declare const _default: (app: App) => {
|
|
|
42
42
|
recordOthers: import("typebox").TBoolean;
|
|
43
43
|
restoreOnStart: import("typebox").TBoolean;
|
|
44
44
|
restoreMaxAgeMinutes: import("typebox").TNumber;
|
|
45
|
+
enableConsole: import("typebox").TBoolean;
|
|
45
46
|
retentionDays: import("typebox").TNumber;
|
|
46
47
|
networkName: import("typebox").TString;
|
|
47
48
|
exposeToContainers: import("typebox").TBoolean;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createHistoryProviderV2 } from "./history-v2.js";
|
|
|
6
6
|
import { createHistoryProviderV1 } from "./history-v1.js";
|
|
7
7
|
import { startRetention } from "./retention.js";
|
|
8
8
|
import { RESTORE_SOURCE, restoreFromHistory } from "./restore.js";
|
|
9
|
+
import { createConsoleProxy } from "./console-proxy.js";
|
|
9
10
|
import { buildFullExportWhere } from "./full-export-range.js";
|
|
10
11
|
import { WalMonitor, buildPendingSegmentsSQL, computeSkipPlan, extractApplyError, isPartitionOpenFailure, skipPlansEqual, } from "./wal-monitor.js";
|
|
11
12
|
import { QUESTDB_INTERNAL_HTTP_PORT, QUESTDB_INTERNAL_ILP_PORT, QUESTDB_INTERNAL_PG_PORT, QUESTDB_ACCESSIBLE_PORTS, resolveManagedEndpoints, resolveLanExposureHost, lanExposureEndpoints, } from "./questdb-endpoint.js";
|
|
@@ -236,6 +237,19 @@ export default (app) => {
|
|
|
236
237
|
const port = currentConfig?.questdbHttpPort ?? QUESTDB_INTERNAL_HTTP_PORT;
|
|
237
238
|
return `http://${host}:${port}`;
|
|
238
239
|
}
|
|
240
|
+
// Built once; it resolves the upstream lazily on every request so a
|
|
241
|
+
// container restart (which can change the resolved address) is picked up
|
|
242
|
+
// without re-registering the route.
|
|
243
|
+
//
|
|
244
|
+
// Gated on config rather than on `questdbEndpoints`: that is only populated
|
|
245
|
+
// on the managed-container path, so keying off it made the console return
|
|
246
|
+
// 503 forever in external mode, where the host/port come from config and
|
|
247
|
+
// questdbHttpBaseUrl() already falls back to them. Null only before any
|
|
248
|
+
// config has loaded, which is the one moment there is nothing to point at.
|
|
249
|
+
const consoleProxy = createConsoleProxy({
|
|
250
|
+
baseUrl: () => (currentConfig ? questdbHttpBaseUrl() : null),
|
|
251
|
+
debug: (msg) => app.debug(msg),
|
|
252
|
+
});
|
|
239
253
|
/**
|
|
240
254
|
* Compute the bind-mount source for QuestDB's /var/lib/questdb volume.
|
|
241
255
|
*
|
|
@@ -1016,6 +1030,27 @@ export default (app) => {
|
|
|
1016
1030
|
currentConfig = null;
|
|
1017
1031
|
},
|
|
1018
1032
|
registerWithRouter(router) {
|
|
1033
|
+
// QuestDB's own console, proxied so the Signal K admin UI can embed it.
|
|
1034
|
+
//
|
|
1035
|
+
// Registered DIRECTLY on the router and NOT through router.access(...):
|
|
1036
|
+
// plugin routes default to admin-only in signalk-server, and only
|
|
1037
|
+
// .access() downgrades them. The console is a full SQL client that can
|
|
1038
|
+
// drop tables, so downgrading it would hand a non-admin the ability to
|
|
1039
|
+
// delete the vessel's recorded history. Leave this as it is.
|
|
1040
|
+
//
|
|
1041
|
+
// The enableConsole check lives INSIDE the handler, not around the
|
|
1042
|
+
// mount: registerWithRouter runs at plugin registration, before start()
|
|
1043
|
+
// has loaded any configuration, so `currentConfig` is still null here
|
|
1044
|
+
// and gating the mount would ignore a saved opt-out entirely. Checking
|
|
1045
|
+
// per-request also means toggling the option takes effect on plugin
|
|
1046
|
+
// restart without the route going stale.
|
|
1047
|
+
router.use("/console", (req, res, next) => {
|
|
1048
|
+
if (currentConfig?.enableConsole === false) {
|
|
1049
|
+
next();
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
consoleProxy(req, res);
|
|
1053
|
+
});
|
|
1019
1054
|
router.get("/api/status", async (_req, res) => {
|
|
1020
1055
|
try {
|
|
1021
1056
|
if (!queryClient) {
|