@hot-updater/console 1.0.0-rc.4 → 1.0.0-rc.41

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 (330) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-DRzeMGRM.js +1 -0
  3. package/.output/public/assets/EventDetails-SB5ygJzS.js +1 -0
  4. package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
  5. package/.output/public/assets/InsightsInfo-btYppp0B.js +1 -0
  6. package/.output/public/assets/InsightsPageHeader-Ba8psD8t.js +1 -0
  7. package/.output/public/assets/NotFoundPage-CyRG2eca.js +1 -0
  8. package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
  9. package/.output/public/assets/api-DUgI6yOI.js +1 -0
  10. package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
  11. package/.output/public/assets/api-keys-CmkNKOhS.js +1 -0
  12. package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
  13. package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
  14. package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
  15. package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
  16. package/.output/public/assets/console-features-api-BMsI3Bj8.js +1 -0
  17. package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
  18. package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
  19. package/.output/public/assets/empty-CuT2UhFp.js +1 -0
  20. package/.output/public/assets/field-BBDpjIqu.js +1 -0
  21. package/.output/public/assets/index-BEfQ2_7f.js +11 -0
  22. package/.output/public/assets/input-group-ja_30qaF.js +1 -0
  23. package/.output/public/assets/insights-B0ib9qOE.js +2 -0
  24. package/.output/public/assets/insights-BIMJx-0p.js +1 -0
  25. package/.output/public/assets/insights-CndqcWEj.js +42 -0
  26. package/.output/public/assets/insights-api-B6Ewc-OG.js +1 -0
  27. package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
  28. package/.output/public/assets/insights_.distribution-B26PMy4k.js +2 -0
  29. package/.output/public/assets/insights_.distribution-BBDtqqYS.js +1 -0
  30. package/.output/public/assets/insights_.distribution-DhHmYS8k.js +1 -0
  31. package/.output/public/assets/installations-C3cMuwRx.js +1 -0
  32. package/.output/public/assets/installations-C3yrjwDh.js +2 -0
  33. package/.output/public/assets/installations-D3edlN75.js +1 -0
  34. package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
  35. package/.output/public/assets/list-CgrYiadM.js +1 -0
  36. package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
  37. package/.output/public/assets/remote-config-0i-gDkeU.js +1 -0
  38. package/.output/public/assets/remote-config-An1ROIOQ.js +6 -0
  39. package/.output/public/assets/remote-config-CoeWgA9i.js +1 -0
  40. package/.output/public/assets/routes-DNoaJIQV.js +2 -0
  41. package/.output/public/assets/routes-cWBDtghZ.js +1 -0
  42. package/.output/public/assets/search-DaTDPW25.js +1 -0
  43. package/.output/public/assets/select-CZMQKT7F.js +1 -0
  44. package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
  45. package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
  46. package/.output/public/assets/styles-D8mxMvCB.css +2 -0
  47. package/.output/public/assets/switch-CU4-fupc.js +1 -0
  48. package/.output/public/assets/table-Gm4qRWwE.js +1 -0
  49. package/.output/public/assets/tabs-R1I8CRjR.js +1 -0
  50. package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
  51. package/.output/public/assets/useCompositeListItem-BaPeC9ZO.js +1 -0
  52. package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
  53. package/.output/server/__23tanstack-start-server-fn-resolver-DTAqlLRd.mjs +178 -0
  54. package/.output/server/_libs/@base-ui/react+[...].mjs +6450 -2741
  55. package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
  56. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
  57. package/.output/server/_libs/lucide-react.mjs +235 -31
  58. package/.output/server/_libs/next-themes.mjs +1 -1
  59. package/.output/server/_libs/react-redux+[...].mjs +1 -1
  60. package/.output/server/_libs/recharts+[...].mjs +483 -901
  61. package/.output/server/_libs/sonner.mjs +1 -1
  62. package/.output/server/_libs/tanstack__history.mjs +68 -41
  63. package/.output/server/_libs/tanstack__query-core.mjs +2010 -309
  64. package/.output/server/_libs/tanstack__react-query.mjs +448 -21
  65. package/.output/server/_libs/verkit.mjs +102 -1
  66. package/.output/server/_ssr/BundleIdDisplay-BNoVW3Dv.mjs +13 -0
  67. package/.output/server/_ssr/ConsoleFeatureUnavailable-BPFa-cmJ.mjs +58 -0
  68. package/.output/server/_ssr/EventDetails-9CeshfdR.mjs +344 -0
  69. package/.output/server/_ssr/HashValueDisplay-DXZlkcps.mjs +55 -0
  70. package/.output/server/_ssr/InsightsInfo-DLNdJQ0m.mjs +75 -0
  71. package/.output/server/_ssr/InsightsPageHeader-DZGVDgvv.mjs +145 -0
  72. package/.output/server/_ssr/NotFoundPage-mYHHg-2s.mjs +37 -0
  73. package/.output/server/_ssr/{dialog-DUaIgpgd.mjs → alert-dialog-Chkim7vZ.mjs} +6 -83
  74. package/.output/server/_ssr/api-C-Il5-sa.mjs +203 -0
  75. package/.output/server/_ssr/api-keys-01FGqj4d.mjs +5 -0
  76. package/.output/server/_ssr/{api-keys-DJml_XoK.mjs → api-keys-B_f9h7ln.mjs} +48 -10
  77. package/.output/server/_ssr/api-keys-rpc-Cf4YCyFc.mjs +52 -0
  78. package/.output/server/_ssr/api-rpc-DRqwxxT_.mjs +296 -0
  79. package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
  80. package/.output/server/_ssr/{auth-rpc-D_hv52-J.mjs → auth-rpc-D8nFpU9C.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-COZoXu4i.mjs +5425 -0
  82. package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
  83. package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
  84. package/.output/server/_ssr/config.server-Dm2l7w0O.mjs +36 -0
  85. package/.output/server/_ssr/console-access--o9FXikU.mjs +17 -0
  86. package/.output/server/_ssr/console-features-BftSkf3f.mjs +60 -0
  87. package/.output/server/_ssr/console-features-api-CYclUQ4h.mjs +43 -0
  88. package/.output/server/_ssr/console-features-rpc-1WaAkPM_.mjs +19 -0
  89. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
  90. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
  91. package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
  92. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
  93. package/.output/server/_ssr/databaseRows-BUuNU-qt.mjs +74 -0
  94. package/.output/server/_ssr/dialog-mfSANqjC.mjs +84 -0
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-wmnTk7w6.mjs} +140 -6
  96. package/.output/server/_ssr/{downloadBundle-B3ert-pY.mjs → downloadBundle-DViuIa9U.mjs} +10 -10
  97. package/.output/server/_ssr/empty-BPayFad3.mjs +60 -0
  98. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
  99. package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
  100. package/.output/server/_ssr/field-DjZ5t7rH.mjs +189 -0
  101. package/.output/server/_ssr/getBundleChildren-CmjDg6Re.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-Bb8ueylQ.mjs → input-group-Dx-MEkNM.mjs} +3 -52
  103. package/.output/server/_ssr/insights-BopxyJjk.mjs +14 -0
  104. package/.output/server/_ssr/insights-Bvm03d61.mjs +5 -0
  105. package/.output/server/_ssr/insights-CrLAHSDy.mjs +2315 -0
  106. package/.output/server/_ssr/insights-api-Bg8ZV_h_.mjs +122 -0
  107. package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
  108. package/.output/server/_ssr/insights-failures-rpc-tyy5m9Cc.mjs +36 -0
  109. package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
  110. package/.output/server/_ssr/{insights-recovery-DSFM-YTL.mjs → insights-recovery-BWQhiZaF.mjs} +9 -2
  111. package/.output/server/_ssr/insights-recovery-rpc-CPJOiJAV.mjs +32 -0
  112. package/.output/server/_ssr/{insights-rpc-DrIQCjpD.mjs → insights-rpc-DoC3hla9.mjs} +22 -14
  113. package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-CFuTYp3T.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-O2ruP4iE.mjs +41 -0
  116. package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-B3BVPD00.mjs +16 -0
  118. package/.output/server/_ssr/insights_.distribution-BMoisxRG.mjs +347 -0
  119. package/.output/server/_ssr/insights_.distribution-BiCcFyrG.mjs +7 -0
  120. package/.output/server/_ssr/{installations-YumqpNpa.mjs → installations-Cx3jIrwl.mjs} +280 -364
  121. package/.output/server/_ssr/installations-DTGPaKXx.mjs +69 -0
  122. package/.output/server/_ssr/installations-osiAuNKr.mjs +5 -0
  123. package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
  124. package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
  125. package/.output/server/_ssr/release-adoption-rpc-BVySDR0L.mjs +41 -0
  126. package/.output/server/_ssr/releaseAdoption-2PJPA09N.mjs +114 -0
  127. package/.output/server/_ssr/releaseManagement-BTCWEY8-.mjs +11 -0
  128. package/.output/server/_ssr/remote-config-CUoAxUMy.mjs +430 -0
  129. package/.output/server/_ssr/remote-config-Cc5P4J-r.mjs +17 -0
  130. package/.output/server/_ssr/remote-config-CjdxcZ9B.mjs +5 -0
  131. package/.output/server/_ssr/remote-config-D2GmbGwI.mjs +2594 -0
  132. package/.output/server/_ssr/remote-config-rpc-kjoZ9PRS.mjs +115 -0
  133. package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
  134. package/.output/server/_ssr/{router-iP95Vqi2.mjs → router-DwGVewL-.mjs} +141 -139
  135. package/.output/server/_ssr/{routes-ivsV2TDu.mjs → routes-C4yxv3JU.mjs} +192 -685
  136. package/.output/server/_ssr/routes-D3_aegLY.mjs +409 -0
  137. package/.output/server/_ssr/runtime.server-BoWf4QhU.mjs +1716 -0
  138. package/.output/server/_ssr/schema-DrOtkRVD.mjs +361 -0
  139. package/.output/server/_ssr/select-DkzM2miy.mjs +107 -0
  140. package/.output/server/_ssr/server-BW0gzB0p.mjs +8325 -0
  141. package/.output/server/_ssr/{createSsrRpc-NHF2PxuK.mjs → sidebar-DPcOijvL.mjs} +107 -47
  142. package/.output/server/_ssr/ssr.mjs +1 -1
  143. package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
  144. package/.output/server/_ssr/switch-BqJ2mAwC.mjs +17 -0
  145. package/.output/server/_ssr/{table-D1mL8fJu.mjs → table-R7RY92q9.mjs} +2 -2
  146. package/.output/server/_ssr/tabs-trDcbRkT.mjs +45 -0
  147. package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
  148. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-ctA_XVZA.mjs} +2 -2
  149. package/.output/server/_tanstack-start-manifest_v-BwInaUTw.mjs +139 -0
  150. package/.output/server/index.mjs +315 -154
  151. package/LICENSE +13 -14
  152. package/README.md +82 -72
  153. package/lib/index.d.mts +23 -11
  154. package/lib/vite-internal.mjs +13 -3
  155. package/lib/vite.d.mts +2 -0
  156. package/package.json +14 -14
  157. package/src/components/AppSidebar.tsx +100 -71
  158. package/src/components/ConsoleAccessPage.tsx +5 -1
  159. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  160. package/src/components/NotFoundPage.tsx +7 -7
  161. package/src/components/features/FeatureSlots.tsx +103 -0
  162. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  163. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  164. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  165. package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
  166. package/src/components/features/insights/AppUsage.tsx +366 -0
  167. package/src/components/features/insights/BuiltinBundle.tsx +36 -0
  168. package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
  169. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  170. package/src/components/features/insights/EventDetails.tsx +400 -39
  171. package/src/components/features/insights/EventHistoryCard.tsx +112 -26
  172. package/src/components/features/insights/EventHistoryList.tsx +2 -2
  173. package/src/components/features/insights/FailureReports.tsx +407 -0
  174. package/src/components/features/insights/InsightsControls.tsx +265 -88
  175. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  176. package/src/components/features/insights/InsightsInfo.tsx +48 -0
  177. package/src/components/features/insights/InsightsOverview.tsx +604 -355
  178. package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
  179. package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
  180. package/src/components/features/insights/InstallationHistoryCard.tsx +66 -11
  181. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  182. package/src/components/features/insights/ReleaseActivity.tsx +308 -0
  183. package/src/components/features/insights/ReportingDevicesSummary.tsx +48 -0
  184. package/src/components/features/insights/RollbackButton.tsx +118 -0
  185. package/src/components/features/insights/UpdateFailures.tsx +382 -0
  186. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  187. package/src/components/features/releases/ReleaseEditorSheet.tsx +26 -12
  188. package/src/components/features/remote-config/ConditionDialog.tsx +581 -0
  189. package/src/components/features/remote-config/ConditionsCard.tsx +244 -0
  190. package/src/components/features/remote-config/ParameterDialog.tsx +407 -0
  191. package/src/components/features/remote-config/ParametersCard.tsx +244 -0
  192. package/src/components/features/remote-config/PreviewCard.tsx +284 -0
  193. package/src/components/features/remote-config/PublishDialog.tsx +319 -0
  194. package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
  195. package/src/components/features/remote-config/RuleSummary.tsx +21 -0
  196. package/src/components/features/remote-config/ValueInput.tsx +148 -0
  197. package/src/components/features/remote-config/VersionsCard.tsx +361 -0
  198. package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
  199. package/src/components/features/remote-config/useDateTimeText.ts +16 -0
  200. package/src/components/ui/card.tsx +1 -1
  201. package/src/components/ui/scroll-area.tsx +52 -0
  202. package/src/components/ui/sidebar.tsx +22 -29
  203. package/src/components/ui/tabs.tsx +81 -0
  204. package/src/index.ts +26 -19
  205. package/src/lib/api-keys-api.ts +0 -28
  206. package/src/lib/api-keys-rpc.ts +27 -38
  207. package/src/lib/api-rpc.ts +189 -266
  208. package/src/lib/api.ts +19 -102
  209. package/src/lib/bundle-activity.ts +24 -4
  210. package/src/lib/console-access.ts +20 -0
  211. package/src/lib/console-features-api.ts +68 -0
  212. package/src/lib/console-features-rpc.ts +13 -0
  213. package/src/lib/console-features.ts +97 -0
  214. package/src/lib/insights-api.ts +56 -2
  215. package/src/lib/insights-errors.ts +64 -0
  216. package/src/lib/insights-failures-rpc.ts +36 -0
  217. package/src/lib/insights-failures.ts +130 -0
  218. package/src/lib/insights-recovery-rpc.ts +13 -17
  219. package/src/lib/insights-recovery.ts +10 -27
  220. package/src/lib/insights-retention.ts +21 -0
  221. package/src/lib/insights-rpc.ts +30 -12
  222. package/src/lib/insights-search.ts +93 -0
  223. package/src/lib/insights-usage-rpc.ts +29 -0
  224. package/src/lib/insights-usage.ts +76 -0
  225. package/src/lib/insights-view.ts +34 -5
  226. package/src/lib/release-adoption-api.ts +214 -0
  227. package/src/lib/release-adoption-rpc.ts +49 -0
  228. package/src/lib/release-adoption.ts +164 -0
  229. package/src/lib/remote-config-api.ts +119 -0
  230. package/src/lib/remote-config-draft.ts +652 -0
  231. package/src/lib/remote-config-rpc.ts +196 -0
  232. package/src/lib/scroll-restoration.ts +21 -0
  233. package/src/lib/server/adminInsights.ts +124 -0
  234. package/src/lib/server/auth.server.ts +13 -5
  235. package/src/lib/server/bundleActivity.ts +27 -28
  236. package/src/lib/server/config.server.ts +21 -74
  237. package/src/lib/server/console-feature-apis.ts +70 -0
  238. package/src/lib/server/console-runtime.server.ts +82 -38
  239. package/src/lib/server/downloadBundle.ts +16 -15
  240. package/src/lib/server/failureReports.ts +49 -0
  241. package/src/lib/server/getBundleChildren.ts +31 -122
  242. package/src/lib/server/insightsUsage.ts +63 -0
  243. package/src/lib/server/listReleases.ts +109 -44
  244. package/src/lib/server/releaseAdoption.ts +154 -0
  245. package/src/lib/server/releaseReachability.ts +8 -7
  246. package/src/lib/server/remoteConfig.ts +191 -0
  247. package/src/lib/server/runtime.server.ts +120 -31
  248. package/src/lib/server/updateFailures.ts +65 -0
  249. package/src/routeTree.gen.ts +65 -44
  250. package/src/router.tsx +3 -2
  251. package/src/routes/-installations-search.ts +32 -8
  252. package/src/routes/-releases-search.ts +76 -29
  253. package/src/routes/__root.tsx +10 -4
  254. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  255. package/src/routes/api-keys.tsx +5 -2
  256. package/src/routes/index.tsx +210 -152
  257. package/src/routes/insights.tsx +133 -18
  258. package/src/routes/insights_.distribution.tsx +422 -0
  259. package/src/routes/installations.tsx +98 -41
  260. package/src/routes/remote-config.tsx +42 -0
  261. package/src/styles.css +9 -0
  262. package/src/vite-internal.ts +15 -3
  263. package/src/vite.ts +2 -0
  264. package/.output/public/assets/InsightsPageHeader-C-Xc2MB9.js +0 -1
  265. package/.output/public/assets/api-keys-C1sgC6ko.js +0 -1
  266. package/.output/public/assets/api-keys-api-LchP65vQ.js +0 -1
  267. package/.output/public/assets/badge-BuMDU0Rf.js +0 -1
  268. package/.output/public/assets/card-BEvx_QKh.js +0 -1
  269. package/.output/public/assets/chart-no-axes-combined-C0G0fQ3J.js +0 -1
  270. package/.output/public/assets/composite-CHTH1Xhh.js +0 -1
  271. package/.output/public/assets/createServerFn-DHc2erub.js +0 -3
  272. package/.output/public/assets/dialog-dyZ1SphZ.js +0 -1
  273. package/.output/public/assets/dist-Bh1gaDfB.js +0 -1
  274. package/.output/public/assets/field-79SVH26s.js +0 -1
  275. package/.output/public/assets/index-BNkxg4jz.js +0 -10
  276. package/.output/public/assets/input-group-BbHfaBzQ.js +0 -1
  277. package/.output/public/assets/insights-D_7uSKfc.js +0 -1
  278. package/.output/public/assets/installations-BzoHWuXw.js +0 -1
  279. package/.output/public/assets/installations-DvjbUqd1.js +0 -2
  280. package/.output/public/assets/lazyRouteComponent-BN0MS05o.js +0 -1
  281. package/.output/public/assets/link-BG06whwB.js +0 -1
  282. package/.output/public/assets/loader-circle-Bbp8LGVC.js +0 -1
  283. package/.output/public/assets/preload-helper-uTix4PVD.js +0 -1
  284. package/.output/public/assets/refresh-cw-DZgiAOMk.js +0 -1
  285. package/.output/public/assets/routes-BFE-oRIT.js +0 -1
  286. package/.output/public/assets/routes-DaPMY31P.js +0 -2
  287. package/.output/public/assets/shield-check-DoIiaaXU.js +0 -1
  288. package/.output/public/assets/signing-BTRhaF5D.js +0 -1
  289. package/.output/public/assets/styles-C7U0oVke.css +0 -2
  290. package/.output/public/assets/table-BBEzJ7Xl.js +0 -1
  291. package/.output/public/assets/useCompositeListItem-BkPDaqyj.js +0 -42
  292. package/.output/server/__23tanstack-start-server-fn-resolver-Dr2ixDgQ.mjs +0 -146
  293. package/.output/server/_ssr/InsightsPageHeader-C5Has8Ry.mjs +0 -60
  294. package/.output/server/_ssr/api-keys-api-CrCZyvnl.mjs +0 -60
  295. package/.output/server/_ssr/api-keys-rpc-CFaNf-q0.mjs +0 -136
  296. package/.output/server/_ssr/api-rpc-B61KL3Eo.mjs +0 -1399
  297. package/.output/server/_ssr/auth.server-DF_shCeq.mjs +0 -47
  298. package/.output/server/_ssr/badge-BgsCP9Dp.mjs +0 -28
  299. package/.output/server/_ssr/bundle-signing-rpc-BBipZnFs.mjs +0 -16
  300. package/.output/server/_ssr/bundle-signing.server-Co_zHdJx.mjs +0 -12
  301. package/.output/server/_ssr/bundleActivity-D3_fIda2.mjs +0 -27
  302. package/.output/server/_ssr/card-DrbxCPtG.mjs +0 -86
  303. package/.output/server/_ssr/chart-qxa-_30a.mjs +0 -162
  304. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  305. package/.output/server/_ssr/config.server-CBr6PDj5.mjs +0 -368
  306. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  307. package/.output/server/_ssr/deleteBundle-BAq3JuGr.mjs +0 -66
  308. package/.output/server/_ssr/field-CqNUlqUq.mjs +0 -74
  309. package/.output/server/_ssr/getBundleChildren-COMt16m_.mjs +0 -50
  310. package/.output/server/_ssr/insights-B-DUbksX.mjs +0 -502
  311. package/.output/server/_ssr/insights-recovery-rpc-P71FH66n.mjs +0 -37
  312. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
  313. package/.output/server/_ssr/insightsRecovery-CqcosG7B.mjs +0 -149
  314. package/.output/server/_ssr/installations-BQrppti7.mjs +0 -29
  315. package/.output/server/_ssr/routes-BDabsmc7.mjs +0 -37
  316. package/.output/server/_ssr/runtime.server-jmZOKIQM.mjs +0 -438
  317. package/.output/server/_ssr/server-p7yrVBfD.mjs +0 -7468
  318. package/.output/server/_ssr/signing-CxCbnFCt.mjs +0 -134
  319. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  320. package/.output/server/_tanstack-start-manifest_v-Cwpu0C9Q.mjs +0 -100
  321. package/src/components/features/bundles/BundleActivityChart.tsx +0 -95
  322. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -121
  323. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  324. package/src/lib/bundle-signing-api.ts +0 -14
  325. package/src/lib/bundle-signing-rpc.ts +0 -16
  326. package/src/lib/server/bundle-signing.server.ts +0 -23
  327. package/src/lib/server/deleteBundle.ts +0 -145
  328. package/src/lib/server/insightsHistory.ts +0 -52
  329. package/src/lib/server/insightsRecovery.ts +0 -186
  330. package/src/routes/signing.tsx +0 -37
