rigscope 0.1.7 → 0.2.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 (86) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +4 -4
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +2 -2
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  14. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  16. package/.next/server/app/administration/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/administration.html +1 -1
  18. package/.next/server/app/administration.rsc +2 -2
  19. package/.next/server/app/administration.segments/_full.segment.rsc +2 -2
  20. package/.next/server/app/administration.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/administration.segments/administration/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/alerts/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/alerts.html +1 -1
  24. package/.next/server/app/alerts.rsc +2 -2
  25. package/.next/server/app/alerts.segments/_full.segment.rsc +2 -2
  26. package/.next/server/app/alerts.segments/_tree.segment.rsc +2 -2
  27. package/.next/server/app/alerts.segments/alerts/__PAGE__.segment.rsc +2 -2
  28. package/.next/server/app/connections/page_client-reference-manifest.js +1 -1
  29. package/.next/server/app/connections.html +1 -1
  30. package/.next/server/app/connections.rsc +2 -2
  31. package/.next/server/app/connections.segments/_full.segment.rsc +2 -2
  32. package/.next/server/app/connections.segments/_tree.segment.rsc +2 -2
  33. package/.next/server/app/connections.segments/connections/__PAGE__.segment.rsc +2 -2
  34. package/.next/server/app/history/page.js +2 -2
  35. package/.next/server/app/history/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/history.html +1 -1
  37. package/.next/server/app/history.rsc +5 -5
  38. package/.next/server/app/history.segments/_full.segment.rsc +5 -5
  39. package/.next/server/app/history.segments/_tree.segment.rsc +2 -2
  40. package/.next/server/app/history.segments/history/__PAGE__.segment.rsc +5 -5
  41. package/.next/server/app/index.html +1 -1
  42. package/.next/server/app/index.rsc +2 -2
  43. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  44. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  45. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  47. package/.next/server/app/login.html +1 -1
  48. package/.next/server/app/login.rsc +2 -2
  49. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  50. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  51. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  52. package/.next/server/app/machines/[machineId]/page_client-reference-manifest.js +1 -1
  53. package/.next/server/app/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/processes/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app/processes.html +1 -1
  56. package/.next/server/app/processes.rsc +2 -2
  57. package/.next/server/app/processes.segments/_full.segment.rsc +2 -2
  58. package/.next/server/app/processes.segments/_tree.segment.rsc +2 -2
  59. package/.next/server/app/processes.segments/processes/__PAGE__.segment.rsc +2 -2
  60. package/.next/server/app-paths-manifest.json +4 -4
  61. package/.next/server/chunks/168.js +1 -1
  62. package/.next/server/middleware-build-manifest.js +1 -1
  63. package/.next/server/pages/404.html +1 -1
  64. package/.next/server/pages/500.html +1 -1
  65. package/.next/static/chunks/408-351fe4a1e9a2d556.js +1 -0
  66. package/.next/static/chunks/app/history/page-adc25194b30c689d.js +3 -0
  67. package/.next/static/css/{f5d3162842dcac31.css → 2abef6d49c1c2970.css} +1 -1
  68. package/ARCHITECTURE.md +25 -9
  69. package/CHANGELOG.md +35 -0
  70. package/README.md +10 -3
  71. package/dist/cli.cjs +106 -2
  72. package/dist/collector.cjs +23 -6
  73. package/dist/hub.cjs +3441 -1457
  74. package/dist/ops.cjs +1196 -896
  75. package/dist/system.cjs +65 -0
  76. package/docs/configuration.md +33 -1
  77. package/docs/installation.md +3 -3
  78. package/docs/monitoring.md +57 -17
  79. package/docs/operations.md +38 -9
  80. package/docs/releasing.md +12 -12
  81. package/docs/telemetry-storage.md +187 -0
  82. package/package.json +3 -1
  83. package/.next/static/chunks/408-9fdba9e4e956ef08.js +0 -1
  84. package/.next/static/chunks/app/history/page-a25ba8fab7bd7cb9.js +0 -3
  85. /package/.next/static/{9SHY6v87ulfunqR2e84n9 → Jlbibe2_iFCljUO4-BUK7}/_buildManifest.js +0 -0
  86. /package/.next/static/{9SHY6v87ulfunqR2e84n9 → Jlbibe2_iFCljUO4-BUK7}/_ssgManifest.js +0 -0
