signalk-questdb 1.6.0 → 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.
Files changed (99) hide show
  1. package/.prettierignore +2 -0
  2. package/CLAUDE.md +20 -8
  3. package/README.md +78 -21
  4. package/dist/api-contract.d.ts +120 -0
  5. package/dist/api-contract.js +12 -0
  6. package/dist/api-contract.js.map +1 -0
  7. package/dist/config/schema.d.ts +25 -23
  8. package/dist/config/schema.js +59 -31
  9. package/dist/config/schema.js.map +1 -1
  10. package/dist/configpanel/responses.d.ts +18 -0
  11. package/dist/configpanel/responses.js +51 -0
  12. package/dist/configpanel/responses.js.map +1 -0
  13. package/dist/container-env.d.ts +1 -1
  14. package/dist/container-env.js +3 -7
  15. package/dist/container-env.js.map +1 -1
  16. package/dist/delta-routing.js +2 -6
  17. package/dist/delta-routing.js.map +1 -1
  18. package/dist/full-export-range.js +1 -4
  19. package/dist/full-export-range.js.map +1 -1
  20. package/dist/history-v1.d.ts +1 -1
  21. package/dist/history-v1.js +7 -10
  22. package/dist/history-v1.js.map +1 -1
  23. package/dist/history-v2.d.ts +1 -1
  24. package/dist/history-v2.js +11 -14
  25. package/dist/history-v2.js.map +1 -1
  26. package/dist/host-limits.js +9 -16
  27. package/dist/host-limits.js.map +1 -1
  28. package/dist/ilp-writer.js +4 -41
  29. package/dist/ilp-writer.js.map +1 -1
  30. package/dist/index.d.ts +55 -1
  31. package/dist/index.js +144 -93
  32. package/dist/index.js.map +1 -1
  33. package/dist/path-matcher.js +6 -12
  34. package/dist/path-matcher.js.map +1 -1
  35. package/dist/query-client.js +4 -11
  36. package/dist/query-client.js.map +1 -1
  37. package/dist/questdb-endpoint.js +18 -26
  38. package/dist/questdb-endpoint.js.map +1 -1
  39. package/dist/restore.d.ts +56 -0
  40. package/dist/restore.js +212 -0
  41. package/dist/restore.js.map +1 -0
  42. package/dist/retention.d.ts +1 -1
  43. package/dist/retention.js +1 -4
  44. package/dist/retention.js.map +1 -1
  45. package/dist/test/config.test.js +107 -26
  46. package/dist/test/config.test.js.map +1 -1
  47. package/dist/test/configpanel-responses.test.d.ts +1 -0
  48. package/dist/test/configpanel-responses.test.js +171 -0
  49. package/dist/test/configpanel-responses.test.js.map +1 -0
  50. package/dist/test/container-env.test.js +38 -43
  51. package/dist/test/container-env.test.js.map +1 -1
  52. package/dist/test/delta-routing.test.js +37 -42
  53. package/dist/test/delta-routing.test.js.map +1 -1
  54. package/dist/test/full-export-range.test.js +45 -50
  55. package/dist/test/full-export-range.test.js.map +1 -1
  56. package/dist/test/history-v1.test.js +82 -87
  57. package/dist/test/history-v1.test.js.map +1 -1
  58. package/dist/test/history-v2.test.js +95 -100
  59. package/dist/test/history-v2.test.js.map +1 -1
  60. package/dist/test/host-limits.test.js +55 -60
  61. package/dist/test/host-limits.test.js.map +1 -1
  62. package/dist/test/ilp-writer.test.js +59 -97
  63. package/dist/test/ilp-writer.test.js.map +1 -1
  64. package/dist/test/package-e2e.test.d.ts +1 -0
  65. package/dist/test/package-e2e.test.js +267 -0
  66. package/dist/test/package-e2e.test.js.map +1 -0
  67. package/dist/test/path-matcher.test.js +104 -109
  68. package/dist/test/path-matcher.test.js.map +1 -1
  69. package/dist/test/query-client.test.js +96 -134
  70. package/dist/test/query-client.test.js.map +1 -1
  71. package/dist/test/questdb-endpoint.test.js +103 -108
  72. package/dist/test/questdb-endpoint.test.js.map +1 -1
  73. package/dist/test/restore.test.d.ts +1 -0
  74. package/dist/test/restore.test.js +389 -0
  75. package/dist/test/restore.test.js.map +1 -0
  76. package/dist/test/time-range.test.js +38 -43
  77. package/dist/test/time-range.test.js.map +1 -1
  78. package/dist/test/wal-monitor.test.js +131 -136
  79. package/dist/test/wal-monitor.test.js.map +1 -1
  80. package/dist/time-range.js +6 -10
  81. package/dist/time-range.js.map +1 -1
  82. package/dist/wal-monitor.js +6 -15
  83. package/dist/wal-monitor.js.map +1 -1
  84. package/package.json +13 -10
  85. package/public/assets/PluginConfigurationPanel-BiADSRcq.js +10 -0
  86. package/public/assets/_virtual_mf-localSharedImportMap___mfe_internal__signalk_questdb__mf_owner__1-CVM43rXb.js +1 -0
  87. package/public/assets/_virtual_mf___mfe_internal__signalk_questdb__mf_owner__1__loadShare__react__loadShare__.js-DzhF4wXp.js +1 -0
  88. package/public/assets/configpanel-BvRk9kiK.js +0 -0
  89. package/public/assets/dist-BxwXn66h.js +4 -0
  90. package/public/assets/hostInit-BK1oa2Z9.js +1 -0
  91. package/public/assets/index-Cv0SCFTN.js +1 -0
  92. package/public/assets/virtualExposes-BT6Pf_MF.js +1 -0
  93. package/public/assets/vite-preload-helper-HclGiUj8.js +1 -0
  94. package/public/remoteEntry.js +1 -1
  95. package/public/540.js +0 -2
  96. package/public/540.js.LICENSE.txt +0 -9
  97. package/public/805.js +0 -1
  98. package/public/main.js +0 -1
  99. package/webpack.config.js +0 -52
