railwatch 0.6.0 → 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 (364) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +42 -26
  3. data/CHANGELOG.md +41 -0
  4. data/README.md +40 -29
  5. data/app/frontend/components/app-content.tsx +26 -0
  6. data/app/frontend/components/app-logo-icon.tsx +23 -0
  7. data/app/frontend/components/app-logo.tsx +14 -0
  8. data/app/frontend/components/app-shell.tsx +38 -0
  9. data/app/frontend/components/app-sidebar-header.tsx +54 -0
  10. data/app/frontend/components/app-sidebar.tsx +94 -0
  11. data/app/frontend/components/app-wordmark.tsx +29 -0
  12. data/app/frontend/components/breadcrumbs.tsx +46 -0
  13. data/app/frontend/components/icon.tsx +16 -0
  14. data/app/frontend/components/input-error.tsx +18 -0
  15. data/app/frontend/components/nav-footer.tsx +46 -0
  16. data/app/frontend/components/nav-main.tsx +41 -0
  17. data/app/frontend/components/nav-user.tsx +50 -0
  18. data/app/frontend/components/railwatch/chart-hover.tsx +34 -0
  19. data/app/frontend/components/railwatch/chart-panel.tsx +170 -0
  20. data/app/frontend/components/railwatch/code.tsx +28 -0
  21. data/app/frontend/components/railwatch/command-palette.tsx +132 -0
  22. data/app/frontend/components/railwatch/copy-block.tsx +52 -0
  23. data/app/frontend/components/railwatch/copy-id.tsx +49 -0
  24. data/app/frontend/components/railwatch/cursor-load-more.tsx +39 -0
  25. data/app/frontend/components/railwatch/data-table.tsx +195 -0
  26. data/app/frontend/components/railwatch/digit-roll.tsx +312 -0
  27. data/app/frontend/components/railwatch/empty-state.tsx +159 -0
  28. data/app/frontend/components/railwatch/env-nav.tsx +130 -0
  29. data/app/frontend/components/railwatch/env-switcher.tsx +145 -0
  30. data/app/frontend/components/railwatch/filter-bar.tsx +153 -0
  31. data/app/frontend/components/railwatch/flamegraph.tsx +324 -0
  32. data/app/frontend/components/railwatch/frames.tsx +295 -0
  33. data/app/frontend/components/railwatch/json-viewer.tsx +159 -0
  34. data/app/frontend/components/railwatch/live-dot.tsx +78 -0
  35. data/app/frontend/components/railwatch/live-toggle.tsx +76 -0
  36. data/app/frontend/components/railwatch/metric-chart.tsx +112 -0
  37. data/app/frontend/components/railwatch/mobile-page-switcher.tsx +89 -0
  38. data/app/frontend/components/railwatch/onboarding.tsx +91 -0
  39. data/app/frontend/components/railwatch/origin-identity.tsx +84 -0
  40. data/app/frontend/components/railwatch/page-header.tsx +42 -0
  41. data/app/frontend/components/railwatch/percentile-picker.tsx +18 -0
  42. data/app/frontend/components/railwatch/relative-time.tsx +18 -0
  43. data/app/frontend/components/railwatch/saved-views.tsx +232 -0
  44. data/app/frontend/components/railwatch/segmented.tsx +64 -0
  45. data/app/frontend/components/railwatch/series-chart.tsx +400 -0
  46. data/app/frontend/components/railwatch/shortcuts-dialog.tsx +89 -0
  47. data/app/frontend/components/railwatch/signal-toast.tsx +121 -0
  48. data/app/frontend/components/railwatch/sort-header.tsx +37 -0
  49. data/app/frontend/components/railwatch/source-link.tsx +57 -0
  50. data/app/frontend/components/railwatch/sparkline-cell.tsx +14 -0
  51. data/app/frontend/components/railwatch/sql-block.tsx +116 -0
  52. data/app/frontend/components/railwatch/stat.tsx +173 -0
  53. data/app/frontend/components/railwatch/status-badge.tsx +79 -0
  54. data/app/frontend/components/railwatch/step-picker.tsx +57 -0
  55. data/app/frontend/components/railwatch/timeline.tsx +405 -0
  56. data/app/frontend/components/railwatch/utilisation-bar.tsx +44 -0
  57. data/app/frontend/components/railwatch/window-picker.tsx +147 -0
  58. data/app/frontend/components/ui/avatar.tsx +51 -0
  59. data/app/frontend/components/ui/badge.tsx +46 -0
  60. data/app/frontend/components/ui/breadcrumb.tsx +109 -0
  61. data/app/frontend/components/ui/button.tsx +60 -0
  62. data/app/frontend/components/ui/card.tsx +92 -0
  63. data/app/frontend/components/ui/chart.tsx +374 -0
  64. data/app/frontend/components/ui/checkbox.tsx +32 -0
  65. data/app/frontend/components/ui/collapsible.tsx +31 -0
  66. data/app/frontend/components/ui/command.tsx +182 -0
  67. data/app/frontend/components/ui/dialog.tsx +158 -0
  68. data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
  69. data/app/frontend/components/ui/hover-card.tsx +42 -0
  70. data/app/frontend/components/ui/input.tsx +21 -0
  71. data/app/frontend/components/ui/label.tsx +24 -0
  72. data/app/frontend/components/ui/popover.tsx +87 -0
  73. data/app/frontend/components/ui/select.tsx +185 -0
  74. data/app/frontend/components/ui/separator.tsx +28 -0
  75. data/app/frontend/components/ui/sheet.tsx +137 -0
  76. data/app/frontend/components/ui/sidebar.tsx +726 -0
  77. data/app/frontend/components/ui/skeleton.tsx +13 -0
  78. data/app/frontend/components/ui/sonner.tsx +41 -0
  79. data/app/frontend/components/ui/table.tsx +114 -0
  80. data/app/frontend/components/ui/tabs.tsx +91 -0
  81. data/app/frontend/components/ui/textarea.tsx +18 -0
  82. data/app/frontend/components/ui/tooltip.tsx +61 -0
  83. data/app/frontend/components/user-info.tsx +32 -0
  84. data/app/frontend/components/user-menu-content.tsx +94 -0
  85. data/app/frontend/entrypoints/application.css +244 -0
  86. data/app/frontend/entrypoints/inertia.tsx +95 -0
  87. data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
  88. data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
  89. data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
  90. data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
  91. data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
  92. data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
  93. data/app/frontend/hooks/use-appearance.tsx +67 -0
  94. data/app/frontend/hooks/use-clipboard.ts +32 -0
  95. data/app/frontend/hooks/use-flash.tsx +23 -0
  96. data/app/frontend/hooks/use-hotkeys.ts +63 -0
  97. data/app/frontend/hooks/use-initials.tsx +15 -0
  98. data/app/frontend/hooks/use-live.ts +179 -0
  99. data/app/frontend/hooks/use-mobile-navigation.ts +8 -0
  100. data/app/frontend/hooks/use-mobile.ts +21 -0
  101. data/app/frontend/hooks/use-percentile.ts +24 -0
  102. data/app/frontend/hooks/use-reduced-motion.ts +25 -0
  103. data/app/frontend/hooks/use-row-nav.ts +106 -0
  104. data/app/frontend/hooks/use-step.ts +27 -0
  105. data/app/frontend/hooks/use-ticker.ts +12 -0
  106. data/app/frontend/hooks/use-window.ts +58 -0
  107. data/app/frontend/layouts/app/app-sidebar-layout.tsx +107 -0
  108. data/app/frontend/layouts/app-layout.tsx +21 -0
  109. data/app/frontend/layouts/env-layout.tsx +53 -0
  110. data/app/frontend/layouts/persistent-layout.tsx +18 -0
  111. data/app/frontend/lib/cable.ts +10 -0
  112. data/app/frontend/lib/env-pages.ts +236 -0
  113. data/app/frontend/lib/execution-path.ts +55 -0
  114. data/app/frontend/lib/explain.ts +50 -0
  115. data/app/frontend/lib/filter.ts +25 -0
  116. data/app/frontend/lib/flamegraph.ts +155 -0
  117. data/app/frontend/lib/format.ts +71 -0
  118. data/app/frontend/lib/keyboard.ts +18 -0
  119. data/app/frontend/lib/source-link.ts +217 -0
  120. data/app/frontend/lib/utils.ts +6 -0
  121. data/app/frontend/pages/alerts/index.tsx +171 -0
  122. data/app/frontend/pages/broadcasts/index.tsx +171 -0
  123. data/app/frontend/pages/cache_events/index.tsx +127 -0
  124. data/app/frontend/pages/commands/index.tsx +143 -0
  125. data/app/frontend/pages/deploys/index.tsx +94 -0
  126. data/app/frontend/pages/deploys/show.tsx +309 -0
  127. data/app/frontend/pages/deprecations/index.tsx +176 -0
  128. data/app/frontend/pages/exceptions/index.tsx +271 -0
  129. data/app/frontend/pages/executions/show.tsx +821 -0
  130. data/app/frontend/pages/issues/index.tsx +399 -0
  131. data/app/frontend/pages/issues/show.tsx +1367 -0
  132. data/app/frontend/pages/jobs/index.tsx +381 -0
  133. data/app/frontend/pages/jobs/klass.tsx +215 -0
  134. data/app/frontend/pages/llm_calls/index.tsx +664 -0
  135. data/app/frontend/pages/logs/index.tsx +303 -0
  136. data/app/frontend/pages/mails/index.tsx +218 -0
  137. data/app/frontend/pages/notifications/index.tsx +161 -0
  138. data/app/frontend/pages/outgoing_requests/index.tsx +309 -0
  139. data/app/frontend/pages/overview/show.tsx +333 -0
  140. data/app/frontend/pages/people/index.tsx +102 -0
  141. data/app/frontend/pages/people/show.tsx +149 -0
  142. data/app/frontend/pages/processes/index.tsx +335 -0
  143. data/app/frontend/pages/profiles/index.tsx +110 -0
  144. data/app/frontend/pages/profiles/show.tsx +105 -0
  145. data/app/frontend/pages/queries/index.tsx +492 -0
  146. data/app/frontend/pages/queries/show.tsx +276 -0
  147. data/app/frontend/pages/releases/index.tsx +190 -0
  148. data/app/frontend/pages/releases/release-health.tsx +68 -0
  149. data/app/frontend/pages/releases/release-path.ts +25 -0
  150. data/app/frontend/pages/releases/show.tsx +262 -0
  151. data/app/frontend/pages/requests/index.tsx +235 -0
  152. data/app/frontend/pages/requests/route.tsx +159 -0
  153. data/app/frontend/pages/scheduled_tasks/index.tsx +220 -0
  154. data/app/frontend/pages/spans/index.tsx +229 -0
  155. data/app/frontend/pages/spans/show.tsx +220 -0
  156. data/app/frontend/pages/storage_ops/index.tsx +150 -0
  157. data/app/frontend/pages/tenants/index.tsx +236 -0
  158. data/app/frontend/pages/tenants/show.tsx +477 -0
  159. data/app/frontend/pages/tenants/tenant-path.ts +26 -0
  160. data/app/frontend/pages/thresholds/index.tsx +414 -0
  161. data/app/frontend/pages/traces/show.tsx +208 -0
  162. data/app/frontend/pages/transactions/index.tsx +220 -0
  163. data/app/frontend/pages/view_renders/index.tsx +241 -0
  164. data/app/frontend/pages/visits/index.tsx +321 -0
  165. data/app/frontend/public/icon.png +0 -0
  166. data/app/frontend/public/icon.svg +5 -0
  167. data/app/frontend/routes/index.d.ts +2136 -0
  168. data/app/frontend/routes/index.js +2112 -0
  169. data/app/frontend/types/globals.d.ts +9 -0
  170. data/app/frontend/types/index.ts +288 -0
  171. data/app/frontend/types/vite-env.d.ts +9 -0
  172. data/app/jobs/railwatch/rollup_job.rb +3 -1
  173. data/app/models/railwatch/ingest/rollup_absorber.rb +1 -1
  174. data/docs/ai-and-mcp.md +9 -3
  175. data/docs/configuration.md +68 -27
  176. data/docs/embedded.md +76 -43
  177. data/docs/faq.md +22 -10
  178. data/docs/getting-started.md +116 -41
  179. data/docs/records.md +13 -10
  180. data/docs/replacing-nightwatch.md +16 -14
  181. data/docs/replacing-sentry.md +19 -10
  182. data/docs/security.md +24 -3
  183. data/docs/self-hosting.md +9 -1
  184. data/docs/testing.md +14 -4
  185. data/docs/troubleshooting.md +62 -23
  186. data/lib/generators/railwatch/install/install_generator.rb +5 -4
  187. data/lib/generators/railwatch/install/templates/initializer.rb.tt +2 -2
  188. data/lib/railwatch/version.rb +1 -1
  189. data/lib/railwatch.rb +8 -3
  190. data/llms.txt +21 -18
  191. data/public/railwatch/assets/app-layout-LvxN9xSS.js +41 -0
  192. data/public/railwatch/assets/application-Cc5v_Vk9.css +1 -0
  193. data/public/railwatch/assets/{arrow-up-EmePsTKP.js → arrow-up-DG01k9AL.js} +1 -1
  194. data/public/railwatch/assets/{braces-iZ7MEfYP.js → braces-C3w7pfxS.js} +1 -1
  195. data/public/railwatch/assets/{card-DqdEEKcJ.js → card-Dpwlr0uY.js} +1 -1
  196. data/public/railwatch/assets/{chart-C4k7Ub9W.js → chart-DJHv3R7W.js} +1 -1
  197. data/public/railwatch/assets/{chart-hover-2yZG3w2u.js → chart-hover-CEH-DtXO.js} +1 -1
  198. data/public/railwatch/assets/{chart-panel-CONkZcn9.js → chart-panel-CUdIzJvk.js} +1 -1
  199. data/public/railwatch/assets/code-BRZprwun.js +1 -0
  200. data/public/railwatch/assets/copy-id-CwhGGiwF.js +1 -0
  201. data/public/railwatch/assets/cursor-load-more-uF1_955Q.js +1 -0
  202. data/public/railwatch/assets/data-table-DYi-N0hm.js +1 -0
  203. data/public/railwatch/assets/env-layout-HdcOaPPX.js +1 -0
  204. data/public/railwatch/assets/{execution-path-DWovhgLi.js → execution-path-CElXXFO_.js} +1 -1
  205. data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
  206. data/public/railwatch/assets/{flamegraph-BNbNIuKT.js → flamegraph-SKD9yxW9.js} +2 -2
  207. data/public/railwatch/assets/frames-CUpyJQDy.js +1 -0
  208. data/public/railwatch/assets/index-2W-ppwap.js +1 -0
  209. data/public/railwatch/assets/index-74ocuU5d.js +1 -0
  210. data/public/railwatch/assets/{index-Zkt4W_CM.js → index-B8NA62Lz.js} +2 -2
  211. data/public/railwatch/assets/index-BBCEkLqp.js +1 -0
  212. data/public/railwatch/assets/index-BDcglJzX.js +1 -0
  213. data/public/railwatch/assets/index-B_KFb9LS.js +1 -0
  214. data/public/railwatch/assets/index-BqK_VKom.js +1 -0
  215. data/public/railwatch/assets/index-BxCXBHP2.js +1 -0
  216. data/public/railwatch/assets/index-C1jc1OoH.js +1 -0
  217. data/public/railwatch/assets/index-C7zREldp.js +1 -0
  218. data/public/railwatch/assets/index-CEetm4oC.js +1 -0
  219. data/public/railwatch/assets/index-CIaZkokF.js +1 -0
  220. data/public/railwatch/assets/index-CNH85TZp.js +1 -0
  221. data/public/railwatch/assets/index-Ce4SB5AB.js +1 -0
  222. data/public/railwatch/assets/index-CiRMSYMn.js +1 -0
  223. data/public/railwatch/assets/index-CjxXgOs-.js +1 -0
  224. data/public/railwatch/assets/index-ClL-PYEV.js +1 -0
  225. data/public/railwatch/assets/index-CteZ6fop.js +1 -0
  226. data/public/railwatch/assets/index-CvqfEExO.js +1 -0
  227. data/public/railwatch/assets/index-CxTLT-Qh.js +1 -0
  228. data/public/railwatch/assets/index-DJd3OUSd.js +1 -0
  229. data/public/railwatch/assets/index-DPD-rLp6.js +1 -0
  230. data/public/railwatch/assets/index-DYK_dps4.js +1 -0
  231. data/public/railwatch/assets/index-DrayARrI.js +1 -0
  232. data/public/railwatch/assets/index-HRXGir-0.js +1 -0
  233. data/public/railwatch/assets/index-UJOUALr_.js +1 -0
  234. data/public/railwatch/assets/index-d-3J7gQ5.js +1 -0
  235. data/public/railwatch/assets/index-oQrvBpOm.js +1 -0
  236. data/public/railwatch/assets/inertia-FoqYZAk8.js +97 -0
  237. data/public/railwatch/assets/json-viewer-CxqMojEm.js +1 -0
  238. data/public/railwatch/assets/klass-DEF-Vat5.js +1 -0
  239. data/public/railwatch/assets/origin-identity-Y6gvfQVR.js +1 -0
  240. data/public/railwatch/assets/{percentile-picker-BxIVblBw.js → percentile-picker-E2aUdLxx.js} +1 -1
  241. data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
  242. data/public/railwatch/assets/{release-health-Cb1tMOOO.js → release-health-C8WSCDKD.js} +1 -1
  243. data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
  244. data/public/railwatch/assets/{segmented-C4oWE73o.js → segmented-BWee8uc9.js} +1 -1
  245. data/public/railwatch/assets/select-G7rNBzv0.js +1 -0
  246. data/public/railwatch/assets/series-chart-C5nXIUc5.js +1 -0
  247. data/public/railwatch/assets/show-BEWisLQw.js +1 -0
  248. data/public/railwatch/assets/show-BEa9PGu1.js +2 -0
  249. data/public/railwatch/assets/{show-gtwKsTsl.js → show-BiT_Aloc.js} +3 -3
  250. data/public/railwatch/assets/show-BpCO49d3.js +1 -0
  251. data/public/railwatch/assets/show-CH7yxt7R.js +1 -0
  252. data/public/railwatch/assets/show-CTO8Lvd2.js +1 -0
  253. data/public/railwatch/assets/show-DnYsDMUt.js +2 -0
  254. data/public/railwatch/assets/show-DyzHM527.js +1 -0
  255. data/public/railwatch/assets/show-Nfpxs8su.js +1 -0
  256. data/public/railwatch/assets/show-ddjUMo9G.js +1 -0
  257. data/public/railwatch/assets/show-ksjbSzDH.js +1 -0
  258. data/public/railwatch/assets/{sort-header-CPM6fhXv.js → sort-header-uOq5VlrB.js} +1 -1
  259. data/public/railwatch/assets/{sparkline-cell-CXCxXPWt.js → sparkline-cell-DgGij0cu.js} +1 -1
  260. data/public/railwatch/assets/stat-BTzNKTm2.js +1 -0
  261. data/public/railwatch/assets/status-badge-BDkOXa-C.js +1 -0
  262. data/public/railwatch/assets/{tenant-path-C4aM_cO0.js → tenant-path-DU0ezKSU.js} +1 -1
  263. data/public/railwatch/assets/{use-clipboard-C1ApSwzA.js → use-clipboard-DoENmlWY.js} +1 -1
  264. data/public/railwatch/assets/{use-live-DWwLg1Nj.js → use-live-CE_fN5_6.js} +1 -1
  265. data/public/railwatch/manifest.json +612 -1591
  266. metadata +242 -117
  267. data/public/railwatch/assets/app-layout-2xAKndVD.js +0 -1
  268. data/public/railwatch/assets/app-wordmark-Bt6XqURo.js +0 -1
  269. data/public/railwatch/assets/appearance-DyCfeh9o.js +0 -1
  270. data/public/railwatch/assets/application-B7h1MIhi.css +0 -1
  271. data/public/railwatch/assets/auth-layout-BuwogWMZ.js +0 -1
  272. data/public/railwatch/assets/badge-Dv7I-ebP.js +0 -1
  273. data/public/railwatch/assets/checkbox-DKbThbxr.js +0 -1
  274. data/public/railwatch/assets/code-CYahYQHZ.js +0 -1
  275. data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
  276. data/public/railwatch/assets/copy-block-CbJum9s8.js +0 -1
  277. data/public/railwatch/assets/copy-id-JmvLfkmx.js +0 -1
  278. data/public/railwatch/assets/cursor-load-more-C1ylbqqL.js +0 -1
  279. data/public/railwatch/assets/data-table-CVKR2wkq.js +0 -1
  280. data/public/railwatch/assets/edit-BXF5oNYA.js +0 -1
  281. data/public/railwatch/assets/edit-BwSEVrTG.js +0 -8
  282. data/public/railwatch/assets/edit-D5BRDwLT.js +0 -1
  283. data/public/railwatch/assets/empty-state-Bn48Gplm.js +0 -1
  284. data/public/railwatch/assets/env-layout-CKLL8Ds7.js +0 -1
  285. data/public/railwatch/assets/filter-bar-xcbJC93z.js +0 -1
  286. data/public/railwatch/assets/frames-Bk39xfJG.js +0 -1
  287. data/public/railwatch/assets/google-sign-in-button-B5as2ps1.js +0 -1
  288. data/public/railwatch/assets/index-Atp1FTgX.js +0 -1
  289. data/public/railwatch/assets/index-B-FXAo1w.js +0 -1
  290. data/public/railwatch/assets/index-B0xgzHuN.js +0 -1
  291. data/public/railwatch/assets/index-BDVShWMs.js +0 -1
  292. data/public/railwatch/assets/index-BRvRkfob.js +0 -1
  293. data/public/railwatch/assets/index-BVWapDeB.js +0 -1
  294. data/public/railwatch/assets/index-Bd4YpOIO.js +0 -1
  295. data/public/railwatch/assets/index-BeQw69GM.js +0 -1
  296. data/public/railwatch/assets/index-BfitxVBv.js +0 -1
  297. data/public/railwatch/assets/index-C-EwH4lO.js +0 -1
  298. data/public/railwatch/assets/index-C8JIdb3_.js +0 -1
  299. data/public/railwatch/assets/index-CBFCtJAR.js +0 -1
  300. data/public/railwatch/assets/index-CDXlITyG.js +0 -1
  301. data/public/railwatch/assets/index-COd9lT84.js +0 -1
  302. data/public/railwatch/assets/index-C_hQmVTP.js +0 -1
  303. data/public/railwatch/assets/index-CcennT28.js +0 -1
  304. data/public/railwatch/assets/index-CiaeS-n7.js +0 -1
  305. data/public/railwatch/assets/index-CrA7jBTK.js +0 -1
  306. data/public/railwatch/assets/index-D0mKj3Hg.js +0 -1
  307. data/public/railwatch/assets/index-DV5MdmpY.js +0 -1
  308. data/public/railwatch/assets/index-DaE-1xhx.js +0 -1
  309. data/public/railwatch/assets/index-DdVg9LKX.js +0 -1
  310. data/public/railwatch/assets/index-DeLtMWou.js +0 -1
  311. data/public/railwatch/assets/index-DiQ4mqT_.js +0 -1
  312. data/public/railwatch/assets/index-LKgpCzoR.js +0 -1
  313. data/public/railwatch/assets/index-S0NVaLj9.js +0 -1
  314. data/public/railwatch/assets/index-Vj93BTR5.js +0 -1
  315. data/public/railwatch/assets/index-XjXcV-EO.js +0 -1
  316. data/public/railwatch/assets/index-aPCZX-pW.js +0 -1
  317. data/public/railwatch/assets/index-cTzBycCg.js +0 -1
  318. data/public/railwatch/assets/index-dpG-19ZN.js +0 -1
  319. data/public/railwatch/assets/index-gCvuyG1l.js +0 -1
  320. data/public/railwatch/assets/index-o1xqGvLo.js +0 -1
  321. data/public/railwatch/assets/index-w_skgMmK.js +0 -1
  322. data/public/railwatch/assets/inertia-CqnzqPVD.js +0 -97
  323. data/public/railwatch/assets/input-error-ByT28UdP.js +0 -1
  324. data/public/railwatch/assets/json-viewer-Bnu9RsJL.js +0 -1
  325. data/public/railwatch/assets/klass-nLiBV-Az.js +0 -1
  326. data/public/railwatch/assets/label-1nSzpSB6.js +0 -1
  327. data/public/railwatch/assets/layout-D7lE1Doo.js +0 -1
  328. data/public/railwatch/assets/live-dot-UdzOfzJb.js +0 -41
  329. data/public/railwatch/assets/nav-BRCgp2w3.js +0 -1
  330. data/public/railwatch/assets/new-C60G72DR.js +0 -1
  331. data/public/railwatch/assets/new-COyDKOOb.js +0 -1
  332. data/public/railwatch/assets/new-Cd9zPNJ8.js +0 -1
  333. data/public/railwatch/assets/new-DMJGKjmH.js +0 -1
  334. data/public/railwatch/assets/new-Omvf-c9s.js +0 -1
  335. data/public/railwatch/assets/new-vjRzERL3.js +0 -1
  336. data/public/railwatch/assets/onboarding-gUjJQc-6.js +0 -1
  337. data/public/railwatch/assets/origin-identity-M1YUPYS0.js +0 -1
  338. data/public/railwatch/assets/relative-time-DQ4kjowZ.js +0 -1
  339. data/public/railwatch/assets/route-BJkoEm7r.js +0 -1
  340. data/public/railwatch/assets/select-ocdQJdQo.js +0 -1
  341. data/public/railwatch/assets/separator-Ct2UcZ2x.js +0 -1
  342. data/public/railwatch/assets/series-chart-D6upsrSn.js +0 -1
  343. data/public/railwatch/assets/show-B4bwI9x2.js +0 -2
  344. data/public/railwatch/assets/show-BAjfddEn.js +0 -6
  345. data/public/railwatch/assets/show-BLJOyLb_.js +0 -1
  346. data/public/railwatch/assets/show-BoVyMQS6.js +0 -1
  347. data/public/railwatch/assets/show-Bsu1rvnK.js +0 -1
  348. data/public/railwatch/assets/show-C6OiJ6Hr.js +0 -1
  349. data/public/railwatch/assets/show-C8TkpEgm.js +0 -1
  350. data/public/railwatch/assets/show-CBFBiBY2.js +0 -1
  351. data/public/railwatch/assets/show-CEhPVMMT.js +0 -1
  352. data/public/railwatch/assets/show-CV-lG2b6.js +0 -2
  353. data/public/railwatch/assets/show-CX_pab9S.js +0 -2
  354. data/public/railwatch/assets/show-D6lgzL6n.js +0 -1
  355. data/public/railwatch/assets/show-DDVsv-uS.js +0 -1
  356. data/public/railwatch/assets/show-Dwiq-8hA.js +0 -1
  357. data/public/railwatch/assets/show-eKazflRI.js +0 -1
  358. data/public/railwatch/assets/show-yEOrk6Pt.js +0 -1
  359. data/public/railwatch/assets/stat-BNEfE8X1.js +0 -1
  360. data/public/railwatch/assets/status-badge-D0aylrM5.js +0 -1
  361. data/public/railwatch/assets/text-link-CP4lqUi6.js +0 -1
  362. data/public/railwatch/assets/textarea-DzJo2ds4.js +0 -1
  363. data/public/railwatch/assets/timeline-BNcJ9PMc.js +0 -1
  364. data/public/railwatch/assets/transition-BX2M1iaq.js +0 -5