package/dist/system.cjs CHANGED
@@ -789,6 +789,13 @@ async function enrollCollector(path, connectUrl, enrollmentToken, sharing, repla
789
789
  // src/hub/config.ts
790
790
  var import_node_net = require("node:net");
791
791
  var import_node_path3 = require("node:path");
792
+
793
+ // src/contracts/telemetry.ts
794
+ var MAX_PERSISTENT_FLUSH_MS = 5e3;
795
+ var MIN_EPOCH = Date.parse("2000-01-01T00:00:00Z");
796
+ var MAX_EPOCH = Date.parse("2200-01-01T00:00:00Z");
797
+
798
+ // src/hub/config.ts
792
799
  var MAX_HUB_COLLECTORS_BYTES = 1024 * 1024;
793
800
  var MAX_COLLECTOR_ID_LENGTH = 128;
794
801
  var MAX_MACHINE_ID_LENGTH = 128;
@@ -816,6 +823,7 @@ function parseHubConfig(env) {
816
823
  maxCollectorConnections = parsed;
817
824
  }
818
825
  const connectionBudget = maxCollectorConnections === void 0 ? {} : { maxCollectorConnections };
826
+ const history = parseHistoryConfig(env);
819
827
  if (publicOrigin === void 0 !== (socketPath === void 0)) {
820
828
  throw new HubConfigError(
821
829
  "HUB_PUBLIC_ORIGIN and HUB_SOCKET_PATH must be set together: remote mode needs both the exact HTTPS origin and the Unix socket path"
@@ -835,6 +843,7 @@ function parseHubConfig(env) {
835
843
  collectors,
836
844
  development,
837
845
  remote: parseRemoteConfig(publicOrigin, socketPath),
846
+ history,
838
847
  ...connectionBudget
839
848
  };
840
849
  }
@@ -844,9 +853,65 @@ function parseHubConfig(env) {
844
853
  collectors,
845
854
  development,
846
855
  remote: null,
856
+ history,
847
857
  ...connectionBudget
848
858
  };
849
859
  }
860
+ var DEFAULT_MEMORY_BUDGET_BYTES = 128 * 1024 * 1024;
861
+ var DEFAULT_PERSISTENT_FLUSH_MS = MAX_PERSISTENT_FLUSH_MS;
862
+ var DEFAULT_RECORDING_MAX_DURATION_MS = 60 * 6e4;
863
+ var DEFAULT_RECORDING_MAX_BYTES = 64 * 1024 * 1024;
864
+ var DEFAULT_RECORDING_BACKLOG_MS = 10 * 6e4;
865
+ var DEFAULT_MAX_ACTIVE_RECORDINGS = 4;
866
+ var HOUR_MS = 36e5;
867
+ var DAY_MS = 864e5;
868
+ function parseHistoryConfig(env) {
869
+ const modeValue = env.HUB_TELEMETRY_MODE ?? "memory";
870
+ if (modeValue !== "memory" && modeValue !== "persistent") {
871
+ throw new HubConfigError("HUB_TELEMETRY_MODE must be memory or persistent");
872
+ }
873
+ const memoryBudgetBytes = envInteger(
874
+ env.HUB_HISTORY_MEMORY_BYTES,
875
+ DEFAULT_MEMORY_BUDGET_BYTES,
876
+ 1 * 1024 * 1024,
877
+ 16 * 1024 * 1024 * 1024,
878
+ "HUB_HISTORY_MEMORY_BYTES"
879
+ );
880
+ const flushIntervalMs = envInteger(
881
+ env.HUB_TELEMETRY_FLUSH_MS,
882
+ DEFAULT_PERSISTENT_FLUSH_MS,
883
+ 1e3,
884
+ MAX_PERSISTENT_FLUSH_MS,
885
+ "HUB_TELEMETRY_FLUSH_MS"
886
+ );
887
+ const retention = {
888
+ rawMs: envInteger(env.HUB_RETENTION_RAW_MS, 2 * DAY_MS, 0, 400 * DAY_MS, "HUB_RETENTION_RAW_MS"),
889
+ tenSecondMs: envInteger(env.HUB_RETENTION_10S_MS, 2 * HOUR_MS, 0, 30 * DAY_MS, "HUB_RETENTION_10S_MS"),
890
+ oneMinuteMs: envInteger(env.HUB_RETENTION_1M_MS, 30 * DAY_MS, 0, 400 * DAY_MS, "HUB_RETENTION_1M_MS"),
891
+ oneHourMs: envInteger(env.HUB_RETENTION_1H_MS, 400 * DAY_MS, 0, 800 * DAY_MS, "HUB_RETENTION_1H_MS"),
892
+ receiptsMs: envInteger(env.HUB_RECEIPT_RETENTION_MS, 7 * DAY_MS, HOUR_MS, 400 * DAY_MS, "HUB_RECEIPT_RETENTION_MS"),
893
+ gapsMs: envInteger(env.HUB_GAP_RETENTION_MS, 400 * DAY_MS, 0, 800 * DAY_MS, "HUB_GAP_RETENTION_MS")
894
+ };
895
+ const recordings = {
896
+ directory: nonEmpty(env.HUB_RECORDINGS_DIR) ?? null,
897
+ maxDurationMs: envInteger(env.HUB_RECORDING_MAX_DURATION_MS, DEFAULT_RECORDING_MAX_DURATION_MS, 1e3, 24 * 60 * 6e4, "HUB_RECORDING_MAX_DURATION_MS"),
898
+ maxBytes: envInteger(env.HUB_RECORDING_MAX_BYTES, DEFAULT_RECORDING_MAX_BYTES, 64 * 1024, 1024 * 1024 * 1024, "HUB_RECORDING_MAX_BYTES"),
899
+ backlogMs: envInteger(env.HUB_RECORDING_BACKLOG_MS, DEFAULT_RECORDING_BACKLOG_MS, 0, 24 * 60 * 6e4, "HUB_RECORDING_BACKLOG_MS"),
900
+ maxActive: envInteger(env.HUB_RECORDING_MAX_ACTIVE, DEFAULT_MAX_ACTIVE_RECORDINGS, 1, 32, "HUB_RECORDING_MAX_ACTIVE")
901
+ };
902
+ return { mode: modeValue, memoryBudgetBytes, flushIntervalMs, retention, recordings };
903
+ }
904
+ function envInteger(raw, fallback, min, max, name) {
905
+ if (raw === void 0 || raw === "") return fallback;
906
+ if (!/^\d+$/.test(raw)) {
907
+ throw new HubConfigError(`${name} must be a nonnegative integer`);
908
+ }
909
+ const value = Number(raw);
910
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
911
+ throw new HubConfigError(`${name} must be between ${min} and ${max}`);
912
+ }
913
+ return value;
914
+ }
850
915
  function nonEmpty(value) {
851
916
  return value === void 0 || value === "" ? void 0 : value;
852
917
  }
@@ -17,7 +17,21 @@ configuration files at `0600`; do not commit them.
17
17
  | --- | --- |
18
18
  | `HUB_HOST`, `HUB_PORT` | Local listener: `127.0.0.1:8081`. Only literal loopback addresses are accepted. Port `0` selects an unused port for tests. |
19
19
  | `HUB_PUBLIC_ORIGIN`, `HUB_SOCKET_PATH` | Remote access: set both the exact HTTPS origin and an absolute Unix socket path in a private directory. The hub then has no TCP listener. |