@@ -1,9 +1,10 @@
1
- import { useQuery } from "@tanstack/react-query";
2
1
  import { Link } from "@tanstack/react-router";
3
- import { ArrowUpRight, RotateCw, TriangleAlert } from "lucide-react";
2
+ import { ListIcon, RotateCcw, RotateCw, TriangleAlert, X } from "lucide-react";
4
3
  import { useState } from "react";
5
- import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
6
4
 
5
+ import { BundleIdDisplay } from "@/components/BundleIdDisplay";
6
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
7
+ import { Badge } from "@/components/ui/badge";
7
8
  import { Button, buttonVariants } from "@/components/ui/button";
8
9
  import {
9
10
  Card,
@@ -13,380 +14,628 @@ import {
13
14
  CardTitle,
14
15
  } from "@/components/ui/card";
15
16
  import {
16
- ChartContainer,
17
- ChartTooltip,
18
- ChartTooltipContent,
19
- } from "@/components/ui/chart";
17
+ Empty,
18
+ EmptyContent,
19
+ EmptyDescription,
20
+ EmptyHeader,
21
+ EmptyTitle,
22
+ } from "@/components/ui/empty";
23
+ import {
24
+ Select,
25
+ SelectContent,
26
+ SelectGroup,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from "@/components/ui/select";
20
31
  import { Skeleton } from "@/components/ui/skeleton";
21
- import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
22
- import type { RecoveryInput, RecoveryReport } from "@/lib/insights-recovery";
23
- import { getRecoveryReportRpc } from "@/lib/insights-recovery-rpc";
32
+ import {
33
+ Table,
34
+ TableBody,
35
+ TableCell,
36
+ TableHead,
37
+ TableHeader,
38
+ TableRow,
39
+ } from "@/components/ui/table";
40
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
41
+ import type { RecoveryInput } from "@/lib/insights-recovery";
42
+ import type { HealthChart } from "@/lib/insights-search";
43
+ import {
44
+ CRASH_MIN_ATTEMPTS,
45
+ CRASH_RATE_THRESHOLD,
46
+ crashRateOf,
47
+ DEFAULT_ADOPTION_RELEASES,
48
+ MAX_ADOPTION_RELEASES,
49
+ recommendsRollback,
50
+ } from "@/lib/release-adoption";
51
+ import type {
52
+ ComparedBundle,
53
+ ReleaseHealthState,
54
+ } from "@/lib/release-adoption-api";
55
+ import { cn } from "@/lib/utils";
24
56
 
57
+ import {
58
+ ComparedBundlesChart,
59
+ formatDeployedAt,
60
+ useSeriesColors,
61
+ } from "./ComparedBundlesChart";
25
62
  import { InsightsErrorAlert } from "./InsightsErrorAlert";
63
+ import { InsightsInfo } from "./InsightsInfo";
64
+ import { InsightsPeriodSelector } from "./InsightsPeriodSelector";
65
+ import { RollbackButton } from "./RollbackButton";
26
66
 
27
- const dates = new Intl.DateTimeFormat("en", {
28
- month: "short",
29
- day: "numeric",
30
- timeZone: "UTC",
31
- });
32
- const times = new Intl.DateTimeFormat("en", {
33
- month: "short",
34
- day: "numeric",
35
- hour: "numeric",
36
- timeZone: "UTC",
37
- });
38
- const colors = [
39
- "var(--chart-2)",
40
- "var(--foreground)",
41
- "var(--chart-1)",
42
- "var(--muted-foreground)",
43
- "var(--chart-4)",
44
- ];
45
- const dashes = [undefined, "6 3", "2 3"];
67
+ const HOUR = 3_600_000;
68
+ const DAY = 24 * HOUR;
69
+ const percent = (rate: number) => `${(rate * 100).toFixed(1)}%`;
70
+ const lengthOf = (durationMs: number) =>
71
+ durationMs > DAY ? `${durationMs / DAY} days` : `${durationMs / HOUR} hours`;
72
+ const cadenceOf = (intervalMs: number) =>
73
+ intervalMs === HOUR
74
+ ? "Hourly"
75
+ : intervalMs === DAY
76
+ ? "Daily"
77
+ : `Every ${intervalMs / HOUR} hours`;
46
78
 
47
- export function ActivityChart({ report }: { readonly report: RecoveryReport }) {
48
- const [metric, setMetric] = useState<"active" | "rollback">("active");
49
- const [selectedId, setSelectedId] = useState<string | null>(null);
50
- const selected = report.series.find(
51
- (series) => series.releaseId === selectedId,
79
+ /** A bundle's crash numbers over the period, once both counts are read. */
80
+ const crashesOf = ({ release, launched, recovered }: ComparedBundle) =>
81
+ launched === undefined || recovered === undefined
82
+ ? undefined
83
+ : {
84
+ crashes: recovered.total,
85
+ ...crashRateOf(launched.total, recovered.total),
86
+ recommended: recommendsRollback(
87
+ release,
88
+ launched.total,
89
+ recovered.total,
90
+ ),
91
+ };
92
+
93
+ /** A line sample for the legend: solid or dashed, in the text color. */
94
+ function LineSample({ dashed }: { readonly dashed?: boolean }) {
95
+ return (
96
+ <svg
97
+ aria-hidden="true"
98
+ className="h-3 w-6 shrink-0"
99
+ viewBox="0 0 24 12"
100
+ preserveAspectRatio="none"
101
+ >
102
+ <line
103
+ x1="0"
104
+ y1="6"
105
+ x2="24"
106
+ y2="6"
107
+ stroke="currentColor"
108
+ strokeWidth="2"
109
+ strokeDasharray={dashed ? "6 4" : undefined}
110
+ />
111
+ </svg>
52
112
  );
53
- const config = Object.fromEntries(
54
- report.series.map((series, index) => [
55
- `bundle${index}`,
56
- {
57
- label: series.releaseId,
58
- color: colors[index % colors.length],
59
- },
60
- ]),
113
+ }
114
+
115
+ /**
116
+ * Release health: how the newest bundles fare after their deployment, on
117
+ * one timeline, per interval. Adoption shows installations downloading each
118
+ * bundle (dashed) and launching it (solid), so the gap is the restarts still
119
+ * to come; Crashes shows them crashing back from it, and offers a rollback
120
+ * when a bundle crashes for too many of them.
121
+ */
122
+ export function InsightsOverview({
123
+ input,
124
+ chart,
125
+ onChartChange,
126
+ health,
127
+ onReleasesChange,
128
+ onWindowChange,
129
+ }: {
130
+ readonly input: RecoveryInput;
131
+ readonly chart: HealthChart;
132
+ readonly onChartChange: (chart: HealthChart) => void;
133
+ readonly health: ReleaseHealthState;
134
+ /** Chooses the bundles to compare; undefined returns to the default. */
135
+ readonly onReleasesChange: (releaseIds?: readonly string[]) => void;
136
+ readonly onWindowChange: (window: RecoveryInput["window"]) => void;
137
+ }) {
138
+ const [active, setActive] = useState<string | null>(null);
139
+ const { period, candidates, releases } = health;
140
+ const ids = (releases ?? []).map(({ release }) => release.releaseId);
141
+ const colorOf = useSeriesColors(ids);
142
+ const addable = (candidates ?? []).filter(
143
+ (candidate) => !ids.includes(candidate.releaseId),
61
144
  );
62
- const data = (report.series[0]?.points ?? []).map((point, pointIndex) => ({
63
- startMs: point.startMs,
64
- ...Object.fromEntries(
65
- report.series.map((series, index) => [
66
- `bundle${index}`,
67
- metric === "active"
68
- ? series.points[pointIndex].active
69
- : series.points[pointIndex].recoveredInstallations,
70
- ]),
71
- ),
72
- }));
73
- const formatTick = (ms: number) =>
74
- (report.intervalMs < 86_400_000 ? times : dates).format(ms);
75
- const lastSpike = selected
76
- ? [...selected.points].reverse().find((point) => point.spike)
77
- : undefined;
78
- const total =
79
- metric === "active"
80
- ? report.series.reduce(
81
- (sum, series) => sum + series.activeInstallations,
82
- 0,
83
- )
84
- : report.series.filter((series) => series.recoveredInstallations > 0)
85
- .length;
86
- return (
87
- <>
88
- <div className="flex flex-wrap items-start justify-between gap-4">
89
- <div>
90
- <p className="text-sm text-muted-foreground">
91
- {metric === "active"
92
- ? "Active installations"
93
- : "Bundles with rollbacks"}
94
- </p>
95
- <p className="mt-1 text-4xl font-semibold tracking-tight tabular-nums">
96
- {total.toLocaleString()}
145
+ const full = ids.length >= MAX_ADOPTION_RELEASES;
146
+ const wider =
147
+ period.durationMs < 7 * DAY
148
+ ? "7d"
149
+ : period.durationMs < 30 * DAY
150
+ ? "30d"
151
+ : null;
152
+ const reset = health.isDefault ? null : (
153
+ <Button
154
+ variant="ghost"
155
+ size="sm"
156
+ onClick={() => onReleasesChange(undefined)}
157
+ >
158
+ <RotateCcw aria-hidden="true" data-icon="inline-start" />
159
+ Show the newest {DEFAULT_ADOPTION_RELEASES}
160
+ </Button>
161
+ );
162
+ // Adoption draws each bundle's launches solid and its downloads dashed;
163
+ // Crashes draws its crashes.
164
+ const seriesOf = (bundle: ComparedBundle) =>
165
+ chart === "adoption" ? bundle.launched : bundle.recovered;
166
+ const loaded = (releases ?? []).filter(
167
+ (bundle) =>
168
+ seriesOf(bundle) !== undefined &&
169
+ (chart !== "adoption" || bundle.downloaded !== undefined),
170
+ );
171
+ const failed =
172
+ (releases ?? []).find(({ error }) => error !== null)?.error ?? null;
173
+ const flagged = (releases ?? []).flatMap((bundle) => {
174
+ const crashes = crashesOf(bundle);
175
+ return crashes?.recommended ? [{ ...bundle, ...crashes }] : [];
176
+ });
177
+
178
+ const remove = (releaseId: string) => {
179
+ const rest = ids.filter((id) => id !== releaseId);
180
+ onReleasesChange(rest.length === 0 ? undefined : rest);
181
+ };
182
+
183
+ const heading =
184
+ chart === "adoption" ? (
185
+ <div className="flex flex-col gap-1">
186
+ <div className="flex items-center gap-1 text-sm font-medium">
187
+ Installations downloading and launching each bundle
188
+ <InsightsInfo label="About adoption">
189
+ Dashed: installations that downloaded the bundle. Solid:
190
+ installations that launched it. After a forced update the two lines
191
+ meet.
192
+ </InsightsInfo>
193
+ </div>
194
+ <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
195
+ <p>
196
+ {cadenceOf(period.intervalMs)} · last {lengthOf(period.durationMs)}{" "}
197
+ · UTC
97
198
  </p>
199
+ <ul aria-label="Line styles" className="flex items-center gap-3">
200
+ <li className="flex items-center gap-1.5">
201
+ <LineSample dashed />
202
+ Downloaded
203
+ </li>
204
+ <li className="flex items-center gap-1.5">
205
+ <LineSample />
206
+ Launched
207
+ </li>
208
+ </ul>
98
209
  </div>
99
- <ToggleGroup
100
- aria-label="Activity metric"
101
- value={[metric]}
102
- onValueChange={(values) => {
103
- if (values[0] === "active" || values[0] === "rollback")
104
- setMetric(values[0]);
105
- }}
106
- spacing={0}
107
- variant="outline"
108
- >
109
- <ToggleGroupItem className="h-11 lg:h-8" value="active">
110
- Active
111
- </ToggleGroupItem>
112
- <ToggleGroupItem className="h-11 lg:h-8" value="rollback">
113
- Rollback
114
- </ToggleGroupItem>
115
- </ToggleGroup>
116
210
  </div>
117
- {report.series.length === 0 ? (
118
- <div className="flex h-48 items-center justify-center text-center text-sm text-muted-foreground">
119
- No bundle reports in this period. Try another channel or check again
120
- after the app reports activity.
211
+ ) : (
212
+ <div className="flex flex-col gap-1">
213
+ <div className="flex items-center gap-1 text-sm font-medium">
214
+ Crashes after each deployment
215
+ <InsightsInfo label="About crashes">
216
+ Installations that crashed on a bundle and rolled back. Crash rate =
217
+ crashed ÷ (launched + crashed). A rollback is suggested at{" "}
218
+ {percent(CRASH_RATE_THRESHOLD)} once {CRASH_MIN_ATTEMPTS}{" "}
219
+ installations tried it.
220
+ </InsightsInfo>
121
221
  </div>
222
+ <p className="text-xs text-muted-foreground">
223
+ {cadenceOf(period.intervalMs)} · last {lengthOf(period.durationMs)} ·
224
+ UTC
225
+ </p>
226
+ </div>
227
+ );
228
+
229
+ const body = (() => {
230
+ if (releases === undefined) {
231
+ return health.error ? (
232
+ <InsightsErrorAlert
233
+ error={health.error}
234
+ fallbackTitle="Release health unavailable"
235
+ />
236
+ ) : (
237
+ <Skeleton aria-label="Loading release health" className="h-64" />
238
+ );
239
+ }
240
+ if (releases.length === 0) {
241
+ return (
242
+ <Empty className="min-h-64">
243
+ <EmptyHeader>
244
+ <EmptyTitle>No bundles deployed here yet</EmptyTitle>
245
+ <EmptyDescription>
246
+ Release health follows the bundle deployments of the selected
247
+ channel and health platform. Deploy one, or choose another in the
248
+ filters.
249
+ </EmptyDescription>
250
+ </EmptyHeader>
251
+ {reset ? <EmptyContent>{reset}</EmptyContent> : null}
252
+ </Empty>
253
+ );
254
+ }
255
+ if (loaded.length === 0) {
256
+ return failed ? (
257
+ <InsightsErrorAlert
258
+ error={failed}
259
+ fallbackTitle="Release health unavailable"
260
+ />
122
261
  ) : (
123
- <>
124
- <div>
125
- <p className="mb-2 text-right text-xs text-muted-foreground">
126
- {metric === "active"
127
- ? "Installations"
128
- : "Recovered installations per interval"}{" "}
129
- · UTC
130
- </p>
131
- <ChartContainer
132
- aria-label={`${metric === "active" ? "Active" : "Rollback"} trend for all reported bundle IDs`}
133
- className="h-48 w-full aspect-auto sm:h-56"
134
- config={config}
135
- >
136
- <LineChart
137
- accessibilityLayer
138
- data={data}
139
- margin={{ left: -12, right: 12, top: 8 }}
262
+ <Skeleton aria-label="Loading release health" className="h-64" />
263
+ );
264
+ }
265
+ if (
266
+ loaded.every(
267
+ (bundle) =>
268
+ seriesOf(bundle)!.total === 0 &&
269
+ (chart !== "adoption" || bundle.downloaded!.total === 0),
270
+ )
271
+ ) {
272
+ return chart === "adoption" ? (
273
+ <Empty className="min-h-48">
274
+ <EmptyHeader>
275
+ <EmptyTitle>
276
+ No installation downloaded or launched these bundles in the last{" "}
277
+ {lengthOf(period.durationMs)}
278
+ </EmptyTitle>
279
+ <EmptyDescription>
280
+ {wider
281
+ ? "Choose a longer period, or refresh after apps download them."
282
+ : "Refresh after apps download them."}
283
+ </EmptyDescription>
284
+ </EmptyHeader>
285
+ {wider ? (
286
+ <EmptyContent>
287
+ <Button
288
+ variant="outline"
289
+ size="sm"
290
+ onClick={() => onWindowChange(wider)}
140
291
  >
141
- <CartesianGrid vertical={false} />
142
- <XAxis
143
- axisLine={false}
144
- dataKey="startMs"
145
- minTickGap={40}
146
- tickFormatter={formatTick}
147
- tickLine={false}
148
- />
149
- <YAxis
150
- allowDecimals={false}
151
- axisLine={false}
152
- tickLine={false}
153
- width={48}
154
- />
155
- <ChartTooltip
156
- content={
157
- <ChartTooltipContent
158
- className="max-h-64 max-w-[calc(100vw-3rem)] overflow-auto"
159
- labelFormatter={(_, payload) =>
160
- `${times.format(payload[0]?.payload.startMs)} · UTC`
161
- }
162
- />
163
- }
164
- />
165
- {report.series.map((series, index) => (
166
- <Line
167
- key={series.releaseId}
168
- dataKey={`bundle${index}`}
169
- type="linear"
170
- stroke={`var(--color-bundle${index})`}
171
- strokeWidth={selectedId === series.releaseId ? 3 : 2}
172
- strokeOpacity={!selected || selected === series ? 1 : 0.4}
173
- strokeDasharray={
174
- dashes[Math.floor(index / colors.length) % dashes.length]
175
- }
176
- dot={{ r: 2 }}
177
- activeDot={{ r: 4 }}
178
- isAnimationActive={false}
179
- />
180
- ))}
181
- </LineChart>
182
- </ChartContainer>
183
- </div>
184
- <div
185
- aria-label="Reported bundles"
186
- className="grid max-h-48 grid-cols-1 gap-1 overflow-y-auto sm:grid-cols-2"
187
- >
188
- {report.series.map((series, index) => (
189
- <button
190
- type="button"
191
- key={series.releaseId}
192
- aria-label={`Highlight bundle ${series.releaseId}`}
193
- aria-pressed={selectedId === series.releaseId}
194
- title={series.releaseId}
195
- onClick={() =>
196
- setSelectedId(
197
- selectedId === series.releaseId ? null : series.releaseId,
198
- )
199
- }
200
- className="flex min-w-0 items-center gap-2 rounded-md px-2 py-2 text-left text-xs hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-muted"
292
+ Show {lengthOf(wider === "7d" ? 7 * DAY : 30 * DAY)}
293
+ </Button>
294
+ </EmptyContent>
295
+ ) : null}
296
+ </Empty>
297
+ ) : (
298
+ <Empty className="min-h-48">
299
+ <EmptyHeader>
300
+ <EmptyTitle>
301
+ No crashes in the last {lengthOf(period.durationMs)}
302
+ </EmptyTitle>
303
+ <EmptyDescription>
304
+ No launch of these bundles crashed and recovered.
305
+ </EmptyDescription>
306
+ </EmptyHeader>
307
+ </Empty>
308
+ );
309
+ }
310
+ return (
311
+ <ComparedBundlesChart
312
+ period={period}
313
+ lines={releases.map((bundle) => ({
314
+ release: bundle.release,
315
+ points: seriesOf(bundle)?.points,
316
+ ...(chart === "adoption"
317
+ ? { dashed: bundle.downloaded?.points }
318
+ : {}),
319
+ measuredAtMs: seriesOf(bundle)?.measuredAtMs,
320
+ }))}
321
+ colorOf={colorOf}
322
+ active={active}
323
+ names={
324
+ chart === "adoption"
325
+ ? { solid: "Launched", dashed: "Downloaded" }
326
+ : { solid: "Crashed" }
327
+ }
328
+ label={
329
+ chart === "adoption"
330
+ ? "Installations downloading (dashed) and launching (solid) each bundle per interval"
331
+ : "Crashes of each bundle per interval"
332
+ }
333
+ />
334
+ );
335
+ })();
336
+
337
+ return (
338
+ <section aria-label="Release health">
339
+ <Card className="min-w-0 overflow-hidden shadow-sm">
340
+ <CardHeader className="flex flex-col gap-4 p-6">
341
+ <div className="flex flex-wrap items-center justify-between gap-4">
342
+ <CardTitle>Release health</CardTitle>
343
+ <div className="flex items-center gap-2">
344
+ <InsightsPeriodSelector
345
+ window={input.window}
346
+ onWindowChange={onWindowChange}
347
+ />
348
+ <Button
349
+ aria-label="Refresh release health"
350
+ variant="ghost"
351
+ size="icon-lg"
352
+ disabled={health.isFetching}
353
+ onClick={health.refresh}
201
354
  >
202
- <svg
355
+ <RotateCw
203
356
  aria-hidden="true"
204
- width="20"
205
- height="8"
206
- className="shrink-0"
207
- >
208
- <line
209
- x1="0"
210
- x2="20"
211
- y1="4"
212
- y2="4"
213
- stroke={colors[index % colors.length]}
214
- strokeWidth="2"
215
- strokeDasharray={
216
- dashes[Math.floor(index / colors.length) % dashes.length]
217
- }
218
- />
219
- </svg>
220
- <span className="min-w-0 truncate font-mono">
221
- {series.releaseId}
222
- </span>
223
- {series.points.some((point) => point.spike) ? (
224
- <TriangleAlert
225
- aria-label="Rollback spike"
226
- className="size-3 shrink-0"
227
- />
228
- ) : null}
229
- <span className="ml-auto tabular-nums">
230
- {(metric === "active"
231
- ? series.activeInstallations
232
- : series.recoveredInstallations
233
- ).toLocaleString()}
234
- </span>
235
- </button>
236
- ))}
357
+ className={cn(
358
+ health.isFetching && "motion-safe:animate-spin",
359
+ )}
360
+ />
361
+ </Button>
362
+ </div>
237
363
  </div>
238
- {selected ? (
239
- <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3">
240
- <div className="min-w-0 flex-1">
241
- <p className="break-all font-mono text-xs">
242
- {selected.releaseId}
243
- </p>
244
- <p className="mt-1 text-xs text-muted-foreground">
245
- Active {selected.activeInstallations.toLocaleString()} ·
246
- Rollback {selected.recoveredInstallations.toLocaleString()} in
247
- this period
248
- </p>
249
- {metric === "rollback" && lastSpike ? (
250
- <p className="mt-2 flex items-center gap-2 text-xs">
251
- <TriangleAlert
252
- className="size-3 shrink-0"
253
- aria-hidden="true"
254
- />
255
- Rollback spike on {dates.format(lastSpike.startMs)}. Review
256
- this bundle’s delivery settings.
257
- </p>
258
- ) : null}
259
- </div>
260
- <Link
261
- className={buttonVariants({ variant: "outline", size: "sm" })}
262
- to="/"
263
- search={{ releaseId: selected.releaseId }}
364
+ </CardHeader>
365
+ <CardContent
366
+ aria-busy={health.isFetching}
367
+ className="flex flex-col gap-5 px-6 pb-6"
368
+ >
369
+ <Tabs
370
+ value={chart}
371
+ onValueChange={(value) => onChartChange(value as HealthChart)}
372
+ className="gap-5"
373
+ >
374
+ <div className="flex flex-wrap items-center justify-between gap-3">
375
+ <TabsList
376
+ aria-label="Release health chart"
377
+ className="min-h-11 sm:min-h-9"
264
378
  >
265
- Review bundle
266
- <ArrowUpRight aria-hidden="true" />
267
- </Link>
379
+ <TabsTrigger value="adoption" className="px-3">
380
+ Adoption
381
+ </TabsTrigger>
382
+ <TabsTrigger value="crashes" className="px-3">
383
+ Crashes
384
+ {flagged.length > 0 ? (
385
+ <>
386
+ <TriangleAlert
387
+ aria-hidden="true"
388
+ className="text-destructive"
389
+ />
390
+ <span className="sr-only">, rollback recommended</span>
391
+ </>
392
+ ) : null}
393
+ </TabsTrigger>
394
+ </TabsList>
395
+ {reset}
268
396
  </div>
269
- ) : null}
270
- <details className="text-xs text-muted-foreground">
271
- <summary className="cursor-pointer">View chart data</summary>
272
- <div className="mt-2 max-h-64 overflow-auto">
273
- <table className="w-full text-left">
274
- <caption className="sr-only">
275
- {metric === "active"
276
- ? "Active installations"
277
- : "Recovered installations"}{" "}
278
- by bundle ID, UTC
279
- </caption>
280
- <thead>
281
- <tr>
282
- <th scope="col" className="pr-4">
283
- Date (UTC)
284
- </th>
285
- {report.series.map((series) => (
286
- <th
287
- scope="col"
288
- className="px-2 font-mono"
289
- key={series.releaseId}
290
- >
291
- {series.releaseId}
292
- </th>
293
- ))}
294
- </tr>
295
- </thead>
296
- <tbody>
297
- {data.map((point, i) => (
298
- <tr key={point.startMs}>
299
- <th
300
- scope="row"
301
- className="whitespace-nowrap pr-4 font-normal"
397
+ <TabsContent value="adoption" className="flex flex-col gap-4">
398
+ {heading}
399
+ {chart === "adoption" ? body : null}
400
+ </TabsContent>
401
+ <TabsContent value="crashes" className="flex flex-col gap-4">
402
+ {flagged.map((bundle) => (
403
+ <Alert key={bundle.release.releaseId} variant="destructive">
404
+ <TriangleAlert aria-hidden="true" />
405
+ <AlertTitle>
406
+ Roll back{" "}
407
+ <BundleIdDisplay
408
+ bundleId={bundle.release.releaseId}
409
+ className="text-sm"
410
+ />
411
+ </AlertTitle>
412
+ <AlertDescription className="flex flex-col items-start gap-3">
413
+ It crashed for {percent(bundle.rate)} of the installations
414
+ that tried it ({bundle.crashes.toLocaleString()} of{" "}
415
+ {bundle.attempts.toLocaleString()}), above{" "}
416
+ {percent(CRASH_RATE_THRESHOLD)}.
417
+ <RollbackButton
418
+ release={bundle.release}
419
+ rate={bundle.rate}
420
+ crashes={bundle.crashes}
421
+ attempts={bundle.attempts}
422
+ />
423
+ </AlertDescription>
424
+ </Alert>
425
+ ))}
426
+ {heading}
427
+ {chart === "crashes" ? body : null}
428
+ </TabsContent>
429
+ </Tabs>
430
+ {releases !== undefined && releases.length > 0 ? (
431
+ <>
432
+ {failed && loaded.length > 0 ? (
433
+ <InsightsErrorAlert
434
+ error={failed}
435
+ fallbackTitle="Some bundles could not be read"
436
+ />
437
+ ) : null}
438
+ <Table aria-label="Compared bundles">
439
+ <TableHeader>
440
+ <TableRow>
441
+ <TableHead>Bundle</TableHead>
442
+ {chart === "adoption" ? (
443
+ <>
444
+ <TableHead className="text-right">Downloaded</TableHead>
445
+ <TableHead className="text-right">Launched</TableHead>
446
+ </>
447
+ ) : (
448
+ <>
449
+ <TableHead className="text-right">Crashed</TableHead>
450
+ <TableHead className="text-right">Crash rate</TableHead>
451
+ </>
452
+ )}
453
+ <TableHead className="w-10">
454
+ <span className="sr-only">Remove</span>
455
+ </TableHead>
456
+ </TableRow>
457
+ </TableHeader>
458
+ <TableBody>
459
+ {releases.map((bundle) => {
460
+ const { release } = bundle;
461
+ const crashes = crashesOf(bundle);
462
+ return (
463
+ <TableRow
464
+ key={release.releaseId}
465
+ data-state={
466
+ active === release.releaseId ? "selected" : undefined
467
+ }
468
+ onMouseEnter={() => setActive(release.releaseId)}
469
+ onMouseLeave={() => setActive(null)}
470
+ onFocus={() => setActive(release.releaseId)}
471
+ onBlur={() => setActive(null)}
302
472
  >
303
- {times.format(point.startMs)}
304
- </th>
305
- {report.series.map((series) => (
306
- <td
307
- className="px-2 tabular-nums"
308
- key={series.releaseId}
473
+ {/* The bundle cell takes the room left, and wraps in it. */}
474
+ <TableCell className="w-full max-w-0 whitespace-normal">
475
+ <div className="flex min-w-0 items-center gap-2 sm:gap-3">
476
+ {/* Narrower on a phone, so a bundle's ID keeps more of the row. */}
477
+ <svg
478
+ aria-hidden="true"
479
+ className="h-3 w-4 shrink-0 sm:w-6"
480
+ viewBox="0 0 24 12"
481
+ preserveAspectRatio="none"
482
+ >
483
+ <line
484
+ x1="0"
485
+ y1="6"
486
+ x2="24"
487
+ y2="6"
488
+ stroke={colorOf(release.releaseId)}
489
+ strokeWidth="3"
490
+ />
491
+ </svg>
492
+ <div className="flex min-w-0 flex-col">
493
+ <span className="flex min-w-0 items-center gap-2">
494
+ <BundleIdDisplay
495
+ bundleId={release.releaseId}
496
+ className="min-w-0"
497
+ />
498
+ {release.enabled ? null : (
499
+ <Badge variant="outline">Disabled</Badge>
500
+ )}
501
+ </span>
502
+ <span className="text-xs break-words text-muted-foreground">
503
+ Deployed{" "}
504
+ {formatDeployedAt(release.deployedAtMs)}
505
+ {release.targetAppVersion
506
+ ? ` · ${release.targetAppVersion}`
507
+ : ""}
508
+ </span>
509
+ </div>
510
+ </div>
511
+ </TableCell>
512
+ {chart === "adoption" ? (
513
+ <>
514
+ <TableCell className="text-right whitespace-nowrap tabular-nums">
515
+ {bundle.downloaded?.total.toLocaleString() ?? "—"}
516
+ </TableCell>
517
+ <TableCell className="text-right font-medium whitespace-nowrap tabular-nums">
518
+ {bundle.launched?.total.toLocaleString() ?? "—"}
519
+ </TableCell>
520
+ </>
521
+ ) : (
522
+ <>
523
+ <TableCell className="text-right whitespace-nowrap tabular-nums">
524
+ {crashes === undefined ? (
525
+ "—"
526
+ ) : crashes.crashes === 0 ? (
527
+ <span className="text-muted-foreground">0</span>
528
+ ) : (
529
+ crashes.crashes.toLocaleString()
530
+ )}
531
+ </TableCell>
532
+ <TableCell className="text-right whitespace-nowrap tabular-nums">
533
+ {crashes === undefined ||
534
+ crashes.attempts === 0 ? (
535
+ "—"
536
+ ) : crashes.recommended ? (
537
+ <span className="inline-flex items-center gap-1 font-medium text-destructive">
538
+ <TriangleAlert
539
+ aria-hidden="true"
540
+ className="size-3.5"
541
+ />
542
+ {percent(crashes.rate)}
543
+ <span className="sr-only">
544
+ , rollback recommended
545
+ </span>
546
+ </span>
547
+ ) : (
548
+ <span
549
+ className={cn(
550
+ crashes.attempts < CRASH_MIN_ATTEMPTS &&
551
+ "text-muted-foreground",
552
+ )}
553
+ title={
554
+ crashes.attempts < CRASH_MIN_ATTEMPTS
555
+ ? `Only ${crashes.attempts} installations tried it`
556
+ : undefined
557
+ }
558
+ >
559
+ {percent(crashes.rate)}
560
+ </span>
561
+ )}
562
+ </TableCell>
563
+ </>
564
+ )}
565
+ <TableCell>
566
+ <Button
567
+ aria-label={`Remove ${release.releaseId}`}
568
+ variant="ghost"
569
+ size="icon-xs"
570
+ onClick={() => remove(release.releaseId)}
571
+ >
572
+ <X aria-hidden="true" />
573
+ </Button>
574
+ </TableCell>
575
+ </TableRow>
576
+ );
577
+ })}
578
+ </TableBody>
579
+ </Table>
580
+ {addable.length > 0 ? (
581
+ <Select
582
+ items={Object.fromEntries(
583
+ addable.map((candidate) => [
584
+ candidate.releaseId,
585
+ `${candidate.releaseId} · ${formatDeployedAt(candidate.deployedAtMs)}`,
586
+ ]),
587
+ )}
588
+ value={null}
589
+ disabled={full}
590
+ onValueChange={(value) => {
591
+ if (value !== null) onReleasesChange([...ids, value]);
592
+ }}
593
+ >
594
+ <SelectTrigger
595
+ aria-label="Add a bundle"
596
+ className="min-h-11 w-full sm:min-h-9 sm:w-80"
597
+ >
598
+ <SelectValue
599
+ placeholder={
600
+ full
601
+ ? `Up to ${MAX_ADOPTION_RELEASES} bundles`
602
+ : "Add a bundle"
603
+ }
604
+ />
605
+ </SelectTrigger>
606
+ <SelectContent>
607
+ <SelectGroup>
608
+ {addable.map((candidate) => (
609
+ <SelectItem
610
+ key={candidate.releaseId}
611
+ value={candidate.releaseId}
309
612
  >
310
- {(metric === "active"
311
- ? series.points[i].active
312
- : series.points[i].recoveredInstallations) ?? "—"}
313
- </td>
613
+ <span className="flex min-w-0 flex-col gap-0.5">
614
+ <BundleIdDisplay bundleId={candidate.releaseId} />{" "}
615
+ <span className="text-xs text-muted-foreground">
616
+ Deployed{" "}
617
+ {formatDeployedAt(candidate.deployedAtMs)}
618
+ </span>
619
+ </span>
620
+ </SelectItem>
314
621
  ))}
315
- </tr>
316
- ))}
317
- </tbody>
318
- </table>
319
- </div>
320
- </details>
321
- </>
322
- )}
323
- </>
324
- );
325
- }
326
-
327
- export function InsightsOverview({ input }: { readonly input: RecoveryInput }) {
328
- const query = useQuery({
329
- queryKey: ["rollout-activity", input],
330
- queryFn: () => getRecoveryReportRpc({ data: input }),
331
- staleTime: 30_000,
332
- });
333
- return (
334
- <Card className="min-w-0 overflow-hidden shadow-sm">
335
- <CardHeader className="flex-row items-center justify-between gap-4 px-4 pt-4 pb-2 sm:px-6 sm:pt-6">
336
- <CardTitle className="text-sm font-medium">
337
- Bundle activity ·{" "}
338
- {input.window === "30d"
339
- ? "30 days"
340
- : input.window === "7d"
341
- ? "7 days"
342
- : "24 hours"}
343
- </CardTitle>
344
- <Button
345
- aria-label="Refresh bundle activity"
346
- variant="ghost"
347
- size="icon-sm"
348
- disabled={query.isFetching}
349
- onClick={() => void query.refetch()}
350
- >
351
- <RotateCw aria-hidden="true" />
352
- </Button>
353
- </CardHeader>
354
- <CardContent className="flex flex-col gap-4 px-4 pb-4 sm:px-6 sm:pb-6">
355
- {query.isPending ? (
356
- <Skeleton
357
- aria-label="Loading bundle activity"
358
- className="h-64 w-full"
359
- />
360
- ) : query.error ? (
361
- <InsightsErrorAlert
362
- error={query.error}
363
- fallbackTitle="Bundle activity unavailable"
364
- />
365
- ) : query.data ? (
366
- <ActivityChart key={JSON.stringify(input)} report={query.data} />
367
- ) : null}
368
- </CardContent>
369
- {query.data && !query.error ? (
370
- <CardFooter className="flex-col items-start gap-2 border-t bg-muted/15 px-4 py-3 text-xs text-muted-foreground sm:px-6">
371
- <p>
372
- Active follows each installation’s last reported ID in this period.
373
- Rollbacks count installations recovered from each ID per interval.
374
- </p>
375
- {query.data.unattributedInstallations > 0 ? (
376
- <p>
377
- {query.data.unattributedInstallations.toLocaleString()} reporting
378
- installations have no observed bundle ID and are excluded.
379
- </p>
380
- ) : null}
381
- {query.data.truncated ? (
382
- <p>
383
- Partial history — only reports since{" "}
384
- {times.format(query.data.sinceMs)} UTC are included. Choose a
385
- shorter period to see more complete counts.
386
- </p>
622
+ </SelectGroup>
623
+ </SelectContent>
624
+ </Select>
625
+ ) : null}
626
+ </>
387
627
  ) : null}
628
+ </CardContent>
629
+ <CardFooter className="flex items-center justify-end border-t px-6 py-5">
630
+ <Link
631
+ className={buttonVariants({ variant: "outline", size: "sm" })}
632
+ to="/installations"
633
+ >
634
+ <ListIcon aria-hidden="true" />
635
+ Event history
636
+ </Link>
388
637
  </CardFooter>
389
- ) : null}
390
- </Card>
638
+ </Card>
639
+ </section>
391
640
  );
392
641
  }