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.
- checksums.yaml +4 -4
- data/AGENTS.md +42 -26
- data/CHANGELOG.md +41 -0
- data/README.md +40 -29
- data/app/frontend/components/app-content.tsx +26 -0
- data/app/frontend/components/app-logo-icon.tsx +23 -0
- data/app/frontend/components/app-logo.tsx +14 -0
- data/app/frontend/components/app-shell.tsx +38 -0
- data/app/frontend/components/app-sidebar-header.tsx +54 -0
- data/app/frontend/components/app-sidebar.tsx +94 -0
- data/app/frontend/components/app-wordmark.tsx +29 -0
- data/app/frontend/components/breadcrumbs.tsx +46 -0
- data/app/frontend/components/icon.tsx +16 -0
- data/app/frontend/components/input-error.tsx +18 -0
- data/app/frontend/components/nav-footer.tsx +46 -0
- data/app/frontend/components/nav-main.tsx +41 -0
- data/app/frontend/components/nav-user.tsx +50 -0
- data/app/frontend/components/railwatch/chart-hover.tsx +34 -0
- data/app/frontend/components/railwatch/chart-panel.tsx +170 -0
- data/app/frontend/components/railwatch/code.tsx +28 -0
- data/app/frontend/components/railwatch/command-palette.tsx +132 -0
- data/app/frontend/components/railwatch/copy-block.tsx +52 -0
- data/app/frontend/components/railwatch/copy-id.tsx +49 -0
- data/app/frontend/components/railwatch/cursor-load-more.tsx +39 -0
- data/app/frontend/components/railwatch/data-table.tsx +195 -0
- data/app/frontend/components/railwatch/digit-roll.tsx +312 -0
- data/app/frontend/components/railwatch/empty-state.tsx +159 -0
- data/app/frontend/components/railwatch/env-nav.tsx +130 -0
- data/app/frontend/components/railwatch/env-switcher.tsx +145 -0
- data/app/frontend/components/railwatch/filter-bar.tsx +153 -0
- data/app/frontend/components/railwatch/flamegraph.tsx +324 -0
- data/app/frontend/components/railwatch/frames.tsx +295 -0
- data/app/frontend/components/railwatch/json-viewer.tsx +159 -0
- data/app/frontend/components/railwatch/live-dot.tsx +78 -0
- data/app/frontend/components/railwatch/live-toggle.tsx +76 -0
- data/app/frontend/components/railwatch/metric-chart.tsx +112 -0
- data/app/frontend/components/railwatch/mobile-page-switcher.tsx +89 -0
- data/app/frontend/components/railwatch/onboarding.tsx +91 -0
- data/app/frontend/components/railwatch/origin-identity.tsx +84 -0
- data/app/frontend/components/railwatch/page-header.tsx +42 -0
- data/app/frontend/components/railwatch/percentile-picker.tsx +18 -0
- data/app/frontend/components/railwatch/relative-time.tsx +18 -0
- data/app/frontend/components/railwatch/saved-views.tsx +232 -0
- data/app/frontend/components/railwatch/segmented.tsx +64 -0
- data/app/frontend/components/railwatch/series-chart.tsx +400 -0
- data/app/frontend/components/railwatch/shortcuts-dialog.tsx +89 -0
- data/app/frontend/components/railwatch/signal-toast.tsx +121 -0
- data/app/frontend/components/railwatch/sort-header.tsx +37 -0
- data/app/frontend/components/railwatch/source-link.tsx +57 -0
- data/app/frontend/components/railwatch/sparkline-cell.tsx +14 -0
- data/app/frontend/components/railwatch/sql-block.tsx +116 -0
- data/app/frontend/components/railwatch/stat.tsx +173 -0
- data/app/frontend/components/railwatch/status-badge.tsx +79 -0
- data/app/frontend/components/railwatch/step-picker.tsx +57 -0
- data/app/frontend/components/railwatch/timeline.tsx +405 -0
- data/app/frontend/components/railwatch/utilisation-bar.tsx +44 -0
- data/app/frontend/components/railwatch/window-picker.tsx +147 -0
- data/app/frontend/components/ui/avatar.tsx +51 -0
- data/app/frontend/components/ui/badge.tsx +46 -0
- data/app/frontend/components/ui/breadcrumb.tsx +109 -0
- data/app/frontend/components/ui/button.tsx +60 -0
- data/app/frontend/components/ui/card.tsx +92 -0
- data/app/frontend/components/ui/chart.tsx +374 -0
- data/app/frontend/components/ui/checkbox.tsx +32 -0
- data/app/frontend/components/ui/collapsible.tsx +31 -0
- data/app/frontend/components/ui/command.tsx +182 -0
- data/app/frontend/components/ui/dialog.tsx +158 -0
- data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
- data/app/frontend/components/ui/hover-card.tsx +42 -0
- data/app/frontend/components/ui/input.tsx +21 -0
- data/app/frontend/components/ui/label.tsx +24 -0
- data/app/frontend/components/ui/popover.tsx +87 -0
- data/app/frontend/components/ui/select.tsx +185 -0
- data/app/frontend/components/ui/separator.tsx +28 -0
- data/app/frontend/components/ui/sheet.tsx +137 -0
- data/app/frontend/components/ui/sidebar.tsx +726 -0
- data/app/frontend/components/ui/skeleton.tsx +13 -0
- data/app/frontend/components/ui/sonner.tsx +41 -0
- data/app/frontend/components/ui/table.tsx +114 -0
- data/app/frontend/components/ui/tabs.tsx +91 -0
- data/app/frontend/components/ui/textarea.tsx +18 -0
- data/app/frontend/components/ui/tooltip.tsx +61 -0
- data/app/frontend/components/user-info.tsx +32 -0
- data/app/frontend/components/user-menu-content.tsx +94 -0
- data/app/frontend/entrypoints/application.css +244 -0
- data/app/frontend/entrypoints/inertia.tsx +95 -0
- data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
- data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
- data/app/frontend/hooks/use-appearance.tsx +67 -0
- data/app/frontend/hooks/use-clipboard.ts +32 -0
- data/app/frontend/hooks/use-flash.tsx +23 -0
- data/app/frontend/hooks/use-hotkeys.ts +63 -0
- data/app/frontend/hooks/use-initials.tsx +15 -0
- data/app/frontend/hooks/use-live.ts +179 -0
- data/app/frontend/hooks/use-mobile-navigation.ts +8 -0
- data/app/frontend/hooks/use-mobile.ts +21 -0
- data/app/frontend/hooks/use-percentile.ts +24 -0
- data/app/frontend/hooks/use-reduced-motion.ts +25 -0
- data/app/frontend/hooks/use-row-nav.ts +106 -0
- data/app/frontend/hooks/use-step.ts +27 -0
- data/app/frontend/hooks/use-ticker.ts +12 -0
- data/app/frontend/hooks/use-window.ts +58 -0
- data/app/frontend/layouts/app/app-sidebar-layout.tsx +107 -0
- data/app/frontend/layouts/app-layout.tsx +21 -0
- data/app/frontend/layouts/env-layout.tsx +53 -0
- data/app/frontend/layouts/persistent-layout.tsx +18 -0
- data/app/frontend/lib/cable.ts +10 -0
- data/app/frontend/lib/env-pages.ts +236 -0
- data/app/frontend/lib/execution-path.ts +55 -0
- data/app/frontend/lib/explain.ts +50 -0
- data/app/frontend/lib/filter.ts +25 -0
- data/app/frontend/lib/flamegraph.ts +155 -0
- data/app/frontend/lib/format.ts +71 -0
- data/app/frontend/lib/keyboard.ts +18 -0
- data/app/frontend/lib/source-link.ts +217 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/pages/alerts/index.tsx +171 -0
- data/app/frontend/pages/broadcasts/index.tsx +171 -0
- data/app/frontend/pages/cache_events/index.tsx +127 -0
- data/app/frontend/pages/commands/index.tsx +143 -0
- data/app/frontend/pages/deploys/index.tsx +94 -0
- data/app/frontend/pages/deploys/show.tsx +309 -0
- data/app/frontend/pages/deprecations/index.tsx +176 -0
- data/app/frontend/pages/exceptions/index.tsx +271 -0
- data/app/frontend/pages/executions/show.tsx +821 -0
- data/app/frontend/pages/issues/index.tsx +399 -0
- data/app/frontend/pages/issues/show.tsx +1367 -0
- data/app/frontend/pages/jobs/index.tsx +381 -0
- data/app/frontend/pages/jobs/klass.tsx +215 -0
- data/app/frontend/pages/llm_calls/index.tsx +664 -0
- data/app/frontend/pages/logs/index.tsx +303 -0
- data/app/frontend/pages/mails/index.tsx +218 -0
- data/app/frontend/pages/notifications/index.tsx +161 -0
- data/app/frontend/pages/outgoing_requests/index.tsx +309 -0
- data/app/frontend/pages/overview/show.tsx +333 -0
- data/app/frontend/pages/people/index.tsx +102 -0
- data/app/frontend/pages/people/show.tsx +149 -0
- data/app/frontend/pages/processes/index.tsx +335 -0
- data/app/frontend/pages/profiles/index.tsx +110 -0
- data/app/frontend/pages/profiles/show.tsx +105 -0
- data/app/frontend/pages/queries/index.tsx +492 -0
- data/app/frontend/pages/queries/show.tsx +276 -0
- data/app/frontend/pages/releases/index.tsx +190 -0
- data/app/frontend/pages/releases/release-health.tsx +68 -0
- data/app/frontend/pages/releases/release-path.ts +25 -0
- data/app/frontend/pages/releases/show.tsx +262 -0
- data/app/frontend/pages/requests/index.tsx +235 -0
- data/app/frontend/pages/requests/route.tsx +159 -0
- data/app/frontend/pages/scheduled_tasks/index.tsx +220 -0
- data/app/frontend/pages/spans/index.tsx +229 -0
- data/app/frontend/pages/spans/show.tsx +220 -0
- data/app/frontend/pages/storage_ops/index.tsx +150 -0
- data/app/frontend/pages/tenants/index.tsx +236 -0
- data/app/frontend/pages/tenants/show.tsx +477 -0
- data/app/frontend/pages/tenants/tenant-path.ts +26 -0
- data/app/frontend/pages/thresholds/index.tsx +414 -0
- data/app/frontend/pages/traces/show.tsx +208 -0
- data/app/frontend/pages/transactions/index.tsx +220 -0
- data/app/frontend/pages/view_renders/index.tsx +241 -0
- data/app/frontend/pages/visits/index.tsx +321 -0
- data/app/frontend/public/icon.png +0 -0
- data/app/frontend/public/icon.svg +5 -0
- data/app/frontend/routes/index.d.ts +2136 -0
- data/app/frontend/routes/index.js +2112 -0
- data/app/frontend/types/globals.d.ts +9 -0
- data/app/frontend/types/index.ts +288 -0
- data/app/frontend/types/vite-env.d.ts +9 -0
- data/app/jobs/railwatch/rollup_job.rb +3 -1
- data/app/models/railwatch/ingest/rollup_absorber.rb +1 -1
- data/docs/ai-and-mcp.md +9 -3
- data/docs/configuration.md +68 -27
- data/docs/embedded.md +76 -43
- data/docs/faq.md +22 -10
- data/docs/getting-started.md +116 -41
- data/docs/records.md +13 -10
- data/docs/replacing-nightwatch.md +16 -14
- data/docs/replacing-sentry.md +19 -10
- data/docs/security.md +24 -3
- data/docs/self-hosting.md +9 -1
- data/docs/testing.md +14 -4
- data/docs/troubleshooting.md +62 -23
- data/lib/generators/railwatch/install/install_generator.rb +5 -4
- data/lib/generators/railwatch/install/templates/initializer.rb.tt +2 -2
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +8 -3
- data/llms.txt +21 -18
- data/public/railwatch/assets/app-layout-LvxN9xSS.js +41 -0
- data/public/railwatch/assets/application-Cc5v_Vk9.css +1 -0
- data/public/railwatch/assets/{arrow-up-EmePsTKP.js → arrow-up-DG01k9AL.js} +1 -1
- data/public/railwatch/assets/{braces-iZ7MEfYP.js → braces-C3w7pfxS.js} +1 -1
- data/public/railwatch/assets/{card-DqdEEKcJ.js → card-Dpwlr0uY.js} +1 -1
- data/public/railwatch/assets/{chart-C4k7Ub9W.js → chart-DJHv3R7W.js} +1 -1
- data/public/railwatch/assets/{chart-hover-2yZG3w2u.js → chart-hover-CEH-DtXO.js} +1 -1
- data/public/railwatch/assets/{chart-panel-CONkZcn9.js → chart-panel-CUdIzJvk.js} +1 -1
- data/public/railwatch/assets/code-BRZprwun.js +1 -0
- data/public/railwatch/assets/copy-id-CwhGGiwF.js +1 -0
- data/public/railwatch/assets/cursor-load-more-uF1_955Q.js +1 -0
- data/public/railwatch/assets/data-table-DYi-N0hm.js +1 -0
- data/public/railwatch/assets/env-layout-HdcOaPPX.js +1 -0
- data/public/railwatch/assets/{execution-path-DWovhgLi.js → execution-path-CElXXFO_.js} +1 -1
- data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
- data/public/railwatch/assets/{flamegraph-BNbNIuKT.js → flamegraph-SKD9yxW9.js} +2 -2
- data/public/railwatch/assets/frames-CUpyJQDy.js +1 -0
- data/public/railwatch/assets/index-2W-ppwap.js +1 -0
- data/public/railwatch/assets/index-74ocuU5d.js +1 -0
- data/public/railwatch/assets/{index-Zkt4W_CM.js → index-B8NA62Lz.js} +2 -2
- data/public/railwatch/assets/index-BBCEkLqp.js +1 -0
- data/public/railwatch/assets/index-BDcglJzX.js +1 -0
- data/public/railwatch/assets/index-B_KFb9LS.js +1 -0
- data/public/railwatch/assets/index-BqK_VKom.js +1 -0
- data/public/railwatch/assets/index-BxCXBHP2.js +1 -0
- data/public/railwatch/assets/index-C1jc1OoH.js +1 -0
- data/public/railwatch/assets/index-C7zREldp.js +1 -0
- data/public/railwatch/assets/index-CEetm4oC.js +1 -0
- data/public/railwatch/assets/index-CIaZkokF.js +1 -0
- data/public/railwatch/assets/index-CNH85TZp.js +1 -0
- data/public/railwatch/assets/index-Ce4SB5AB.js +1 -0
- data/public/railwatch/assets/index-CiRMSYMn.js +1 -0
- data/public/railwatch/assets/index-CjxXgOs-.js +1 -0
- data/public/railwatch/assets/index-ClL-PYEV.js +1 -0
- data/public/railwatch/assets/index-CteZ6fop.js +1 -0
- data/public/railwatch/assets/index-CvqfEExO.js +1 -0
- data/public/railwatch/assets/index-CxTLT-Qh.js +1 -0
- data/public/railwatch/assets/index-DJd3OUSd.js +1 -0
- data/public/railwatch/assets/index-DPD-rLp6.js +1 -0
- data/public/railwatch/assets/index-DYK_dps4.js +1 -0
- data/public/railwatch/assets/index-DrayARrI.js +1 -0
- data/public/railwatch/assets/index-HRXGir-0.js +1 -0
- data/public/railwatch/assets/index-UJOUALr_.js +1 -0
- data/public/railwatch/assets/index-d-3J7gQ5.js +1 -0
- data/public/railwatch/assets/index-oQrvBpOm.js +1 -0
- data/public/railwatch/assets/inertia-FoqYZAk8.js +97 -0
- data/public/railwatch/assets/json-viewer-CxqMojEm.js +1 -0
- data/public/railwatch/assets/klass-DEF-Vat5.js +1 -0
- data/public/railwatch/assets/origin-identity-Y6gvfQVR.js +1 -0
- data/public/railwatch/assets/{percentile-picker-BxIVblBw.js → percentile-picker-E2aUdLxx.js} +1 -1
- data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
- data/public/railwatch/assets/{release-health-Cb1tMOOO.js → release-health-C8WSCDKD.js} +1 -1
- data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
- data/public/railwatch/assets/{segmented-C4oWE73o.js → segmented-BWee8uc9.js} +1 -1
- data/public/railwatch/assets/select-G7rNBzv0.js +1 -0
- data/public/railwatch/assets/series-chart-C5nXIUc5.js +1 -0
- data/public/railwatch/assets/show-BEWisLQw.js +1 -0
- data/public/railwatch/assets/show-BEa9PGu1.js +2 -0
- data/public/railwatch/assets/{show-gtwKsTsl.js → show-BiT_Aloc.js} +3 -3
- data/public/railwatch/assets/show-BpCO49d3.js +1 -0
- data/public/railwatch/assets/show-CH7yxt7R.js +1 -0
- data/public/railwatch/assets/show-CTO8Lvd2.js +1 -0
- data/public/railwatch/assets/show-DnYsDMUt.js +2 -0
- data/public/railwatch/assets/show-DyzHM527.js +1 -0
- data/public/railwatch/assets/show-Nfpxs8su.js +1 -0
- data/public/railwatch/assets/show-ddjUMo9G.js +1 -0
- data/public/railwatch/assets/show-ksjbSzDH.js +1 -0
- data/public/railwatch/assets/{sort-header-CPM6fhXv.js → sort-header-uOq5VlrB.js} +1 -1
- data/public/railwatch/assets/{sparkline-cell-CXCxXPWt.js → sparkline-cell-DgGij0cu.js} +1 -1
- data/public/railwatch/assets/stat-BTzNKTm2.js +1 -0
- data/public/railwatch/assets/status-badge-BDkOXa-C.js +1 -0
- data/public/railwatch/assets/{tenant-path-C4aM_cO0.js → tenant-path-DU0ezKSU.js} +1 -1
- data/public/railwatch/assets/{use-clipboard-C1ApSwzA.js → use-clipboard-DoENmlWY.js} +1 -1
- data/public/railwatch/assets/{use-live-DWwLg1Nj.js → use-live-CE_fN5_6.js} +1 -1
- data/public/railwatch/manifest.json +612 -1591
- metadata +242 -117
- data/public/railwatch/assets/app-layout-2xAKndVD.js +0 -1
- data/public/railwatch/assets/app-wordmark-Bt6XqURo.js +0 -1
- data/public/railwatch/assets/appearance-DyCfeh9o.js +0 -1
- data/public/railwatch/assets/application-B7h1MIhi.css +0 -1
- data/public/railwatch/assets/auth-layout-BuwogWMZ.js +0 -1
- data/public/railwatch/assets/badge-Dv7I-ebP.js +0 -1
- data/public/railwatch/assets/checkbox-DKbThbxr.js +0 -1
- data/public/railwatch/assets/code-CYahYQHZ.js +0 -1
- data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
- data/public/railwatch/assets/copy-block-CbJum9s8.js +0 -1
- data/public/railwatch/assets/copy-id-JmvLfkmx.js +0 -1
- data/public/railwatch/assets/cursor-load-more-C1ylbqqL.js +0 -1
- data/public/railwatch/assets/data-table-CVKR2wkq.js +0 -1
- data/public/railwatch/assets/edit-BXF5oNYA.js +0 -1
- data/public/railwatch/assets/edit-BwSEVrTG.js +0 -8
- data/public/railwatch/assets/edit-D5BRDwLT.js +0 -1
- data/public/railwatch/assets/empty-state-Bn48Gplm.js +0 -1
- data/public/railwatch/assets/env-layout-CKLL8Ds7.js +0 -1
- data/public/railwatch/assets/filter-bar-xcbJC93z.js +0 -1
- data/public/railwatch/assets/frames-Bk39xfJG.js +0 -1
- data/public/railwatch/assets/google-sign-in-button-B5as2ps1.js +0 -1
- data/public/railwatch/assets/index-Atp1FTgX.js +0 -1
- data/public/railwatch/assets/index-B-FXAo1w.js +0 -1
- data/public/railwatch/assets/index-B0xgzHuN.js +0 -1
- data/public/railwatch/assets/index-BDVShWMs.js +0 -1
- data/public/railwatch/assets/index-BRvRkfob.js +0 -1
- data/public/railwatch/assets/index-BVWapDeB.js +0 -1
- data/public/railwatch/assets/index-Bd4YpOIO.js +0 -1
- data/public/railwatch/assets/index-BeQw69GM.js +0 -1
- data/public/railwatch/assets/index-BfitxVBv.js +0 -1
- data/public/railwatch/assets/index-C-EwH4lO.js +0 -1
- data/public/railwatch/assets/index-C8JIdb3_.js +0 -1
- data/public/railwatch/assets/index-CBFCtJAR.js +0 -1
- data/public/railwatch/assets/index-CDXlITyG.js +0 -1
- data/public/railwatch/assets/index-COd9lT84.js +0 -1
- data/public/railwatch/assets/index-C_hQmVTP.js +0 -1
- data/public/railwatch/assets/index-CcennT28.js +0 -1
- data/public/railwatch/assets/index-CiaeS-n7.js +0 -1
- data/public/railwatch/assets/index-CrA7jBTK.js +0 -1
- data/public/railwatch/assets/index-D0mKj3Hg.js +0 -1
- data/public/railwatch/assets/index-DV5MdmpY.js +0 -1
- data/public/railwatch/assets/index-DaE-1xhx.js +0 -1
- data/public/railwatch/assets/index-DdVg9LKX.js +0 -1
- data/public/railwatch/assets/index-DeLtMWou.js +0 -1
- data/public/railwatch/assets/index-DiQ4mqT_.js +0 -1
- data/public/railwatch/assets/index-LKgpCzoR.js +0 -1
- data/public/railwatch/assets/index-S0NVaLj9.js +0 -1
- data/public/railwatch/assets/index-Vj93BTR5.js +0 -1
- data/public/railwatch/assets/index-XjXcV-EO.js +0 -1
- data/public/railwatch/assets/index-aPCZX-pW.js +0 -1
- data/public/railwatch/assets/index-cTzBycCg.js +0 -1
- data/public/railwatch/assets/index-dpG-19ZN.js +0 -1
- data/public/railwatch/assets/index-gCvuyG1l.js +0 -1
- data/public/railwatch/assets/index-o1xqGvLo.js +0 -1
- data/public/railwatch/assets/index-w_skgMmK.js +0 -1
- data/public/railwatch/assets/inertia-CqnzqPVD.js +0 -97
- data/public/railwatch/assets/input-error-ByT28UdP.js +0 -1
- data/public/railwatch/assets/json-viewer-Bnu9RsJL.js +0 -1
- data/public/railwatch/assets/klass-nLiBV-Az.js +0 -1
- data/public/railwatch/assets/label-1nSzpSB6.js +0 -1
- data/public/railwatch/assets/layout-D7lE1Doo.js +0 -1
- data/public/railwatch/assets/live-dot-UdzOfzJb.js +0 -41
- data/public/railwatch/assets/nav-BRCgp2w3.js +0 -1
- data/public/railwatch/assets/new-C60G72DR.js +0 -1
- data/public/railwatch/assets/new-COyDKOOb.js +0 -1
- data/public/railwatch/assets/new-Cd9zPNJ8.js +0 -1
- data/public/railwatch/assets/new-DMJGKjmH.js +0 -1
- data/public/railwatch/assets/new-Omvf-c9s.js +0 -1
- data/public/railwatch/assets/new-vjRzERL3.js +0 -1
- data/public/railwatch/assets/onboarding-gUjJQc-6.js +0 -1
- data/public/railwatch/assets/origin-identity-M1YUPYS0.js +0 -1
- data/public/railwatch/assets/relative-time-DQ4kjowZ.js +0 -1
- data/public/railwatch/assets/route-BJkoEm7r.js +0 -1
- data/public/railwatch/assets/select-ocdQJdQo.js +0 -1
- data/public/railwatch/assets/separator-Ct2UcZ2x.js +0 -1
- data/public/railwatch/assets/series-chart-D6upsrSn.js +0 -1
- data/public/railwatch/assets/show-B4bwI9x2.js +0 -2
- data/public/railwatch/assets/show-BAjfddEn.js +0 -6
- data/public/railwatch/assets/show-BLJOyLb_.js +0 -1
- data/public/railwatch/assets/show-BoVyMQS6.js +0 -1
- data/public/railwatch/assets/show-Bsu1rvnK.js +0 -1
- data/public/railwatch/assets/show-C6OiJ6Hr.js +0 -1
- data/public/railwatch/assets/show-C8TkpEgm.js +0 -1
- data/public/railwatch/assets/show-CBFBiBY2.js +0 -1
- data/public/railwatch/assets/show-CEhPVMMT.js +0 -1
- data/public/railwatch/assets/show-CV-lG2b6.js +0 -2
- data/public/railwatch/assets/show-CX_pab9S.js +0 -2
- data/public/railwatch/assets/show-D6lgzL6n.js +0 -1
- data/public/railwatch/assets/show-DDVsv-uS.js +0 -1
- data/public/railwatch/assets/show-Dwiq-8hA.js +0 -1
- data/public/railwatch/assets/show-eKazflRI.js +0 -1
- data/public/railwatch/assets/show-yEOrk6Pt.js +0 -1
- data/public/railwatch/assets/stat-BNEfE8X1.js +0 -1
- data/public/railwatch/assets/status-badge-D0aylrM5.js +0 -1
- data/public/railwatch/assets/text-link-CP4lqUi6.js +0 -1
- data/public/railwatch/assets/textarea-DzJo2ds4.js +0 -1
- data/public/railwatch/assets/timeline-BNcJ9PMc.js +0 -1
- data/public/railwatch/assets/transition-BX2M1iaq.js +0 -5
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Check, ChevronDown, ChevronRight, Copy } from "lucide-react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
import { Button } from "@/components/ui/button"
|
|
5
|
+
import { useClipboard } from "@/hooks/use-clipboard"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
type Json = null | boolean | number | string | Json[] | { [key: string]: Json }
|
|
9
|
+
|
|
10
|
+
function isContainer(v: unknown): v is Json[] | Record<string, Json> {
|
|
11
|
+
return v !== null && typeof v === "object"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function primitiveClass(v: Json) {
|
|
15
|
+
if (v === null) return "text-muted-foreground"
|
|
16
|
+
if (typeof v === "string") return "text-emerald-600 dark:text-emerald-400"
|
|
17
|
+
if (typeof v === "number") return "text-amber-600 dark:text-amber-400"
|
|
18
|
+
if (typeof v === "boolean") return "text-primary"
|
|
19
|
+
return ""
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Primitive({ value }: { value: string | number | boolean | null }) {
|
|
23
|
+
const text =
|
|
24
|
+
value === null
|
|
25
|
+
? "null"
|
|
26
|
+
: typeof value === "string"
|
|
27
|
+
? `"${value}"`
|
|
28
|
+
: String(value)
|
|
29
|
+
return <span className={primitiveClass(value)}>{text}</span>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Node({
|
|
33
|
+
label,
|
|
34
|
+
value,
|
|
35
|
+
depth,
|
|
36
|
+
collapsed,
|
|
37
|
+
}: {
|
|
38
|
+
label?: string
|
|
39
|
+
value: Json
|
|
40
|
+
depth: number
|
|
41
|
+
collapsed: boolean
|
|
42
|
+
}) {
|
|
43
|
+
const [open, setOpen] = useState(!collapsed)
|
|
44
|
+
const indent = { paddingLeft: `${depth * 12}px` }
|
|
45
|
+
|
|
46
|
+
if (!isContainer(value)) {
|
|
47
|
+
return (
|
|
48
|
+
<div style={indent}>
|
|
49
|
+
{label !== undefined && (
|
|
50
|
+
<span className="text-muted-foreground">{label}: </span>
|
|
51
|
+
)}
|
|
52
|
+
<Primitive value={value} />
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entries = Array.isArray(value)
|
|
58
|
+
? value.map((v, i) => [String(i), v] as const)
|
|
59
|
+
: Object.entries(value)
|
|
60
|
+
const [open_, close_] = Array.isArray(value) ? ["[", "]"] : ["{", "}"]
|
|
61
|
+
|
|
62
|
+
if (entries.length === 0) {
|
|
63
|
+
return (
|
|
64
|
+
<div style={indent}>
|
|
65
|
+
{label !== undefined && (
|
|
66
|
+
<span className="text-muted-foreground">{label}: </span>
|
|
67
|
+
)}
|
|
68
|
+
<span className="text-muted-foreground">
|
|
69
|
+
{open_}
|
|
70
|
+
{close_}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => setOpen((o) => !o)}
|
|
81
|
+
className="hover:bg-muted/60 inline-flex items-center gap-0.5 rounded"
|
|
82
|
+
style={indent}
|
|
83
|
+
>
|
|
84
|
+
{open ? (
|
|
85
|
+
<ChevronDown className="text-muted-foreground size-3" />
|
|
86
|
+
) : (
|
|
87
|
+
<ChevronRight className="text-muted-foreground size-3" />
|
|
88
|
+
)}
|
|
89
|
+
{label !== undefined && (
|
|
90
|
+
<span className="text-muted-foreground">{label}:</span>
|
|
91
|
+
)}
|
|
92
|
+
<span className="text-muted-foreground">
|
|
93
|
+
{open_}
|
|
94
|
+
{!open && `…${entries.length}…${close_}`}
|
|
95
|
+
</span>
|
|
96
|
+
</button>
|
|
97
|
+
{open && (
|
|
98
|
+
<div>
|
|
99
|
+
{entries.map(([k, v]) => (
|
|
100
|
+
<Node
|
|
101
|
+
key={k}
|
|
102
|
+
label={Array.isArray(value) ? undefined : k}
|
|
103
|
+
value={v}
|
|
104
|
+
depth={depth + 1}
|
|
105
|
+
collapsed={collapsed}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
<div className="text-muted-foreground" style={indent}>
|
|
109
|
+
{close_}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function JsonViewer({
|
|
118
|
+
data,
|
|
119
|
+
collapsed = false,
|
|
120
|
+
className,
|
|
121
|
+
}: {
|
|
122
|
+
data: unknown
|
|
123
|
+
collapsed?: boolean
|
|
124
|
+
className?: string
|
|
125
|
+
}) {
|
|
126
|
+
const [, copy] = useClipboard()
|
|
127
|
+
const [copied, setCopied] = useState(false)
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
"bg-muted/60 relative rounded p-2 pr-8 font-mono text-xs",
|
|
132
|
+
className,
|
|
133
|
+
)}
|
|
134
|
+
>
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="icon-sm"
|
|
139
|
+
className="absolute top-1 right-1"
|
|
140
|
+
onClick={() => {
|
|
141
|
+
void (async () => {
|
|
142
|
+
const ok = await copy(JSON.stringify(data, null, 2))
|
|
143
|
+
if (ok) {
|
|
144
|
+
setCopied(true)
|
|
145
|
+
setTimeout(() => setCopied(false), 1200)
|
|
146
|
+
}
|
|
147
|
+
})()
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{copied ? (
|
|
151
|
+
<Check className="size-3.5" />
|
|
152
|
+
) : (
|
|
153
|
+
<Copy className="size-3.5" />
|
|
154
|
+
)}
|
|
155
|
+
</Button>
|
|
156
|
+
<Node value={data as Json} depth={0} collapsed={collapsed} />
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useTicker } from "@/hooks/use-ticker"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
function isFresh(lastSeenAt: string | null, thresholdMs: number): boolean {
|
|
5
|
+
if (lastSeenAt === null) return false
|
|
6
|
+
return Date.now() - new Date(lastSeenAt).getTime() < thresholdMs
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const UNLIT = "bg-neutral-400/15"
|
|
10
|
+
|
|
11
|
+
// A three-aspect signal head standing in for the usual status dot, lamps
|
|
12
|
+
// in their real order: stop on top, caution in the middle, clear at the
|
|
13
|
+
// bottom. Fresh telemetry lights the bottom lamp green and pings it; stale
|
|
14
|
+
// lights the middle one amber; nothing yet leaves all three unlit. `erroring`
|
|
15
|
+
// lights the top lamp red instead, for an environment that is reporting
|
|
16
|
+
// but reporting failures. Same colour vocabulary as the empty-state signal
|
|
17
|
+
// and the status badges.
|
|
18
|
+
export function LiveDot({
|
|
19
|
+
lastSeenAt,
|
|
20
|
+
thresholdMs = 60_000,
|
|
21
|
+
erroring = false,
|
|
22
|
+
className,
|
|
23
|
+
}: {
|
|
24
|
+
lastSeenAt: string | null
|
|
25
|
+
thresholdMs?: number
|
|
26
|
+
erroring?: boolean
|
|
27
|
+
className?: string
|
|
28
|
+
}) {
|
|
29
|
+
useTicker()
|
|
30
|
+
const fresh = isFresh(lastSeenAt, thresholdMs)
|
|
31
|
+
const state =
|
|
32
|
+
lastSeenAt === null
|
|
33
|
+
? "none"
|
|
34
|
+
: erroring
|
|
35
|
+
? "stop"
|
|
36
|
+
: fresh
|
|
37
|
+
? "clear"
|
|
38
|
+
: "caution"
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
className={cn(
|
|
43
|
+
"border-foreground/30 bg-background/60 inline-flex w-2.5 shrink-0 flex-col items-center justify-between rounded-full border px-px py-[3px]",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
style={{ height: 24 }}
|
|
47
|
+
>
|
|
48
|
+
<span className="relative inline-flex size-1.5">
|
|
49
|
+
{state === "stop" && (
|
|
50
|
+
<span className="bg-danger absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
|
|
51
|
+
)}
|
|
52
|
+
<span
|
|
53
|
+
className={cn(
|
|
54
|
+
"relative inline-flex size-1.5 rounded-full transition-colors duration-300",
|
|
55
|
+
state === "stop" ? "bg-danger" : UNLIT,
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
</span>
|
|
59
|
+
<span
|
|
60
|
+
className={cn(
|
|
61
|
+
"inline-flex size-1.5 rounded-full transition-colors duration-300",
|
|
62
|
+
state === "caution" ? "bg-warning" : UNLIT,
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
<span className="relative inline-flex size-1.5">
|
|
66
|
+
{state === "clear" && (
|
|
67
|
+
<span className="bg-live absolute inline-flex h-full w-full animate-ping rounded-full opacity-75" />
|
|
68
|
+
)}
|
|
69
|
+
<span
|
|
70
|
+
className={cn(
|
|
71
|
+
"relative inline-flex size-1.5 rounded-full transition-colors duration-300",
|
|
72
|
+
state === "clear" ? "bg-live" : UNLIT,
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
</span>
|
|
76
|
+
</span>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { usePage } from "@inertiajs/react"
|
|
2
|
+
|
|
3
|
+
import { badgeVariants } from "@/components/ui/badge"
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from "@/components/ui/tooltip"
|
|
9
|
+
import { useLive } from "@/hooks/use-live"
|
|
10
|
+
import { useTicker } from "@/hooks/use-ticker"
|
|
11
|
+
import { ago } from "@/lib/format"
|
|
12
|
+
import { cn } from "@/lib/utils"
|
|
13
|
+
import type { SharedProps } from "@/types"
|
|
14
|
+
|
|
15
|
+
import { LiveDot } from "./live-dot"
|
|
16
|
+
|
|
17
|
+
// Small pill embedded in every environment page's PageHeader: pulses while
|
|
18
|
+
// live ingest events are flowing, click to pause/resume the automatic
|
|
19
|
+
// background reloads that keep the page's data fresh.
|
|
20
|
+
export function LiveToggle({ environmentId }: { environmentId: number }) {
|
|
21
|
+
useTicker()
|
|
22
|
+
const { environment, telemetry_freshness: freshness } =
|
|
23
|
+
usePage<SharedProps>().props
|
|
24
|
+
const { connected, lastEventAt, lastRefreshedAt, paused, setPaused } =
|
|
25
|
+
useLive(environmentId)
|
|
26
|
+
const lastEventIso =
|
|
27
|
+
lastEventAt != null
|
|
28
|
+
? new Date(lastEventAt).toISOString()
|
|
29
|
+
: (freshness?.received_at ?? environment?.last_seen_at ?? null)
|
|
30
|
+
const refreshedIso =
|
|
31
|
+
lastRefreshedAt != null ? new Date(lastRefreshedAt).toISOString() : null
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Tooltip>
|
|
35
|
+
<TooltipTrigger asChild>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-pressed={paused}
|
|
39
|
+
aria-label={paused ? "Resume live updates" : "Pause live updates"}
|
|
40
|
+
onClick={() => setPaused(!paused)}
|
|
41
|
+
className={cn(
|
|
42
|
+
badgeVariants({ variant: "outline" }),
|
|
43
|
+
"hover:bg-accent h-7 cursor-pointer gap-1.5 rounded-md font-mono text-[11px] tracking-wide uppercase",
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
<LiveDot
|
|
47
|
+
lastSeenAt={connected ? lastEventIso : null}
|
|
48
|
+
thresholdMs={10_000}
|
|
49
|
+
/>
|
|
50
|
+
{paused ? "Paused" : connected ? "Live" : "Disconnected"}
|
|
51
|
+
</button>
|
|
52
|
+
</TooltipTrigger>
|
|
53
|
+
<TooltipContent>
|
|
54
|
+
<p>
|
|
55
|
+
{lastEventIso
|
|
56
|
+
? `Telemetry received ${ago(lastEventIso)}`
|
|
57
|
+
: "No telemetry received yet"}
|
|
58
|
+
</p>
|
|
59
|
+
<p>
|
|
60
|
+
{refreshedIso
|
|
61
|
+
? `Page refreshed ${ago(refreshedIso)}`
|
|
62
|
+
: "No live refresh yet"}
|
|
63
|
+
</p>
|
|
64
|
+
{freshness?.processing_lag_seconds != null && (
|
|
65
|
+
<p>
|
|
66
|
+
Ingest processing pending:{" "}
|
|
67
|
+
{Math.round(freshness.processing_lag_seconds)}s
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
{freshness?.aggregated_at && (
|
|
71
|
+
<p>Charts updated {ago(freshness.aggregated_at)}</p>
|
|
72
|
+
)}
|
|
73
|
+
</TooltipContent>
|
|
74
|
+
</Tooltip>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ChartConfig,
|
|
5
|
+
ChartContainer,
|
|
6
|
+
ChartTooltip,
|
|
7
|
+
ChartTooltipContent,
|
|
8
|
+
} from "@/components/ui/chart"
|
|
9
|
+
|
|
10
|
+
export interface MetricPoint {
|
|
11
|
+
t: string
|
|
12
|
+
value: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const axisTick = { fontSize: 10, fontFamily: "var(--font-mono)" }
|
|
16
|
+
|
|
17
|
+
// Time-of-day ticks up to two days, dates beyond, like series-chart.tsx.
|
|
18
|
+
const DAY = 24 * 60 * 60 * 1000
|
|
19
|
+
|
|
20
|
+
function tickFormatter(data: { t: string }[]) {
|
|
21
|
+
const span =
|
|
22
|
+
data.length > 1
|
|
23
|
+
? new Date(data[data.length - 1].t).getTime() -
|
|
24
|
+
new Date(data[0].t).getTime()
|
|
25
|
+
: 0
|
|
26
|
+
if (span > 2 * DAY)
|
|
27
|
+
return (t: string) =>
|
|
28
|
+
new Date(t).toLocaleDateString(undefined, {
|
|
29
|
+
month: "short",
|
|
30
|
+
day: "numeric",
|
|
31
|
+
})
|
|
32
|
+
return (t: string) =>
|
|
33
|
+
new Date(t).toLocaleTimeString(undefined, {
|
|
34
|
+
hour: "2-digit",
|
|
35
|
+
minute: "2-digit",
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stamp(t: string) {
|
|
40
|
+
return new Date(t).toLocaleString(undefined, {
|
|
41
|
+
day: "2-digit",
|
|
42
|
+
month: "short",
|
|
43
|
+
hour: "2-digit",
|
|
44
|
+
minute: "2-digit",
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// One plain {t, value} metric over time — health samples rather than
|
|
49
|
+
// rollups, so there is no ok/4xx/5xx vocabulary to stack. Same axes,
|
|
50
|
+
// gridlines and bucket-range caption as series-chart.tsx.
|
|
51
|
+
export function MetricChart({
|
|
52
|
+
label,
|
|
53
|
+
data,
|
|
54
|
+
format,
|
|
55
|
+
color = "var(--primary)",
|
|
56
|
+
className = "h-32",
|
|
57
|
+
}: {
|
|
58
|
+
label: string
|
|
59
|
+
data: MetricPoint[]
|
|
60
|
+
format: (value: number) => string
|
|
61
|
+
color?: string
|
|
62
|
+
className?: string
|
|
63
|
+
}) {
|
|
64
|
+
const config = { value: { label, color } } satisfies ChartConfig
|
|
65
|
+
const tick = tickFormatter(data)
|
|
66
|
+
return (
|
|
67
|
+
<div>
|
|
68
|
+
<ChartContainer config={config} className={`${className} w-full`}>
|
|
69
|
+
<AreaChart data={data}>
|
|
70
|
+
<CartesianGrid vertical={false} strokeDasharray="2 4" />
|
|
71
|
+
<XAxis
|
|
72
|
+
dataKey="t"
|
|
73
|
+
tickFormatter={tick}
|
|
74
|
+
tickLine={false}
|
|
75
|
+
axisLine={false}
|
|
76
|
+
minTickGap={40}
|
|
77
|
+
tick={axisTick}
|
|
78
|
+
/>
|
|
79
|
+
<YAxis
|
|
80
|
+
width={44}
|
|
81
|
+
tickLine={false}
|
|
82
|
+
axisLine={false}
|
|
83
|
+
tick={axisTick}
|
|
84
|
+
tickFormatter={(v) => format(Number(v))}
|
|
85
|
+
/>
|
|
86
|
+
<ChartTooltip
|
|
87
|
+
content={
|
|
88
|
+
<ChartTooltipContent
|
|
89
|
+
labelFormatter={(v) => stamp(String(v))}
|
|
90
|
+
formatter={(value) => [format(Number(value)), label]}
|
|
91
|
+
/>
|
|
92
|
+
}
|
|
93
|
+
/>
|
|
94
|
+
<Area
|
|
95
|
+
dataKey="value"
|
|
96
|
+
stroke="var(--color-value)"
|
|
97
|
+
fill="var(--color-value)"
|
|
98
|
+
fillOpacity={0.15}
|
|
99
|
+
strokeWidth={1.75}
|
|
100
|
+
isAnimationActive={false}
|
|
101
|
+
/>
|
|
102
|
+
</AreaChart>
|
|
103
|
+
</ChartContainer>
|
|
104
|
+
{data.length > 0 && (
|
|
105
|
+
<div className="text-muted-foreground mt-1 flex justify-between font-mono text-[10px]">
|
|
106
|
+
<span>{stamp(data[0].t)}</span>
|
|
107
|
+
<span>{stamp(data[data.length - 1].t)}</span>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { router, usePage } from "@inertiajs/react"
|
|
2
|
+
import { Bookmark, ChevronDown } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { isViewActive, samePage } from "@/components/railwatch/saved-views"
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "@/components/ui/dropdown-menu"
|
|
13
|
+
import { ENV_PAGE_GROUPS, envPages } from "@/lib/env-pages"
|
|
14
|
+
import { cn } from "@/lib/utils"
|
|
15
|
+
import type { SharedProps } from "@/types"
|
|
16
|
+
|
|
17
|
+
// Phone-only replacement for the sidebar's environment navigation: the
|
|
18
|
+
// current page's name in the header opens a menu of every environment page.
|
|
19
|
+
export function MobilePageSwitcher() {
|
|
20
|
+
const page = usePage<SharedProps>()
|
|
21
|
+
const { environment } = page.props
|
|
22
|
+
if (!environment) return null
|
|
23
|
+
const items = envPages(
|
|
24
|
+
environment.application_id,
|
|
25
|
+
environment.id,
|
|
26
|
+
page.props.window as string,
|
|
27
|
+
)
|
|
28
|
+
const current = page.url.split("?")[0]
|
|
29
|
+
const pinnedViews = (page.props.saved_views ?? []).filter((v) => v.pinned)
|
|
30
|
+
// Overview's path is a prefix of every other page's, so prefer the
|
|
31
|
+
// longest matching path rather than the first.
|
|
32
|
+
const active =
|
|
33
|
+
items
|
|
34
|
+
.filter((i) => {
|
|
35
|
+
const path = i.href.split("?")[0]
|
|
36
|
+
return current === path || current.startsWith(path + "/")
|
|
37
|
+
})
|
|
38
|
+
.sort((a, b) => b.href.length - a.href.length)[0] ?? items[0]
|
|
39
|
+
return (
|
|
40
|
+
<DropdownMenu>
|
|
41
|
+
<DropdownMenuTrigger className="flex min-w-0 items-center gap-1 text-sm font-medium">
|
|
42
|
+
{active.icon && <active.icon className="size-4 shrink-0 opacity-70" />}
|
|
43
|
+
<span className="truncate">{active.title}</span>
|
|
44
|
+
<ChevronDown className="size-3.5 shrink-0 opacity-60" />
|
|
45
|
+
</DropdownMenuTrigger>
|
|
46
|
+
<DropdownMenuContent
|
|
47
|
+
align="start"
|
|
48
|
+
className="max-h-[70vh] w-60 overflow-y-auto"
|
|
49
|
+
>
|
|
50
|
+
{ENV_PAGE_GROUPS.map((group, gi) => (
|
|
51
|
+
<div key={group.key}>
|
|
52
|
+
{gi > 0 && <DropdownMenuSeparator />}
|
|
53
|
+
<DropdownMenuLabel className="label-caps">
|
|
54
|
+
{group.title}
|
|
55
|
+
</DropdownMenuLabel>
|
|
56
|
+
{items
|
|
57
|
+
.filter((i) => i.group === group.key)
|
|
58
|
+
.map((item) => (
|
|
59
|
+
<div key={item.title}>
|
|
60
|
+
<DropdownMenuItem
|
|
61
|
+
onSelect={() => router.visit(item.href)}
|
|
62
|
+
className={item === active ? "bg-accent" : undefined}
|
|
63
|
+
>
|
|
64
|
+
{item.icon && <item.icon className="size-4" />}
|
|
65
|
+
{item.title}
|
|
66
|
+
</DropdownMenuItem>
|
|
67
|
+
{pinnedViews
|
|
68
|
+
.filter((view) => samePage(view.url, item.href))
|
|
69
|
+
.map((view) => (
|
|
70
|
+
<DropdownMenuItem
|
|
71
|
+
key={view.id}
|
|
72
|
+
onSelect={() => router.visit(view.url)}
|
|
73
|
+
className={cn(
|
|
74
|
+
"pl-8 text-xs",
|
|
75
|
+
isViewActive(page.url, view.url) && "bg-accent",
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
<Bookmark className="size-3.5 opacity-60" />
|
|
79
|
+
{view.name}
|
|
80
|
+
</DropdownMenuItem>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</DropdownMenuContent>
|
|
87
|
+
</DropdownMenu>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Link, router } from "@inertiajs/react"
|
|
2
|
+
import { RefreshCwIcon } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
import { CopyBlock } from "@/components/railwatch/copy-block"
|
|
5
|
+
import { LiveDot } from "@/components/railwatch/live-dot"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
8
|
+
import { settingsProfilePath } from "@/routes"
|
|
9
|
+
|
|
10
|
+
// Shown on Overview and the Application show page while an environment has
|
|
11
|
+
// no events yet (environment.last_seen_at is null): a step list to wire up
|
|
12
|
+
// the gem plus a "waiting for first event..." status that live-checks via
|
|
13
|
+
// router.reload (no full navigation needed once events start arriving).
|
|
14
|
+
export function Onboarding({
|
|
15
|
+
tokenPrefix,
|
|
16
|
+
newToken,
|
|
17
|
+
className,
|
|
18
|
+
}: {
|
|
19
|
+
tokenPrefix: string
|
|
20
|
+
newToken?: string | null
|
|
21
|
+
className?: string
|
|
22
|
+
}) {
|
|
23
|
+
const token = newToken ?? `${tokenPrefix}…`
|
|
24
|
+
const checkNow = () => router.reload()
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Card className={className}>
|
|
28
|
+
<CardHeader>
|
|
29
|
+
<CardTitle>Set up this environment</CardTitle>
|
|
30
|
+
</CardHeader>
|
|
31
|
+
<CardContent className="space-y-4">
|
|
32
|
+
<ol className="list-inside list-decimal space-y-3 text-sm">
|
|
33
|
+
<li>
|
|
34
|
+
Add the gem
|
|
35
|
+
<CopyBlock code={'gem "railwatch"'} className="mt-1.5" />
|
|
36
|
+
</li>
|
|
37
|
+
<li>
|
|
38
|
+
Run the generator
|
|
39
|
+
<CopyBlock
|
|
40
|
+
code="bin/rails generate railwatch:install --cloud"
|
|
41
|
+
className="mt-1.5"
|
|
42
|
+
/>
|
|
43
|
+
</li>
|
|
44
|
+
<li>
|
|
45
|
+
Set the token in the app's environment
|
|
46
|
+
<CopyBlock code={`RAILWATCH_TOKEN=${token}`} className="mt-1.5" />
|
|
47
|
+
{!newToken && (
|
|
48
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
49
|
+
Showing the token prefix only. The full token is shown once,
|
|
50
|
+
right after an environment is created.
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
53
|
+
</li>
|
|
54
|
+
<li>
|
|
55
|
+
Check every piece is wired up
|
|
56
|
+
<CopyBlock code="bin/rails railwatch:doctor" className="mt-1.5" />
|
|
57
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
58
|
+
Prints a ✓/✗ line per check — token, ingest, middleware, routes,
|
|
59
|
+
deploy marker, browser client, test matchers — and exits non-zero
|
|
60
|
+
if anything required is missing.
|
|
61
|
+
</p>
|
|
62
|
+
</li>
|
|
63
|
+
<li>
|
|
64
|
+
Connect your AI assistant
|
|
65
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
66
|
+
Generate a personal token on your{" "}
|
|
67
|
+
<Link
|
|
68
|
+
href={settingsProfilePath()}
|
|
69
|
+
className="underline underline-offset-4"
|
|
70
|
+
>
|
|
71
|
+
profile page
|
|
72
|
+
</Link>{" "}
|
|
73
|
+
for paste-ready Claude Code, Cursor, VS Code, and Zed config.
|
|
74
|
+
</p>
|
|
75
|
+
</li>
|
|
76
|
+
<li>Deploy</li>
|
|
77
|
+
</ol>
|
|
78
|
+
<div className="flex items-center justify-between border-t pt-3">
|
|
79
|
+
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
|
80
|
+
<LiveDot lastSeenAt={null} />
|
|
81
|
+
Waiting for first event…
|
|
82
|
+
</div>
|
|
83
|
+
<Button type="button" variant="outline" size="sm" onClick={checkNow}>
|
|
84
|
+
<RefreshCwIcon className="size-3.5" />
|
|
85
|
+
Check now
|
|
86
|
+
</Button>
|
|
87
|
+
</div>
|
|
88
|
+
</CardContent>
|
|
89
|
+
</Card>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Link } from "@inertiajs/react"
|
|
2
|
+
|
|
3
|
+
import { tenantPath } from "@/pages/tenants/tenant-path"
|
|
4
|
+
import * as R from "@/routes"
|
|
5
|
+
import type { TelemetryTimeParams, Window, WindowRange } from "@/types"
|
|
6
|
+
|
|
7
|
+
export interface OriginIdentityData {
|
|
8
|
+
user_ref: string | null
|
|
9
|
+
tenant: string | null
|
|
10
|
+
person: { ref: string; name: string } | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function personPath(
|
|
14
|
+
applicationId: number,
|
|
15
|
+
environmentId: number,
|
|
16
|
+
ref: string,
|
|
17
|
+
timeParams: TelemetryTimeParams,
|
|
18
|
+
) {
|
|
19
|
+
const url = String(
|
|
20
|
+
R.applicationEnvironmentPersonPath(applicationId, environmentId, ref, {
|
|
21
|
+
...timeParams,
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
const [path, query] = url.split("?")
|
|
25
|
+
return `${path.replace(/\./g, "%2E")}${query ? `?${query}` : ""}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Props extends OriginIdentityData {
|
|
29
|
+
applicationId: number
|
|
30
|
+
environmentId: number
|
|
31
|
+
kind: "user" | "tenant"
|
|
32
|
+
window?: Window
|
|
33
|
+
range?: WindowRange
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function identityTimeParams(
|
|
37
|
+
window: Window | undefined,
|
|
38
|
+
range: WindowRange | undefined,
|
|
39
|
+
): TelemetryTimeParams {
|
|
40
|
+
if (window === "custom") {
|
|
41
|
+
if (!range) throw new Error("Custom telemetry window requires a range")
|
|
42
|
+
return { from: range.from, to: range.to }
|
|
43
|
+
}
|
|
44
|
+
return { window: window ?? "24h" }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A propagated job identity describes the enqueue origin. A Person link is
|
|
48
|
+
// only offered when this environment has a matching user record; the raw ref
|
|
49
|
+
// remains useful for older payloads that never emitted one.
|
|
50
|
+
export function OriginIdentity({
|
|
51
|
+
applicationId,
|
|
52
|
+
environmentId,
|
|
53
|
+
kind,
|
|
54
|
+
person,
|
|
55
|
+
range,
|
|
56
|
+
tenant,
|
|
57
|
+
user_ref,
|
|
58
|
+
window,
|
|
59
|
+
}: Props) {
|
|
60
|
+
if (kind === "user") {
|
|
61
|
+
if (!user_ref) return <span className="text-muted-foreground">–</span>
|
|
62
|
+
if (!person) return <span className="font-mono text-xs">{user_ref}</span>
|
|
63
|
+
const timeParams = identityTimeParams(window, range)
|
|
64
|
+
return (
|
|
65
|
+
<Link
|
|
66
|
+
className="font-mono text-xs hover:underline"
|
|
67
|
+
href={personPath(applicationId, environmentId, person.ref, timeParams)}
|
|
68
|
+
>
|
|
69
|
+
{person.name}
|
|
70
|
+
</Link>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!tenant) return <span className="text-muted-foreground">–</span>
|
|
75
|
+
const timeParams = identityTimeParams(window, range)
|
|
76
|
+
return (
|
|
77
|
+
<Link
|
|
78
|
+
className="font-mono text-xs hover:underline"
|
|
79
|
+
href={tenantPath(applicationId, environmentId, tenant, timeParams)}
|
|
80
|
+
>
|
|
81
|
+
{tenant}
|
|
82
|
+
</Link>
|
|
83
|
+
)
|
|
84
|
+
}
|