20
- | `HUB_DATABASE_PATH` | SQLite database for telemetry, users, alerts and the collector registry. Defaults beside `HUB_STATE_FILE`, or under `$XDG_STATE_HOME/rigscope/hub/` (falling back to `~/.local/state`). |
20
+ | `HUB_DATABASE_PATH` | SQLite database for control state (users, alerts and the collector registry) and, only when enabled, telemetry history. Defaults beside `HUB_STATE_FILE`, or under `$XDG_STATE_HOME/rigscope/hub/` (falling back to `~/.local/state`). |
21
+ | `HUB_TELEMETRY_MODE` | `memory` (default) keeps bounded history in RAM and writes no routine telemetry rows. `persistent` opts into compact SQLite summaries. See [telemetry storage](telemetry-storage.md). |
22
+ | `HUB_HISTORY_MEMORY_BYTES` | Global RAM budget for memory history. Default `134217728` (128 MiB). |
23
+ | `HUB_TELEMETRY_FLUSH_MS` | Persistent-mode write interval. Default and maximum `5000` ms (5 seconds); allowed 1000–5000. The bound keeps one commit inside the 64-batch collector window at the fastest supported 100 ms cadence and well below the collector's 30 s ack timeout. |
24
+ | `HUB_RETENTION_RAW_MS` | Persistent raw-mode retention. Default 2 days. Persistent summary mode does not write raw rows. |
25
+ | `HUB_RETENTION_10S_MS` | Ten-second summary retention in persistent mode. Default 2 hours. |
26
+ | `HUB_RETENTION_1M_MS` | One-minute rollup retention. Default 30 days. |
27
+ | `HUB_RETENTION_1H_MS` | One-hour rollup retention. Default 400 days. |
28
+ | `HUB_RECEIPT_RETENTION_MS` | Deduplication receipt lifetime, independent of chart retention. Default 7 days. |
29
+ | `HUB_GAP_RETENTION_MS` | Explicit collector gap retention. Default 400 days. |
30
+ | `HUB_RECORDINGS_DIR` | Private directory for explicit recordings. Default `recordings/` beside the hub database (0700 directory, 0600 files). |
31
+ | `HUB_RECORDING_MAX_DURATION_MS` | Maximum recording duration. Default 1 hour; maximum 24 hours. |
32
+ | `HUB_RECORDING_MAX_BYTES` | Maximum recording file size. Default 64 MiB; maximum 1 GiB. |
33
+ | `HUB_RECORDING_BACKLOG_MS` | Recent RAM history a new recording may include. Default 10 minutes. |
34
+ | `HUB_RECORDING_MAX_ACTIVE` | Concurrent active recordings. Default 4; maximum 32. |
21
35
  | `HUB_BROWSER_USERS` | First-run users as JSON: `{username,passwordHash,machineIds,admin?}`. `machineIds` is `"*"` or a list; `[]` allows no machines. |
22
36
  | `HUB_ADMIN_USERS` | Comma-separated usernames to make administrators during first import. If omitted, an existing bootstrap account named `owner` becomes an administrator. Other users need an explicit `admin:true`. |
23
37
  | `HUB_COLLECTORS` | First-run collector credentials as JSON: `{collectorId,machineId,token}`. The limit applies to JSON bytes, not a fixed machine count. |
@@ -25,6 +39,24 @@ configuration files at `0600`; do not commit them.
25
39
  | `HUB_STATE_FILE` | Private enrollment file to import from an existing installation; also enables enrollment. Keep it with the deployment configuration for recovery. |
26
40
  | `HUB_ENROLLMENT_GRANTS` | Initial grants as JSON: `{token,expiresAt}`. Expiry must be within 15 minutes. Used grants stay consumed after import and restart. |
27
41
 
42
+ ### Telemetry history mode
43
+
44
+ The default `HUB_TELEMETRY_MODE=memory` keeps recent charts in a bounded RAM
45
+ budget and deliberately loses telemetry history when the hub restarts. Users,
46
+ credentials, enrollment, settings and alert records remain in SQLite.
47
+
48
+ Set `HUB_TELEMETRY_MODE=persistent` to keep compact ten-second summaries, with
49
+ one transaction roughly every `HUB_TELEMETRY_FLUSH_MS` (five seconds by
50
+ default). Collectors still acknowledge only after that transaction commits, so
51
+ the flush interval cannot exceed the collector's 64-batch in-flight window at
52
+ its advertised cadence; the hub rejects values above five seconds. See
53
+ [telemetry storage](telemetry-storage.md) for aggregation, deduplication,
54
+ recording and reset details.
55
+
56
+ Switching an existing installation to memory mode does not automatically delete
57
+ old telemetry rows. They are ignored, but can be reclaimed explicitly with
58
+ `ops reset-telemetry` after stopping the hub (see [operations](operations.md)).
59
+
28
60
  ### First-run users and credentials
29
61
 
30
62
  The database imports bootstrap users and collector credentials once. Later
@@ -27,7 +27,7 @@ install or source checkout is needed. The launcher and services use the installe
27
27
  runtime, so removing the npx cache does not break them.
28
28
 
