signalk-backup 0.4.0 → 0.6.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 (40) hide show
  1. package/README.md +35 -25
  2. package/package.json +6 -4
  3. package/plugin/config/image-tag.d.ts +3 -0
  4. package/plugin/config/image-tag.d.ts.map +1 -0
  5. package/plugin/config/image-tag.js +9 -0
  6. package/plugin/config/image-tag.js.map +1 -0
  7. package/plugin/config/schema.d.ts +2 -0
  8. package/plugin/config/schema.d.ts.map +1 -1
  9. package/plugin/config/schema.js +16 -4
  10. package/plugin/config/schema.js.map +1 -1
  11. package/plugin/database-export/grafana.d.ts +25 -0
  12. package/plugin/database-export/grafana.d.ts.map +1 -0
  13. package/plugin/database-export/grafana.js +262 -0
  14. package/plugin/database-export/grafana.js.map +1 -0
  15. package/plugin/database-export/index.d.ts +9 -2
  16. package/plugin/database-export/index.d.ts.map +1 -1
  17. package/plugin/database-export/index.js +24 -6
  18. package/plugin/database-export/index.js.map +1 -1
  19. package/plugin/database-export/signalk-database.d.ts +21 -0
  20. package/plugin/database-export/signalk-database.d.ts.map +1 -0
  21. package/plugin/database-export/signalk-database.js +148 -0
  22. package/plugin/database-export/signalk-database.js.map +1 -0
  23. package/plugin/database-export/staging-routes.d.ts +19 -0
  24. package/plugin/database-export/staging-routes.d.ts.map +1 -0
  25. package/plugin/database-export/staging-routes.js +205 -0
  26. package/plugin/database-export/staging-routes.js.map +1 -0
  27. package/plugin/index.d.ts.map +1 -1
  28. package/plugin/index.js +147 -44
  29. package/plugin/index.js.map +1 -1
  30. package/plugin/restore-host-write.d.ts +26 -0
  31. package/plugin/restore-host-write.d.ts.map +1 -0
  32. package/plugin/restore-host-write.js +363 -0
  33. package/plugin/restore-host-write.js.map +1 -0
  34. package/public/assets/{index-DMT6pdi_.js → index-C8j20cZ-.js} +2 -2
  35. package/public/assets/{index-DMT6pdi_.js.map → index-C8j20cZ-.js.map} +1 -1
  36. package/public/assets/main-B53J3gmZ.js +11 -0
  37. package/public/assets/main-B53J3gmZ.js.map +1 -0
  38. package/public/index.html +1 -1
  39. package/public/assets/main-BZ1xX0IR.js +0 -11
  40. package/public/assets/main-BZ1xX0IR.js.map +0 -1
package/README.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # signalk-backup
2
2
 
3
- Scheduled deduplicated backups of your SignalK config (and optionally history) to local storage and Google Drive.
3
+ Scheduled deduplicated backups of your SignalK config (and optionally history) to local storage, USB / NAS, or Google Drive — with a tree-browser that lets you restore individual files from any snapshot, anywhere your user can write.
4
4
 
