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,112 +0,0 @@
1
- import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
2
-
3
- import {
4
- type ChartConfig,
5
- ChartContainer,
6
- ChartTooltip,
7
- ChartTooltipContent,
8
- } from "@/components/ui/chart"
9
-
10
- export interface MetricPoint {
11
- t: string
12
- value: number
13
- }
14
-
15
- const axisTick = { fontSize: 10, fontFamily: "var(--font-mono)" }
16
-
17
- // Time-of-day ticks up to two days, dates beyond, like series-chart.tsx.
18
- const DAY = 24 * 60 * 60 * 1000
19
-
20
- function tickFormatter(data: { t: string }[]) {
21
- const span =
22
- data.length > 1
23
- ? new Date(data[data.length - 1].t).getTime() -
24
- new Date(data[0].t).getTime()
25
- : 0
26
- if (span > 2 * DAY)
27
- return (t: string) =>
28
- new Date(t).toLocaleDateString(undefined, {
29
- month: "short",
30
- day: "numeric",
31
- })
32
- return (t: string) =>
33
- new Date(t).toLocaleTimeString(undefined, {
34
- hour: "2-digit",
35
- minute: "2-digit",
36
- })
37
- }
38
-
39
- function stamp(t: string) {
40
- return new Date(t).toLocaleString(undefined, {
41
- day: "2-digit",
42
- month: "short",
43
- hour: "2-digit",
44
- minute: "2-digit",
45
- })
46
- }
47
-
48
- // One plain {t, value} metric over time — health samples rather than
49
- // rollups, so there is no ok/4xx/5xx vocabulary to stack. Same axes,
50
- // gridlines and bucket-range caption as series-chart.tsx.
51
- export function MetricChart({
52
- label,
53
- data,
54
- format,
55
- color = "var(--primary)",
56
- className = "h-32",
57
- }: {
58
- label: string
59
- data: MetricPoint[]
60
- format: (value: number) => string
61
- color?: string
62
- className?: string
63
- }) {
64
- const config = { value: { label, color } } satisfies ChartConfig
65
- const tick = tickFormatter(data)
66
- return (
67
- <div>
68
- <ChartContainer config={config} className={`${className} w-full`}>
69
- <AreaChart data={data}>
70
- <CartesianGrid vertical={false} strokeDasharray="2 4" />
71
- <XAxis
72
- dataKey="t"
73
- tickFormatter={tick}
74
- tickLine={false}
75
- axisLine={false}
76
- minTickGap={40}
77
- tick={axisTick}
78
- />
79
- <YAxis
80
- width={44}
81
- tickLine={false}
82
- axisLine={false}
83
- tick={axisTick}
84
- tickFormatter={(v) => format(Number(v))}
85
- />
86
- <ChartTooltip
87
- content={
88
- <ChartTooltipContent
89
- labelFormatter={(v) => stamp(String(v))}
90
- formatter={(value) => [format(Number(value)), label]}
91
- />
92
- }
93
- />
94
- <Area
95
- dataKey="value"
96
- stroke="var(--color-value)"
97
- fill="var(--color-value)"
98
- fillOpacity={0.15}
99
- strokeWidth={1.75}
100
- isAnimationActive={false}
101
- />
102
- </AreaChart>
103
- </ChartContainer>
104
- {data.length > 0 && (
105
- <div className="text-muted-foreground mt-1 flex justify-between font-mono text-[10px]">
106
- <span>{stamp(data[0].t)}</span>
107
- <span>{stamp(data[data.length - 1].t)}</span>
108
- </div>
109
- )}
110
- </div>
111
- )
112
- }
@@ -1,89 +0,0 @@
1
- import { router, usePage } from "@inertiajs/react"
2
- import { Bookmark, ChevronDown } from "lucide-react"
3
-
4
- import { isViewActive, samePage } from "@/components/railwatch/saved-views"
5
- import {
6
- DropdownMenu,
7
- DropdownMenuContent,
8
- DropdownMenuItem,
9
- DropdownMenuLabel,
10
- DropdownMenuSeparator,
11
- DropdownMenuTrigger,
12
- } from "@/components/ui/dropdown-menu"
13
- import { ENV_PAGE_GROUPS, envPages } from "@/lib/env-pages"
14
- import { cn } from "@/lib/utils"
15
- import type { SharedProps } from "@/types"
16
-
17
- // Phone-only replacement for the sidebar's environment navigation: the
18
- // current page's name in the header opens a menu of every environment page.
19
- export function MobilePageSwitcher() {
20
- const page = usePage<SharedProps>()
21
- const { environment } = page.props
22
- if (!environment) return null
23
- const items = envPages(
24
- environment.application_id,
25
- environment.id,
26
- page.props.window as string,
27
- )
28
- const current = page.url.split("?")[0]
29
- const pinnedViews = (page.props.saved_views ?? []).filter((v) => v.pinned)
30
- // Overview's path is a prefix of every other page's, so prefer the
31
- // longest matching path rather than the first.
32
- const active =
33
- items
34
- .filter((i) => {
35
- const path = i.href.split("?")[0]
36
- return current === path || current.startsWith(path + "/")
37
- })
38
- .sort((a, b) => b.href.length - a.href.length)[0] ?? items[0]
39
- return (
40
- <DropdownMenu>
41
- <DropdownMenuTrigger className="flex min-w-0 items-center gap-1 text-sm font-medium">
42
- {active.icon && <active.icon className="size-4 shrink-0 opacity-70" />}
43
- <span className="truncate">{active.title}</span>
44
- <ChevronDown className="size-3.5 shrink-0 opacity-60" />
45
- </DropdownMenuTrigger>
46
- <DropdownMenuContent
47
- align="start"
48
- className="max-h-[70vh] w-60 overflow-y-auto"
49
- >
50
- {ENV_PAGE_GROUPS.map((group, gi) => (
51
- <div key={group.key}>
52
- {gi > 0 && <DropdownMenuSeparator />}
53
- <DropdownMenuLabel className="label-caps">
54
- {group.title}
55
- </DropdownMenuLabel>
56
- {items
57
- .filter((i) => i.group === group.key)
58
- .map((item) => (
59
- <div key={item.title}>
60
- <DropdownMenuItem
61
- onSelect={() => router.visit(item.href)}
62
- className={item === active ? "bg-accent" : undefined}
63
- >
64
- {item.icon && <item.icon className="size-4" />}
65
- {item.title}
66
- </DropdownMenuItem>
67
- {pinnedViews
68
- .filter((view) => samePage(view.url, item.href))
69
- .map((view) => (
70
- <DropdownMenuItem
71
- key={view.id}
72
- onSelect={() => router.visit(view.url)}
73
- className={cn(
74
- "pl-8 text-xs",
75
- isViewActive(page.url, view.url) && "bg-accent",
76
- )}
77
- >
78
- <Bookmark className="size-3.5 opacity-60" />
79
- {view.name}
80
- </DropdownMenuItem>
81
- ))}
82
- </div>
83
- ))}
84
- </div>
85
- ))}
86
- </DropdownMenuContent>
87
- </DropdownMenu>
88
- )
89
- }
@@ -1,91 +0,0 @@
1
- import { Link, router } from "@inertiajs/react"
2
- import { RefreshCwIcon } from "lucide-react"
3
-
4
- import { CopyBlock } from "@/components/railwatch/copy-block"
5
- import { LiveDot } from "@/components/railwatch/live-dot"
6
- import { Button } from "@/components/ui/button"
7
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
8
- import { settingsProfilePath } from "@/routes"
9
-
10
- // Shown on Overview and the Application show page while an environment has
11
- // no events yet (environment.last_seen_at is null): a step list to wire up
12
- // the gem plus a "waiting for first event..." status that live-checks via
13
- // router.reload (no full navigation needed once events start arriving).
14
- export function Onboarding({
15
- tokenPrefix,
16
- newToken,
17
- className,
18
- }: {
19
- tokenPrefix: string
20
- newToken?: string | null
21
- className?: string
22
- }) {
23
- const token = newToken ?? `${tokenPrefix}…`
24
- const checkNow = () => router.reload()
25
-
26
- return (
27
- <Card className={className}>
28
- <CardHeader>
29
- <CardTitle>Set up this environment</CardTitle>
30
- </CardHeader>
31
- <CardContent className="space-y-4">
32
- <ol className="list-inside list-decimal space-y-3 text-sm">
33
- <li>
34
- Add the gem
35
- <CopyBlock code={'gem "railwatch"'} className="mt-1.5" />
36
- </li>
37
- <li>
38
- Run the generator
39
- <CopyBlock
40
- code="bin/rails generate railwatch:install --cloud"
41
- className="mt-1.5"
42
- />
43
- </li>
44
- <li>
45
- Set the token in the app&apos;s environment
46
- <CopyBlock code={`RAILWATCH_TOKEN=${token}`} className="mt-1.5" />
47
- {!newToken && (
48
- <p className="text-muted-foreground mt-1 text-xs">
49
- Showing the token prefix only. The full token is shown once,
50
- right after an environment is created.
51
- </p>
52
- )}
53
- </li>
54
- <li>
55
- Check every piece is wired up
56
- <CopyBlock code="bin/rails railwatch:doctor" className="mt-1.5" />
57
- <p className="text-muted-foreground mt-1 text-xs">
58
- Prints a ✓/✗ line per check — token, ingest, middleware, routes,
59
- deploy marker, browser client, test matchers — and exits non-zero
60
- if anything required is missing.
61
- </p>
62
- </li>
63
- <li>
64
- Connect your AI assistant
65
- <p className="text-muted-foreground mt-1 text-xs">
66
- Generate a personal token on your{" "}
67
- <Link
68
- href={settingsProfilePath()}
69
- className="underline underline-offset-4"
70
- >
71
- profile page
72
- </Link>{" "}
73
- for paste-ready Claude Code, Cursor, VS Code, and Zed config.
74
- </p>
75
- </li>
76
- <li>Deploy</li>
77
- </ol>
78
- <div className="flex items-center justify-between border-t pt-3">
79
- <div className="text-muted-foreground flex items-center gap-2 text-xs">
80
- <LiveDot lastSeenAt={null} />
81
- Waiting for first event…
82
- </div>
83
- <Button type="button" variant="outline" size="sm" onClick={checkNow}>
84
- <RefreshCwIcon className="size-3.5" />
85
- Check now
86
- </Button>
87
- </div>
88
- </CardContent>
89
- </Card>
90
- )
91
- }
@@ -1,84 +0,0 @@
1
- import { Link } from "@inertiajs/react"
2
-
3
- import { tenantPath } from "@/pages/tenants/tenant-path"
4
- import * as R from "@/routes"
5
- import type { TelemetryTimeParams, Window, WindowRange } from "@/types"
6
-
7
- export interface OriginIdentityData {
8
- user_ref: string | null
9
- tenant: string | null
10
- person: { ref: string; name: string } | null
11
- }
12
-
13
- function personPath(
14
- applicationId: number,
15
- environmentId: number,
16
- ref: string,
17
- timeParams: TelemetryTimeParams,
18
- ) {
19
- const url = String(
20
- R.applicationEnvironmentPersonPath(applicationId, environmentId, ref, {
21
- ...timeParams,
22
- }),
23
- )
24
- const [path, query] = url.split("?")
25
- return `${path.replace(/\./g, "%2E")}${query ? `?${query}` : ""}`
26
- }
27
-
28
- interface Props extends OriginIdentityData {
29
- applicationId: number
30
- environmentId: number
31
- kind: "user" | "tenant"
32
- window?: Window
33
- range?: WindowRange
34
- }
35
-
36
- function identityTimeParams(
37
- window: Window | undefined,
38
- range: WindowRange | undefined,
39
- ): TelemetryTimeParams {
40
- if (window === "custom") {
41
- if (!range) throw new Error("Custom telemetry window requires a range")
42
- return { from: range.from, to: range.to }
43
- }
44
- return { window: window ?? "24h" }
45
- }
46
-
47
- // A propagated job identity describes the enqueue origin. A Person link is
48
- // only offered when this environment has a matching user record; the raw ref
49
- // remains useful for older payloads that never emitted one.
50
- export function OriginIdentity({
51
- applicationId,
52
- environmentId,
53
- kind,
54
- person,
55
- range,
56
- tenant,
57
- user_ref,
58
- window,
59
- }: Props) {
60
- if (kind === "user") {
61
- if (!user_ref) return <span className="text-muted-foreground">–</span>
62
- if (!person) return <span className="font-mono text-xs">{user_ref}</span>
63
- const timeParams = identityTimeParams(window, range)
64
- return (
65
- <Link
66
- className="font-mono text-xs hover:underline"
67
- href={personPath(applicationId, environmentId, person.ref, timeParams)}
68
- >
69
- {person.name}
70
- </Link>
71
- )
72
- }
73
-
74
- if (!tenant) return <span className="text-muted-foreground">–</span>
75
- const timeParams = identityTimeParams(window, range)
76
- return (
77
- <Link
78
- className="font-mono text-xs hover:underline"
79
- href={tenantPath(applicationId, environmentId, tenant, timeParams)}
80
- >
81
- {tenant}
82
- </Link>
83
- )
84
- }
@@ -1,42 +0,0 @@
1
- import { usePage } from "@inertiajs/react"
2
- import type { ReactNode } from "react"
3
-
4
- import type { SharedProps } from "@/types"
5
-
6
- import { LiveToggle } from "./live-toggle"
7
- import { StepPicker } from "./step-picker"
8
- import { WindowPicker } from "./window-picker"
9
-
10
- // Page title row. On phones the controls drop under the title and wrap onto
11
- // as many rows as they need, so nothing is clipped at the viewport edge.
12
- export function PageHeader({
13
- title,
14
- description,
15
- actions,
16
- withWindow = true,
17
- }: {
18
- title: ReactNode
19
- description?: ReactNode
20
- actions?: ReactNode
21
- withWindow?: boolean
22
- }) {
23
- const { environment } = usePage<SharedProps>().props
24
- return (
25
- <div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between md:gap-4">
26
- <div className="min-w-0">
27
- <h1 className="truncate text-xl font-semibold tracking-tight md:text-2xl">
28
- {title}
29
- </h1>
30
- {description && (
31
- <p className="text-muted-foreground mt-0.5 text-sm">{description}</p>
32
- )}
33
- </div>
34
- <div className="flex flex-wrap items-center gap-2 md:shrink-0 md:justify-end">
35
- {environment && <LiveToggle environmentId={environment.id} />}
36
- {actions}
37
- {withWindow && <StepPicker />}
38
- {withWindow && <WindowPicker />}
39
- </div>
40
- </div>
41
- )
42
- }
@@ -1,18 +0,0 @@
1
- import { Segmented, SegmentedItem } from "@/components/railwatch/segmented"
2
- import { usePercentile } from "@/hooks/use-percentile"
3
- import type { Percentile } from "@/types"
4
-
5
- const PERCENTILES: Percentile[] = ["p50", "p95", "p99"]
6
-
7
- export function PercentilePicker() {
8
- const { percentile, set } = usePercentile()
9
- return (
10
- <Segmented>
11
- {PERCENTILES.map((p) => (
12
- <SegmentedItem key={p} active={percentile === p} onClick={() => set(p)}>
13
- {p}
14
- </SegmentedItem>
15
- ))}
16
- </Segmented>
17
- )
18
- }
@@ -1,18 +0,0 @@
1
- import { useTicker } from "@/hooks/use-ticker"
2
- import { ago, when } from "@/lib/format"
3
-
4
- export function RelativeTime({
5
- iso,
6
- className,
7
- }: {
8
- iso: string | null | undefined
9
- className?: string
10
- }) {
11
- useTicker()
12
- if (!iso) return <span className={className}>never</span>
13
- return (
14
- <span className={className} title={when(iso)}>
15
- {ago(iso)}
16
- </span>
17
- )
18
- }
@@ -1,232 +0,0 @@
1
- import { router, usePage } from "@inertiajs/react"
2
- import {
3
- Bookmark,
4
- BookmarkPlus,
5
- Check,
6
- ChevronDown,
7
- Link2,
8
- Pin,
9
- PinOff,
10
- Trash2,
11
- } from "lucide-react"
12
- import { useState } from "react"
13
-
14
- import { Button } from "@/components/ui/button"
15
- import { Checkbox } from "@/components/ui/checkbox"
16
- import {
17
- Dialog,
18
- DialogContent,
19
- DialogDescription,
20
- DialogFooter,
21
- DialogHeader,
22
- DialogTitle,
23
- } from "@/components/ui/dialog"
24
- import {
25
- DropdownMenu,
26
- DropdownMenuContent,
27
- DropdownMenuItem,
28
- DropdownMenuLabel,
29
- DropdownMenuSeparator,
30
- DropdownMenuTrigger,
31
- } from "@/components/ui/dropdown-menu"
32
- import { Input } from "@/components/ui/input"
33
- import { Label } from "@/components/ui/label"
34
- import { cn } from "@/lib/utils"
35
- import * as R from "@/routes"
36
- import type { SavedView, SharedProps } from "@/types"
37
-
38
- // A view's URL and the browser's differ only in the order of their query
39
- // params, so both are normalised (empty values dropped, keys sorted) before
40
- // comparing. Used by the menu and by both navigations to mark the view the
41
- // page is currently showing.
42
- function normalize(url: string) {
43
- const [path, search] = url.split("?")
44
- const params = [...new URLSearchParams(search)]
45
- .filter(([, value]) => value !== "")
46
- .sort(([a], [b]) => a.localeCompare(b))
47
- return `${path}?${new URLSearchParams(params).toString()}`
48
- }
49
-
50
- export function isViewActive(currentUrl: string, viewUrl: string) {
51
- return normalize(currentUrl) === normalize(viewUrl)
52
- }
53
-
54
- export function samePage(viewUrl: string, pageHref: string) {
55
- return viewUrl.split("?")[0] === pageHref.split("?")[0]
56
- }
57
-
58
- // Saved views for one page, as a PageHeader action: a menu of everyone's
59
- // visible views, a dialog that saves the filters currently in the URL, and
60
- // a copy-link button for sharing the exact page you are looking at.
61
- export function SavedViewsMenu({ page }: { page: string }) {
62
- const inertia = usePage<SharedProps>()
63
- const { environment, saved_views: allViews = [] } = inertia.props
64
- const [saving, setSaving] = useState(false)
65
- const [name, setName] = useState("")
66
- const [pinned, setPinned] = useState(true)
67
- const [shared, setShared] = useState(true)
68
- const [copied, setCopied] = useState(false)
69
- const a = environment!.application_id
70
- const e = environment!.id
71
- const views = allViews.filter((v) => v.page === page)
72
- const search = new URLSearchParams(inertia.url.split("?")[1] ?? "")
73
-
74
- const viewPath = (v: SavedView) =>
75
- R.applicationEnvironmentSavedViewPath(a, e, v.id)
76
-
77
- const save = () => {
78
- const params: Record<string, string> = {}
79
- for (const [key, value] of search) {
80
- if (key !== "window" && key !== "q" && value) params[key] = value
81
- }
82
- router.post(
83
- R.applicationEnvironmentSavedViewsPath(a, e),
84
- {
85
- name: name.trim(),
86
- page,
87
- query: search.get("q") ?? "",
88
- window: search.get("window") ?? inertia.props.window ?? "",
89
- pinned,
90
- shared,
91
- params,
92
- },
93
- { preserveScroll: true },
94
- )
95
- setSaving(false)
96
- setName("")
97
- }
98
-
99
- const copyLink = () => {
100
- void navigator.clipboard?.writeText(globalThis.location.href)
101
- setCopied(true)
102
- globalThis.setTimeout(() => setCopied(false), 1500)
103
- }
104
-
105
- return (
106
- <>
107
- <div className="flex shrink-0 items-center gap-1">
108
- <DropdownMenu>
109
- <DropdownMenuTrigger asChild>
110
- <Button variant="outline" size="sm" className="h-8 gap-1.5 px-2">
111
- <Bookmark className="size-3.5" />
112
- <span className="label-caps text-foreground">Views</span>
113
- <ChevronDown className="size-3 opacity-60" />
114
- </Button>
115
- </DropdownMenuTrigger>
116
- <DropdownMenuContent align="end" className="w-64">
117
- <DropdownMenuLabel className="label-caps">
118
- Saved views
119
- </DropdownMenuLabel>
120
- {views.length === 0 && (
121
- <DropdownMenuItem disabled>No saved views yet</DropdownMenuItem>
122
- )}
123
- {views.map((v) => (
124
- <DropdownMenuItem
125
- key={v.id}
126
- onSelect={() => router.visit(v.url)}
127
- className={cn(isViewActive(inertia.url, v.url) && "bg-accent")}
128
- >
129
- <span className="truncate">{v.name}</span>
130
- {v.mine && (
131
- <span className="ml-auto flex shrink-0 items-center gap-0.5">
132
- <button
133
- type="button"
134
- aria-label={`${v.pinned ? "Unpin" : "Pin"} ${v.name}`}
135
- className="text-muted-foreground hover:text-foreground p-0.5"
136
- onClick={(event) => {
137
- event.preventDefault()
138
- event.stopPropagation()
139
- router.patch(
140
- viewPath(v),
141
- { pinned: !v.pinned },
142
- { preserveScroll: true },
143
- )
144
- }}
145
- >
146
- {v.pinned ? (
147
- <PinOff className="size-3.5" />
148
- ) : (
149
- <Pin className="size-3.5" />
150
- )}
151
- </button>
152
- <button
153
- type="button"
154
- aria-label={`Delete ${v.name}`}
155
- className="text-muted-foreground hover:text-destructive p-0.5"
156
- onClick={(event) => {
157
- event.preventDefault()
158
- event.stopPropagation()
159
- router.delete(viewPath(v), { preserveScroll: true })
160
- }}
161
- >
162
- <Trash2 className="size-3.5" />
163
- </button>
164
- </span>
165
- )}
166
- </DropdownMenuItem>
167
- ))}
168
- <DropdownMenuSeparator />
169
- <DropdownMenuItem onSelect={() => setSaving(true)}>
170
- <BookmarkPlus className="size-3.5" />
171
- Save current view…
172
- </DropdownMenuItem>
173
- </DropdownMenuContent>
174
- </DropdownMenu>
175
- <Button
176
- variant="outline"
177
- size="sm"
178
- className="h-8 px-2"
179
- aria-label="Copy link to this view"
180
- onClick={copyLink}
181
- >
182
- {copied ? (
183
- <Check className="size-3.5" />
184
- ) : (
185
- <Link2 className="size-3.5" />
186
- )}
187
- </Button>
188
- </div>
189
- <Dialog open={saving} onOpenChange={setSaving}>
190
- <DialogContent className="sm:max-w-sm">
191
- <DialogHeader>
192
- <DialogTitle>Save this view</DialogTitle>
193
- <DialogDescription>
194
- Keeps the filters, window, and sort currently in the URL under a
195
- name you can link to.
196
- </DialogDescription>
197
- </DialogHeader>
198
- <div className="flex flex-col gap-3">
199
- <div className="grid gap-1.5">
200
- <Label htmlFor="saved-view-name">Name</Label>
201
- <Input
202
- id="saved-view-name"
203
- value={name}
204
- onChange={(event) => setName(event.target.value)}
205
- placeholder="5xx checkout requests"
206
- />
207
- </div>
208
- <Label className="gap-2 font-normal">
209
- <Checkbox
210
- checked={pinned}
211
- onCheckedChange={(value) => setPinned(value === true)}
212
- />
213
- Pin to the sidebar
214
- </Label>
215
- <Label className="gap-2 font-normal">
216
- <Checkbox
217
- checked={shared}
218
- onCheckedChange={(value) => setShared(value === true)}
219
- />
220
- Share with the team
221
- </Label>
222
- </div>
223
- <DialogFooter>
224
- <Button size="sm" onClick={save} disabled={!name.trim()}>
225
- Save view
226
- </Button>
227
- </DialogFooter>
228
- </DialogContent>
229
- </Dialog>
230
- </>
231
- )
232
- }