signalk-questdb 1.6.1 → 1.7.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/.prettierignore +2 -0
- package/CLAUDE.md +20 -8
- package/README.md +54 -21
- package/dist/api-contract.d.ts +120 -0
- package/dist/api-contract.js +12 -0
- package/dist/api-contract.js.map +1 -0
- package/dist/config/schema.d.ts +25 -23
- package/dist/config/schema.js +43 -28
- package/dist/config/schema.js.map +1 -1
- package/dist/configpanel/responses.d.ts +18 -0
- package/dist/configpanel/responses.js +51 -0
- package/dist/configpanel/responses.js.map +1 -0
- package/dist/container-env.d.ts +1 -1
- package/dist/container-env.js +3 -7
- package/dist/container-env.js.map +1 -1
- package/dist/delta-routing.js +2 -6
- package/dist/delta-routing.js.map +1 -1
- package/dist/full-export-range.js +1 -4
- package/dist/full-export-range.js.map +1 -1
- package/dist/history-v1.d.ts +1 -1
- package/dist/history-v1.js +7 -10
- package/dist/history-v1.js.map +1 -1
- package/dist/history-v2.d.ts +1 -1
- package/dist/history-v2.js +11 -14
- package/dist/history-v2.js.map +1 -1
- package/dist/host-limits.js +9 -16
- package/dist/host-limits.js.map +1 -1
- package/dist/ilp-writer.js +4 -41
- package/dist/ilp-writer.js.map +1 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +144 -93
- package/dist/index.js.map +1 -1
- package/dist/path-matcher.js +6 -12
- package/dist/path-matcher.js.map +1 -1
- package/dist/query-client.js +4 -11
- package/dist/query-client.js.map +1 -1
- package/dist/questdb-endpoint.js +18 -26
- package/dist/questdb-endpoint.js.map +1 -1
- package/dist/restore.d.ts +56 -0
- package/dist/restore.js +212 -0
- package/dist/restore.js.map +1 -0
- package/dist/retention.d.ts +1 -1
- package/dist/retention.js +1 -4
- package/dist/retention.js.map +1 -1
- package/dist/test/config.test.js +79 -48
- package/dist/test/config.test.js.map +1 -1
- package/dist/test/configpanel-responses.test.d.ts +1 -0
- package/dist/test/configpanel-responses.test.js +171 -0
- package/dist/test/configpanel-responses.test.js.map +1 -0
- package/dist/test/container-env.test.js +38 -43
- package/dist/test/container-env.test.js.map +1 -1
- package/dist/test/delta-routing.test.js +37 -42
- package/dist/test/delta-routing.test.js.map +1 -1
- package/dist/test/full-export-range.test.js +45 -50
- package/dist/test/full-export-range.test.js.map +1 -1
- package/dist/test/history-v1.test.js +82 -87
- package/dist/test/history-v1.test.js.map +1 -1
- package/dist/test/history-v2.test.js +95 -100
- package/dist/test/history-v2.test.js.map +1 -1
- package/dist/test/host-limits.test.js +55 -60
- package/dist/test/host-limits.test.js.map +1 -1
- package/dist/test/ilp-writer.test.js +59 -97
- package/dist/test/ilp-writer.test.js.map +1 -1
- package/dist/test/package-e2e.test.d.ts +1 -0
- package/dist/test/package-e2e.test.js +267 -0
- package/dist/test/package-e2e.test.js.map +1 -0
- package/dist/test/path-matcher.test.js +104 -109
- package/dist/test/path-matcher.test.js.map +1 -1
- package/dist/test/query-client.test.js +96 -134
- package/dist/test/query-client.test.js.map +1 -1
- package/dist/test/questdb-endpoint.test.js +103 -108
- package/dist/test/questdb-endpoint.test.js.map +1 -1
- package/dist/test/restore.test.d.ts +1 -0
- package/dist/test/restore.test.js +389 -0
- package/dist/test/restore.test.js.map +1 -0
- package/dist/test/time-range.test.js +38 -43
- package/dist/test/time-range.test.js.map +1 -1
- package/dist/test/wal-monitor.test.js +131 -136
- package/dist/test/wal-monitor.test.js.map +1 -1
- package/dist/time-range.js +6 -10
- package/dist/time-range.js.map +1 -1
- package/dist/wal-monitor.js +6 -15
- package/dist/wal-monitor.js.map +1 -1
- package/package.json +13 -10
- package/public/assets/PluginConfigurationPanel-BiADSRcq.js +10 -0
- package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-CVM43rXb.js +1 -0
- package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__1__loadShare__react__loadShare__.js-DzhF4wXp.js +1 -0
- package/public/assets/configpanel-BvRk9kiK.js +0 -0
- package/public/assets/dist-BxwXn66h.js +4 -0
- package/public/assets/hostInit-BK1oa2Z9.js +1 -0
- package/public/assets/index-Cv0SCFTN.js +1 -0
- package/public/assets/virtualExposes-BT6Pf_MF.js +1 -0
- package/public/assets/vite-preload-helper-HclGiUj8.js +1 -0
- package/public/remoteEntry.js +1 -1
- package/public/540.js +0 -2
- package/public/540.js.LICENSE.txt +0 -9
- package/public/805.js +0 -1
- package/public/main.js +0 -1
- package/webpack.config.js +0 -52
package/.prettierignore
CHANGED
package/CLAUDE.md
CHANGED
|
@@ -4,8 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Commands
|
|
6
6
|
|
|
7
|
-
- `npm run build` — TypeScript compile (`tsc`) +
|
|
8
|
-
- `npm run build:config` —
|
|
7
|
+
- `npm run build` — TypeScript compile (`tsc`) + panel typecheck + Vite bundle of the React config panel into `public/`
|
|
8
|
+
- `npm run build:config` — Vite only (rebuild the config panel without recompiling TS)
|
|
9
|
+
- `npm run typecheck:panel` — typecheck the config panel (`tsc -p src/configpanel --noEmit`); Vite only strips its types, so this is the only thing that checks them
|
|
9
10
|
- `npm run watch` — `tsc --watch` for the plugin source
|
|
10
11
|
- `npm test` — runs the Node built-in test runner against compiled output (`node --test 'dist/test/**/*.test.js'`). Tests must be built first; use `npm run build:all` to build then test.
|
|
11
12
|
- `npm run format` — prettier write + eslint --fix
|
|
@@ -20,8 +21,10 @@ This is a Signal K **server plugin** that ingests vessel data into a managed **Q
|
|
|
20
21
|
|
|
21
22
|
### Two surfaces
|
|
22
23
|
|
|
23
|
-
1. **Plugin runtime** (`src/index.ts` and siblings) — compiled by `tsc` to `dist/`, loaded by Signal K server as `main` (`dist/index.js`).
|
|
24
|
-
2. **React config panel** (`src/configpanel/`) — bundled by
|
|
24
|
+
1. **Plugin runtime** (`src/index.ts` and siblings) — compiled by `tsc` to `dist/`, loaded by Signal K server as `main` (`dist/index.js`). The package is **ESM** (`"type": "module"`): `src/index.ts` ends in `export default`, and Signal K's `importOrRequire()` unwraps it. Node ≥ 20.19 can `require()` an ESM module, so this works on both supported Node versions.
|
|
25
|
+
2. **React config panel** (`src/configpanel/`) — TypeScript + JSX, bundled by **Vite** (`vite.config.ts`) via **Module Federation** into `public/remoteEntry.js`, exposed to the Signal K Admin UI as `./PluginConfigurationPanel`. React 19 is shared as a singleton with the host UI. It has its own `tsconfig.json` (browser libs, classic JSX, `noEmit`) so DOM types never reach the Node compile.
|
|
26
|
+
|
|
27
|
+
Because the package is `"type": "module"`, the server injects the panel's script tag with `type="module"`, and the Admin UI then loads it as an **ESM remote**: `await import(remoteEntry.js)` expecting exported `get`/`init` (see the admin UI's `views/Webapps/dynamicutilities.ts`). Vite's federation output provides exactly that.
|
|
25
28
|
|
|
26
29
|
Both build outputs are shipped in the npm package; `prepublishOnly` rebuilds them.
|
|
27
30
|
|
|
@@ -42,19 +45,28 @@ When `managedContainer` is false, the plugin connects to an external QuestDB at
|
|
|
42
45
|
|
|
43
46
|
### Config schema
|
|
44
47
|
|
|
45
|
-
`src/config/schema.ts` uses **TypeBox**
|
|
48
|
+
`src/config/schema.ts` uses **TypeBox** — the schema is the single source of truth for the JSON-schema Signal K shows in the Admin UI **and** for the `Config` TS type. Add new options there.
|
|
49
|
+
|
|
50
|
+
The package is the unscoped `typebox` (1.x), not `@sinclair/typebox` (which stopped at 0.34). TypeBox 1 was published under a new name rather than a major bump, so `npm outdated` will never point at it.
|
|
46
51
|
|
|
47
52
|
### REST endpoints
|
|
48
53
|
|
|
49
54
|
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.
|
|
50
55
|
|
|
56
|
+
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
|
+
|
|
51
58
|
### Tests
|
|
52
59
|
|
|
53
60
|
`src/test/` contains `node:test` suites for ILP encoding, time range parsing, query building, and v2 history. They run against the **compiled** JS in `dist/test/`, so always build before testing.
|
|
54
61
|
|
|
55
62
|
## Conventions
|
|
56
63
|
|
|
57
|
-
- TypeScript strict mode; do not loosen `tsconfig.json`.
|
|
58
|
-
-
|
|
59
|
-
-
|
|
64
|
+
- TypeScript strict mode everywhere, panel included; do not loosen either `tsconfig.json`. In particular never add `jsx` or a DOM `lib` to the root one — server code referencing `document` would then typecheck clean.
|
|
65
|
+
- **Relative imports in the runtime need the `.js` extension** (`./query-client.js`, even though the source is `.ts`). That is Node's ESM rule, enforced by `moduleResolution: "nodenext"`. Omitting it fails the build, which is the point — switching to `"bundler"` resolution would silently emit unresolvable specifiers instead.
|
|
66
|
+
- **`.npmignore` excludes config files by exact filename** — rename one and the stale entry silently publishes it. `eslint.config.ts` is loaded through `jiti` (eslint 10 needs it for TypeScript configs); it is a devDependency and never ships.
|
|
67
|
+
- **The three ignore files serve opposite purposes for build output.** `public/assets/` is gitignored and prettier-ignored, but must NEVER be added to `.npmignore`: `remoteEntry.js` imports those chunks by name, so excluding them publishes a panel that 404s on load. Only genuinely local artifacts (`.mf/`) belong in all three.
|
|
68
|
+
- Prettier + eslint flat config (`eslint.config.ts`); run `npm run format` before committing.
|
|
69
|
+
- The config panel uses React 19 with Module Federation — keep the federation `shared` block in sync with `package.json`'s React version.
|
|
70
|
+
- **Classic JSX is load-bearing.** `jsxRuntime: "classic"` (vite.config.ts) and `"jsx": "react"` (panel tsconfig) must agree. The automatic runtime imports `react/jsx-runtime`, which is not in the federation `shared` scope, so the remote would carry its own copy of React's jsx runtime — a second React instance whose dispatcher is not the host's, so `useState` reads null and the panel fails to mount. At runtime, with no build error.
|
|
71
|
+
- **Imports leaving `src/configpanel/` must be `import type`.** A value import pulls the module's runtime dependencies into the browser bundle (typebox via `config/schema`, `fs/promises` via `host-limits`). After changing panel imports, confirm with `grep -rl "typebox\|fs/promises" public/` — it must print nothing.
|
|
60
72
|
- `signalk.appIcon` and `signalk.displayName` in `package.json` control how the plugin appears in the Admin UI.
|
package/README.md
CHANGED
|
@@ -113,27 +113,60 @@ wants to slice it into kopia-dedup-friendly shards. Allowed tables:
|
|
|
113
113
|
|
|
114
114
|
## Configuration
|
|
115
115
|
|
|
116
|
-
| Setting
|
|
117
|
-
|
|
|
118
|
-
| QuestDB version
|
|
119
|
-
| Managed container
|
|
120
|
-
| QuestDB host
|
|
121
|
-
| HTTP port
|
|
122
|
-
| ILP port
|
|
123
|
-
| PostgreSQL port
|
|
124
|
-
| Sampling rate (ms)
|
|
125
|
-
| Write batch interval (ms)
|
|
126
|
-
| Memory limit
|
|
127
|
-
| CPU limit (cores)
|
|
128
|
-
| Record own vessel
|
|
129
|
-
| Record AIS targets
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
116
|
+
| Setting | Default | Description |
|
|
117
|
+
| -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| QuestDB version | `latest` | Docker image tag (dropdown shows stable + pre-releases) |
|
|
119
|
+
| Managed container | `true` | Let signalk-container manage QuestDB, or connect to external |
|
|
120
|
+
| QuestDB host | `127.0.0.1` | External QuestDB host (only used when managed=false) |
|
|
121
|
+
| HTTP port | `9000` | External mode, or the host binding when "Bind to 0.0.0.0" is on |
|
|
122
|
+
| ILP port | `9009` | External mode, or the host binding when "Bind to 0.0.0.0" is on |
|
|
123
|
+
| PostgreSQL port | `8812` | Host binding for Grafana/psql when "Bind to 0.0.0.0" is on |
|
|
124
|
+
| Sampling rate (ms) | `2000` | Default min ms between writes per path (0 = every update) |
|
|
125
|
+
| Write batch interval (ms) | `5000` | How often buffered samples are committed — one WAL transaction per table per commit (see Performance) |
|
|
126
|
+
| Memory limit | `768m` | Hard cgroup cap on QuestDB container RAM (empty = unlimited) |
|
|
127
|
+
| CPU limit (cores) | `1.5` | Max CPU cores QuestDB can use (0 = unlimited) |
|
|
128
|
+
| Record own vessel | `true` | Record self context |
|
|
129
|
+
| Record AIS targets | `true` | Record other vessels |
|
|
130
|
+
| Restore vessels on startup | `false` | Replay each vessel's last recorded position after a restart (see Startup restore) |
|
|
131
|
+
| Restore max age (minutes) | `9` | Only replay values recorded within this window |
|
|
132
|
+
| Retention (days) | `0` | Auto-delete old partitions (0 = keep forever) |
|
|
133
|
+
| Path filter mode | `exclude` | `exclude` matching paths, or `include` only matching paths |
|
|
134
|
+
| Path filter paths | _(empty)_ | Glob patterns, one per line (e.g. `navigation.position`); empty = record everything |
|
|
135
|
+
| Compression codec | `lz4` | On-disk WAL compression: `none`, `lz4`, or `zstd` |
|
|
136
|
+
| Compression level | `3` | ZSTD level 1-22 (only when codec is zstd) |
|
|
137
|
+
| Container network | `sk-network` | Shared network for QuestDB (only applied when binding to 0.0.0.0) |
|
|
138
|
+
| Bind to 0.0.0.0 | `false` | Expose QuestDB's ports on the LAN (see Connectivity below) |
|
|
139
|
+
|
|
140
|
+
## Startup restore
|
|
141
|
+
|
|
142
|
+
Signal K's data model lives in memory only. After a server restart every vessel
|
|
143
|
+
is gone until it transmits again — for AIS that means roughly 30 seconds for a
|
|
144
|
+
Class B target, up to 3 minutes for a Class A at anchor, and about 6 minutes
|
|
145
|
+
before names arrive. The chart fills in gradually instead of showing the traffic
|
|
146
|
+
that was there a moment ago.
|
|
147
|
+
|
|
148
|
+
With **Restore vessels on startup** enabled, the plugin replays each vessel's
|
|
149
|
+
last recorded position (plus course, speed, heading and identity) from QuestDB
|
|
150
|
+
as soon as it connects, so the chart is populated immediately.
|
|
151
|
+
|
|
152
|
+
**A restored position is where a vessel _was_, not where it is now.** Nothing is
|
|
153
|
+
dead-reckoned forward. The plugin presents these values as the history they are
|
|
154
|
+
rather than passing them off as live: each delta carries its original recorded
|
|
155
|
+
timestamp, so a chart plotter ages it out under the same staleness rules it
|
|
156
|
+
applies to any other target, and the deltas are tagged
|
|
157
|
+
`$source: signalk-questdb.restore`.
|
|
158
|
+
|
|
159
|
+
Values outside the **Restore max age** window are not replayed at all. The
|
|
160
|
+
9-minute default matches Freeboard's AIS expiry, so a restored target is one that
|
|
161
|
+
would still have been on the chart had the server never stopped. Raising it puts
|
|
162
|
+
progressively staler positions in front of you — treat the window as a
|
|
163
|
+
collision-avoidance setting, not a convenience one.
|
|
164
|
+
|
|
165
|
+
Only navigation and identity paths are replayed, so stale tank levels and engine
|
|
166
|
+
temperatures are not resurrected as though they were current readings. A vessel
|
|
167
|
+
with no position in the window is skipped, as is one that has already transmitted
|
|
168
|
+
since startup. Restore honours the recording toggles: with **Record AIS targets**
|
|
169
|
+
off, no AIS target is restored.
|
|
137
170
|
|
|
138
171
|
## Connectivity
|
|
139
172
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { MaxMapCountStatus } from "./host-limits.js";
|
|
2
|
+
import type { SkipPlan, SuspendedTable } from "./wal-monitor.js";
|
|
3
|
+
/**
|
|
4
|
+
* Every handler answers failures with `{ error }`, and the action endpoints
|
|
5
|
+
* answer success with `{ message }`. The panel reads both off one object and
|
|
6
|
+
* renders whichever is present, so both are optional everywhere rather than
|
|
7
|
+
* split into a union the callers would have to narrow at ten sites.
|
|
8
|
+
*/
|
|
9
|
+
export interface ApiError {
|
|
10
|
+
error?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Mirror of signalk-container's `UlimitClamp` event, redeclared (not
|
|
15
|
+
* imported) for the same reason index.ts redeclares it: signalk-container is
|
|
16
|
+
* an optional peer reached through globalThis, so there is no type to import.
|
|
17
|
+
*/
|
|
18
|
+
export interface UlimitClampStatus {
|
|
19
|
+
ulimit: string;
|
|
20
|
+
requested: number;
|
|
21
|
+
granted: number;
|
|
22
|
+
reason: string;
|
|
23
|
+
}
|
|
24
|
+
export type SuspendedTableStatus = SuspendedTable & {
|
|
25
|
+
/** WAL monitor's verdict for the current stall point: "pending" | "failed". */
|
|
26
|
+
autoResume: string | null;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* GET /api/status.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately flat rather than a union discriminated on `status`: the panel
|
|
32
|
+
* gates the whole running-state block behind one `isRunning` alias, and TS
|
|
33
|
+
* does not narrow through an alias. Optional fields let the panel read
|
|
34
|
+
* `dbStatus?.totalRows` at the leaves instead of restructuring the render.
|
|
35
|
+
*/
|
|
36
|
+
export interface DbStatus extends ApiError {
|
|
37
|
+
status: "running" | "unhealthy" | "not_running";
|
|
38
|
+
totalRows?: number;
|
|
39
|
+
activePathsToday?: number;
|
|
40
|
+
walSuspended?: boolean;
|
|
41
|
+
suspendedTables?: SuspendedTableStatus[];
|
|
42
|
+
schemaMismatch?: boolean;
|
|
43
|
+
ulimitClamp?: UlimitClampStatus | null;
|
|
44
|
+
hostMaxMapCount?: MaxMapCountStatus | null;
|
|
45
|
+
endpoint?: string | null;
|
|
46
|
+
}
|
|
47
|
+
/** GET /api/versions — QuestDB releases, drafts filtered out. */
|
|
48
|
+
export interface QuestdbVersion {
|
|
49
|
+
tag: string;
|
|
50
|
+
prerelease: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** GET /api/update/check. */
|
|
53
|
+
export interface UpdateInfo extends ApiError {
|
|
54
|
+
currentVersion: string;
|
|
55
|
+
latestVersion: string;
|
|
56
|
+
updateAvailable: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* POST /api/update/apply. `newVersion` is the tag the container was actually
|
|
60
|
+
* recreated with, so the panel can sync its version field to reality.
|
|
61
|
+
*/
|
|
62
|
+
export interface UpdateApplyResponse extends ApiError {
|
|
63
|
+
status?: string;
|
|
64
|
+
newVersion?: string;
|
|
65
|
+
}
|
|
66
|
+
export type WalDiagnosisTable = SuspendedTable & {
|
|
67
|
+
autoResume: string | null;
|
|
68
|
+
/** Commit-time of the first unapplied txn = when apply froze. */
|
|
69
|
+
suspendedSince: string | null;
|
|
70
|
+
pendingSegments: number;
|
|
71
|
+
/** Null when the failure is a partition-open error, where skipping cannot help. */
|
|
72
|
+
skipPlan: SkipPlan | null;
|
|
73
|
+
partitionOpenFailure: boolean;
|
|
74
|
+
segmentError: string | null;
|
|
75
|
+
applyError: string | null;
|
|
76
|
+
};
|
|
77
|
+
/** GET /api/wal-diagnosis. */
|
|
78
|
+
export interface WalDiagnosis extends ApiError {
|
|
79
|
+
tables: WalDiagnosisTable[];
|
|
80
|
+
}
|
|
81
|
+
export interface ResumeWalResult {
|
|
82
|
+
table: string;
|
|
83
|
+
ok: boolean;
|
|
84
|
+
error?: string;
|
|
85
|
+
writerTxn: number;
|
|
86
|
+
sequencerTxn: number;
|
|
87
|
+
}
|
|
88
|
+
/** POST /api/resume-wal. */
|
|
89
|
+
export interface ResumeWalResponse extends ApiError {
|
|
90
|
+
results?: ResumeWalResult[];
|
|
91
|
+
/** How many of `results` succeeded. */
|
|
92
|
+
resumed?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* POST /api/resume-wal/skip — the lossy repair. The handler walks a safety
|
|
96
|
+
* ladder (lossless resume first, destructive skip only if that re-froze), and
|
|
97
|
+
* these flags say which rung it stopped on. The skip target itself is always
|
|
98
|
+
* computed server-side; `skipPlan` echoes back what was actually executed.
|
|
99
|
+
*/
|
|
100
|
+
export interface SkipWalResponse extends ApiError {
|
|
101
|
+
/** True only when the destructive RESUME WAL FROM TXN ran. */
|
|
102
|
+
skipped?: boolean;
|
|
103
|
+
/** True when the table was not suspended, or a lossless resume cleared it. */
|
|
104
|
+
healed?: boolean;
|
|
105
|
+
/** True when the writer advanced before re-suspending — state changed. */
|
|
106
|
+
progressed?: boolean;
|
|
107
|
+
/** True when the table re-suspended at a later segment after the skip. */
|
|
108
|
+
stillSuspended?: boolean;
|
|
109
|
+
skipPlan?: SkipPlan;
|
|
110
|
+
}
|
|
111
|
+
export interface MigrationSource {
|
|
112
|
+
type: string;
|
|
113
|
+
url: string;
|
|
114
|
+
status: string;
|
|
115
|
+
version?: string;
|
|
116
|
+
}
|
|
117
|
+
/** GET /api/migration/detect. */
|
|
118
|
+
export interface MigrationDetectResponse extends ApiError {
|
|
119
|
+
sources: MigrationSource[];
|
|
120
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Wire shapes for the plugin's own REST endpoints under
|
|
2
|
+
// /plugins/signalk-questdb/api/.
|
|
3
|
+
//
|
|
4
|
+
// Both surfaces import these: the handlers in index.ts assert their responses
|
|
5
|
+
// against them with `satisfies`, and the config panel casts fetch results to
|
|
6
|
+
// them. That makes a field rename a compile error on both sides instead of an
|
|
7
|
+
// "undefined" that only shows up as a blank spot in the Admin UI.
|
|
8
|
+
//
|
|
9
|
+
// The panel is a browser bundle, so this file must stay type-only — no
|
|
10
|
+
// imports that pull runtime code (fs, typebox) into webpack's graph.
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=api-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-contract.js","sourceRoot":"","sources":["../src/api-contract.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,iCAAiC;AACjC,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAC9E,kEAAkE;AAClE,EAAE;AACF,uEAAuE;AACvE,qEAAqE"}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import { Static } from "
|
|
2
|
-
export declare const ConfigSchema:
|
|
3
|
-
questdbHost:
|
|
4
|
-
questdbIlpPort:
|
|
5
|
-
questdbHttpPort:
|
|
6
|
-
questdbPgPort:
|
|
7
|
-
questdbVersion:
|
|
8
|
-
managedContainer:
|
|
9
|
-
questdbMemoryLimit:
|
|
10
|
-
questdbCpuLimit:
|
|
11
|
-
pathFilter:
|
|
12
|
-
mode:
|
|
13
|
-
paths:
|
|
1
|
+
import { Type, Static } from "typebox";
|
|
2
|
+
export declare const ConfigSchema: Type.TObject<{
|
|
3
|
+
questdbHost: Type.TString;
|
|
4
|
+
questdbIlpPort: Type.TNumber;
|
|
5
|
+
questdbHttpPort: Type.TNumber;
|
|
6
|
+
questdbPgPort: Type.TNumber;
|
|
7
|
+
questdbVersion: Type.TString;
|
|
8
|
+
managedContainer: Type.TBoolean;
|
|
9
|
+
questdbMemoryLimit: Type.TString;
|
|
10
|
+
questdbCpuLimit: Type.TNumber;
|
|
11
|
+
pathFilter: Type.TObject<{
|
|
12
|
+
mode: Type.TUnion<[Type.TLiteral<"exclude">, Type.TLiteral<"include">]>;
|
|
13
|
+
paths: Type.TArray<Type.TString>;
|
|
14
14
|
}>;
|
|
15
|
-
defaultSamplingRate:
|
|
16
|
-
ilpFlushIntervalMs:
|
|
17
|
-
samplingRates:
|
|
18
|
-
recordSelf:
|
|
19
|
-
recordOthers:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
defaultSamplingRate: Type.TNumber;
|
|
16
|
+
ilpFlushIntervalMs: Type.TNumber;
|
|
17
|
+
samplingRates: Type.TRecord<"^.*$", Type.TNumber>;
|
|
18
|
+
recordSelf: Type.TBoolean;
|
|
19
|
+
recordOthers: Type.TBoolean;
|
|
20
|
+
restoreOnStart: Type.TBoolean;
|
|
21
|
+
restoreMaxAgeMinutes: Type.TNumber;
|
|
22
|
+
retentionDays: Type.TNumber;
|
|
23
|
+
networkName: Type.TString;
|
|
24
|
+
exposeToContainers: Type.TBoolean;
|
|
25
|
+
compression: Type.TUnion<[Type.TLiteral<"none">, Type.TLiteral<"lz4">, Type.TLiteral<"zstd">]>;
|
|
26
|
+
compressionLevel: Type.TNumber;
|
|
25
27
|
}>;
|
|
26
28
|
export type Config = Static<typeof ConfigSchema>;
|
|
27
29
|
/**
|
package/dist/config/schema.js
CHANGED
|
@@ -1,104 +1,111 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.normalizeConfig = normalizeConfig;
|
|
5
|
-
const typebox_1 = require("@sinclair/typebox");
|
|
6
|
-
exports.ConfigSchema = typebox_1.Type.Object({
|
|
7
|
-
questdbHost: typebox_1.Type.String({
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
export const ConfigSchema = Type.Object({
|
|
3
|
+
questdbHost: Type.String({
|
|
8
4
|
default: "127.0.0.1",
|
|
9
5
|
title: "QuestDB host",
|
|
10
6
|
description: "Only used when managed container is off (external QuestDB). In managed mode the address is resolved automatically.",
|
|
11
7
|
}),
|
|
12
|
-
questdbIlpPort:
|
|
8
|
+
questdbIlpPort: Type.Number({
|
|
13
9
|
default: 9009,
|
|
14
10
|
title: "ILP port (writes)",
|
|
15
11
|
description: "External mode, or the host binding when 'Bind to 0.0.0.0' is on. Ignored otherwise (signalk-container allocates the port).",
|
|
16
12
|
}),
|
|
17
|
-
questdbHttpPort:
|
|
13
|
+
questdbHttpPort: Type.Number({
|
|
18
14
|
default: 9000,
|
|
19
15
|
title: "HTTP port (queries)",
|
|
20
16
|
description: "External mode, or the host binding when 'Bind to 0.0.0.0' is on. Ignored otherwise (signalk-container allocates the port).",
|
|
21
17
|
}),
|
|
22
|
-
questdbPgPort:
|
|
18
|
+
questdbPgPort: Type.Number({
|
|
23
19
|
default: 8812,
|
|
24
20
|
title: "PostgreSQL wire port",
|
|
25
21
|
description: "Host binding for Grafana/psql when 'Bind to 0.0.0.0' is on. Ignored otherwise (signalk-container allocates the port).",
|
|
26
22
|
}),
|
|
27
|
-
questdbVersion:
|
|
23
|
+
questdbVersion: Type.String({
|
|
28
24
|
default: "latest",
|
|
29
25
|
title: "QuestDB image version",
|
|
30
26
|
}),
|
|
31
|
-
managedContainer:
|
|
27
|
+
managedContainer: Type.Boolean({
|
|
32
28
|
default: true,
|
|
33
29
|
title: "Manage QuestDB container via signalk-container",
|
|
34
30
|
description: "Disable to connect to an external QuestDB instance",
|
|
35
31
|
}),
|
|
36
|
-
questdbMemoryLimit:
|
|
32
|
+
questdbMemoryLimit: Type.String({
|
|
37
33
|
default: "768m",
|
|
38
34
|
title: "QuestDB memory limit",
|
|
39
35
|
description: "Hard cgroup memory cap (e.g. 512m, 1g, 2g). Empty = unlimited. The JVM auto-sizes its heap to a fraction of this, so capping here bounds total footprint including off-heap.",
|
|
40
36
|
}),
|
|
41
|
-
questdbCpuLimit:
|
|
37
|
+
questdbCpuLimit: Type.Number({
|
|
42
38
|
default: 1.5,
|
|
43
39
|
title: "QuestDB CPU limit (cores)",
|
|
44
40
|
description: "Max CPU cores QuestDB can use (fractional, e.g. 1.5). 0 = unlimited.",
|
|
45
41
|
}),
|
|
46
|
-
pathFilter:
|
|
47
|
-
mode:
|
|
42
|
+
pathFilter: Type.Object({
|
|
43
|
+
mode: Type.Union([Type.Literal("exclude"), Type.Literal("include")], {
|
|
48
44
|
default: "exclude",
|
|
49
45
|
title: "Filter mode",
|
|
50
46
|
}),
|
|
51
|
-
paths:
|
|
47
|
+
paths: Type.Array(Type.String(), {
|
|
52
48
|
default: [],
|
|
53
49
|
title: "Path patterns (glob supported)",
|
|
54
50
|
description: 'e.g. "notifications.*", "environment.wind.*"',
|
|
55
51
|
}),
|
|
56
52
|
}),
|
|
57
|
-
defaultSamplingRate:
|
|
53
|
+
defaultSamplingRate: Type.Number({
|
|
58
54
|
default: 2000,
|
|
59
55
|
title: "Default sampling rate (ms)",
|
|
60
56
|
description: "Minimum ms between writes for any path (0 = write every update). 2000ms is a sensible default for Pi/Cerbo-class hardware; lower it per-path via samplingRates when you need finer resolution.",
|
|
61
57
|
}),
|
|
62
|
-
ilpFlushIntervalMs:
|
|
58
|
+
ilpFlushIntervalMs: Type.Number({
|
|
63
59
|
default: 5000,
|
|
64
60
|
minimum: 500,
|
|
65
61
|
maximum: 300000,
|
|
66
62
|
title: "Write batch interval (ms)",
|
|
67
63
|
description: "How often buffered samples are committed to QuestDB. Each commit is one WAL transaction per table, so short intervals create huge numbers of tiny transactions whose apply cost eventually stalls recording on Pi-class hardware — QuestDB recommends >100 rows per transaction and its console flags tables below that (see README, 'Small transactions' alert). Longer intervals mean at most this many ms of buffered data is lost on a hard crash.",
|
|
68
64
|
}),
|
|
69
|
-
samplingRates:
|
|
65
|
+
samplingRates: Type.Record(Type.String(), Type.Number(), {
|
|
70
66
|
default: {},
|
|
71
67
|
title: "Per-path sampling rates (ms)",
|
|
72
68
|
description: 'Override default rate for specific paths. e.g. { "environment.wind.*": 200, "tanks.*": 10000 }',
|
|
73
69
|
}),
|
|
74
|
-
recordSelf:
|
|
70
|
+
recordSelf: Type.Boolean({
|
|
75
71
|
default: true,
|
|
76
72
|
title: "Record own vessel",
|
|
77
73
|
}),
|
|
78
|
-
recordOthers:
|
|
74
|
+
recordOthers: Type.Boolean({
|
|
79
75
|
default: true,
|
|
80
76
|
title: "Record AIS targets",
|
|
81
77
|
}),
|
|
82
|
-
|
|
78
|
+
restoreOnStart: Type.Boolean({
|
|
79
|
+
default: false,
|
|
80
|
+
title: "Restore vessels on startup",
|
|
81
|
+
description: "After a server restart, replay each vessel's last recorded position from QuestDB so AIS targets appear on the chart immediately instead of only when they next transmit. Restored values are historical — they carry their original timestamp and are aged out normally by the consumer.",
|
|
82
|
+
}),
|
|
83
|
+
restoreMaxAgeMinutes: Type.Number({
|
|
84
|
+
default: 9,
|
|
85
|
+
minimum: 1,
|
|
86
|
+
title: "Restore max age (minutes)",
|
|
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
|
+
}),
|
|
89
|
+
retentionDays: Type.Number({
|
|
83
90
|
default: 0,
|
|
84
91
|
title: "Retention (days, 0 = keep forever)",
|
|
85
92
|
}),
|
|
86
|
-
networkName:
|
|
93
|
+
networkName: Type.String({
|
|
87
94
|
default: "sk-network",
|
|
88
95
|
title: "Container network",
|
|
89
96
|
description: "Shared Podman/Docker network for QuestDB. Only applied when 'Bind to 0.0.0.0' is on (so a separate-Docker Grafana can reach QuestDB by DNS).",
|
|
90
97
|
}),
|
|
91
|
-
exposeToContainers:
|
|
98
|
+
exposeToContainers: Type.Boolean({
|
|
92
99
|
default: false,
|
|
93
100
|
title: "Bind to 0.0.0.0",
|
|
94
101
|
description: "Publish QuestDB's ports on all interfaces so another machine or a separate-Docker Grafana can reach them. Off (default) keeps QuestDB private — Signal K still reaches it automatically. Caution! Enabling exposes your data to the network.",
|
|
95
102
|
}),
|
|
96
|
-
compression:
|
|
103
|
+
compression: Type.Union([Type.Literal("none"), Type.Literal("lz4"), Type.Literal("zstd")], {
|
|
97
104
|
default: "lz4",
|
|
98
105
|
title: "Compression codec",
|
|
99
106
|
description: "Used for QuestDB on-disk WAL segments and Parquet exports (lz4 = fast, zstd = smaller)",
|
|
100
107
|
}),
|
|
101
|
-
compressionLevel:
|
|
108
|
+
compressionLevel: Type.Number({
|
|
102
109
|
default: 3,
|
|
103
110
|
title: "Compression level",
|
|
104
111
|
description: "zstd: 1-22 (default 3), lz4: ignored",
|
|
@@ -114,7 +121,7 @@ exports.ConfigSchema = typebox_1.Type.Object({
|
|
|
114
121
|
* stopped. Normalize once at the config boundary instead of guarding every
|
|
115
122
|
* consumer.
|
|
116
123
|
*/
|
|
117
|
-
function normalizeConfig(config) {
|
|
124
|
+
export function normalizeConfig(config) {
|
|
118
125
|
return {
|
|
119
126
|
...config,
|
|
120
127
|
pathFilter: {
|
|
@@ -127,6 +134,14 @@ function normalizeConfig(config) {
|
|
|
127
134
|
// pre-panel config silently disabled the recording the schema promises.
|
|
128
135
|
recordSelf: config.recordSelf !== false,
|
|
129
136
|
recordOthers: config.recordOthers !== false,
|
|
137
|
+
// Restore is opt-in, so a missing key means OFF — the inverse of the
|
|
138
|
+
// recording toggles above. A zero or negative window would replay
|
|
139
|
+
// nothing; treat it as "unset" and use the schema default rather than
|
|
140
|
+
// silently disabling a feature the user turned on.
|
|
141
|
+
restoreOnStart: config.restoreOnStart === true,
|
|
142
|
+
restoreMaxAgeMinutes: config.restoreMaxAgeMinutes && config.restoreMaxAgeMinutes > 0
|
|
143
|
+
? config.restoreMaxAgeMinutes
|
|
144
|
+
: 9,
|
|
130
145
|
// Missing resource limits get the schema defaults. Config-panel saves
|
|
131
146
|
// used to drop these keys entirely (issue #98), which ran QuestDB with
|
|
132
147
|
// 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":"
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MigrationSource, QuestdbVersion } from "../api-contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* GET /api/versions returns an array of releases. Anything else becomes an
|
|
4
|
+
* empty list, and malformed members are dropped: the version dropdown is
|
|
5
|
+
* then short or empty rather than the panel being gone.
|
|
6
|
+
*
|
|
7
|
+
* Members are validated, not just the array — `versions.filter(v =>
|
|
8
|
+
* !v.prerelease)` runs during render, so a single null element is as fatal
|
|
9
|
+
* as a non-array body.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toVersionList(body: unknown): QuestdbVersion[];
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/migration/detect returns `{ sources: [...] }`. A body without it
|
|
14
|
+
* means "nothing detected", which is also what an empty array means — so the
|
|
15
|
+
* user gets the honest "no InfluxDB found" instead of a TypeError surfaced
|
|
16
|
+
* as "Detection failed".
|
|
17
|
+
*/
|
|
18
|
+
export declare function toMigrationSources(body: unknown): MigrationSource[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Normalizing the plugin's own API responses before they reach React state.
|
|
2
|
+
//
|
|
3
|
+
// Every fetch in the panel casts `res.json()` to a contract type. A cast is
|
|
4
|
+
// an assertion, not a check: the plugin's own handlers are well behaved, but
|
|
5
|
+
// a reverse proxy, auth gateway or captive portal can answer 200 with a body
|
|
6
|
+
// of an entirely different shape. Where the panel then dereferences the
|
|
7
|
+
// result during render — `versions.filter(...)` runs with no error boundary
|
|
8
|
+
// above it — that throws mid-render and blanks the whole config panel.
|
|
9
|
+
//
|
|
10
|
+
// These helpers keep the "trust but verify" boundary in one testable place.
|
|
11
|
+
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/versions returns an array of releases. Anything else becomes an
|
|
14
|
+
* empty list, and malformed members are dropped: the version dropdown is
|
|
15
|
+
* then short or empty rather than the panel being gone.
|
|
16
|
+
*
|
|
17
|
+
* Members are validated, not just the array — `versions.filter(v =>
|
|
18
|
+
* !v.prerelease)` runs during render, so a single null element is as fatal
|
|
19
|
+
* as a non-array body.
|
|
20
|
+
*/
|
|
21
|
+
export function toVersionList(body) {
|
|
22
|
+
if (!Array.isArray(body))
|
|
23
|
+
return [];
|
|
24
|
+
return body.filter((v) => isRecord(v) &&
|
|
25
|
+
typeof v.tag === "string" &&
|
|
26
|
+
// Not merely cosmetic: the two filters below partition on this, so a
|
|
27
|
+
// missing or non-boolean value would list a pre-release as stable —
|
|
28
|
+
// and the stable list is what the version dropdown defaults to.
|
|
29
|
+
typeof v.prerelease === "boolean");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* GET /api/migration/detect returns `{ sources: [...] }`. A body without it
|
|
33
|
+
* means "nothing detected", which is also what an empty array means — so the
|
|
34
|
+
* user gets the honest "no InfluxDB found" instead of a TypeError surfaced
|
|
35
|
+
* as "Detection failed".
|
|
36
|
+
*/
|
|
37
|
+
export function toMigrationSources(body) {
|
|
38
|
+
const sources = isRecord(body) ? body.sources : undefined;
|
|
39
|
+
if (!Array.isArray(sources))
|
|
40
|
+
return [];
|
|
41
|
+
return sources.filter((s) => isRecord(s) &&
|
|
42
|
+
typeof s.type === "string" &&
|
|
43
|
+
typeof s.url === "string" &&
|
|
44
|
+
// Every field below is rendered as a React child. A plain object
|
|
45
|
+
// there throws "Objects are not valid as a React child" and takes
|
|
46
|
+
// the panel down — the same blank-screen failure as an unguarded
|
|
47
|
+
// array, just reached from the detected-sources list.
|
|
48
|
+
typeof s.status === "string" &&
|
|
49
|
+
(s.version === undefined || typeof s.version === "string"));
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=responses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responses.js","sourceRoot":"","sources":["../../src/configpanel/responses.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,wEAAwE;AACxE,4EAA4E;AAC5E,uEAAuE;AACvE,EAAE;AACF,4EAA4E;AAI5E,MAAM,QAAQ,GAAG,CAAC,CAAU,EAAgC,EAAE,CAC5D,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC;AAEtC;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC,MAAM,CAChB,CAAC,CAAC,EAAuB,EAAE,CACzB,QAAQ,CAAC,CAAC,CAAC;QACX,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ;QACzB,qEAAqE;QACrE,oEAAoE;QACpE,gEAAgE;QAChE,OAAO,CAAC,CAAC,UAAU,KAAK,SAAS,CACpC,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IACvC,OAAO,OAAO,CAAC,MAAM,CACnB,CAAC,CAAC,EAAwB,EAAE,CAC1B,QAAQ,CAAC,CAAC,CAAC;QACX,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ;QACzB,iEAAiE;QACjE,kEAAkE;QAClE,iEAAiE;QACjE,sDAAsD;QACtD,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;QAC5B,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAC7D,CAAC;AACJ,CAAC"}
|
package/dist/container-env.d.ts
CHANGED