railwatch 0.6.1 → 0.7.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 (345) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/app/frontend/components/app-content.tsx +26 -0
  4. data/app/frontend/components/app-logo-icon.tsx +23 -0
  5. data/app/frontend/components/app-logo.tsx +14 -0
  6. data/app/frontend/components/app-shell.tsx +38 -0
  7. data/app/frontend/components/app-sidebar-header.tsx +54 -0
  8. data/app/frontend/components/app-sidebar.tsx +94 -0
  9. data/app/frontend/components/app-wordmark.tsx +29 -0
  10. data/app/frontend/components/breadcrumbs.tsx +46 -0
  11. data/app/frontend/components/icon.tsx +16 -0
  12. data/app/frontend/components/input-error.tsx +18 -0
  13. data/app/frontend/components/nav-footer.tsx +46 -0
  14. data/app/frontend/components/nav-main.tsx +41 -0
  15. data/app/frontend/components/nav-user.tsx +50 -0
  16. data/app/frontend/components/railwatch/chart-hover.tsx +34 -0
  17. data/app/frontend/components/railwatch/chart-panel.tsx +170 -0
  18. data/app/frontend/components/railwatch/code.tsx +28 -0
  19. data/app/frontend/components/railwatch/command-palette.tsx +132 -0
  20. data/app/frontend/components/railwatch/copy-block.tsx +52 -0
  21. data/app/frontend/components/railwatch/copy-id.tsx +49 -0
  22. data/app/frontend/components/railwatch/cursor-load-more.tsx +39 -0
  23. data/app/frontend/components/railwatch/data-table.tsx +195 -0
  24. data/app/frontend/components/railwatch/digit-roll.tsx +312 -0
  25. data/app/frontend/components/railwatch/empty-state.tsx +159 -0
  26. data/app/frontend/components/railwatch/env-nav.tsx +130 -0
  27. data/app/frontend/components/railwatch/env-switcher.tsx +145 -0
  28. data/app/frontend/components/railwatch/filter-bar.tsx +153 -0
  29. data/app/frontend/components/railwatch/flamegraph.tsx +324 -0
  30. data/app/frontend/components/railwatch/frames.tsx +295 -0
  31. data/app/frontend/components/railwatch/json-viewer.tsx +159 -0
  32. data/app/frontend/components/railwatch/live-dot.tsx +78 -0
  33. data/app/frontend/components/railwatch/live-toggle.tsx +76 -0
  34. data/app/frontend/components/railwatch/metric-chart.tsx +112 -0
  35. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +89 -0
  36. data/app/frontend/components/railwatch/onboarding.tsx +91 -0
  37. data/app/frontend/components/railwatch/origin-identity.tsx +84 -0
  38. data/app/frontend/components/railwatch/page-header.tsx +42 -0
  39. data/app/frontend/components/railwatch/percentile-picker.tsx +18 -0
  40. data/app/frontend/components/railwatch/relative-time.tsx +18 -0
  41. data/app/frontend/components/railwatch/saved-views.tsx +232 -0
  42. data/app/frontend/components/railwatch/segmented.tsx +64 -0
  43. data/app/frontend/components/railwatch/series-chart.tsx +400 -0
  44. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +89 -0
  45. data/app/frontend/components/railwatch/signal-toast.tsx +121 -0
  46. data/app/frontend/components/railwatch/sort-header.tsx +37 -0
  47. data/app/frontend/components/railwatch/source-link.tsx +57 -0
  48. data/app/frontend/components/railwatch/sparkline-cell.tsx +14 -0
  49. data/app/frontend/components/railwatch/sql-block.tsx +116 -0
  50. data/app/frontend/components/railwatch/stat.tsx +173 -0
  51. data/app/frontend/components/railwatch/status-badge.tsx +79 -0
  52. data/app/frontend/components/railwatch/step-picker.tsx +57 -0
  53. data/app/frontend/components/railwatch/timeline.tsx +405 -0
  54. data/app/frontend/components/railwatch/utilisation-bar.tsx +44 -0
  55. data/app/frontend/components/railwatch/window-picker.tsx +147 -0
  56. data/app/frontend/components/ui/avatar.tsx +51 -0
  57. data/app/frontend/components/ui/badge.tsx +46 -0
  58. data/app/frontend/components/ui/breadcrumb.tsx +109 -0
  59. data/app/frontend/components/ui/button.tsx +60 -0
  60. data/app/frontend/components/ui/card.tsx +92 -0
  61. data/app/frontend/components/ui/chart.tsx +374 -0
  62. data/app/frontend/components/ui/checkbox.tsx +32 -0
  63. data/app/frontend/components/ui/collapsible.tsx +31 -0
  64. data/app/frontend/components/ui/command.tsx +182 -0
  65. data/app/frontend/components/ui/dialog.tsx +158 -0
  66. data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
  67. data/app/frontend/components/ui/hover-card.tsx +42 -0
  68. data/app/frontend/components/ui/input.tsx +21 -0
  69. data/app/frontend/components/ui/label.tsx +24 -0
  70. data/app/frontend/components/ui/popover.tsx +87 -0
  71. data/app/frontend/components/ui/select.tsx +185 -0
  72. data/app/frontend/components/ui/separator.tsx +28 -0
  73. data/app/frontend/components/ui/sheet.tsx +137 -0
  74. data/app/frontend/components/ui/sidebar.tsx +726 -0
  75. data/app/frontend/components/ui/skeleton.tsx +13 -0
  76. data/app/frontend/components/ui/sonner.tsx +41 -0
  77. data/app/frontend/components/ui/table.tsx +114 -0
  78. data/app/frontend/components/ui/tabs.tsx +91 -0
  79. data/app/frontend/components/ui/textarea.tsx +18 -0
  80. data/app/frontend/components/ui/tooltip.tsx +61 -0
  81. data/app/frontend/components/user-info.tsx +32 -0
  82. data/app/frontend/components/user-menu-content.tsx +94 -0
  83. data/app/frontend/entrypoints/application.css +244 -0
  84. data/app/frontend/entrypoints/inertia.tsx +95 -0
  85. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  86. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  87. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  88. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  89. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  90. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  91. data/app/frontend/hooks/use-appearance.tsx +67 -0
  92. data/app/frontend/hooks/use-clipboard.ts +32 -0
  93. data/app/frontend/hooks/use-flash.tsx +23 -0
  94. data/app/frontend/hooks/use-hotkeys.ts +63 -0
  95. data/app/frontend/hooks/use-initials.tsx +15 -0
  96. data/app/frontend/hooks/use-live.ts +179 -0
  97. data/app/frontend/hooks/use-mobile-navigation.ts +8 -0
  98. data/app/frontend/hooks/use-mobile.ts +21 -0
  99. data/app/frontend/hooks/use-percentile.ts +24 -0
  100. data/app/frontend/hooks/use-reduced-motion.ts +25 -0
  101. data/app/frontend/hooks/use-row-nav.ts +106 -0
  102. data/app/frontend/hooks/use-step.ts +27 -0
  103. data/app/frontend/hooks/use-ticker.ts +12 -0
  104. data/app/frontend/hooks/use-window.ts +58 -0
  105. data/app/frontend/layouts/app/app-sidebar-layout.tsx +107 -0
  106. data/app/frontend/layouts/app-layout.tsx +21 -0
  107. data/app/frontend/layouts/env-layout.tsx +53 -0
  108. data/app/frontend/layouts/persistent-layout.tsx +18 -0
  109. data/app/frontend/lib/cable.ts +10 -0
  110. data/app/frontend/lib/env-pages.ts +236 -0
  111. data/app/frontend/lib/execution-path.ts +55 -0
  112. data/app/frontend/lib/explain.ts +50 -0
  113. data/app/frontend/lib/filter.ts +25 -0
  114. data/app/frontend/lib/flamegraph.ts +155 -0
  115. data/app/frontend/lib/format.ts +71 -0
  116. data/app/frontend/lib/keyboard.ts +18 -0
  117. data/app/frontend/lib/source-link.ts +217 -0
  118. data/app/frontend/lib/utils.ts +6 -0
  119. data/app/frontend/pages/alerts/index.tsx +171 -0
  120. data/app/frontend/pages/broadcasts/index.tsx +171 -0
  121. data/app/frontend/pages/cache_events/index.tsx +127 -0
  122. data/app/frontend/pages/commands/index.tsx +143 -0
  123. data/app/frontend/pages/deploys/index.tsx +94 -0
  124. data/app/frontend/pages/deploys/show.tsx +309 -0
  125. data/app/frontend/pages/deprecations/index.tsx +176 -0
  126. data/app/frontend/pages/exceptions/index.tsx +271 -0
  127. data/app/frontend/pages/executions/show.tsx +821 -0
  128. data/app/frontend/pages/issues/index.tsx +399 -0
  129. data/app/frontend/pages/issues/show.tsx +1367 -0
  130. data/app/frontend/pages/jobs/index.tsx +381 -0
  131. data/app/frontend/pages/jobs/klass.tsx +215 -0
  132. data/app/frontend/pages/llm_calls/index.tsx +664 -0
  133. data/app/frontend/pages/logs/index.tsx +303 -0
  134. data/app/frontend/pages/mails/index.tsx +218 -0
  135. data/app/frontend/pages/notifications/index.tsx +161 -0
  136. data/app/frontend/pages/outgoing_requests/index.tsx +309 -0
  137. data/app/frontend/pages/overview/show.tsx +333 -0
  138. data/app/frontend/pages/people/index.tsx +102 -0
  139. data/app/frontend/pages/people/show.tsx +149 -0
  140. data/app/frontend/pages/processes/index.tsx +335 -0
  141. data/app/frontend/pages/profiles/index.tsx +110 -0
  142. data/app/frontend/pages/profiles/show.tsx +105 -0
  143. data/app/frontend/pages/queries/index.tsx +492 -0
  144. data/app/frontend/pages/queries/show.tsx +276 -0
  145. data/app/frontend/pages/releases/index.tsx +190 -0
  146. data/app/frontend/pages/releases/release-health.tsx +68 -0
  147. data/app/frontend/pages/releases/release-path.ts +25 -0
  148. data/app/frontend/pages/releases/show.tsx +262 -0
  149. data/app/frontend/pages/requests/index.tsx +235 -0
  150. data/app/frontend/pages/requests/route.tsx +159 -0
  151. data/app/frontend/pages/scheduled_tasks/index.tsx +220 -0
  152. data/app/frontend/pages/spans/index.tsx +229 -0
  153. data/app/frontend/pages/spans/show.tsx +220 -0
  154. data/app/frontend/pages/storage_ops/index.tsx +150 -0
  155. data/app/frontend/pages/tenants/index.tsx +236 -0
  156. data/app/frontend/pages/tenants/show.tsx +477 -0
  157. data/app/frontend/pages/tenants/tenant-path.ts +26 -0
  158. data/app/frontend/pages/thresholds/index.tsx +414 -0
  159. data/app/frontend/pages/traces/show.tsx +208 -0
  160. data/app/frontend/pages/transactions/index.tsx +220 -0
  161. data/app/frontend/pages/view_renders/index.tsx +241 -0
  162. data/app/frontend/pages/visits/index.tsx +321 -0
  163. data/app/frontend/public/icon.png +0 -0
  164. data/app/frontend/public/icon.svg +5 -0
  165. data/app/frontend/routes/index.d.ts +2136 -0
  166. data/app/frontend/routes/index.js +2112 -0
  167. data/app/frontend/types/globals.d.ts +9 -0
  168. data/app/frontend/types/index.ts +288 -0
  169. data/app/frontend/types/vite-env.d.ts +9 -0
  170. data/lib/railwatch/version.rb +1 -1
  171. data/lib/railwatch.rb +8 -3
  172. data/public/railwatch/assets/app-layout-LvxN9xSS.js +41 -0
  173. data/public/railwatch/assets/application-Cc5v_Vk9.css +1 -0
  174. data/public/railwatch/assets/{arrow-up-CLQ-7heQ.js → arrow-up-DG01k9AL.js} +1 -1
  175. data/public/railwatch/assets/{braces-rSYydpLY.js → braces-C3w7pfxS.js} +1 -1
  176. data/public/railwatch/assets/{card-DPjFKfen.js → card-Dpwlr0uY.js} +1 -1
  177. data/public/railwatch/assets/{chart-DWh7l8yM.js → chart-DJHv3R7W.js} +1 -1
  178. data/public/railwatch/assets/{chart-hover-CfoZUY4J.js → chart-hover-CEH-DtXO.js} +1 -1
  179. data/public/railwatch/assets/{chart-panel-CC49WTQL.js → chart-panel-CUdIzJvk.js} +1 -1
  180. data/public/railwatch/assets/code-BRZprwun.js +1 -0
  181. data/public/railwatch/assets/copy-id-CwhGGiwF.js +1 -0
  182. data/public/railwatch/assets/cursor-load-more-uF1_955Q.js +1 -0
  183. data/public/railwatch/assets/data-table-DYi-N0hm.js +1 -0
  184. data/public/railwatch/assets/env-layout-HdcOaPPX.js +1 -0
  185. data/public/railwatch/assets/{execution-path-FAuIzOXB.js → execution-path-CElXXFO_.js} +1 -1
  186. data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
  187. data/public/railwatch/assets/{flamegraph-EaGkP2NT.js → flamegraph-SKD9yxW9.js} +2 -2
  188. data/public/railwatch/assets/frames-CUpyJQDy.js +1 -0
  189. data/public/railwatch/assets/index-2W-ppwap.js +1 -0
  190. data/public/railwatch/assets/index-74ocuU5d.js +1 -0
  191. data/public/railwatch/assets/{index-C8Cggnbw.js → index-B8NA62Lz.js} +2 -2
  192. data/public/railwatch/assets/index-BBCEkLqp.js +1 -0
  193. data/public/railwatch/assets/index-BDcglJzX.js +1 -0
  194. data/public/railwatch/assets/index-B_KFb9LS.js +1 -0
  195. data/public/railwatch/assets/index-BqK_VKom.js +1 -0
  196. data/public/railwatch/assets/index-BxCXBHP2.js +1 -0
  197. data/public/railwatch/assets/index-C1jc1OoH.js +1 -0
  198. data/public/railwatch/assets/index-C7zREldp.js +1 -0
  199. data/public/railwatch/assets/index-CEetm4oC.js +1 -0
  200. data/public/railwatch/assets/index-CIaZkokF.js +1 -0
  201. data/public/railwatch/assets/index-CNH85TZp.js +1 -0
  202. data/public/railwatch/assets/index-Ce4SB5AB.js +1 -0
  203. data/public/railwatch/assets/index-CiRMSYMn.js +1 -0
  204. data/public/railwatch/assets/index-CjxXgOs-.js +1 -0
  205. data/public/railwatch/assets/index-ClL-PYEV.js +1 -0
  206. data/public/railwatch/assets/index-CteZ6fop.js +1 -0
  207. data/public/railwatch/assets/index-CvqfEExO.js +1 -0
  208. data/public/railwatch/assets/index-CxTLT-Qh.js +1 -0
  209. data/public/railwatch/assets/index-DJd3OUSd.js +1 -0
  210. data/public/railwatch/assets/index-DPD-rLp6.js +1 -0
  211. data/public/railwatch/assets/index-DYK_dps4.js +1 -0
  212. data/public/railwatch/assets/index-DrayARrI.js +1 -0
  213. data/public/railwatch/assets/index-HRXGir-0.js +1 -0
  214. data/public/railwatch/assets/index-UJOUALr_.js +1 -0
  215. data/public/railwatch/assets/index-d-3J7gQ5.js +1 -0
  216. data/public/railwatch/assets/index-oQrvBpOm.js +1 -0
  217. data/public/railwatch/assets/inertia-FoqYZAk8.js +97 -0
  218. data/public/railwatch/assets/json-viewer-CxqMojEm.js +1 -0
  219. data/public/railwatch/assets/klass-DEF-Vat5.js +1 -0
  220. data/public/railwatch/assets/origin-identity-Y6gvfQVR.js +1 -0
  221. data/public/railwatch/assets/{percentile-picker-CeQgllxD.js → percentile-picker-E2aUdLxx.js} +1 -1
  222. data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
  223. data/public/railwatch/assets/{release-health-uP-GF3_V.js → release-health-C8WSCDKD.js} +1 -1
  224. data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
  225. data/public/railwatch/assets/{segmented-BeIe4uqk.js → segmented-BWee8uc9.js} +1 -1
  226. data/public/railwatch/assets/select-G7rNBzv0.js +1 -0
  227. data/public/railwatch/assets/series-chart-C5nXIUc5.js +1 -0
  228. data/public/railwatch/assets/show-BEWisLQw.js +1 -0
  229. data/public/railwatch/assets/show-BEa9PGu1.js +2 -0
  230. data/public/railwatch/assets/{show-YsNpqbcI.js → show-BiT_Aloc.js} +3 -3
  231. data/public/railwatch/assets/show-BpCO49d3.js +1 -0
  232. data/public/railwatch/assets/show-CH7yxt7R.js +1 -0
  233. data/public/railwatch/assets/show-CTO8Lvd2.js +1 -0
  234. data/public/railwatch/assets/show-DnYsDMUt.js +2 -0
  235. data/public/railwatch/assets/show-DyzHM527.js +1 -0
  236. data/public/railwatch/assets/show-Nfpxs8su.js +1 -0
  237. data/public/railwatch/assets/show-ddjUMo9G.js +1 -0
  238. data/public/railwatch/assets/show-ksjbSzDH.js +1 -0
  239. data/public/railwatch/assets/{sort-header-mEJUM3Av.js → sort-header-uOq5VlrB.js} +1 -1
  240. data/public/railwatch/assets/{sparkline-cell-Dwg7awwh.js → sparkline-cell-DgGij0cu.js} +1 -1
  241. data/public/railwatch/assets/stat-BTzNKTm2.js +1 -0
  242. data/public/railwatch/assets/status-badge-BDkOXa-C.js +1 -0
  243. data/public/railwatch/assets/{tenant-path-CQoP-BeF.js → tenant-path-DU0ezKSU.js} +1 -1
  244. data/public/railwatch/assets/{use-clipboard-DNefo-ky.js → use-clipboard-DoENmlWY.js} +1 -1
  245. data/public/railwatch/assets/{use-live-DMTuhKfB.js → use-live-CE_fN5_6.js} +1 -1
  246. data/public/railwatch/manifest.json +612 -1591
  247. metadata +242 -117
  248. data/public/railwatch/assets/app-layout-Zc0v-hYh.js +0 -1
  249. data/public/railwatch/assets/app-wordmark-BA_60AVb.js +0 -1
  250. data/public/railwatch/assets/appearance-CcfP9tZ7.js +0 -1
  251. data/public/railwatch/assets/application-BrN3Sz94.css +0 -1
  252. data/public/railwatch/assets/auth-layout-C05gEBIQ.js +0 -1
  253. data/public/railwatch/assets/badge-DRae8XwK.js +0 -1
  254. data/public/railwatch/assets/checkbox-DPkLUiwM.js +0 -1
  255. data/public/railwatch/assets/code-CLmYS6FU.js +0 -1
  256. data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
  257. data/public/railwatch/assets/copy-block-CGcXxp8J.js +0 -1
  258. data/public/railwatch/assets/copy-id-vBYHQwxg.js +0 -1
  259. data/public/railwatch/assets/cursor-load-more-Ddwxe5dZ.js +0 -1
  260. data/public/railwatch/assets/data-table-CzKTEE-O.js +0 -1
  261. data/public/railwatch/assets/edit-B1kmWkzd.js +0 -8
  262. data/public/railwatch/assets/edit-D9cx4pbG.js +0 -1
  263. data/public/railwatch/assets/edit-yv6j9p-V.js +0 -1
  264. data/public/railwatch/assets/empty-state-CW4wclK_.js +0 -1
  265. data/public/railwatch/assets/env-layout-Kz7wks1x.js +0 -1
  266. data/public/railwatch/assets/filter-bar-CJDjWFib.js +0 -1
  267. data/public/railwatch/assets/frames-zZuIaNH9.js +0 -1
  268. data/public/railwatch/assets/google-sign-in-button-2_zgbgVy.js +0 -1
  269. data/public/railwatch/assets/index-B49SWz7K.js +0 -1
  270. data/public/railwatch/assets/index-BHlY4wKe.js +0 -1
  271. data/public/railwatch/assets/index-BZpPtyFY.js +0 -1
  272. data/public/railwatch/assets/index-BeVK6jCL.js +0 -1
  273. data/public/railwatch/assets/index-BfbSo01U.js +0 -1
  274. data/public/railwatch/assets/index-BfgncAv6.js +0 -1
  275. data/public/railwatch/assets/index-BhNszK1k.js +0 -1
  276. data/public/railwatch/assets/index-Bu01uWvw.js +0 -1
  277. data/public/railwatch/assets/index-C1s_hK3p.js +0 -1
  278. data/public/railwatch/assets/index-CBip6V4z.js +0 -1
  279. data/public/railwatch/assets/index-CMJGss5R.js +0 -1
  280. data/public/railwatch/assets/index-CRo3yK20.js +0 -1
  281. data/public/railwatch/assets/index-CWB_p2J8.js +0 -1
  282. data/public/railwatch/assets/index-CWHpndGc.js +0 -1
  283. data/public/railwatch/assets/index-Ca_S4Sc3.js +0 -1
  284. data/public/railwatch/assets/index-CanPDDOa.js +0 -1
  285. data/public/railwatch/assets/index-Cie90Yat.js +0 -1
  286. data/public/railwatch/assets/index-CiepQ_pR.js +0 -1
  287. data/public/railwatch/assets/index-Cm1uCIGN.js +0 -1
  288. data/public/railwatch/assets/index-CzitnnSC.js +0 -1
  289. data/public/railwatch/assets/index-DEUfClv3.js +0 -1
  290. data/public/railwatch/assets/index-DJKwo-mI.js +0 -1
  291. data/public/railwatch/assets/index-DK6y0YHp.js +0 -1
  292. data/public/railwatch/assets/index-DMNPpLH9.js +0 -1
  293. data/public/railwatch/assets/index-DZO1mSfX.js +0 -1
  294. data/public/railwatch/assets/index-Db5wj3M5.js +0 -1
  295. data/public/railwatch/assets/index-Dcy5WktB.js +0 -1
  296. data/public/railwatch/assets/index-DvG0-7Lx.js +0 -1
  297. data/public/railwatch/assets/index-Dvj1wuka.js +0 -1
  298. data/public/railwatch/assets/index-KyZX46qX.js +0 -1
  299. data/public/railwatch/assets/index-Ze-KP-sl.js +0 -1
  300. data/public/railwatch/assets/index-mhRbWLBM.js +0 -1
  301. data/public/railwatch/assets/index-x099JL5f.js +0 -1
  302. data/public/railwatch/assets/index-x28zb_nf.js +0 -1
  303. data/public/railwatch/assets/inertia-Cuyz2ZHO.js +0 -97
  304. data/public/railwatch/assets/input-error-hog6gGxg.js +0 -1
  305. data/public/railwatch/assets/json-viewer-DH2W9HXf.js +0 -1
  306. data/public/railwatch/assets/klass-DHbDelLk.js +0 -1
  307. data/public/railwatch/assets/label-DdCBgiUn.js +0 -1
  308. data/public/railwatch/assets/layout-Cueyl7c5.js +0 -1
  309. data/public/railwatch/assets/live-dot-BZgYTYdt.js +0 -41
  310. data/public/railwatch/assets/nav-BSSGObDZ.js +0 -1
  311. data/public/railwatch/assets/new-B8FSb8Bl.js +0 -1
  312. data/public/railwatch/assets/new-C39_v2Ll.js +0 -1
  313. data/public/railwatch/assets/new-DVOPwaC5.js +0 -1
  314. data/public/railwatch/assets/new-Dd40nvJR.js +0 -1
  315. data/public/railwatch/assets/new-SxnYe1SE.js +0 -1
  316. data/public/railwatch/assets/new-eP5vKD3Y.js +0 -1
  317. data/public/railwatch/assets/onboarding-CUpZl5KB.js +0 -1
  318. data/public/railwatch/assets/origin-identity-BYt2uiuo.js +0 -1
  319. data/public/railwatch/assets/relative-time-D5UbF4oO.js +0 -1
  320. data/public/railwatch/assets/route-DQAY8JEr.js +0 -1
  321. data/public/railwatch/assets/select-2R16457Z.js +0 -1
  322. data/public/railwatch/assets/separator-D5I0UCB5.js +0 -1
  323. data/public/railwatch/assets/series-chart-gnrzhmM6.js +0 -1
  324. data/public/railwatch/assets/show-2BkeNRUC.js +0 -2
  325. data/public/railwatch/assets/show-B0X1hRQH.js +0 -1
  326. data/public/railwatch/assets/show-BKUV5l5q.js +0 -1
  327. data/public/railwatch/assets/show-BQJD_CsF.js +0 -6
  328. data/public/railwatch/assets/show-BhmD6Sxp.js +0 -1
  329. data/public/railwatch/assets/show-C3KcnVo2.js +0 -1
  330. data/public/railwatch/assets/show-C95cHd06.js +0 -2
  331. data/public/railwatch/assets/show-CdB4uVQS.js +0 -1
  332. data/public/railwatch/assets/show-CoCqcVIp.js +0 -1
  333. data/public/railwatch/assets/show-D2LqjEsU.js +0 -1
  334. data/public/railwatch/assets/show-DNpnKyTh.js +0 -1
  335. data/public/railwatch/assets/show-DOlTxbig.js +0 -1
  336. data/public/railwatch/assets/show-DQCkttL8.js +0 -1
  337. data/public/railwatch/assets/show-DcxesipC.js +0 -1
  338. data/public/railwatch/assets/show-IGJoc_X0.js +0 -1
  339. data/public/railwatch/assets/show-qNV6H8SH.js +0 -2
  340. data/public/railwatch/assets/stat-ZtxGU8lE.js +0 -1
  341. data/public/railwatch/assets/status-badge-CH5P-Xkj.js +0 -1
  342. data/public/railwatch/assets/text-link-DHJ8BXx5.js +0 -1
  343. data/public/railwatch/assets/textarea-BeQtQyl5.js +0 -1
  344. data/public/railwatch/assets/timeline-CaKQVu48.js +0 -1
  345. data/public/railwatch/assets/transition-ksDpqhKJ.js +0 -5
