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,217 @@
1
+ // Builds "open this file" URLs: either into the application's repository on
2
+ // GitHub/GitLab (pinned to the deploy's git SHA when we have one, else the
3
+ // default branch) or into the user's local editor via its URL scheme. The
4
+ // repository URL comes from the application, the editor and local checkout
5
+ // path from the user's profile.
6
+
7
+ export interface SourceUrlOptions {
8
+ file: string
9
+ line?: number | null
10
+ repositoryUrl?: string | null
11
+ ref?: string | null
12
+ defaultBranch?: string | null
13
+ editor?: string | null
14
+ editorRoot?: string | null
15
+ }
16
+
17
+ function present(value: string | null | undefined): string | null {
18
+ const trimmed = (value ?? "").trim()
19
+ return trimmed === "" ? null : trimmed
20
+ }
21
+
22
+ function hasControlCharacters(value: string): boolean {
23
+ return [...value].some((character) => {
24
+ const code = character.codePointAt(0) ?? 0
25
+ return code <= 0x1f || (code >= 0x7f && code <= 0x9f)
26
+ })
27
+ }
28
+
29
+ function decodeToFixedPoint(value: string): string | null {
30
+ let decoded = value
31
+
32
+ try {
33
+ // Each useful decode consumes at least one percent escape, so an
34
+ // input-length bound reaches a fixed point without an arbitrary pass cap.
35
+ for (let remaining = value.length + 1; remaining > 0; remaining -= 1) {
36
+ const next = decodeURIComponent(decoded)
37
+ if (next === decoded) return decoded
38
+ decoded = next
39
+ }
40
+ } catch {
41
+ return null
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ function encodePathSegments(value: string): string | null {
48
+ try {
49
+ return value.split("/").map(encodeURIComponent).join("/")
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
55
+ function isAbsolutePath(value: string): boolean {
56
+ return (
57
+ value.startsWith("/") ||
58
+ value.startsWith("\\") ||
59
+ /^[a-z]:[\\/]/iu.test(value)
60
+ )
61
+ }
62
+
63
+ function sourcePath(value: string | null | undefined): {
64
+ raw: string
65
+ encoded: string
66
+ } | null {
67
+ const raw = present(value)
68
+ if (!raw || isAbsolutePath(raw)) return null
69
+
70
+ const decoded = decodeToFixedPoint(raw)
71
+ if (!decoded || isAbsolutePath(decoded)) return null
72
+
73
+ if (
74
+ decoded.includes("\\") ||
75
+ hasControlCharacters(decoded) ||
76
+ /%(?:2e|2f|5c)/iu.test(decoded) ||
77
+ decoded
78
+ .split("/")
79
+ .some((segment) => segment === "" || segment === "." || segment === "..")
80
+ )
81
+ return null
82
+
83
+ const encoded = encodePathSegments(raw)
84
+ return encoded ? { raw, encoded } : null
85
+ }
86
+
87
+ function encodedRoot(value: string | null | undefined): {
88
+ raw: string
89
+ encoded: string
90
+ } | null {
91
+ const raw = present((value ?? "").replace(/\/+$/, ""))
92
+ if (!raw) return null
93
+ const encoded = encodePathSegments(raw)
94
+ return encoded ? { raw, encoded } : null
95
+ }
96
+
97
+ function gitRef(value: string | null | undefined): {
98
+ raw: string
99
+ encoded: string
100
+ } | null {
101
+ const raw = present(value)
102
+ if (
103
+ !raw ||
104
+ raw === "@" ||
105
+ raw.startsWith("/") ||
106
+ raw.endsWith("/") ||
107
+ raw.endsWith(".") ||
108
+ raw.includes("//") ||
109
+ raw.includes("..") ||
110
+ raw.includes("@{") ||
111
+ raw.includes("[") ||
112
+ hasControlCharacters(raw) ||
113
+ /[ ~^:?*\\]/u.test(raw) ||
114
+ raw
115
+ .split("/")
116
+ .some(
117
+ (component) => component.startsWith(".") || component.endsWith(".lock"),
118
+ )
119
+ )
120
+ return null
121
+
122
+ try {
123
+ return { raw, encoded: encodeURIComponent(raw) }
124
+ } catch {
125
+ return null
126
+ }
127
+ }
128
+
129
+ // "https://github.com/org/repo.git/" -> "https://github.com/org/repo"
130
+ function repository(url: string | null | undefined): string | null {
131
+ return present((url ?? "").replace(/\/+$/, "").replace(/\.git$/, ""))
132
+ }
133
+
134
+ function host(url: string | null | undefined): "github" | "gitlab" | null {
135
+ const repo = repository(url)
136
+ if (!repo) return null
137
+ if (repo.includes("gitlab")) return "gitlab"
138
+ if (repo.includes("github")) return "github"
139
+ return null
140
+ }
141
+
142
+ export function sourceUrl({
143
+ file,
144
+ line,
145
+ repositoryUrl,
146
+ ref,
147
+ defaultBranch,
148
+ editor,
149
+ editorRoot,
150
+ }: SourceUrlOptions): string | null {
151
+ const path = sourcePath(file)
152
+ if (!path) return null
153
+
154
+ if (editor === "github" || editor === "gitlab") {
155
+ const repo = repository(repositoryUrl)
156
+ const at = gitRef(present(ref) ?? defaultBranch)
157
+ if (!repo || !at) return null
158
+ const blob = editor === "gitlab" ? "-/blob" : "blob"
159
+ return `${repo}/${blob}/${at.encoded}/${path.encoded}${line ? `#L${line}` : ""}`
160
+ }
161
+
162
+ const root = encodedRoot(editorRoot)
163
+ if (!root) return null
164
+ const target = `${root.raw}/${path.raw}`
165
+ const encodedTarget = `${root.encoded}/${path.encoded}`
166
+ const suffix = line ? `:${line}` : ""
167
+ switch (editor) {
168
+ case "vscode":
169
+ return `vscode://file${encodedTarget}${suffix}`
170
+ case "cursor":
171
+ return `cursor://file${encodedTarget}${suffix}`
172
+ case "zed":
173
+ return `zed://file${encodedTarget}${suffix}`
174
+ case "rubymine":
175
+ return `jetbrains://rubymine/navigate/reference?project=&path=${encodeURIComponent(`${target}${suffix}`)}`
176
+ case "textmate":
177
+ return `txmt://open?url=${encodeURIComponent(`file://${target}`)}${line ? `&line=${line}` : ""}`
178
+ default:
179
+ return null
180
+ }
181
+ }
182
+
183
+ export function commitUrl(
184
+ repositoryUrl: string | null | undefined,
185
+ sha: string | null | undefined,
186
+ ): string | null {
187
+ const repo = repository(repositoryUrl)
188
+ const commit = gitRef(sha)
189
+ if (!repo || !commit) return null
190
+ switch (host(repositoryUrl)) {
191
+ case "github":
192
+ return `${repo}/commit/${commit.encoded}`
193
+ case "gitlab":
194
+ return `${repo}/-/commit/${commit.encoded}`
195
+ default:
196
+ return null
197
+ }
198
+ }
199
+
200
+ export function compareUrl(
201
+ repositoryUrl: string | null | undefined,
202
+ from: string | null | undefined,
203
+ to: string | null | undefined,
204
+ ): string | null {
205
+ const repo = repository(repositoryUrl)
206
+ const start = gitRef(from)
207
+ const end = gitRef(to)
208
+ if (!repo || !start || !end) return null
209
+ switch (host(repositoryUrl)) {
210
+ case "github":
211
+ return `${repo}/compare/${start.encoded}...${end.encoded}`
212
+ case "gitlab":
213
+ return `${repo}/-/compare/${start.encoded}...${end.encoded}`
214
+ default:
215
+ return null
216
+ }
217
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,171 @@
1
+ import { Link, router, usePage } from "@inertiajs/react"
2
+ import { ChevronDown, ChevronRight } from "lucide-react"
3
+ import { useState } from "react"
4
+
5
+ import { EmptyState } from "@/components/railwatch/empty-state"
6
+ import { FilterBar } from "@/components/railwatch/filter-bar"
7
+ import { JsonViewer } from "@/components/railwatch/json-viewer"
8
+ import { PageHeader } from "@/components/railwatch/page-header"
9
+ import { RelativeTime } from "@/components/railwatch/relative-time"
10
+ import { Badge } from "@/components/ui/badge"
11
+ import { Button } from "@/components/ui/button"
12
+ import EnvLayout from "@/layouts/env-layout"
13
+ import { parseFilter, serializeFilter } from "@/lib/filter"
14
+ import * as R from "@/routes"
15
+ import type { SharedProps } from "@/types"
16
+
17
+ interface AlertRow {
18
+ id: number
19
+ event: string
20
+ status: string
21
+ sent_at: string | null
22
+ error: string | null
23
+ payload: Record<string, unknown>
24
+ created_at: string
25
+ integration: { kind: string; name: string }
26
+ issue: { id: number; key: string; title: string } | null
27
+ }
28
+ interface Props {
29
+ alerts: AlertRow[]
30
+ q: string
31
+ statuses: string[]
32
+ }
33
+
34
+ export default function AlertsIndex(p: Props) {
35
+ const { environment } = usePage<SharedProps>().props
36
+ const [expanded, setExpanded] = useState<number | null>(null)
37
+ const visit = (q: string) => {
38
+ const path = environment
39
+ ? R.applicationEnvironmentAlertsPath(
40
+ environment.application_id,
41
+ environment.id,
42
+ { q: q || undefined },
43
+ )
44
+ : R.alertsPath({ q: q || undefined })
45
+ router.visit(path, { preserveState: true, preserveScroll: true })
46
+ }
47
+ const retry = (id: number) =>
48
+ router.post(R.retryAlertPath(id), {}, { preserveScroll: true })
49
+
50
+ // "Did anything fail to deliver?" is the question this page exists to
51
+ // answer after an incident, so it gets a button rather than a token the
52
+ // reader has to know how to type.
53
+ const parsed = parseFilter(p.q)
54
+ const failedOnly = parsed.fields.status === "failed"
55
+ const toggleFailed = () => {
56
+ const fields = { ...parsed.fields }
57
+ if (failedOnly) delete fields.status
58
+ else fields.status = "failed"
59
+ visit(serializeFilter({ text: parsed.text, fields }))
60
+ }
61
+
62
+ return (
63
+ <EnvLayout title="Alerts" crumbs={[{ title: "Alerts", href: "#" }]}>
64
+ <PageHeader
65
+ withWindow={false}
66
+ title="Alerts"
67
+ description="Fired alerts across your integrations, newest first."
68
+ />
69
+ <div className="flex flex-wrap items-center gap-2">
70
+ <FilterBar
71
+ value={p.q}
72
+ fields={[
73
+ {
74
+ key: "event",
75
+ label: "Event",
76
+ options: ["new_issue", "regressed_issue", "threshold", "quota"],
77
+ },
78
+ { key: "status", label: "Status", options: p.statuses },
79
+ ]}
80
+ onChange={visit}
81
+ placeholder="event:new_issue status:failed"
82
+ />
83
+ <Button
84
+ size="sm"
85
+ variant={failedOnly ? "default" : "outline"}
86
+ className="h-8"
87
+ onClick={toggleFailed}
88
+ >
89
+ Failed only
90
+ </Button>
91
+ </div>
92
+ {p.alerts.length === 0 ? (
93
+ <EmptyState title="No alerts have fired." />
94
+ ) : (
95
+ <div className="divide-y rounded-md border">
96
+ {p.alerts.map((a) => {
97
+ const isOpen = expanded === a.id
98
+ return (
99
+ <div key={a.id} className="p-3">
100
+ <button
101
+ type="button"
102
+ className="flex w-full flex-wrap items-center gap-3 text-left text-sm"
103
+ onClick={() => setExpanded(isOpen ? null : a.id)}
104
+ >
105
+ {isOpen ? (
106
+ <ChevronDown className="text-muted-foreground size-4 shrink-0" />
107
+ ) : (
108
+ <ChevronRight className="text-muted-foreground size-4 shrink-0" />
109
+ )}
110
+ <span className="font-mono text-xs">{a.event}</span>
111
+ <span className="text-muted-foreground text-xs">
112
+ {a.integration.name} ({a.integration.kind})
113
+ </span>
114
+ {a.issue && (
115
+ <Link
116
+ href={R.issuePath(a.issue.id)}
117
+ onClick={(e) => e.stopPropagation()}
118
+ className="text-xs hover:underline"
119
+ >
120
+ <span className="font-mono font-semibold">
121
+ {a.issue.key}
122
+ </span>{" "}
123
+ <span className="text-muted-foreground">
124
+ {a.issue.title}
125
+ </span>
126
+ </Link>
127
+ )}
128
+ <span className="ml-auto flex items-center gap-2">
129
+ <Badge
130
+ variant={
131
+ a.status === "failed" ? "destructive" : "outline"
132
+ }
133
+ className="capitalize"
134
+ title={a.error ?? undefined}
135
+ >
136
+ {a.status}
137
+ </Badge>
138
+ <RelativeTime
139
+ iso={a.sent_at ?? a.created_at}
140
+ className="text-muted-foreground text-xs"
141
+ />
142
+ {a.status === "failed" && (
143
+ <Button
144
+ size="sm"
145
+ variant="outline"
146
+ onClick={(e) => {
147
+ e.stopPropagation()
148
+ retry(a.id)
149
+ }}
150
+ >
151
+ Retry
152
+ </Button>
153
+ )}
154
+ </span>
155
+ </button>
156
+ {isOpen && (
157
+ <div className="mt-3 space-y-2 pl-7">
158
+ {a.error && (
159
+ <p className="text-destructive text-xs">{a.error}</p>
160
+ )}
161
+ <JsonViewer data={a.payload} />
162
+ </div>
163
+ )}
164
+ </div>
165
+ )
166
+ })}
167
+ </div>
168
+ )}
169
+ </EnvLayout>
170
+ )
171
+ }
@@ -0,0 +1,171 @@
1
+ import { Link, router, usePage } from "@inertiajs/react"
2
+ import { Cable } from "lucide-react"
3
+
4
+ import { DataTable } from "@/components/railwatch/data-table"
5
+ import { EmptyState } from "@/components/railwatch/empty-state"
6
+ import { PageHeader } from "@/components/railwatch/page-header"
7
+ import { Badge } from "@/components/ui/badge"
8
+ import EnvLayout from "@/layouts/env-layout"
9
+ import { bytes, count, ms, when } from "@/lib/format"
10
+ import * as R from "@/routes"
11
+ import type { SharedProps } from "@/types"
12
+
13
+ interface Stream {
14
+ group_hash: string
15
+ kind: string
16
+ name: string
17
+ count: number
18
+ bytes: number
19
+ avg: number
20
+ }
21
+ interface Recent {
22
+ id: number
23
+ kind: string
24
+ failed: boolean
25
+ stream: string | null
26
+ channel: string | null
27
+ action: string | null
28
+ bytes: number | null
29
+ duration: number
30
+ occurred_at: string
31
+ execution_id: string | null
32
+ execution_preview: string | null
33
+ }
34
+ interface Props {
35
+ streams: Stream[]
36
+ recent: Recent[]
37
+ }
38
+
39
+ export default function Broadcasts(p: Props) {
40
+ const { environment } = usePage<SharedProps>().props
41
+ const a = environment!.application_id
42
+ const e = environment!.id
43
+ const recentHref = (r: Recent) =>
44
+ r.execution_id
45
+ ? R.applicationEnvironmentRequestPath(a, e, r.execution_id)
46
+ : null
47
+ return (
48
+ <EnvLayout title="Broadcasts">
49
+ <PageHeader
50
+ title="Broadcasts"
51
+ description="Action Cable broadcasts, transmits, and channel actions (covers inertia_cable and Turbo Streams)."
52
+ />
53
+ <DataTable
54
+ rows={p.streams}
55
+ rowKey={(s) => `${s.group_hash}-${s.kind}`}
56
+ empty={
57
+ <EmptyState
58
+ icon={Cable}
59
+ title="No broadcasts in this window"
60
+ description="Action Cable broadcasts appear here as your app sends them."
61
+ />
62
+ }
63
+ columns={[
64
+ { key: "k", header: "Kind", cell: (s) => s.kind },
65
+ {
66
+ key: "n",
67
+ header: "Stream / channel",
68
+ cell: (s) => <span className="font-mono text-xs">{s.name}</span>,
69
+ },
70
+ {
71
+ key: "c",
72
+ hideOnMobile: true,
73
+ header: "Count",
74
+ align: "right",
75
+ cell: (s) => count(s.count),
76
+ },
77
+ {
78
+ key: "b",
79
+ hideOnMobile: true,
80
+ header: "Bytes",
81
+ align: "right",
82
+ cell: (s) => bytes(s.bytes),
83
+ },
84
+ {
85
+ key: "avg",
86
+ hideOnMobile: true,
87
+ header: "Avg",
88
+ align: "right",
89
+ cell: (s) => ms(s.avg, 3),
90
+ },
91
+ ]}
92
+ />
93
+ <h2 className="text-sm font-semibold">Recent</h2>
94
+ <DataTable
95
+ rows={p.recent}
96
+ rowKey={(r) => r.id}
97
+ empty={
98
+ <EmptyState
99
+ icon={Cable}
100
+ title="No broadcasts in this window"
101
+ description="Action Cable broadcasts appear here as your app sends them."
102
+ />
103
+ }
104
+ keyboardNav={{
105
+ onOpen: (r, opts) => {
106
+ const href = recentHref(r)
107
+ if (!href) return
108
+ if (opts?.newTab) window.open(href, "_blank")
109
+ else router.visit(href)
110
+ },
111
+ }}
112
+ columns={[
113
+ {
114
+ key: "when",
115
+ header: "When",
116
+ cell: (r) => <span className="text-xs">{when(r.occurred_at)}</span>,
117
+ },
118
+ {
119
+ key: "k",
120
+ header: "Kind",
121
+ cell: (r) =>
122
+ r.failed ? (
123
+ <span className="flex items-center gap-1.5">
124
+ {r.kind}
125
+ <Badge variant="destructive">failed</Badge>
126
+ </span>
127
+ ) : (
128
+ r.kind
129
+ ),
130
+ },
131
+ {
132
+ key: "s",
133
+ header: "Stream / channel",
134
+ cell: (r) => (
135
+ <span className="font-mono text-xs">
136
+ {r.stream ?? r.channel}
137
+ {r.action ? `#${r.action}` : ""}
138
+ </span>
139
+ ),
140
+ },
141
+ {
142
+ key: "b",
143
+ hideOnMobile: true,
144
+ header: "Bytes",
145
+ align: "right",
146
+ cell: (r) => bytes(r.bytes),
147
+ },
148
+ {
149
+ key: "d",
150
+ header: "Duration",
151
+ align: "right",
152
+ cell: (r) => ms(r.duration, 3),
153
+ },
154
+ {
155
+ key: "in",
156
+ hideOnMobile: true,
157
+ header: "In",
158
+ cell: (r) => {
159
+ const href = recentHref(r)
160
+ return href ? (
161
+ <Link className="font-mono text-xs hover:underline" href={href}>
162
+ {r.execution_preview}
163
+ </Link>
164
+ ) : null
165
+ },
166
+ },
167
+ ]}
168
+ />
169
+ </EnvLayout>
170
+ )
171
+ }
@@ -0,0 +1,127 @@
1
+ import { router, usePage } from "@inertiajs/react"
2
+ import { Layers } from "lucide-react"
3
+
4
+ import { VolumePanel } from "@/components/railwatch/chart-panel"
5
+ import { DataTable } from "@/components/railwatch/data-table"
6
+ import { EmptyState } from "@/components/railwatch/empty-state"
7
+ import { FilterBar } from "@/components/railwatch/filter-bar"
8
+ import { PageHeader } from "@/components/railwatch/page-header"
9
+ import { SparklineCell } from "@/components/railwatch/sparkline-cell"
10
+ import EnvLayout from "@/layouts/env-layout"
11
+ import { count, ms } from "@/lib/format"
12
+ import * as R from "@/routes"
13
+ import type { SeriesPoint, SharedProps } from "@/types"
14
+
15
+ interface Key {
16
+ group_hash: string
17
+ key: string
18
+ count: number
19
+ hits: number
20
+ misses: number
21
+ hit_rate: number | null
22
+ avg: number
23
+ sparkline: number[]
24
+ }
25
+ interface Props {
26
+ keys: Key[]
27
+ series: SeriesPoint[]
28
+ q: string
29
+ }
30
+
31
+ export default function CacheEvents(p: Props) {
32
+ const { environment, window } = usePage<SharedProps>().props
33
+ const a = environment!.application_id
34
+ const e = environment!.id
35
+ return (
36
+ <EnvLayout title="Cache">
37
+ <PageHeader
38
+ title="Cache"
39
+ description="Rails.cache reads, writes, and hit rates by key shape (ids collapsed)."
40
+ />
41
+ <VolumePanel
42
+ legend="cache"
43
+ label="Cache ops"
44
+ seriesLabel="Cache ops"
45
+ data={p.series}
46
+ />
47
+ <FilterBar
48
+ value={p.q}
49
+ fields={[{ key: "store", label: "Store" }]}
50
+ onChange={(q) =>
51
+ router.visit(
52
+ R.applicationEnvironmentCacheEventsPath(a, e, {
53
+ window,
54
+ q: q || undefined,
55
+ }),
56
+ { preserveState: true },
57
+ )
58
+ }
59
+ placeholder="store:MemoryStore"
60
+ />
61
+ <DataTable
62
+ rows={p.keys}
63
+ rowKey={(k) => k.group_hash}
64
+ empty={
65
+ <EmptyState
66
+ icon={Layers}
67
+ title="No cache activity in this window"
68
+ description="Cache reads and writes instrumented by the gem appear here."
69
+ />
70
+ }
71
+ columns={[
72
+ {
73
+ key: "k",
74
+ header: "Store · key",
75
+ cell: (k) => <span className="font-mono text-xs">{k.key}</span>,
76
+ },
77
+ {
78
+ key: "trend",
79
+ hideOnMobile: true,
80
+ header: "Trend",
81
+ cell: (k) => <SparklineCell data={k.sparkline} />,
82
+ },
83
+ {
84
+ key: "n",
85
+ header: "Ops",
86
+ align: "right",
87
+ cell: (k) => count(k.count),
88
+ },
89
+ {
90
+ key: "h",
91
+ header: "Hits",
92
+ align: "right",
93
+ cell: (k) => count(k.hits),
94
+ },
95
+ {
96
+ key: "m",
97
+ hideOnMobile: true,
98
+ header: "Misses",
99
+ align: "right",
100
+ cell: (k) => count(k.misses),
101
+ },
102
+ {
103
+ key: "r",
104
+ hideOnMobile: true,
105
+ header: "Hit rate",
106
+ align: "right",
107
+ cell: (k) =>
108
+ k.hit_rate == null ? (
109
+ "–"
110
+ ) : (
111
+ <span className={k.hit_rate < 50 ? "text-amber-600" : ""}>
112
+ {k.hit_rate}%
113
+ </span>
114
+ ),
115
+ },
116
+ {
117
+ key: "avg",
118
+ hideOnMobile: true,
119
+ header: "Avg",
120
+ align: "right",
121
+ cell: (k) => ms(k.avg, 3),
122
+ },
123
+ ]}
124
+ />
125
+ </EnvLayout>
126
+ )
127
+ }