railwatch 0.9.3 → 1.0.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 (454) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +66 -0
  4. data/README.md +51 -60
  5. data/config/routes.rb +0 -59
  6. data/docs/ai-and-mcp.md +9 -8
  7. data/docs/configuration.md +57 -71
  8. data/docs/faq.md +17 -25
  9. data/docs/getting-started.md +105 -117
  10. data/docs/replacing-nightwatch.md +8 -7
  11. data/docs/replacing-sentry.md +12 -16
  12. data/docs/security.md +14 -20
  13. data/docs/self-hosting.md +24 -22
  14. data/docs/testing.md +6 -14
  15. data/docs/troubleshooting.md +12 -46
  16. data/lib/generators/railwatch/install/install_generator.rb +12 -240
  17. data/lib/generators/railwatch/install/templates/initializer.rb.tt +10 -40
  18. data/lib/generators/railwatch/install/templates/post-deploy +0 -7
  19. data/lib/railwatch/authentication.rb +6 -5
  20. data/lib/railwatch/buffer.rb +9 -0
  21. data/lib/railwatch/configuration.rb +21 -151
  22. data/lib/railwatch/engine.rb +9 -118
  23. data/lib/railwatch/patches.rb +0 -11
  24. data/lib/railwatch/reporter.rb +92 -21
  25. data/lib/railwatch/sampler.rb +10 -2
  26. data/lib/railwatch/subscribers/process_info.rb +1 -2
  27. data/lib/railwatch/version.rb +1 -1
  28. data/lib/railwatch.rb +8 -64
  29. data/lib/tasks/railwatch_tasks.rake +3 -325
  30. data/llms.txt +23 -25
  31. metadata +3 -453
  32. data/app/channels/railwatch/environment_channel.rb +0 -62
  33. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  34. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  35. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  36. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  37. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  38. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  39. data/app/controllers/railwatch/commands_controller.rb +0 -16
  40. data/app/controllers/railwatch/comments_controller.rb +0 -11
  41. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  42. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  43. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  44. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  45. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  46. data/app/controllers/railwatch/executions_controller.rb +0 -13
  47. data/app/controllers/railwatch/issues_controller.rb +0 -258
  48. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  49. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  50. data/app/controllers/railwatch/logs_controller.rb +0 -40
  51. data/app/controllers/railwatch/mails_controller.rb +0 -19
  52. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  53. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  54. data/app/controllers/railwatch/overview_controller.rb +0 -38
  55. data/app/controllers/railwatch/people_controller.rb +0 -25
  56. data/app/controllers/railwatch/processes_controller.rb +0 -33
  57. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  58. data/app/controllers/railwatch/queries_controller.rb +0 -59
  59. data/app/controllers/railwatch/releases_controller.rb +0 -75
  60. data/app/controllers/railwatch/requests_controller.rb +0 -37
  61. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  62. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  63. data/app/controllers/railwatch/spans_controller.rb +0 -47
  64. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  65. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  66. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  67. data/app/controllers/railwatch/traces_controller.rb +0 -72
  68. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  69. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  70. data/app/controllers/railwatch/visits_controller.rb +0 -53
  71. data/app/frontend/components/app-content.tsx +0 -26
  72. data/app/frontend/components/app-logo-icon.tsx +0 -23
  73. data/app/frontend/components/app-logo.tsx +0 -14
  74. data/app/frontend/components/app-shell.tsx +0 -38
  75. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  76. data/app/frontend/components/app-sidebar.tsx +0 -94
  77. data/app/frontend/components/app-wordmark.tsx +0 -29
  78. data/app/frontend/components/breadcrumbs.tsx +0 -46
  79. data/app/frontend/components/icon.tsx +0 -16
  80. data/app/frontend/components/input-error.tsx +0 -18
  81. data/app/frontend/components/nav-footer.tsx +0 -46
  82. data/app/frontend/components/nav-main.tsx +0 -41
  83. data/app/frontend/components/nav-user.tsx +0 -50
  84. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  85. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  86. data/app/frontend/components/railwatch/code.tsx +0 -28
  87. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  88. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  89. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  90. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  91. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  92. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  93. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  94. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  95. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  96. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  97. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  98. data/app/frontend/components/railwatch/frames.tsx +0 -295
  99. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  100. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  101. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  102. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  103. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  104. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  105. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  106. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  107. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  108. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  109. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  110. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  111. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  112. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  113. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  114. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  115. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  116. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  117. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  118. data/app/frontend/components/railwatch/stat.tsx +0 -185
  119. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  120. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  121. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  122. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  123. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  124. data/app/frontend/components/ui/avatar.tsx +0 -51
  125. data/app/frontend/components/ui/badge.tsx +0 -46
  126. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  127. data/app/frontend/components/ui/button.tsx +0 -60
  128. data/app/frontend/components/ui/card.tsx +0 -92
  129. data/app/frontend/components/ui/chart.tsx +0 -374
  130. data/app/frontend/components/ui/checkbox.tsx +0 -32
  131. data/app/frontend/components/ui/collapsible.tsx +0 -31
  132. data/app/frontend/components/ui/command.tsx +0 -182
  133. data/app/frontend/components/ui/dialog.tsx +0 -158
  134. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  135. data/app/frontend/components/ui/hover-card.tsx +0 -42
  136. data/app/frontend/components/ui/input.tsx +0 -21
  137. data/app/frontend/components/ui/label.tsx +0 -24
  138. data/app/frontend/components/ui/popover.tsx +0 -87
  139. data/app/frontend/components/ui/select.tsx +0 -185
  140. data/app/frontend/components/ui/separator.tsx +0 -28
  141. data/app/frontend/components/ui/sheet.tsx +0 -137
  142. data/app/frontend/components/ui/sidebar.tsx +0 -726
  143. data/app/frontend/components/ui/skeleton.tsx +0 -13
  144. data/app/frontend/components/ui/sonner.tsx +0 -41
  145. data/app/frontend/components/ui/table.tsx +0 -114
  146. data/app/frontend/components/ui/tabs.tsx +0 -91
  147. data/app/frontend/components/ui/textarea.tsx +0 -18
  148. data/app/frontend/components/ui/tooltip.tsx +0 -61
  149. data/app/frontend/components/user-info.tsx +0 -32
  150. data/app/frontend/components/user-menu-content.tsx +0 -94
  151. data/app/frontend/entrypoints/application.css +0 -244
  152. data/app/frontend/entrypoints/inertia.tsx +0 -92
  153. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  154. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  157. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  158. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  159. data/app/frontend/hooks/use-appearance.tsx +0 -67
  160. data/app/frontend/hooks/use-clipboard.ts +0 -32
  161. data/app/frontend/hooks/use-flash.tsx +0 -23
  162. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  163. data/app/frontend/hooks/use-initials.tsx +0 -15
  164. data/app/frontend/hooks/use-live.ts +0 -205
  165. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  166. data/app/frontend/hooks/use-mobile.ts +0 -21
  167. data/app/frontend/hooks/use-percentile.ts +0 -24
  168. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  169. data/app/frontend/hooks/use-row-nav.ts +0 -106
  170. data/app/frontend/hooks/use-step.ts +0 -27
  171. data/app/frontend/hooks/use-ticker.ts +0 -12
  172. data/app/frontend/hooks/use-window.ts +0 -58
  173. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  174. data/app/frontend/layouts/app-layout.tsx +0 -21
  175. data/app/frontend/layouts/env-layout.tsx +0 -53
  176. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  177. data/app/frontend/lib/cable.ts +0 -10
  178. data/app/frontend/lib/env-pages.ts +0 -236
  179. data/app/frontend/lib/execution-path.ts +0 -55
  180. data/app/frontend/lib/explain.ts +0 -50
  181. data/app/frontend/lib/filter.ts +0 -25
  182. data/app/frontend/lib/flamegraph.ts +0 -155
  183. data/app/frontend/lib/format.ts +0 -71
  184. data/app/frontend/lib/keyboard.ts +0 -18
  185. data/app/frontend/lib/mount.ts +0 -23
  186. data/app/frontend/lib/source-link.ts +0 -217
  187. data/app/frontend/lib/utils.ts +0 -6
  188. data/app/frontend/pages/alerts/index.tsx +0 -171
  189. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  190. data/app/frontend/pages/cache_events/index.tsx +0 -127
  191. data/app/frontend/pages/commands/index.tsx +0 -143
  192. data/app/frontend/pages/deploys/index.tsx +0 -94
  193. data/app/frontend/pages/deploys/show.tsx +0 -309
  194. data/app/frontend/pages/deprecations/index.tsx +0 -176
  195. data/app/frontend/pages/exceptions/index.tsx +0 -271
  196. data/app/frontend/pages/executions/show.tsx +0 -821
  197. data/app/frontend/pages/issues/index.tsx +0 -399
  198. data/app/frontend/pages/issues/show.tsx +0 -1367
  199. data/app/frontend/pages/jobs/index.tsx +0 -381
  200. data/app/frontend/pages/jobs/klass.tsx +0 -215
  201. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  202. data/app/frontend/pages/logs/index.tsx +0 -303
  203. data/app/frontend/pages/mails/index.tsx +0 -218
  204. data/app/frontend/pages/notifications/index.tsx +0 -161
  205. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  206. data/app/frontend/pages/overview/show.tsx +0 -333
  207. data/app/frontend/pages/people/index.tsx +0 -102
  208. data/app/frontend/pages/people/show.tsx +0 -149
  209. data/app/frontend/pages/processes/index.tsx +0 -335
  210. data/app/frontend/pages/profiles/index.tsx +0 -110
  211. data/app/frontend/pages/profiles/show.tsx +0 -105
  212. data/app/frontend/pages/queries/index.tsx +0 -492
  213. data/app/frontend/pages/queries/show.tsx +0 -276
  214. data/app/frontend/pages/releases/index.tsx +0 -190
  215. data/app/frontend/pages/releases/release-health.tsx +0 -68
  216. data/app/frontend/pages/releases/release-path.ts +0 -25
  217. data/app/frontend/pages/releases/show.tsx +0 -262
  218. data/app/frontend/pages/requests/index.tsx +0 -235
  219. data/app/frontend/pages/requests/route.tsx +0 -159
  220. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  221. data/app/frontend/pages/spans/index.tsx +0 -229
  222. data/app/frontend/pages/spans/show.tsx +0 -220
  223. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  224. data/app/frontend/pages/tenants/index.tsx +0 -236
  225. data/app/frontend/pages/tenants/show.tsx +0 -477
  226. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  227. data/app/frontend/pages/thresholds/index.tsx +0 -414
  228. data/app/frontend/pages/traces/show.tsx +0 -208
  229. data/app/frontend/pages/transactions/index.tsx +0 -220
  230. data/app/frontend/pages/view_renders/index.tsx +0 -241
  231. data/app/frontend/pages/visits/index.tsx +0 -321
  232. data/app/frontend/public/icon.png +0 -0
  233. data/app/frontend/public/icon.svg +0 -5
  234. data/app/frontend/routes/index.d.ts +0 -2136
  235. data/app/frontend/routes/index.js +0 -2112
  236. data/app/frontend/types/globals.d.ts +0 -9
  237. data/app/frontend/types/index.ts +0 -288
  238. data/app/frontend/types/vite-env.d.ts +0 -9
  239. data/app/helpers/railwatch/assets_helper.rb +0 -52
  240. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  241. data/app/jobs/railwatch/application_job.rb +0 -7
  242. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  243. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  244. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  245. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  246. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  247. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  248. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  249. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  250. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  251. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  252. data/app/jobs/railwatch/rollup_job.rb +0 -132
  253. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  254. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  255. data/app/models/railwatch/alert.rb +0 -229
  256. data/app/models/railwatch/alert_rule.rb +0 -121
  257. data/app/models/railwatch/anomaly_rule.rb +0 -51
  258. data/app/models/railwatch/application.rb +0 -44
  259. data/app/models/railwatch/application_record.rb +0 -33
  260. data/app/models/railwatch/comment.rb +0 -36
  261. data/app/models/railwatch/deploy.rb +0 -67
  262. data/app/models/railwatch/environment.rb +0 -68
  263. data/app/models/railwatch/execution_presenter.rb +0 -185
  264. data/app/models/railwatch/filter_query.rb +0 -184
  265. data/app/models/railwatch/followup_receipt.rb +0 -27
  266. data/app/models/railwatch/ingest/batch.rb +0 -355
  267. data/app/models/railwatch/ingest/mapper.rb +0 -585
  268. data/app/models/railwatch/ingest/payload.rb +0 -96
  269. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  270. data/app/models/railwatch/ingest/writer.rb +0 -137
  271. data/app/models/railwatch/issue.rb +0 -277
  272. data/app/models/railwatch/issue_activity.rb +0 -23
  273. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  274. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  275. data/app/models/railwatch/maintenance_task.rb +0 -53
  276. data/app/models/railwatch/saved_view.rb +0 -63
  277. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  278. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  279. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  280. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  281. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  282. data/app/models/railwatch/telemetry/child.rb +0 -53
  283. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  284. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  285. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  286. data/app/models/railwatch/telemetry/exception.rb +0 -28
  287. data/app/models/railwatch/telemetry/execution.rb +0 -152
  288. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  289. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  290. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  291. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  292. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  293. data/app/models/railwatch/telemetry/log.rb +0 -91
  294. data/app/models/railwatch/telemetry/mail.rb +0 -13
  295. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  296. data/app/models/railwatch/telemetry/notification.rb +0 -13
  297. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  298. data/app/models/railwatch/telemetry/person.rb +0 -50
  299. data/app/models/railwatch/telemetry/process.rb +0 -10
  300. data/app/models/railwatch/telemetry/profile.rb +0 -37
  301. data/app/models/railwatch/telemetry/query.rb +0 -57
  302. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  303. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  304. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  305. data/app/models/railwatch/telemetry/session.rb +0 -24
  306. data/app/models/railwatch/telemetry/span.rb +0 -19
  307. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  308. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  309. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  310. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  311. data/app/models/railwatch/telemetry/visit.rb +0 -24
  312. data/app/models/railwatch/telemetry_record.rb +0 -107
  313. data/app/models/railwatch/threshold.rb +0 -66
  314. data/app/models/railwatch/user.rb +0 -47
  315. data/app/models/railwatch/viewer.rb +0 -13
  316. data/app/models/railwatch/window.rb +0 -66
  317. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  318. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  319. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  320. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  321. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  322. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  323. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  324. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  325. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  327. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  328. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  329. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  330. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  331. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  332. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  333. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  334. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  335. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  336. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  337. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  338. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  340. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  341. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  342. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  343. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  344. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  345. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  347. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  348. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  349. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  350. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  351. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  352. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  353. data/docs/embedded.md +0 -503
  354. data/lib/puma/plugin/railwatch.rb +0 -215
  355. data/lib/railwatch/dashboard_assets.rb +0 -45
  356. data/lib/railwatch/embedded.rb +0 -50
  357. data/lib/railwatch/export/client.rb +0 -124
  358. data/lib/railwatch/export/lease.rb +0 -56
  359. data/lib/railwatch/export/outbox.rb +0 -361
  360. data/lib/railwatch/export/policy.rb +0 -79
  361. data/lib/railwatch/export/sender.rb +0 -130
  362. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  363. data/lib/railwatch/maintenance.rb +0 -194
  364. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  365. data/lib/railwatch/transport/local.rb +0 -78
  366. data/lib/railwatch/transport/socket.rb +0 -183
  367. data/lib/railwatch/writer.rb +0 -370
  368. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  369. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  372. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  373. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  374. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  375. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  376. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  377. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  378. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  379. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  380. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  381. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  382. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  383. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  384. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  385. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  386. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  387. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  388. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  389. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  390. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  391. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  392. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  393. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  394. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  395. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  396. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  397. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  398. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  399. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  400. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  401. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  402. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  403. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  404. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  405. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  406. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  407. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  408. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  409. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  410. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  411. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  412. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  413. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  414. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  415. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  416. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  417. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  418. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  419. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  420. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  421. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  422. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  423. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  424. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  425. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  426. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  427. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  428. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  429. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  430. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  431. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  432. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  433. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  434. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  435. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  436. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  437. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  438. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  439. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  440. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  441. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  442. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  443. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  444. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  445. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  446. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  447. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  448. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  449. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  450. data/public/railwatch/icon.png +0 -0
  451. data/public/railwatch/icon.svg +0 -5
  452. data/public/railwatch/manifest.json +0 -1245
  453. data/public/railwatch/rails-vite.json +0 -1
  454. /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