@@ -0,0 +1,821 @@
1
+ import { Link, usePage } from "@inertiajs/react"
2
+ import { Check, ClipboardCopy, Copy } from "lucide-react"
3
+ import { useState } from "react"
4
+
5
+ import { DataTable } from "@/components/railwatch/data-table"
6
+ import { Flamegraph } from "@/components/railwatch/flamegraph"
7
+ import { ExceptionCard } from "@/components/railwatch/frames"
8
+ import { JsonViewer } from "@/components/railwatch/json-viewer"
9
+ import { OriginIdentity } from "@/components/railwatch/origin-identity"
10
+ import { PageHeader } from "@/components/railwatch/page-header"
11
+ import { SqlBlock } from "@/components/railwatch/sql-block"
12
+ import { Stat, StatStrip } from "@/components/railwatch/stat"
13
+ import { LevelBadge, StatusBadge } from "@/components/railwatch/status-badge"
14
+ import { Timeline } from "@/components/railwatch/timeline"
15
+ import { Badge } from "@/components/ui/badge"
16
+ import { Button } from "@/components/ui/button"
17
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
18
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
19
+ import { useClipboard } from "@/hooks/use-clipboard"
20
+ import EnvLayout from "@/layouts/env-layout"
21
+ import { executionPath } from "@/lib/execution-path"
22
+ import { bytes, count, ms, when } from "@/lib/format"
23
+ import * as R from "@/routes"
24
+ import type { ExceptionDetail, SharedProps, TimelineEntry } from "@/types"
25
+
26
+ interface Execution {
27
+ execution_id: string
28
+ kind: string
29
+ name: string
30
+ duration: number
31
+ status: number | null
32
+ outcome: string | null
33
+ method: string | null
34
+ route: string | null
35
+ controller: string | null
36
+ action: string | null
37
+ queue: string | null
38
+ attempt: number | null
39
+ queue_latency: number | null
40
+ queue_time: number | null
41
+ job_id: string | null
42
+ task_key: string | null
43
+ inertia_component: string | null
44
+ occurred_at: string
45
+ deploy: string | null
46
+ server: string | null
47
+ user_ref: string | null
48
+ tenant: string | null
49
+ trace_id: string
50
+ allocations: number | null
51
+ peak_memory: number | null
52
+ exception_preview: string | null
53
+ profiled: boolean
54
+ stages: Record<string, number>
55
+ counters: Record<string, number>
56
+ detail: Record<string, unknown>
57
+ }
58
+ interface Attachment {
59
+ id: number
60
+ name: string
61
+ content_type: string | null
62
+ bytes: number
63
+ truncated: boolean
64
+ viewable: boolean
65
+ occurred_at: string
66
+ }
67
+ interface Props {
68
+ execution: Execution
69
+ timeline: TimelineEntry[]
70
+ exceptions: ExceptionDetail[]
71
+ logs: {
72
+ id: number
73
+ level: string
74
+ message: string
75
+ tags: string[]
76
+ occurred_at: string
77
+ stage: string | null
78
+ }[]
79
+ queries: {
80
+ id: number
81
+ sql: string
82
+ duration: number
83
+ source: string | null
84
+ group_hash: string
85
+ row_count: number | null
86
+ stage: string | null
87
+ offset: number
88
+ }[]
89
+ spans: {
90
+ id: number
91
+ name: string
92
+ duration: number | null
93
+ status: string | null
94
+ attributes: Record<string, unknown>
95
+ offset: number
96
+ stage: string | null
97
+ occurred_at: string
98
+ }[]
99
+ profile: {
100
+ id: number
101
+ profiler: string
102
+ mode: string | null
103
+ interval: number | null
104
+ duration: number
105
+ samples: number
106
+ stacks_bytes: number | null
107
+ collapsed: string
108
+ truncated: boolean
109
+ } | null
110
+ attachments: Attachment[]
111
+ enqueued_jobs: {
112
+ id: number
113
+ name: string
114
+ queue: string
115
+ job_id: string
116
+ offset: number
117
+ attempt_execution_id: string | null
118
+ attempt_outcome: string | null
119
+ }[]
120
+ parent: { execution_id: string; kind: string; name: string } | null
121
+ trace: {
122
+ execution_id: string
123
+ kind: string
124
+ name: string
125
+ duration: number
126
+ status: number | null
127
+ outcome: string | null
128
+ occurred_at: string
129
+ parent_id: string | null
130
+ server: string | null
131
+ current: boolean
132
+ }[]
133
+ trace_url: string | null
134
+ issues: {
135
+ id: number
136
+ key: string
137
+ title: string
138
+ status: string
139
+ group_hash: string
140
+ }[]
141
+ person: { ref: string; name: string | null; email: string | null } | null
142
+ }
143
+
144
+ const kindLabel: Record<string, string> = {
145
+ request: "Request",
146
+ job_attempt: "Job attempt",
147
+ scheduled_task: "Scheduled task",
148
+ command: "Command",
149
+ channel_action: "Channel action",
150
+ }
151
+
152
+ // Tiny local copy-to-clipboard affordance for an inline id. CopyId doesn't
153
+ // exist yet (owned elsewhere), so this stays private to this page.
154
+ function CopyId({ value }: { value: string }) {
155
+ const [, copy] = useClipboard()
156
+ const [copied, setCopied] = useState(false)
157
+ return (
158
+ <button
159
+ type="button"
160
+ onClick={() => {
161
+ void (async () => {
162
+ const ok = await copy(value)
163
+ if (ok) {
164
+ setCopied(true)
165
+ setTimeout(() => setCopied(false), 1200)
166
+ }
167
+ })()
168
+ }}
169
+ className="hover:text-foreground text-muted-foreground ml-1 inline-flex align-middle"
170
+ title="Copy"
171
+ >
172
+ {copied ? <Check className="size-3" /> : <Copy className="size-3" />}
173
+ </button>
174
+ )
175
+ }
176
+
177
+ function parseContext(v: unknown): unknown {
178
+ try {
179
+ return JSON.parse(String(v)) as unknown
180
+ } catch {
181
+ return String(v)
182
+ }
183
+ }
184
+
185
+ function markdownReport(p: Props, x: Execution) {
186
+ const lines: string[] = []
187
+ lines.push(`# ${x.name}`)
188
+ lines.push("")
189
+ lines.push(`- status: ${x.status ?? x.outcome ?? "-"}`)
190
+ lines.push(`- duration: ${ms(x.duration)}`)
191
+ lines.push(`- occurred_at: ${x.occurred_at}`)
192
+ lines.push("")
193
+ lines.push("## Stages")
194
+ for (const [k, v] of Object.entries(x.stages)) lines.push(`- ${k}: ${ms(v)}`)
195
+ lines.push("")
196
+ lines.push("## Counters")
197
+ for (const [k, v] of Object.entries(x.counters)) lines.push(`- ${k}: ${v}`)
198
+ if (p.exceptions.length > 0) {
199
+ lines.push("")
200
+ lines.push("## Exceptions")
201
+ for (const ex of p.exceptions) {
202
+ lines.push(`### ${ex.class_name}: ${ex.message}`)
203
+ for (const f of ex.frames.filter((fr) => fr.in_app)) {
204
+ lines.push(`- ${f.file}:${f.line} in \`${f.function}\``)
205
+ }
206
+ }
207
+ }
208
+ const slow = [...p.queries]
209
+ .sort((a, b) => b.duration - a.duration)
210
+ .slice(0, 5)
211
+ if (slow.length > 0) {
212
+ lines.push("")
213
+ lines.push("## Slow queries")
214
+ for (const q of slow) lines.push(`- ${ms(q.duration, 2)} — \`${q.sql}\``)
215
+ }
216
+ return lines.join("\n")
217
+ }
218
+
219
+ export default function ExecutionShow(p: Props) {
220
+ const { environment, range, window } = usePage<SharedProps>().props
221
+ const a = environment!.application_id
222
+ const e = environment!.id
223
+ const x = p.execution
224
+ const detail = x.detail as Record<string, never>
225
+ const headers = (detail.headers ?? {}) as Record<string, string>
226
+ const inertia = detail.inertia as Record<string, unknown> | undefined
227
+ const [, copyMd] = useClipboard()
228
+ const [mdCopied, setMdCopied] = useState(false)
229
+ return (
230
+ <EnvLayout
231
+ title={x.name}
232
+ crumbs={[
233
+ { title: kindLabel[x.kind] ?? x.kind, href: "#" },
234
+ { title: x.name, href: "#" },
235
+ ]}
236
+ >
237
+ <PageHeader
238
+ withWindow={false}
239
+ title={
240
+ <span className="flex flex-wrap items-center gap-2">
241
+ <StatusBadge status={x.status} outcome={x.outcome} />
242
+ <span className="font-mono">{x.name}</span>
243
+ {x.profiled && (
244
+ <Badge variant="secondary" className="font-mono">
245
+ profiled
246
+ </Badge>
247
+ )}
248
+ </span>
249
+ }
250
+ description={
251
+ <span className="flex flex-wrap gap-x-3 text-xs">
252
+ <span>{when(x.occurred_at)}</span>
253
+ {x.server && <span>server {x.server}</span>}
254
+ {x.deploy && <span>deploy {x.deploy.slice(0, 12)}</span>}
255
+ {x.user_ref && (
256
+ <span className="inline-flex items-center gap-1">
257
+ origin user
258
+ <OriginIdentity
259
+ applicationId={a}
260
+ environmentId={e}
261
+ kind="user"
262
+ range={range}
263
+ window={window}
264
+ user_ref={x.user_ref}
265
+ tenant={x.tenant}
266
+ person={
267
+ p.person && {
268
+ ref: p.person.ref,
269
+ name: p.person.name ?? p.person.ref,
270
+ }
271
+ }
272
+ />
273
+ </span>
274
+ )}
275
+ {x.tenant && (
276
+ <span className="inline-flex items-center gap-1">
277
+ origin tenant
278
+ <OriginIdentity
279
+ applicationId={a}
280
+ environmentId={e}
281
+ kind="tenant"
282
+ range={range}
283
+ window={window}
284
+ user_ref={x.user_ref}
285
+ tenant={x.tenant}
286
+ person={null}
287
+ />
288
+ </span>
289
+ )}
290
+ </span>
291
+ }
292
+ actions={
293
+ <Button
294
+ type="button"
295
+ variant="outline"
296
+ size="sm"
297
+ onClick={() => {
298
+ void (async () => {
299
+ const ok = await copyMd(markdownReport(p, x))
300
+ if (ok) {
301
+ setMdCopied(true)
302
+ setTimeout(() => setMdCopied(false), 1200)
303
+ }
304
+ })()
305
+ }}
306
+ >
307
+ {mdCopied ? (
308
+ <Check className="size-3.5" />
309
+ ) : (
310
+ <ClipboardCopy className="size-3.5" />
311
+ )}
312
+ Copy as Markdown
313
+ </Button>
314
+ }
315
+ />
316
+ {p.issues.length > 0 && (
317
+ <div className="flex flex-wrap gap-2">
318
+ {p.issues.map((i) => (
319
+ <Link key={i.id} href={R.issuePath(i.id)}>
320
+ <Badge variant="destructive" className="font-mono">
321
+ {i.key}
322
+ </Badge>
323
+ </Link>
324
+ ))}
325
+ </div>
326
+ )}
327
+ <StatStrip>
328
+ <Stat
329
+ label="Duration"
330
+ value={ms(x.duration)}
331
+ hint={
332
+ x.queue_latency != null
333
+ ? `queued ${ms(x.queue_latency)}`
334
+ : undefined
335
+ }
336
+ />
337
+ <Stat
338
+ label="Queries"
339
+ value={x.counters.queries ?? 0}
340
+ hint={`${x.counters.cached_queries ?? 0} cached · ${x.counters.hydrated_models ?? 0} models`}
341
+ />
342
+ {x.queue_time != null && (
343
+ <Stat label="Queue time" value={ms(x.queue_time)} />
344
+ )}
345
+ <Stat label="Cache" value={x.counters.cache_events ?? 0} />
346
+ <Stat
347
+ label="Outgoing"
348
+ value={x.counters.outgoing_requests ?? 0}
349
+ hint={`${x.counters.jobs_enqueued ?? 0} jobs enqueued · ${x.counters.mail ?? 0} mail`}
350
+ />
351
+ <Stat
352
+ label="Allocations"
353
+ value={count(x.allocations)}
354
+ hint={x.peak_memory ? `RSS ${bytes(x.peak_memory)}` : undefined}
355
+ />
356
+ <Stat
357
+ label="Exceptions"
358
+ value={x.counters.exceptions ?? 0}
359
+ tone={x.counters.exceptions ? "destructive" : undefined}
360
+ hint={
361
+ x.counters.lazy_loads
362
+ ? `${x.counters.lazy_loads} lazy loads`
363
+ : undefined
364
+ }
365
+ />
366
+ </StatStrip>
367
+ {p.trace.length > 1 && (
368
+ <Card>
369
+ <CardHeader>
370
+ <CardTitle className="flex flex-wrap items-center justify-between gap-2">
371
+ Trace
372
+ {p.trace_url && (
373
+ <Link
374
+ href={p.trace_url}
375
+ className="text-primary text-xs font-normal hover:underline"
376
+ >
377
+ View distributed trace
378
+ </Link>
379
+ )}
380
+ </CardTitle>
381
+ </CardHeader>
382
+ <CardContent>
383
+ <div className="flex flex-wrap items-center gap-2 text-xs">
384
+ {p.trace.map((t, i) => {
385
+ const maxDur = Math.max(...p.trace.map((tt) => tt.duration), 1)
386
+ return (
387
+ <span
388
+ key={t.execution_id}
389
+ className="flex items-center gap-2"
390
+ >
391
+ {i > 0 && <span className="text-muted-foreground">→</span>}
392
+ <Link
393
+ href={executionPath({
394
+ applicationId: a,
395
+ environmentId: e,
396
+ source: t.kind,
397
+ executionId: t.execution_id,
398
+ })!}
399
+ className={
400
+ t.current
401
+ ? "bg-muted flex items-center gap-1.5 rounded px-2 py-1 font-semibold"
402
+ : "hover:bg-muted flex items-center gap-1.5 rounded px-2 py-1"
403
+ }
404
+ >
405
+ <StatusBadge status={t.status} outcome={t.outcome} />{" "}
406
+ <span className="font-mono">{t.name}</span>
407
+ <span className="bg-muted-foreground/25 relative h-1.5 w-10 overflow-hidden rounded-full">
408
+ <span
409
+ className="bg-primary absolute inset-y-0 left-0 rounded-full"
410
+ style={{
411
+ width: `${Math.max(4, (t.duration / maxDur) * 100)}%`,
412
+ }}
413
+ />
414
+ </span>
415
+ <span className="text-muted-foreground">
416
+ {ms(t.duration)}
417
+ </span>
418
+ </Link>
419
+ </span>
420
+ )
421
+ })}
422
+ </div>
423
+ </CardContent>
424
+ </Card>
425
+ )}
426
+ {p.exceptions.map((ex) => (
427
+ <Card key={ex.id} className="border-destructive/40">
428
+ <CardHeader>
429
+ <CardTitle>Exception</CardTitle>
430
+ </CardHeader>
431
+ <CardContent>
432
+ <ExceptionCard exception={ex} deploy={p.execution.deploy} />
433
+ </CardContent>
434
+ </Card>
435
+ ))}
436
+ <Card>
437
+ <CardHeader>
438
+ <CardTitle>Timeline</CardTitle>
439
+ </CardHeader>
440
+ <CardContent>
441
+ <Timeline entries={p.timeline} total={x.duration} stages={x.stages} />
442
+ </CardContent>
443
+ </Card>
444
+ {p.spans.length > 0 && (
445
+ <Card>
446
+ <CardHeader>
447
+ <CardTitle>Spans</CardTitle>
448
+ </CardHeader>
449
+ <CardContent>
450
+ <DataTable
451
+ rows={p.spans}
452
+ rowKey={(s) => s.id}
453
+ columns={[
454
+ {
455
+ key: "off",
456
+ header: "+ms",
457
+ align: "right",
458
+ cell: (s) => s.offset.toFixed(1),
459
+ },
460
+ {
461
+ key: "name",
462
+ header: "Span",
463
+ cell: (s) => (
464
+ <span className="font-mono text-xs">{s.name}</span>
465
+ ),
466
+ },
467
+ {
468
+ key: "status",
469
+ header: "Status",
470
+ cell: (s) => <StatusBadge outcome={s.status} />,
471
+ },
472
+ {
473
+ key: "attrs",
474
+ header: "Attributes",
475
+ cell: (s) => (
476
+ <span className="flex flex-wrap gap-1">
477
+ {Object.entries(s.attributes).map(([key, value]) => (
478
+ <span
479
+ key={key}
480
+ className="bg-muted rounded-sm px-1.5 font-mono text-[10px]"
481
+ >
482
+ {key}={String(value)}
483
+ </span>
484
+ ))}
485
+ </span>
486
+ ),
487
+ },
488
+ {
489
+ key: "dur",
490
+ header: "Duration",
491
+ align: "right",
492
+ cell: (s) => ms(s.duration, 2),
493
+ },
494
+ ]}
495
+ />
496
+ </CardContent>
497
+ </Card>
498
+ )}
499
+ {p.profile && (
500
+ <Card>
501
+ <CardHeader>
502
+ <CardTitle className="flex flex-wrap items-center justify-between gap-2">
503
+ Profile
504
+ <Link
505
+ href={R.applicationEnvironmentProfilePath(a, e, p.profile.id)}
506
+ className="text-primary text-xs font-normal hover:underline"
507
+ >
508
+ Open full profile
509
+ </Link>
510
+ </CardTitle>
511
+ </CardHeader>
512
+ <CardContent className="space-y-2">
513
+ <p className="text-muted-foreground font-mono text-[11px]">
514
+ {p.profile.profiler}
515
+ {p.profile.mode ? ` · ${p.profile.mode}` : ""} ·{" "}
516
+ {count(p.profile.samples)} samples · {ms(p.profile.duration)}
517
+ {p.profile.truncated && " · truncated for this page"}
518
+ </p>
519
+ <Flamegraph collapsed={p.profile.collapsed} />
520
+ </CardContent>
521
+ </Card>
522
+ )}
523
+ {p.attachments.length > 0 && (
524
+ <Card>
525
+ <CardHeader>
526
+ <CardTitle>Attachments</CardTitle>
527
+ </CardHeader>
528
+ <CardContent>
529
+ <DataTable
530
+ rows={p.attachments}
531
+ rowKey={(f) => f.id}
532
+ columns={[
533
+ {
534
+ key: "name",
535
+ header: "Name",
536
+ className: "max-w-0",
537
+ cell: (f) => (
538
+ <span className="flex items-center gap-1.5">
539
+ <span className="truncate font-mono text-xs">
540
+ {f.name}
541
+ </span>
542
+ {f.truncated && (
543
+ <Badge variant="secondary">truncated</Badge>
544
+ )}
545
+ </span>
546
+ ),
547
+ },
548
+ {
549
+ key: "type",
550
+ hideOnMobile: true,
551
+ header: "Type",
552
+ cell: (f) => (
553
+ <span className="text-muted-foreground font-mono text-xs">
554
+ {f.content_type ?? "–"}
555
+ </span>
556
+ ),
557
+ },
558
+ {
559
+ key: "size",
560
+ header: "Size",
561
+ align: "right",
562
+ cell: (f) => bytes(f.bytes),
563
+ },
564
+ {
565
+ key: "links",
566
+ header: "",
567
+ align: "right",
568
+ cell: (f) => (
569
+ <span className="flex justify-end gap-3">
570
+ {f.viewable && (
571
+ <a
572
+ className="text-primary hover:underline"
573
+ href={R.applicationEnvironmentAttachmentPath(
574
+ a,
575
+ e,
576
+ f.id,
577
+ { view: 1 },
578
+ )}
579
+ >
580
+ View
581
+ </a>
582
+ )}
583
+ <a
584
+ className="text-primary hover:underline"
585
+ href={R.applicationEnvironmentAttachmentPath(
586
+ a,
587
+ e,
588
+ f.id,
589
+ )}
590
+ >
591
+ Download
592
+ </a>
593
+ </span>
594
+ ),
595
+ },
596
+ ]}
597
+ />
598
+ </CardContent>
599
+ </Card>
600
+ )}
601
+ <Tabs defaultValue="queries">
602
+ <TabsList>
603
+ <TabsTrigger value="queries">
604
+ Queries ({p.queries.length})
605
+ </TabsTrigger>
606
+ <TabsTrigger value="logs">Logs ({p.logs.length})</TabsTrigger>
607
+ <TabsTrigger value="jobs">
608
+ Enqueued jobs ({p.enqueued_jobs.length})
609
+ </TabsTrigger>
610
+ <TabsTrigger value="request">Details</TabsTrigger>
611
+ </TabsList>
612
+ <TabsContent value="queries">
613
+ <DataTable
614
+ rows={p.queries}
615
+ rowKey={(q) => q.id}
616
+ empty="No queries."
617
+ columns={[
618
+ {
619
+ key: "off",
620
+ header: "+ms",
621
+ align: "right",
622
+ cell: (q) => q.offset.toFixed(1),
623
+ },
624
+ {
625
+ key: "sql",
626
+ header: "SQL",
627
+ cell: (q) => (
628
+ <Link
629
+ href={R.applicationEnvironmentQueryPath(a, e, q.group_hash)}
630
+ >
631
+ <SqlBlock sql={q.sql} className="hover:underline" />
632
+ </Link>
633
+ ),
634
+ },
635
+ {
636
+ key: "rows",
637
+ header: "Rows",
638
+ align: "right",
639
+ cell: (q) => q.row_count ?? "–",
640
+ },
641
+ {
642
+ key: "src",
643
+ header: "Source",
644
+ cell: (q) => (
645
+ <span className="text-muted-foreground font-mono text-xs">
646
+ {q.source ?? ""}
647
+ </span>
648
+ ),
649
+ },
650
+ {
651
+ key: "dur",
652
+ header: "Duration",
653
+ align: "right",
654
+ cell: (q) => ms(q.duration, 2),
655
+ },
656
+ ]}
657
+ />
658
+ </TabsContent>
659
+ <TabsContent value="logs">
660
+ <DataTable
661
+ rows={p.logs}
662
+ rowKey={(l) => l.id}
663
+ empty="No logs."
664
+ columns={[
665
+ {
666
+ key: "lvl",
667
+ header: "Level",
668
+ cell: (l) => <LevelBadge level={l.level} />,
669
+ },
670
+ {
671
+ key: "msg",
672
+ header: "Message",
673
+ cell: (l) => (
674
+ <span className="font-mono text-xs whitespace-pre-wrap">
675
+ {l.message}
676
+ </span>
677
+ ),
678
+ },
679
+ { key: "tags", header: "Tags", cell: (l) => l.tags.join(" ") },
680
+ {
681
+ key: "stage",
682
+ header: "Stage",
683
+ cell: (l) => (
684
+ <span className="text-muted-foreground text-xs">
685
+ {l.stage ?? ""}
686
+ </span>
687
+ ),
688
+ },
689
+ ]}
690
+ />
691
+ </TabsContent>
692
+ <TabsContent value="jobs">
693
+ <DataTable
694
+ rows={p.enqueued_jobs}
695
+ rowKey={(j) => j.id}
696
+ empty="No jobs enqueued."
697
+ columns={[
698
+ {
699
+ key: "name",
700
+ header: "Job",
701
+ cell: (j) => (
702
+ <span className="font-mono text-xs">{j.name}</span>
703
+ ),
704
+ },
705
+ { key: "queue", header: "Queue", cell: (j) => j.queue },
706
+ {
707
+ key: "id",
708
+ header: "Job id",
709
+ cell: (j) => (
710
+ <span className="font-mono text-xs">{j.job_id}</span>
711
+ ),
712
+ },
713
+ {
714
+ key: "attempt",
715
+ header: "Attempt",
716
+ cell: (j) =>
717
+ j.attempt_execution_id ? (
718
+ <Link
719
+ className="hover:underline"
720
+ href={R.applicationEnvironmentJobPath(
721
+ a,
722
+ e,
723
+ j.attempt_execution_id,
724
+ )}
725
+ >
726
+ <StatusBadge outcome={j.attempt_outcome} />
727
+ </Link>
728
+ ) : (
729
+ <span className="text-muted-foreground text-xs">
730
+ not seen yet
731
+ </span>
732
+ ),
733
+ },
734
+ ]}
735
+ />
736
+ </TabsContent>
737
+ <TabsContent value="request">
738
+ <div className="grid gap-4 lg:grid-cols-2">
739
+ <Card>
740
+ <CardHeader>
741
+ <CardTitle>Execution</CardTitle>
742
+ </CardHeader>
743
+ <CardContent>
744
+ <dl className="grid gap-y-1 text-xs sm:grid-cols-[10rem_1fr]">
745
+ {Object.entries({
746
+ kind: x.kind,
747
+ trace_id: x.trace_id,
748
+ execution_id: x.execution_id,
749
+ controller: x.controller && `${x.controller}#${x.action}`,
750
+ route: x.route,
751
+ url: detail.url,
752
+ ip: detail.ip,
753
+ format: detail.format,
754
+ queue: x.queue,
755
+ attempt: x.attempt,
756
+ job_id: x.job_id,
757
+ task_key: x.task_key,
758
+ schedule: detail.schedule,
759
+ inertia_component: x.inertia_component,
760
+ redirect_to: detail.redirect_to,
761
+ halted_callback: detail.halted_callback,
762
+ view_runtime: detail.view_runtime,
763
+ db_runtime: detail.db_runtime,
764
+ user_agent: detail.user_agent,
765
+ arguments: Array.isArray(detail.arguments_preview)
766
+ ? (detail.arguments_preview as string[]).join(", ")
767
+ : undefined,
768
+ })
769
+ .filter(
770
+ ([, v]) => v !== null && v !== undefined && v !== "",
771
+ )
772
+ .map(([k, v]) => (
773
+ <div key={k} className="contents">
774
+ <dt className="text-muted-foreground">{k}</dt>
775
+ <dd className="font-mono break-all">
776
+ {String(v)}
777
+ {(k === "trace_id" || k === "execution_id") && (
778
+ <CopyId value={String(v)} />
779
+ )}
780
+ </dd>
781
+ </div>
782
+ ))}
783
+ </dl>
784
+ {inertia && (
785
+ <div className="mt-3">
786
+ <JsonViewer data={inertia} />
787
+ </div>
788
+ )}
789
+ {detail.context && detail.context !== "{}" && (
790
+ <div className="mt-3">
791
+ <JsonViewer data={parseContext(detail.context)} />
792
+ </div>
793
+ )}
794
+ </CardContent>
795
+ </Card>
796
+ <Card>
797
+ <CardHeader>
798
+ <CardTitle>Headers</CardTitle>
799
+ </CardHeader>
800
+ <CardContent>
801
+ <dl className="grid gap-y-1 text-xs sm:grid-cols-[12rem_1fr]">
802
+ {Object.entries(headers).map(([k, v]) => (
803
+ <div key={k} className="contents">
804
+ <dt className="text-muted-foreground font-mono">{k}</dt>
805
+ <dd className="font-mono break-all">{v}</dd>
806
+ </div>
807
+ ))}
808
+ </dl>
809
+ {detail.payload != null && (
810
+ <div className="mt-3">
811
+ <JsonViewer data={detail.payload} />
812
+ </div>
813
+ )}
814
+ </CardContent>
815
+ </Card>
816
+ </div>
817
+ </TabsContent>
818
+ </Tabs>
819
+ </EnvLayout>
820
+ )
821
+ }