rigscope 0.0.0-stage → 0.1.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 (188) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-path-routes-manifest.json +13 -0
  3. package/.next/build-manifest.json +20 -0
  4. package/.next/export-marker.json +6 -0
  5. package/.next/images-manifest.json +68 -0
  6. package/.next/next-minimal-server.js.nft.json +1 -0
  7. package/.next/next-server.js.nft.json +1 -0
  8. package/.next/package.json +1 -0
  9. package/.next/prerender-manifest.json +297 -0
  10. package/.next/react-loadable-manifest.json +1 -0
  11. package/.next/required-server-files.json +369 -0
  12. package/.next/routes-manifest.json +114 -0
  13. package/.next/server/app/_global-error/page.js +31 -0
  14. package/.next/server/app/_global-error/page.js.nft.json +1 -0
  15. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  16. package/.next/server/app/_global-error.html +1 -0
  17. package/.next/server/app/_global-error.meta +35 -0
  18. package/.next/server/app/_global-error.rsc +18 -0
  19. package/.next/server/app/_global-error.segments/_full.segment.rsc +18 -0
  20. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +24 -0
  21. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -0
  22. package/.next/server/app/_not-found/page.js +1 -0
  23. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  24. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  25. package/.next/server/app/_not-found.html +1 -0
  26. package/.next/server/app/_not-found.meta +35 -0
  27. package/.next/server/app/_not-found.rsc +24 -0
  28. package/.next/server/app/_not-found.segments/_full.segment.rsc +24 -0
  29. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +30 -0
  30. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
  31. package/.next/server/app/administration/page.js +1 -0
  32. package/.next/server/app/administration/page.js.nft.json +1 -0
  33. package/.next/server/app/administration/page_client-reference-manifest.js +1 -0
  34. package/.next/server/app/administration.html +1 -0
  35. package/.next/server/app/administration.meta +34 -0
  36. package/.next/server/app/administration.rsc +29 -0
  37. package/.next/server/app/administration.segments/_full.segment.rsc +29 -0
  38. package/.next/server/app/administration.segments/_tree.segment.rsc +4 -0
  39. package/.next/server/app/administration.segments/administration/__PAGE__.segment.rsc +35 -0
  40. package/.next/server/app/alerts/page.js +1 -0
  41. package/.next/server/app/alerts/page.js.nft.json +1 -0
  42. package/.next/server/app/alerts/page_client-reference-manifest.js +1 -0
  43. package/.next/server/app/alerts.html +1 -0
  44. package/.next/server/app/alerts.meta +34 -0
  45. package/.next/server/app/alerts.rsc +28 -0
  46. package/.next/server/app/alerts.segments/_full.segment.rsc +28 -0
  47. package/.next/server/app/alerts.segments/_tree.segment.rsc +4 -0
  48. package/.next/server/app/alerts.segments/alerts/__PAGE__.segment.rsc +34 -0
  49. package/.next/server/app/connections/page.js +1 -0
  50. package/.next/server/app/connections/page.js.nft.json +1 -0
  51. package/.next/server/app/connections/page_client-reference-manifest.js +1 -0
  52. package/.next/server/app/connections.html +1 -0
  53. package/.next/server/app/connections.meta +34 -0
  54. package/.next/server/app/connections.rsc +29 -0
  55. package/.next/server/app/connections.segments/_full.segment.rsc +29 -0
  56. package/.next/server/app/connections.segments/_tree.segment.rsc +4 -0
  57. package/.next/server/app/connections.segments/connections/__PAGE__.segment.rsc +35 -0
  58. package/.next/server/app/history/page.js +2 -0
  59. package/.next/server/app/history/page.js.nft.json +1 -0
  60. package/.next/server/app/history/page_client-reference-manifest.js +1 -0
  61. package/.next/server/app/history.html +1 -0
  62. package/.next/server/app/history.meta +34 -0
  63. package/.next/server/app/history.rsc +33 -0
  64. package/.next/server/app/history.segments/_full.segment.rsc +33 -0
  65. package/.next/server/app/history.segments/_tree.segment.rsc +4 -0
  66. package/.next/server/app/history.segments/history/__PAGE__.segment.rsc +37 -0
  67. package/.next/server/app/icon.svg/route.js +1 -0
  68. package/.next/server/app/icon.svg/route.js.nft.json +1 -0
  69. package/.next/server/app/icon.svg.body +1 -0
  70. package/.next/server/app/icon.svg.meta +1 -0
  71. package/.next/server/app/index.html +1 -0
  72. package/.next/server/app/index.meta +29 -0
  73. package/.next/server/app/index.rsc +26 -0
  74. package/.next/server/app/index.segments/__PAGE__.segment.rsc +31 -0
  75. package/.next/server/app/index.segments/_full.segment.rsc +26 -0
  76. package/.next/server/app/index.segments/_tree.segment.rsc +3 -0
  77. package/.next/server/app/login/page.js +1 -0
  78. package/.next/server/app/login/page.js.nft.json +1 -0
  79. package/.next/server/app/login/page_client-reference-manifest.js +1 -0
  80. package/.next/server/app/login.html +1 -0
  81. package/.next/server/app/login.meta +34 -0
  82. package/.next/server/app/login.rsc +26 -0
  83. package/.next/server/app/login.segments/_full.segment.rsc +26 -0
  84. package/.next/server/app/login.segments/_tree.segment.rsc +4 -0
  85. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +32 -0
  86. package/.next/server/app/machines/[machineId]/page.js +1 -0
  87. package/.next/server/app/machines/[machineId]/page.js.nft.json +1 -0
  88. package/.next/server/app/machines/[machineId]/page_client-reference-manifest.js +1 -0
  89. package/.next/server/app/page.js +1 -0
  90. package/.next/server/app/page.js.nft.json +1 -0
  91. package/.next/server/app/page_client-reference-manifest.js +1 -0
  92. package/.next/server/app/processes/page.js +2 -0
  93. package/.next/server/app/processes/page.js.nft.json +1 -0
  94. package/.next/server/app/processes/page_client-reference-manifest.js +1 -0
  95. package/.next/server/app/processes.html +1 -0
  96. package/.next/server/app/processes.meta +34 -0
  97. package/.next/server/app/processes.rsc +30 -0
  98. package/.next/server/app/processes.segments/_full.segment.rsc +30 -0
  99. package/.next/server/app/processes.segments/_tree.segment.rsc +4 -0
  100. package/.next/server/app/processes.segments/processes/__PAGE__.segment.rsc +36 -0
  101. package/.next/server/app-paths-manifest.json +13 -0
  102. package/.next/server/chunks/161.js +1 -0
  103. package/.next/server/chunks/168.js +1 -0
  104. package/.next/server/chunks/266.js +68 -0
  105. package/.next/server/chunks/430.js +116 -0
  106. package/.next/server/chunks/721.js +1 -0
  107. package/.next/server/chunks/751.js +1 -0
  108. package/.next/server/chunks/877.js +1 -0
  109. package/.next/server/chunks/894.js +2 -0
  110. package/.next/server/functions-config-manifest.json +4 -0
  111. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  112. package/.next/server/middleware-build-manifest.js +1 -0
  113. package/.next/server/middleware-manifest.json +6 -0
  114. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  115. package/.next/server/next-font-manifest.js +1 -0
  116. package/.next/server/next-font-manifest.json +1 -0
  117. package/.next/server/pages/404.html +1 -0
  118. package/.next/server/pages/500.html +1 -0
  119. package/.next/server/pages-manifest.json +4 -0
  120. package/.next/server/prefetch-hints.json +123 -0
  121. package/.next/server/preview-props.json +5 -0
  122. package/.next/server/server-reference-manifest.js +1 -0
  123. package/.next/server/server-reference-manifest.json +1 -0
  124. package/.next/server/webpack-runtime.js +1 -0
  125. package/.next/static/DZKWF9ur7H5wI0c7RAhdq/_buildManifest.js +1 -0
  126. package/.next/static/DZKWF9ur7H5wI0c7RAhdq/_ssgManifest.js +1 -0
  127. package/.next/static/chunks/103-c47318242b24e9ac.js +1 -0
  128. package/.next/static/chunks/157-0d36ebf66af048ac.js +1 -0
  129. package/.next/static/chunks/227-417939c480f50747.js +1 -0
  130. package/.next/static/chunks/408-18819c6490a5f8d3.js +1 -0
  131. package/.next/static/chunks/4bd1b696-c7408084347438ae.js +1 -0
  132. package/.next/static/chunks/794-ceeb732a1a81bc27.js +52 -0
  133. package/.next/static/chunks/939-2600d7c8e583d5ef.js +1 -0
  134. package/.next/static/chunks/app/_global-error/page-aba28f88d5781771.js +1 -0
  135. package/.next/static/chunks/app/_not-found/page-a2a510233e4836e2.js +1 -0
  136. package/.next/static/chunks/app/administration/page-4b16b19bb6a39c36.js +1 -0
  137. package/.next/static/chunks/app/alerts/page-fe0e877778c60af8.js +1 -0
  138. package/.next/static/chunks/app/connections/page-99cd54d6929b3f1f.js +1 -0
  139. package/.next/static/chunks/app/history/page-3fa5846eca26b7f7.js +3 -0
  140. package/.next/static/chunks/app/layout-4feff677c4071745.js +1 -0
  141. package/.next/static/chunks/app/login/page-c57e12dbe5146737.js +1 -0
  142. package/.next/static/chunks/app/machines/[machineId]/page-a5aa1d8676272f38.js +1 -0
  143. package/.next/static/chunks/app/page-1ae9fc94c897785b.js +1 -0
  144. package/.next/static/chunks/app/processes/page-e471ed23a1d5632f.js +3 -0
  145. package/.next/static/chunks/framework-ea4f97ca039bd777.js +1 -0
  146. package/.next/static/chunks/main-a458d32948147b84.js +5 -0
  147. package/.next/static/chunks/main-app-f92328c8950d2e53.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/app-error-aba28f88d5781771.js +1 -0
  149. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-aba28f88d5781771.js +1 -0
  150. package/.next/static/chunks/next/dist/client/components/builtin/global-error-43cbaa3aa1576eb4.js +1 -0
  151. package/.next/static/chunks/next/dist/client/components/builtin/not-found-aba28f88d5781771.js +1 -0
  152. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-aba28f88d5781771.js +1 -0
  153. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  154. package/.next/static/chunks/webpack-9ca43f4ce1bb8938.js +1 -0
  155. package/.next/static/css/e41ccbccc3afaae5.css +1 -0
  156. package/ARCHITECTURE.md +279 -0
  157. package/CHANGELOG.md +25 -0
  158. package/DESIGN.md +299 -0
  159. package/LICENSE +21 -0
  160. package/README.md +157 -2
  161. package/SECURITY.md +12 -0
  162. package/THIRD_PARTY_NOTICES.md +8847 -0
  163. package/bin/rigscope.cjs +6 -0
  164. package/dist/cli.cjs +9075 -0
  165. package/dist/collector.cjs +5671 -0
  166. package/dist/hub.cjs +8355 -0
  167. package/dist/ops.cjs +4972 -0
  168. package/dist/system.cjs +4333 -0
  169. package/docs/bottlenecks.md +62 -0
  170. package/docs/development.md +46 -0
  171. package/docs/exports.md +39 -0
  172. package/docs/installation.md +477 -0
  173. package/docs/operations.md +515 -0
  174. package/docs/releasing.md +152 -0
  175. package/docs/scale-acceptance.json +412 -0
  176. package/docs/scale-validation.json +1898 -0
  177. package/docs/scale-validation.md +263 -0
  178. package/next.config.js +38 -0
  179. package/package.json +109 -4
  180. package/packaging/README.md +5 -0
  181. package/packaging/collector.env.example +24 -0
  182. package/packaging/hub.env.example +31 -0
  183. package/packaging/system/90-rigscope-rapl.rules +7 -0
  184. package/packaging/system/README.md +5 -0
  185. package/packaging/system/rigscope-collector.service.example +53 -0
  186. package/packaging/system/rigscope-hub.service.example +50 -0
  187. package/packaging/system/rigscope-rapl.sysusers +5 -0
  188. package/packaging/system/rigscope.sysusers +7 -0
