railwatch 0.9.4 → 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 (452) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +37 -33
  3. data/CHANGELOG.md +50 -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 +35 -66
  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/configuration.rb +21 -151
  21. data/lib/railwatch/engine.rb +9 -118
  22. data/lib/railwatch/patches.rb +0 -11
  23. data/lib/railwatch/reporter.rb +3 -19
  24. data/lib/railwatch/subscribers/process_info.rb +1 -2
  25. data/lib/railwatch/version.rb +1 -1
  26. data/lib/railwatch.rb +4 -62
  27. data/lib/tasks/railwatch_tasks.rake +3 -325
  28. data/llms.txt +23 -25
  29. metadata +3 -453
  30. data/app/channels/railwatch/environment_channel.rb +0 -62
  31. data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
  32. data/app/controllers/railwatch/alerts_controller.rb +0 -48
  33. data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
  34. data/app/controllers/railwatch/attachments_controller.rb +0 -22
  35. data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
  36. data/app/controllers/railwatch/cache_events_controller.rb +0 -30
  37. data/app/controllers/railwatch/commands_controller.rb +0 -16
  38. data/app/controllers/railwatch/comments_controller.rb +0 -11
  39. data/app/controllers/railwatch/dashboard_controller.rb +0 -70
  40. data/app/controllers/railwatch/deploys_controller.rb +0 -34
  41. data/app/controllers/railwatch/deprecations_controller.rb +0 -54
  42. data/app/controllers/railwatch/environment_scoped.rb +0 -81
  43. data/app/controllers/railwatch/exceptions_controller.rb +0 -52
  44. data/app/controllers/railwatch/executions_controller.rb +0 -13
  45. data/app/controllers/railwatch/issues_controller.rb +0 -258
  46. data/app/controllers/railwatch/jobs_controller.rb +0 -63
  47. data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
  48. data/app/controllers/railwatch/logs_controller.rb +0 -40
  49. data/app/controllers/railwatch/mails_controller.rb +0 -19
  50. data/app/controllers/railwatch/notifications_controller.rb +0 -11
  51. data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
  52. data/app/controllers/railwatch/overview_controller.rb +0 -38
  53. data/app/controllers/railwatch/people_controller.rb +0 -25
  54. data/app/controllers/railwatch/processes_controller.rb +0 -33
  55. data/app/controllers/railwatch/profiles_controller.rb +0 -65
  56. data/app/controllers/railwatch/queries_controller.rb +0 -59
  57. data/app/controllers/railwatch/releases_controller.rb +0 -75
  58. data/app/controllers/railwatch/requests_controller.rb +0 -37
  59. data/app/controllers/railwatch/saved_views_controller.rb +0 -53
  60. data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
  61. data/app/controllers/railwatch/spans_controller.rb +0 -47
  62. data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
  63. data/app/controllers/railwatch/tenants_controller.rb +0 -44
  64. data/app/controllers/railwatch/thresholds_controller.rb +0 -47
  65. data/app/controllers/railwatch/traces_controller.rb +0 -72
  66. data/app/controllers/railwatch/transactions_controller.rb +0 -21
  67. data/app/controllers/railwatch/view_renders_controller.rb +0 -28
  68. data/app/controllers/railwatch/visits_controller.rb +0 -53
  69. data/app/frontend/components/app-content.tsx +0 -26
  70. data/app/frontend/components/app-logo-icon.tsx +0 -23
  71. data/app/frontend/components/app-logo.tsx +0 -14
  72. data/app/frontend/components/app-shell.tsx +0 -38
  73. data/app/frontend/components/app-sidebar-header.tsx +0 -54
  74. data/app/frontend/components/app-sidebar.tsx +0 -94
  75. data/app/frontend/components/app-wordmark.tsx +0 -29
  76. data/app/frontend/components/breadcrumbs.tsx +0 -46
  77. data/app/frontend/components/icon.tsx +0 -16
  78. data/app/frontend/components/input-error.tsx +0 -18
  79. data/app/frontend/components/nav-footer.tsx +0 -46
  80. data/app/frontend/components/nav-main.tsx +0 -41
  81. data/app/frontend/components/nav-user.tsx +0 -50
  82. data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
  83. data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
  84. data/app/frontend/components/railwatch/code.tsx +0 -28
  85. data/app/frontend/components/railwatch/command-palette.tsx +0 -132
  86. data/app/frontend/components/railwatch/copy-block.tsx +0 -52
  87. data/app/frontend/components/railwatch/copy-id.tsx +0 -49
  88. data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
  89. data/app/frontend/components/railwatch/data-table.tsx +0 -195
  90. data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
  91. data/app/frontend/components/railwatch/empty-state.tsx +0 -159
  92. data/app/frontend/components/railwatch/env-nav.tsx +0 -130
  93. data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
  94. data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
  95. data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
  96. data/app/frontend/components/railwatch/frames.tsx +0 -295
  97. data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
  98. data/app/frontend/components/railwatch/live-dot.tsx +0 -78
  99. data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
  100. data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
  101. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
  102. data/app/frontend/components/railwatch/onboarding.tsx +0 -91
  103. data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
  104. data/app/frontend/components/railwatch/page-header.tsx +0 -42
  105. data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
  106. data/app/frontend/components/railwatch/relative-time.tsx +0 -18
  107. data/app/frontend/components/railwatch/saved-views.tsx +0 -232
  108. data/app/frontend/components/railwatch/segmented.tsx +0 -64
  109. data/app/frontend/components/railwatch/series-chart.tsx +0 -400
  110. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
  111. data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
  112. data/app/frontend/components/railwatch/sort-header.tsx +0 -37
  113. data/app/frontend/components/railwatch/source-link.tsx +0 -57
  114. data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
  115. data/app/frontend/components/railwatch/sql-block.tsx +0 -116
  116. data/app/frontend/components/railwatch/stat.tsx +0 -185
  117. data/app/frontend/components/railwatch/status-badge.tsx +0 -79
  118. data/app/frontend/components/railwatch/step-picker.tsx +0 -57
  119. data/app/frontend/components/railwatch/timeline.tsx +0 -405
  120. data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
  121. data/app/frontend/components/railwatch/window-picker.tsx +0 -147
  122. data/app/frontend/components/ui/avatar.tsx +0 -51
  123. data/app/frontend/components/ui/badge.tsx +0 -46
  124. data/app/frontend/components/ui/breadcrumb.tsx +0 -109
  125. data/app/frontend/components/ui/button.tsx +0 -60
  126. data/app/frontend/components/ui/card.tsx +0 -92
  127. data/app/frontend/components/ui/chart.tsx +0 -374
  128. data/app/frontend/components/ui/checkbox.tsx +0 -32
  129. data/app/frontend/components/ui/collapsible.tsx +0 -31
  130. data/app/frontend/components/ui/command.tsx +0 -182
  131. data/app/frontend/components/ui/dialog.tsx +0 -158
  132. data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
  133. data/app/frontend/components/ui/hover-card.tsx +0 -42
  134. data/app/frontend/components/ui/input.tsx +0 -21
  135. data/app/frontend/components/ui/label.tsx +0 -24
  136. data/app/frontend/components/ui/popover.tsx +0 -87
  137. data/app/frontend/components/ui/select.tsx +0 -185
  138. data/app/frontend/components/ui/separator.tsx +0 -28
  139. data/app/frontend/components/ui/sheet.tsx +0 -137
  140. data/app/frontend/components/ui/sidebar.tsx +0 -726
  141. data/app/frontend/components/ui/skeleton.tsx +0 -13
  142. data/app/frontend/components/ui/sonner.tsx +0 -41
  143. data/app/frontend/components/ui/table.tsx +0 -114
  144. data/app/frontend/components/ui/tabs.tsx +0 -91
  145. data/app/frontend/components/ui/textarea.tsx +0 -18
  146. data/app/frontend/components/ui/tooltip.tsx +0 -61
  147. data/app/frontend/components/user-info.tsx +0 -32
  148. data/app/frontend/components/user-menu-content.tsx +0 -94
  149. data/app/frontend/entrypoints/application.css +0 -244
  150. data/app/frontend/entrypoints/inertia.tsx +0 -92
  151. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  152. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  153. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  154. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  155. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  156. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  157. data/app/frontend/hooks/use-appearance.tsx +0 -67
  158. data/app/frontend/hooks/use-clipboard.ts +0 -32
  159. data/app/frontend/hooks/use-flash.tsx +0 -23
  160. data/app/frontend/hooks/use-hotkeys.ts +0 -63
  161. data/app/frontend/hooks/use-initials.tsx +0 -15
  162. data/app/frontend/hooks/use-live.ts +0 -205
  163. data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
  164. data/app/frontend/hooks/use-mobile.ts +0 -21
  165. data/app/frontend/hooks/use-percentile.ts +0 -24
  166. data/app/frontend/hooks/use-reduced-motion.ts +0 -25
  167. data/app/frontend/hooks/use-row-nav.ts +0 -106
  168. data/app/frontend/hooks/use-step.ts +0 -27
  169. data/app/frontend/hooks/use-ticker.ts +0 -12
  170. data/app/frontend/hooks/use-window.ts +0 -58
  171. data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
  172. data/app/frontend/layouts/app-layout.tsx +0 -21
  173. data/app/frontend/layouts/env-layout.tsx +0 -53
  174. data/app/frontend/layouts/persistent-layout.tsx +0 -18
  175. data/app/frontend/lib/cable.ts +0 -10
  176. data/app/frontend/lib/env-pages.ts +0 -236
  177. data/app/frontend/lib/execution-path.ts +0 -55
  178. data/app/frontend/lib/explain.ts +0 -50
  179. data/app/frontend/lib/filter.ts +0 -25
  180. data/app/frontend/lib/flamegraph.ts +0 -155
  181. data/app/frontend/lib/format.ts +0 -71
  182. data/app/frontend/lib/keyboard.ts +0 -18
  183. data/app/frontend/lib/mount.ts +0 -23
  184. data/app/frontend/lib/source-link.ts +0 -217
  185. data/app/frontend/lib/utils.ts +0 -6
  186. data/app/frontend/pages/alerts/index.tsx +0 -171
  187. data/app/frontend/pages/broadcasts/index.tsx +0 -171
  188. data/app/frontend/pages/cache_events/index.tsx +0 -127
  189. data/app/frontend/pages/commands/index.tsx +0 -143
  190. data/app/frontend/pages/deploys/index.tsx +0 -94
  191. data/app/frontend/pages/deploys/show.tsx +0 -309
  192. data/app/frontend/pages/deprecations/index.tsx +0 -176
  193. data/app/frontend/pages/exceptions/index.tsx +0 -271
  194. data/app/frontend/pages/executions/show.tsx +0 -821
  195. data/app/frontend/pages/issues/index.tsx +0 -399
  196. data/app/frontend/pages/issues/show.tsx +0 -1367
  197. data/app/frontend/pages/jobs/index.tsx +0 -381
  198. data/app/frontend/pages/jobs/klass.tsx +0 -215
  199. data/app/frontend/pages/llm_calls/index.tsx +0 -664
  200. data/app/frontend/pages/logs/index.tsx +0 -303
  201. data/app/frontend/pages/mails/index.tsx +0 -218
  202. data/app/frontend/pages/notifications/index.tsx +0 -161
  203. data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
  204. data/app/frontend/pages/overview/show.tsx +0 -333
  205. data/app/frontend/pages/people/index.tsx +0 -102
  206. data/app/frontend/pages/people/show.tsx +0 -149
  207. data/app/frontend/pages/processes/index.tsx +0 -335
  208. data/app/frontend/pages/profiles/index.tsx +0 -110
  209. data/app/frontend/pages/profiles/show.tsx +0 -105
  210. data/app/frontend/pages/queries/index.tsx +0 -492
  211. data/app/frontend/pages/queries/show.tsx +0 -276
  212. data/app/frontend/pages/releases/index.tsx +0 -190
  213. data/app/frontend/pages/releases/release-health.tsx +0 -68
  214. data/app/frontend/pages/releases/release-path.ts +0 -25
  215. data/app/frontend/pages/releases/show.tsx +0 -262
  216. data/app/frontend/pages/requests/index.tsx +0 -235
  217. data/app/frontend/pages/requests/route.tsx +0 -159
  218. data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
  219. data/app/frontend/pages/spans/index.tsx +0 -229
  220. data/app/frontend/pages/spans/show.tsx +0 -220
  221. data/app/frontend/pages/storage_ops/index.tsx +0 -150
  222. data/app/frontend/pages/tenants/index.tsx +0 -236
  223. data/app/frontend/pages/tenants/show.tsx +0 -477
  224. data/app/frontend/pages/tenants/tenant-path.ts +0 -26
  225. data/app/frontend/pages/thresholds/index.tsx +0 -414
  226. data/app/frontend/pages/traces/show.tsx +0 -208
  227. data/app/frontend/pages/transactions/index.tsx +0 -220
  228. data/app/frontend/pages/view_renders/index.tsx +0 -241
  229. data/app/frontend/pages/visits/index.tsx +0 -321
  230. data/app/frontend/public/icon.png +0 -0
  231. data/app/frontend/public/icon.svg +0 -5
  232. data/app/frontend/routes/index.d.ts +0 -2136
  233. data/app/frontend/routes/index.js +0 -2112
  234. data/app/frontend/types/globals.d.ts +0 -9
  235. data/app/frontend/types/index.ts +0 -288
  236. data/app/frontend/types/vite-env.d.ts +0 -9
  237. data/app/helpers/railwatch/assets_helper.rb +0 -52
  238. data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
  239. data/app/jobs/railwatch/application_job.rb +0 -7
  240. data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
  241. data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
  242. data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
  243. data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
  244. data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
  245. data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
  246. data/app/jobs/railwatch/performance_scan_job.rb +0 -11
  247. data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
  248. data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
  249. data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
  250. data/app/jobs/railwatch/rollup_job.rb +0 -132
  251. data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
  252. data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
  253. data/app/models/railwatch/alert.rb +0 -229
  254. data/app/models/railwatch/alert_rule.rb +0 -121
  255. data/app/models/railwatch/anomaly_rule.rb +0 -51
  256. data/app/models/railwatch/application.rb +0 -44
  257. data/app/models/railwatch/application_record.rb +0 -33
  258. data/app/models/railwatch/comment.rb +0 -36
  259. data/app/models/railwatch/deploy.rb +0 -67
  260. data/app/models/railwatch/environment.rb +0 -68
  261. data/app/models/railwatch/execution_presenter.rb +0 -185
  262. data/app/models/railwatch/filter_query.rb +0 -184
  263. data/app/models/railwatch/followup_receipt.rb +0 -27
  264. data/app/models/railwatch/ingest/batch.rb +0 -355
  265. data/app/models/railwatch/ingest/mapper.rb +0 -585
  266. data/app/models/railwatch/ingest/payload.rb +0 -96
  267. data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
  268. data/app/models/railwatch/ingest/writer.rb +0 -137
  269. data/app/models/railwatch/issue.rb +0 -277
  270. data/app/models/railwatch/issue_activity.rb +0 -23
  271. data/app/models/railwatch/issue_detection_presenter.rb +0 -309
  272. data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
  273. data/app/models/railwatch/maintenance_task.rb +0 -53
  274. data/app/models/railwatch/saved_view.rb +0 -63
  275. data/app/models/railwatch/telemetry/aggregations.rb +0 -251
  276. data/app/models/railwatch/telemetry/attachment.rb +0 -31
  277. data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
  278. data/app/models/railwatch/telemetry/broadcast.rb +0 -13
  279. data/app/models/railwatch/telemetry/cache_event.rb +0 -16
  280. data/app/models/railwatch/telemetry/child.rb +0 -53
  281. data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
  282. data/app/models/railwatch/telemetry/deprecation.rb +0 -13
  283. data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
  284. data/app/models/railwatch/telemetry/exception.rb +0 -28
  285. data/app/models/railwatch/telemetry/execution.rb +0 -152
  286. data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
  287. data/app/models/railwatch/telemetry/export_destination.rb +0 -66
  288. data/app/models/railwatch/telemetry/health_sample.rb +0 -72
  289. data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
  290. data/app/models/railwatch/telemetry/llm_call.rb +0 -37
  291. data/app/models/railwatch/telemetry/log.rb +0 -91
  292. data/app/models/railwatch/telemetry/mail.rb +0 -13
  293. data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
  294. data/app/models/railwatch/telemetry/notification.rb +0 -13
  295. data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
  296. data/app/models/railwatch/telemetry/person.rb +0 -50
  297. data/app/models/railwatch/telemetry/process.rb +0 -10
  298. data/app/models/railwatch/telemetry/profile.rb +0 -37
  299. data/app/models/railwatch/telemetry/query.rb +0 -57
  300. data/app/models/railwatch/telemetry/query_shape.rb +0 -45
  301. data/app/models/railwatch/telemetry/release_health.rb +0 -69
  302. data/app/models/railwatch/telemetry/rollup.rb +0 -157
  303. data/app/models/railwatch/telemetry/session.rb +0 -24
  304. data/app/models/railwatch/telemetry/span.rb +0 -19
  305. data/app/models/railwatch/telemetry/storage_op.rb +0 -13
  306. data/app/models/railwatch/telemetry/tenant.rb +0 -240
  307. data/app/models/railwatch/telemetry/transaction.rb +0 -13
  308. data/app/models/railwatch/telemetry/view_render.rb +0 -13
  309. data/app/models/railwatch/telemetry/visit.rb +0 -24
  310. data/app/models/railwatch/telemetry_record.rb +0 -107
  311. data/app/models/railwatch/threshold.rb +0 -66
  312. data/app/models/railwatch/user.rb +0 -47
  313. data/app/models/railwatch/viewer.rb +0 -13
  314. data/app/models/railwatch/window.rb +0 -66
  315. data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
  316. data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
  317. data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
  318. data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
  319. data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
  320. data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
  321. data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
  322. data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
  323. data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
  324. data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
  325. data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
  326. data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
  327. data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
  328. data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
  329. data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
  330. data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
  331. data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
  332. data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
  333. data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
  334. data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
  335. data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
  336. data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
  337. data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
  338. data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
  339. data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
  340. data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
  341. data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
  342. data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
  343. data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
  344. data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
  345. data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
  346. data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
  347. data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
  348. data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
  349. data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
  350. data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
  351. data/docs/embedded.md +0 -503
  352. data/lib/puma/plugin/railwatch.rb +0 -215
  353. data/lib/railwatch/dashboard_assets.rb +0 -45
  354. data/lib/railwatch/embedded.rb +0 -50
  355. data/lib/railwatch/export/client.rb +0 -124
  356. data/lib/railwatch/export/lease.rb +0 -56
  357. data/lib/railwatch/export/outbox.rb +0 -361
  358. data/lib/railwatch/export/policy.rb +0 -79
  359. data/lib/railwatch/export/sender.rb +0 -130
  360. data/lib/railwatch/ingest_request_body_limit.rb +0 -10
  361. data/lib/railwatch/maintenance.rb +0 -194
  362. data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
  363. data/lib/railwatch/transport/local.rb +0 -78
  364. data/lib/railwatch/transport/socket.rb +0 -183
  365. data/lib/railwatch/writer.rb +0 -370
  366. data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
  367. data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
  368. data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
  369. data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
  370. data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
  371. data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
  372. data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
  373. data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
  374. data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
  375. data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
  376. data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
  377. data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
  378. data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
  379. data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
  380. data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
  381. data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
  382. data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
  383. data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
  384. data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
  385. data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
  386. data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
  387. data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
  388. data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
  389. data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
  390. data/public/railwatch/assets/index--NQ7us-L.js +0 -1
  391. data/public/railwatch/assets/index-2kgYe_99.js +0 -1
  392. data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
  393. data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
  394. data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
  395. data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
  396. data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
  397. data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
  398. data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
  399. data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
  400. data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
  401. data/public/railwatch/assets/index-C2j50AL2.js +0 -1
  402. data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
  403. data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
  404. data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
  405. data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
  406. data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
  407. data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
  408. data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
  409. data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
  410. data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
  411. data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
  412. data/public/railwatch/assets/index-Deq14BxB.js +0 -1
  413. data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
  414. data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
  415. data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
  416. data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
  417. data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
  418. data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
  419. data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
  420. data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
  421. data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
  422. data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
  423. data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
  424. data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
  425. data/public/railwatch/assets/route-CWh58C7K.js +0 -1
  426. data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
  427. data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
  428. data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
  429. data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
  430. data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
  431. data/public/railwatch/assets/show-BYPB74t4.js +0 -2
  432. data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
  433. data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
  434. data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
  435. data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
  436. data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
  437. data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
  438. data/public/railwatch/assets/show-g__jnN36.js +0 -1
  439. data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
  440. data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
  441. data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
  442. data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
  443. data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
  444. data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
  445. data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
  446. data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
  447. data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
  448. data/public/railwatch/icon.png +0 -0
  449. data/public/railwatch/icon.svg +0 -5
  450. data/public/railwatch/manifest.json +0 -1245
  451. data/public/railwatch/rails-vite.json +0 -1
  452. /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