package/.prettierignore CHANGED
@@ -1,2 +1,4 @@
1
1
  dist/
2
2
  node_modules/
3
+ .mf/
4
+ public/
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`) + webpack bundle of the React config panel into `public/`
8
- - `npm run build:config` — Webpack only (rebuild the config panel without recompiling TS)
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 webpack 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.
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** (`@sinclair/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.
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
- - Prettier + eslint flat config (`eslint.config.js`); run `npm run format` before committing.
59
- - The config panel is plain JS (not TS) and uses React 19 with Module Federation — keep the federation `shared` block in sync with `package.json`'s React version.
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 | 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 | `false` | Record other vessels |
130
- | Retention (days) | `0` | Auto-delete old partitions (0 = keep forever) |
131
- | Path filter mode | `exclude` | `exclude` matching paths, or `include` only matching paths |
132
- | Path filter paths | _(empty)_ | Glob patterns, one per line (e.g. `navigation.position`); empty = record everything |
133
- | Compression codec | `lz4` | On-disk WAL compression: `none`, `lz4`, or `zstd` |
134
- | Compression level | `3` | ZSTD level 1-22 (only when codec is zstd) |
135
- | Container network | `sk-network` | Shared network for QuestDB (only applied when binding to 0.0.0.0) |
136
- | Bind to 0.0.0.0 | `false` | Expose QuestDB's ports on the LAN (see Connectivity below) |
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
 
@@ -206,6 +239,30 @@ then browse to `http://localhost:9000`, or enable **"Bind to 0.0.0.0"** in the
206
239
  plugin config and use `http://<signalk-host-ip>:9000` (this exposes QuestDB to
207
240
  your network — see the warning under Grafana Integration).
208
241
 
242
+ ### The "Small transactions — consider batching" alert
243
+
244
+ The console's Monitoring view flags any table whose 90th-percentile WAL
245
+ transaction stays under QuestDB's recommended batch size (100 rows).
246
+ `signalk_position` triggers this **structurally**: the plugin commits its
247
+ write buffer every "Write batch interval", each commit is one WAL transaction
248
+ per table, and the position table holds exactly one path — so its share of
249
+ every commit is just the handful of GPS fixes since the last one. The other
250
+ tables spread hundreds of paths across each transaction and are rarely
251
+ flagged.
252
+
253
+ At vessel data rates this alert is cosmetic. The numbers that actually matter
254
+ are on the same page: **Write Amplification** near 1x, **Pending Rows** 0, and
255
+ **Transaction Lag** 0 mean the WAL apply is perfectly healthy — QuestDB merges
256
+ many small transactions per apply cycle anyway.
257
+
258
+ If you want bigger transactions regardless, raise **"Write batch interval"**
259
+ in the plugin config. Clearing the alert for the position table needs roughly
260
+ 100 fixes per commit — a batch window in the minutes, not seconds (the exact
261
+ numbers depend on your sampling rate; the config panel states the current
262
+ bounds). The trade-off is honest: up to one batch interval of buffered data is
263
+ lost on a hard crash (a clean Signal K shutdown flushes first), and live-ish
264
+ history queries see new data that much later.
265
+
209
266
  ## Performance (Pi / Low-Power Devices)
