@hot-updater/console 1.0.0-rc.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-jB4Loglh.js +1 -0
  3. package/.output/public/assets/EventDetails-B6yUsyNA.js +1 -0
  4. package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
  5. package/.output/public/assets/InsightsInfo-BNO0WznZ.js +1 -0
  6. package/.output/public/assets/InsightsPageHeader-B892oVml.js +1 -0
  7. package/.output/public/assets/NotFoundPage-DIqCpWa3.js +1 -0
  8. package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
  9. package/.output/public/assets/api-DCM0wr6O.js +1 -0
  10. package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
  11. package/.output/public/assets/api-keys-DVTL_--f.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-BH5m_PtQ.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-6o51GkFe.js +11 -0
  22. package/.output/public/assets/input-group-ja_30qaF.js +1 -0
  23. package/.output/public/assets/insights-CmraDzQh.js +1 -0
  24. package/.output/public/assets/insights-CxZ2GCYV.js +2 -0
  25. package/.output/public/assets/insights-api-BigAU9sZ.js +1 -0
  26. package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
  27. package/.output/public/assets/insights-vNRCrfnS.js +42 -0
  28. package/.output/public/assets/insights_.distribution-ChOerfJt.js +1 -0
  29. package/.output/public/assets/insights_.distribution-D-e4iga8.js +1 -0
  30. package/.output/public/assets/insights_.distribution-DZtkibX1.js +2 -0
  31. package/.output/public/assets/installations-BazSJXE0.js +2 -0
  32. package/.output/public/assets/installations-ClHTqdAO.js +1 -0
  33. package/.output/public/assets/installations-DpKsBuHi.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-BWwHOkCK.js +1 -0
  38. package/.output/public/assets/remote-config-Br56BODb.js +6 -0
  39. package/.output/public/assets/remote-config-CXOi0LRu.js +1 -0
  40. package/.output/public/assets/routes-B88Erkpk.js +2 -0
  41. package/.output/public/assets/routes-CfzsAleC.js +1 -0
  42. package/.output/public/assets/search-DaTDPW25.js +1 -0
  43. package/.output/public/assets/select-BZZBXMkr.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-DTGEAJFv.css +2 -0
  47. package/.output/public/assets/switch-DZ1TPwx-.js +1 -0
  48. package/.output/public/assets/table-Gm4qRWwE.js +1 -0
  49. package/.output/public/assets/tabs-C7FOEjFC.js +1 -0
  50. package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
  51. package/.output/public/assets/useCompositeListItem-9E4pj_CH.js +1 -0
  52. package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
  53. package/.output/server/__23tanstack-start-server-fn-resolver-BpRlos5Q.mjs +170 -0
  54. package/.output/server/_libs/@base-ui/react+[...].mjs +4389 -4207
  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 +221 -9
  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-x94o9rlU.mjs +13 -0
  67. package/.output/server/_ssr/ConsoleFeatureUnavailable-ldW8v8-q.mjs +58 -0
  68. package/.output/server/_ssr/EventDetails-Q8kfj9Nr.mjs +344 -0
  69. package/.output/server/_ssr/{HashValueDisplay-CyMmr0O9.mjs → HashValueDisplay-n1GholGS.mjs} +3 -3
  70. package/.output/server/_ssr/InsightsInfo-DuJFatQO.mjs +75 -0
  71. package/.output/server/_ssr/InsightsPageHeader-BSUX3Ye9.mjs +145 -0
  72. package/.output/server/_ssr/NotFoundPage-DZzOOzS3.mjs +37 -0
  73. package/.output/server/_ssr/{dialog-CKvQ4VIE.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
  74. package/.output/server/_ssr/{api-keys-CzZ7Ont4.mjs → api-keys-B4sYm8dM.mjs} +48 -9
  75. package/.output/server/_ssr/api-keys-CEnM9fqG.mjs +5 -0
  76. package/.output/server/_ssr/api-keys-rpc-BIFuUgMt.mjs +52 -0
  77. package/.output/server/_ssr/api-oZjRn9tP.mjs +186 -0
  78. package/.output/server/_ssr/api-rpc-BNiFRRK8.mjs +240 -0
  79. package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
  80. package/.output/server/_ssr/{auth-rpc-DH6Bhwcz.mjs → auth-rpc-DnZqYxyq.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-DFpyxRDd.mjs +5374 -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-DskXCAyI.mjs +35 -0
  85. package/.output/server/_ssr/console-access-BJfAXknd.mjs +17 -0
  86. package/.output/server/_ssr/console-features-Oz0q20jJ.mjs +61 -0
  87. package/.output/server/_ssr/console-features-api-C9kiKvaV.mjs +43 -0
  88. package/.output/server/_ssr/console-features-rpc-P0GL7Z-R.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-CPphvbag.mjs +73 -0
  94. package/.output/server/_ssr/dialog-BbYDMjdC.mjs +84 -0
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-CbQbBIw-.mjs} +140 -7
  96. package/.output/server/_ssr/{downloadBundle-CI7JUKvC.mjs → downloadBundle-CZysjmNW.mjs} +10 -10
  97. package/.output/server/_ssr/empty-CVLGfxuu.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-asw83X0v.mjs +189 -0
  101. package/.output/server/_ssr/getBundleChildren-Cv9iz1As.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-BrGSjz4S.mjs → input-group-CuYJHosb.mjs} +2 -2
  103. package/.output/server/_ssr/insights-B55FG5fm.mjs +14 -0
  104. package/.output/server/_ssr/insights-CWnHIFUV.mjs +5 -0
  105. package/.output/server/_ssr/insights-CpLpedFZ.mjs +2315 -0
  106. package/.output/server/_ssr/insights-api-sjws23WF.mjs +122 -0
  107. package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
  108. package/.output/server/_ssr/insights-failures-rpc-BvbWvqNf.mjs +36 -0
  109. package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
  110. package/.output/server/_ssr/insights-recovery-BWQhiZaF.mjs +28 -0
  111. package/.output/server/_ssr/insights-recovery-rpc-GiYXdkaY.mjs +32 -0
  112. package/.output/server/_ssr/{insights-rpc-DDKmIrpV.mjs → insights-rpc-DP_8Rmz4.mjs} +22 -14
  113. package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-BiolgB8e.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-KStL0zOx.mjs +41 -0
  116. package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-CVrnbP3R.mjs +7 -0
  118. package/.output/server/_ssr/insights_.distribution-D2HaNTAA.mjs +16 -0
  119. package/.output/server/_ssr/{insights_.distribution-cIsU-kHG.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
  120. package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
  121. package/.output/server/_ssr/{installations-C5anhyP0.mjs → installations-BSCmFq8Q.mjs} +279 -367
  122. package/.output/server/_ssr/installations-DD1g__Nc.mjs +69 -0
  123. package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
  124. package/.output/server/_ssr/plugins-BaD1slHx.mjs +2086 -0
  125. package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
  126. package/.output/server/_ssr/release-adoption-rpc-BsstqxUp.mjs +41 -0
  127. package/.output/server/_ssr/releaseAdoption-CtOFkwli.mjs +114 -0
  128. package/.output/server/_ssr/releaseManagement-DFMUnDF5.mjs +11 -0
  129. package/.output/server/_ssr/remote-config-C2Ee8lXO.mjs +5 -0
  130. package/.output/server/_ssr/remote-config-Dv1ccMo-.mjs +17 -0
  131. package/.output/server/_ssr/remote-config-of0lGQH8.mjs +2648 -0
  132. package/.output/server/_ssr/remote-config-rpc-BzX31mUS.mjs +115 -0
  133. package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
  134. package/.output/server/_ssr/{router-BuanSsRI.mjs → router-D8NuoJ_I.mjs} +107 -134
  135. package/.output/server/_ssr/{routes-DY-vqU6R.mjs → routes-DgvnFqDy.mjs} +195 -429
  136. package/.output/server/_ssr/routes-V8sDybGf.mjs +409 -0
  137. package/.output/server/_ssr/runtime.server-De0QVWc9.mjs +104 -0
  138. package/.output/server/_ssr/schema-BiAVyEj2.mjs +361 -0
  139. package/.output/server/_ssr/{select-DD2szs3Y.mjs → select-t3iXRwc9.mjs} +3 -3
  140. package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
  141. package/.output/server/_ssr/{createSsrRpc-CdlfejmI.mjs → sidebar-DJcErBMo.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-E-btwMGt.mjs +43 -0
  145. package/.output/server/_ssr/{table-CTj6n-4F.mjs → table-37Dd23TN.mjs} +2 -2
  146. package/.output/server/_ssr/tabs-DEFnTm95.mjs +45 -0
  147. package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
  148. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C88s9jWF.mjs} +2 -2
  149. package/.output/server/_tanstack-start-manifest_v-55AGsehh.mjs +139 -0
  150. package/.output/server/index.mjs +309 -211
  151. package/LICENSE +13 -14
  152. package/README.md +80 -71
  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 +69 -74
  158. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  159. package/src/components/NotFoundPage.tsx +7 -7
  160. package/src/components/features/FeatureSlots.tsx +103 -0
  161. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  162. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  163. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  164. package/src/components/features/channels/ChannelManagementDialog.tsx +8 -6
  165. package/src/components/features/insights/AppUsage.tsx +41 -14
  166. package/src/components/features/insights/BuiltinBundle.tsx +36 -0
  167. package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
  168. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  169. package/src/components/features/insights/EventDetails.tsx +390 -26
  170. package/src/components/features/insights/EventHistoryCard.tsx +111 -25
  171. package/src/components/features/insights/EventHistoryList.tsx +1 -1
  172. package/src/components/features/insights/FailureReports.tsx +407 -0
  173. package/src/components/features/insights/InsightsControls.tsx +240 -142
  174. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  175. package/src/components/features/insights/InsightsOverview.tsx +601 -403
  176. package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
  177. package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
  178. package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
  179. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  180. package/src/components/features/insights/ReleaseActivity.tsx +308 -0
  181. package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
  182. package/src/components/features/insights/RollbackButton.tsx +118 -0
  183. package/src/components/features/insights/UpdateFailures.tsx +382 -0
  184. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  185. package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
  186. package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
  187. package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
  188. package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
  189. package/src/components/features/remote-config/ParametersCard.tsx +254 -0
  190. package/src/components/features/remote-config/PreviewCard.tsx +284 -0
  191. package/src/components/features/remote-config/PublishDialog.tsx +325 -0
  192. package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
  193. package/src/components/features/remote-config/RuleSummary.tsx +21 -0
  194. package/src/components/features/remote-config/ValueInput.tsx +151 -0
  195. package/src/components/features/remote-config/VersionsCard.tsx +384 -0
  196. package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
  197. package/src/components/features/remote-config/useDateTimeText.ts +16 -0
  198. package/src/components/ui/sidebar.tsx +22 -29
  199. package/src/index.ts +26 -19
  200. package/src/lib/api-keys-api.ts +0 -28
  201. package/src/lib/api-keys-rpc.ts +27 -38
  202. package/src/lib/api-rpc.ts +125 -288
  203. package/src/lib/api.ts +15 -134
  204. package/src/lib/bundle-activity.ts +24 -4
  205. package/src/lib/console-access.ts +20 -0
  206. package/src/lib/console-features-api.ts +68 -0
  207. package/src/lib/console-features-rpc.ts +13 -0
  208. package/src/lib/console-features.ts +98 -0
  209. package/src/lib/insights-api.ts +56 -2
  210. package/src/lib/insights-errors.ts +64 -0
  211. package/src/lib/insights-failures-rpc.ts +36 -0
  212. package/src/lib/insights-failures.ts +130 -0
  213. package/src/lib/insights-recovery-rpc.ts +13 -17
  214. package/src/lib/insights-recovery.ts +12 -36
  215. package/src/lib/insights-retention.ts +21 -0
  216. package/src/lib/insights-rpc.ts +30 -12
  217. package/src/lib/insights-search.ts +50 -4
  218. package/src/lib/insights-usage-rpc.ts +15 -9
  219. package/src/lib/insights-usage.ts +41 -3
  220. package/src/lib/insights-view.ts +34 -5
  221. package/src/lib/release-adoption-api.ts +214 -0
  222. package/src/lib/release-adoption-rpc.ts +49 -0
  223. package/src/lib/release-adoption.ts +164 -0
  224. package/src/lib/remote-config-api.ts +119 -0
  225. package/src/lib/remote-config-draft.ts +666 -0
  226. package/src/lib/remote-config-rpc.ts +196 -0
  227. package/src/lib/scroll-restoration.ts +21 -0
  228. package/src/lib/server/auth.server.ts +13 -5
  229. package/src/lib/server/bundleActivity.ts +27 -28
  230. package/src/lib/server/config.server.ts +21 -76
  231. package/src/lib/server/console-feature-apis.ts +70 -0
  232. package/src/lib/server/console-runtime.server.ts +82 -38
  233. package/src/lib/server/downloadBundle.ts +14 -15
  234. package/src/lib/server/failureReports.ts +50 -0
  235. package/src/lib/server/getBundleChildren.ts +31 -122
  236. package/src/lib/server/insightsUsage.ts +52 -113
  237. package/src/lib/server/listReleases.ts +109 -44
  238. package/src/lib/server/releaseAdoption.ts +151 -0
  239. package/src/lib/server/releaseReachability.ts +8 -8
  240. package/src/lib/server/remoteConfig.ts +75 -0
  241. package/src/lib/server/runtime.server.ts +120 -31
  242. package/src/lib/server/updateFailures.ts +66 -0
  243. package/src/lib/use-dialog-target.ts +27 -0
  244. package/src/routeTree.gen.ts +44 -44
  245. package/src/router.tsx +3 -2
  246. package/src/routes/-installations-search.ts +32 -8
  247. package/src/routes/-releases-search.ts +76 -29
  248. package/src/routes/__root.tsx +7 -1
  249. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  250. package/src/routes/api-keys.tsx +5 -2
  251. package/src/routes/index.tsx +210 -152
  252. package/src/routes/insights.tsx +91 -23
  253. package/src/routes/insights_.distribution.tsx +50 -9
  254. package/src/routes/installations.tsx +97 -40
  255. package/src/routes/remote-config.tsx +42 -0
  256. package/src/styles.css +9 -0
  257. package/src/vite-internal.ts +15 -3
  258. package/src/vite.ts +2 -0
  259. package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
  260. package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
  261. package/.output/public/assets/api-CdUyUqzR.js +0 -42
  262. package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
  263. package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
  264. package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
  265. package/.output/public/assets/card-CYHs5FZv.js +0 -1
  266. package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
  267. package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
  268. package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
  269. package/.output/public/assets/dist-BR4GzeEu.js +0 -1
  270. package/.output/public/assets/field-B3CYDpjg.js +0 -1
  271. package/.output/public/assets/index-Cb_D1D6N.js +0 -10
  272. package/.output/public/assets/info-C3y9HNoU.js +0 -1
  273. package/.output/public/assets/input-group-nLZylhWF.js +0 -1
  274. package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
  275. package/.output/public/assets/insights-DS3iO7Up.js +0 -1
  276. package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
  277. package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
  278. package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
  279. package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
  280. package/.output/public/assets/installations-CkFW4fV9.js +0 -2
  281. package/.output/public/assets/installations-DxTaIcHD.js +0 -1
  282. package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
  283. package/.output/public/assets/list-Dqt53fWZ.js +0 -1
  284. package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
  285. package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
  286. package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
  287. package/.output/public/assets/routes-Cgtvspjg.js +0 -2
  288. package/.output/public/assets/routes-O2r2UUN8.js +0 -1
  289. package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
  290. package/.output/public/assets/signing-BgspC-xP.js +0 -1
  291. package/.output/public/assets/styles-DnrWuBwd.css +0 -2
  292. package/.output/public/assets/table-BuWQgxJJ.js +0 -1
  293. package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
  294. package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
  295. package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
  296. package/.output/server/__23tanstack-start-server-fn-resolver-DBRojHaG.mjs +0 -150
  297. package/.output/server/_ssr/InsightsPageHeader-mtxVuZKK.mjs +0 -64
  298. package/.output/server/_ssr/api-BfCgYlj5.mjs +0 -332
  299. package/.output/server/_ssr/api-keys-api-C3oeq01R.mjs +0 -60
  300. package/.output/server/_ssr/api-keys-rpc-CdPFdGOx.mjs +0 -136
  301. package/.output/server/_ssr/api-rpc-B2E1ZIG5.mjs +0 -1399
  302. package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
  303. package/.output/server/_ssr/bundle-signing-rpc-Di7-I6fE.mjs +0 -16
  304. package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
  305. package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
  306. package/.output/server/_ssr/card-Bemj0dpY.mjs +0 -108
  307. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  308. package/.output/server/_ssr/config.server-B8WsT3s0.mjs +0 -368
  309. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  310. package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
  311. package/.output/server/_ssr/field-BrQ2u0fT.mjs +0 -74
  312. package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
  313. package/.output/server/_ssr/insights-BwQ-wF2a.mjs +0 -10
  314. package/.output/server/_ssr/insights-gr1I_dJU.mjs +0 -938
  315. package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
  316. package/.output/server/_ssr/insights-recovery-rpc-DOzxfYfv.mjs +0 -37
  317. package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
  318. package/.output/server/_ssr/insights-usage-rpc-4ImOq_0p.mjs +0 -184
  319. package/.output/server/_ssr/insights-usage-rpc-BUruODjE.mjs +0 -24
  320. package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
  321. package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
  322. package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
  323. package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
  324. package/.output/server/_ssr/insights_.distribution-VmHuikRE.mjs +0 -10
  325. package/.output/server/_ssr/installations-HXMF_kKf.mjs +0 -29
  326. package/.output/server/_ssr/routes-Dru3IRj0.mjs +0 -37
  327. package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
  328. package/.output/server/_ssr/server-DJm72Kj-.mjs +0 -7468
  329. package/.output/server/_ssr/signing-H8Lg4lPe.mjs +0 -133
  330. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  331. package/.output/server/_tanstack-start-manifest_v-DsFcTLw_.mjs +0 -125
  332. package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
  333. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
  334. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  335. package/src/lib/bundle-signing-api.ts +0 -14
  336. package/src/lib/bundle-signing-rpc.ts +0 -16
  337. package/src/lib/server/bundle-signing.server.ts +0 -23
  338. package/src/lib/server/deleteBundle.ts +0 -145
  339. package/src/lib/server/insightsHistory.ts +0 -52
  340. package/src/lib/server/insightsRecovery.ts +0 -187
  341. package/src/routes/signing.tsx +0 -37
@@ -1,8 +1,10 @@
1
1
  import { Link } from "@tanstack/react-router";
2
- import { ArrowUpRight, ListIcon, RotateCw, TriangleAlert } from "lucide-react";
2
+ import { ListIcon, RotateCcw, RotateCw, TriangleAlert, X } from "lucide-react";
3
3
  import { useState } from "react";
4
- import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
5
4
 
5
+ import { BundleIdDisplay } from "@/components/BundleIdDisplay";
6
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
7
+ import { Badge } from "@/components/ui/badge";
6
8
  import { Button, buttonVariants } from "@/components/ui/button";
7
9
  import {
8
10
  Card,
@@ -12,432 +14,628 @@ import {
12
14
  CardTitle,
13
15
  } from "@/components/ui/card";
14
16
  import {
15
- ChartContainer,
16
- ChartTooltip,
17
- ChartTooltipContent,
18
- } from "@/components/ui/chart";
19
- import { ScrollArea } from "@/components/ui/scroll-area";
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";
32
+ import {
33
+ Table,
34
+ TableBody,
35
+ TableCell,
36
+ TableHead,
37
+ TableHeader,
38
+ TableRow,
39
+ } from "@/components/ui/table";
21
40
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
22
- import type { RecoveryInput, RecoveryReport } from "@/lib/insights-recovery";
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";
23
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";
26
64
  import { InsightsPeriodSelector } from "./InsightsPeriodSelector";
65
+ import { RollbackButton } from "./RollbackButton";
27
66
 
28
- const dates = new Intl.DateTimeFormat("en", {
29
- month: "short",
30
- day: "numeric",
31
- timeZone: "UTC",
32
- });
33
- const times = new Intl.DateTimeFormat("en", {
34
- month: "short",
35
- day: "numeric",
36
- hour: "numeric",
37
- timeZone: "UTC",
38
- });
39
- const colors = [
40
- "var(--chart-2)",
41
- "var(--foreground)",
42
- "var(--chart-1)",
43
- "var(--muted-foreground)",
44
- "var(--chart-4)",
45
- ];
46
- 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`;
47
78
 
48
- export function ActivityChart({ report }: { readonly report: RecoveryReport }) {
49
- const [metric, setMetric] = useState<"active" | "rollback">("active");
50
- const [selectedId, setSelectedId] = useState<string | null>(null);
51
- const selected = report.series.find(
52
- (series) => series.releaseId === selectedId,
53
- );
54
- const config = Object.fromEntries(
55
- report.series.map((series, index) => [
56
- `bundle${index}`,
57
- {
58
- label: series.releaseId,
59
- color: colors[index % colors.length],
60
- },
61
- ]),
62
- );
63
- const data = (report.series[0]?.points ?? []).map((point, pointIndex) => ({
64
- startMs: point.startMs,
65
- ...Object.fromEntries(
66
- report.series.map((series, index) => [
67
- `bundle${index}`,
68
- metric === "active"
69
- ? series.points[pointIndex].active
70
- : series.points[pointIndex].recoveredInstallations,
71
- ]),
72
- ),
73
- }));
74
- const formatTick = (ms: number) =>
75
- (report.intervalMs < 86_400_000 ? times : dates).format(ms);
76
- const lastSpike = selected
77
- ? [...selected.points].reverse().find((point) => point.spike)
78
- : undefined;
79
- const total =
80
- metric === "active"
81
- ? report.series.reduce(
82
- (sum, series) => sum + series.activeInstallations,
83
- 0,
84
- )
85
- : report.series.filter((series) => series.recoveredInstallations > 0)
86
- .length;
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 }) {
87
95
  return (
88
- <Tabs
89
- value={metric}
90
- onValueChange={(value) => {
91
- if (value === "active" || value === "rollback") setMetric(value);
92
- }}
93
- className="gap-6"
96
+ <svg
97
+ aria-hidden="true"
98
+ className="h-3 w-6 shrink-0"
99
+ viewBox="0 0 24 12"
100
+ preserveAspectRatio="none"
94
101
  >
95
- <TabsList
96
- aria-label="Activity metric"
97
- variant="line"
98
- className="min-h-11 sm:min-h-9"
99
- >
100
- <TabsTrigger className="min-w-24 px-4" value="active">
101
- Active
102
- </TabsTrigger>
103
- <TabsTrigger className="min-w-24 px-4" value="rollback">
104
- Rollback
105
- </TabsTrigger>
106
- </TabsList>
107
- <TabsContent value={metric} className="flex flex-col gap-4">
108
- <div className="flex items-baseline gap-3">
109
- <p className="text-4xl font-semibold tracking-tight tabular-nums">
110
- {total.toLocaleString()}
111
- </p>
112
- <p className="text-sm text-muted-foreground">
113
- {metric === "active"
114
- ? "Active installations"
115
- : "Bundles with rollbacks"}
116
- </p>
117
- </div>
118
- {report.series.length === 0 ? (
119
- <div className="flex h-48 items-center justify-center text-center text-sm text-muted-foreground">
120
- No bundle reports in this period. Try another channel or check again
121
- after the app reports activity.
122
- </div>
123
- ) : (
124
- <>
125
- <div>
126
- <p className="mb-2 text-right text-xs text-muted-foreground">
127
- {metric === "active"
128
- ? "Installations"
129
- : "Recovered installations per interval"}{" "}
130
- · UTC
131
- </p>
132
- <ChartContainer
133
- aria-label={`${metric === "active" ? "Active" : "Rollback"} trend for all reported bundle IDs`}
134
- className="h-56 w-full aspect-auto sm:h-64"
135
- config={config}
136
- >
137
- <LineChart
138
- accessibilityLayer
139
- data={data}
140
- margin={{ left: -12, right: 12, top: 8 }}
141
- >
142
- <CartesianGrid vertical={false} />
143
- <XAxis
144
- tick={{ fill: "var(--muted-foreground)" }}
145
- axisLine={false}
146
- dataKey="startMs"
147
- minTickGap={40}
148
- tickFormatter={formatTick}
149
- tickLine={false}
150
- />
151
- <YAxis
152
- tick={{ fill: "var(--muted-foreground)" }}
153
- allowDecimals={false}
154
- axisLine={false}
155
- tickLine={false}
156
- width={48}
157
- />
158
- <ChartTooltip
159
- content={
160
- <ChartTooltipContent
161
- className="max-h-64 max-w-[calc(100vw-3rem)] overflow-auto"
162
- labelFormatter={(_, payload) =>
163
- `${times.format(payload[0]?.payload.startMs)} · UTC`
164
- }
165
- />
166
- }
167
- />
168
- {report.series.map((series, index) => (
169
- <Line
170
- key={series.releaseId}
171
- dataKey={`bundle${index}`}
172
- type="linear"
173
- stroke={`var(--color-bundle${index})`}
174
- strokeWidth={selectedId === series.releaseId ? 3 : 2}
175
- strokeOpacity={!selected || selected === series ? 1 : 0.4}
176
- strokeDasharray={
177
- dashes[
178
- Math.floor(index / colors.length) % dashes.length
179
- ]
180
- }
181
- dot={{ r: 2 }}
182
- activeDot={{ r: 4 }}
183
- isAnimationActive={false}
184
- />
185
- ))}
186
- </LineChart>
187
- </ChartContainer>
188
- </div>
189
- <ScrollArea
190
- aria-label="Reported bundles"
191
- className="min-w-0 [&_[data-slot=scroll-area-viewport]]:max-h-48"
192
- >
193
- <div
194
- className={cn(
195
- "grid grid-cols-1 gap-1 pr-3",
196
- report.series.length > 1 && "xl:grid-cols-2",
197
- )}
198
- >
199
- {report.series.map((series, index) => (
200
- <button
201
- type="button"
202
- key={series.releaseId}
203
- aria-label={`Highlight bundle ${series.releaseId}`}
204
- aria-pressed={selectedId === series.releaseId}
205
- title={series.releaseId}
206
- onClick={() =>
207
- setSelectedId(
208
- selectedId === series.releaseId
209
- ? null
210
- : series.releaseId,
211
- )
212
- }
213
- 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"
214
- >
215
- <svg
216
- aria-hidden="true"
217
- width="20"
218
- height="8"
219
- className="shrink-0"
220
- >
221
- <line
222
- x1="0"
223
- x2="20"
224
- y1="4"
225
- y2="4"
226
- stroke={colors[index % colors.length]}
227
- strokeWidth="2"
228
- strokeDasharray={
229
- dashes[
230
- Math.floor(index / colors.length) % dashes.length
231
- ]
232
- }
233
- />
234
- </svg>
235
- <span className="min-w-0 truncate font-mono">
236
- {series.releaseId}
237
- </span>
238
- {series.points.some((point) => point.spike) ? (
239
- <TriangleAlert
240
- aria-label="Rollback spike"
241
- className="size-3 shrink-0"
242
- />
243
- ) : null}
244
- <span className="ml-auto tabular-nums">
245
- {(metric === "active"
246
- ? series.activeInstallations
247
- : series.recoveredInstallations
248
- ).toLocaleString()}
249
- </span>
250
- </button>
251
- ))}
252
- </div>
253
- </ScrollArea>
254
- {selected ? (
255
- <div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3">
256
- <div className="min-w-0 flex-1">
257
- <p className="break-all font-mono text-xs">
258
- {selected.releaseId}
259
- </p>
260
- <p className="mt-1 text-xs text-muted-foreground">
261
- Active {selected.activeInstallations.toLocaleString()} ·
262
- Rollback {selected.recoveredInstallations.toLocaleString()}{" "}
263
- in this period
264
- </p>
265
- {metric === "rollback" && lastSpike ? (
266
- <p className="mt-2 flex items-center gap-2 text-xs">
267
- <TriangleAlert
268
- className="size-3 shrink-0"
269
- aria-hidden="true"
270
- />
271
- Rollback spike on {dates.format(lastSpike.startMs)}.
272
- Review this bundle’s delivery settings.
273
- </p>
274
- ) : null}
275
- </div>
276
- <Link
277
- className={buttonVariants({ variant: "outline", size: "sm" })}
278
- to="/"
279
- search={{ releaseId: selected.releaseId }}
280
- >
281
- Review bundle
282
- <ArrowUpRight aria-hidden="true" />
283
- </Link>
284
- </div>
285
- ) : null}
286
- <details className="text-xs text-muted-foreground">
287
- <summary className="cursor-pointer">View chart data</summary>
288
- <div className="mt-2 max-h-64 overflow-auto">
289
- <table className="w-full text-left">
290
- <caption className="sr-only">
291
- {metric === "active"
292
- ? "Active installations"
293
- : "Recovered installations"}{" "}
294
- by bundle ID, UTC
295
- </caption>
296
- <thead>
297
- <tr>
298
- <th scope="col" className="pr-4">
299
- Date (UTC)
300
- </th>
301
- {report.series.map((series) => (
302
- <th
303
- scope="col"
304
- className="px-2 font-mono"
305
- key={series.releaseId}
306
- >
307
- {series.releaseId}
308
- </th>
309
- ))}
310
- </tr>
311
- </thead>
312
- <tbody>
313
- {data.map((point, i) => (
314
- <tr key={point.startMs}>
315
- <th
316
- scope="row"
317
- className="whitespace-nowrap pr-4 font-normal"
318
- >
319
- {times.format(point.startMs)}
320
- </th>
321
- {report.series.map((series) => (
322
- <td
323
- className="px-2 tabular-nums"
324
- key={series.releaseId}
325
- >
326
- {(metric === "active"
327
- ? series.points[i].active
328
- : series.points[i].recoveredInstallations) ?? "—"}
329
- </td>
330
- ))}
331
- </tr>
332
- ))}
333
- </tbody>
334
- </table>
335
- </div>
336
- </details>
337
- </>
338
- )}
339
- </TabsContent>
340
- </Tabs>
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>
341
112
  );
342
113
  }
343
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
+ */
344
122
  export function InsightsOverview({
345
123
  input,
124
+ chart,
125
+ onChartChange,
126
+ health,
127
+ onReleasesChange,
346
128
  onWindowChange,
347
- query,
348
- onRefresh,
349
129
  }: {
350
130
  readonly input: RecoveryInput;
351
- readonly query: {
352
- readonly data: RecoveryReport | undefined;
353
- readonly error: Error | null;
354
- readonly isPending: boolean;
355
- readonly isFetching: boolean;
356
- };
357
- readonly onRefresh: () => void;
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;
358
136
  readonly onWindowChange: (window: RecoveryInput["window"]) => void;
359
137
  }) {
360
- return (
361
- <Card
362
- aria-label="Bundle activity"
363
- className="min-w-0 overflow-hidden shadow-sm"
364
- role="region"
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),
144
+ );
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)}
365
157
  >
366
- <CardHeader className="flex-row flex-wrap items-center justify-between gap-4 p-6">
367
- <CardTitle>Bundle activity</CardTitle>
368
- <div className="flex w-full items-center justify-between gap-4 sm:w-auto">
369
- <InsightsPeriodSelector
370
- window={input.window}
371
- onWindowChange={onWindowChange}
372
- />
373
- <Button
374
- aria-label="Refresh bundle activity"
375
- className="size-11 sm:size-9"
376
- variant="ghost"
377
- size="icon-lg"
378
- disabled={query.isFetching}
379
- onClick={onRefresh}
380
- >
381
- <RotateCw aria-hidden="true" />
382
- </Button>
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>
383
193
  </div>
384
- </CardHeader>
385
- <CardContent className="px-6 pb-6">
386
- {query.isPending ? (
387
- <Skeleton
388
- aria-label="Loading bundle activity"
389
- className="h-80 w-full"
390
- />
391
- ) : query.error ? (
392
- <InsightsErrorAlert
393
- error={query.error}
394
- fallbackTitle="Bundle activity unavailable"
395
- />
396
- ) : query.data ? (
397
- <ActivityChart key={JSON.stringify(input)} report={query.data} />
398
- ) : null}
399
- </CardContent>
400
- <CardFooter className="flex-wrap items-center justify-between gap-4 border-t px-6 py-5">
401
- <div className="flex min-w-0 basis-64 flex-1 flex-col gap-2 text-xs text-muted-foreground">
402
- {query.data && !query.error ? (
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
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>
209
+ </div>
210
+ </div>
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>
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
+ />
261
+ ) : (
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)}
291
+ >
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}
354
+ >
355
+ <RotateCw
356
+ aria-hidden="true"
357
+ className={cn(
358
+ health.isFetching && "motion-safe:animate-spin",
359
+ )}
360
+ />
361
+ </Button>
362
+ </div>
363
+ </div>
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"
378
+ >
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}
396
+ </div>
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 ? (
403
431
  <>
404
- <p>
405
- Active follows each installation’s last reported ID in this
406
- period. Rollbacks count recovered installations per interval.
407
- </p>
408
- {query.data.unattributedInstallations > 0 ? (
409
- <p>
410
- {query.data.unattributedInstallations.toLocaleString()}{" "}
411
- reporting installations have no observed bundle ID and are
412
- excluded.
413
- </p>
432
+ {failed && loaded.length > 0 ? (
433
+ <InsightsErrorAlert
434
+ error={failed}
435
+ fallbackTitle="Some bundles could not be read"
436
+ />
414
437
  ) : null}
415
- {query.data.truncated ? (
416
- <p>
417
- Partial history — only reports since{" "}
418
- {times.format(query.data.sinceMs)} UTC are included. Choose a
419
- shorter period to see more complete counts.
420
- </p>
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)}
472
+ >
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}
612
+ >
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>
621
+ ))}
622
+ </SelectGroup>
623
+ </SelectContent>
624
+ </Select>
421
625
  ) : null}
422
626
  </>
423
- ) : (
424
- <p>Browse individual reports in the event log.</p>
425
- )}
426
- </div>
427
- <Link
428
- className={cn(
429
- buttonVariants({
430
- className: "h-11 px-4 sm:h-9",
431
- size: "lg",
432
- variant: "outline",
433
- }),
434
- )}
435
- to="/installations"
436
- >
437
- <ListIcon aria-hidden="true" data-icon="inline-start" />
438
- All events
439
- </Link>
440
- </CardFooter>
441
- </Card>
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>
637
+ </CardFooter>
638
+ </Card>
639
+ </section>
442
640
  );
443
641
  }