@@ -1,312 +0,0 @@
1
- import { useEffect, useRef } from "react"
2
-
3
- import { cn } from "@/lib/utils"
4
-
5
- // A number whose digits roll when it changes: the digits that actually
6
- // changed slide up (or down, when the number falls) to their new glyph and
7
- // the rest of the number stays exactly where it is.
8
- //
9
- // This is motion on an operational dashboard, which is a thing to be
10
- // careful with rather than a thing to sprinkle around -- see the gates on
11
- // `Stat`'s rolling path, which is what decides whether this component is
12
- // rendered at all. What this component owns is the part of the contract
13
- // that no caller can get right by itself:
14
- //
15
- // * the roll is 300ms, so it is over long before the next update can
16
- // arrive (see ROLL_MIN_INTERVAL_MS) and the strip is never in motion
17
- // for more than ~15% of the time;
18
- // * updates are coalesced to one roll every 2s. An update that lands
19
- // inside that window is remembered, not painted, and the roll that
20
- // follows goes straight to the newest value. A roll is never
21
- // interrupted, because an animation that never finishes is worse than
22
- // no animation at all;
23
- // * the first paint is static. A page that opens, or one that is
24
- // navigated back to, shows its numbers rather than animating up to
25
- // them.
26
- //
27
- // The cells are built by hand rather than rendered, because they are keyed
28
- // by place value and animated with the Web Animations API, and neither of
29
- // those survives React re-rendering the digits underneath them. They are
30
- // decoration -- whoever renders this is expected to hide it from assistive
31
- // tech and put the number in a text node beside it (`Stat` does).
32
-
33
- // The one switch. Flip this to false and every rolling number in the app --
34
- // the dashboard's stats, and whatever the landing page does with the same
35
- // component -- becomes a plain static number.
36
- export const DIGIT_ROLL_ENABLED = true
37
-
38
- export const ROLL_DURATION_MS = 300
39
- export const ROLL_EASING = "cubic-bezier(.22, 1, .36, 1)"
40
- export const ROLL_MIN_INTERVAL_MS = 2_000
41
-
42
- // Fixed em heights on every piece, so a roll cannot change the line box and
43
- // the strip below the number cannot be pushed around by it.
44
- const CELL_CLASS =
45
- "inline-block h-[1.15em] overflow-hidden leading-[1.15em] align-top"
46
- const SEPARATOR_CLASS =
47
- "inline-block h-[1.15em] overflow-visible leading-[1.15em] align-top text-center"
48
- const STRIP_CLASS = "block will-change-transform"
49
- const SLOT_CLASS = "block h-[1.15em] leading-[1.15em]"
50
- const SUFFIX_CLASS = "inline-block h-[1.15em] leading-[1.15em]"
51
-
52
- type Direction = -1 | 0 | 1
53
-
54
- // A thousands separator (or a decimal point, or a minus sign) never rolls,
55
- // so it is a plain glyph rather than a two-slot stack.
56
- interface SeparatorCell {
57
- separator: true
58
- el: HTMLSpanElement
59
- char: string | null
60
- }
61
-
62
- interface DigitCell {
63
- separator: false
64
- el: HTMLSpanElement
65
- strip: HTMLSpanElement
66
- top: HTMLSpanElement
67
- bottom: HTMLSpanElement
68
- animation: Animation | null
69
- char: string | null
70
- }
71
-
72
- type Cell = SeparatorCell | DigitCell
73
-
74
- function span(className: string) {
75
- const el = document.createElement("span")
76
- el.className = className
77
- return el
78
- }
79
-
80
- // "412,908ms" -> { digits: "412,908", suffix: "ms" }. The unit is one glyph
81
- // run at the end of the number that nothing ever moves.
82
- function split(formatted: string) {
83
- let end = formatted.length
84
- while (end > 0 && !/[0-9]/.test(formatted[end - 1])) end--
85
- return { digits: formatted.slice(0, end), suffix: formatted.slice(end) }
86
- }
87
-
88
- function makeCell(separator: boolean): Cell {
89
- const el = span(separator ? SEPARATOR_CLASS : CELL_CLASS)
90
- if (separator) return { separator: true, el, char: null }
91
- const strip = span(STRIP_CLASS)
92
- const top = span(SLOT_CLASS)
93
- const bottom = span(SLOT_CLASS)
94
- strip.append(top, bottom)
95
- el.append(strip)
96
- return {
97
- separator: false,
98
- el,
99
- strip,
100
- top,
101
- bottom,
102
- animation: null,
103
- char: null,
104
- }
105
- }
106
-
107
- // Cells are keyed by distance from the right of the number, so a digit keeps
108
- // its identity -- its place value -- even when the number gains a digit:
109
- // 999 -> 1,000 leaves keys 0,1,2 rolling 9 -> 0 and simply inserts key 3
110
- // (the comma) and key 4 (the 1). No re-keying, no cascade, and the digits
111
- // that did not change never move.
112
- function createRoll(
113
- host: HTMLElement,
114
- { durationMs, easing }: { durationMs: number; easing: string },
115
- ) {
116
- const cells = new Map<number, Cell>()
117
- let order = ""
118
-
119
- // Collapse a cell back to the single digit that is showing. The glyph it
120
- // rolled away from is out of view but still in the DOM, and a selection
121
- // (or a copy) would otherwise pick it up.
122
- function settle(cell: DigitCell, char: string) {
123
- cell.animation?.cancel()
124
- cell.animation = null
125
- cell.top.textContent = char
126
- cell.bottom.textContent = ""
127
- cell.char = char
128
- }
129
-
130
- function roll(cell: DigitCell, char: string, direction: -1 | 1) {
131
- // jsdom has no Web Animations API. Nothing to roll there: show the digit.
132
- if (typeof cell.strip.animate !== "function") {
133
- settle(cell, char)
134
- return
135
- }
136
- // Cancel, re-fill the slots, and start the new animation in one task, so
137
- // the browser never paints the intermediate state: an interrupted roll
138
- // restarts from wherever it had got to, towards the new target.
139
- cell.animation?.cancel()
140
- const from = cell.char ?? char
141
- if (direction > 0) {
142
- cell.top.textContent = from
143
- cell.bottom.textContent = char
144
- } else {
145
- cell.top.textContent = char
146
- cell.bottom.textContent = from
147
- }
148
- const keyframes =
149
- direction > 0
150
- ? [{ transform: "translateY(0)" }, { transform: "translateY(-50%)" }]
151
- : [{ transform: "translateY(-50%)" }, { transform: "translateY(0)" }]
152
- const animation = cell.strip.animate(keyframes, {
153
- duration: durationMs,
154
- easing,
155
- fill: "forwards",
156
- })
157
- cell.animation = animation
158
- cell.char = char
159
- animation.onfinish = () => {
160
- if (cell.animation === animation) settle(cell, char)
161
- }
162
- }
163
-
164
- function enter(cell: Cell) {
165
- if (typeof cell.el.animate !== "function") return
166
- cell.el.animate(
167
- [
168
- { opacity: 0, transform: "translateY(0.35em)" },
169
- { opacity: 1, transform: "none" },
170
- ],
171
- { duration: durationMs, easing },
172
- )
173
- }
174
-
175
- return {
176
- paint(formatted: string, direction: Direction) {
177
- const { digits, suffix } = split(formatted)
178
- const chars = digits.split("")
179
- const wanted = chars.map((char, index) => ({
180
- key: chars.length - 1 - index,
181
- char,
182
- }))
183
- const nextOrder = `${wanted.map((cell) => cell.key).join(",")}|${suffix}`
184
- const first = order === ""
185
-
186
- if (nextOrder !== order) {
187
- // The key set changed (the number gained or lost a digit): re-lay the
188
- // row, reusing every cell whose key survived.
189
- const fragment = document.createDocumentFragment()
190
- const keep = new Set<number>()
191
- for (const { key, char } of wanted) {
192
- keep.add(key)
193
- const separator = !/[0-9]/.test(char)
194
- let cell = cells.get(key)
195
- if (cell?.separator !== separator) {
196
- cell = makeCell(separator)
197
- cells.set(key, cell)
198
- if (!first && direction !== 0) enter(cell)
199
- }
200
- fragment.append(cell.el)
201
- }
202
- for (const key of [...cells.keys()])
203
- if (!keep.has(key)) cells.delete(key)
204
- host.replaceChildren(fragment)
205
- if (suffix) {
206
- const el = span(SUFFIX_CLASS)
207
- el.textContent = suffix
208
- host.append(el)
209
- }
210
- order = nextOrder
211
- }
212
-
213
- for (const { key, char } of wanted) {
214
- const cell = cells.get(key)
215
- if (!cell || cell.char === char) continue // this digit did not change
216
- if (cell.separator) {
217
- cell.el.textContent = char
218
- cell.char = char
219
- } else if (cell.char === null || direction === 0) {
220
- settle(cell, char)
221
- } else {
222
- roll(cell, char, direction)
223
- }
224
- }
225
- },
226
-
227
- destroy() {
228
- for (const cell of cells.values())
229
- if (!cell.separator) cell.animation?.cancel()
230
- },
231
- }
232
- }
233
-
234
- export function DigitRoll({
235
- value,
236
- format,
237
- className,
238
- durationMs = ROLL_DURATION_MS,
239
- easing = ROLL_EASING,
240
- minIntervalMs = ROLL_MIN_INTERVAL_MS,
241
- }: {
242
- value: number
243
- format: (value: number) => string
244
- className?: string
245
- durationMs?: number
246
- easing?: string
247
- minIntervalMs?: number
248
- }) {
249
- const hostRef = useRef<HTMLSpanElement>(null)
250
- const rollRef = useRef<ReturnType<typeof createRoll> | null>(null)
251
- // What the cells are showing, which is not `value` while an update is
252
- // being held back by the coalescing window.
253
- const shownRef = useRef(value)
254
- const pendingRef = useRef<number | null>(null)
255
- const lastRollAtRef = useRef(0)
256
- const timerRef = useRef<number | undefined>(undefined)
257
- const optionsRef = useRef({ format, durationMs, easing, minIntervalMs })
258
- useEffect(() => {
259
- optionsRef.current = { format, durationMs, easing, minIntervalMs }
260
- })
261
-
262
- useEffect(() => {
263
- const host = hostRef.current
264
- if (!host) return
265
- const { format, durationMs, easing } = optionsRef.current
266
- const roll = createRoll(host, { durationMs, easing })
267
- rollRef.current = roll
268
- // Never on mount: the first paint is the number, not a roll up to it.
269
- roll.paint(format(shownRef.current), 0)
270
- return () => {
271
- clearTimeout(timerRef.current)
272
- timerRef.current = undefined
273
- pendingRef.current = null
274
- rollRef.current = null
275
- roll.destroy()
276
- }
277
- // Built once; every later value goes through the effect below.
278
- }, [])
279
-
280
- useEffect(() => {
281
- const roll = rollRef.current
282
- if (!roll || value === shownRef.current) return
283
-
284
- const apply = (next: number) => {
285
- lastRollAtRef.current = Date.now()
286
- roll.paint(
287
- optionsRef.current.format(next),
288
- next >= shownRef.current ? 1 : -1,
289
- )
290
- shownRef.current = next
291
- }
292
-
293
- const elapsed = Date.now() - lastRollAtRef.current
294
- const wait = optionsRef.current.minIntervalMs - elapsed
295
- if (wait <= 0) {
296
- apply(value)
297
- return
298
- }
299
- // Inside the window: hold this value (replacing any other one being
300
- // held) and let the timer already running land the newest of them. The
301
- // roll in flight is left alone.
302
- pendingRef.current = value
303
- timerRef.current ??= window.setTimeout(() => {
304
- timerRef.current = undefined
305
- const next = pendingRef.current
306
- pendingRef.current = null
307
- if (next !== null && next !== shownRef.current) apply(next)
308
- }, wait)
309
- }, [value])
310
-
311
- return <span ref={hostRef} className={cn(className)} />
312
- }
@@ -1,159 +0,0 @@
1
- import { type LucideIcon } from "lucide-react"
2
- import { type ReactNode, useId } from "react"
3
-
4
- import { cn } from "@/lib/utils"
5
-
6
- // A stretch of track behind the message: two rails and a run of ties. Two
7
- // masks shape it. A horizontal one fades the ends so the line runs off the
8
- // edge of the panel rather than stopping at it, and a radial one dissolves
9
- // the middle so the message sits in a clear pocket while the rails stay
10
- // legible on either side. The pocket is narrower below md so a phone card
11
- // still shows a good run of track on each side of the message.
12
- export function Track({ className }: { className?: string }) {
13
- const id = useId()
14
- return (
15
- <svg
16
- aria-hidden
17
- className={cn(
18
- "text-foreground/35 dark:text-foreground/40 pointer-events-none absolute inset-x-0 top-1/2 h-12 w-full -translate-y-1/2",
19
- "[mask-composite:intersect]",
20
- "[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent),radial-gradient(ellipse_200px_160%_at_50%_50%,transparent_20%,black_100%)]",
21
- "md:[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent),radial-gradient(ellipse_300px_160%_at_50%_50%,transparent_35%,black_100%)]",
22
- className,
23
- )}
24
- preserveAspectRatio="none"
25
- >
26
- <defs>
27
- <pattern id={id} width="22" height="48" patternUnits="userSpaceOnUse">
28
- <rect x="9" y="9" width="4" height="30" rx="1" fill="currentColor" />
29
- </pattern>
30
- </defs>
31
- <rect width="100%" height="100%" fill={`url(#${id})`} />
32
- <rect y="16" width="100%" height="2.5" fill="currentColor" />
33
- <rect y="29.5" width="100%" height="2.5" fill="currentColor" />
34
- </svg>
35
- )
36
- }
37
-
38
- export type SignalAspect = "clear" | "caution" | "stop"
39
-
40
- const ASPECT_CLASS: Record<SignalAspect, string> = {
41
- clear: "text-live",
42
- caution: "text-warning",
43
- stop: "text-danger",
44
- }
45
-
46
- // Lamps in their real order on a three-aspect head: stop on top, caution in
47
- // the middle, clear at the bottom.
48
- const LAMP_Y: Record<SignalAspect, number> = { stop: 8, caution: 17, clear: 26 }
49
- const LAMP_ORDER: SignalAspect[] = ["stop", "caution", "clear"]
50
-
51
- // A lineside signal standing just past the pocket, on the right. The post
52
- // and hood take the track's ink; the one lit aspect uses the status
53
- // vocabulary the rest of the UI already speaks (green = clear, amber =
54
- // caution, rose = stop). An empty table is a clear line, so that is the
55
- // default; pages whose empty state is a filter miss pass "caution".
56
- export function Signal({
57
- aspect,
58
- className,
59
- }: {
60
- aspect: SignalAspect
61
- className?: string
62
- }) {
63
- return (
64
- <svg
65
- aria-hidden
66
- viewBox="0 0 20 64"
67
- className={cn(
68
- "pointer-events-none absolute top-1/2 h-16 w-5 -translate-y-[calc(50%+8px)]",
69
- "left-[calc(50%+100px)] md:left-[calc(50%+150px)]",
70
- className,
71
- )}
72
- >
73
- <g className="text-foreground/35 dark:text-foreground/40">
74
- <rect x="9" y="33" width="2" height="26" rx="1" fill="currentColor" />
75
- <rect x="5" y="58" width="10" height="2" rx="1" fill="currentColor" />
76
- <rect
77
- x="4.75"
78
- y="1.75"
79
- width="10.5"
80
- height="31.5"
81
- rx="5.25"
82
- fill="none"
83
- stroke="currentColor"
84
- strokeWidth="1.5"
85
- />
86
- </g>
87
- {LAMP_ORDER.map((lamp) =>
88
- lamp === aspect ? (
89
- <g key={lamp} className={ASPECT_CLASS[lamp]} data-lamp={lamp}>
90
- <circle
91
- cx="10"
92
- cy={LAMP_Y[lamp]}
93
- r="6"
94
- fill="currentColor"
95
- opacity="0.2"
96
- className="blur-[2px]"
97
- />
98
- <circle cx="10" cy={LAMP_Y[lamp]} r="3" fill="currentColor" />
99
- </g>
100
- ) : (
101
- <circle
102
- key={lamp}
103
- cx="10"
104
- cy={LAMP_Y[lamp]}
105
- r="3"
106
- className="text-foreground/15"
107
- fill="currentColor"
108
- />
109
- ),
110
- )}
111
- </svg>
112
- )
113
- }
114
-
115
- // Empty state for tables and panels: the icon, title, optional description
116
- // and action, set in the clear pocket of a track that runs behind them,
117
- // with a signal beside the line showing the aspect.
118
- export function EmptyState({
119
- icon: Icon,
120
- title,
121
- description,
122
- action,
123
- signal = "clear",
124
- className,
125
- }: {
126
- icon?: LucideIcon
127
- title: ReactNode
128
- description?: ReactNode
129
- action?: ReactNode
130
- signal?: SignalAspect
131
- className?: string
132
- }) {
133
- return (
134
- <div
135
- className={cn(
136
- "relative flex min-h-48 items-center justify-center overflow-hidden px-4 py-8 text-center",
137
- className,
138
- )}
139
- aria-live="polite"
140
- >
141
- <Track />
142
- <Signal aspect={signal} />
143
- <div className="relative flex max-w-xs flex-col items-center gap-1.5">
144
- {Icon && (
145
- <span className="bg-muted text-muted-foreground mb-1 flex size-9 items-center justify-center rounded-md">
146
- <Icon className="size-4" />
147
- </span>
148
- )}
149
- <span className="text-sm font-medium">{title}</span>
150
- {description && (
151
- <span className="text-muted-foreground text-xs leading-relaxed">
152
- {description}
153
- </span>
154
- )}
155
- {action && <div className="mt-2">{action}</div>}
156
- </div>
157
- </div>
158
- )
159
- }
@@ -1,130 +0,0 @@
1
- import { Link, usePage } from "@inertiajs/react"
2
- import { Bookmark, ChevronRight } from "lucide-react"
3
- import { useState } from "react"
4
-
5
- import { isViewActive, samePage } from "@/components/railwatch/saved-views"
6
- import {
7
- Collapsible,
8
- CollapsibleContent,
9
- CollapsibleTrigger,
10
- } from "@/components/ui/collapsible"
11
- import {
12
- SidebarGroup,
13
- SidebarMenu,
14
- SidebarMenuButton,
15
- SidebarMenuItem,
16
- SidebarMenuSub,
17
- SidebarMenuSubButton,
18
- SidebarMenuSubItem,
19
- } from "@/components/ui/sidebar"
20
- import { ENV_PAGE_GROUPS, envPages } from "@/lib/env-pages"
21
- import { cn } from "@/lib/utils"
22
- import type { SharedProps } from "@/types"
23
-
24
- // Environment navigation in the sidebar: Overview at the top, then one
25
- // collapsible section per page group (Activity, Monitoring, Settings). The
26
- // section containing the current page starts open; the others start
27
- // closed so the list stays short.
28
- export function EnvNav() {
29
- const page = usePage<SharedProps>()
30
- const { environment } = page.props
31
- const current = page.url.split("?")[0]
32
- const [openOverride, setOpenOverride] = useState<
33
- Record<string, boolean | undefined>
34
- >({})
35
- if (!environment) return null
36
-
37
- const items = envPages(
38
- environment.application_id,
39
- environment.id,
40
- page.props.window as string,
41
- )
42
- const pinnedViews = (page.props.saved_views ?? []).filter((v) => v.pinned)
43
- const isCurrent = (href: string) => {
44
- const path = href.split("?")[0]
45
- return current === path || current.startsWith(path + "/")
46
- }
47
- const overview = items[0]
48
-
49
- return (
50
- <SidebarGroup className="p-0">
51
- <SidebarMenu>
52
- <SidebarMenuItem>
53
- <SidebarMenuButton
54
- asChild
55
- isActive={current === overview.href.split("?")[0]}
56
- tooltip={{ children: overview.title }}
57
- >
58
- <Link href={overview.href} prefetch>
59
- {overview.icon && <overview.icon />}
60
- <span>{overview.title}</span>
61
- </Link>
62
- </SidebarMenuButton>
63
- </SidebarMenuItem>
64
- {ENV_PAGE_GROUPS.map((group) => {
65
- const groupItems = items.filter(
66
- (i) => i.group === group.key && i !== overview,
67
- )
68
- const containsCurrent = groupItems.some((i) => isCurrent(i.href))
69
- const open = openOverride[group.key] ?? containsCurrent
70
- const GroupIcon = groupItems[0]?.icon
71
- return (
72
- <Collapsible
73
- key={group.key}
74
- open={open}
75
- onOpenChange={(v) =>
76
- setOpenOverride((o) => ({ ...o, [group.key]: v }))
77
- }
78
- className="group/collapsible"
79
- >
80
- <SidebarMenuItem>
81
- <CollapsibleTrigger asChild>
82
- <SidebarMenuButton
83
- tooltip={{ children: group.title }}
84
- className={cn(containsCurrent && "text-sidebar-foreground")}
85
- >
86
- {GroupIcon && <GroupIcon />}
87
- <span>{group.title}</span>
88
- <ChevronRight className="ml-auto size-3.5 opacity-60 transition-transform group-data-[state=open]/collapsible:rotate-90" />
89
- </SidebarMenuButton>
90
- </CollapsibleTrigger>
91
- <CollapsibleContent>
92
- <SidebarMenuSub className="mr-0 gap-0.5 pr-0">
93
- {groupItems.map((item) => (
94
- <SidebarMenuSubItem key={item.title}>
95
- <SidebarMenuSubButton
96
- asChild
97
- isActive={isCurrent(item.href)}
98
- >
99
- <Link href={item.href} prefetch>
100
- <span>{item.title}</span>
101
- </Link>
102
- </SidebarMenuSubButton>
103
- {pinnedViews
104
- .filter((view) => samePage(view.url, item.href))
105
- .map((view) => (
106
- <SidebarMenuSubButton
107
- key={view.id}
108
- asChild
109
- size="sm"
110
- className="pl-4"
111
- isActive={isViewActive(page.url, view.url)}
112
- >
113
- <Link href={view.url}>
114
- <Bookmark className="opacity-60" />
115
- <span>{view.name}</span>
116
- </Link>
117
- </SidebarMenuSubButton>
118
- ))}
119
- </SidebarMenuSubItem>
120
- ))}
121
- </SidebarMenuSub>
122
- </CollapsibleContent>
123
- </SidebarMenuItem>
124
- </Collapsible>
125
- )
126
- })}
127
- </SidebarMenu>
128
- </SidebarGroup>
129
- )
130
- }