@@ -1,170 +0,0 @@
1
- import type { ReactNode } from "react"
2
-
3
- import {
4
- LatencyChart,
5
- ThroughputChart,
6
- } from "@/components/railwatch/series-chart"
7
- import { count, ms } from "@/lib/format"
8
- import { cn } from "@/lib/utils"
9
- import type { DeployMarker, Percentile, SeriesPoint } from "@/types"
10
-
11
- // Nightwatch's chart card: a mono label with the headline number at the
12
- // left, a colour-keyed legend with per-series totals at the right, the
13
- // chart below, and the bucket range under it (drawn by the chart itself).
14
- export function ChartPanel({
15
- label,
16
- value,
17
- legend,
18
- children,
19
- className,
20
- }: {
21
- label: string
22
- value: ReactNode
23
- legend?: { key: string; label: string; value: ReactNode; color: string }[]
24
- children: ReactNode
25
- className?: string
26
- }) {
27
- return (
28
- <div className={cn("bg-card rounded-lg border p-4", className)}>
29
- <div className="mb-3 flex items-start justify-between gap-4">
30
- <div className="min-w-0">
31
- <div className="label-caps">{label}</div>
32
- <div className="mt-0.5 text-xl font-semibold tracking-tight tabular-nums">
33
- {value}
34
- </div>
35
- </div>
36
- {legend && (
37
- <div className="flex shrink-0 gap-4">
38
- {legend.map((l) => (
39
- <div key={l.key} className="text-right">
40
- <div className="label-caps flex items-center justify-end gap-1.5">
41
- <span
42
- className="inline-block h-2.5 w-1 rounded-sm"
43
- style={{ background: l.color }}
44
- />
45
- {l.label}
46
- </div>
47
- <div className="mt-0.5 font-mono text-sm tabular-nums">
48
- {l.value}
49
- </div>
50
- </div>
51
- ))}
52
- </div>
53
- )}
54
- </div>
55
- {children}
56
- </div>
57
- )
58
- }
59
-
60
- function sum(data: SeriesPoint[], key: "count" | "errors" | "client_errors") {
61
- return data.reduce((n, d) => n + d[key], 0)
62
- }
63
-
64
- // What the three stacked series mean for each record type. Rollups store
65
- // them as count / client_errors / errors; the words differ per page.
66
- export const VOLUME_LEGENDS = {
67
- request: { ok: "1/2/3xx", warn: "4xx", error: "5xx" },
68
- job: { ok: "processed", warn: "released", error: "failed" },
69
- outcome: { ok: "ok", warn: null, error: "failed" },
70
- exception: { ok: "handled", warn: null, error: "unhandled" },
71
- cache: { ok: "hit", warn: "miss", error: "write" },
72
- } as const
73
-
74
- export type VolumeLegend = keyof typeof VOLUME_LEGENDS
75
-
76
- // Volume panel: total as the headline, and a colour-keyed legend naming the
77
- // ok / warning / error slices in the record type's own words.
78
- export function VolumePanel({
79
- data,
80
- deploys,
81
- label = "Requests",
82
- seriesLabel,
83
- legend = "request",
84
- className,
85
- }: {
86
- data: SeriesPoint[]
87
- deploys?: DeployMarker[]
88
- label?: string
89
- seriesLabel?: string
90
- legend?: VolumeLegend
91
- className?: string
92
- }) {
93
- const total = sum(data, "count")
94
- const errors = sum(data, "errors")
95
- const client = sum(data, "client_errors")
96
- const words = VOLUME_LEGENDS[legend]
97
- const items = [
98
- {
99
- key: "ok",
100
- label: words.ok,
101
- value: count(total - errors - client),
102
- color: "var(--ok)",
103
- },
104
- words.warn && {
105
- key: "warn",
106
- label: words.warn,
107
- value: count(client),
108
- color: "var(--warning)",
109
- },
110
- {
111
- key: "error",
112
- label: words.error,
113
- value: count(errors),
114
- color: "var(--danger)",
115
- },
116
- ].filter((i): i is NonNullable<typeof i> => Boolean(i))
117
- return (
118
- <ChartPanel
119
- label={label}
120
- value={count(total)}
121
- className={className}
122
- legend={items}
123
- >
124
- <ThroughputChart data={data} deploys={deploys} label={seriesLabel} />
125
- </ChartPanel>
126
- )
127
- }
128
-
129
- // Duration panel: min — max range as the headline, avg and pNN legend.
130
- export function DurationPanel({
131
- data,
132
- deploys,
133
- percentile = "p95",
134
- label = "Duration",
135
- className,
136
- }: {
137
- data: SeriesPoint[]
138
- deploys?: DeployMarker[]
139
- percentile?: Percentile
140
- label?: string
141
- className?: string
142
- }) {
143
- const withData = data.filter((d) => d.count > 0)
144
- const avgs = withData.flatMap((d) => (d.avg == null ? [] : [d.avg]))
145
- const pcts = withData.flatMap((d) =>
146
- d[percentile] == null ? [] : [d[percentile]],
147
- )
148
- const avg = avgs.length ? avgs.reduce((a, b) => a + b, 0) / avgs.length : 0
149
- const pct = pcts.length ? Math.max(...pcts) : 0
150
- const lo = avgs.length ? Math.min(...avgs) : 0
151
- const hi = pcts.length ? Math.max(...pcts) : 0
152
- return (
153
- <ChartPanel
154
- label={label}
155
- value={withData.length ? `${ms(lo)} — ${ms(hi)}` : "—"}
156
- className={className}
157
- legend={[
158
- { key: "avg", label: "avg", value: ms(avg), color: "var(--warning)" },
159
- {
160
- key: percentile,
161
- label: percentile,
162
- value: ms(pct),
163
- color: "var(--primary)",
164
- },
165
- ]}
166
- >
167
- <LatencyChart data={data} deploys={deploys} percentile={percentile} />
168
- </ChartPanel>
169
- )
170
- }
@@ -1,28 +0,0 @@
1
- import { cn } from "@/lib/utils"
2
-
3
- export function Sql({
4
- children,
5
- className,
6
- }: {
7
- children: string
8
- className?: string
9
- }) {
10
- return (
11
- <code
12
- className={cn("block max-w-full truncate font-mono text-xs", className)}
13
- title={children}
14
- >
15
- {children}
16
- </code>
17
- )
18
- }
19
-
20
- export function Mono({
21
- children,
22
- className,
23
- }: {
24
- children: React.ReactNode
25
- className?: string
26
- }) {
27
- return <span className={cn("font-mono text-xs", className)}>{children}</span>
28
- }
@@ -1,132 +0,0 @@
1
- import { router, usePage } from "@inertiajs/react"
2
- import { AlertOctagon, LayoutGrid, Plug, SearchIcon, Users } from "lucide-react"
3
- import { useState } from "react"
4
-
5
- import {
6
- CommandDialog,
7
- CommandEmpty,
8
- CommandGroup,
9
- CommandInput,
10
- CommandItem,
11
- CommandList,
12
- CommandSeparator,
13
- } from "@/components/ui/command"
14
- import { envPages } from "@/lib/env-pages"
15
- import {
16
- applicationEnvironmentOverviewPath,
17
- dashboardPath,
18
- issuesPath,
19
- settingsIntegrationsPath,
20
- settingsMembersPath,
21
- } from "@/routes"
22
- import type { NavItem, SharedProps } from "@/types"
23
-
24
- const staticNav = (embedded: boolean): NavItem[] => [
25
- ...(embedded
26
- ? []
27
- : [{ title: "Dashboard", href: dashboardPath(), icon: LayoutGrid }]),
28
- { title: "Issues", href: issuesPath(), icon: AlertOctagon },
29
- ...(embedded
30
- ? []
31
- : [
32
- { title: "Integrations", href: settingsIntegrationsPath(), icon: Plug },
33
- { title: "Members", href: settingsMembersPath(), icon: Users },
34
- ]),
35
- ]
36
-
37
- export function CommandPalette({
38
- open,
39
- onOpenChange,
40
- }: {
41
- open: boolean
42
- onOpenChange: (open: boolean) => void
43
- }) {
44
- const [query, setQuery] = useState("")
45
- const {
46
- applications,
47
- environment,
48
- embedded,
49
- window: currentWindow,
50
- } = usePage<SharedProps>().props
51
-
52
- const go = (href: string) => {
53
- onOpenChange(false)
54
- router.visit(href)
55
- }
56
-
57
- const envItems = environment
58
- ? envPages(
59
- environment.application_id,
60
- environment.id,
61
- currentWindow ?? "24h",
62
- )
63
- : []
64
-
65
- return (
66
- <CommandDialog open={open} onOpenChange={onOpenChange}>
67
- <CommandInput
68
- placeholder="Search pages, applications, issues…"
69
- value={query}
70
- onValueChange={setQuery}
71
- />
72
- <CommandList>
73
- <CommandEmpty>No results found.</CommandEmpty>
74
- <CommandGroup heading="Navigation">
75
- {staticNav(!!embedded).map((item) => (
76
- <CommandItem key={item.title} onSelect={() => go(item.href)}>
77
- {item.icon && <item.icon />}
78
- <span>{item.title}</span>
79
- </CommandItem>
80
- ))}
81
- </CommandGroup>
82
- {envItems.length > 0 && (
83
- <CommandGroup
84
- heading={`${environment!.application_name} · ${environment!.name}`}
85
- >
86
- {envItems.map((item) => (
87
- <CommandItem key={item.title} onSelect={() => go(item.href)}>
88
- {item.icon && <item.icon />}
89
- <span>{item.title}</span>
90
- </CommandItem>
91
- ))}
92
- </CommandGroup>
93
- )}
94
- {!embedded && (
95
- <CommandGroup heading="Applications">
96
- {applications.flatMap((app) =>
97
- app.environments.map((env) => (
98
- <CommandItem
99
- key={`${app.id}-${env.id}`}
100
- value={`${app.name} ${env.name}`}
101
- onSelect={() =>
102
- go(applicationEnvironmentOverviewPath(app.id, env.id))
103
- }
104
- >
105
- <span>
106
- {app.name} · {env.name}
107
- </span>
108
- </CommandItem>
109
- )),
110
- )}
111
- </CommandGroup>
112
- )}
113
- {query.trim() && (
114
- <>
115
- <CommandSeparator />
116
- <CommandGroup heading="Search">
117
- <CommandItem
118
- value={query}
119
- onSelect={() =>
120
- go(`${issuesPath()}?q=${encodeURIComponent(query)}`)
121
- }
122
- >
123
- <SearchIcon />
124
- <span>Search issues for &ldquo;{query}&rdquo;</span>
125
- </CommandItem>
126
- </CommandGroup>
127
- </>
128
- )}
129
- </CommandList>
130
- </CommandDialog>
131
- )
132
- }
@@ -1,52 +0,0 @@
1
- import { CheckIcon, CopyIcon } from "lucide-react"
2
- import { useState } from "react"
3
-
4
- import { Button } from "@/components/ui/button"
5
- import { useClipboard } from "@/hooks/use-clipboard"
6
- import { cn } from "@/lib/utils"
7
-
8
- const COPIED_RESET_MS = 1500
9
-
10
- // A code block with a copy button in its corner. Everything Railwatch asks a
11
- // developer to paste somewhere else — a Gemfile line, a token, an MCP client
12
- // config — goes through this, so "copy" behaves the same everywhere.
13
- export function CopyBlock({
14
- code,
15
- className,
16
- }: {
17
- code: string
18
- className?: string
19
- }) {
20
- const [, copy] = useClipboard()
21
- const [justCopied, setJustCopied] = useState(false)
22
-
23
- const handleCopy = () => {
24
- void copy(code).then((ok) => {
25
- if (!ok) return
26
- setJustCopied(true)
27
- window.setTimeout(() => setJustCopied(false), COPIED_RESET_MS)
28
- })
29
- }
30
-
31
- return (
32
- <div className={cn("relative", className)}>
33
- <pre className="bg-muted overflow-x-auto rounded p-3 pr-10 font-mono text-xs">
34
- {code}
35
- </pre>
36
- <Button
37
- type="button"
38
- aria-label={justCopied ? "Copied code" : "Copy code"}
39
- variant="ghost"
40
- size="icon"
41
- className="absolute top-1.5 right-1.5 size-6"
42
- onClick={handleCopy}
43
- >
44
- {justCopied ? (
45
- <CheckIcon className="size-3.5 text-emerald-500" />
46
- ) : (
47
- <CopyIcon className="size-3.5 opacity-60" />
48
- )}
49
- </Button>
50
- </div>
51
- )
52
- }
@@ -1,49 +0,0 @@
1
- import { CheckIcon, CopyIcon } from "lucide-react"
2
- import { useState } from "react"
3
-
4
- import { Button } from "@/components/ui/button"
5
- import { useClipboard } from "@/hooks/use-clipboard"
6
- import { cn } from "@/lib/utils"
7
-
8
- const COPIED_RESET_MS = 1500
9
-
10
- export function CopyId({
11
- value,
12
- label,
13
- className,
14
- }: {
15
- value: string
16
- label?: string
17
- className?: string
18
- }) {
19
- const [, copy] = useClipboard()
20
- const [justCopied, setJustCopied] = useState(false)
21
-
22
- const handleCopy = () => {
23
- void copy(value).then((ok) => {
24
- if (!ok) return
25
- setJustCopied(true)
26
- window.setTimeout(() => setJustCopied(false), COPIED_RESET_MS)
27
- })
28
- }
29
-
30
- return (
31
- <Button
32
- type="button"
33
- variant="ghost"
34
- size="sm"
35
- className={cn(
36
- "h-auto gap-1.5 px-1.5 py-0.5 font-mono text-xs",
37
- className,
38
- )}
39
- onClick={handleCopy}
40
- >
41
- {label ?? value}
42
- {justCopied ? (
43
- <CheckIcon className="size-3 text-emerald-500" />
44
- ) : (
45
- <CopyIcon className="size-3 opacity-60" />
46
- )}
47
- </Button>
48
- )
49
- }
@@ -1,39 +0,0 @@
1
- import { router } from "@inertiajs/react"
2
- import { useState } from "react"
3
-
4
- import { Button } from "@/components/ui/button"
5
- import type { CursorMeta } from "@/types"
6
-
7
- export function CursorLoadMore({
8
- meta,
9
- href,
10
- only,
11
- }: {
12
- meta: CursorMeta
13
- href: (cursor: string) => string
14
- only: string[]
15
- }) {
16
- const [loading, setLoading] = useState(false)
17
- if (!meta.has_more || !meta.next_cursor) return null
18
-
19
- return (
20
- <div className="flex justify-center pt-3">
21
- <Button
22
- type="button"
23
- variant="outline"
24
- disabled={loading}
25
- onClick={() =>
26
- router.visit(href(meta.next_cursor!), {
27
- only,
28
- preserveState: true,
29
- preserveScroll: true,
30
- onStart: () => setLoading(true),
31
- onFinish: () => setLoading(false),
32
- })
33
- }
34
- >
35
- {loading ? "Loading…" : "Load " + meta.limit + " more"}
36
- </Button>
37
- </div>
38
- )
39
- }
@@ -1,195 +0,0 @@
1
- import type { ReactNode } from "react"
2
- import { useEffect, useRef } from "react"
3
-
4
- import { useChartHover } from "@/components/railwatch/chart-hover"
5
- import { EmptyState } from "@/components/railwatch/empty-state"
6
- import {
7
- Table,
8
- TableBody,
9
- TableCell,
10
- TableHead,
11
- TableHeader,
12
- TableRow,
13
- } from "@/components/ui/table"
14
- import { useRowNav } from "@/hooks/use-row-nav"
15
- import { cn } from "@/lib/utils"
16
-
17
- const INTERACTIVE_TARGET =
18
- 'a, button, input, select, textarea, [role="button"], [role="link"], [contenteditable="true"]'
19
-
20
- function isInteractiveTarget(target: EventTarget | null) {
21
- return target instanceof Element && target.closest(INTERACTIVE_TARGET)
22
- }
23
-
24
- export interface Column<T> {
25
- key: string
26
- header: ReactNode
27
- cell: (row: T) => ReactNode
28
- className?: string
29
- align?: "left" | "right"
30
- // Hide this column below the md breakpoint. Phones get the columns that
31
- // identify the row and the one number that matters; the rest come back
32
- // on wider screens. Every page marks its own secondary columns.
33
- hideOnMobile?: boolean
34
- // The column that identifies the row (route, SQL, job class). It takes
35
- // whatever width the numeric columns leave over and truncates, so the
36
- // table fits its container instead of forcing a sideways scroll.
37
- grow?: boolean
38
- }
39
-
40
- export interface KeyboardNav<T> {
41
- onOpen: (row: T, opts?: { newTab?: boolean }) => void
42
- enabled?: boolean
43
- // Fires whenever j/k/Esc change the highlighted row, so a page can drive
44
- // other row-scoped shortcuts (e.g. issues' r/i/a) off the same state.
45
- onHighlightChange?: (row: T | null) => void
46
- }
47
-
48
- export function DataTable<T>({
49
- rows,
50
- columns,
51
- rowKey,
52
- empty = "Nothing here yet.",
53
- onRowClick,
54
- keyboardNav,
55
- hoverKey,
56
- rowClassName,
57
- className,
58
- }: {
59
- rows: T[]
60
- columns: Column<T>[]
61
- rowKey: (row: T) => string | number
62
- empty?: ReactNode
63
- onRowClick?: (row: T) => void
64
- keyboardNav?: KeyboardNav<T>
65
- // Per-row tint for a row whose own state matters (a crashed session), on
66
- // top of the highlight/hover states below.
67
- rowClassName?: (row: T) => string | undefined
68
- // Links rows to a chart via ChartHoverProvider: hovering a row sets this
69
- // key (e.g. the row's group_hash) as the shared hovered key, and rows
70
- // whose key matches an externally-hovered chart point get `data-hovered`.
71
- hoverKey?: (row: T) => string | undefined
72
- className?: string
73
- }) {
74
- const { highlighted, activate } = useRowNav({
75
- rows,
76
- rowKey,
77
- onOpen: keyboardNav?.onOpen ?? (() => undefined),
78
- enabled: Boolean(keyboardNav) && keyboardNav?.enabled !== false,
79
- })
80
-
81
- const rowRefs = useRef(new Map<string | number, HTMLTableRowElement>())
82
-
83
- useEffect(() => {
84
- if (highlighted == null) return
85
- rowRefs.current.get(highlighted)?.scrollIntoView({ block: "nearest" })
86
- }, [highlighted])
87
-
88
- useEffect(() => {
89
- if (!keyboardNav?.onHighlightChange) return
90
- const row = rows.find((r) => rowKey(r) === highlighted) ?? null
91
- keyboardNav.onHighlightChange(row)
92
- // eslint-disable-next-line react-hooks/exhaustive-deps
93
- }, [highlighted])
94
-
95
- const { hoveredKey, setHoveredKey } = useChartHover()
96
- const colClass = (c: Column<T>) =>
97
- cn(
98
- c.align === "right" && "text-right",
99
- c.hideOnMobile && "hidden md:table-cell",
100
- c.grow && "w-full max-w-0 truncate",
101
- c.className,
102
- )
103
-
104
- return (
105
- <div
106
- tabIndex={keyboardNav && keyboardNav.enabled !== false ? 0 : undefined}
107
- onFocusCapture={activate}
108
- onPointerDownCapture={activate}
109
- className={cn(
110
- "bg-card -mx-3 overflow-x-auto border-y md:mx-0 md:rounded-lg md:border",
111
- className,
112
- )}
113
- >
114
- <Table>
115
- <TableHeader>
116
- <TableRow className="hover:bg-transparent">
117
- {columns.map((c) => (
118
- <TableHead key={c.key} className={colClass(c)}>
119
- {c.header}
120
- </TableHead>
121
- ))}
122
- </TableRow>
123
- </TableHeader>
124
- <TableBody>
125
- {rows.length === 0 && (
126
- <TableRow>
127
- <TableCell
128
- colSpan={columns.length}
129
- className="p-0 whitespace-normal"
130
- >
131
- {typeof empty === "string" ? (
132
- <EmptyState title={empty} />
133
- ) : (
134
- <div className="text-muted-foreground min-h-24 text-center">
135
- {empty}
136
- </div>
137
- )}
138
- </TableCell>
139
- </TableRow>
140
- )}
141
- {rows.map((row) => {
142
- const key = rowKey(row)
143
- const isHighlighted = Boolean(keyboardNav) && highlighted === key
144
- const rowHoverKey = hoverKey?.(row)
145
- const isHovered = rowHoverKey != null && hoveredKey === rowHoverKey
146
- return (
147
- <TableRow
148
- key={key}
149
- ref={(el) => {
150
- if (el) rowRefs.current.set(key, el)
151
- else rowRefs.current.delete(key)
152
- }}
153
- data-state={isHighlighted ? "highlighted" : undefined}
154
- data-hovered={isHovered || undefined}
155
- className={cn(
156
- onRowClick && "cursor-pointer",
157
- rowClassName?.(row),
158
- isHighlighted &&
159
- "bg-accent shadow-[inset_2px_0_0_0_var(--primary)]",
160
- isHovered && !isHighlighted && "bg-accent/50",
161
- )}
162
- onClick={
163
- onRowClick
164
- ? (event) => {
165
- if (!isInteractiveTarget(event.target)) onRowClick(row)
166
- }
167
- : undefined
168
- }
169
- onMouseEnter={
170
- rowHoverKey ? () => setHoveredKey(rowHoverKey) : undefined
171
- }
172
- onMouseLeave={
173
- rowHoverKey ? () => setHoveredKey(null) : undefined
174
- }
175
- >
176
- {columns.map((c) => (
177
- <TableCell
178
- key={c.key}
179
- className={cn(
180
- "py-1.5 text-xs",
181
- c.align === "right" && "font-mono tabular-nums",
182
- colClass(c),
183
- )}
184
- >
185
- {c.cell(row)}
186
- </TableCell>
187
- ))}
188
- </TableRow>
189
- )
190
- })}
191
- </TableBody>
192
- </Table>
193
- </div>
194
- )
195
- }