210
267
 
211
268
  The plugin is optimized for Raspberry Pi and similar low-power devices:
@@ -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"}
@@ -1,27 +1,29 @@
1
- import { Static } from "@sinclair/typebox";
2
- export declare const ConfigSchema: import("@sinclair/typebox").TObject<{
3
- questdbHost: import("@sinclair/typebox").TString;
4
- questdbIlpPort: import("@sinclair/typebox").TNumber;
5
- questdbHttpPort: import("@sinclair/typebox").TNumber;
6
- questdbPgPort: import("@sinclair/typebox").TNumber;
7
- questdbVersion: import("@sinclair/typebox").TString;
8
- managedContainer: import("@sinclair/typebox").TBoolean;
9
- questdbMemoryLimit: import("@sinclair/typebox").TString;
10
- questdbCpuLimit: import("@sinclair/typebox").TNumber;
11
- pathFilter: import("@sinclair/typebox").TObject<{
12
- mode: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"exclude">, import("@sinclair/typebox").TLiteral<"include">]>;
13
- paths: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
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: import("@sinclair/typebox").TNumber;
16
- ilpFlushIntervalMs: import("@sinclair/typebox").TNumber;
17
- samplingRates: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TNumber>;
18
- recordSelf: import("@sinclair/typebox").TBoolean;
19
- recordOthers: import("@sinclair/typebox").TBoolean;
20
- retentionDays: import("@sinclair/typebox").TNumber;
21
- networkName: import("@sinclair/typebox").TString;
22
- exposeToContainers: import("@sinclair/typebox").TBoolean;
23
- compression: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"none">, import("@sinclair/typebox").TLiteral<"lz4">, import("@sinclair/typebox").TLiteral<"zstd">]>;
24
- compressionLevel: import("@sinclair/typebox").TNumber;
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
  /**
@@ -1,104 +1,111 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ConfigSchema = void 0;
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: typebox_1.Type.Number({
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: typebox_1.Type.Number({
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: typebox_1.Type.Number({
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: typebox_1.Type.String({
23
+ questdbVersion: Type.String({
28
24
  default: "latest",
29
25
  title: "QuestDB image version",
30
26
  }),
31
- managedContainer: typebox_1.Type.Boolean({
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: typebox_1.Type.String({
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: typebox_1.Type.Number({
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: typebox_1.Type.Object({
47
- mode: typebox_1.Type.Union([typebox_1.Type.Literal("exclude"), typebox_1.Type.Literal("include")], {
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: typebox_1.Type.Array(typebox_1.Type.String(), {
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: typebox_1.Type.Number({
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: typebox_1.Type.Number({
58
+ ilpFlushIntervalMs: Type.Number({
63
59
  default: 5000,
64
60
  minimum: 500,
65
- maximum: 60000,
61
+ maximum: 300000,
66
62
  title: "Write batch interval (ms)",
67
- 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. Longer intervals mean at most this many ms of buffered data is lost on a hard crash.",
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: typebox_1.Type.Record(typebox_1.Type.String(), typebox_1.Type.Number(), {
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: typebox_1.Type.Boolean({
70
+ recordSelf: Type.Boolean({
75
71
  default: true,
76
72
  title: "Record own vessel",
77
73
  }),
78
- recordOthers: typebox_1.Type.Boolean({
79
- default: false,
74
+ recordOthers: Type.Boolean({
75
+ default: true,
80
76
  title: "Record AIS targets",
81
77
  }),
82
- retentionDays: typebox_1.Type.Number({
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: typebox_1.Type.String({
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: typebox_1.Type.Boolean({
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: typebox_1.Type.Union([typebox_1.Type.Literal("none"), typebox_1.Type.Literal("lz4"), typebox_1.Type.Literal("zstd")], {
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: typebox_1.Type.Number({
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: {
@@ -122,6 +129,27 @@ function normalizeConfig(config) {
122
129
  paths: config.pathFilter?.paths ?? [],
123
130
  },
124
131
  samplingRates: config.samplingRates ?? {},
132
+ // Missing recording toggles get the schema defaults (both ON). The
133
+ // runtime guards read these directly, so without this a hand-edited or
134
+ // pre-panel config silently disabled the recording the schema promises.
135
+ recordSelf: config.recordSelf !== false,
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,
145
+ // Missing resource limits get the schema defaults. Config-panel saves
146
+ // used to drop these keys entirely (issue #98), which ran QuestDB with
147
+ // NO memory cap instead of the promised 768m — this backfill repairs
148
+ // configs already damaged by those saves. The documented opt-outs
149
+ // survive: an explicit "" (memory) or 0 (CPU) means unlimited and is
150
+ // not a missing key.
151
+ questdbMemoryLimit: config.questdbMemoryLimit ?? "768m",
152
+ questdbCpuLimit: config.questdbCpuLimit ?? 1.5,
125
153
  };
126
154
  }
127
155
  //# sourceMappingURL=schema.js.map
@@ -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":"AAAA,OAAO,EAAE,IAAI,EAAU,MAAM,SAAS,CAAC;AAEvC,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IACtC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,WAAW;QACpB,KAAK,EAAE,cAAc;QACrB,WAAW,EACT,oHAAoH;KACvH,CAAC;IACF,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC;QAC1B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,4HAA4H;KAC/H,CAAC;IACF,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC;QAC3B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,4HAA4H;KAC/H,CAAC;IACF,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC;QACzB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,uHAAuH;KAC1H,CAAC;IACF,cAAc,EAAE,IAAI,CAAC,MAAM,CAAC;QAC1B,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,uBAAuB;KAC/B,CAAC;IAEF,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC7B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,gDAAgD;QACvD,WAAW,EAAE,oDAAoD;KAClE,CAAC;IAEF,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC9B,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,8KAA8K;KACjL,CAAC;IAEF,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC;QAC3B,OAAO,EAAE,GAAG;QACZ,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,sEAAsE;KACzE,CAAC;IAEF,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC;QACtB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE;YACnE,OAAO,EAAE,SAAS;YAClB,KAAK,EAAE,aAAa;SACrB,CAAC;QACF,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE;YAC/B,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,gCAAgC;YACvC,WAAW,EAAE,8CAA8C;SAC5D,CAAC;KACH,CAAC;IAEF,mBAAmB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC/B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,gMAAgM;KACnM,CAAC;IAEF,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC9B,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,GAAG;QACZ,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,wbAAwb;KAC3b,CAAC;IAEF,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;QACvD,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,8BAA8B;QACrC,WAAW,EACT,gGAAgG;KACnG,CAAC;IAEF,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC;QACvB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,mBAAmB;KAC3B,CAAC;IACF,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC;QACzB,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,oBAAoB;KAC5B,CAAC;IAEF,cAAc,EAAE,IAAI,CAAC,OAAO,CAAC;QAC3B,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,0RAA0R;KAC7R,CAAC;IACF,oBAAoB,EAAE,IAAI,CAAC,MAAM,CAAC;QAChC,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,2BAA2B;QAClC,WAAW,EACT,+OAA+O;KAClP,CAAC;IAEF,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC;QACzB,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,oCAAoC;KAC5C,CAAC;IAEF,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,YAAY;QACrB,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,8IAA8I;KACjJ,CAAC;IAEF,kBAAkB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC/B,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,8OAA8O;KACjP,CAAC;IAEF,WAAW,EAAE,IAAI,CAAC,KAAK,CACrB,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,EACjE;QACE,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,wFAAwF;KAC3F,CACF;IACD,gBAAgB,EAAE,IAAI,CAAC,MAAM,CAAC;QAC5B,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EAAE,sCAAsC;KACpD,CAAC;CACH,CAAC,CAAC;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE;YACV,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,IAAI,SAAS;YAC1C,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE;SACtC;QACD,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE;QACzC,mEAAmE;QACnE,uEAAuE;QACvE,wEAAwE;QACxE,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,KAAK;QACvC,YAAY,EAAE,MAAM,CAAC,YAAY,KAAK,KAAK;QAC3C,qEAAqE;QACrE,kEAAkE;QAClE,sEAAsE;QACtE,mDAAmD;QACnD,cAAc,EAAE,MAAM,CAAC,cAAc,KAAK,IAAI;QAC9C,oBAAoB,EAClB,MAAM,CAAC,oBAAoB,IAAI,MAAM,CAAC,oBAAoB,GAAG,CAAC;YAC5D,CAAC,CAAC,MAAM,CAAC,oBAAoB;YAC7B,CAAC,CAAC,CAAC;QACP,sEAAsE;QACtE,uEAAuE;QACvE,qEAAqE;QACrE,kEAAkE;QAClE,qEAAqE;QACrE,qBAAqB;QACrB,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,IAAI,MAAM;QACvD,eAAe,EAAE,MAAM,CAAC,eAAe,IAAI,GAAG;KAC/C,CAAC;AACJ,CAAC"}
@@ -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[];