@@ -0,0 +1,414 @@
1
+ import { Form, router, usePage } from "@inertiajs/react"
2
+ import { Bell } from "lucide-react"
3
+ import { useState } from "react"
4
+
5
+ import InputError from "@/components/input-error"
6
+ import { DataTable } from "@/components/railwatch/data-table"
7
+ import { EmptyState } from "@/components/railwatch/empty-state"
8
+ import { PageHeader } from "@/components/railwatch/page-header"
9
+ import { RelativeTime } from "@/components/railwatch/relative-time"
10
+ import { Button } from "@/components/ui/button"
11
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
12
+ import { Input } from "@/components/ui/input"
13
+ import { Label } from "@/components/ui/label"
14
+ import EnvLayout from "@/layouts/env-layout"
15
+ import * as R from "@/routes"
16
+ import type { SharedProps } from "@/types"
17
+
18
+ interface Threshold {
19
+ id: number
20
+ target_kind: string
21
+ target: string
22
+ metric: string
23
+ limit: number
24
+ window_minutes: number
25
+ description: string
26
+ }
27
+ interface AnomalyRule {
28
+ id: number
29
+ target_kind: string
30
+ target: string
31
+ metric: string
32
+ deviation: number
33
+ window_minutes: number
34
+ baseline_days: number
35
+ enabled: boolean
36
+ last_fired_at: string | null
37
+ description: string
38
+ }
39
+ interface Props {
40
+ thresholds: Threshold[]
41
+ routes: string[]
42
+ jobs: string[]
43
+ kinds: string[]
44
+ metrics: string[]
45
+ metrics_for_kind: Record<string, string[]>
46
+ units: Record<string, string>
47
+ llm_models: string[]
48
+ anomaly_rules: AnomalyRule[]
49
+ anomaly_target_kinds: string[]
50
+ anomaly_metrics: string[]
51
+ anomaly_metrics_for_kind: Record<string, string[]>
52
+ }
53
+
54
+ export default function Thresholds(p: Props) {
55
+ const { environment } = usePage<SharedProps>().props
56
+ // Spend and tokens only exist on llm_calls, so the metric list narrows
57
+ // with the kind rather than offering a rule the server would reject.
58
+ const [kind, setKind] = useState(p.kinds[0])
59
+ const [metric, setMetric] = useState(p.metrics[0])
60
+ const [anomalyKind, setAnomalyKind] = useState(p.anomaly_target_kinds[0])
61
+ const [anomalyMetric, setAnomalyMetric] = useState(p.anomaly_metrics[0])
62
+ const anomalyAvailable =
63
+ p.anomaly_metrics_for_kind[anomalyKind] ?? p.anomaly_metrics
64
+ const available = p.metrics_for_kind[kind] ?? p.metrics
65
+ const unit = p.units[metric] ?? "ms"
66
+ const limitLabel =
67
+ unit === "$"
68
+ ? "Limit (US$)"
69
+ : unit === "%"
70
+ ? "Limit (%)"
71
+ : unit === ""
72
+ ? "Limit (count)"
73
+ : "Limit (ms)"
74
+ const a = environment!.application_id
75
+ const e = environment!.id
76
+ return (
77
+ <EnvLayout title="Thresholds">
78
+ <PageHeader
79
+ withWindow={false}
80
+ title="Thresholds"
81
+ description="Performance rules. When a route, job, or query crosses one, Railwatch opens a performance issue and alerts the rules subscribed to thresholds."
82
+ />
83
+ <Card>
84
+ <CardHeader>
85
+ <CardTitle>Add a threshold</CardTitle>
86
+ </CardHeader>
87
+ <CardContent>
88
+ <Form
89
+ method="post"
90
+ action={R.applicationEnvironmentThresholdsPath(a, e)}
91
+ className="grid gap-3 md:grid-cols-6"
92
+ >
93
+ {({ errors, processing }) => (
94
+ <>
95
+ <div className="grid gap-1">
96
+ <Label htmlFor="target_kind">Kind</Label>
97
+ <select
98
+ id="target_kind"
99
+ name="target_kind"
100
+ value={kind}
101
+ onChange={(e) => {
102
+ setKind(e.target.value)
103
+ const next =
104
+ p.metrics_for_kind[e.target.value] ?? p.metrics
105
+ if (!next.includes(metric)) setMetric(next[0])
106
+ }}
107
+ className="border-input h-9 rounded-md border bg-transparent px-2 text-sm"
108
+ >
109
+ {p.kinds.map((k) => (
110
+ <option key={k} value={k}>
111
+ {k}
112
+ </option>
113
+ ))}
114
+ </select>
115
+ </div>
116
+ <div className="grid gap-1 md:col-span-2">
117
+ <Label htmlFor="target">Target</Label>
118
+ <Input
119
+ id="target"
120
+ name="target"
121
+ defaultValue="*"
122
+ list="targets"
123
+ placeholder="* for all, or a route / job name"
124
+ />
125
+ <datalist id="targets">
126
+ <option value="*" />
127
+ <option value="unmatched" />
128
+ {p.routes.map((r) => (
129
+ <option key={r} value={r} />
130
+ ))}
131
+ {p.jobs.map((j) => (
132
+ <option key={j} value={j} />
133
+ ))}
134
+ {p.llm_models.map((m) => (
135
+ <option key={m} value={m} />
136
+ ))}
137
+ </datalist>
138
+ <InputError messages={errors.target} />
139
+ </div>
140
+ <div className="grid gap-1">
141
+ <Label htmlFor="metric">Metric</Label>
142
+ <select
143
+ id="metric"
144
+ name="metric"
145
+ className="border-input h-9 rounded-md border bg-transparent px-2 text-sm"
146
+ value={metric}
147
+ onChange={(e) => setMetric(e.target.value)}
148
+ >
149
+ {available.map((m) => (
150
+ <option key={m} value={m}>
151
+ {m}
152
+ </option>
153
+ ))}
154
+ </select>
155
+ </div>
156
+ <div className="grid gap-1">
157
+ <Label htmlFor="limit">{limitLabel}</Label>
158
+ <Input
159
+ id="limit"
160
+ name="limit"
161
+ type="number"
162
+ step="any"
163
+ defaultValue={2000}
164
+ />
165
+ <InputError messages={errors.limit} />
166
+ </div>
167
+ <div className="grid gap-1">
168
+ <Label htmlFor="window_minutes">Window (min)</Label>
169
+ <Input
170
+ id="window_minutes"
171
+ name="window_minutes"
172
+ type="number"
173
+ defaultValue={5}
174
+ />
175
+ </div>
176
+ <div className="md:col-span-6">
177
+ <Button disabled={processing}>Add threshold</Button>
178
+ </div>
179
+ </>
180
+ )}
181
+ </Form>
182
+ </CardContent>
183
+ </Card>
184
+ <DataTable
185
+ rows={p.thresholds}
186
+ rowKey={(t) => t.id}
187
+ empty={
188
+ <EmptyState
189
+ icon={Bell}
190
+ title="No thresholds yet"
191
+ description="Start with requests * p95 2000ms to get alerted on regressions."
192
+ />
193
+ }
194
+ columns={[
195
+ {
196
+ key: "d",
197
+ header: "Rule",
198
+ cell: (t) => <span className="text-sm">{t.description}</span>,
199
+ },
200
+ {
201
+ key: "x",
202
+ header: "",
203
+ align: "right",
204
+ cell: (t) => (
205
+ <Button
206
+ size="sm"
207
+ variant="ghost"
208
+ onClick={() =>
209
+ router.delete(
210
+ R.applicationEnvironmentThresholdPath(a, e, t.id),
211
+ )
212
+ }
213
+ >
214
+ Remove
215
+ </Button>
216
+ ),
217
+ },
218
+ ]}
219
+ />
220
+ <div>
221
+ <h2 className="text-base font-semibold">Anomaly rules</h2>
222
+ <p className="text-muted-foreground text-sm">
223
+ A threshold needs a number you already know. An anomaly rule learns
224
+ one: it compares the last few minutes against the same clock window on
225
+ each of the previous days and opens an issue when today sits more than
226
+ the given number of standard deviations above that baseline.
227
+ </p>
228
+ </div>
229
+ <Card>
230
+ <CardHeader>
231
+ <CardTitle>Add an anomaly rule</CardTitle>
232
+ </CardHeader>
233
+ <CardContent>
234
+ <Form
235
+ method="post"
236
+ action={R.applicationEnvironmentAnomalyRulesPath(a, e)}
237
+ className="grid gap-3 md:grid-cols-6"
238
+ >
239
+ {({ errors, processing }) => (
240
+ <>
241
+ <div className="grid gap-1">
242
+ <Label htmlFor="anomaly_target_kind">Kind</Label>
243
+ <select
244
+ id="anomaly_target_kind"
245
+ name="target_kind"
246
+ className="border-input h-9 rounded-md border bg-transparent px-2 text-sm"
247
+ value={anomalyKind}
248
+ onChange={(e) => {
249
+ setAnomalyKind(e.target.value)
250
+ const next =
251
+ p.anomaly_metrics_for_kind[e.target.value] ??
252
+ p.anomaly_metrics
253
+ if (!next.includes(anomalyMetric))
254
+ setAnomalyMetric(next[0])
255
+ }}
256
+ >
257
+ {p.anomaly_target_kinds.map((k) => (
258
+ <option key={k} value={k}>
259
+ {k}
260
+ </option>
261
+ ))}
262
+ </select>
263
+ </div>
264
+ <div className="grid gap-1 md:col-span-2">
265
+ <Label htmlFor="anomaly_target">Target</Label>
266
+ <Input
267
+ id="anomaly_target"
268
+ name="target"
269
+ defaultValue="*"
270
+ list="targets"
271
+ placeholder="* for all, or a route / job name"
272
+ />
273
+ <InputError messages={errors.target} />
274
+ </div>
275
+ <div className="grid gap-1">
276
+ <Label htmlFor="anomaly_metric">Metric</Label>
277
+ <select
278
+ id="anomaly_metric"
279
+ name="metric"
280
+ className="border-input h-9 rounded-md border bg-transparent px-2 text-sm"
281
+ value={anomalyMetric}
282
+ onChange={(e) => setAnomalyMetric(e.target.value)}
283
+ >
284
+ {anomalyAvailable.map((m) => (
285
+ <option key={m} value={m}>
286
+ {m}
287
+ </option>
288
+ ))}
289
+ </select>
290
+ </div>
291
+ <div className="grid gap-1">
292
+ <Label htmlFor="deviation">Deviation (σ)</Label>
293
+ <Input
294
+ id="deviation"
295
+ name="deviation"
296
+ type="number"
297
+ step="0.5"
298
+ min="0.5"
299
+ defaultValue={3}
300
+ />
301
+ <InputError messages={errors.deviation} />
302
+ </div>
303
+ <div className="grid gap-1">
304
+ <Label htmlFor="anomaly_window_minutes">Window (min)</Label>
305
+ <Input
306
+ id="anomaly_window_minutes"
307
+ name="window_minutes"
308
+ type="number"
309
+ defaultValue={15}
310
+ />
311
+ </div>
312
+ <div className="grid gap-1">
313
+ <Label htmlFor="baseline_days">Baseline (days)</Label>
314
+ <Input
315
+ id="baseline_days"
316
+ name="baseline_days"
317
+ type="number"
318
+ defaultValue={7}
319
+ />
320
+ <InputError messages={errors.baseline_days} />
321
+ </div>
322
+ <div className="grid gap-1">
323
+ <Label htmlFor="enabled">Status</Label>
324
+ <select
325
+ id="enabled"
326
+ name="enabled"
327
+ className="border-input h-9 rounded-md border bg-transparent px-2 text-sm"
328
+ >
329
+ <option value="true">enabled</option>
330
+ <option value="false">disabled</option>
331
+ </select>
332
+ </div>
333
+ <div className="md:col-span-6">
334
+ <Button disabled={processing}>Add anomaly rule</Button>
335
+ </div>
336
+ </>
337
+ )}
338
+ </Form>
339
+ </CardContent>
340
+ </Card>
341
+ <DataTable
342
+ rows={p.anomaly_rules}
343
+ rowKey={(r) => r.id}
344
+ empty={
345
+ <EmptyState
346
+ icon={Bell}
347
+ title="No anomaly rules yet"
348
+ description="Start with requests * p95 at 3σ over a 7-day baseline."
349
+ />
350
+ }
351
+ columns={[
352
+ {
353
+ key: "d",
354
+ header: "Rule",
355
+ cell: (r) => (
356
+ <span className="text-sm">
357
+ {r.description}
358
+ {r.enabled ? null : (
359
+ <span className="text-muted-foreground label-caps ml-2">
360
+ disabled
361
+ </span>
362
+ )}
363
+ </span>
364
+ ),
365
+ },
366
+ {
367
+ key: "f",
368
+ header: "Last fired",
369
+ hideOnMobile: true,
370
+ cell: (r) => (
371
+ <RelativeTime
372
+ iso={r.last_fired_at}
373
+ className="text-muted-foreground font-mono text-xs"
374
+ />
375
+ ),
376
+ },
377
+ {
378
+ key: "x",
379
+ header: "",
380
+ align: "right",
381
+ cell: (r) => (
382
+ <span className="flex justify-end gap-1">
383
+ <Button
384
+ size="sm"
385
+ variant="ghost"
386
+ onClick={() =>
387
+ router.patch(
388
+ R.applicationEnvironmentAnomalyRulePath(a, e, r.id),
389
+ { enabled: !r.enabled },
390
+ )
391
+ }
392
+ >
393
+ {r.enabled ? "Disable" : "Enable"}
394
+ </Button>
395
+ <Button
396
+ size="sm"
397
+ variant="ghost"
398
+ className="text-destructive"
399
+ onClick={() =>
400
+ router.delete(
401
+ R.applicationEnvironmentAnomalyRulePath(a, e, r.id),
402
+ )
403
+ }
404
+ >
405
+ Remove
406
+ </Button>
407
+ </span>
408
+ ),
409
+ },
410
+ ]}
411
+ />
412
+ </EnvLayout>
413
+ )
414
+ }
@@ -0,0 +1,208 @@
1
+ import { Link, usePage } from "@inertiajs/react"
2
+
3
+ import { CopyId } from "@/components/railwatch/copy-id"
4
+ import { EmptyState } from "@/components/railwatch/empty-state"
5
+ import { PageHeader } from "@/components/railwatch/page-header"
6
+ import { Stat, StatStrip } from "@/components/railwatch/stat"
7
+ import { KindBadge } from "@/components/railwatch/status-badge"
8
+ import EnvLayout from "@/layouts/env-layout"
9
+ import { executionPath } from "@/lib/execution-path"
10
+ import { count, ms, statusTone } from "@/lib/format"
11
+ import { cn } from "@/lib/utils"
12
+ import type { SharedProps } from "@/types"
13
+
14
+ interface Outgoing {
15
+ id: number
16
+ method: string | null
17
+ url: string
18
+ status_code: number | null
19
+ duration: number | null
20
+ offset: number
21
+ }
22
+ interface TraceExecution {
23
+ execution_id: string
24
+ kind: string
25
+ name: string
26
+ status: number | null
27
+ outcome: string | null
28
+ parent_id: string | null
29
+ server: string | null
30
+ occurred_at: string
31
+ duration: number
32
+ offset: number
33
+ outgoing: Outgoing[]
34
+ }
35
+ interface Node {
36
+ execution: TraceExecution
37
+ children: Node[]
38
+ }
39
+ interface Props {
40
+ trace_id: string
41
+ roots: Node[]
42
+ span_count: number
43
+ services: string[]
44
+ duration: number
45
+ }
46
+
47
+ interface Row {
48
+ key: string
49
+ depth: number
50
+ kind: string
51
+ label: string
52
+ server: string | null
53
+ offset: number
54
+ duration: number
55
+ href: string | null
56
+ failed: boolean
57
+ }
58
+
59
+ const kindLabels: Record<string, string> = {
60
+ request: "REQ",
61
+ job_attempt: "JOB",
62
+ scheduled_task: "TASK",
63
+ command: "CMD",
64
+ outgoing_request: "HTTP",
65
+ }
66
+
67
+ // Depth-first walk of the trace: each execution, then the HTTP calls it made
68
+ // (one level in, and not clickable — the hop they led to is a row of its own
69
+ // if that service reports to Railwatch), then its child executions.
70
+ function flatten(nodes: Node[], a: number, e: number, depth = 0): Row[] {
71
+ return nodes.flatMap((node) => {
72
+ const x = node.execution
73
+ return [
74
+ {
75
+ key: x.execution_id,
76
+ depth,
77
+ kind: x.kind,
78
+ label: x.name,
79
+ server: x.server,
80
+ offset: x.offset,
81
+ duration: x.duration,
82
+ href: executionPath({
83
+ applicationId: a,
84
+ environmentId: e,
85
+ source: x.kind,
86
+ executionId: x.execution_id,
87
+ })!,
88
+ failed: statusTone(x.status, x.outcome) === "destructive",
89
+ },
90
+ ...x.outgoing.map((o) => ({
91
+ key: `o-${o.id}`,
92
+ depth: depth + 1,
93
+ kind: "outgoing_request",
94
+ label: `${o.method ?? ""} ${o.url}`.trim(),
95
+ server: null,
96
+ offset: o.offset,
97
+ duration: o.duration ?? 0,
98
+ href: null,
99
+ failed: statusTone(o.status_code) === "destructive",
100
+ })),
101
+ ...flatten(node.children, a, e, depth + 1),
102
+ ]
103
+ })
104
+ }
105
+
106
+ // One waterfall row. Phones stack the bar under the name/duration line;
107
+ // from md up the three parts sit on one line against a shared time scale.
108
+ function WaterfallRow({ row, scale }: { row: Row; scale: number }) {
109
+ const body = (
110
+ <>
111
+ <span
112
+ className="col-start-1 row-start-1 flex min-w-0 items-center gap-1.5"
113
+ style={{ paddingLeft: `${row.depth * 0.75}rem` }}
114
+ >
115
+ <KindBadge tone={row.failed ? "destructive" : "muted"}>
116
+ {kindLabels[row.kind] ?? row.kind}
117
+ </KindBadge>
118
+ <span className="truncate font-mono">{row.label}</span>
119
+ {row.server && (
120
+ <span className="text-muted-foreground hidden shrink-0 font-mono text-[10px] sm:inline">
121
+ {row.server}
122
+ </span>
123
+ )}
124
+ </span>
125
+ <span className="col-start-2 row-start-1 text-right font-mono tabular-nums md:col-start-3">
126
+ {ms(row.duration)}
127
+ </span>
128
+ <span className="bg-muted/30 col-span-2 col-start-1 row-start-2 h-2 rounded md:col-span-1 md:col-start-2 md:row-start-1 md:h-3">
129
+ <span
130
+ className={cn(
131
+ "block h-full rounded",
132
+ row.failed ? "bg-danger" : row.href ? "bg-primary" : "bg-ok",
133
+ )}
134
+ style={{
135
+ marginLeft: `${Math.min(99, (row.offset / scale) * 100)}%`,
136
+ width: `${Math.max(0.5, (row.duration / scale) * 100)}%`,
137
+ }}
138
+ />
139
+ </span>
140
+ </>
141
+ )
142
+ const className =
143
+ "grid grid-cols-[1fr_4rem] items-center gap-x-2 gap-y-1 border-b px-2 py-1.5 text-xs last:border-b-0 md:grid-cols-[minmax(0,18rem)_1fr_4rem]"
144
+ return row.href ? (
145
+ <Link href={row.href} className={cn(className, "hover:bg-muted/40")}>
146
+ {body}
147
+ </Link>
148
+ ) : (
149
+ <div className={cn(className, "text-muted-foreground")}>{body}</div>
150
+ )
151
+ }
152
+
153
+ export default function TraceShow(p: Props) {
154
+ const { environment } = usePage<SharedProps>().props
155
+ const a = environment!.application_id
156
+ const e = environment!.id
157
+ const rows = flatten(p.roots, a, e)
158
+ const executions = rows.filter((r) => r.href !== null)
159
+ const errors = executions.filter((r) => r.failed).length
160
+ const scale = Math.max(p.duration, 1)
161
+
162
+ return (
163
+ <EnvLayout title="Trace" crumbs={[{ title: "Trace", href: "#" }]}>
164
+ <PageHeader
165
+ withWindow={false}
166
+ title={
167
+ <span className="flex min-w-0 items-center gap-1">
168
+ <span className="truncate font-mono text-base md:text-xl">
169
+ {p.trace_id}
170
+ </span>
171
+ <CopyId value={p.trace_id} label="" />
172
+ </span>
173
+ }
174
+ description="Every execution that shares this trace id, nested by the call that started it."
175
+ />
176
+ <StatStrip>
177
+ <Stat
178
+ label="Executions"
179
+ value={count(executions.length)}
180
+ hint={`${p.span_count} spans`}
181
+ />
182
+ <Stat
183
+ label="Services"
184
+ value={count(p.services.length)}
185
+ hint={p.services.join(" · ")}
186
+ />
187
+ <Stat label="Total duration" value={ms(p.duration)} />
188
+ <Stat
189
+ label="Errors"
190
+ value={count(errors)}
191
+ tone={errors ? "destructive" : undefined}
192
+ />
193
+ </StatStrip>
194
+ <div className="bg-card -mx-3 overflow-hidden border-y md:mx-0 md:rounded-lg md:border">
195
+ {rows.length === 0 ? (
196
+ <EmptyState
197
+ title="Nothing in this trace"
198
+ description="No execution reported this trace id."
199
+ />
200
+ ) : (
201
+ rows.map((row) => (
202
+ <WaterfallRow key={row.key} row={row} scale={scale} />
203
+ ))
204
+ )}
205
+ </div>
206
+ </EnvLayout>
207
+ )
208
+ }