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,664 @@
1
+ import { Link, router, usePage } from "@inertiajs/react"
2
+ import {
3
+ ChevronDown,
4
+ ChevronRight,
5
+ Paperclip,
6
+ Scissors,
7
+ Sparkles,
8
+ } from "lucide-react"
9
+ import { Fragment, useState } from "react"
10
+
11
+ import { DurationPanel, VolumePanel } from "@/components/railwatch/chart-panel"
12
+ import { DataTable } from "@/components/railwatch/data-table"
13
+ import { EmptyState } from "@/components/railwatch/empty-state"
14
+ import { FilterBar } from "@/components/railwatch/filter-bar"
15
+ import { PageHeader } from "@/components/railwatch/page-header"
16
+ import { SparklineCell } from "@/components/railwatch/sparkline-cell"
17
+ import { StatusBadge } from "@/components/railwatch/status-badge"
18
+ import EnvLayout from "@/layouts/env-layout"
19
+ import { count, ms, pct, usd, when } from "@/lib/format"
20
+ import { cn } from "@/lib/utils"
21
+ import * as R from "@/routes"
22
+ import type { SeriesPoint, SharedProps } from "@/types"
23
+
24
+ interface ModelRow {
25
+ group_hash: string
26
+ name: string
27
+ count: number
28
+ errors: number
29
+ truncated: number
30
+ with_attachments: number
31
+ input_tokens: number
32
+ output_tokens: number
33
+ cache_read_tokens: number
34
+ cache_write_tokens: number
35
+ cost: number | null
36
+ unpriced: number
37
+ avg: number
38
+ p95: number
39
+ sparkline: number[]
40
+ }
41
+
42
+ interface Recent {
43
+ id: number
44
+ operation: string
45
+ provider: string | null
46
+ model: string | null
47
+ response_model: string | null
48
+ tool_name: string | null
49
+ duration: number
50
+ status: string | null
51
+ error: string | null
52
+ streaming: boolean | null
53
+ message_count: number | null
54
+ tool_count: number | null
55
+ input_tokens: number | null
56
+ output_tokens: number | null
57
+ cache_read_tokens: number | null
58
+ cache_write_tokens: number | null
59
+ thinking_tokens: number | null
60
+ cost: number | null
61
+ cost_reported: boolean | null
62
+ finish_reason: string | null
63
+ provider_request_id: string | null
64
+ tools: string | null
65
+ attachments: number | null
66
+ attachment_types: string | null
67
+ attachment_names: string | null
68
+ tool_call_id: string | null
69
+ params: Record<string, unknown> | null
70
+ workflow_id: string | null
71
+ workflow_name: string | null
72
+ workflow_step_name: string | null
73
+ prompt: string | null
74
+ completion: string | null
75
+ occurred_at: string
76
+ execution_id: string | null
77
+ execution_preview: string | null
78
+ }
79
+
80
+ interface ToolRow {
81
+ group_hash: string
82
+ name: string
83
+ count: number
84
+ errors: number
85
+ avg: number
86
+ p95: number
87
+ max: number
88
+ sparkline: number[]
89
+ }
90
+
91
+ interface Totals {
92
+ count: number
93
+ errors: number
94
+ tool_count: number
95
+ tool_errors: number
96
+ input_tokens: number
97
+ output_tokens: number
98
+ cost: number | null
99
+ unpriced: number
100
+ }
101
+
102
+ interface Props {
103
+ models: ModelRow[]
104
+ tools: ToolRow[]
105
+ recent: Recent[]
106
+ totals: Totals
107
+ series: SeriesPoint[]
108
+ tool_series: SeriesPoint[]
109
+ q: string
110
+ }
111
+
112
+ const NO_MODELS = (
113
+ <EmptyState
114
+ icon={Sparkles}
115
+ title="No model calls in this window"
116
+ description="Chats, embeddings, and other model calls your app makes through RubyLLM appear here, with the request or job that made them."
117
+ />
118
+ )
119
+
120
+ const NO_TOOLS = (
121
+ <EmptyState
122
+ icon={Sparkles}
123
+ title="No tool calls in this window"
124
+ description="Tools your models invoke appear here, timed separately from the calls that asked for them."
125
+ />
126
+ )
127
+
128
+ // Only present when capture_llm_content is on, which is off by default.
129
+ // Same shape as the outgoing-request body viewer: the text is already in
130
+ // the row, so opening it fetches nothing.
131
+ // max_tokens means the answer was cut off mid-sentence. It is a successful
132
+ // HTTP call, so nothing else on this page would ever show it.
133
+ function FinishReason({ reason }: { reason: string | null }) {
134
+ if (!reason || reason === "stop" || reason === "tool_calls") return null
135
+ const bad = reason === "max_tokens" || reason === "content_filter"
136
+ return (
137
+ <span
138
+ className={cn(
139
+ "ml-2 inline-flex items-center gap-1 text-xs",
140
+ bad ? "text-destructive" : "text-muted-foreground",
141
+ )}
142
+ title={
143
+ reason === "max_tokens"
144
+ ? "The answer was cut off at the output token limit"
145
+ : `Model stopped: ${reason}`
146
+ }
147
+ >
148
+ <Scissors className="size-3" />
149
+ {reason}
150
+ </span>
151
+ )
152
+ }
153
+
154
+ function Attachments({ call }: { call: Recent }) {
155
+ if (!call.attachments) return null
156
+ return (
157
+ <span
158
+ className="text-muted-foreground ml-2 inline-flex items-center gap-1 text-xs"
159
+ title={
160
+ call.attachment_names ??
161
+ `${call.attachments} file(s): ${call.attachment_types}`
162
+ }
163
+ >
164
+ <Paperclip className="size-3" />
165
+ {call.attachment_types ?? call.attachments}
166
+ </span>
167
+ )
168
+ }
169
+
170
+ function Content({ call }: { call: Recent }) {
171
+ const [open, setOpen] = useState(false)
172
+ const { prompt, completion, params, provider_request_id, tools } = call
173
+ const hasDetail =
174
+ prompt !== null ||
175
+ completion !== null ||
176
+ provider_request_id !== null ||
177
+ tools !== null ||
178
+ (params !== null && Object.keys(params).length > 0)
179
+ if (!hasDetail) return <span className="text-muted-foreground">–</span>
180
+ const Chevron = open ? ChevronDown : ChevronRight
181
+ return (
182
+ <div className="flex flex-col items-end gap-1">
183
+ <button
184
+ type="button"
185
+ onClick={() => setOpen(!open)}
186
+ aria-expanded={open}
187
+ className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
188
+ >
189
+ <Chevron className="size-3" />
190
+ Detail
191
+ </button>
192
+ {open && (
193
+ <div className="w-[70vw] max-w-[36rem] space-y-2 text-left">
194
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 font-mono text-[10px]">
195
+ {provider_request_id && (
196
+ <>
197
+ <dt className="text-muted-foreground">provider id</dt>
198
+ <dd className="break-all">{provider_request_id}</dd>
199
+ </>
200
+ )}
201
+ {tools && (
202
+ <>
203
+ <dt className="text-muted-foreground">tools</dt>
204
+ <dd className="break-all">{tools}</dd>
205
+ </>
206
+ )}
207
+ {params &&
208
+ Object.entries(params).map(([k, v]) => (
209
+ <Fragment key={k}>
210
+ <dt className="text-muted-foreground">{k}</dt>
211
+ <dd className="break-all">
212
+ {typeof v === "string" || typeof v === "number"
213
+ ? v
214
+ : JSON.stringify(v)}
215
+ </dd>
216
+ </Fragment>
217
+ ))}
218
+ </dl>
219
+ {[
220
+ ["Prompt", prompt],
221
+ ["Completion", completion],
222
+ ].map(([label, text]) =>
223
+ text ? (
224
+ <div key={label}>
225
+ <div className="text-muted-foreground text-[10px] tracking-wide uppercase">
226
+ {label}
227
+ </div>
228
+ <pre className="bg-muted max-h-64 overflow-auto rounded p-2 font-mono text-[10px] break-all whitespace-pre-wrap">
229
+ {text}
230
+ </pre>
231
+ </div>
232
+ ) : null,
233
+ )}
234
+ </div>
235
+ )}
236
+ </div>
237
+ )
238
+ }
239
+
240
+ // A spend figure that leaves calls out is worse than no figure, so say how
241
+ // many it leaves out rather than letting the number pass for the whole bill.
242
+ function Spend({ cost, unpriced }: { cost: number | null; unpriced: number }) {
243
+ if (cost === null)
244
+ return unpriced > 0 ? (
245
+ <span className="text-muted-foreground" title="No registry pricing">
246
+ unpriced
247
+ </span>
248
+ ) : (
249
+ <span className="text-muted-foreground" title="Nothing here has a price">
250
+ –
251
+ </span>
252
+ )
253
+ return (
254
+ <span className="font-semibold">
255
+ {usd(cost)}
256
+ {unpriced > 0 && (
257
+ <span
258
+ className="text-muted-foreground ml-1 text-xs font-normal"
259
+ title={`${unpriced} call${unpriced === 1 ? "" : "s"} had no registry pricing and are not in this total`}
260
+ >
261
+ +{count(unpriced)} unpriced
262
+ </span>
263
+ )}
264
+ </span>
265
+ )
266
+ }
267
+
268
+ function Stat({
269
+ label,
270
+ children,
271
+ }: {
272
+ label: string
273
+ children: React.ReactNode
274
+ }) {
275
+ return (
276
+ <div className="min-w-0">
277
+ <div className="text-muted-foreground text-xs">{label}</div>
278
+ <div className="truncate text-lg">{children}</div>
279
+ </div>
280
+ )
281
+ }
282
+
283
+ export default function LlmCalls(p: Props) {
284
+ const { environment, window } = usePage<SharedProps>().props
285
+ const a = environment!.application_id
286
+ const e = environment!.id
287
+ const href = (r: Recent) =>
288
+ r.execution_id
289
+ ? R.applicationEnvironmentRequestPath(a, e, r.execution_id)
290
+ : null
291
+
292
+ return (
293
+ <EnvLayout title="LLM">
294
+ <PageHeader
295
+ title="LLM"
296
+ description="Model calls and tool invocations through RubyLLM, by model and by the request or job that made them."
297
+ />
298
+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-5">
299
+ <Stat label="Model calls">{count(p.totals.count)}</Stat>
300
+ <Stat label="Tool calls">{count(p.totals.tool_count)}</Stat>
301
+ <Stat label="Spend">
302
+ <Spend cost={p.totals.cost} unpriced={p.totals.unpriced} />
303
+ </Stat>
304
+ <Stat label="Tokens in">{count(p.totals.input_tokens)}</Stat>
305
+ <Stat label="Tokens out">{count(p.totals.output_tokens)}</Stat>
306
+ </div>
307
+ <div className="grid gap-4 lg:grid-cols-2">
308
+ <VolumePanel
309
+ legend="outcome"
310
+ label="Model calls"
311
+ seriesLabel="Calls"
312
+ data={p.series}
313
+ />
314
+ <DurationPanel label="Model latency" data={p.series} percentile="p95" />
315
+ </div>
316
+ {p.totals.tool_count > 0 && (
317
+ <div className="grid gap-4 lg:grid-cols-2">
318
+ <VolumePanel
319
+ legend="outcome"
320
+ label="Tool calls"
321
+ seriesLabel="Calls"
322
+ data={p.tool_series}
323
+ />
324
+ <DurationPanel
325
+ label="Tool latency"
326
+ data={p.tool_series}
327
+ percentile="p95"
328
+ />
329
+ </div>
330
+ )}
331
+ <DataTable
332
+ rows={p.models}
333
+ rowKey={(m) => m.group_hash}
334
+ empty={NO_MODELS}
335
+ columns={[
336
+ {
337
+ key: "m",
338
+ header: "Model",
339
+ cell: (m) => <span className="font-mono text-xs">{m.name}</span>,
340
+ },
341
+ {
342
+ key: "trend",
343
+ hideOnMobile: true,
344
+ header: "Trend",
345
+ cell: (m) => <SparklineCell data={m.sparkline} />,
346
+ },
347
+ {
348
+ key: "n",
349
+ header: "Calls",
350
+ align: "right",
351
+ cell: (m) => count(m.count),
352
+ },
353
+ {
354
+ key: "f",
355
+ hideOnMobile: true,
356
+ header: "Failed",
357
+ align: "right",
358
+ cell: (m) => (
359
+ <span className={m.errors ? "text-destructive" : ""}>
360
+ {pct(m.errors, m.count)}
361
+ </span>
362
+ ),
363
+ },
364
+ {
365
+ key: "cut",
366
+ hideOnMobile: true,
367
+ header: "Cut off",
368
+ align: "right",
369
+ cell: (m) =>
370
+ m.truncated ? (
371
+ <span
372
+ className="text-destructive"
373
+ title="Answers stopped at the output token limit"
374
+ >
375
+ {pct(m.truncated, m.count)}
376
+ </span>
377
+ ) : (
378
+ <span className="text-muted-foreground">–</span>
379
+ ),
380
+ },
381
+ {
382
+ key: "att",
383
+ hideOnMobile: true,
384
+ header: "With files",
385
+ align: "right",
386
+ cell: (m) =>
387
+ m.with_attachments ? (
388
+ count(m.with_attachments)
389
+ ) : (
390
+ <span className="text-muted-foreground">–</span>
391
+ ),
392
+ },
393
+ {
394
+ key: "in",
395
+ hideOnMobile: true,
396
+ header: "Tokens in",
397
+ align: "right",
398
+ cell: (m) => count(m.input_tokens),
399
+ },
400
+ {
401
+ key: "out",
402
+ hideOnMobile: true,
403
+ header: "Tokens out",
404
+ align: "right",
405
+ cell: (m) => count(m.output_tokens),
406
+ },
407
+ {
408
+ key: "cached",
409
+ hideOnMobile: true,
410
+ header: "Cached",
411
+ align: "right",
412
+ cell: (m) => count(m.cache_read_tokens),
413
+ },
414
+ {
415
+ key: "p95",
416
+ hideOnMobile: true,
417
+ header: "p95",
418
+ align: "right",
419
+ cell: (m) => ms(m.p95),
420
+ },
421
+ {
422
+ key: "cost",
423
+ header: "Spend",
424
+ align: "right",
425
+ cell: (m) => <Spend cost={m.cost} unpriced={m.unpriced} />,
426
+ },
427
+ ]}
428
+ />
429
+ {p.tools.length > 0 && (
430
+ <>
431
+ <h2 className="text-sm font-semibold">Tools</h2>
432
+ <DataTable
433
+ rows={p.tools}
434
+ rowKey={(t) => t.group_hash}
435
+ empty={NO_TOOLS}
436
+ columns={[
437
+ {
438
+ key: "t",
439
+ header: "Tool",
440
+ cell: (t) => (
441
+ <span className="font-mono text-xs">{t.name}</span>
442
+ ),
443
+ },
444
+ {
445
+ key: "trend",
446
+ hideOnMobile: true,
447
+ header: "Trend",
448
+ cell: (t) => <SparklineCell data={t.sparkline} />,
449
+ },
450
+ {
451
+ key: "n",
452
+ header: "Calls",
453
+ align: "right",
454
+ cell: (t) => count(t.count),
455
+ },
456
+ {
457
+ key: "f",
458
+ hideOnMobile: true,
459
+ header: "Failed",
460
+ align: "right",
461
+ cell: (t) => (
462
+ <span className={t.errors ? "text-destructive" : ""}>
463
+ {pct(t.errors, t.count)}
464
+ </span>
465
+ ),
466
+ },
467
+ {
468
+ key: "avg",
469
+ hideOnMobile: true,
470
+ header: "Avg",
471
+ align: "right",
472
+ cell: (t) => ms(t.avg),
473
+ },
474
+ {
475
+ key: "p95",
476
+ header: "p95",
477
+ align: "right",
478
+ cell: (t) => <span className="font-semibold">{ms(t.p95)}</span>,
479
+ },
480
+ {
481
+ key: "max",
482
+ hideOnMobile: true,
483
+ header: "Max",
484
+ align: "right",
485
+ cell: (t) => ms(t.max),
486
+ },
487
+ ]}
488
+ />
489
+ </>
490
+ )}
491
+ <h2 className="text-sm font-semibold">Recent</h2>
492
+ <FilterBar
493
+ value={p.q}
494
+ fields={[
495
+ { key: "model", label: "Model" },
496
+ { key: "provider", label: "Provider" },
497
+ {
498
+ key: "operation",
499
+ label: "Operation",
500
+ options: [
501
+ "chat",
502
+ "tool",
503
+ "embedding",
504
+ "image",
505
+ "speech",
506
+ "transcription",
507
+ "moderation",
508
+ "rerank",
509
+ "ocr",
510
+ ],
511
+ },
512
+ { key: "workflow", label: "Workflow" },
513
+ {
514
+ key: "finish",
515
+ label: "Finish",
516
+ options: ["stop", "max_tokens", "tool_calls", "content_filter"],
517
+ },
518
+ {
519
+ key: "attachments",
520
+ label: "Files",
521
+ options: ["any", "image", "pdf", "audio", "video"],
522
+ },
523
+ ]}
524
+ onChange={(q) =>
525
+ router.visit(
526
+ R.applicationEnvironmentLlmCallsPath(a, e, {
527
+ window,
528
+ q: q || undefined,
529
+ }),
530
+ { preserveState: true },
531
+ )
532
+ }
533
+ placeholder="model:claude-opus-5 operation:chat"
534
+ />
535
+ <DataTable
536
+ rows={p.recent}
537
+ rowKey={(r) => r.id}
538
+ empty={NO_MODELS}
539
+ keyboardNav={{
540
+ onOpen: (r, opts) => {
541
+ const to = href(r)
542
+ if (!to) return
543
+ if (opts?.newTab) globalThis.window.open(to, "_blank")
544
+ else router.visit(to)
545
+ },
546
+ }}
547
+ columns={[
548
+ {
549
+ key: "when",
550
+ header: "When",
551
+ cell: (r) => <span className="text-xs">{when(r.occurred_at)}</span>,
552
+ },
553
+ {
554
+ key: "st",
555
+ header: "Status",
556
+ cell: (r) => (
557
+ <StatusBadge
558
+ status={null}
559
+ label={r.status === "failed" ? "error" : r.operation}
560
+ outcome={r.status === "failed" ? "failed" : undefined}
561
+ />
562
+ ),
563
+ },
564
+ {
565
+ key: "what",
566
+ header: "Call",
567
+ grow: true,
568
+ cell: (r) => (
569
+ <span className="font-mono text-xs">
570
+ {r.operation === "tool" ? (
571
+ r.tool_name
572
+ ) : (
573
+ <>
574
+ <span className="text-muted-foreground">{r.provider}/</span>
575
+ <span className="font-semibold">{r.model}</span>
576
+ </>
577
+ )}
578
+ {r.error && (
579
+ <span className="text-destructive ml-2">{r.error}</span>
580
+ )}
581
+ <FinishReason reason={r.finish_reason} />
582
+ <Attachments call={r} />
583
+ </span>
584
+ ),
585
+ },
586
+ {
587
+ key: "wf",
588
+ hideOnMobile: true,
589
+ header: "Workflow",
590
+ cell: (r) =>
591
+ r.workflow_name ? (
592
+ <span className="text-xs">
593
+ {r.workflow_name}
594
+ {r.workflow_step_name && (
595
+ <span className="text-muted-foreground">
596
+ {" · "}
597
+ {r.workflow_step_name}
598
+ </span>
599
+ )}
600
+ </span>
601
+ ) : null,
602
+ },
603
+ {
604
+ key: "tok",
605
+ hideOnMobile: true,
606
+ header: "Tokens",
607
+ align: "right",
608
+ cell: (r) =>
609
+ r.input_tokens === null && r.output_tokens === null ? (
610
+ <span className="text-muted-foreground">–</span>
611
+ ) : (
612
+ <span className="font-mono text-xs">
613
+ {count(r.input_tokens ?? 0)} / {count(r.output_tokens ?? 0)}
614
+ </span>
615
+ ),
616
+ },
617
+ {
618
+ key: "d",
619
+ header: "Duration",
620
+ align: "right",
621
+ cell: (r) => ms(r.duration),
622
+ },
623
+ {
624
+ key: "cost",
625
+ header: "Cost",
626
+ align: "right",
627
+ cell: (r) =>
628
+ r.cost === null &&
629
+ r.operation !== "tool" &&
630
+ r.status !== "failed" ? (
631
+ <span
632
+ className="text-muted-foreground"
633
+ title="No registry pricing"
634
+ >
635
+ unpriced
636
+ </span>
637
+ ) : (
638
+ usd(r.cost)
639
+ ),
640
+ },
641
+ {
642
+ key: "in",
643
+ hideOnMobile: true,
644
+ header: "In",
645
+ cell: (r) => {
646
+ const to = href(r)
647
+ return to ? (
648
+ <Link className="font-mono text-xs hover:underline" href={to}>
649
+ {r.execution_preview}
650
+ </Link>
651
+ ) : null
652
+ },
653
+ },
654
+ {
655
+ key: "content",
656
+ header: "Detail",
657
+ align: "right",
658
+ cell: (r) => <Content call={r} />,
659
+ },
660
+ ]}
661
+ />
662
+ </EnvLayout>
663
+ )
664
+ }