@@ -0,0 +1,263 @@
1
+ # Sustained scale validation
2
+
3
+ Status: implementation and local sustained measurement, 2026-11-04.
4
+
5
+ This document records the isolated sustained workload added after the earlier
6
+ `ops measure` burst. It is deliberately **not** a capacity guarantee. The
7
+ numbers below describe one host, one payload shape, one database and the
8
+ concurrency listed for each case. They do not prove that a larger fleet,
9
+ different hardware, slower storage or a different browser workload will behave
10
+ the same way.
11
+
12
+ ## What was missing
13
+
14
+ The earlier `ops measure` command was a bounded synthetic burst capped at 50
15
+ synthetic machines; it queues a fixed number of records and by default runs
16
+ History query iterations sequentially unless --history-concurrency is supplied. It never opens a service socket, so it
17
+ cannot show concurrent browser requests, SSE delivery, slow consumers,
18
+ collector reconnects or replay. Those paths are now measured by a standalone
19
+ harness instead of being inferred from the burst runner.
20
+
21
+ `src/operations/measure.ts` was also extended:
22
+
23
+ - the synthetic machine bound is now 1,000 instead of 50,
24
+ - `historyConcurrency` initiates overlapping History request rounds and reports
25
+ their wall time, p95 and query count,
26
+ - the existing CLI arguments and report fields remain compatible.
27
+
28
+ ## Harness and isolation
29
+
30
+ The harness is `scripts/scale-harness.ts`. It is a standalone source-checkout
31
+ tool, separate from the installed operational CLI. With development dependencies
32
+ installed, run it with `tsx`:
33
+
34
+ ```sh
35
+ npx tsx scripts/scale-harness.ts --suite \
36
+ --json-out docs/scale-validation.json
37
+ ```
38
+
39
+ The parent process:
40
+
41
+ 1. creates a fresh `0700` temporary directory and a `0600` hub configuration,
42
+ 2. writes a throwaway SQLite database path,
43
+ 3. spawns the same script in `hub` child mode on an ephemeral `127.0.0.1`
44
+ port,
45
+ 4. logs in real browser sessions through `/api/auth/login` (including the
46
+ normal scrypt work and login token bucket),
47
+ 5. connects real collector WebSockets through `/api/collector/connect`,
48
+ 6. reads authenticated `/api/live` SSE streams and `/api/history` responses,
49
+ 7. stops the child, closes the database and removes the temporary directory
50
+ unless `--keep` is set.
51
+
52
+ The child is composed from the production modules: `HubState`,
53
+ `TelemetryStore`, `SampleIngestion`, `CollectorGateway`, `LiveFeed`,
54
+ `BrowserHttpAuth`, `BrowserSessionStore` and `MonitorHttp`. The database uses
55
+ WAL, `synchronous=FULL` and the real commit-before-ack path. The only synthetic
56
+ parts are the metric values and the cadence driver. No live hub, service
57
+ socket, production database or Tailscale endpoint is contacted.
58
+
59
+ The harness exposes a private `GET /__scale/stats` control endpoint on the
60
+ isolated child. It returns event-loop delay percentiles, event-loop
61
+ utilization, process CPU/RSS, database/WAL bytes, telemetry store counters,
62
+ connected collector count and SSE client count. The endpoint is not part of
63
+ the hub and is never reachable from a deployment.
64
+
65
+ ## Workload
66
+
67
+ Each case uses representative metrics: eight CPU cores, two GPUs, two
68
+ filesystems, two disk devices, two network interfaces, plus the optional
69
+ bottleneck fields (temperature, package power, I/O wait, pressure, inode use,
70
+ disk latency and utilization). One record carries roughly 50 stored series
71
+ points before rollups.
72
+
73
+ | Case | Machines | Measured phase | Live cadence | History viewers | SSE viewers | Special workload |
74
+ | --- | ---: | ---: | ---: | ---: | ---: | --- |
75
+ | `small-5m` | 5 | 60 s | 1 s | 4 | 2 | small-fleet baseline |
76
+ | `fleet-75m` | 75 | 60 s | 1 s | 8 | 4 | beyond the old 50-machine cap |
77
+ | `fleet-75m-churn` | 75 | 60 s | 1 s | 8 | 4 | 15 collectors reconnect every 15 s with 20 replay records; SSE viewers reconnect with `Last-Event-ID` |
78
+ | `fleet-75m-slow-sse` | 75 | 60 s | 1 s | 8 | 6 | two SSE viewers pause reading for 15 s, measuring stale delivery and any buffer drop/reconnect |
79
+ | `fleet-75m-history` | 75 | 60 s | 1 s | 8 | 2 | broad History metric set every 300 ms per viewer |
80
+
81
+ Before each measured phase the harness connects all collectors and waits
82
+ 1.5 seconds. The measured phase starts after that warmup. Records are counted
83
+ from collector startup, so the ingestion counters include the warmup and bounded drain tail; the
84
+ event-loop, CPU, RSS and database-growth windows are reset at the measured
85
+ phase boundary.
86
+
87
+ ### Bounds
88
+
89
+ - machines: 1–1,000,
90
+ - measured phase: up to 1 hour,
91
+ - sample cadence: at least 100 ms (the protocol rate limit),
92
+ - records per live batch: 1–32,
93
+ - viewers: 1–50 authenticated sessions,
94
+ - SSE clients: 1–64,
95
+ - replay: at most 128 records per reconnect,
96
+ - estimated live plus replay records: at most 500,000 per case,
97
+ - database: 512 MiB main+WAL by default (`--database-budget-mb 0` disables the
98
+ cap for inspection only),
99
+ - temporary database/config directory: removed after the case unless
100
+ `--keep` is used.
101
+
102
+ ## Host
103
+
104
+ The measurement host was:
105
+
106
+ - AMD EPYC 7402P 24-Core Processor, 48 logical CPUs,
107
+ - 121 GiB usable memory (130,351,214,592 bytes reported by the OS),
108
+ - Linux 7.3.0-9-generic, Ubuntu 26.10, x86_64,
109
+ - Node.js 26.13.0,
110
+ - local NVMe root filesystem (the harness database lived under `/tmp`),
111
+ - load average 0.92 / 0.76 / 0.84 around the run; unrelated user services were
112
+ active, so this is not a quiet benchmark host.
113
+
114
+ `monitorEventLoopDelay` used a 10 ms resolution. The reported p50 therefore
115
+ floors near 10 ms even when the process is mostly idle; p95/p99/max are the
116
+ meaningful tail values.
117
+
118
+ ## Results
119
+
120
+ All five cases completed with zero rejected records, zero `retry` acknowledgments,
121
+ zero unacknowledged live records, zero write failures and no disk-budget
122
+ pressure. The event-loop window excludes the 1.5 s collector warmup. "History /
123
+ SSE" is authenticated History sessions / SSE clients. "Sample→commit" in the original report is capture-to-durable-ack latency: an
124
+ upper bound on commit lag that also includes the acknowledgment return path.
125
+ It is measured on the same host clock, not across uncalibrated remote clocks.
126
+ "Normal SSE lag" is the
127
+ age of the newest captured sample when an `update`/`snapshot` arrives at a
128
+ normal viewer; it includes the hub's 500 ms coalescing window. Hub CPU is the
129
+ hub child's process CPU as a percentage of one core, not the whole host.
130
+
131
+ | Case | Machines | History / SSE | Records accepted | Ack p95 | Sample→commit p95 | History p95 / max | Normal SSE lag p95 | Event-loop p99 / max | Hub CPU | RSS peak | DB+WAL growth |
132
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
133
+ | `small-5m` | 5 | 4 / 2 | 317 / 317 | 26.7 ms | 27 ms | 12.9 / 20.0 ms | 526 ms | 12.5 / 18.5 ms | 2.6% | 135.9 MiB | 4.63 MiB |
134
+ | `fleet-75m` | 75 | 8 / 4 | 4,789 / 4,789 | 55.6 ms | 56 ms | 20.9 / 35.2 ms | 439 ms | 30.4 / 44.6 ms | 12.4% | 188.7 MiB | 12.08 MiB |
135
+ | `fleet-75m-churn` | 75 | 8 / 4 | 4,845 / 4,845 | 52.2 ms | 52 ms | 23.4 / 35.0 ms | 438 ms | 29.0 / 42.4 ms | 14.2% | 203.2 MiB | 14.22 MiB |
136
+ | `fleet-75m-slow-sse` | 75 | 8 / 6 | 4,791 / 4,791 | 64.4 ms | 64 ms | 23.0 / 36.6 ms | 502 ms | 38.5 / 76.2 ms | 12.0% | 190.2 MiB | 12.95 MiB |
137
+ | `fleet-75m-history` | 75 | 8 / 2 | 4,785 / 4,785 | 70.7 ms | 71 ms | 50.3 / 60.5 ms | 453 ms | 48.0 / 72.7 ms | 31.2% | 229.7 MiB | 12.98 MiB |
138
+
139
+ Additional observations:
140
+
141
+ - **History authorization budget held.** The broad case ran 1,608
142
+ authenticated queries (8 sessions polling every 300 ms) with 1,608 `200`
143
+ responses, zero `429` responses and zero errors. Each response returned
144
+ about 25 series and 800 points; the 25,000-row/100 ms budget was not
145
+ truncated.
146
+ - **Collector reconnect/replay recovered.** The churn case performed 45
147
+ collector reconnects and replayed 900 historical records; all 900 were
148
+ accepted. First live acknowledgment after reconnect was 167.9 ms p95
149
+ (175.8 ms max). The durable queue finished empty.
150
+ - **SSE reconnect/reset recovered.** The churn case reconnected 48 SSE streams
151
+ and received 48 `reset` snapshots; all resets followed a reconnect. First
152
+ event after reconnect was 10.1 ms p95.
153
+ - **Slow consumers stayed connected but stale.** The two paused viewers
154
+ received no new bytes for 15 s and then read buffered snapshots. Their
155
+ delivery lag was 11.6 s p95 while normal viewers stayed at 502 ms p95. On
156
+ this loopback path no slow-viewer drop or reconnect was observed, so the
157
+ hub's 1 MiB writable-buffer guard did not fire; kernel socket buffering
158
+ likely absorbed the pause. The deliberate disconnect/reconnect path is
159
+ covered by the churn case instead.
160
+ - **Database/WAL stayed bounded.** The final main+WAL size was 14.8 MiB for
161
+ the 75-machine baseline, 16.9 MiB with churn, and 15.7 MiB with broad
162
+ History. The 512 MiB measurement budget was never approached, no checkpoint
163
+ pressure appeared, and the store queue was empty at the end of every case.
164
+ - **Small fleet stayed cheap.** Five machines, four History viewers and two SSE
165
+ viewers used 2.6% of one core and stayed at 12.5 ms event-loop p99.
166
+
167
+ The full machine-readable report, including per-case store counters, database
168
+ sizes, SSE byte counts, tier counts and host load averages, is
169
+ [scale-validation.json](scale-validation.json). The generated JSON is the raw
170
+ artifact; this page is the summary.
171
+
172
+ ## Interpretation
173
+
174
+ These results support the following limited statements on this host:
175
+
176
+ - A 75-machine, 1-second-cadence synthetic fleet can be ingested with
177
+ commit-before-ack p95 under 100 ms while eight authenticated History viewers
178
+ and four full-fleet SSE viewers run concurrently.
179
+ - Collector reconnect plus 20-record replay per affected machine recovered
180
+ within a few hundred milliseconds in this run, without rejected or unacked
181
+ records.
182
+ - SSE reconnect with `Last-Event-ID` produced a fresh `reset` snapshot within
183
+ about 10 ms on loopback, and the next update was fresh again.
184
+ - Broad History polling near the per-session token budget raised the
185
+ event-loop tail to about 48 ms p99 and used about 31% of one core, but
186
+ produced no 429s and no truncated responses at this data size.
187
+
188
+ They do **not** support claims that:
189
+
190
+ - 75 machines is a capacity limit or a safe upper bound,
191
+ - a larger fleet, more viewers, slower storage or a different payload will meet
192
+ the same latencies,
193
+ - the hub is multi-core, clustered or horizontally scalable,
194
+ - slow consumers are always disconnected before they fall behind,
195
+ - the default 100 concurrent collector socket budget should be raised without
196
+ a deployment-specific measurement.
197
+
198
+ The 75-machine cases used the default 100-connection budget. A deployment that
199
+ raises `HUB_MAX_COLLECTOR_CONNECTIONS` must re-run the harness with its own
200
+ payloads, storage and viewer counts.
201
+
202
+ ## Reproduction
203
+
204
+ ```sh
205
+ # full five-case suite (about five minutes)
206
+ npx tsx scripts/scale-harness.ts --suite \
207
+ --json-out docs/scale-validation.json
208
+
209
+ # one case with overrides
210
+ npx tsx scripts/scale-harness.ts --case fleet-75m \
211
+ --machines 75 --viewers 8 --sse-clients 4 --duration-ms 60000 \
212
+ --json-out /tmp/rigscope-scale-75m.json
213
+
214
+ # retain the throwaway database and configuration for inspection
215
+ npx tsx scripts/scale-harness.ts --case small-5m --keep
216
+ ```
217
+
218
+ The short `tests/scale-harness.test.ts` smoke runs the same child with two
219
+ machines, collector reconnect/replay and an SSE reset. It is not a capacity
220
+ test; it proves the harness paths work in CI.
221
+
222
+ ## Limits
223
+
224
+ - The hub child and all load generators share one host. The hub event loop uses
225
+ one thread; the load processes can run on other cores. This is
226
+ not a distributed-load test and the generator competes with the hub.
227
+ - History queries are synchronous SQLite work on the hub event loop.
228
+ "Concurrent viewers" means overlapping authenticated HTTP requests, not
229
+ parallel SQLite readers.
230
+ - The payload is representative but not captured from a real fleet. Different
231
+ process counts, GPUs, filesystems or optional fields change the stored row
232
+ count.
233
+ - The host may run unrelated services. Load averages and CPU are recorded in
234
+ the JSON artifact, but they are not a controlled benchmark environment.
235
+ - Passing these cases does not remove the concurrent collector socket budget
236
+ (default 100, configurable with `HUB_MAX_COLLECTOR_CONNECTIONS`) or the
237
+ history row/elapsed/frame/storage budgets.
238
+ - Authentication and machine scopes use the production modules with loopback
239
+ origin rules. This harness does not exercise the deployed HTTPS/private-socket
240
+ boundary; its separate runtime and actual-path checks remain distinct.
241
+ - A result is evidence for this host and workload only. It is not a claim of
242
+ unlimited machine, viewer or History capacity.
243
+
244
+
245
+ ## Root integration acceptance
246
+
247
+ After integration, a separate 75-machine, eight-second churn run used eight
248
+ History sessions and four SSE consumers. Its authorization canary required
249
+ anonymous SSE 401, an allowed scoped History query 200, a forbidden machine query
250
+ 403, and correctly filtered initial and reconnect/reset SSE snapshots. All passed.
251
+ The run accepted 930 live records plus 600 replay records, recorded 30 collector
252
+ reconnects, finished with zero unacked live records and zero write failures.
253
+ `scale-acceptance.json` records this shorter integration check. It is not another
254
+ 60-second capacity measurement; the five sustained results above remain the
255
+ worker's original raw artifact from before the authorization canary was added.
256
+
257
+ The original `sampleToCommitLagMs` field names are retained for compatibility.
258
+ Those numbers are capture-to-durable-ack upper bounds on commit latency, including
259
+ the return path; they are not a server-side SQL commit timestamp. Queue/backpressure
260
+ counters and end-of-run queue emptiness are recorded, but the original report does
261
+ not sample an exact queue high-water mark. Slow viewers did not reach the server's
262
+ buffer-disconnect threshold in these runs, so that threshold is not a measured
263
+ fleet-capacity limit.
package/next.config.js ADDED
@@ -0,0 +1,38 @@
1
+ const { PHASE_DEVELOPMENT_SERVER } = require("next/constants");
2
+ const { availableParallelism } = require("node:os");
3
+
4
+ const workers = Number(process.env.RIGSCOPE_BUILD_WORKERS ?? Math.min(4, availableParallelism()));
5
+ if (!Number.isInteger(workers) || workers < 1 || workers > 64) {
6
+ throw new Error(
7
+ `RIGSCOPE_BUILD_WORKERS must be an integer from 1 to 64 (received ${JSON.stringify(process.env.RIGSCOPE_BUILD_WORKERS)}). ` +
8
+ "Set it to the number of build workers, or leave it unset to auto-select up to four.",
9
+ );
10
+ }
11
+
12
+ /** @param {string} phase @returns {import("next").NextConfig} */
13
+ const nextConfig = (phase) => ({
14
+ // Keep dev writes separate from the packaged hub's production artifact.
15
+ // Use a small static-generation worker pool for this six-page scaffold.
16
+ distDir: phase === PHASE_DEVELOPMENT_SERVER ? ".next-dev" : ".next",
17
+ experimental: { cpus: workers },
18
+ // Remote development goes through an outer TLS terminator and a Unix
19
+ // socket, so the browser's Origin hostname is the configured public origin,
20
+ // not localhost. Keep an explicit hostname allowlist for /_next assets and
21
+ // HMR (Next also adds the createNextApp hostname). Configuring this list
22
+ // makes Next's dev guard block unlisted origins instead of only warning.
23
+ // The hub validates the full exact origin before delegation.
24
+ allowedDevOrigins: phase === PHASE_DEVELOPMENT_SERVER ? publicDevOrigin() : undefined,
25
+ });
26
+
27
+ function publicDevOrigin() {
28
+ const value = process.env.HUB_PUBLIC_ORIGIN;
29
+ if (value === undefined || value === "") return undefined;
30
+ try {
31
+ const origin = new URL(value);
32
+ return origin.protocol === "https:" ? [origin.hostname] : undefined;
33
+ } catch {
34
+ return undefined;
35
+ }
36
+ }
37
+
38
+ module.exports = nextConfig;
package/package.json CHANGED
@@ -1,6 +1,111 @@
1
1
  {
2
2
  "name": "rigscope",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "rigscopeConfigVersion": 2,
5
+ "private": false,
6
+ "engines": {
7
+ "node": ">=26.0.0"
8
+ },
9
+ "os": [
10
+ "linux"
11
+ ],
12
+ "scripts": {
13
+ "dev": "NODE_ENV=development tsx src/hub/main.ts",
14
+ "build": "npm run build:ui && npm run build:runtime",
15
+ "build:runtime": "node scripts/build-runtime.cjs",
16
+ "start": "node dist/hub.cjs",
17
+ "collector:mock": "COLLECTOR_MODE=mock tsx collector/main.ts",
18
+ "collector:enroll": "tsx collector/main.ts --enroll",
19
+ "collector:start": "node dist/collector.cjs",
20
+ "ops": "node dist/ops.cjs",
21
+ "package:release": "npm run build && node scripts/package-release.cjs",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "node scripts/test.cjs",
24
+ "test:runtime": "npm run build:runtime && node scripts/test-runtime.cjs",
25
+ "test:dev": "npm run build:runtime && node scripts/test.cjs --suite development",
26
+ "validate:https": "node scripts/validate-https.cjs",
27
+ "build:ui": "next build --webpack",
28
+ "rigscope": "node bin/rigscope.cjs",
29
+ "verify:build": "node scripts/verify-build.cjs",
30
+ "verify:package": "node scripts/verify-package.cjs",
31
+ "verify:installed": "node scripts/verify-installed-package.cjs",
32
+ "prepack": "node scripts/prepack.cjs",
33
+ "package:npm": "npm pack",
34
+ "notices": "node scripts/third-party-notices.cjs"
35
+ },
36
+ "dependencies": {
37
+ "lucide-react": "^0.475.0",
38
+ "next": "^16.4.5",
39
+ "next-themes": "^0.4.6",
40
+ "react": "^19.1.0",
41
+ "react-dom": "^19.1.0",
42
+ "ws": "^8.22.1"
43
+ },
44
+ "devDependencies": {
45
+ "@tailwindcss/postcss": "^4.1.0",
46
+ "@types/node": "^22.13.0",
47
+ "@types/react": "^19.1.0",
48
+ "@types/react-dom": "^19.1.0",
49
+ "@types/ws": "^8.18.2",
50
+ "esbuild": "^0.28.2",
51
+ "postcss": "^8.5.23",
52
+ "tailwindcss": "^4.1.0",
53
+ "tsx": "^4.23.15",
54
+ "typescript": "^5.7.0"
55
+ },
56
+ "description": "Your machines, in view. A self-hosted Linux system and process monitor.",
57
+ "bin": {
58
+ "rigscope": "bin/rigscope.cjs"
59
+ },
60
+ "files": [
61
+ "bin/",
62
+ "dist/",
63
+ ".next/BUILD_ID",
64
+ ".next/*.json",
65
+ ".next/server/",
66
+ ".next/static/",
67
+ "next.config.js",
68
+ "README.md",
69
+ "CHANGELOG.md",
70
+ "docs/installation.md",
71
+ "docs/operations.md",
72
+ "docs/bottlenecks.md",
73
+ "docs/exports.md",
74
+ "docs/development.md",
75
+ "docs/releasing.md",
76
+ "docs/scale-validation.md",
77
+ "docs/scale-validation.json",
78
+ "docs/scale-acceptance.json",
79
+ "ARCHITECTURE.md",
80
+ "DESIGN.md",
81
+ "LICENSE",
82
+ "SECURITY.md",
83
+ "THIRD_PARTY_NOTICES.md",
84
+ "packaging/"
85
+ ],
86
+ "license": "MIT",
87
+ "author": {
88
+ "name": "Triet (@Triet1402)",
89
+ "url": "https://github.com/Triet1402"
90
+ },
91
+ "repository": {
92
+ "type": "git",
93
+ "url": "git+https://github.com/Triet1402/rigscope.git"
94
+ },
95
+ "homepage": "https://github.com/Triet1402/rigscope#readme",
96
+ "bugs": {
97
+ "url": "https://github.com/Triet1402/rigscope/issues"
98
+ },
99
+ "keywords": [
100
+ "linux",
101
+ "monitoring",
102
+ "system-monitor",
103
+ "process-monitor",
104
+ "self-hosted",
105
+ "gpu"
106
+ ],
107
+ "publishConfig": {
108
+ "access": "public",
109
+ "registry": "https://registry.npmjs.org/"
110
+ }
111
+ }
@@ -0,0 +1,5 @@
1
+ # Packaging reference files
2
+
3
+ The `.env.example` files contain public examples, never deployment credentials. Copy configuration to private files (0600) outside the source tree and generate fresh credentials. No configuration starts a production deployment by itself.
4
+
5
+ Use [installation](../docs/installation.md) for managed user/system installs, [operations](../docs/operations.md) for manifest archives and recovery, and [releasing](../docs/releasing.md) for package validation. System reference units are in [system](system/README.md). Generated units in the installer source are authoritative.
@@ -0,0 +1,24 @@
1
+ # Collector environment example
2
+ #
3
+ # Copy what you need into the private collector.env created by the installer
4
+ # (mode 0600). The generated file starts with every value commented out: with
5
+ # no destinations the collector fails closed and delivers nothing.
6
+ #
7
+ # PUBLIC DEMO VALUES ONLY. demo-token-local is a deliberately public local test
8
+ # string, not a credential for any real deployment.
9
+
10
+ # COLLECTOR_MODE=real
11
+ # COLLECTOR_STATE_FILE=/home/operator/.local/state/rigscope/collector/collector.json
12
+ # COLLECTOR_INTERVAL_MS=1000
13
+ # COLLECTOR_HOSTNAME=rig-01
14
+
15
+ # Enroll once against the hub, then remove the one-use grant from the hub.
16
+ # COLLECTOR_ENROLL_URL=ws://127.0.0.1:8081/api/collector/connect
17
+ # COLLECTOR_ENROLL_TOKEN=<one-use grant from the hub>
18
+
19
+ # Or supply destinations explicitly (enrollment writes these into private state
20
+ # for real installations). Full websocket URL including the connect path:
21
+ # COLLECTOR_HUBS=[{"url":"ws://127.0.0.1:8081/api/collector/connect","token":"demo-token-local"}]
22
+
23
+ # Per-destination sharing policy can withhold sensitive families. Defaults are
24
+ # CPU/memory only; process details remain request-scoped and are never spooled.
@@ -0,0 +1,31 @@
1
+ # Hub environment example
2
+ #
3
+ # Copy the values you need into the private hub.env created by the installer
4
+ # (mode 0600), not into a repository file. The generated hub.env starts with
5
+ # every value commented out: no browser users and no collector credentials
6
+ # means protected browser data fails closed.
7
+ #
8
+ # PUBLIC DEMO VALUES ONLY below. Never use demo-token-local for a real host,
9
+ # never reuse a demo browser password, and never put credentials in URLs.
10
+
11
+ # Loopback-only development listener.
12
+ # HUB_HOST=127.0.0.1
13
+ # HUB_PORT=8081
14
+
15
+ # Remote mode is selected only when both are set. The socket must live under a
16
+ # private directory (the installer's state directory is writable by the unit).
17
+ # HUB_PUBLIC_ORIGIN=https://monitor.example.invalid
18
+ # HUB_SOCKET_PATH=/home/operator/.local/state/rigscope/run/hub.sock
19
+
20
+ # Browser access fails closed without this. Generate the real hash locally with
21
+ # scripts/hash-browser-password.cjs; never paste a plaintext password here.
22
+ # HUB_BROWSER_USERS=[{"username":"operator","passwordHash":"<private hash>","machineIds":"*"}]
23
+
24
+ # Static collector credentials (public demo shape/token).
25
+ # HUB_COLLECTORS=[{"collectorId":"demo-collector","machineId":"demo-machine","token":"demo-token-local"}]
26
+
27
+ # Enrollment/registry state. The installer sets HUB_DATABASE_PATH in the unit;
28
+ # keep enrollment state outside the repository, and HUB_STATE_FILE also serves
29
+ # as the recovery import source for previously enrolled collectors.
30
+ # HUB_STATE_FILE=/home/operator/.local/state/rigscope/hub/enrollment.json
31
+ # HUB_ENROLLMENT_GRANTS=[{"token":"<one-use grant>","expiresAt":"<ISO timestamp>"}]
@@ -0,0 +1,7 @@
1
+ # Managed by Rigscope: read-only package energy/range counters for the collector group.
2
+ # Top-level package devices only; subdomains and constraint files are not touched.
3
+ SUBSYSTEM=="powercap", ACTION=="add|change", KERNEL=="intel-rapl:[0-9]*", KERNEL!="intel-rapl:*:*", ATTR{name}=="package*", \
4
+ RUN+="/usr/bin/chgrp rigscope-rapl /sys%p/energy_uj", \
5
+ RUN+="/usr/bin/chmod 0440 /sys%p/energy_uj", \
6
+ RUN+="/usr/bin/chgrp rigscope-rapl /sys%p/max_energy_range_uj", \
7
+ RUN+="/usr/bin/chmod 0440 /sys%p/max_energy_range_uj"
@@ -0,0 +1,5 @@
1
+ # System installation references
2
+
3
+ These files illustrate dedicated `rigscope-hub` and `rigscope-collector` accounts, systemd units and read-only package-energy access. The renderer in `src/cli/system.ts` is authoritative; do not install examples blindly.
4
+
5
+ See [system installation](../../docs/installation.md#system-installation) for planning, `--apply`, independent component controls and RAPL permission limits. The collector needs GPU device access and does not use `PrivateDevices=yes`; the hub does. Both run without root privileges, with private state and bounded writable paths.
@@ -0,0 +1,53 @@
1
+ # Managed by `rigscope system`. Enable explicitly after enrollment.
2
+ # Independent component: enabling or restarting this unit never touches the hub.
3
+ # The collector is nologin and has no capability grants.
4
+ # Package power reads use the private group rule installed separately.
5
+ # Node receives no capabilities, no root, and no device-control access.
6
+ [Unit]
7
+ Description=Rigscope collector (system scope)
8
+ Documentation=file:/opt/rigscope/current/README.md
9
+ After=network-online.target
10
+ Wants=network-online.target
11
+ StartLimitIntervalSec=60
12
+ StartLimitBurst=5
13
+
14
+ [Service]
15
+ Type=simple
16
+ User=rigscope-collector
17
+ Group=rigscope-collector
18
+ WorkingDirectory=/opt/rigscope/current
19
+ EnvironmentFile=/etc/rigscope/collector.env
20
+ Environment=NODE_ENV=production
21
+ Restart=on-failure
22
+ RestartSec=2s
23
+ KillSignal=SIGTERM
24
+ UMask=0077
25
+ NoNewPrivileges=yes
26
+ PrivateTmp=yes
27
+ ProtectSystem=strict
28
+ ProtectHome=yes
29
+ ProtectKernelTunables=yes
30
+ ProtectKernelModules=yes
31
+ ProtectKernelLogs=yes
32
+ ProtectControlGroups=yes
33
+ RestrictRealtime=yes
34
+ RestrictSUIDSGID=yes
35
+ RestrictNamespaces=yes
36
+ LockPersonality=yes
37
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
38
+ CapabilityBoundingSet=
39
+ AmbientCapabilities=
40
+ ExecStart="/opt/rigscope/node/node" "/opt/rigscope/current/dist/collector.cjs" --await-connection
41
+ SupplementaryGroups=rigscope-rapl
42
+ Environment="COLLECTOR_MODE=real"
43
+ Environment="COLLECTOR_STATE_FILE=/var/lib/rigscope/collector/state.json"
44
+ Environment="COLLECTOR_HEALTH_FILE=/var/lib/rigscope/collector/health.json"
45
+ ReadWritePaths="/var/lib/rigscope/collector"
46
+ ReadOnlyPaths="/opt/rigscope/current"
47
+ TimeoutStopSec=10s
48
+ StandardOutput=journal
49
+ StandardError=journal
50
+ SyslogIdentifier=rigscope-collector
51
+
52
+ [Install]
53
+ WantedBy=multi-user.target
@@ -0,0 +1,50 @@
1
+ # Managed by `rigscope system`. Enable explicitly; do not edit in place.
2
+ # Independent component: enabling or restarting this unit never touches the collector.
3
+ [Unit]
4
+ Description=Rigscope hub (system scope)
5
+ Documentation=file:/opt/rigscope/current/README.md
6
+ After=network-online.target
7
+ Wants=network-online.target
8
+ StartLimitIntervalSec=60
9
+ StartLimitBurst=5
10
+
11
+ [Service]
12
+ Type=simple
13
+ User=rigscope-hub
14
+ Group=rigscope-hub
15
+ WorkingDirectory=/opt/rigscope/current
16
+ EnvironmentFile=/etc/rigscope/hub.env
17
+ Environment=NODE_ENV=production
18
+ Restart=on-failure
19
+ RestartSec=2s
20
+ KillSignal=SIGTERM
21
+ UMask=0077
22
+ NoNewPrivileges=yes
23
+ PrivateTmp=yes
24
+ ProtectSystem=strict
25
+ ProtectHome=yes
26
+ ProtectKernelTunables=yes
27
+ ProtectKernelModules=yes
28
+ ProtectKernelLogs=yes
29
+ ProtectControlGroups=yes
30
+ RestrictRealtime=yes
31
+ RestrictSUIDSGID=yes
32
+ RestrictNamespaces=yes
33
+ LockPersonality=yes
34
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
35
+ CapabilityBoundingSet=
36
+ AmbientCapabilities=
37
+ ExecStart="/opt/rigscope/node/node" "/opt/rigscope/current/dist/hub.cjs"
38
+ Environment="HUB_DATABASE_PATH=/var/lib/rigscope/hub/telemetry.sqlite"
39
+ Environment="HUB_STATE_FILE=/var/lib/rigscope/hub/enrollment.json"
40
+ Environment="HUB_SOCKET_PATH=/var/lib/rigscope/hub/run/hub.sock"
41
+ ReadWritePaths="/var/lib/rigscope/hub"
42
+ ReadOnlyPaths="/opt/rigscope/current"
43
+ PrivateDevices=yes
44
+ TimeoutStopSec=15s
45
+ StandardOutput=journal
46
+ StandardError=journal
47
+ SyslogIdentifier=rigscope-hub
48
+
49
+ [Install]
50
+ WantedBy=multi-user.target
@@ -0,0 +1,5 @@
1
+ # Reference only. Generated by `rigscope system install --apply`
2
+ # or `rigscope system power grant --apply`.
3
+ # Only the collector account joins the narrow power group.
4
+ g rigscope-rapl - -
5
+ m rigscope-collector rigscope-rapl
@@ -0,0 +1,7 @@
1
+ # Reference only. Generated by `rigscope system install --apply`.
2
+ # One complete package: both dedicated, unprivileged, non-login accounts exist
3
+ # even when one component is disabled, so it can be enabled later.
4
+ g rigscope-hub - -
5
+ u rigscope-hub - "Rigscope hub" /nonexistent /usr/sbin/nologin
6
+ g rigscope-collector - -
7
+ u rigscope-collector - "Rigscope collector" /nonexistent /usr/sbin/nologin