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,244 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Commit Mono for numbers, identifiers, and labels (SIL OFL). Roboto for UI
4
+ text, matching the rebulk.com brand system (Apache 2.0); four weights,
5
+ subset to the Latin range (about 18 KB each) and preloaded from the layout
6
+ head so they arrive with the HTML rather than after first layout. */
7
+ @font-face {
8
+ font-family: "CommitMono";
9
+ font-style: normal;
10
+ font-weight: 400;
11
+ font-display: swap;
12
+ src: url("../fonts/CommitMono-Regular.woff2") format("woff2");
13
+ }
14
+ @font-face {
15
+ font-family: "CommitMono";
16
+ font-style: normal;
17
+ font-weight: 700;
18
+ font-display: swap;
19
+ src: url("../fonts/CommitMono-Bold.woff2") format("woff2");
20
+ }
21
+ @font-face {
22
+ font-family: "Roboto";
23
+ font-style: normal;
24
+ font-weight: 400;
25
+ font-display: swap;
26
+ src: url("../fonts/Roboto-Regular.woff2") format("woff2");
27
+ }
28
+ @font-face {
29
+ font-family: "Roboto";
30
+ font-style: normal;
31
+ font-weight: 500;
32
+ font-display: swap;
33
+ src: url("../fonts/Roboto-Medium.woff2") format("woff2");
34
+ }
35
+ @font-face {
36
+ font-family: "Roboto";
37
+ font-style: normal;
38
+ font-weight: 700;
39
+ font-display: swap;
40
+ src: url("../fonts/Roboto-Bold.woff2") format("woff2");
41
+ }
42
+ @font-face {
43
+ font-family: "Roboto";
44
+ font-style: normal;
45
+ font-weight: 900;
46
+ font-display: swap;
47
+ src: url("../fonts/Roboto-Black.woff2") format("woff2");
48
+ }
49
+ /* While Roboto is still in flight the page renders in this face instead: a
50
+ local sans with its metrics overridden to Roboto's (size-adjust from the
51
+ average glyph width, ascent and descent from Roboto's hhea table), so line
52
+ breaks and box heights are the same before and after the swap and nothing
53
+ moves when the real font lands. */
54
+ @font-face {
55
+ font-family: "Roboto Fallback";
56
+ src:
57
+ local("Liberation Sans"), local("Arial"), local("Helvetica Neue"),
58
+ local("Helvetica");
59
+ size-adjust: 96.1%;
60
+ ascent-override: 96.5%;
61
+ descent-override: 25.4%;
62
+ line-gap-override: 0%;
63
+ }
64
+
65
+ @import "tw-animate-css";
66
+ @plugin "@tailwindcss/typography";
67
+ @plugin "@tailwindcss/forms";
68
+
69
+ @custom-variant dark (&:is(.dark *));
70
+
71
+ /* Railwatch palette: the rebulk.com brand system. Navy ink, gold as the sole
72
+ accent, Roboto for UI text. Light: soft blue-grey page, white panels, navy
73
+ text. Dark: the navy stack from the marketing site (#020618 page, #0F1729
74
+ panel, #1E2A45 edge) with gold as primary. Status vocabulary is shared by
75
+ badges, charts and signals: grey = fine, amber = warning/4xx, rose =
76
+ error/5xx, green = live. */
77
+ :root {
78
+ --background: #f5f7fb;
79
+ --foreground: #0f1729;
80
+ --card: #ffffff;
81
+ --card-foreground: #0f1729;
82
+ --popover: #ffffff;
83
+ --popover-foreground: #0f1729;
84
+ --primary: #0f1729;
85
+ --primary-foreground: #ffffff;
86
+ --secondary: #e9edf5;
87
+ --secondary-foreground: #0f1729;
88
+ --muted: #eef1f7;
89
+ --muted-foreground: #5e708f;
90
+ --accent: #eef1f7;
91
+ --accent-foreground: #0f1729;
92
+ --destructive: oklch(0.645 0.246 16.439);
93
+ --destructive-foreground: oklch(0.645 0.246 16.439);
94
+ --border: #dbe1ec;
95
+ --input: #dbe1ec;
96
+ --ring: rgb(253 193 26 / 0.6);
97
+ --ok: #c9d2e3;
98
+ --warning: oklch(0.769 0.188 70.08);
99
+ --danger: oklch(0.645 0.246 16.439);
100
+ --live: oklch(0.696 0.17 162.48);
101
+ --chart-1: var(--ok);
102
+ --chart-2: #0f1729;
103
+ --chart-3: var(--warning);
104
+ --chart-4: var(--warning);
105
+ --chart-5: var(--danger);
106
+ --radius: 0.5rem;
107
+ --sidebar: #ffffff;
108
+ --sidebar-foreground: #5e708f;
109
+ --sidebar-primary: #0f1729;
110
+ --sidebar-primary-foreground: #ffffff;
111
+ --sidebar-accent: #eef1f7;
112
+ --sidebar-accent-foreground: #0f1729;
113
+ --sidebar-border: #dbe1ec;
114
+ --sidebar-ring: rgb(253 193 26 / 0.6);
115
+ }
116
+
117
+ .dark {
118
+ --background: #020618;
119
+ --foreground: #e6ebf4;
120
+ --card: #0f1729;
121
+ --card-foreground: #e6ebf4;
122
+ --popover: #0f1729;
123
+ --popover-foreground: #e6ebf4;
124
+ --primary: #fdc11a;
125
+ --primary-foreground: #020618;
126
+ --secondary: #1e2a45;
127
+ --secondary-foreground: #e6ebf4;
128
+ --muted: #0a1122;
129
+ --muted-foreground: #93a6c4;
130
+ --accent: #1e2a45;
131
+ --accent-foreground: #ffffff;
132
+ --destructive-foreground: oklch(0.72 0.2 16.439);
133
+ --border: #1e2a45;
134
+ --input: #26355a;
135
+ --ring: rgb(253 193 26 / 0.5);
136
+ --ok: #26355a;
137
+ --warning: oklch(0.666 0.179 58.318);
138
+ --chart-2: #fdc11a;
139
+ --sidebar: #0a1122;
140
+ --sidebar-foreground: #93a6c4;
141
+ --sidebar-primary: #fdc11a;
142
+ --sidebar-primary-foreground: #020618;
143
+ --sidebar-accent: #1e2a45;
144
+ --sidebar-accent-foreground: #ffffff;
145
+ --sidebar-border: #1e2a45;
146
+ --sidebar-ring: rgb(253 193 26 / 0.5);
147
+ }
148
+
149
+ @theme inline {
150
+ --color-background: var(--background);
151
+ --color-foreground: var(--foreground);
152
+ --color-card: var(--card);
153
+ --color-card-foreground: var(--card-foreground);
154
+ --color-popover: var(--popover);
155
+ --color-popover-foreground: var(--popover-foreground);
156
+ --color-primary: var(--primary);
157
+ --color-primary-foreground: var(--primary-foreground);
158
+ --color-secondary: var(--secondary);
159
+ --color-secondary-foreground: var(--secondary-foreground);
160
+ --color-muted: var(--muted);
161
+ --color-muted-foreground: var(--muted-foreground);
162
+ --color-accent: var(--accent);
163
+ --color-accent-foreground: var(--accent-foreground);
164
+ --color-destructive: var(--destructive);
165
+ --color-destructive-foreground: var(--destructive-foreground);
166
+ --color-border: var(--border);
167
+ --color-input: var(--input);
168
+ --color-ring: var(--ring);
169
+ --color-chart-1: var(--chart-1);
170
+ --color-chart-2: var(--chart-2);
171
+ --color-chart-3: var(--chart-3);
172
+ --color-chart-4: var(--chart-4);
173
+ --color-chart-5: var(--chart-5);
174
+ --color-ok: var(--ok);
175
+ --color-warning: var(--warning);
176
+ --color-danger: var(--danger);
177
+ --color-live: var(--live);
178
+ --font-sans:
179
+ "Roboto", "Roboto Fallback", ui-sans-serif, system-ui, sans-serif;
180
+ --font-mono: "CommitMono", ui-monospace, SFMono-Regular, Menlo, monospace;
181
+ --radius-sm: calc(var(--radius) - 4px);
182
+ --radius-md: calc(var(--radius) - 2px);
183
+ --radius-lg: var(--radius);
184
+ --radius-xl: calc(var(--radius) + 4px);
185
+ --color-sidebar: var(--sidebar);
186
+ --color-sidebar-foreground: var(--sidebar-foreground);
187
+ --color-sidebar-primary: var(--sidebar-primary);
188
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
189
+ --color-sidebar-accent: var(--sidebar-accent);
190
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
191
+ --color-sidebar-border: var(--sidebar-border);
192
+ --color-sidebar-ring: var(--sidebar-ring);
193
+ --animate-accordion-down: accordion-down 0.2s ease-out;
194
+ --animate-accordion-up: accordion-up 0.2s ease-out;
195
+
196
+ @keyframes accordion-down {
197
+ from {
198
+ height: 0;
199
+ }
200
+ to {
201
+ height: var(--radix-accordion-content-height);
202
+ }
203
+ }
204
+
205
+ @keyframes accordion-up {
206
+ from {
207
+ height: var(--radix-accordion-content-height);
208
+ }
209
+ to {
210
+ height: 0;
211
+ }
212
+ }
213
+ }
214
+
215
+ @layer base {
216
+ * {
217
+ @apply border-border outline-ring/50;
218
+ }
219
+ html {
220
+ color-scheme: light dark;
221
+ }
222
+ body {
223
+ @apply bg-background text-foreground antialiased;
224
+ font-feature-settings: "cv11", "ss01", "tnum";
225
+ letter-spacing: -0.02em;
226
+ }
227
+ /* Small uppercase mono label used on stat tiles, table heads, and badges. */
228
+ .label-caps {
229
+ @apply text-muted-foreground font-mono text-[11px] tracking-wide uppercase;
230
+ }
231
+ /* Full-text search hit inside a log message snippet. */
232
+ .fts-mark {
233
+ background-color: color-mix(in oklch, var(--warning) 30%, transparent);
234
+ @apply rounded-[2px] px-0.5 text-inherit;
235
+ }
236
+ /* Faint dot grid backdrop behind the app canvas. */
237
+ .dotted-canvas {
238
+ background-image: radial-gradient(
239
+ color-mix(in oklch, var(--foreground) 7%, transparent) 1px,
240
+ transparent 1px
241
+ );
242
+ background-size: 22px 22px;
243
+ }
244
+ }
@@ -0,0 +1,95 @@
1
+ import type { ResolvedComponent } from "@inertiajs/react"
2
+ import { createInertiaApp } from "@inertiajs/react"
3
+ import { StrictMode } from "react"
4
+ import { createRoot } from "react-dom/client"
5
+
6
+ import { initializeTheme } from "@/hooks/use-appearance"
7
+ import PersistentLayout from "@/layouts/persistent-layout"
8
+ import { railwatchRootOptions, startRailwatch } from "@/lib/railwatch"
9
+ import { configure as configureRoutes } from "@/routes"
10
+
11
+ // When served by the embedded engine, every route helper must carry the
12
+ // engine's mount point. The engine layout emits it; the hosted platform does
13
+ // not, so this is a no-op there.
14
+ const mount = document.querySelector<HTMLMetaElement>(
15
+ 'meta[name="railwatch-mount"]',
16
+ )?.content
17
+ if (mount) configureRoutes({ default_url_options: { script_name: mount } })
18
+
19
+ const appName = import.meta.env.VITE_APP_NAME ?? "Railwatch"
20
+
21
+ void createInertiaApp({
22
+ // Set default page title
23
+ // see https://inertia-rails.dev/guide/title-and-meta
24
+ //
25
+ title: (title) => (title ? `${title} - ${appName}` : appName),
26
+
27
+ resolve: async (name) => {
28
+ const pages = import.meta.glob<{ default: ResolvedComponent }>([
29
+ "../pages/**/*.tsx",
30
+ "!../pages/**/*.test.tsx",
31
+ ])
32
+ const loadPage = pages[`../pages/${name}.tsx`]
33
+ if (!loadPage) {
34
+ throw new Error(`Missing Inertia page component: '${name}.tsx'`)
35
+ }
36
+ const page = await loadPage()
37
+
38
+ // To use a default layout, import the Layout component
39
+ // and use the following line.
40
+ // see https://inertia-rails.dev/guide/pages#default-layouts
41
+ //
42
+ page.default.layout ??= [PersistentLayout]
43
+
44
+ return page
45
+ },
46
+
47
+ setup({ el, App, props }) {
48
+ // Uncomment the following to enable SSR hydration:
49
+ // if (el.hasChildNodes()) {
50
+ // hydrateRoot(el, <App {...props} />)
51
+ // return
52
+ // }
53
+ createRoot(el, railwatchRootOptions()).render(
54
+ <StrictMode>
55
+ <App {...props} />
56
+ </StrictMode>,
57
+ )
58
+ },
59
+
60
+ defaults: {
61
+ form: {
62
+ forceIndicesArrayFormatInFormData: false,
63
+ withAllErrors: true,
64
+ },
65
+ future: {
66
+ useScriptElementForInitialPage: true,
67
+ useDataInertiaHeadAttribute: true,
68
+ useDialogForErrorModal: true,
69
+ preserveEqualProps: true,
70
+ },
71
+ },
72
+
73
+ progress: {
74
+ color: "#4B5563",
75
+ },
76
+ }).catch((error) => {
77
+ // This ensures this entrypoint is only loaded on Inertia pages
78
+ // by checking for the presence of the root element (#app by default).
79
+ // Feel free to remove this `catch` if you don't need it.
80
+ if (document.getElementById("app")) {
81
+ throw error
82
+ } else {
83
+ console.error(
84
+ "Missing root element.\n\n" +
85
+ "If you see this error, it probably means you loaded Inertia.js on non-Inertia pages.\n" +
86
+ 'Consider moving <%= vite_typescript_tag "inertia" %> to the Inertia-specific layout instead.',
87
+ )
88
+ }
89
+ })
90
+
91
+ // This will set light / dark mode on load...
92
+ initializeTheme()
93
+
94
+ // Report Inertia visit timings to Railwatch (the platform monitors itself).
95
+ startRailwatch()
@@ -0,0 +1,67 @@
1
+ import { useCallback, useEffect, useState } from "react"
2
+
3
+ export type Appearance = "light" | "dark" | "system"
4
+
5
+ const prefersDark = () => {
6
+ if (typeof window === "undefined") {
7
+ return false
8
+ }
9
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
10
+ }
11
+
12
+ const applyTheme = (appearance: Appearance) => {
13
+ const isDark =
14
+ appearance === "dark" || (appearance === "system" && prefersDark())
15
+
16
+ document.documentElement.classList.toggle("dark", isDark)
17
+ document.documentElement.style.colorScheme = isDark ? "dark" : "light"
18
+ }
19
+
20
+ const mediaQuery = () => {
21
+ if (typeof window === "undefined") {
22
+ return null
23
+ }
24
+
25
+ return window.matchMedia("(prefers-color-scheme: dark)")
26
+ }
27
+
28
+ const handleSystemThemeChange = () => {
29
+ const currentAppearance = localStorage.getItem("appearance") as Appearance
30
+ applyTheme(currentAppearance ?? "system")
31
+ }
32
+
33
+ export function initializeTheme() {
34
+ const savedAppearance =
35
+ (localStorage.getItem("appearance") as Appearance) || "system"
36
+
37
+ applyTheme(savedAppearance)
38
+
39
+ mediaQuery()?.addEventListener("change", handleSystemThemeChange)
40
+ }
41
+
42
+ export function useAppearance() {
43
+ const [appearance, setAppearance] = useState<Appearance>(() => {
44
+ if (typeof window === "undefined") return "system"
45
+ const saved = localStorage.getItem("appearance") as Appearance | null
46
+ return saved ?? "system"
47
+ })
48
+
49
+ const updateAppearance = useCallback((mode: Appearance) => {
50
+ setAppearance(mode)
51
+ if (mode === "system") {
52
+ localStorage.removeItem("appearance")
53
+ } else {
54
+ localStorage.setItem("appearance", mode)
55
+ }
56
+ applyTheme(mode)
57
+ }, [])
58
+
59
+ useEffect(() => {
60
+ applyTheme(appearance)
61
+
62
+ return () =>
63
+ mediaQuery()?.removeEventListener("change", handleSystemThemeChange)
64
+ }, [appearance])
65
+
66
+ return { appearance, updateAppearance } as const
67
+ }
@@ -0,0 +1,32 @@
1
+ // Credit: https://usehooks-ts.com/
2
+ import { useCallback, useState } from "react"
3
+
4
+ type CopiedValue = string | null
5
+
6
+ type CopyFn = (text: string) => Promise<boolean>
7
+
8
+ export function useClipboard(): [CopiedValue, CopyFn] {
9
+ const [copiedText, setCopiedText] = useState<CopiedValue>(null)
10
+
11
+ const copy: CopyFn = useCallback(async (text) => {
12
+ if (!navigator?.clipboard) {
13
+ console.warn("Clipboard not supported")
14
+
15
+ return false
16
+ }
17
+
18
+ try {
19
+ await navigator.clipboard.writeText(text)
20
+ setCopiedText(text)
21
+
22
+ return true
23
+ } catch (error) {
24
+ console.warn("Copy failed", error)
25
+ setCopiedText(null)
26
+
27
+ return false
28
+ }
29
+ }, [])
30
+
31
+ return [copiedText, copy]
32
+ }
@@ -0,0 +1,23 @@
1
+ import { usePage } from "@inertiajs/react"
2
+ import { useEffect } from "react"
3
+
4
+ import { signalToast } from "@/components/railwatch/signal-toast"
5
+ import type { FlashData } from "@/types"
6
+
7
+ // Rails flash to signal aspects: alert is stop, warning is caution, notice
8
+ // is clear.
9
+ function showFlash(flash: FlashData) {
10
+ if (flash.alert) signalToast("stop", flash.alert)
11
+ if (flash.warning) signalToast("caution", flash.warning)
12
+ if (flash.notice) signalToast("clear", flash.notice)
13
+ }
14
+
15
+ export function useFlash() {
16
+ const { flash } = usePage()
17
+
18
+ useEffect(() => {
19
+ // setTimeout + cleanup prevents double-firing in React StrictMode
20
+ const timeout = setTimeout(() => showFlash(flash), 0)
21
+ return () => clearTimeout(timeout)
22
+ }, [flash])
23
+ }
@@ -0,0 +1,63 @@
1
+ import { useEffect, useRef } from "react"
2
+
3
+ type HotkeyMap = Record<string, () => void>
4
+
5
+ const CHORD_TIMEOUT_MS = 800
6
+
7
+ // Tiny keyboard shortcut hook supporting single keys ("?") and two-key
8
+ // chords ("g d") without pulling in a dependency. Ignores keystrokes while
9
+ // focus is in a form field, and while a modifier key is held.
10
+ export function useHotkeys(map: HotkeyMap) {
11
+ const mapRef = useRef(map)
12
+ useEffect(() => {
13
+ mapRef.current = map
14
+ })
15
+
16
+ const pending = useRef<string | null>(null)
17
+ const timer = useRef<number | undefined>(undefined)
18
+
19
+ useEffect(() => {
20
+ const onKeyDown = (event: KeyboardEvent) => {
21
+ const target = event.target as HTMLElement
22
+ if (
23
+ target.tagName === "INPUT" ||
24
+ target.tagName === "TEXTAREA" ||
25
+ target.tagName === "SELECT" ||
26
+ target.isContentEditable
27
+ ) {
28
+ return
29
+ }
30
+ if (event.metaKey || event.ctrlKey || event.altKey) return
31
+
32
+ if (pending.current) {
33
+ const combo = `${pending.current} ${event.key}`
34
+ window.clearTimeout(timer.current)
35
+ pending.current = null
36
+ if (mapRef.current[combo]) {
37
+ event.preventDefault()
38
+ mapRef.current[combo]()
39
+ return
40
+ }
41
+ }
42
+
43
+ if (mapRef.current[event.key]) {
44
+ event.preventDefault()
45
+ mapRef.current[event.key]()
46
+ return
47
+ }
48
+
49
+ if (event.key === "g") {
50
+ pending.current = "g"
51
+ timer.current = window.setTimeout(() => {
52
+ pending.current = null
53
+ }, CHORD_TIMEOUT_MS)
54
+ }
55
+ }
56
+
57
+ document.addEventListener("keydown", onKeyDown)
58
+ return () => {
59
+ document.removeEventListener("keydown", onKeyDown)
60
+ window.clearTimeout(timer.current)
61
+ }
62
+ }, [])
63
+ }
@@ -0,0 +1,15 @@
1
+ import { useCallback } from "react"
2
+
3
+ export function useInitials() {
4
+ return useCallback((fullName: string): string => {
5
+ const names = fullName.trim().split(" ")
6
+
7
+ if (names.length === 0) return ""
8
+ if (names.length === 1) return names[0].charAt(0).toUpperCase()
9
+
10
+ const firstInitial = names[0].charAt(0)
11
+ const lastInitial = names[names.length - 1].charAt(0)
12
+
13
+ return `${firstInitial}${lastInitial}`.toUpperCase()
14
+ }, [])
15
+ }