5
- The plugin is a thin shell: it asks [signalk-container](https://github.com/dirkwa/signalk-container) to run the backup engine in a container ([signalk-backup-server](https://github.com/dirkwa/signalk-backup-server)) and presents a redirect to the engine's UI as a SignalK webapp. All backup work — Kopia snapshots, rclone Google Drive sync, scheduling, restore-with-rollback happens inside the container.
5
+ The plugin runs a React webapp inside the SignalK Admin UI and orchestrates a separate backup-engine container ([signalk-backup-server](https://github.com/dirkwa/signalk-backup-server)). All Kopia snapshot work happens in the container; the plugin handles the UI, the database-export tick, and host-side file writes for restores that target paths the container can't see.
6
6
 
7
7
  ## Features
8
8
 
9
9
  - **Content-addressable deduplication** — Kopia stores file blobs once across all snapshots; identical config files between hourly/daily/weekly retention tiers cost nothing
10
- - **Cloud sync to Google Drive** — optional, opt-in. Uses rclone with `drive.file` scope (the app only sees files it created)
11
10
  - **Scheduled backups** — hourly / daily / weekly / on-startup tiers, each with independent retention
12
- - **Restore with safety backup** — every restore creates a snapshot of the current state first, so a botched restore can be rolled back automatically
11
+ - **Cloud + local destinations** — Google Drive (rclone, `drive.file` scope, opt-in OAuth), USB / NAS via local-fs, or SMB share. Snapshots stay in the local Kopia repo even when cloud sync is configured
12
+ - **Selective restore** — browse any backup's file tree, download a single file or whole subdirectory, or restore in-place. The trailing slash on a custom path means "land inside as a directory", so `tmp/` plus a file source writes `tmp/<filename>` instead of overwriting `tmp` as a file
13
+ - **Restore-anywhere** — custom-path restores stream through the plugin and write under the SignalK user's permissions, so you can land restored bytes in `/tmp`, `/media/usb/...`, or your home directory — anywhere your user can write
14
+ - **Restore with safety backup** — every full restore creates a snapshot of the current state first; partial restores stash the existing target as a sibling before overwriting. Both flows can be rolled back automatically on failure
15
+ - **Database export pipeline** — opt-in exporters for [signalk-questdb](https://github.com/dirkwa/signalk-questdb), [signalk-grafana](https://github.com/dirkwa/signalk-grafana), and [signalk-database](https://github.com/dirkwa/signalk-database) periodically write consistent snapshots (parquet, SQLite checkpoint, provisioning YAML) to a staging dir that Kopia includes in every snapshot. Live DB files are excluded from filesystem backup to avoid torn-write hazards. The dedicated "Database exports" tab also extracts the export subtree from any historical backup without pulling the whole snapshot — a direct answer to "QuestDB shards bloat the full zip"
13
16
  - **First-run safe default** — installs with a daily local-only schedule already seeded; no surprise data egress
14
17
  - **Update detection** — checks ghcr.io for new container images via signalk-container's centralized update service
15
18
 
@@ -17,7 +20,7 @@ The plugin is a thin shell: it asks [signalk-container](https://github.com/dirkw
17
20
 
18
21
  - SignalK Server ≥ 2.24.0
19
22
  - Node 22 or newer (24 recommended)
20
- - [signalk-container](https://github.com/dirkwa/signalk-container) ≥ 0.1.6 (provides the container runtime)
23
+ - [signalk-container](https://github.com/dirkwa/signalk-container) ≥ 1.6.0 (provides the container runtime)
21
24
  - Podman or Docker on the host
22
25
 
23
26
  ## Install
@@ -27,37 +30,44 @@ The plugin is a thin shell: it asks [signalk-container](https://github.com/dirkw
27
30
  # (auto-enables on install — first-run schedules a daily local backup)
28
31
  ```
29
32
 
30
- The plugin pulls `ghcr.io/dirkwa/signalk-backup-server:latest` on first run. The container is small (~80 MB compressed). Subsequent updates are detected and surfaced in the signalk-container config panel.
33
+ The plugin pulls `ghcr.io/dirkwa/signalk-backup-server` at a server version it pins itself (see [src/config/image-tag.ts](src/config/image-tag.ts)). Plugin releases and server releases are decoupled you can pin or float either independently via the plugin's `imageTag` setting.
31
34
 
32
35
  ## How it works
33
36
 
34
- ```
35
- Your browser
36
- └── /plugins/signalk-backup/ redirect HTML
37
- └── plugin proxies /api/gui-url
38
- └── backup-server container UI
39
- (snapshots, restore wizard, cloud sync, settings)
37
+ ```text
38
+ Browser
39
+ └── /signalk-backup/ plugin webapp (React)
40
+ └── /plugins/signalk-backup/api/* ← plugin's reverse proxy
41
+ ├── /api/db-export/staging ← plugin-local (file lister)
42
+ ├── /api/restore-partial-host ← plugin-local (writes to host fs)
43
+ └── /api/backups/*, /api/cloud/*, … ← proxied to backup-server container
44
+ └── http://127.0.0.1:<port>/api/*
45
+ (Kopia snapshots, restore-with-rollback,
46
+ partial-restore state machine, cloud sync)
40
47
  ```
41
48
 
42
- When you visit `/plugins/signalk-backup/` in the SignalK Admin UI, the plugin's redirect HTML asks `/plugins/signalk-backup/api/gui-url` for the current backup-server location and bounces your browser there. This means the UI version is always in sync with the running container.
49
+ The plugin is most of the user experience:
43
50
 
44
- ## Configuration
51
+ - The **webapp** at `/signalk-backup/` is a React SPA bundled by Vite. It runs in the SignalK admin origin and reads/writes via `fetch('/plugins/signalk-backup/api/*')`. No CORS dance, no separate auth — it inherits SignalK's.
52
+ - The **plugin process** owns three things directly: the database-export scheduler tick, an HTTP route that lists / streams files from the staging tree (so the webapp can show "live" exports), and an HTTP route that performs host-side file writes for custom-path restores.
53
+ - Everything else — Kopia snapshots, restore-with-rollback, cloud sync, scheduling — runs in the [signalk-backup-server](https://github.com/dirkwa/signalk-backup-server) container. The plugin reverse-proxies `/api/*` through to it, and adds a small set of explicit overrides above the catch-all proxy.
45
54
 
46
- Most configuration lives in the **Backup Console** (open it from the redirect, or from the SignalK Admin UI plugin config: "Open Backup Console"). The Console handles:
55
+ The webapp has four tabs:
47
56
 
48
- - Backup schedule (hourly / daily / weekly / startup) and retention tiers
49
- - Backup excludes (paths to skip default: `node_modules/`, `charts*/`)
50
- - Repository password (Kopia encryption key)
51
- - Cloud sync mode (off / manual / daily / weekly)
52
- - Google Drive connection (one-click OAuth via `rclone authorize drive`)
53
- - Snapshot browser, restore wizard
57
+ - **Dashboard** status, recent backups, scheduler next-run times
58
+ - **Backups** create, list, download, restore (full or selective), upload a ZIP, delete
59
+ - **Database exports** — live staging table (one row per exporter file currently on disk) plus a "pick a backup → browse the database-exports subtree" extractor for grabbing historical shards without pulling the whole snapshot
60
+ - **Cloud sync** provider chooser (Google Drive / local / SMB), credentials, sync mode, on-demand sync
61
+
62
+ ## Configuration
54
63
 
55
- The SignalK Admin UI plugin panel (small) handles only deployment-level settings:
64
+ Most settings live in the **webapp** under Settings. The plugin's SignalK Admin UI config panel is intentionally small and handles only deployment-level options (the schema in [src/config/schema.ts](src/config/schema.ts) is the source of truth — update this list if you bump it):
56
65
 
57
- - `managedContainer` — whether to manage the container yourself or point at an external backup-server
58
- - `imageTag` — pin a specific version or use a floating tag like `latest`
66
+ - `managedContainer` — let the plugin manage the backup-server container (default), or point at an external instance
67
+ - `imageTag` — pin a specific version or use `auto` (default resolves to the BACKUP_SERVER_VERSION constant)
59
68
  - `externalUrl` — only used when `managedContainer: false`
60
- - `logLevel` — forwarded to the container
69
+ - `databaseExport.{questdb, grafana, signalkDatabase}` — enable per-exporter (default off; users opt in once their DB plugin is producing data)
70
+ - `databaseExport.intervalMinutes` — how often the export tick runs (5–1440)
61
71
 
62
72
  ## License
63
73
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "signalk-backup",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Scheduled deduplicated backups of SignalK config to local storage and Google Drive (via Kopia + rclone)",
5
5
  "main": "plugin/index.js",
6
6
  "type": "module",
7
7
  "signalk-plugin-enabled-by-default": true,
8
8
  "scripts": {
9
- "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts' 'webapp/**/*.{ts,tsx,html}' && eslint --fix 'src/**/*.ts' 'test/**/*.ts' 'webapp/**/*.{ts,tsx}'",
10
- "lint": "eslint 'src/**/*.ts' 'test/**/*.ts' 'webapp/**/*.{ts,tsx}'",
9
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"webapp/**/*.{ts,tsx,html}\" && eslint --fix \"src/**/*.ts\" \"test/**/*.ts\" \"webapp/**/*.{ts,tsx}\"",
10
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" \"webapp/**/*.{ts,tsx}\"",
11
11
  "typecheck:webapp": "tsc -p tsconfig.webapp.json",
12
12
  "build:plugin": "tsc",
13
13
  "build:webapp": "vite build",
@@ -49,7 +49,8 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@sinclair/typebox": "^0.34.0",
52
- "bonjour-service": "^1.3.0"
52
+ "bonjour-service": "^1.3.0",
53
+ "unzipper": "^0.12.3"
53
54
  },
54
55
  "peerDependencies": {
55
56
  "signalk-container": ">=1.6.0"
@@ -68,6 +69,7 @@
68
69
  "@types/react": "^19.2.14",
69
70
  "@types/react-dom": "^19.2.3",
70
71
  "@types/supertest": "^7.2.0",
72
+ "@types/unzipper": "^0.10.11",
71
73
  "@vitejs/plugin-react": "^6.0.1",
72
74
  "bootstrap": "^5.3.8",
73
75
  "eslint": "^10.3.0",
@@ -0,0 +1,3 @@
1
+ export declare const BACKUP_SERVER_VERSION = "0.5.0";
2
+ export declare function resolveImageTag(tag: string): string;
3
+ //# sourceMappingURL=image-tag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-tag.d.ts","sourceRoot":"","sources":["../../src/config/image-tag.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,UAAU,CAAA;AAE5C,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,9 @@
1
+ // The signalk-backup-server image version that "auto" resolves to.
2
+ // Bump this when a new signalk-backup-server release is published to ghcr.io.
3
+ // Independent of signalk-backup's own package.json version — the two repos
4
+ // release on independent cadences. See AGENTS.md "Gotchas" for rationale.
5
+ export const BACKUP_SERVER_VERSION = '0.5.0';
6
+ export function resolveImageTag(tag) {
7
+ return tag === 'auto' ? BACKUP_SERVER_VERSION : tag;
8
+ }
9
+ //# sourceMappingURL=image-tag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-tag.js","sourceRoot":"","sources":["../../src/config/image-tag.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,8EAA8E;AAC9E,2EAA2E;AAC3E,0EAA0E;AAC1E,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAA;AAE5C,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,OAAO,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,GAAG,CAAA;AACrD,CAAC"}
@@ -15,6 +15,8 @@ export declare const ConfigSchema: import("@sinclair/typebox").TObject<{
15
15
  externalUrl: import("@sinclair/typebox").TString;
16
16
  databaseExport: import("@sinclair/typebox").TObject<{
17
17
  questdb: import("@sinclair/typebox").TBoolean;
18
+ grafana: import("@sinclair/typebox").TBoolean;
19
+ signalkDatabase: import("@sinclair/typebox").TBoolean;
18
20
  intervalMinutes: import("@sinclair/typebox").TNumber;
19
21
  }>;
20
22
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;EA6BvB,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAQ7B,CAAA"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;EAwCvB,CAAA;AAEF,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAA;AAEhD;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAU7B,CAAA"}
@@ -17,9 +17,10 @@ export const ConfigSchema = Type.Object({
17
17
  'Disable to point at an external backup-server instance via "External URL".'
18
18
  }),
19
19
  imageTag: Type.String({
20
- default: 'latest',
20
+ default: 'auto',
21
21
  title: 'Container image tag',
22
- description: 'Pin to a specific version (e.g. "0.1.0") or use a floating tag (e.g. "latest").'
22
+ description: '"auto" (default) tracks the signalk-backup-server version this plugin release was tested against. ' +
23
+ 'Pin to a specific version (e.g. "0.4.0") or use a floating tag (e.g. "latest") to override.'
23
24
  }),
24
25
  externalUrl: Type.String({
25
26
  default: '',
@@ -31,8 +32,17 @@ export const ConfigSchema = Type.Object({
31
32
  // form hides it via uiSchema in src/index.ts (webapp owns the UI).
32
33
  databaseExport: Type.Object({
33
34
  questdb: Type.Boolean({ default: false }),
35
+ grafana: Type.Boolean({ default: false }),
36
+ signalkDatabase: Type.Boolean({ default: false }),
34
37
  intervalMinutes: Type.Number({ default: 60, minimum: 5, maximum: 1440 })
35
- }, { default: { questdb: false, intervalMinutes: 60 } })
38
+ }, {
39
+ default: {
40
+ questdb: false,
41
+ grafana: false,
42
+ signalkDatabase: false,
43
+ intervalMinutes: 60
44
+ }
45
+ })
36
46
  });
37
47
  /**
38
48
  * Materialised defaults — Signal K only uses the schema's `default` fields
@@ -46,10 +56,12 @@ export const ConfigSchema = Type.Object({
46
56
  */
47
57
  export const SCHEMA_DEFAULTS = {
48
58
  managedContainer: true,
49
- imageTag: 'latest',
59
+ imageTag: 'auto',
50
60
  externalUrl: '',
51
61
  databaseExport: {
52
62
  questdb: false,
63
+ grafana: false,
64
+ signalkDatabase: false,
53
65
  intervalMinutes: 60
54
66
  }
55
67
  };
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAU,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IACtC,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC7B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,+CAA+C;QACtD,WAAW,EACT,0FAA0F;YAC1F,4EAA4E;KAC/E,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,iFAAiF;KAC/F,CAAC;IACF,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,8EAA8E;YAC9E,0CAA0C;KAC7C,CAAC;IACF,oEAAoE;IACpE,mEAAmE;IACnE,cAAc,EAAE,IAAI,CAAC,MAAM,CACzB;QACE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;KACzE,EACD,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,EAAE,CACrD;CACF,CAAC,CAAA;AAIF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAW;IACrC,gBAAgB,EAAE,IAAI;IACtB,QAAQ,EAAE,QAAQ;IAClB,WAAW,EAAE,EAAE;IACf,cAAc,EAAE;QACd,OAAO,EAAE,KAAK;QACd,eAAe,EAAE,EAAE;KACpB;CACF,CAAA"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAU,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;IACtC,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC;QAC7B,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,+CAA+C;QACtD,WAAW,EACT,0FAA0F;YAC1F,4EAA4E;KAC/E,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM;QACf,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,oGAAoG;YACpG,6FAA6F;KAChG,CAAC;IACF,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,EAAE;QACX,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,8EAA8E;YAC9E,0CAA0C;KAC7C,CAAC;IACF,oEAAoE;IACpE,mEAAmE;IACnE,cAAc,EAAE,IAAI,CAAC,MAAM,CACzB;QACE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACzC,eAAe,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACjD,eAAe,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;KACzE,EACD;QACE,OAAO,EAAE;YACP,OAAO,EAAE,KAAK;YACd,OAAO,EAAE,KAAK;YACd,eAAe,EAAE,KAAK;YACtB,eAAe,EAAE,EAAE;SACpB;KACF,CACF;CACF,CAAC,CAAA;AAIF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAW;IACrC,gBAAgB,EAAE,IAAI;IACtB,QAAQ,EAAE,MAAM;IAChB,WAAW,EAAE,EAAE;IACf,cAAc,EAAE;QACd,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,KAAK;QACd,eAAe,EAAE,KAAK;QACtB,eAAe,EAAE,EAAE;KACpB;CACF,CAAA"}
@@ -0,0 +1,25 @@
1
+ import type { DatabaseExporter, ExportResult } from './types.js';
2
+ export interface GrafanaExporterOptions {
3
+ signalkBaseUrl?: string;
4
+ log?: (msg: string) => void;
5
+ /** Override fetch (tests). */
6
+ fetch?: typeof fetch;
7
+ }
8
+ export declare class GrafanaExporter implements DatabaseExporter {
9
+ readonly pluginId = "signalk-grafana";
10
+ private readonly baseUrl;
11
+ private readonly fetchImpl;
12
+ private readonly log;
13
+ constructor(opts?: GrafanaExporterOptions);
14
+ detect(): Promise<boolean>;
15
+ exportAll(stagingDir: string): Promise<ExportResult>;
16
+ private exportDb;
17
+ private exportDashboards;
18
+ private parseDashboardEntry;
19
+ private fetchDashboardFile;
20
+ private exportProvisioning;
21
+ private parseProvisioningEntry;
22
+ private fetchProvisioningFile;
23
+ private resetStagingDir;
24
+ }
25
+ //# sourceMappingURL=grafana.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grafana.d.ts","sourceRoot":"","sources":["../../src/database-export/grafana.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAe,MAAM,YAAY,CAAA;AAS7E,MAAM,WAAW,sBAAsB;IACrC,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3B,8BAA8B;IAC9B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAeD,qBAAa,eAAgB,YAAW,gBAAgB;IACtD,QAAQ,CAAC,QAAQ,qBAAoB;IAErC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAuB;gBAE/B,IAAI,GAAE,sBAA2B;IAMvC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAc1B,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;YA8D5C,QAAQ;YAkBR,gBAAgB;IAuC9B,OAAO,CAAC,mBAAmB;YAYb,kBAAkB;YASlB,kBAAkB;IAuChC,OAAO,CAAC,sBAAsB;YAiBhB,qBAAqB;YAerB,eAAe;CAI9B"}
@@ -0,0 +1,262 @@
1
+ // Mirrors the QuestDB exporter: pulls signalk-grafana's /api/full-export/{db,dashboards,provisioning} over loopback and stages into kopia's snapshot tree.
2
+ import { mkdir, rename, rm, unlink } from 'node:fs/promises';
3
+ import { createWriteStream, existsSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { Readable } from 'node:stream';
7
+ const GRAFANA_PLUGIN_ID = 'signalk-grafana';
8
+ const DEFAULT_SIGNALK_BASE = 'http://127.0.0.1:3000';
9
+ const FETCH_TIMEOUT_MS = 600_000;
10
+ // Engine-side validation rejects names outside this set; reject early
11
+ // here too so we never even try a fetch that the server would 400.
12
+ const SAFE_FILENAME = /^[a-zA-Z0-9._-]+$/;
13
+ export class GrafanaExporter {
14
+ pluginId = GRAFANA_PLUGIN_ID;
15
+ baseUrl;
16
+ fetchImpl;
17
+ log;
18
+ constructor(opts = {}) {
19
+ this.baseUrl = (opts.signalkBaseUrl ?? DEFAULT_SIGNALK_BASE).replace(/\/$/, '');
20
+ this.fetchImpl = opts.fetch ?? fetch;
21
+ this.log = (msg) => opts.log?.(`[grafana-export] ${msg}`);
22
+ }
23
+ async detect() {
24
+ // Dashboards manifest is the cheapest probe — GET, no container exec, 200 even when /var/lib/grafana is empty.
25
+ try {
26
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/dashboards`;
27
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(5_000) });
28
+ if (!res.ok)
29
+ return false;
30
+ const body = (await res.json());
31
+ return Array.isArray(body.dashboards);
32
+ }
33
+ catch (err) {
34
+ this.log(`detect failed: ${errMsg(err)}`);
35
+ return false;
36
+ }
37
+ }
38
+ async exportAll(stagingDir) {
39
+ const startedAt = Date.now();
40
+ await mkdir(stagingDir, { recursive: true });
41
+ const exports = [];
42
+ let totalBytes = 0;
43
+ // Use TableExport-as-single-shard so totals line up with QuestDB reports.
44
+ try {
45
+ const dbBytes = await this.exportDb(stagingDir);
46
+ exports.push({
47
+ table: 'grafana.db',
48
+ tableDir: stagingDir,
49
+ shardsWritten: 1,
50
+ shardsSkipped: 0,
51
+ bytes: dbBytes
52
+ });
53
+ totalBytes += dbBytes;
54
+ }
55
+ catch (err) {
56
+ this.log(`db export failed: ${errMsg(err)}`);
57
+ }
58
+ try {
59
+ const bytes = await this.exportDashboards(stagingDir);
60
+ exports.push({
61
+ table: 'dashboards',
62
+ tableDir: join(stagingDir, 'dashboards'),
63
+ shardsWritten: bytes.count,
64
+ shardsSkipped: bytes.skipped,
65
+ bytes: bytes.totalBytes
66
+ });
67
+ totalBytes += bytes.totalBytes;
68
+ }
69
+ catch (err) {
70
+ this.log(`dashboards export failed: ${errMsg(err)}`);
71
+ }
72
+ try {
73
+ const bytes = await this.exportProvisioning(stagingDir);
74
+ exports.push({
75
+ table: 'provisioning',
76
+ tableDir: join(stagingDir, 'provisioning'),
77
+ shardsWritten: bytes.count,
78
+ shardsSkipped: bytes.skipped,
79
+ bytes: bytes.totalBytes
80
+ });
81
+ totalBytes += bytes.totalBytes;
82
+ }
83
+ catch (err) {
84
+ this.log(`provisioning export failed: ${errMsg(err)}`);
85
+ }
86
+ return {
87
+ pluginId: this.pluginId,
88
+ tables: exports,
89
+ totalBytes,
90
+ durationMs: Date.now() - startedAt
91
+ };
92
+ }
93
+ // -------------------------------------------------------------------
94
+ // Internal
95
+ // -------------------------------------------------------------------
96
+ async exportDb(stagingDir) {
97
+ const finalPath = join(stagingDir, 'grafana.db');
98
+ // Drop the prior-cycle file first so a mid-cycle failure leaves no stale grafana.db beside fresh dashboards.
99
+ if (existsSync(finalPath))
100
+ await unlink(finalPath).catch(() => undefined);
101
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/db`;
102
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
103
+ if (!res.ok) {
104
+ throw new Error(`db HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
105
+ }
106
+ if (!res.body) {
107
+ throw new Error('db response body is empty');
108
+ }
109
+ const bytes = await atomicWrite(res.body, finalPath);
110
+ this.log(`wrote grafana.db (${bytes} bytes)`);
111
+ return bytes;
112
+ }
113
+ async exportDashboards(stagingDir) {
114
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/dashboards`;
115
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(10_000) });
116
+ if (!res.ok) {
117
+ throw new Error(`dashboards manifest HTTP ${res.status}`);
118
+ }
119
+ const body = (await res.json());
120
+ if (!Array.isArray(body.dashboards)) {
121
+ throw new Error(`dashboards manifest missing 'dashboards' array`);
122
+ }
123
+ const dashboardsDir = join(stagingDir, 'dashboards');
124
+ // Reset so dashboards deleted in Grafana don't linger in kopia forever.
125
+ await this.resetStagingDir(dashboardsDir);
126
+ let totalBytes = 0;
127
+ let count = 0;
128
+ let skipped = 0;
129
+ for (const raw of body.dashboards) {
130
+ const entry = this.parseDashboardEntry(raw);
131
+ if (!entry) {
132
+ skipped++;
133
+ continue;
134
+ }
135
+ try {
136
+ totalBytes += await this.fetchDashboardFile(entry, dashboardsDir);
137
+ count++;
138
+ }
139
+ catch (err) {
140
+ skipped++;
141
+ this.log(`dashboard ${entry.name} failed: ${errMsg(err)}`);
142
+ }
143
+ }
144
+ return { count, skipped, totalBytes };
145
+ }
146
+ parseDashboardEntry(raw) {
147
+ if (!raw || typeof raw !== 'object')
148
+ return null;
149
+ const o = raw;
150
+ if (typeof o.name !== 'string' || !SAFE_FILENAME.test(o.name))
151
+ return null;
152
+ // SAFE_FILENAME matches "." and "..", which would resolve to the dir
153
+ // itself or its parent under join(). Server rejects these too, but
154
+ // belt-and-braces is cheap.
155
+ if (o.name === '.' || o.name === '..')
156
+ return null;
157
+ if (typeof o.sha256 !== 'string' || typeof o.bytes !== 'number')
158
+ return null;
159
+ return { name: o.name, sha256: o.sha256, bytes: o.bytes };
160
+ }
161
+ async fetchDashboardFile(entry, dashboardsDir) {
162
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/dashboards/${encodeURIComponent(entry.name)}`;
163
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
164
+ if (!res.ok || !res.body) {
165
+ throw new Error(`HTTP ${res.status}`);
166
+ }
167
+ return atomicWrite(res.body, join(dashboardsDir, entry.name));
168
+ }
169
+ async exportProvisioning(stagingDir) {
170
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/provisioning`;
171
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(10_000) });
172
+ if (!res.ok) {
173
+ throw new Error(`provisioning manifest HTTP ${res.status}`);
174
+ }
175
+ const body = (await res.json());
176
+ if (!Array.isArray(body.files)) {
177
+ throw new Error(`provisioning manifest missing 'files' array`);
178
+ }
179
+ const provisioningDir = join(stagingDir, 'provisioning');
180
+ // Reset so removed YAMLs don't linger in kopia forever.
181
+ await this.resetStagingDir(provisioningDir);
182
+ let totalBytes = 0;
183
+ let count = 0;
184
+ let skipped = 0;
185
+ for (const raw of body.files) {
186
+ const entry = this.parseProvisioningEntry(raw);
187
+ if (!entry) {
188
+ skipped++;
189
+ continue;
190
+ }
191
+ try {
192
+ totalBytes += await this.fetchProvisioningFile(entry, provisioningDir);
193
+ count++;
194
+ }
195
+ catch (err) {
196
+ skipped++;
197
+ this.log(`provisioning ${entry.relPath} failed: ${errMsg(err)}`);
198
+ }
199
+ }
200
+ return { count, skipped, totalBytes };
201
+ }
202
+ parseProvisioningEntry(raw) {
203
+ if (!raw || typeof raw !== 'object')
204
+ return null;
205
+ const o = raw;
206
+ if (typeof o.name !== 'string' || !SAFE_FILENAME.test(o.name))
207
+ return null;
208
+ if (typeof o.relPath !== 'string')
209
+ return null;
210
+ // Reject relPaths that try to escape via `..` or absolute paths
211
+ // before we use them in a host filesystem write. The server-side
212
+ // endpoint also rejects these, but defending in depth is cheap.
213
+ const segments = o.relPath.split('/').filter(Boolean);
214
+ if (segments.length === 0)
215
+ return null;
216
+ for (const seg of segments) {
217
+ if (seg === '..' || seg === '.' || !SAFE_FILENAME.test(seg))
218
+ return null;
219
+ }
220
+ if (typeof o.sha256 !== 'string' || typeof o.bytes !== 'number')
221
+ return null;
222
+ return { name: o.name, relPath: o.relPath, sha256: o.sha256, bytes: o.bytes };
223
+ }
224
+ async fetchProvisioningFile(entry, provisioningDir) {
225
+ const url = `${this.baseUrl}/plugins/${this.pluginId}/api/full-export/provisioning/${encodeURIComponent(entry.relPath)}`;
226
+ const res = await this.fetchImpl(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
227
+ if (!res.ok || !res.body) {
228
+ throw new Error(`HTTP ${res.status}`);
229
+ }
230
+ const finalPath = join(provisioningDir, entry.relPath);
231
+ await mkdir(dirname(finalPath), { recursive: true });
232
+ return atomicWrite(res.body, finalPath);
233
+ }
234
+ // Errors propagate so a failed reset reports as failed-section instead of emitting a half-fresh half-stale tree.
235
+ async resetStagingDir(root) {
236
+ await rm(root, { recursive: true, force: true });
237
+ await mkdir(root, { recursive: true });
238
+ }
239
+ }
240
+ // Failed writes unlink the partial so half-written data can't slip into the next snapshot.
241
+ async function atomicWrite(body, finalPath) {
242
+ const partialPath = `${finalPath}.partial`;
243
+ const nodeStream = Readable.fromWeb(body);
244
+ const sink = createWriteStream(partialPath);
245
+ let bytes = 0;
246
+ nodeStream.on('data', (chunk) => {
247
+ bytes += chunk.length;
248
+ });
249
+ try {
250
+ await pipeline(nodeStream, sink);
251
+ await rename(partialPath, finalPath);
252
+ return bytes;
253
+ }
254
+ catch (err) {
255
+ await unlink(partialPath).catch(() => undefined);
256
+ throw err;
257
+ }
258
+ }
259
+ function errMsg(err) {
260
+ return err instanceof Error ? err.message : String(err);
261
+ }
262
+ //# sourceMappingURL=grafana.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grafana.js","sourceRoot":"","sources":["../../src/database-export/grafana.ts"],"names":[],"mappings":"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"}
@@ -5,8 +5,8 @@
5
5
  * exporter pulls data via the source plugin's HTTP route on the SignalK
6
6
  * server itself — no container exec or shared filesystems involved.
7
7
  *
8
- * Currently supports QuestDB only. InfluxDB is intentionally out of
9
- * scope (see design doc).
8
+ * Supports QuestDB and Grafana. InfluxDB is intentionally out of scope
9
+ * (see design doc).
10
10
  */
11
11
  import type { ExportResult } from './types.js';
12
12
  export interface ExportOrchestratorOptions {
@@ -16,6 +16,11 @@ export interface ExportOrchestratorOptions {
16
16
  signalkBaseUrl: string;
17
17
  /** Optional debug logger. */
18
18
  log?: (msg: string) => void;
19
+ enabled?: {
20
+ questdb?: boolean;
21
+ grafana?: boolean;
22
+ signalkDatabase?: boolean;
23
+ };
19
24
  }
20
25
  /**
21
26
  * Run every supported exporter whose `detect()` returns true. Each
@@ -29,5 +34,7 @@ export interface ExportOrchestratorOptions {
29
34
  */
30
35
  export declare function runAllExports(opts: ExportOrchestratorOptions): Promise<ExportResult[]>;
31
36
  export type { DatabaseExporter, ExportResult, TableExport } from './types.js';
37
+ export { GrafanaExporter } from './grafana.js';
32
38
  export { QuestDBExporter } from './questdb.js';
39
+ export { SignalKDatabaseExporter } from './signalk-database.js';
33
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database-export/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,YAAY,CAAA;AAKhE,MAAM,WAAW,yBAAyB;IACxC,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAA;IACtB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgC5F;AAED,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database-export/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,YAAY,CAAA;AAKhE,MAAM,WAAW,yBAAyB;IACxC,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAA;IACtB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAE3B,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAC9E;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAoD5F;AAED,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA"}