29
29
  Releases are on [npm](https://www.npmjs.com/package/rigscope). `@latest` selects
30
- the current stable release; replace it with `@0.1.7` to select that exact version.
30
+ the current stable release; replace it with `@0.2.0` to select that exact version.
31
31
 
32
32
  ### Installation options
33
33
 
@@ -53,7 +53,7 @@ For a downloaded package or one built from source, run from the directory
53
53
  containing the archive, or give its absolute path:
54
54
 
55
55
  ```sh
56
- npx --yes --package ./rigscope-0.1.7.tgz rigscope install --boot
56
+ npx --yes --package ./rigscope-0.2.0.tgz rigscope install --boot
57
57
  ```
58
58
 
59
59
  The same installation options apply. npm still needs registry access or cached
@@ -295,7 +295,7 @@ settings and collector identities.
295
295
 
296
296
  | Alternative | Command |
297
297
  | --- | --- |
298
- | Select an exact published version | `rigscope upgrade --from rigscope@0.1.7` |
298
+ | Select an exact published version | `rigscope upgrade --from rigscope@0.2.0` |
299
299
  | Install a local archive | `rigscope upgrade --from ./rigscope-VERSION.tgz` |
300
300
  | Return to the retained previous runtime | `rigscope rollback` |
301
301
 
@@ -20,22 +20,58 @@ driver and readable counters; see [Hardware](installation.md#hardware) and
20
20
 
21
21
  ## History and delivery
22
22
 
23
- The hub acknowledges samples after committing them to SQLite. A collector keeps
24
- unacknowledged samples in a persistent queue for each hub and retries after a
25
- disconnect. The hub removes duplicates using the collector, run and sequence IDs.
23
+ History is **RAM-only by default**. Live readings update immediately, recent
24
+ charts come from a bounded memory budget, and telemetry history disappears when
25
+ the hub restarts. Users, credentials, enrollment, settings, alert records,
26
+ machine names/tags and inventory remain persistent.
27
+
28
+ In memory mode the collector's acknowledgement means the hub accepted the sample
29
+ into RAM. The collector keeps the same durable outbox and retries after a
30
+ disconnect; it does not gain extra disk work from the hub's choice. Deduplication
31
+ uses a compact per-run interval set for the hub process lifetime. Because both
32
+ history and receipts disappear together on restart, a replay cannot double-count
33
+ restored chart data.
34
+
35
+ Set `HUB_TELEMETRY_MODE=persistent` to opt into disk history. The hub buffers a
36
+ five-second window, publishes live state immediately, writes one compact
37
+ transaction, and only then acknowledges. Replay deduplication uses durable
38
+ receipts whose lifetime is independent of chart retention (default seven days).
26
39
  Replayed samples fill History without replacing current live readings or opening
27
- alerts for past threshold crossings.
40
+ alerts for past threshold crossings. A graceful shutdown flushes pending
41
+ summaries; after a crash the collector replays anything that was never committed.
28
42
 
29
- Default retention:
43
+ Default memory history:
30
44
 
31
45
  | Resolution | Kept for |
32
46
  | --- | --- |
33
- | Raw samples | 48 hours |
34
- | One-minute summaries | 30 days |
35
- | One-hour summaries | 400 days |
36
-
37
- Summaries retain minimum and maximum values. History queries limit the number
38
- of points, series, scanned rows and time spent. The UI marks partial results.
47
+ | 1-second detail | 2 minutes |
48
+ | 10-second summaries | 1 hour |
49
+ | One-minute summaries | 24 hours |
50
+ | One-hour summaries | 7 days |
51
+
52
+ Per-core CPU keeps less detail; machine-wide and capacity metrics keep more.
53
+ Persistent mode writes compact ten-second summaries and folds them into
54
+ one-minute and one-hour tiers; it does not write a raw row per reading. It keeps
55
+ 10-second summaries for 2 hours, one-minute summaries for 30 days and hourly
56
+ summaries for 400 days by default. Both modes retain minimum and maximum values,
57
+ so short spikes remain visible, and both preserve explicit unavailable readings
58
+ and gaps.
59
+
60
+ RAM history is bounded by `HUB_HISTORY_MEMORY_BYTES` (default 128 MiB) across all
61
+ machines. When the budget fills, the oldest fine-grained tiers are dropped first
62
+ and new cardinality beyond the sustainable budget is rejected with visible
63
+ pressure. History queries limit points, series, scanned rows and elapsed time;
64
+ the UI marks partial results.
65
+
66
+ ### Recording
67
+
68
+ Use **History → Recording** to start an explicit capture for the current
69
+ machine and selected metrics. Choose a resolution from 1 second to 1 hour,
70
+ a duration and whether to include recent RAM history. The capture is bounded by
71
+ duration and bytes, survives restart through its private persisted output, and
72
+ can be downloaded by its creator. Recording also works when chart history is
73
+ temporary, which is the intended way to keep an incident snapshot. See
74
+ [telemetry storage](telemetry-storage.md#recording).
39
75
 
40
76
  Hover over a History or Overview recent-usage chart to inspect a recorded time
41
77
  and its bucket averages. The time marker follows across charts in the same view;
@@ -49,11 +85,12 @@ Each collector queue keeps up to ten minutes or 64 MiB of queued JSON per hub,
49
85
  with a 256 MiB total JSON limit. Physical SQLite files, including WAL files,
50
86
  have separate limits of 80 MiB per hub and 640 MiB across eight hubs.
51
87
 
52
- The hub's default database limit is 2 GiB, including its WAL file and all tables.
53
- Retention protects the newest hour of telemetry. If old-data cleanup cannot
54
- free enough space, the hub stops acknowledging new samples and collectors retry.
55
- Existing oversized databases report pressure; startup does not run a destructive
56
- cleanup.
88
+ In persistent mode the hub's default database limit is 2 GiB, including WAL and
89
+ all control tables. Retention protects the newest hour; if old-data cleanup
90
+ cannot free enough space, the hub stops acknowledging new samples and collectors
91
+ retry. Existing oversized databases report pressure; startup does not run a
92
+ destructive cleanup. In memory mode old historical rows may still occupy the
93
+ file until you run `ops reset-telemetry`; they are not read or updated.
57
94
 
58
95
  These are application limits, not filesystem quotas. A transaction can briefly
59
96
  grow the WAL before checkpointing. Blocked checkpoints stop further ingestion
@@ -92,7 +129,10 @@ credentials persist in SQLite. Browser sessions stay in memory, expire after
92
129
  Alerts support above/below thresholds, durations, unavailable readings, missing
93
130
  receipts, maintenance and recovery. Value, unavailable and stale-receipt rules
94
131
  apply to the machines tracked by that hub. Replayed samples do not open past
95
- incidents.
132
+ incidents. Alert rules, incidents and pending notifications stay durable in both
133
+ history modes; with no rules configured, live ingestion does not write per-sample
134
+ alert progress. Enabling rules may write alert state on evaluation, which is
135
+ operational state rather than telemetry history.
96
136
 
97
137
  In **Alerts**, configure one HTTPS webhook channel. Its URL, headers and HMAC
98
138
  secret are write-only. Loopback HTTP is allowed for controlled test sinks.
@@ -10,6 +10,7 @@ building and deploying from source.
10
10
  | Reset a lost password | [Password recovery](#password-recovery) |
11
11
  | Change localhost/HTTPS access | [Access changes](#access-changes) |
12
12
  | Back up or restore a database | [Database commands](#database-commands) |
13
+ | Discard old telemetry history only | [Database commands](#database-commands) |
13
14
  | Plan what to back up | [Consistent backup set](#consistent-backup-set) |
14
15
  | Handle a full database or queue | [Storage pressure recovery](#storage-budgets-and-pressure-recovery) |
15
16
 
@@ -369,6 +370,11 @@ node dist/ops.cjs restore \
369
370
  --database /path/telemetry.sqlite \
370
371
  --replace --confirm-stopped
371
372
 
373
+ # Discard only telemetry history, preserving users/enrollment/settings/alerts.
374
+ node dist/ops.cjs reset-telemetry \
375
+ --database /path/telemetry.sqlite \
376
+ --backup-dir /path/private-backups --confirm-stopped
377
+
372
378
  # Offline managed-user recovery; password comes from a private 0600 file.
373
379
  node dist/ops.cjs recover-admin \
374
380
  --database /path/telemetry.sqlite \
@@ -408,8 +414,9 @@ release must never be pointed at a newer schema. `inspect` reports this state.
408
414
 
409
415
  The database backup is not the whole installation. A restorable set is:
410
416
 
411
- 1. Hub SQLite database: `telemetry.sqlite` (includes registry/admin/alerts).
412
- Use `ops backup`; do not copy the main file alone while the hub is running.
417
+ 1. Hub SQLite database: `telemetry.sqlite` (includes registry/admin/alerts and
418
+ any opt-in persistent history). Use `ops backup`; do not copy the main file
419
+ alone while the hub is running.
413
420
  2. Enrollment/config state when configured: `HUB_STATE_FILE` (for
414
421
  example `hub/enrollment.json`, the import source for previously enrolled
415
422
  collectors) and the private hub environment file. These
@@ -420,6 +427,9 @@ The database backup is not the whole installation. A restorable set is:
420
427
  deduplicates by collector/run/sequence.
421
428
  5. Optional operational metadata: `install-state.json`, units and the built
422
429
  release. Keep the release matching the database schema for rollback.
430
+ 6. Explicit recordings: the private `HUB_RECORDINGS_DIR` (default
431
+ `<hub database directory>/recordings`) if you want downloadable captures to
432
+ survive a restore. They are user output, not part of the database backup.
423
433
 
424
434
  For a consistent set, prefer stopping both services cleanly, then run the online
425
435
  database backup and copy the other private files. For an online backup, the SQLite
@@ -486,7 +496,18 @@ node dist/ops.cjs measure --machines 2 --batches 30 --records-per-batch 5 \
486
496
  It writes synthetic records through the real `TelemetryStore` into a
487
497
  temporary private database and reports accepted/rejected/busy counts, ingestion
488
498
  latency, event-loop delay/utilization, RSS, database/WAL size, store queue and
489
- flush counters, and one history-query latency. The output is labelled synthetic.
499
+ flush counters, and one history-query latency.
500
+
501
+ For the history-mode tradeoff, use the repeatable RAM-vs-persistent benchmark.
502
+ It measures process-attributed disk writes, latency, RSS and rows:
503
+
504
+ ```sh
505
+ npm run benchmark:telemetry -- --machines 2 --seconds 120
506
+ npm run benchmark:telemetry -- --machines 10 --seconds 60 --json
507
+ ```
508
+
509
+ Use `--dir` on a real filesystem; tmpfs does not account storage writes in
510
+ `/proc/self/io`. See [telemetry storage](telemetry-storage.md#benchmark). The output is labelled synthetic.
490
511
  Defaults are intentionally small; upper bounds are 1,000 machines, 2,000 batches,
491
512
  32 records per batch and 100,000 records total. It never opens a service socket
492
513
  or contacts a hub. `--keep` retains the temporary directory for inspection. The
@@ -536,12 +557,20 @@ cadence, inventory and metadata through Sample ingestion while retaining a
536
557
  disconnected state until an authenticated Collector reconnects.
537
558
 
538
559
  The default hub main-file plus WAL budget is 2 GiB, shared by every SQLite table.
539
- The main file has a page-count limit; WAL reserves up to 128 MiB inside that budget.
540
- Telemetry writes return busy on pressure or SQLITE_FULL, with no receipt/ack for a
541
- failed transaction. Retention reclaims old raw/one-minute points in bounded chunks
542
- while preserving the newest hour; hourly rollups retain their configured age window.
543
- A full newest-hour working set must fit the deployment's budget. Outboxes retry
544
- while the operator frees space or provisions a larger supported configuration.
560
+ The main file has a page-count limit; WAL reserves up to 128 MiB inside that budget
561
+ and automatic checkpointing waits for a meaningful 32–64 MiB reserve where the
562
+ budget permits. Persistent-mode writes return busy on pressure or SQLITE_FULL,
563
+ with no receipt/ack for a failed transaction. Retention reclaims old raw or
564
+ ten-second points and one-minute rollups in bounded chunks while preserving the
565
+ newest hour; hourly rollups retain their configured age window. A full
566
+ newest-hour working set must fit the deployment's budget. Outboxes retry while
567
+ the operator frees space or provisions a larger supported configuration.
568
+
569
+ Memory mode keeps only a bounded RAM budget (`HUB_HISTORY_MEMORY_BYTES`, default
570
+ 128 MiB). It does not write routine telemetry rows, but existing historical rows
571
+ from an older installation still occupy the database until removed with
572
+ `ops reset-telemetry` while the hub is stopped. That command preserves users,
573
+ credentials, enrollment, settings, alert state, metadata and inventory.
545
574
 
546
575
  Collector defaults are 64 MiB logical JSON and 80 MiB physical main+WAL per hub,
547
576
  256 MiB logical and 640 MiB physical across eight destinations. Physical pressure
package/docs/releasing.md CHANGED
@@ -5,7 +5,7 @@ The first public version, `0.1.0`, is published on
5
5
  [GitHub](https://github.com/Triet1402/rigscope). The package includes MIT licensing,
6
6
  dependency notices, the security policy and [release notes](../CHANGELOG.md).
7
7
 
8
- The commands below use `0.1.7` as an example. For a new release, substitute its
8
+ The commands below use `0.1.8` as an example. For a new release, substitute its
9
9
  new version everywhere; do not attempt to republish an already published version.
10
10
  Documentation edits can be committed and pushed to GitHub independently. The published archive and
11
11
  its bundled documentation stay unchanged; ship updated package documentation
@@ -21,7 +21,7 @@ with the next version. Keep each release tag on the revision used to build it.
21
21
  4. Run the publish dry run, check the npm account, and publish the verified archive.
22
22
  5. Verify the registry version and install it into a disposable test installation.
23
23
  Check sign-in, fresh telemetry and the installed command.
24
- 6. Tag the exact source revision as `v0.1.7`, push the tag, and create a GitHub
24
+ 6. Tag the exact source revision as `v0.1.8`, push the tag, and create a GitHub
25
25
  Release with the release notes, verified archive and checksum. Announce it
26
26
  after the registry installation works.
27
27
 
@@ -45,14 +45,14 @@ npm pack --pack-destination out/release
45
45
  `npm pack` runs `prepack`, which regenerates dependency notices, builds fresh
46
46
  runtime/UI artifacts and verifies them. If notice generation changes a tracked
47
47
  file, include that change in the release commit before producing the final
48
- archive. Reuse the archive for every check below. Replace `0.1.7` in these
48
+ archive. Reuse the archive for every check below. Replace `0.1.8` in these
49
49
  commands when releasing another version.
50
50
 
51
51
  ## Verify the exact archive
52
52
 
53
53
  ```sh
54
- node scripts/verify-package.cjs ./out/release/rigscope-0.1.7.tgz
55
- npm run verify:installed -- --archive ./out/release/rigscope-0.1.7.tgz
54
+ node scripts/verify-package.cjs ./out/release/rigscope-0.1.8.tgz
55
+ npm run verify:installed -- --archive ./out/release/rigscope-0.1.8.tgz
56
56
  npm test
57
57
  node scripts/test-runtime.cjs
58
58
  node scripts/test.cjs --suite development
@@ -69,9 +69,9 @@ create disposable installations with separate state, launchers, ports and unit
69
69
  names, then remove their services. They do not select an existing installation.
70
70
 
71
71
  ```sh
72
- RIGSCOPE_TEST_ARCHIVE="$PWD/out/release/rigscope-0.1.7.tgz" \
72
+ RIGSCOPE_TEST_ARCHIVE="$PWD/out/release/rigscope-0.1.8.tgz" \
73
73
  node --import tsx --test tests/cli-install.integration.ts tests/cli-reset.integration.ts
74
- node scripts/verify-operations.cjs ./out/release/rigscope-0.1.7.tgz
74
+ node scripts/verify-operations.cjs ./out/release/rigscope-0.1.8.tgz
75
75
  ```
76
76
 
77
77
  These cover installation, pairing, independent component controls, bootstrap
@@ -93,7 +93,7 @@ Record the final checksum after verification:
93
93
 
94
94
  ```sh
95
95
  cd out/release
96
- sha256sum rigscope-0.1.7.tgz > SHA256SUMS
96
+ sha256sum rigscope-0.1.8.tgz > SHA256SUMS
97
97
  sha256sum --check SHA256SUMS
98
98
  ```
99
99
 
@@ -110,7 +110,7 @@ then verify the account and preview the exact archive:
110
110
  ```sh
111
111
  npm login --registry=https://registry.npmjs.org
112
112
  npm whoami --registry=https://registry.npmjs.org
113
- npm publish ./out/release/rigscope-0.1.7.tgz --access public --tag latest \
113
+ npm publish ./out/release/rigscope-0.1.8.tgz --access public --tag latest \
114
114
  --registry=https://registry.npmjs.org --dry-run
115
115
  ```
116
116
 
@@ -120,12 +120,12 @@ to publish or complete two-factor authentication.
120
120
  When releasing, this command actually publishes the verified archive publicly:
121
121
 
122
122
  ```sh
123
- npm publish ./out/release/rigscope-0.1.7.tgz --access public --tag latest \
123
+ npm publish ./out/release/rigscope-0.1.8.tgz --access public --tag latest \
124
124
  --registry=https://registry.npmjs.org
125
- npm view rigscope@0.1.7 version dist.integrity --registry=https://registry.npmjs.org
125
+ npm view rigscope@0.1.8 version dist.integrity --registry=https://registry.npmjs.org
126
126
  ```
127
127
 
128
- Then use `npx --yes rigscope@0.1.7 install` with a disposable `--state-dir`, an
128
+ Then use `npx --yes rigscope@0.1.8 install` with a disposable `--state-dir`, an
129
129
  unused `--port` and an isolated `XDG_BIN_HOME` for the registry installation
130
130
  check. Uninstall that test installation afterward. Use the same isolation
131
131
  settings for all test commands so the check does not select a real installation.
@@ -0,0 +1,187 @@
1
+ # Telemetry storage, history and recording
2
+
3
+ Rigscope has two explicit telemetry-history modes. The default is RAM-only.
4
+ Persistent history is opt-in and writes compact summaries, not one row per
5
+ collector reading.
6
+
7
+ > "No persistent telemetry history" is accurate for memory mode. It is not a
8
+ > promise of zero disk writes: hub logs, SQLite control state, alert state,
9
+ > recordings and operating-system swap may still write.
10
+
11
+ Configuration and non-history state always remain in SQLite: users, password
12
+ hashes, sessions' audit records, collector enrollment and credentials, grants,
13
+ alert rules/incidents/jobs/webhook settings, machine display names/tags, and
14
+ inventory. Only telemetry history is ephemeral by default.
15
+
16
+ ## Modes
17
+
18
+ | | `HUB_TELEMETRY_MODE=memory` (default) | `HUB_TELEMETRY_MODE=persistent` |
19
+ | --- | --- | --- |
20
+ | Live readings | Updated immediately in RAM | Updated immediately in RAM |
21
+ | Recent charts | Bounded typed-array RAM rings | Compact worst-case summaries in SQLite |
22
+ | Ack meaning | Accepted into hub RAM; intentional loss on restart | Committed transaction; durable delivery |
23
+ | Restart | History and latest snapshots disappear | Summaries and latest snapshots restore |
24
+ | Routine telemetry writes | None | One transaction roughly every 5 seconds |
25
+ | Deduplication | Per-run in-RAM interval set | `sample_receipts` with an independent 7-day TTL |
26
+
27
+ The hub sends `delivery: "memory"` or `"durable"` in `welcome`. Collectors keep
28
+ the same durable per-destination outbox, retry and in-flight behavior in both
29
+ cases, so delayed acknowledgements do not move sustained writes from the hub to
30
+ the collectors.
31
+
32
+ ### Memory history defaults
33
+
34
+ The RAM budget is `HUB_HISTORY_MEMORY_BYTES` (default **128 MiB**) for the whole
35
+ hub, not per machine. Within it, each series keeps:
36
+
37
+ | Tier | Default retention | Purpose |
38
+ | --- | --- | --- |
39
+ | 1 second | 2 minutes | Short incident lead-up |
40
+ | 10 seconds | 1 hour | Recent troubleshooting |
41
+ | 1 minute | 24 hours | Medium-term history |
42
+ | 1 hour | 7 days | Longer-term trends |
43
+
44
+ Per-core CPU uses shorter detail: 10-second buckets for 10 minutes, 1-minute for
45
+ 6 hours and 1-hour for 2 days. Other metrics use the table above.
46
+
47
+ Each reading updates the current bucket of each applicable tier. Buckets keep
48
+ `min`, `max`, `sum`, non-null `count` and an explicit unavailable `nullCount`, so
49
+ a one-second spike is still represented after it is aggregated into a minute or
50
+ hour. This is aggregation, not "keep every tenth sample". Empty buckets are
51
+ omitted; an all-unavailable bucket renders as unknown, never zero.
52
+
53
+ All current metrics are gauges or already-derived rates: the collector computes
54
+ network/disk bytes per second from monotonic counters before sending, so the hub
55
+ never averages a cumulative counter and does not need first/last values for rate
56
+ reconstruction. If a cumulative counter metric is added later, it needs an
57
+ explicit delta/first-last aggregation policy rather than this gauge path.
58
+
59
+ When a new series would exceed the global budget, the least-recently-seen series
60
+ lose their finest tiers first, then whole series are evicted. New cardinality
61
+ that still does not fit is rejected and reported as pressure. This makes the
62
+ budget finite even for a machine that adds hundreds of disks or cores.
63
+
64
+ ### Persistent summary history
65
+
66
+ Persistent mode buffers ingestion and commits all series for roughly five
67
+ seconds in one transaction. It writes `points_10s` plus folded one-minute and
68
+ one-hour rollups. It does **not** write `points_raw` unless an operator
69
+ explicitly constructs the store in raw mode (used by tests and measurement).
70
+
71
+ - Live publication is decoupled from the commit, so the dashboard is not one
72
+ flush behind.
73
+ - A sample is acknowledged only after `COMMIT` returns with `synchronous=FULL`.
74
+ - Retries are deduplicated by `(collectorId, runId, sequence)` before any
75
+ point/rollup side effect. Committed records cannot be counted twice.
76
+ - After a crash, uncommitted buffered samples are replayed by the collector
77
+ because they were never acknowledged. Committed samples have receipts.
78
+ - A graceful shutdown flushes pending summaries before closing within the hub's
79
+ bounded shutdown window.
80
+ - Late/replayed samples extend the correct immutable summary bucket; the
81
+ one-minute and one-hour rows receive only the new delta, never a recomputed
82
+ total. Out-of-order samples inside the retained RAM ring update the matching
83
+ completed bucket; samples older than the retained window are counted as late
84
+ and dropped rather than corrupting a newer bucket.
85
+ - Buckets use absolute minute/hour boundaries. A query includes the current
86
+ partial window once, and a later query recomputes only that bucket from the
87
+ same summaries, so completed data is not double-counted.
88
+ - Flush failures resolve pending batches as retry/busy, so collectors keep the
89
+ data and back off. The queue is bounded (`HUB` storage queue and the
90
+ collector's in-flight window) and does not grow without limit.
91
+ - The collector's local in-flight window and the hub's welcome window are both
92
+ 64 batches, the wire maximum. The hub's persistent flush interval is capped
93
+ at five seconds, which fits that window at the fastest supported 100 ms
94
+ cadence (6.4 s of coverage) and leaves 1.4 s for COMMIT/ack latency; it is
95
+ also far below the collector's 30-second per-batch ack timeout. The
96
+ collector reserves enough slots for a full flush window of live samples,
97
+ plus two samples of headroom, and drains a waiting live
98
+ record as soon as an acknowledgement frees capacity. Its durable outbox
99
+ keeps the existing 10-minute/64 MiB per-destination bounds, so delayed acks
100
+ do not turn into sustained collector writes or silent data movement.
101
+
102
+ Dedup receipts default to **7 days** and are independent of chart retention.
103
+ Shortening raw/10-second history can no longer let a replayed sample recreate
104
+ expired chart data.
105
+
106
+ ### Which readers exist
107
+
108
+ Charts, exports and recordings all use the same bounded history query interface,
109
+ so memory and persistent modes do not have separate chart code. Query work is
110
+ bounded by series count, scanned rows, elapsed time and output points. The
111
+ finest tier whose retention covers the requested range is selected; an empty
112
+ finer tier falls through to the next one (for example after upgrading an
113
+ existing database).
114
+
115
+ ## Recording
116
+
117
+ A recording is an explicit, bounded capture and is separate from chart history.
118
+
119
+ - Start/stop from **History → Recording**.
120
+ - Choose the current machine, selected metrics, resolution (1s, 10s, 1m, 5m or
121
+ 1h), duration, and whether to include recent RAM history (the lead-up).
122
+ - The hub writes private JSON-lines data under `HUB_RECORDINGS_DIR`
123
+ (default `<hub database directory>/recordings`, mode 0700 files 0600).
124
+ - Recordings are bounded by `HUB_RECORDING_MAX_DURATION_MS` (default 1 hour) and
125
+ `HUB_RECORDING_MAX_BYTES` (default 64 MiB). A duration or size limit ends the
126
+ capture with an explicit reason.
127
+ - Metadata survives restart. A capture active during a restart is marked
128
+ interrupted and its partial file stays downloadable.
129
+ - Download/list/delete are authenticated and authorized to the creating account
130
+ (administrators can see all) and to the caller's current machine scope. The
131
+ browser names an opaque recording id; it can never name a filesystem path.
132
+
133
+ Recordings are not part of a database-only backup. If they matter, include the
134
+ recordings directory in the same private backup set as the hub database.
135
+
136
+ ## Reset old history
137
+
138
+ Switching an existing installation to memory mode does not read or rewrite old
139
+ telemetry rows, but they still occupy the database. To discard them explicitly
140
+ while preserving users, enrollment, settings and alert state:
141
+
142
+ ```sh
143
+ rigscope stop
144
+ node dist/ops.cjs reset-telemetry \
145
+ --database /path/to/telemetry.sqlite \
146
+ --backup-dir /path/to/private-backups \
147
+ --confirm-stopped
148
+ ```
149
+
150
+ The command deletes only `points_raw`, `points_10s`, `points_1m`, `points_1h`,
151
+ `sample_receipts`, `gaps`, `machine_latest` and `series`. It refuses without
152
+ `--confirm-stopped` and while the hub lock exists. It never drops the database
153
+ file, users, collector registry, alert rules/incidents, metadata, inventory or
154
+ recordings. `--backup-dir` is optional but recommended; a consistent backup is
155
+ taken first when supplied.
156
+
157
+ ## Benchmark
158
+
159
+ `npm run benchmark:telemetry -- --machines 2 --seconds 120` drives the real
160
+ stores with a realistic ~186-series-per-machine, one-second payload. It reports
161
+ process-attributed `/proc/self/io` write bytes (not database size), ingestion
162
+ enqueue/commit latency, history-query latency, RSS, row counts and database
163
+ size. `--json` prints machine-readable output; `--dir` selects a real filesystem
164
+ because tmpfs does not account storage writes in `/proc/self/io`.
165
+
166
+ One development-host result for **2 machines × 60 simulated seconds** (186
167
+ series each) was:
168
+
169
+ | Mode | Steady writes | Close/checkpoint | DB size | Commit p95 | History query |
170
+ | --- | --- | --- | --- | --- | --- |
171
+ | memory | **0.00 MiB** | 248 KiB | 0.3 MiB | 42 ms | 3.0 ms |
172
+ | persistent summaries | **0.98 MiB** | 264 KiB | 1.2 MiB | 35 ms | 5.9 ms |
173
+ | legacy raw, per-batch commit | 21.66 MiB | 692 KiB | 21.6 MiB | 15 ms | 6.8 ms |
174
+
175
+ That run measured persistent summaries at about **0.05×** the legacy write
176
+ volume (roughly 22× less) and memory mode at zero sustained ingestion writes.
177
+
178
+ A second run with **10 machines** (1,860 series) and the default 128 MiB RAM
179
+ budget reported `RAM 128/128 MiB pressure` while keeping all series readable
180
+ through the coarser tiers: memory wrote 0.00 MiB, persistent summaries 4.05 MiB,
181
+ and legacy raw 104.76 MiB of steady ingestion-phase writes. RSS peaks include
182
+ the Node baseline and temporary query/flush allocations, so they are higher than
183
+ the history-array budget.
184
+
185
+ These numbers are development-host measurements, not a promised multiplier or
186
+ capacity guarantee. Re-run on the deployment host and payload before choosing
187
+ retention or budgets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rigscope",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "engines": {
6
6
  "node": ">=26.0.0"
@@ -22,6 +22,7 @@
22
22
  "test": "node scripts/test.cjs",
23
23
  "test:runtime": "npm run build:runtime && node scripts/test-runtime.cjs",
24
24
  "test:dev": "npm run build:runtime && node scripts/test.cjs --suite development",
25
+ "benchmark:telemetry": "tsx scripts/telemetry-benchmark.ts",
25
26
  "validate:https": "node scripts/validate-https.cjs",
26
27
  "build:ui": "next build --webpack",
27
28
  "rigscope": "node bin/rigscope.cjs",
@@ -70,6 +71,7 @@
70
71
  "docs/system-installation.md",
71
72
  "docs/configuration.md",
72
73
  "docs/monitoring.md",
74
+ "docs/telemetry-storage.md",
73
75
  "docs/operations.md",
74
76
  "docs/bottlenecks.md",
75
77
  "docs/exports.md",