signalk-questdb 1.6.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/config/schema.js +16 -3
- package/dist/config/schema.js.map +1 -1
- package/dist/test/config.test.js +50 -0
- package/dist/test/config.test.js.map +1 -1
- package/package.json +1 -1
- package/public/805.js +1 -1
package/README.md
CHANGED
|
@@ -206,6 +206,30 @@ then browse to `http://localhost:9000`, or enable **"Bind to 0.0.0.0"** in the
|
|
|
206
206
|
plugin config and use `http://<signalk-host-ip>:9000` (this exposes QuestDB to
|
|
207
207
|
your network — see the warning under Grafana Integration).
|
|
208
208
|
|
|
209
|
+
### The "Small transactions — consider batching" alert
|
|
210
|
+
|
|
211
|
+
The console's Monitoring view flags any table whose 90th-percentile WAL
|
|
212
|
+
transaction stays under QuestDB's recommended batch size (100 rows).
|
|
213
|
+
`signalk_position` triggers this **structurally**: the plugin commits its
|
|
214
|
+
write buffer every "Write batch interval", each commit is one WAL transaction
|
|
215
|
+
per table, and the position table holds exactly one path — so its share of
|
|
216
|
+
every commit is just the handful of GPS fixes since the last one. The other
|
|
217
|
+
tables spread hundreds of paths across each transaction and are rarely
|
|
218
|
+
flagged.
|
|
219
|
+
|
|
220
|
+
At vessel data rates this alert is cosmetic. The numbers that actually matter
|
|
221
|
+
are on the same page: **Write Amplification** near 1x, **Pending Rows** 0, and
|
|
222
|
+
**Transaction Lag** 0 mean the WAL apply is perfectly healthy — QuestDB merges
|
|
223
|
+
many small transactions per apply cycle anyway.
|
|
224
|
+
|
|
225
|
+
If you want bigger transactions regardless, raise **"Write batch interval"**
|
|
226
|
+
in the plugin config. Clearing the alert for the position table needs roughly
|
|
227
|
+
100 fixes per commit — a batch window in the minutes, not seconds (the exact
|
|
228
|
+
numbers depend on your sampling rate; the config panel states the current
|
|
229
|
+
bounds). The trade-off is honest: up to one batch interval of buffered data is
|
|
230
|
+
lost on a hard crash (a clean Signal K shutdown flushes first), and live-ish
|
|
231
|
+
history queries see new data that much later.
|
|
232
|
+
|
|
209
233
|
## Performance (Pi / Low-Power Devices)
|
|
210
234
|
|
|
211
235
|
The plugin is optimized for Raspberry Pi and similar low-power devices:
|
package/dist/config/schema.js
CHANGED
|
@@ -62,9 +62,9 @@ exports.ConfigSchema = typebox_1.Type.Object({
|
|
|
62
62
|
ilpFlushIntervalMs: typebox_1.Type.Number({
|
|
63
63
|
default: 5000,
|
|
64
64
|
minimum: 500,
|
|
65
|
-
maximum:
|
|
65
|
+
maximum: 300000,
|
|
66
66
|
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.",
|
|
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 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
68
|
}),
|
|
69
69
|
samplingRates: typebox_1.Type.Record(typebox_1.Type.String(), typebox_1.Type.Number(), {
|
|
70
70
|
default: {},
|
|
@@ -76,7 +76,7 @@ exports.ConfigSchema = typebox_1.Type.Object({
|
|
|
76
76
|
title: "Record own vessel",
|
|
77
77
|
}),
|
|
78
78
|
recordOthers: typebox_1.Type.Boolean({
|
|
79
|
-
default:
|
|
79
|
+
default: true,
|
|
80
80
|
title: "Record AIS targets",
|
|
81
81
|
}),
|
|
82
82
|
retentionDays: typebox_1.Type.Number({
|
|
@@ -122,6 +122,19 @@ function normalizeConfig(config) {
|
|
|
122
122
|
paths: config.pathFilter?.paths ?? [],
|
|
123
123
|
},
|
|
124
124
|
samplingRates: config.samplingRates ?? {},
|
|
125
|
+
// Missing recording toggles get the schema defaults (both ON). The
|
|
126
|
+
// runtime guards read these directly, so without this a hand-edited or
|
|
127
|
+
// pre-panel config silently disabled the recording the schema promises.
|
|
128
|
+
recordSelf: config.recordSelf !== false,
|
|
129
|
+
recordOthers: config.recordOthers !== false,
|
|
130
|
+
// Missing resource limits get the schema defaults. Config-panel saves
|
|
131
|
+
// used to drop these keys entirely (issue #98), which ran QuestDB with
|
|
132
|
+
// NO memory cap instead of the promised 768m — this backfill repairs
|
|
133
|
+
// configs already damaged by those saves. The documented opt-outs
|
|
134
|
+
// survive: an explicit "" (memory) or 0 (CPU) means unlimited and is
|
|
135
|
+
// not a missing key.
|
|
136
|
+
questdbMemoryLimit: config.questdbMemoryLimit ?? "768m",
|
|
137
|
+
questdbCpuLimit: config.questdbCpuLimit ?? 1.5,
|
|
125
138
|
};
|
|
126
139
|
}
|
|
127
140
|
//# sourceMappingURL=schema.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":";;;AA+IA,
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/test/config.test.js
CHANGED
|
@@ -58,4 +58,54 @@ const schema_1 = require("../config/schema");
|
|
|
58
58
|
strict_1.default.equal(legacy.pathFilter, undefined, "input object must stay untouched");
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
(0, node_test_1.describe)("normalizeConfig recording toggles", () => {
|
|
62
|
+
(0, node_test_1.it)("defaults missing toggles to enabled, matching the schema", () => {
|
|
63
|
+
// A hand-edited or pre-panel config misses the keys entirely; the
|
|
64
|
+
// runtime guards read them directly, so missing must mean the schema
|
|
65
|
+
// default (both ON), not "disabled".
|
|
66
|
+
const legacy = { managedContainer: true };
|
|
67
|
+
const normalized = (0, schema_1.normalizeConfig)(legacy);
|
|
68
|
+
strict_1.default.equal(normalized.recordSelf, true);
|
|
69
|
+
strict_1.default.equal(normalized.recordOthers, true);
|
|
70
|
+
});
|
|
71
|
+
(0, node_test_1.it)("preserves an explicit opt-out", () => {
|
|
72
|
+
const config = {
|
|
73
|
+
recordSelf: false,
|
|
74
|
+
recordOthers: false,
|
|
75
|
+
};
|
|
76
|
+
const normalized = (0, schema_1.normalizeConfig)(config);
|
|
77
|
+
strict_1.default.equal(normalized.recordSelf, false);
|
|
78
|
+
strict_1.default.equal(normalized.recordOthers, false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
(0, node_test_1.describe)("normalizeConfig resource limits", () => {
|
|
82
|
+
(0, node_test_1.it)("backfills limits dropped by pre-fix panel saves (issue #98)", () => {
|
|
83
|
+
// The panel used to replace the config wholesale, stripping the resource
|
|
84
|
+
// keys — which ran QuestDB with NO memory cap instead of the schema's
|
|
85
|
+
// 768m. Missing keys must recover the schema defaults.
|
|
86
|
+
const damaged = { managedContainer: true };
|
|
87
|
+
const normalized = (0, schema_1.normalizeConfig)(damaged);
|
|
88
|
+
strict_1.default.equal(normalized.questdbMemoryLimit, "768m");
|
|
89
|
+
strict_1.default.equal(normalized.questdbCpuLimit, 1.5);
|
|
90
|
+
});
|
|
91
|
+
(0, node_test_1.it)("preserves the documented unlimited opt-outs", () => {
|
|
92
|
+
// "" (memory) and 0 (CPU) are explicit choices, not missing keys.
|
|
93
|
+
const config = {
|
|
94
|
+
questdbMemoryLimit: "",
|
|
95
|
+
questdbCpuLimit: 0,
|
|
96
|
+
};
|
|
97
|
+
const normalized = (0, schema_1.normalizeConfig)(config);
|
|
98
|
+
strict_1.default.equal(normalized.questdbMemoryLimit, "");
|
|
99
|
+
strict_1.default.equal(normalized.questdbCpuLimit, 0);
|
|
100
|
+
});
|
|
101
|
+
(0, node_test_1.it)("keeps explicit user limits untouched", () => {
|
|
102
|
+
const config = {
|
|
103
|
+
questdbMemoryLimit: "2g",
|
|
104
|
+
questdbCpuLimit: 3,
|
|
105
|
+
};
|
|
106
|
+
const normalized = (0, schema_1.normalizeConfig)(config);
|
|
107
|
+
strict_1.default.equal(normalized.questdbMemoryLimit, "2g");
|
|
108
|
+
strict_1.default.equal(normalized.questdbCpuLimit, 3);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
61
111
|
//# sourceMappingURL=config.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/test/config.test.ts"],"names":[],"mappings":";;;;;AAAA,yCAAyC;AACzC,gEAAwC;AACxC,6CAAgE;AAEhE,yEAAyE;AACzE,oEAAoE;AACpE,4EAA4E;AAC5E,0DAA0D;AAC1D,IAAA,oBAAQ,EAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,IAAA,cAAE,EAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,MAAM,GAAG;YACb,gBAAgB,EAAE,IAAI;YACtB,UAAU,EAAE,IAAI;SACI,CAAC;QAEvB,MAAM,UAAU,GAAG,IAAA,wBAAe,EAAC,MAAM,CAAC,CAAC;QAC3C,gBAAM,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,gBAAM,CAAC,SAAS,CAAC,UAAU,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;QAC/C,0EAA0E;QAC1E,wDAAwD;QACxD,gBAAM,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACpD,gBAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,IAAA,cAAE,EAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,OAAO,GAAG;YACd,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;SACX,CAAC;QACvB,MAAM,UAAU,GAAG,IAAA,wBAAe,EAAC,OAAO,CAAC,CAAC;QAC5C,gBAAM,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QAExE,MAAM,SAAS,GAAG;YAChB,UAAU,EAAE,EAAE,KAAK,EAAE,CAAC,cAAc,CAAC,EAAE;SACnB,CAAC;QACvB,gBAAM,CAAC,SAAS,CAAC,IAAA,wBAAe,EAAC,SAAS,CAAC,CAAC,UAAU,EAAE;YACtD,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,CAAC,cAAc,CAAC;SACxB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAA,cAAE,EAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,MAAM,GAAG;YACb,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,cAAc,CAAC,EAAE;YACxD,aAAa,EAAE,EAAE,oBAAoB,EAAE,GAAG,EAAE;SACxB,CAAC;QAEvB,MAAM,UAAU,GAAG,IAAA,wBAAe,EAAC,MAAM,CAAC,CAAC;QAC3C,gBAAM,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,EAAE;YACtC,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,CAAC,cAAc,CAAC;SACxB,CAAC,CAAC;QACH,gBAAM,CAAC,SAAS,CAAC,UAAU,CAAC,aAAa,EAAE;YACzC,oBAAoB,EAAE,GAAG;SAC1B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,IAAA,cAAE,EAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,MAAM,GAAG,EAAE,UAAU,EAAE,IAAI,EAAuB,CAAC;QACzD,IAAA,wBAAe,EAAC,MAAM,CAAC,CAAC;QACxB,gBAAM,CAAC,KAAK,CACT,MAAkC,CAAC,UAAU,EAC9C,SAAS,EACT,kCAAkC,CACnC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/test/config.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/public/805.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";(self.webpackChunksignalk_questdb=self.webpackChunksignalk_questdb||[]).push([[805],{805(e,t,a){a.r(t),a.d(t,{default:()=>i});var n=a(231);const l={root:{fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',color:"#333",padding:"16px 0"},sectionTitle:{fontSize:13,fontWeight:600,color:"#888",textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:10,marginTop:24},btn:{display:"inline-flex",alignItems:"center",gap:8,padding:"8px 16px",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},btnPrimary:{background:"#3b82f6",color:"#fff"},btnSave:{background:"#3b82f6",color:"#fff"},btnDanger:{background:"#ef4444",color:"#fff",padding:"6px 12px",fontSize:12},btnDisabled:{opacity:.5,cursor:"not-allowed"},status:{marginTop:8,fontSize:12,minHeight:18},warnBanner:{padding:"12px 16px",background:"#fef2f2",border:"1px solid #fecaca",borderRadius:10,marginBottom:12,fontSize:13,color:"#991b1b",lineHeight:1.5},warnBannerTitle:{fontWeight:700,marginBottom:4},infoBanner:{padding:"12px 16px",background:"#fffbeb",border:"1px solid #fde68a",borderRadius:10,marginBottom:12,fontSize:13,color:"#92400e",lineHeight:1.5},warnBannerCode:{display:"block",marginTop:8,padding:"8px 10px",background:"#fff",border:"1px solid #fecaca",borderRadius:6,fontFamily:"monospace",fontSize:12,color:"#7f1d1d",whiteSpace:"pre-wrap",wordBreak:"break-word"},card:{display:"flex",alignItems:"center",gap:14,padding:"14px 18px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,marginBottom:12},cardIcon:{width:44,height:44,borderRadius:10,display:"flex",alignItems:"center",justifyContent:"center",fontSize:20,fontWeight:700,flexShrink:0},cardInfo:{flex:1},cardTitle:{fontSize:15,fontWeight:600,color:"#333"},cardMeta:{fontSize:12,color:"#888"},stateIndicator:{width:10,height:10,borderRadius:"50%",flexShrink:0},fieldRow:{display:"flex",alignItems:"center",gap:12,marginBottom:10},label:{fontSize:13,fontWeight:500,color:"#555",width:180,flexShrink:0},select:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333",minWidth:200},input:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333",width:200},inputSmall:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333",width:80},checkbox:{width:16,height:16,accentColor:"#3b82f6"},hint:{fontSize:11,color:"#aaa",marginLeft:8},textarea:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,fontFamily:"monospace",background:"#fff",color:"#333",width:"100%",minHeight:70,boxSizing:"border-box",resize:"vertical"},empty:{textAlign:"center",padding:"30px 16px",color:"#999",fontSize:13},tag:{display:"inline-block",padding:"2px 8px",borderRadius:4,fontSize:10,fontWeight:600,marginLeft:8},tagPre:{background:"#fef3c7",color:"#92400e"},tagLatest:{background:"#dcfce7",color:"#166534"},migrationItem:{display:"flex",alignItems:"center",gap:12,padding:"12px 16px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,marginBottom:8},migrationActions:{display:"flex",gap:8,alignItems:"center"},statsGrid:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(140px, 1fr))",gap:10,marginBottom:12},statCard:{padding:"12px 16px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,textAlign:"center"},statValue:{fontSize:22,fontWeight:700,color:"#333"},statLabel:{fontSize:11,color:"#888",marginTop:2}};function r({title:e,children:t}){const[a,r]=(0,n.useState)(!1);return n.createElement("div",null,n.createElement("button",{type:"button","aria-expanded":a,style:{...l.sectionTitle,cursor:"pointer",userSelect:"none",display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",background:"none",border:"none",padding:0},onClick:()=>r(!a)},n.createElement("span",{style:{fontSize:10,transition:"transform 0.15s",transform:a?"rotate(90deg)":"rotate(0deg)"}},"▶"),e),a&&n.createElement("div",{style:{marginBottom:16}},t))}function s(e){return null==e?"—":e>=1e6?(e/1e6).toFixed(1)+"M":e>=1e3?(e/1e3).toFixed(1)+"K":String(e)}function i({configuration:e,save:t}){const a=e||{},[i,o]=(0,n.useState)(a.questdbHost||"127.0.0.1"),[c,d]=(0,n.useState)(a.questdbIlpPort||9009),[u,m]=(0,n.useState)(a.questdbHttpPort||9e3),[p,g]=(0,n.useState)(a.questdbPgPort||8812),[h,f]=(0,n.useState)(a.questdbVersion||"latest"),[b,y]=(0,n.useState)(!1!==a.managedContainer),[E,v]=(0,n.useState)(!1!==a.recordSelf),[w,k]=(0,n.useState)(a.recordOthers||!1),[x,S]=(0,n.useState)(a.defaultSamplingRate??2e3),[R,T]=(0,n.useState)(a.retentionDays||0),[B,C]=(0,n.useState)("include"===a.pathFilter?.mode?"include":"exclude"),[D,I]=(0,n.useState)((Array.isArray(a.pathFilter?.paths)?a.pathFilter.paths:[]).join("\n")),[L,$]=(0,n.useState)(a.compression||"lz4"),[z,P]=(0,n.useState)(a.compressionLevel||3),[q,Q]=(0,n.useState)(a.networkName||"sk-network"),[A,N]=(0,n.useState)(a.exposeToContainers||!1),[M,W]=(0,n.useState)([]),[j,F]=(0,n.useState)(!1),[U,O]=(0,n.useState)(null),[V,H]=(0,n.useState)(!0),[_,G]=(0,n.useState)(null),[K,Z]=(0,n.useState)(!1),[J,X]=(0,n.useState)(""),[Y,ee]=(0,n.useState)(""),[te,ae]=(0,n.useState)(!1),[ne,le]=(0,n.useState)(null),[re,se]=(0,n.useState)(!1),[ie,oe]=(0,n.useState)(!1),[ce,de]=(0,n.useState)(!1),[ue,me]=(0,n.useState)(!1),[pe,ge]=(0,n.useState)(""),[he,fe]=(0,n.useState)(null),[be,ye]=(0,n.useState)(!1),Ee=(0,n.useRef)(0),[ve,we]=(0,n.useState)(!1),ke=(0,n.useCallback)(async()=>{F(!0);try{const e=await fetch("/plugins/signalk-questdb/api/versions");if(e.ok){const t=await e.json();W(t)}}catch{}F(!1)},[]),xe=(0,n.useCallback)(async()=>{try{const e=await fetch("/plugins/signalk-questdb/api/status"),t=await e.json().catch(()=>null);O(t&&"object"==typeof t?t:{status:"not_running"})}catch{O({status:"not_running"})}H(!1)},[]),Se=(0,n.useCallback)(async e=>{const t=++Ee.current;try{const a=await fetch("/plugins/signalk-questdb/api/wal-diagnosis",{signal:e});if(t!==Ee.current)return;if(a.ok){const e=await a.json();if(t!==Ee.current)return;fe(e),ye(!1)}else ye(!0)}catch{t===Ee.current&&ye(!0)}},[]),Re=Boolean(U&&"running"===U.status&&(U.suspendedTables||[]).length>0);(0,n.useEffect)(()=>{if(!Re)return fe(null),void ye(!1);let e=!1,t=null;const a=new AbortController,n=async()=>{await Se(a.signal),e||(t=setTimeout(n,15e3))};return n(),()=>{e=!0,a.abort(),t&&clearTimeout(t)}},[Re,Se]),(0,n.useEffect)(()=>{ke(),xe();const e=setInterval(xe,5e3);return()=>clearInterval(e)},[ke,xe]);const[Te,Be]=(0,n.useState)(""),[Ce,De]=(0,n.useState)(""),[Ie,Le]=(0,n.useState)("parquet"),[$e,ze]=(0,n.useState)(!1),Pe=U&&"running"===U.status,qe=Pe&&U.suspendedTables||[],Qe=qe.length>0,Ae=Pe&&U.schemaMismatch||!1,Ne=Pe&&U.ulimitClamp||null,Me=U?.hostMaxMapCount?.tooLow&&U.hostMaxMapCount||null,We=M.filter(e=>!e.prerelease).slice(0,3),je=M.filter(e=>e.prerelease).slice(0,2);return n.createElement("div",{style:l.root},n.createElement("div",{style:l.sectionTitle},"QuestDB Status"),Me&&n.createElement("div",{style:l.infoBanner},n.createElement("div",{style:l.warnBannerTitle},"Host vm.max_map_count is low — ",s(Me.current)," ","(QuestDB recommends ",s(Me.recommended),")"),"QuestDB memory-maps every partition and WAL segment it touches. As the database grows, this kernel limit can run out — mmap then fails with out-of-memory errors even though RAM is free, which shows up as slow or failing history queries, can suspend recording, and can take QuestDB down entirely. Run this in a shell on the host machine — not inside the QuestDB container, which can neither change the kernel-global limit nor has sudo (takes effect immediately, no restart needed):",n.createElement("code",{style:l.warnBannerCode},`echo 'vm.max_map_count=${Me.recommended}' | sudo tee /etc/sysctl.d/99-questdb.conf && sudo sysctl --system`)),V?n.createElement("div",{style:l.empty},"Checking QuestDB..."):Pe?n.createElement(n.Fragment,null,Qe&&n.createElement("div",{style:l.warnBanner},n.createElement("div",{style:l.warnBannerTitle},"QuestDB WAL suspended — recording stalled"),"Rows are arriving but no longer commit, so the counts below have stopped advancing. This follows an error while QuestDB applied the write-ahead log — see the reason reported per table below. Resuming replays every pending transaction, so no data is lost.",n.createElement("code",{style:l.warnBannerCode},qe.map(e=>{const t=String(e.errorMessage||e.errorTag||"reason not reported").replace(/[\r\n]+/g," "),a=(he?.tables||[]).find(t=>t.name===e.name),n=a?.suspendedSince?` — frozen since ${a.suspendedSince}`:"";return`${e.name}: ${s(e.txnLag)} txns behind — ${t}${n}`}).join("\n")),(he?.tables||[]).filter(e=>e.applyError).map(e=>n.createElement("div",{key:e.name,style:{marginTop:8,fontSize:12}},"QuestDB's own log reports the apply failure on"," ",n.createElement("code",null,e.name)," as:",n.createElement("code",{style:l.warnBannerCode},e.applyError))),n.createElement("div",{style:{marginTop:10}},n.createElement("button",{style:{...l.btn,...l.btnPrimary,...ue||ve?l.btnDisabled:{}},disabled:ue||ve,onClick:async()=>{me(!0),ge("");try{const e=await fetch("/plugins/signalk-questdb/api/resume-wal",{method:"POST"}),t=await e.json().catch(()=>({error:e.statusText}));if(e.ok){const e=(t.results||[]).filter(e=>!e.ok);ge(0===e.length?`${t.message}. Recording resumes as the backlog drains — the banner clears once tables catch up.`:`${t.message}. `+e.map(e=>`${e.table}: ${e.error}`).join(" · ")),xe(),Se()}else ge(`Resume failed: ${t.error}`)}catch(e){ge(`Resume failed: ${e.message}`)}me(!1)}},ue?"Resuming...":"Resume recording")),qe.filter(e=>"failed"===e.autoResume).map(e=>{const t=(he?.tables||[]).find(t=>t.name===e.name);if(t?.partitionOpenFailure)return n.createElement("div",{key:e.name,style:{marginTop:12}},n.createElement("div",{style:l.warnBannerTitle},e.name,": table data damaged — automatic repair not possible"),"QuestDB fails while opening this table's stored data, before it reaches any pending transaction. That means the commit record on disk describes rows whose data never made it to storage — the signature of a power loss during a write. Skipping transactions cannot fix this (the failure happens before them) and would discard the ",s(e.txnLag)," pending transactions for nothing, so no repair button is offered. Recovering this table means repairing its commit record by hand; please open an issue with the error above. Recording continues into the other tables, and “Resume recording” will re-suspend this one within milliseconds — it is safe to press, but it cannot repair this damage.");const a=t?.skipPlan;return a?n.createElement("div",{key:e.name,style:{marginTop:12}},n.createElement("div",{style:l.warnBannerTitle},e.name,": automatic resume failed — WAL segment unreadable"),"A lossless resume was attempted and the table re-suspended at the same transaction: the write-ahead-log segment at the stall point cannot be read back (typically corrupted by an unclean shutdown mid-write). Replaying can never get past it. The only repair is to skip the unreadable segment, losing"," ",n.createElement("b",null,s(a.skippedTxns)," transaction(s)")," ","recorded between ",n.createElement("b",null,a.skipWindowStart)," and"," ",n.createElement("b",null,a.skipWindowEnd),a.tailSkip?" — and this is the newest pending segment, so skipping it drops the ENTIRE pending backlog":` — everything after the skipped segment (${s(Math.max(0,e.txnLag-a.skippedTxns))} txns) is replayed without loss`,".",n.createElement("div",{style:{marginTop:8}},n.createElement("button",{style:{...l.btn,...l.btnDanger,...ve||ue||be?l.btnDisabled:{}},disabled:ve||ue||be,onClick:()=>(async(e,t)=>{const a=t.tailSkip?"\n\nWARNING: this is the newest pending segment — skipping it drops the ENTIRE pending backlog.":"";if(window.confirm(`Skip the unreadable WAL segment on ${e}?\n\nThis permanently loses ${s(t.skippedTxns)} transaction(s) recorded between ${t.skipWindowStart} and ${t.skipWindowEnd}. Everything after the skipped segment is replayed without loss.`+a)){we(!0),ge("");try{const a=await fetch("/plugins/signalk-questdb/api/resume-wal/skip",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({table:e,confirmPlan:t})}),n=await a.json().catch(()=>({error:a.statusText}));a.ok?ge(n.message):ge(`Skip failed: ${n.error}`),xe(),Se()}catch(e){ge(`Skip failed: ${e.message}`)}we(!1)}})(e.name,a)},ve?"Skipping...":"Skip unreadable segment (loses data)"),be&&n.createElement("div",{style:{marginTop:6,fontSize:12}},"The diagnosis could not be refreshed — the numbers above may be stale. The skip re-enables once a refresh succeeds."))):null}),n.createElement("div",{style:{marginTop:8,fontSize:12}},"Manual fallback in the QuestDB SQL console (",n.createElement("code",null,":9000"),"), one statement at a time — avoid running it from several tabs, the console retries a busy statement forever:",n.createElement("code",{style:l.warnBannerCode},qe.map(e=>{const t=`"${String(e.name).replace(/"/g,'""')}"`,a=(he?.tables||[]).find(t=>t.name===e.name),n=[`ALTER TABLE ${t} RESUME WAL;`];return"failed"===e.autoResume&&a?.skipPlan&&!be&&n.push(`-- if the resume re-suspends: skip the unreadable segment (loses ${s(a.skipPlan.skippedTxns)} txns)`,`ALTER TABLE ${t} RESUME WAL FROM TXN ${a.skipPlan.skipToTxn};`),n.join("\n")}).join("\n")))),pe&&n.createElement("div",{style:l.infoBanner},pe),Ae&&n.createElement("div",{style:l.warnBanner},n.createElement("div",{style:l.warnBannerTitle},"QuestDB table schema mismatch — data not readable"),"A QuestDB table was re-created by ILP ingestion with the wrong timestamp column, so rows are being stored but reads filtering on time (history, Grafana, and the counts above) return nothing. The plugin rebuilds the affected table with the correct schema automatically within a minute; if this persists, restart the plugin. (The few rows written into the wrong-schema table are lost — they were unreadable anyway.)"),Ne&&n.createElement("div",{style:l.infoBanner},n.createElement("div",{style:l.warnBannerTitle},"QuestDB open-files limit capped by the host"),"QuestDB requested a ",n.createElement("code",null,Ne.ulimit)," limit of"," ",s(Ne.requested),", but this host only allows"," ",s(Ne.granted),", so it was capped. QuestDB is running on the lower limit. To grant the full value, raise the host limit for the user running the container runtime — under rootless Podman that means a systemd ",n.createElement("code",null,"user@.service")," ",n.createElement("code",null,"LimitNOFILE")," drop-in (editing"," ",n.createElement("code",null,"/etc/security/limits.conf")," alone is usually not enough) — then restart the Signal K server so the plugin can re-create the QuestDB container with the raised limit (signalk-container 1.25.3+; on older versions remove the container once with ",n.createElement("code",null,"podman rm -f sk-signalk-questdb")," instead). This warning clears itself once the container is running with the full value."," ",n.createElement("a",{href:"https://github.com/dirkwa/signalk-container#raising-the-open-files-limit-nofile",target:"_blank",rel:"noreferrer",style:{color:"#92400e",textDecoration:"underline"}},"Step-by-step instructions"),"."),n.createElement("div",{style:l.card},n.createElement("div",{style:{...l.cardIcon,background:"#7c3aed",color:"#fff"}},"Q"),n.createElement("div",{style:l.cardInfo},n.createElement("div",{style:l.cardTitle},"QuestDB"),n.createElement("div",{style:l.cardMeta},U?.endpoint||`${i}:${u}`," ","· ",Qe?"WAL suspended":"Recording")),n.createElement("div",{style:{...l.stateIndicator,background:Qe?"#ef4444":"#10b981"},title:Qe?"WAL suspended":"Running"})),n.createElement("div",{style:l.statsGrid},n.createElement("div",{style:l.statCard},n.createElement("div",{style:l.statValue},s(U.totalRows)),n.createElement("div",{style:l.statLabel},"Total Rows")),n.createElement("div",{style:l.statCard},n.createElement("div",{style:l.statValue},s(U.activePathsToday)),n.createElement("div",{style:l.statLabel},"Active Paths Today"))),n.createElement("div",{style:{display:"flex",alignItems:"center",gap:10,marginBottom:12}},ne&&ne.updateAvailable?n.createElement(n.Fragment,null,n.createElement("span",{style:{fontSize:13}},"v",ne.currentVersion," →"," ",n.createElement("strong",null,"v",ne.latestVersion)," available"),n.createElement("button",{style:{...l.btn,...l.btnPrimary,padding:"4px 12px",fontSize:12,...ie?l.btnDisabled:{}},onClick:async()=>{oe(!0),ee("Pulling new image and restarting..."),ae(!1);try{const e=await fetch("/plugins/signalk-questdb/api/update/apply",{method:"POST"});if(e.ok){const t=await e.json();ee(t.message),le(null),t.newVersion&&f(t.newVersion),xe()}else{const t=await e.json().catch(()=>({error:e.statusText}));ee(`Update failed: ${t.error}`),ae(!0)}}catch(e){ee(`Update failed: ${e.message}`),ae(!0)}oe(!1)},disabled:ie},ie?"Updating...":"Update QuestDB")):ne&&!ne.updateAvailable?n.createElement("span",{style:{fontSize:12,color:"#888"}},"v",ne.currentVersion," (up to date)"):n.createElement("button",{style:{...l.btn,padding:"4px 12px",fontSize:12,background:"#f1f5f9",color:"#475569",border:"1px solid #e2e8f0",...re?l.btnDisabled:{}},onClick:async()=>{se(!0);try{const e=await fetch("/plugins/signalk-questdb/api/update/check");e.ok&&le(await e.json())}catch{}se(!1)},disabled:re},re?"Checking...":"Check for updates"))):n.createElement("div",{style:l.card},n.createElement("div",{style:{...l.cardIcon,background:"#fef2f2",color:"#ef4444"}},"Q"),n.createElement("div",{style:l.cardInfo},n.createElement("div",{style:l.cardTitle},"QuestDB"),n.createElement("div",{style:l.cardMeta},"unhealthy"===U?.status?"Not responding":"Not running",b?" — enable plugin to start container":"")),n.createElement("div",{style:{...l.stateIndicator,background:"#ef4444"}})),n.createElement("div",{style:l.sectionTitle},"Image Version"),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"QuestDB version"),n.createElement("select",{style:l.select,value:h,onChange:e=>f(e.target.value)},n.createElement("option",{value:"latest"},"latest (recommended)"),je.map(e=>n.createElement("option",{key:e.tag,value:e.tag},e.tag," (pre-release)")),We.map((e,t)=>n.createElement("option",{key:e.tag,value:e.tag},e.tag,0===t?" (current stable)":""))),j&&n.createElement("span",{style:l.hint},"loading releases..."),n.createElement("button",{style:{...l.btn,...l.btnPrimary,padding:"4px 10px",fontSize:11},onClick:ke},"↻")),n.createElement("div",{style:l.sectionTitle},"Connection"),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Managed container"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:b,onChange:e=>y(e.target.checked)}),n.createElement("span",{style:l.hint},b?"signalk-container manages QuestDB":"Connect to external QuestDB")),!b&&n.createElement(n.Fragment,null,n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"QuestDB host"),n.createElement("input",{style:l.input,value:i,onChange:e=>o(e.target.value)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"HTTP port (queries)"),n.createElement("input",{style:l.inputSmall,type:"number",value:u,onChange:e=>m(Number(e.target.value))})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"ILP port (writes)"),n.createElement("input",{style:l.inputSmall,type:"number",value:c,onChange:e=>d(Number(e.target.value))}))),b&&n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Bind to 0.0.0.0"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:A,onChange:e=>N(e.target.checked)}),n.createElement("span",{style:{...l.hint,color:A?"#ef4444":void 0}},A?"Caution! This exposes your data to the network":"Signal K reaches QuestDB automatically — only enable to reach it from another machine or a separate-Docker Grafana")),b&&A&&n.createElement(n.Fragment,null,n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"HTTP port (queries)"),n.createElement("input",{style:l.inputSmall,type:"number",value:u,onChange:e=>m(Number(e.target.value))})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"ILP port (writes)"),n.createElement("input",{style:l.inputSmall,type:"number",value:c,onChange:e=>d(Number(e.target.value))})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"PostgreSQL wire port"),n.createElement("input",{style:l.inputSmall,type:"number",value:p,onChange:e=>g(Number(e.target.value))}),n.createElement("span",{style:l.hint},"for Grafana")),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Container network"),n.createElement("input",{style:l.input,value:q,onChange:e=>Q(e.target.value)}),n.createElement("span",{style:l.hint},"shared with signalk-grafana"))),n.createElement("div",{style:l.sectionTitle},"Recording"),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Default sampling rate (ms)"),n.createElement("input",{style:l.inputSmall,type:"number",value:x,onChange:e=>S(Number(e.target.value))}),n.createElement("span",{style:l.hint},"1000 = max 1 write/sec per path (0 = every update)")),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Record own vessel"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:E,onChange:e=>v(e.target.checked)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Record AIS targets"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:w,onChange:e=>k(e.target.checked)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Retention (days)"),n.createElement("input",{style:l.inputSmall,type:"number",value:R,onChange:e=>T(Number(e.target.value))}),n.createElement("span",{style:l.hint},"0 = keep forever")),n.createElement(r,{title:"Path filtering"},n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Filter mode"),n.createElement("select",{style:l.select,value:B,onChange:e=>C(e.target.value)},n.createElement("option",{value:"exclude"},"Exclude matching paths"),n.createElement("option",{value:"include"},"Include only matching paths"))),n.createElement("div",{style:{marginBottom:10}},n.createElement("div",{style:{...l.label,width:"auto",marginBottom:6}},"Path patterns (one per line, glob supported)"),n.createElement("textarea",{style:l.textarea,value:D,onChange:e=>I(e.target.value),placeholder:"navigation.position\nnavigation.*\nnotifications.*"}),n.createElement("div",{style:l.hint},"exclude"===B?"These paths are NOT recorded; everything else is.":"ONLY these paths are recorded; everything else is dropped."," ","Leave empty to record everything."))),n.createElement(r,{title:"Compression (on-disk)"},n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Compression codec"),n.createElement("select",{style:l.select,value:L,onChange:e=>$(e.target.value)},n.createElement("option",{value:"none"},"None"),n.createElement("option",{value:"lz4"},"LZ4 (fast)"),n.createElement("option",{value:"zstd"},"ZSTD (smaller)")),n.createElement("span",{style:l.hint},"applies to new data after save")),"zstd"===L&&n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"ZSTD level"),n.createElement("input",{style:l.inputSmall,type:"number",min:"1",max:"22",value:z,onChange:e=>P(Number(e.target.value))}),n.createElement("span",{style:l.hint},"1 (fast) to 22 (smallest)"))),n.createElement(r,{title:"InfluxDB Migration"},n.createElement("div",{style:{display:"flex",gap:10,alignItems:"center",marginBottom:10}},n.createElement("button",{style:{...l.btn,...l.btnPrimary,...K?l.btnDisabled:{}},onClick:async()=>{Z(!0),ee("");try{const e=J?`?url=${encodeURIComponent(J)}`:"",t=await fetch(`/plugins/signalk-questdb/api/migration/detect${e}`);if(t.ok){const e=await t.json();G(e.sources),0===e.sources.length&&(ee(J?`No InfluxDB found at ${J}.`:"No InfluxDB instances detected on localhost:8086."),ae(!1))}}catch(e){ee("Detection failed: "+e.message),ae(!0)}Z(!1)},disabled:K},K?"Detecting...":"Detect InfluxDB"),n.createElement("span",{style:l.hint},"Checks localhost:8086 for InfluxDB 1.x and 2.x")),_&&_.length>0&&n.createElement("div",null,_.map((e,t)=>n.createElement("div",{key:t,style:l.migrationItem},n.createElement("div",{style:{...l.cardIcon,width:36,height:36,fontSize:16,background:"influxdb2"===e.type?"#020a47":"#22adf6",color:"#fff"}},"influxdb2"===e.type?"2":"1"),n.createElement("div",{style:l.cardInfo},n.createElement("div",{style:l.cardTitle},"InfluxDB ","influxdb2"===e.type?"2.x":"1.x",n.createElement("span",{style:{...l.tag,...l.tagLatest}},e.status)),n.createElement("div",{style:l.cardMeta},e.url," · v",e.version))))),n.createElement("div",{style:{...l.fieldRow,marginTop:10}},n.createElement("span",{style:l.label},"Manual InfluxDB URL"),n.createElement("input",{style:l.input,placeholder:"http://192.168.1.100:8086",value:J,onChange:e=>X(e.target.value)}),n.createElement("span",{style:l.hint},"for remote instances")),_&&0===_.length&&!K&&n.createElement("div",{style:{...l.empty,padding:"16px",textAlign:"left"}},"No InfluxDB found on localhost. Use manual URL above for remote instances.")),n.createElement(r,{title:"Data Export"},n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"From"),n.createElement("input",{style:l.input,type:"datetime-local",value:Te,onChange:e=>Be(e.target.value)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"To"),n.createElement("input",{style:l.input,type:"datetime-local",value:Ce,onChange:e=>De(e.target.value)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Format"),n.createElement("select",{style:l.select,value:Ie,onChange:e=>Le(e.target.value)},n.createElement("option",{value:"parquet"},"Parquet"),n.createElement("option",{value:"csv"},"CSV"))),n.createElement("button",{style:{...l.btn,...l.btnPrimary,...$e?l.btnDisabled:{}},onClick:async()=>{if(!Te||!Ce)return ee("Set both from and to dates for export."),void ae(!0);ze(!0),ee(`Exporting ${Ie.toUpperCase()}...`),ae(!1);try{const e=`/plugins/signalk-questdb/api/export?from=${encodeURIComponent(Te)}&to=${encodeURIComponent(Ce)}&format=${Ie}`,t=await fetch(e);if(!t.ok){const e=await t.json().catch(()=>({error:t.statusText}));return ee(`Export failed: ${e.error}`),ae(!0),void ze(!1)}const a=await t.blob(),n="parquet"===Ie?"parquet":"csv",l=document.createElement("a");l.href=URL.createObjectURL(a),l.download=`signalk-export-${Te.slice(0,10)}.${n}`,l.click(),URL.revokeObjectURL(l.href),ee(`Exported ${(a.size/1024/1024).toFixed(1)} MB`)}catch(e){ee(`Export failed: ${e.message}`),ae(!0)}ze(!1)},disabled:$e},$e?"Exporting...":"Export Data")),!1!==a.managedContainer&&n.createElement(r,{title:"Danger zone"},n.createElement("div",{style:{fontSize:13,color:"#555",marginBottom:10}},"Remove the QuestDB container and permanently delete all recorded data. Use this to fully reset QuestDB — Signal K's plugin-uninstall cannot delete this data on rootless Podman. This cannot be undone."),n.createElement("button",{style:{...l.btn,...l.btnDanger,...ce?l.btnDisabled:{}},onClick:async()=>{if(window.confirm("Remove the QuestDB container and DELETE ALL recorded data? This cannot be undone.")){de(!0),ee("Removing QuestDB container and data..."),ae(!1);try{const e=await fetch("/plugins/signalk-questdb/api/purge-data",{method:"POST"}),t=await e.json().catch(()=>({error:e.statusText}));e.ok?(ee(t.message),xe()):(ee(`Remove failed: ${t.error}`),ae(!0))}catch(e){ee(`Remove failed: ${e.message}`),ae(!0)}de(!1)}},disabled:ce},ce?"Removing...":"Remove container & all data")),Y&&n.createElement("div",{style:{...l.status,color:te?"#ef4444":"#10b981",marginTop:16}},Y),n.createElement("div",{style:{marginTop:24}},n.createElement("button",{style:{...l.btn,...l.btnSave},onClick:()=>{t({questdbHost:i,questdbIlpPort:c,questdbHttpPort:u,questdbPgPort:p,questdbVersion:h,managedContainer:b,defaultSamplingRate:x,recordSelf:E,recordOthers:w,retentionDays:R,compression:L,compressionLevel:z,networkName:q,exposeToContainers:A,pathFilter:{mode:B,paths:D.split("\n").map(e=>e.trim()).filter(Boolean)},samplingRates:a.samplingRates||{}}),ee("Saved! Plugin will restart with new configuration."),ae(!1)}},"Save Configuration")))}}}]);
|
|
1
|
+
"use strict";(self.webpackChunksignalk_questdb=self.webpackChunksignalk_questdb||[]).push([[805],{805(e,t,a){a.r(t),a.d(t,{default:()=>i});var n=a(231);const l={root:{fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',color:"#333",padding:"16px 0"},sectionTitle:{fontSize:13,fontWeight:600,color:"#888",textTransform:"uppercase",letterSpacing:"0.05em",marginBottom:10,marginTop:24},btn:{display:"inline-flex",alignItems:"center",gap:8,padding:"8px 16px",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},btnPrimary:{background:"#3b82f6",color:"#fff"},btnSave:{background:"#3b82f6",color:"#fff"},btnDanger:{background:"#ef4444",color:"#fff",padding:"6px 12px",fontSize:12},btnDisabled:{opacity:.5,cursor:"not-allowed"},status:{marginTop:8,fontSize:12,minHeight:18},warnBanner:{padding:"12px 16px",background:"#fef2f2",border:"1px solid #fecaca",borderRadius:10,marginBottom:12,fontSize:13,color:"#991b1b",lineHeight:1.5},warnBannerTitle:{fontWeight:700,marginBottom:4},infoBanner:{padding:"12px 16px",background:"#fffbeb",border:"1px solid #fde68a",borderRadius:10,marginBottom:12,fontSize:13,color:"#92400e",lineHeight:1.5},warnBannerCode:{display:"block",marginTop:8,padding:"8px 10px",background:"#fff",border:"1px solid #fecaca",borderRadius:6,fontFamily:"monospace",fontSize:12,color:"#7f1d1d",whiteSpace:"pre-wrap",wordBreak:"break-word"},card:{display:"flex",alignItems:"center",gap:14,padding:"14px 18px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,marginBottom:12},cardIcon:{width:44,height:44,borderRadius:10,display:"flex",alignItems:"center",justifyContent:"center",fontSize:20,fontWeight:700,flexShrink:0},cardInfo:{flex:1},cardTitle:{fontSize:15,fontWeight:600,color:"#333"},cardMeta:{fontSize:12,color:"#888"},stateIndicator:{width:10,height:10,borderRadius:"50%",flexShrink:0},fieldRow:{display:"flex",alignItems:"center",gap:12,marginBottom:10},label:{fontSize:13,fontWeight:500,color:"#555",width:180,flexShrink:0},select:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333",minWidth:200},input:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333",width:200},inputSmall:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,background:"#fff",color:"#333",width:80},checkbox:{width:16,height:16,accentColor:"#3b82f6"},hint:{fontSize:11,color:"#aaa",marginLeft:8},textarea:{padding:"6px 10px",borderRadius:6,border:"1px solid #ccc",fontSize:13,fontFamily:"monospace",background:"#fff",color:"#333",width:"100%",minHeight:70,boxSizing:"border-box",resize:"vertical"},empty:{textAlign:"center",padding:"30px 16px",color:"#999",fontSize:13},tag:{display:"inline-block",padding:"2px 8px",borderRadius:4,fontSize:10,fontWeight:600,marginLeft:8},tagPre:{background:"#fef3c7",color:"#92400e"},tagLatest:{background:"#dcfce7",color:"#166534"},migrationItem:{display:"flex",alignItems:"center",gap:12,padding:"12px 16px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,marginBottom:8},migrationActions:{display:"flex",gap:8,alignItems:"center"},statsGrid:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(140px, 1fr))",gap:10,marginBottom:12},statCard:{padding:"12px 16px",background:"#f8f9fa",border:"1px solid #e0e0e0",borderRadius:10,textAlign:"center"},statValue:{fontSize:22,fontWeight:700,color:"#333"},statLabel:{fontSize:11,color:"#888",marginTop:2}};function r({title:e,children:t}){const[a,r]=(0,n.useState)(!1);return n.createElement("div",null,n.createElement("button",{type:"button","aria-expanded":a,style:{...l.sectionTitle,cursor:"pointer",userSelect:"none",display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",background:"none",border:"none",padding:0},onClick:()=>r(!a)},n.createElement("span",{style:{fontSize:10,transition:"transform 0.15s",transform:a?"rotate(90deg)":"rotate(0deg)"}},"▶"),e),a&&n.createElement("div",{style:{marginBottom:16}},t))}function s(e){return null==e?"—":e>=1e6?(e/1e6).toFixed(1)+"M":e>=1e3?(e/1e3).toFixed(1)+"K":String(e)}function i({configuration:e,save:t}){const a=e||{},[i,o]=(0,n.useState)(a.questdbHost||"127.0.0.1"),[c,d]=(0,n.useState)(a.questdbIlpPort||9009),[u,m]=(0,n.useState)(a.questdbHttpPort||9e3),[p,g]=(0,n.useState)(a.questdbPgPort||8812),[h,f]=(0,n.useState)(a.questdbVersion||"latest"),[b,y]=(0,n.useState)(!1!==a.managedContainer),[E,v]=(0,n.useState)(!1!==a.recordSelf),[w,k]=(0,n.useState)(!1!==a.recordOthers),[x,S]=(0,n.useState)(a.defaultSamplingRate??2e3),[R,T]=(0,n.useState)(String(a.ilpFlushIntervalMs??5e3)),[B,C]=(0,n.useState)(a.retentionDays||0),[D,I]=(0,n.useState)("include"===a.pathFilter?.mode?"include":"exclude"),[L,$]=(0,n.useState)((Array.isArray(a.pathFilter?.paths)?a.pathFilter.paths:[]).join("\n")),[z,P]=(0,n.useState)(a.compression||"lz4"),[q,Q]=(0,n.useState)(a.compressionLevel||3),[A,N]=(0,n.useState)(a.networkName||"sk-network"),[W,F]=(0,n.useState)(a.exposeToContainers||!1),[M,j]=(0,n.useState)([]),[U,O]=(0,n.useState)(!1),[V,H]=(0,n.useState)(null),[_,G]=(0,n.useState)(!0),[K,Z]=(0,n.useState)(null),[J,X]=(0,n.useState)(!1),[Y,ee]=(0,n.useState)(""),[te,ae]=(0,n.useState)(""),[ne,le]=(0,n.useState)(!1),[re,se]=(0,n.useState)(null),[ie,oe]=(0,n.useState)(!1),[ce,de]=(0,n.useState)(!1),[ue,me]=(0,n.useState)(!1),[pe,ge]=(0,n.useState)(!1),[he,fe]=(0,n.useState)(""),[be,ye]=(0,n.useState)(null),[Ee,ve]=(0,n.useState)(!1),we=(0,n.useRef)(0),[ke,xe]=(0,n.useState)(!1),Se=(0,n.useCallback)(async()=>{O(!0);try{const e=await fetch("/plugins/signalk-questdb/api/versions");if(e.ok){const t=await e.json();j(t)}}catch{}O(!1)},[]),Re=(0,n.useCallback)(async()=>{try{const e=await fetch("/plugins/signalk-questdb/api/status"),t=await e.json().catch(()=>null);H(t&&"object"==typeof t?t:{status:"not_running"})}catch{H({status:"not_running"})}G(!1)},[]),Te=(0,n.useCallback)(async e=>{const t=++we.current;try{const a=await fetch("/plugins/signalk-questdb/api/wal-diagnosis",{signal:e});if(t!==we.current)return;if(a.ok){const e=await a.json();if(t!==we.current)return;ye(e),ve(!1)}else ve(!0)}catch{t===we.current&&ve(!0)}},[]),Be=Boolean(V&&"running"===V.status&&(V.suspendedTables||[]).length>0);(0,n.useEffect)(()=>{if(!Be)return ye(null),void ve(!1);let e=!1,t=null;const a=new AbortController,n=async()=>{await Te(a.signal),e||(t=setTimeout(n,15e3))};return n(),()=>{e=!0,a.abort(),t&&clearTimeout(t)}},[Be,Te]),(0,n.useEffect)(()=>{Se(),Re();const e=setInterval(Re,5e3);return()=>clearInterval(e)},[Se,Re]);const[Ce,De]=(0,n.useState)(""),[Ie,Le]=(0,n.useState)(""),[$e,ze]=(0,n.useState)("parquet"),[Pe,qe]=(0,n.useState)(!1),Qe=V&&"running"===V.status,Ae=Qe&&V.suspendedTables||[],Ne=Ae.length>0,We=Qe&&V.schemaMismatch||!1,Fe=Qe&&V.ulimitClamp||null,Me=V?.hostMaxMapCount?.tooLow&&V.hostMaxMapCount||null,je=M.filter(e=>!e.prerelease).slice(0,3),Ue=M.filter(e=>e.prerelease).slice(0,2);return n.createElement("div",{style:l.root},n.createElement("div",{style:l.sectionTitle},"QuestDB Status"),Me&&n.createElement("div",{style:l.infoBanner},n.createElement("div",{style:l.warnBannerTitle},"Host vm.max_map_count is low — ",s(Me.current)," ","(QuestDB recommends ",s(Me.recommended),")"),"QuestDB memory-maps every partition and WAL segment it touches. As the database grows, this kernel limit can run out — mmap then fails with out-of-memory errors even though RAM is free, which shows up as slow or failing history queries, can suspend recording, and can take QuestDB down entirely. Run this in a shell on the host machine — not inside the QuestDB container, which can neither change the kernel-global limit nor has sudo (takes effect immediately, no restart needed):",n.createElement("code",{style:l.warnBannerCode},`echo 'vm.max_map_count=${Me.recommended}' | sudo tee /etc/sysctl.d/99-questdb.conf && sudo sysctl --system`)),_?n.createElement("div",{style:l.empty},"Checking QuestDB..."):Qe?n.createElement(n.Fragment,null,Ne&&n.createElement("div",{style:l.warnBanner},n.createElement("div",{style:l.warnBannerTitle},"QuestDB WAL suspended — recording stalled"),"Rows are arriving but no longer commit, so the counts below have stopped advancing. This follows an error while QuestDB applied the write-ahead log — see the reason reported per table below. Resuming replays every pending transaction, so no data is lost.",n.createElement("code",{style:l.warnBannerCode},Ae.map(e=>{const t=String(e.errorMessage||e.errorTag||"reason not reported").replace(/[\r\n]+/g," "),a=(be?.tables||[]).find(t=>t.name===e.name),n=a?.suspendedSince?` — frozen since ${a.suspendedSince}`:"";return`${e.name}: ${s(e.txnLag)} txns behind — ${t}${n}`}).join("\n")),(be?.tables||[]).filter(e=>e.applyError).map(e=>n.createElement("div",{key:e.name,style:{marginTop:8,fontSize:12}},"QuestDB's own log reports the apply failure on"," ",n.createElement("code",null,e.name)," as:",n.createElement("code",{style:l.warnBannerCode},e.applyError))),n.createElement("div",{style:{marginTop:10}},n.createElement("button",{style:{...l.btn,...l.btnPrimary,...pe||ke?l.btnDisabled:{}},disabled:pe||ke,onClick:async()=>{ge(!0),fe("");try{const e=await fetch("/plugins/signalk-questdb/api/resume-wal",{method:"POST"}),t=await e.json().catch(()=>({error:e.statusText}));if(e.ok){const e=(t.results||[]).filter(e=>!e.ok);fe(0===e.length?`${t.message}. Recording resumes as the backlog drains — the banner clears once tables catch up.`:`${t.message}. `+e.map(e=>`${e.table}: ${e.error}`).join(" · ")),Re(),Te()}else fe(`Resume failed: ${t.error}`)}catch(e){fe(`Resume failed: ${e.message}`)}ge(!1)}},pe?"Resuming...":"Resume recording")),Ae.filter(e=>"failed"===e.autoResume).map(e=>{const t=(be?.tables||[]).find(t=>t.name===e.name);if(t?.partitionOpenFailure)return n.createElement("div",{key:e.name,style:{marginTop:12}},n.createElement("div",{style:l.warnBannerTitle},e.name,": table data damaged — automatic repair not possible"),"QuestDB fails while opening this table's stored data, before it reaches any pending transaction. That means the commit record on disk describes rows whose data never made it to storage — the signature of a power loss during a write. Skipping transactions cannot fix this (the failure happens before them) and would discard the ",s(e.txnLag)," pending transactions for nothing, so no repair button is offered. Recovering this table means repairing its commit record by hand; please open an issue with the error above. Recording continues into the other tables, and “Resume recording” will re-suspend this one within milliseconds — it is safe to press, but it cannot repair this damage.");const a=t?.skipPlan;return a?n.createElement("div",{key:e.name,style:{marginTop:12}},n.createElement("div",{style:l.warnBannerTitle},e.name,": automatic resume failed — WAL segment unreadable"),"A lossless resume was attempted and the table re-suspended at the same transaction: the write-ahead-log segment at the stall point cannot be read back (typically corrupted by an unclean shutdown mid-write). Replaying can never get past it. The only repair is to skip the unreadable segment, losing"," ",n.createElement("b",null,s(a.skippedTxns)," transaction(s)")," ","recorded between ",n.createElement("b",null,a.skipWindowStart)," and"," ",n.createElement("b",null,a.skipWindowEnd),a.tailSkip?" — and this is the newest pending segment, so skipping it drops the ENTIRE pending backlog":` — everything after the skipped segment (${s(Math.max(0,e.txnLag-a.skippedTxns))} txns) is replayed without loss`,".",n.createElement("div",{style:{marginTop:8}},n.createElement("button",{style:{...l.btn,...l.btnDanger,...ke||pe||Ee?l.btnDisabled:{}},disabled:ke||pe||Ee,onClick:()=>(async(e,t)=>{const a=t.tailSkip?"\n\nWARNING: this is the newest pending segment — skipping it drops the ENTIRE pending backlog.":"";if(window.confirm(`Skip the unreadable WAL segment on ${e}?\n\nThis permanently loses ${s(t.skippedTxns)} transaction(s) recorded between ${t.skipWindowStart} and ${t.skipWindowEnd}. Everything after the skipped segment is replayed without loss.`+a)){xe(!0),fe("");try{const a=await fetch("/plugins/signalk-questdb/api/resume-wal/skip",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({table:e,confirmPlan:t})}),n=await a.json().catch(()=>({error:a.statusText}));a.ok?fe(n.message):fe(`Skip failed: ${n.error}`),Re(),Te()}catch(e){fe(`Skip failed: ${e.message}`)}xe(!1)}})(e.name,a)},ke?"Skipping...":"Skip unreadable segment (loses data)"),Ee&&n.createElement("div",{style:{marginTop:6,fontSize:12}},"The diagnosis could not be refreshed — the numbers above may be stale. The skip re-enables once a refresh succeeds."))):null}),n.createElement("div",{style:{marginTop:8,fontSize:12}},"Manual fallback in the QuestDB SQL console (",n.createElement("code",null,":9000"),"), one statement at a time — avoid running it from several tabs, the console retries a busy statement forever:",n.createElement("code",{style:l.warnBannerCode},Ae.map(e=>{const t=`"${String(e.name).replace(/"/g,'""')}"`,a=(be?.tables||[]).find(t=>t.name===e.name),n=[`ALTER TABLE ${t} RESUME WAL;`];return"failed"===e.autoResume&&a?.skipPlan&&!Ee&&n.push(`-- if the resume re-suspends: skip the unreadable segment (loses ${s(a.skipPlan.skippedTxns)} txns)`,`ALTER TABLE ${t} RESUME WAL FROM TXN ${a.skipPlan.skipToTxn};`),n.join("\n")}).join("\n")))),he&&n.createElement("div",{style:l.infoBanner},he),We&&n.createElement("div",{style:l.warnBanner},n.createElement("div",{style:l.warnBannerTitle},"QuestDB table schema mismatch — data not readable"),"A QuestDB table was re-created by ILP ingestion with the wrong timestamp column, so rows are being stored but reads filtering on time (history, Grafana, and the counts above) return nothing. The plugin rebuilds the affected table with the correct schema automatically within a minute; if this persists, restart the plugin. (The few rows written into the wrong-schema table are lost — they were unreadable anyway.)"),Fe&&n.createElement("div",{style:l.infoBanner},n.createElement("div",{style:l.warnBannerTitle},"QuestDB open-files limit capped by the host"),"QuestDB requested a ",n.createElement("code",null,Fe.ulimit)," limit of"," ",s(Fe.requested),", but this host only allows"," ",s(Fe.granted),", so it was capped. QuestDB is running on the lower limit. To grant the full value, raise the host limit for the user running the container runtime — under rootless Podman that means a systemd ",n.createElement("code",null,"user@.service")," ",n.createElement("code",null,"LimitNOFILE")," drop-in (editing"," ",n.createElement("code",null,"/etc/security/limits.conf")," alone is usually not enough) — then restart the Signal K server so the plugin can re-create the QuestDB container with the raised limit (signalk-container 1.25.3+; on older versions remove the container once with ",n.createElement("code",null,"podman rm -f sk-signalk-questdb")," instead). This warning clears itself once the container is running with the full value."," ",n.createElement("a",{href:"https://github.com/dirkwa/signalk-container#raising-the-open-files-limit-nofile",target:"_blank",rel:"noreferrer",style:{color:"#92400e",textDecoration:"underline"}},"Step-by-step instructions"),"."),n.createElement("div",{style:l.card},n.createElement("div",{style:{...l.cardIcon,background:"#7c3aed",color:"#fff"}},"Q"),n.createElement("div",{style:l.cardInfo},n.createElement("div",{style:l.cardTitle},"QuestDB"),n.createElement("div",{style:l.cardMeta},V?.endpoint||`${i}:${u}`," ","· ",Ne?"WAL suspended":"Recording")),n.createElement("div",{style:{...l.stateIndicator,background:Ne?"#ef4444":"#10b981"},title:Ne?"WAL suspended":"Running"})),n.createElement("div",{style:l.statsGrid},n.createElement("div",{style:l.statCard},n.createElement("div",{style:l.statValue},s(V.totalRows)),n.createElement("div",{style:l.statLabel},"Total Rows")),n.createElement("div",{style:l.statCard},n.createElement("div",{style:l.statValue},s(V.activePathsToday)),n.createElement("div",{style:l.statLabel},"Active Paths Today"))),n.createElement("div",{style:{display:"flex",alignItems:"center",gap:10,marginBottom:12}},re&&re.updateAvailable?n.createElement(n.Fragment,null,n.createElement("span",{style:{fontSize:13}},"v",re.currentVersion," →"," ",n.createElement("strong",null,"v",re.latestVersion)," available"),n.createElement("button",{style:{...l.btn,...l.btnPrimary,padding:"4px 12px",fontSize:12,...ce?l.btnDisabled:{}},onClick:async()=>{de(!0),ae("Pulling new image and restarting..."),le(!1);try{const e=await fetch("/plugins/signalk-questdb/api/update/apply",{method:"POST"});if(e.ok){const t=await e.json();ae(t.message),se(null),t.newVersion&&f(t.newVersion),Re()}else{const t=await e.json().catch(()=>({error:e.statusText}));ae(`Update failed: ${t.error}`),le(!0)}}catch(e){ae(`Update failed: ${e.message}`),le(!0)}de(!1)},disabled:ce},ce?"Updating...":"Update QuestDB")):re&&!re.updateAvailable?n.createElement("span",{style:{fontSize:12,color:"#888"}},"v",re.currentVersion," (up to date)"):n.createElement("button",{style:{...l.btn,padding:"4px 12px",fontSize:12,background:"#f1f5f9",color:"#475569",border:"1px solid #e2e8f0",...ie?l.btnDisabled:{}},onClick:async()=>{oe(!0);try{const e=await fetch("/plugins/signalk-questdb/api/update/check");e.ok&&se(await e.json())}catch{}oe(!1)},disabled:ie},ie?"Checking...":"Check for updates"))):n.createElement("div",{style:l.card},n.createElement("div",{style:{...l.cardIcon,background:"#fef2f2",color:"#ef4444"}},"Q"),n.createElement("div",{style:l.cardInfo},n.createElement("div",{style:l.cardTitle},"QuestDB"),n.createElement("div",{style:l.cardMeta},"unhealthy"===V?.status?"Not responding":"Not running",b?" — enable plugin to start container":"")),n.createElement("div",{style:{...l.stateIndicator,background:"#ef4444"}})),n.createElement("div",{style:l.sectionTitle},"Image Version"),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"QuestDB version"),n.createElement("select",{style:l.select,value:h,onChange:e=>f(e.target.value)},n.createElement("option",{value:"latest"},"latest (recommended)"),Ue.map(e=>n.createElement("option",{key:e.tag,value:e.tag},e.tag," (pre-release)")),je.map((e,t)=>n.createElement("option",{key:e.tag,value:e.tag},e.tag,0===t?" (current stable)":""))),U&&n.createElement("span",{style:l.hint},"loading releases..."),n.createElement("button",{style:{...l.btn,...l.btnPrimary,padding:"4px 10px",fontSize:11},onClick:Se},"↻")),n.createElement("div",{style:l.sectionTitle},"Connection"),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Managed container"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:b,onChange:e=>y(e.target.checked)}),n.createElement("span",{style:l.hint},b?"signalk-container manages QuestDB":"Connect to external QuestDB")),!b&&n.createElement(n.Fragment,null,n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"QuestDB host"),n.createElement("input",{style:l.input,value:i,onChange:e=>o(e.target.value)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"HTTP port (queries)"),n.createElement("input",{style:l.inputSmall,type:"number",value:u,onChange:e=>m(Number(e.target.value))})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"ILP port (writes)"),n.createElement("input",{style:l.inputSmall,type:"number",value:c,onChange:e=>d(Number(e.target.value))}))),b&&n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Bind to 0.0.0.0"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:W,onChange:e=>F(e.target.checked)}),n.createElement("span",{style:{...l.hint,color:W?"#ef4444":void 0}},W?"Caution! This exposes your data to the network":"Signal K reaches QuestDB automatically — only enable to reach it from another machine or a separate-Docker Grafana")),b&&W&&n.createElement(n.Fragment,null,n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"HTTP port (queries)"),n.createElement("input",{style:l.inputSmall,type:"number",value:u,onChange:e=>m(Number(e.target.value))})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"ILP port (writes)"),n.createElement("input",{style:l.inputSmall,type:"number",value:c,onChange:e=>d(Number(e.target.value))})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"PostgreSQL wire port"),n.createElement("input",{style:l.inputSmall,type:"number",value:p,onChange:e=>g(Number(e.target.value))}),n.createElement("span",{style:l.hint},"for Grafana")),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Container network"),n.createElement("input",{style:l.input,value:A,onChange:e=>N(e.target.value)}),n.createElement("span",{style:l.hint},"shared with signalk-grafana"))),n.createElement("div",{style:l.sectionTitle},"Recording"),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Default sampling rate (ms)"),n.createElement("input",{style:l.inputSmall,type:"number",value:x,onChange:e=>S(Number(e.target.value))}),n.createElement("span",{style:l.hint},"1000 = max 1 write/sec per path (0 = every update)")),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Write batch interval (ms)"),n.createElement("input",{style:l.inputSmall,type:"number",min:500,max:3e5,"aria-label":"Write batch interval in milliseconds",value:R,onChange:e=>T(e.target.value)}),n.createElement("span",{style:l.hint},"one QuestDB transaction per table per interval; longer = bigger batches, but up to this many ms lost on a hard crash (default 5000)")),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Record own vessel"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:E,onChange:e=>v(e.target.checked)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Record AIS targets"),n.createElement("input",{type:"checkbox",style:l.checkbox,checked:w,onChange:e=>k(e.target.checked)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Retention (days)"),n.createElement("input",{style:l.inputSmall,type:"number",value:B,onChange:e=>C(Number(e.target.value))}),n.createElement("span",{style:l.hint},"0 = keep forever")),n.createElement(r,{title:"Path filtering"},n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Filter mode"),n.createElement("select",{style:l.select,value:D,onChange:e=>I(e.target.value)},n.createElement("option",{value:"exclude"},"Exclude matching paths"),n.createElement("option",{value:"include"},"Include only matching paths"))),n.createElement("div",{style:{marginBottom:10}},n.createElement("div",{style:{...l.label,width:"auto",marginBottom:6}},"Path patterns (one per line, glob supported)"),n.createElement("textarea",{style:l.textarea,value:L,onChange:e=>$(e.target.value),placeholder:"navigation.position\nnavigation.*\nnotifications.*"}),n.createElement("div",{style:l.hint},"exclude"===D?"These paths are NOT recorded; everything else is.":"ONLY these paths are recorded; everything else is dropped."," ","Leave empty to record everything."))),n.createElement(r,{title:"Compression (on-disk)"},n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Compression codec"),n.createElement("select",{style:l.select,value:z,onChange:e=>P(e.target.value)},n.createElement("option",{value:"none"},"None"),n.createElement("option",{value:"lz4"},"LZ4 (fast)"),n.createElement("option",{value:"zstd"},"ZSTD (smaller)")),n.createElement("span",{style:l.hint},"applies to new data after save")),"zstd"===z&&n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"ZSTD level"),n.createElement("input",{style:l.inputSmall,type:"number",min:"1",max:"22",value:q,onChange:e=>Q(Number(e.target.value))}),n.createElement("span",{style:l.hint},"1 (fast) to 22 (smallest)"))),n.createElement(r,{title:"InfluxDB Migration"},n.createElement("div",{style:{display:"flex",gap:10,alignItems:"center",marginBottom:10}},n.createElement("button",{style:{...l.btn,...l.btnPrimary,...J?l.btnDisabled:{}},onClick:async()=>{X(!0),ae("");try{const e=Y?`?url=${encodeURIComponent(Y)}`:"",t=await fetch(`/plugins/signalk-questdb/api/migration/detect${e}`);if(t.ok){const e=await t.json();Z(e.sources),0===e.sources.length&&(ae(Y?`No InfluxDB found at ${Y}.`:"No InfluxDB instances detected on localhost:8086."),le(!1))}}catch(e){ae("Detection failed: "+e.message),le(!0)}X(!1)},disabled:J},J?"Detecting...":"Detect InfluxDB"),n.createElement("span",{style:l.hint},"Checks localhost:8086 for InfluxDB 1.x and 2.x")),K&&K.length>0&&n.createElement("div",null,K.map((e,t)=>n.createElement("div",{key:t,style:l.migrationItem},n.createElement("div",{style:{...l.cardIcon,width:36,height:36,fontSize:16,background:"influxdb2"===e.type?"#020a47":"#22adf6",color:"#fff"}},"influxdb2"===e.type?"2":"1"),n.createElement("div",{style:l.cardInfo},n.createElement("div",{style:l.cardTitle},"InfluxDB ","influxdb2"===e.type?"2.x":"1.x",n.createElement("span",{style:{...l.tag,...l.tagLatest}},e.status)),n.createElement("div",{style:l.cardMeta},e.url," · v",e.version))))),n.createElement("div",{style:{...l.fieldRow,marginTop:10}},n.createElement("span",{style:l.label},"Manual InfluxDB URL"),n.createElement("input",{style:l.input,placeholder:"http://192.168.1.100:8086",value:Y,onChange:e=>ee(e.target.value)}),n.createElement("span",{style:l.hint},"for remote instances")),K&&0===K.length&&!J&&n.createElement("div",{style:{...l.empty,padding:"16px",textAlign:"left"}},"No InfluxDB found on localhost. Use manual URL above for remote instances.")),n.createElement(r,{title:"Data Export"},n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"From"),n.createElement("input",{style:l.input,type:"datetime-local",value:Ce,onChange:e=>De(e.target.value)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"To"),n.createElement("input",{style:l.input,type:"datetime-local",value:Ie,onChange:e=>Le(e.target.value)})),n.createElement("div",{style:l.fieldRow},n.createElement("span",{style:l.label},"Format"),n.createElement("select",{style:l.select,value:$e,onChange:e=>ze(e.target.value)},n.createElement("option",{value:"parquet"},"Parquet"),n.createElement("option",{value:"csv"},"CSV"))),n.createElement("button",{style:{...l.btn,...l.btnPrimary,...Pe?l.btnDisabled:{}},onClick:async()=>{if(!Ce||!Ie)return ae("Set both from and to dates for export."),void le(!0);qe(!0),ae(`Exporting ${$e.toUpperCase()}...`),le(!1);try{const e=`/plugins/signalk-questdb/api/export?from=${encodeURIComponent(Ce)}&to=${encodeURIComponent(Ie)}&format=${$e}`,t=await fetch(e);if(!t.ok){const e=await t.json().catch(()=>({error:t.statusText}));return ae(`Export failed: ${e.error}`),le(!0),void qe(!1)}const a=await t.blob(),n="parquet"===$e?"parquet":"csv",l=document.createElement("a");l.href=URL.createObjectURL(a),l.download=`signalk-export-${Ce.slice(0,10)}.${n}`,l.click(),URL.revokeObjectURL(l.href),ae(`Exported ${(a.size/1024/1024).toFixed(1)} MB`)}catch(e){ae(`Export failed: ${e.message}`),le(!0)}qe(!1)},disabled:Pe},Pe?"Exporting...":"Export Data")),!1!==a.managedContainer&&n.createElement(r,{title:"Danger zone"},n.createElement("div",{style:{fontSize:13,color:"#555",marginBottom:10}},"Remove the QuestDB container and permanently delete all recorded data. Use this to fully reset QuestDB — Signal K's plugin-uninstall cannot delete this data on rootless Podman. This cannot be undone."),n.createElement("button",{style:{...l.btn,...l.btnDanger,...ue?l.btnDisabled:{}},onClick:async()=>{if(window.confirm("Remove the QuestDB container and DELETE ALL recorded data? This cannot be undone.")){me(!0),ae("Removing QuestDB container and data..."),le(!1);try{const e=await fetch("/plugins/signalk-questdb/api/purge-data",{method:"POST"}),t=await e.json().catch(()=>({error:e.statusText}));e.ok?(ae(t.message),Re()):(ae(`Remove failed: ${t.error}`),le(!0))}catch(e){ae(`Remove failed: ${e.message}`),le(!0)}me(!1)}},disabled:ue},ue?"Removing...":"Remove container & all data")),te&&n.createElement("div",{style:{...l.status,color:ne?"#ef4444":"#10b981",marginTop:16}},te),n.createElement("div",{style:{marginTop:24}},n.createElement("button",{style:{...l.btn,...l.btnSave},onClick:()=>{const e=Number(R);if(""===R||!Number.isFinite(e)||e<500||e>3e5)return ae("Write batch interval must be 500–300000 ms."),void le(!0);t({...a,questdbHost:i,questdbIlpPort:c,questdbHttpPort:u,questdbPgPort:p,questdbVersion:h,managedContainer:b,defaultSamplingRate:x,ilpFlushIntervalMs:e,recordSelf:E,recordOthers:w,retentionDays:B,compression:z,compressionLevel:q,networkName:A,exposeToContainers:W,pathFilter:{mode:D,paths:L.split("\n").map(e=>e.trim()).filter(Boolean)},samplingRates:a.samplingRates||{}}),ae("Saved! Plugin will restart with new configuration."),le(!1)}},"Save Configuration")))}}}]);
|