@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.21

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 (285) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-D0LPL1Gy.js +1 -0
  3. package/.output/public/assets/HashValueDisplay-BmuB4FW-.js +1 -0
  4. package/.output/public/assets/InsightsInfo--v4SC07c.js +1 -0
  5. package/.output/public/assets/InsightsPageHeader-dU83CAQY.js +1 -0
  6. package/.output/public/assets/NotFoundPage-B5mhGG2a.js +1 -0
  7. package/.output/public/assets/api-D-vOVlQ4.js +1 -0
  8. package/.output/public/assets/api-keys-Csh8ARKU.js +1 -0
  9. package/.output/public/assets/api-keys-OMPUoA_A.js +1 -0
  10. package/.output/public/assets/arrow-left-Bwq9X1eS.js +1 -0
  11. package/.output/public/assets/arrow-up-right-BkOVn4if.js +1 -0
  12. package/.output/public/assets/card-C_7d6O9a.js +1 -0
  13. package/.output/public/assets/console-features-api-C1jRZTGw.js +1 -0
  14. package/.output/public/assets/dialog-DE8wFN_P.js +1 -0
  15. package/.output/public/assets/dist-C7ENGzpS.js +1 -0
  16. package/.output/public/assets/empty-CVd5NlQD.js +1 -0
  17. package/.output/public/assets/field-C06q1iC9.js +1 -0
  18. package/.output/public/assets/index-BeWlQtO6.js +11 -0
  19. package/.output/public/assets/input-group-Bi61biIA.js +1 -0
  20. package/.output/public/assets/insights-BjnAtC0f.js +2 -0
  21. package/.output/public/assets/insights-D86m11yH.js +42 -0
  22. package/.output/public/assets/insights-DpYstMoY.js +1 -0
  23. package/.output/public/assets/insights-recovery-rpc-BIm5_ALc.js +1 -0
  24. package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
  25. package/.output/public/assets/insights-usage-rpc-VHj8vyBt.js +1 -0
  26. package/.output/public/assets/insights_.distribution-ADcrX48c.js +2 -0
  27. package/.output/public/assets/insights_.distribution-CuuJUN6Y.js +1 -0
  28. package/.output/public/assets/insights_.distribution-DJRjuXsO.js +1 -0
  29. package/.output/public/assets/installations-BSBBzqWs.js +2 -0
  30. package/.output/public/assets/installations-DqIE6WcO.js +1 -0
  31. package/.output/public/assets/installations-UsELbDvw.js +1 -0
  32. package/.output/public/assets/loader-circle-CnCBxzHp.js +1 -0
  33. package/.output/public/assets/mutation-Csp7xUNh.js +1 -0
  34. package/.output/public/assets/preload-helper-B-cOmpLH.js +1 -0
  35. package/.output/public/assets/refresh-cw-FF1NtzlQ.js +1 -0
  36. package/.output/public/assets/routes-CoN9XHP_.js +2 -0
  37. package/.output/public/assets/routes-DT1jB0Zi.js +1 -0
  38. package/.output/public/assets/sidebar-D8mJRF15.js +3 -0
  39. package/.output/public/assets/styles-Bwt7v6U5.css +2 -0
  40. package/.output/public/assets/table-WBgfvgXg.js +1 -0
  41. package/.output/public/assets/useCompositeListItem-CloBmDwn.js +1 -0
  42. package/.output/public/assets/useMutation-DO8cvsTx.js +1 -0
  43. package/.output/public/assets/useOpenInteractionType-ReGr9GzJ.js +1 -0
  44. package/.output/public/assets/valueToPercent-BvPAi4bP.js +1 -0
  45. package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-DMRD4e4f.mjs} +51 -47
  46. package/.output/server/_libs/@base-ui/react+[...].mjs +7044 -3622
  47. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  48. package/.output/server/_libs/@tanstack/react-router+[...].mjs +3803 -4023
  49. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +141 -98
  50. package/.output/server/_libs/d3-array.mjs +231 -0
  51. package/.output/server/_libs/d3-color.mjs +330 -0
  52. package/.output/server/_libs/d3-format.mjs +252 -0
  53. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  54. package/.output/server/_libs/d3-path.mjs +77 -0
  55. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  56. package/.output/server/_libs/d3-shape.mjs +818 -0
  57. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  58. package/.output/server/_libs/es-toolkit.mjs +1159 -0
  59. package/.output/server/_libs/eventemitter3.mjs +236 -0
  60. package/.output/server/_libs/immer.mjs +532 -0
  61. package/.output/server/_libs/lucide-react.mjs +46 -32
  62. package/.output/server/_libs/next-themes.mjs +2 -2
  63. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  64. package/.output/server/_libs/recharts+[...].mjs +14350 -0
  65. package/.output/server/_libs/sonner.mjs +2 -2
  66. package/.output/server/_libs/tanstack__history.mjs +68 -41
  67. package/.output/server/_libs/tanstack__query-core.mjs +1618 -309
  68. package/.output/server/_libs/tanstack__react-query.mjs +262 -22
  69. package/.output/server/_libs/verkit.mjs +99 -1
  70. package/.output/server/_runtime.mjs +10 -1
  71. package/.output/server/_ssr/ConsoleFeatureUnavailable-Du7jNPNS.mjs +58 -0
  72. package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-GQvFkXoi.mjs} +3 -3
  73. package/.output/server/_ssr/InsightsInfo-Do7QSaFR.mjs +37 -0
  74. package/.output/server/_ssr/InsightsPageHeader-DSUWUWRc.mjs +154 -0
  75. package/.output/server/_ssr/NotFoundPage-DsF4Vrhg.mjs +37 -0
  76. package/.output/server/_ssr/api-BEiVs0H7.mjs +203 -0
  77. package/.output/server/_ssr/api-keys-BrvBWGRf.mjs +5 -0
  78. package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-K5v32nf7.mjs} +49 -9
  79. package/.output/server/_ssr/api-keys-rpc-B96bBllm.mjs +52 -0
  80. package/.output/server/_ssr/api-rpc-DcUe280B.mjs +296 -0
  81. package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-C68W56DP.mjs} +6 -6
  82. package/.output/server/_ssr/auth.server-DM1J6NwR.mjs +5712 -0
  83. package/.output/server/_ssr/bundleActivity-CpuY9CTP.mjs +19 -0
  84. package/.output/server/_ssr/card-CPAEKx0w.mjs +49 -0
  85. package/.output/server/_ssr/config-nsIcsHh8.mjs +15 -0
  86. package/.output/server/_ssr/config.server-DDCZk3Sq.mjs +36 -0
  87. package/.output/server/_ssr/console-access-Da-2Qd7q.mjs +17 -0
  88. package/.output/server/_ssr/console-features-Dzu2iUGD.mjs +55 -0
  89. package/.output/server/_ssr/console-features-api-xAsSgFa_.mjs +43 -0
  90. package/.output/server/_ssr/console-features-rpc-qARGpB24.mjs +19 -0
  91. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-BdBf_ezN.mjs} +67 -56
  92. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-B_y2lrwA.mjs} +2 -2
  93. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Bhxciy7Z.mjs} +28 -5
  94. package/.output/server/_ssr/databaseRows-DwMAjaF8.mjs +74 -0
  95. package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-q__BJ7mQ.mjs} +2 -2
  96. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-Cxo9ghYv.mjs} +140 -6
  97. package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-BW9I-pMQ.mjs} +10 -10
  98. package/.output/server/_ssr/empty-DcBTAooP.mjs +49 -0
  99. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CYdinE-i.mjs} +1 -1
  100. package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-QCDBVpXA.mjs} +63 -51
  101. package/.output/server/_ssr/getBundleChildren-DIAGCF4a.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-hCk2nUoU.mjs} +2 -2
  103. package/.output/server/_ssr/insights-BfBO0RFr.mjs +1238 -0
  104. package/.output/server/_ssr/insights-DsAU3-TB.mjs +14 -0
  105. package/.output/server/_ssr/insights-RIfg447W.mjs +5 -0
  106. package/.output/server/_ssr/insights-api-CDn-QQHU.mjs +105 -0
  107. package/.output/server/_ssr/insights-failures-BBGMhuUr.mjs +44 -0
  108. package/.output/server/_ssr/insights-failures-rpc-rA2dctK2.mjs +21 -0
  109. package/.output/server/_ssr/insights-recovery-Dpk6Mfb9.mjs +28 -0
  110. package/.output/server/_ssr/insights-recovery-rpc-BCaF-xSd.mjs +46 -0
  111. package/.output/server/_ssr/insights-recovery-rpc-CcgYyUHk.mjs +20 -0
  112. package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-keycmwzw.mjs} +22 -14
  113. package/.output/server/_ssr/insights-search-BRg5VsNe.mjs +25 -0
  114. package/.output/server/_ssr/insights-usage-cbw9n2Fq.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-BKgV1QDf.mjs +29 -0
  116. package/.output/server/_ssr/insights-usage-rpc-D-JYachy.mjs +41 -0
  117. package/.output/server/_ssr/insightsRecovery-B7oMc8d8.mjs +45 -0
  118. package/.output/server/_ssr/insightsUsage-Oa7kk5ek.mjs +56 -0
  119. package/.output/server/_ssr/insights_.distribution-BIqOqRW0.mjs +335 -0
  120. package/.output/server/_ssr/insights_.distribution-CuUrDa7P.mjs +5 -0
  121. package/.output/server/_ssr/insights_.distribution-RT_6x_ud.mjs +14 -0
  122. package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-BO5CjX2z.mjs} +517 -54
  123. package/.output/server/_ssr/installations-BjaHiIfh.mjs +5 -0
  124. package/.output/server/_ssr/installations-Dt5luA-N.mjs +69 -0
  125. package/.output/server/_ssr/listReleases-BitHzOYB.mjs +79 -0
  126. package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
  127. package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-DfVAixxe.mjs} +135 -139
  128. package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-C6LstNp8.mjs} +176 -455
  129. package/.output/server/_ssr/routes-Q3Uhmwwz.mjs +292 -0
  130. package/.output/server/_ssr/runtime.server-fL0o6I2b.mjs +1459 -0
  131. package/.output/server/_ssr/schema-BKY9SqaY.mjs +25 -0
  132. package/.output/server/_ssr/select-CUlInmjk.mjs +107 -0
  133. package/.output/server/_ssr/server-C43wq9ft.mjs +8330 -0
  134. package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-DKbx2ach.mjs} +117 -63
  135. package/.output/server/_ssr/ssr.mjs +1 -1
  136. package/.output/server/_ssr/start-BXUOv_n8.mjs +4 -0
  137. package/.output/server/_ssr/table-NGWV8B-S.mjs +53 -0
  138. package/.output/server/_ssr/updateFailures-Jwe2Baab.mjs +24 -0
  139. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C8bwiZr8.mjs} +2 -2
  140. package/.output/server/_tanstack-start-manifest_v-WQ5qGn8n.mjs +124 -0
  141. package/.output/server/index.mjs +267 -162
  142. package/LICENSE +13 -14
  143. package/README.md +82 -72
  144. package/lib/index.d.mts +23 -11
  145. package/lib/vite-internal.mjs +13 -3
  146. package/lib/vite.d.mts +2 -0
  147. package/package.json +14 -14
  148. package/src/components/AppSidebar.tsx +100 -71
  149. package/src/components/ConsoleAccessPage.tsx +5 -1
  150. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  151. package/src/components/NotFoundPage.tsx +7 -7
  152. package/src/components/features/FeatureSlots.tsx +99 -0
  153. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  154. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  155. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  156. package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
  157. package/src/components/features/insights/AppUsage.tsx +370 -0
  158. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  159. package/src/components/features/insights/EventDetails.tsx +117 -37
  160. package/src/components/features/insights/EventHistoryCard.tsx +112 -26
  161. package/src/components/features/insights/EventHistoryList.tsx +2 -2
  162. package/src/components/features/insights/InsightsControls.tsx +265 -102
  163. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  164. package/src/components/features/insights/InsightsInfo.tsx +48 -0
  165. package/src/components/features/insights/InsightsOverview.tsx +176 -173
  166. package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
  167. package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
  168. package/src/components/features/insights/InstallationHistoryCard.tsx +52 -10
  169. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  170. package/src/components/features/insights/ReleaseActivity.tsx +188 -0
  171. package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
  172. package/src/components/features/insights/UpdateFailures.tsx +319 -0
  173. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  174. package/src/components/features/releases/ReleaseEditorSheet.tsx +30 -7
  175. package/src/components/ui/card.tsx +1 -1
  176. package/src/components/ui/empty.tsx +101 -0
  177. package/src/components/ui/scroll-area.tsx +52 -0
  178. package/src/components/ui/sidebar.tsx +22 -29
  179. package/src/components/ui/tabs.tsx +81 -0
  180. package/src/index.ts +26 -19
  181. package/src/lib/api-keys-api.ts +0 -28
  182. package/src/lib/api-keys-rpc.ts +27 -38
  183. package/src/lib/api-rpc.ts +189 -266
  184. package/src/lib/api.ts +19 -102
  185. package/src/lib/bundle-activity.ts +31 -0
  186. package/src/lib/console-access.ts +20 -0
  187. package/src/lib/console-features-api.ts +68 -0
  188. package/src/lib/console-features-rpc.ts +13 -0
  189. package/src/lib/console-features.ts +91 -0
  190. package/src/lib/insights-api.ts +37 -1
  191. package/src/lib/insights-failures-rpc.ts +21 -0
  192. package/src/lib/insights-failures.ts +129 -0
  193. package/src/lib/insights-recovery-rpc.ts +51 -0
  194. package/src/lib/insights-recovery.ts +68 -0
  195. package/src/lib/insights-retention.ts +21 -0
  196. package/src/lib/insights-rpc.ts +30 -12
  197. package/src/lib/insights-search.ts +58 -0
  198. package/src/lib/insights-usage-rpc.ts +29 -0
  199. package/src/lib/insights-usage.ts +74 -0
  200. package/src/lib/insights-view.ts +38 -2
  201. package/src/lib/scroll-restoration.ts +21 -0
  202. package/src/lib/server/adminInsights.ts +124 -0
  203. package/src/lib/server/auth.server.ts +13 -5
  204. package/src/lib/server/bundleActivity.ts +37 -0
  205. package/src/lib/server/config.server.ts +21 -74
  206. package/src/lib/server/console-feature-apis.ts +59 -0
  207. package/src/lib/server/console-runtime.server.ts +82 -38
  208. package/src/lib/server/downloadBundle.ts +16 -15
  209. package/src/lib/server/getBundleChildren.ts +31 -122
  210. package/src/lib/server/insightsRecovery.ts +55 -0
  211. package/src/lib/server/insightsUsage.ts +63 -0
  212. package/src/lib/server/listReleases.ts +109 -44
  213. package/src/lib/server/releaseReachability.ts +8 -7
  214. package/src/lib/server/runtime.server.ts +120 -31
  215. package/src/lib/server/updateFailures.ts +31 -0
  216. package/src/routeTree.gen.ts +44 -44
  217. package/src/router.tsx +3 -2
  218. package/src/routes/-installations-search.ts +32 -8
  219. package/src/routes/-releases-search.ts +76 -29
  220. package/src/routes/__root.tsx +10 -4
  221. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  222. package/src/routes/api-keys.tsx +5 -2
  223. package/src/routes/index.tsx +167 -104
  224. package/src/routes/insights.tsx +100 -59
  225. package/src/routes/insights_.distribution.tsx +398 -0
  226. package/src/routes/installations.tsx +98 -41
  227. package/src/vite-internal.ts +15 -3
  228. package/src/vite.ts +2 -0
  229. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
  230. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
  231. package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
  232. package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
  233. package/.output/public/assets/card-Dhr_eFYj.js +0 -1
  234. package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
  235. package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
  236. package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
  237. package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
  238. package/.output/public/assets/dist-C_NydL0e.js +0 -1
  239. package/.output/public/assets/index-DdAz-CXv.js +0 -10
  240. package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
  241. package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
  242. package/.output/public/assets/installations-CE33wgbY.js +0 -2
  243. package/.output/public/assets/installations-CStZja8x.js +0 -1
  244. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
  245. package/.output/public/assets/link-CI8idAUi.js +0 -1
  246. package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
  247. package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
  248. package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
  249. package/.output/public/assets/routes-DGVXdzpw.js +0 -2
  250. package/.output/public/assets/routes-DTlwOr8I.js +0 -1
  251. package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
  252. package/.output/public/assets/signing-CMf4aqDO.js +0 -1
  253. package/.output/public/assets/styles-BCvbtKsm.css +0 -2
  254. package/.output/public/assets/table-2vcV89QB.js +0 -1
  255. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
  256. package/.output/public/assets/x-DCd0j8c6.js +0 -1
  257. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
  258. package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
  259. package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
  260. package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
  261. package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
  262. package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
  263. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
  264. package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
  265. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  266. package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
  267. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  268. package/.output/server/_ssr/deleteBundle-BTGwBgzx.mjs +0 -66
  269. package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
  270. package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
  271. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
  272. package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
  273. package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
  274. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
  275. package/.output/server/_ssr/server-D_C2rMzf.mjs +0 -7468
  276. package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
  277. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  278. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
  279. package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
  280. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  281. package/src/lib/bundle-signing-api.ts +0 -14
  282. package/src/lib/bundle-signing-rpc.ts +0 -16
  283. package/src/lib/server/bundle-signing.server.ts +0 -23
  284. package/src/lib/server/deleteBundle.ts +0 -145
  285. package/src/routes/signing.tsx +0 -37
@@ -0,0 +1,319 @@
1
+ import { ChevronDown, RotateCw } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardFooter,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Skeleton } from "@/components/ui/skeleton";
14
+ import {
15
+ Table,
16
+ TableBody,
17
+ TableCell,
18
+ TableHead,
19
+ TableHeader,
20
+ TableRow,
21
+ } from "@/components/ui/table";
22
+ import {
23
+ checkFailureRate,
24
+ failureDetailParts,
25
+ failureRate,
26
+ failureReasonLabel,
27
+ failureStageLabel,
28
+ formatRate,
29
+ type InsightsFailureBreakdown,
30
+ patchFallbackRate,
31
+ type UpdateFailuresReport,
32
+ } from "@/lib/insights-failures";
33
+
34
+ import { EstimatedCount } from "./EstimatedCount";
35
+ import { InsightsErrorAlert } from "./InsightsErrorAlert";
36
+ import { InsightsInfo } from "./InsightsInfo";
37
+
38
+ const share = (events: number, total: number) =>
39
+ total === 0 ? "—" : `${((events / total) * 100).toFixed(1)}%`;
40
+
41
+ function Metric({
42
+ label,
43
+ info,
44
+ children,
45
+ }: {
46
+ readonly label: string;
47
+ readonly info?: ReactNode;
48
+ readonly children: ReactNode;
49
+ }) {
50
+ return (
51
+ <div className="flex flex-col gap-1">
52
+ <dt className="flex items-center gap-1 text-sm text-muted-foreground">
53
+ {label}
54
+ {info ? (
55
+ <InsightsInfo label={`About ${label.toLowerCase()}`}>
56
+ {info}
57
+ </InsightsInfo>
58
+ ) : null}
59
+ </dt>
60
+ <dd className="text-3xl font-semibold tracking-tight tabular-nums">
61
+ {children}
62
+ </dd>
63
+ </div>
64
+ );
65
+ }
66
+
67
+ /** One stage and reason, and what else its clients knew, most first. */
68
+ function FailureRow({
69
+ failure,
70
+ total,
71
+ }: {
72
+ readonly failure: InsightsFailureBreakdown;
73
+ readonly total: number;
74
+ }) {
75
+ const label = `${failureStageLabel(failure.stage)} · ${failureReasonLabel(failure.reason)}`;
76
+ return (
77
+ <TableRow className="[&>td]:px-4 [&>td]:py-3 [&>td]:align-top sm:[&>td]:px-6">
78
+ <TableCell className="whitespace-normal">
79
+ <details className="group/failure">
80
+ <summary className="flex cursor-pointer list-none items-center gap-1 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring/30 [&::-webkit-details-marker]:hidden">
81
+ <span className="font-medium">{label}</span>
82
+ <ChevronDown
83
+ aria-hidden="true"
84
+ className="size-3 text-muted-foreground group-open/failure:rotate-180"
85
+ />
86
+ </summary>
87
+ <ul
88
+ aria-label={`${label} details`}
89
+ className="mt-2 flex flex-col gap-1 text-xs text-muted-foreground"
90
+ >
91
+ {failure.details.map((detail) => {
92
+ const parts = failureDetailParts(detail);
93
+ return (
94
+ <li
95
+ key={JSON.stringify(detail)}
96
+ className="flex justify-between gap-4"
97
+ >
98
+ <span className="wrap-anywhere">
99
+ {parts.length === 0
100
+ ? "No details reported"
101
+ : parts.join(" · ")}
102
+ </span>
103
+ <span className="tabular-nums">
104
+ {detail.events.toLocaleString()}
105
+ </span>
106
+ </li>
107
+ );
108
+ })}
109
+ </ul>
110
+ </details>
111
+ </TableCell>
112
+ <TableCell className="text-right tabular-nums">
113
+ {failure.events.toLocaleString()}
114
+ </TableCell>
115
+ <TableCell className="text-right tabular-nums text-muted-foreground">
116
+ {share(failure.events, total)}
117
+ </TableCell>
118
+ </TableRow>
119
+ );
120
+ }
121
+
122
+ function Recoveries({
123
+ recoveries,
124
+ }: {
125
+ readonly recoveries: NonNullable<UpdateFailuresReport["recoveries"]>;
126
+ }) {
127
+ const known = recoveries.byExitReason.reduce(
128
+ (sum, { events }) => sum + events,
129
+ 0,
130
+ );
131
+ const unreported = Math.max(0, recoveries.failedLaunches - known);
132
+ const reasons = [
133
+ ...recoveries.byExitReason.map(({ exitReason, events }) => ({
134
+ name: exitReason,
135
+ code: true,
136
+ events,
137
+ })),
138
+ ...(unreported > 0
139
+ ? [{ name: "Not reported", code: false, events: unreported }]
140
+ : []),
141
+ ];
142
+ return (
143
+ <section
144
+ aria-labelledby="recoveries-by-exit-reason"
145
+ className="flex flex-col gap-2"
146
+ >
147
+ <h3
148
+ className="flex items-center gap-1 text-sm font-medium"
149
+ id="recoveries-by-exit-reason"
150
+ >
151
+ Recoveries by exit reason
152
+ <InsightsInfo label="About exit reasons">
153
+ Why the crashed process exited, as Android 11 and later report it: a
154
+ crash, an ANR, or the system or the user closing the app. iOS and
155
+ older Android report none.
156
+ </InsightsInfo>
157
+ </h3>
158
+ {reasons.length === 0 ? (
159
+ <p className="text-sm text-muted-foreground">
160
+ No recoveries in this period.
161
+ </p>
162
+ ) : (
163
+ <ul className="flex flex-col gap-1 text-sm">
164
+ {reasons.map(({ name, code, events }) => (
165
+ <li key={name} className="flex justify-between gap-4">
166
+ <span className={code ? "font-mono text-xs" : undefined}>
167
+ {name}
168
+ </span>
169
+ <span className="tabular-nums">
170
+ {events.toLocaleString()}
171
+ <span className="ml-2 text-muted-foreground">
172
+ {share(events, recoveries.failedLaunches)}
173
+ </span>
174
+ </span>
175
+ </li>
176
+ ))}
177
+ </ul>
178
+ )}
179
+ </section>
180
+ );
181
+ }
182
+
183
+ function FailuresReport({ report }: { readonly report: UpdateFailuresReport }) {
184
+ const breakdown = report.breakdown ?? [];
185
+ const total = breakdown.reduce((sum, { events }) => sum + events, 0);
186
+ return (
187
+ <div className="flex flex-col gap-8">
188
+ <dl className="grid grid-cols-2 gap-5 lg:grid-cols-4">
189
+ <Metric label="Failed updates">
190
+ {report.failedUpdates.toLocaleString()}
191
+ </Metric>
192
+ <Metric
193
+ label="Attempt failure rate"
194
+ info="The failure rate of update attempts: failed download and install reports divided by those plus download reports."
195
+ >
196
+ {formatRate(failureRate(report))}
197
+ </Metric>
198
+ <Metric
199
+ label="Failed installations"
200
+ info="Distinct installations with a failed download or install, estimated: typically within about 3%. A client reports each failure at most once a UTC day."
201
+ >
202
+ <EstimatedCount value={report.failedInstallations} />
203
+ </Metric>
204
+ <Metric
205
+ label="Patch fallback rate"
206
+ info="Of the downloads that tried a patch, the share that fell back to the changed files or the full archive."
207
+ >
208
+ {formatRate(patchFallbackRate(report))}
209
+ </Metric>
210
+ {report.checks ? (
211
+ <>
212
+ <Metric label="Failed checks">
213
+ {report.checks.failures.toLocaleString()}
214
+ </Metric>
215
+ <Metric
216
+ label="Check failure rate"
217
+ info="Installations whose update check failed, divided by the channel's active installations; both estimated. A check the device could not send while offline is not reported."
218
+ >
219
+ {formatRate(checkFailureRate(report.checks))}
220
+ </Metric>
221
+ </>
222
+ ) : null}
223
+ </dl>
224
+ <section
225
+ aria-labelledby="failures-by-stage"
226
+ className="flex flex-col gap-2"
227
+ >
228
+ <h3 className="text-sm font-medium" id="failures-by-stage">
229
+ Failures by stage and reason
230
+ </h3>
231
+ {breakdown.length === 0 ? (
232
+ <p className="text-sm text-muted-foreground">
233
+ No update failures in this period.
234
+ </p>
235
+ ) : (
236
+ <Table>
237
+ <TableHeader>
238
+ <TableRow className="[&>th]:px-4 sm:[&>th]:px-6">
239
+ <TableHead>Stage · reason</TableHead>
240
+ <TableHead className="w-24 text-right">Reports</TableHead>
241
+ <TableHead className="w-24 text-right">Share</TableHead>
242
+ </TableRow>
243
+ </TableHeader>
244
+ <TableBody>
245
+ {breakdown.map((failure) => (
246
+ <FailureRow
247
+ key={`${failure.stage} ${failure.reason}`}
248
+ failure={failure}
249
+ total={total}
250
+ />
251
+ ))}
252
+ </TableBody>
253
+ </Table>
254
+ )}
255
+ </section>
256
+ {report.recoveries ? <Recoveries recoveries={report.recoveries} /> : null}
257
+ </div>
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Update failures for Release health's scope and period: counts, rates, the
263
+ * stage and reason breakdown with what else the clients knew, and why
264
+ * crashed processes exited.
265
+ */
266
+ export function UpdateFailures({
267
+ query,
268
+ onRefresh,
269
+ }: {
270
+ readonly query: {
271
+ readonly data: UpdateFailuresReport | undefined;
272
+ readonly error: Error | null;
273
+ readonly isPending: boolean;
274
+ readonly isFetching: boolean;
275
+ };
276
+ readonly onRefresh: () => void;
277
+ }) {
278
+ return (
279
+ <section aria-label="Update failures">
280
+ <Card className="min-w-0 overflow-hidden shadow-sm">
281
+ <CardHeader className="flex flex-col gap-1.5 p-6">
282
+ <div className="flex flex-wrap items-center justify-between gap-4">
283
+ <CardTitle>Update failures</CardTitle>
284
+ <Button
285
+ aria-label="Refresh update failures"
286
+ variant="ghost"
287
+ size="icon-lg"
288
+ disabled={query.isFetching}
289
+ onClick={onRefresh}
290
+ >
291
+ <RotateCw aria-hidden="true" />
292
+ </Button>
293
+ </div>
294
+ <CardDescription>
295
+ Checks, downloads, and installs that failed, for Release health's
296
+ scope and period.
297
+ </CardDescription>
298
+ </CardHeader>
299
+ <CardContent className="px-6 pb-6">
300
+ {query.isPending ? (
301
+ <Skeleton aria-label="Loading update failures" className="h-64" />
302
+ ) : query.error ? (
303
+ <InsightsErrorAlert
304
+ error={query.error}
305
+ fallbackTitle="Update failures unavailable"
306
+ />
307
+ ) : query.data ? (
308
+ <FailuresReport report={query.data} />
309
+ ) : null}
310
+ </CardContent>
311
+ {query.data?.coverage.kind === "partial" ? (
312
+ <CardFooter className="border-t px-6 py-5">
313
+ <span className="text-xs text-muted-foreground">Partial</span>
314
+ </CardFooter>
315
+ ) : null}
316
+ </Card>
317
+ </section>
318
+ );
319
+ }
@@ -0,0 +1,35 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { ChartNoAxesCombined } from "lucide-react";
3
+
4
+ import type { ConsoleFeatureUiRegistry } from "../FeatureSlots";
5
+ import {
6
+ BundleInsightsSummary,
7
+ ReleaseFailuresSection,
8
+ releaseActivityColumn,
9
+ } from "./ReleaseActivity";
10
+
11
+ /**
12
+ * What the Insights features add: their navigation item, release activity on
13
+ * the Bundles page and in the release editor, and a release's download
14
+ * failures in the release editor, which a self-hosted server serves too.
15
+ */
16
+ export const insightsFeatureUi = {
17
+ insights: {
18
+ navigation: {
19
+ icon: <ChartNoAxesCombined />,
20
+ paths: ["/insights", "/insights/distribution", "/installations"],
21
+ // A self-hosted server's admin API serves events, not the overview.
22
+ link: ({ features, onClick }) =>
23
+ features.insightsAnalytics ? (
24
+ <Link to="/insights" onClick={onClick} />
25
+ ) : (
26
+ <Link to="/installations" onClick={onClick} />
27
+ ),
28
+ },
29
+ releaseSection: ({ release }) => <ReleaseFailuresSection input={release} />,
30
+ },
31
+ insightsAnalytics: {
32
+ releaseColumn: releaseActivityColumn,
33
+ releaseSection: ({ release }) => <BundleInsightsSummary input={release} />,
34
+ },
35
+ } satisfies ConsoleFeatureUiRegistry;
@@ -1,14 +1,14 @@
1
- import {
2
- getPatchBaseBundleId,
3
- INVALID_COHORT_ERROR_MESSAGE,
4
- isValidCohort,
5
- normalizeCohortValue,
6
- } from "@hot-updater/core";
7
1
  import type {
8
2
  ChannelRow,
9
3
  ReleasePolicyPatch,
10
4
  ReleaseRow,
11
5
  } from "@hot-updater/plugin-core";
6
+ import {
7
+ getPatchBaseBundleId,
8
+ INVALID_COHORT_ERROR_MESSAGE,
9
+ isValidCohort,
10
+ normalizeCohortValue,
11
+ } from "@hot-updater/protocol";
12
12
  import { AlertTriangle, Download, Plus, X } from "lucide-react";
13
13
  import { useEffect, useState } from "react";
14
14
  import { toast } from "sonner";
@@ -17,6 +17,7 @@ import { normalizeRange } from "verkit";
17
17
  import { BundleIdDisplay } from "@/components/BundleIdDisplay";
18
18
  import { BundleMetadata } from "@/components/features/bundles/BundleMetadata";
19
19
  import { RolloutCohortsDialog } from "@/components/features/bundles/RolloutCohortsDialog";
20
+ import { ReleaseSections } from "@/components/features/FeatureSlots";
20
21
  import { PlatformIcon } from "@/components/PlatformIcon";
21
22
  import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
22
23
  import {
@@ -203,11 +204,14 @@ function RolloutPercentageInput({
203
204
  export function ReleaseEditorSheet({
204
205
  channels,
205
206
  onOpenChange,
207
+ onShowScope,
206
208
  open,
207
209
  releaseId,
208
210
  }: {
209
211
  channels: readonly ChannelRow[];
210
212
  onOpenChange: (open: boolean) => void;
213
+ /** Lists the bundles of this release's catalog scope: its channel, platform, and target kind. */
214
+ onShowScope?: (scopeKey: string) => void;
211
215
  open: boolean;
212
216
  releaseId: string;
213
217
  }) {
@@ -408,6 +412,15 @@ export function ReleaseEditorSheet({
408
412
 
409
413
  {release && draft ? (
410
414
  <div className="flex flex-col gap-6 px-4 pb-4 sm:px-6 sm:pb-6">
415
+ {channelName ? (
416
+ <ReleaseSections
417
+ release={{
418
+ platform: release.platform,
419
+ channel: channelName,
420
+ releaseId: release.id,
421
+ }}
422
+ />
423
+ ) : null}
411
424
  <section
412
425
  aria-labelledby="delivery-settings-heading"
413
426
  className="flex flex-col gap-4"
@@ -614,7 +627,7 @@ export function ReleaseEditorSheet({
614
627
  variant="outline"
615
628
  >
616
629
  <Download data-icon="inline-start" />
617
- Download bundle
630
+ Download manifest
618
631
  </Button>
619
632
  ) : null}
620
633
  <Button
@@ -688,6 +701,16 @@ export function ReleaseEditorSheet({
688
701
  <dd className="mt-1 break-all font-mono text-foreground">
689
702
  {release.scope_key}
690
703
  </dd>
704
+ {onShowScope ? (
705
+ <Button
706
+ className="mt-1 h-auto px-0 text-xs"
707
+ onClick={() => onShowScope(release.scope_key)}
708
+ size="sm"
709
+ variant="link"
710
+ >
711
+ Show bundles for this target
712
+ </Button>
713
+ ) : null}
691
714
  </div>
692
715
  </dl>
693
716
  {diagnostics.isError ? (
@@ -19,7 +19,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
19
  return (
20
20
  <div
21
21
  data-slot="card-header"
22
- className={cn("flex flex-col space-y-1.5 p-6", className)}
22
+ className={cn("flex flex-col gap-1.5 p-6", className)}
23
23
  {...props}
24
24
  />
25
25
  );
@@ -0,0 +1,101 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Empty({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="empty"
9
+ className={cn(
10
+ "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="empty-header"
22
+ className={cn("flex max-w-sm flex-col items-center gap-1", className)}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ const emptyMediaVariants = cva(
29
+ "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
30
+ {
31
+ variants: {
32
+ variant: {
33
+ default: "bg-transparent",
34
+ icon: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "default",
39
+ },
40
+ },
41
+ );
42
+
43
+ function EmptyMedia({
44
+ className,
45
+ variant = "default",
46
+ ...props
47
+ }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
48
+ return (
49
+ <div
50
+ data-slot="empty-icon"
51
+ data-variant={variant}
52
+ className={cn(emptyMediaVariants({ variant, className }))}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="empty-title"
62
+ className={cn("text-sm font-medium tracking-tight", className)}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
69
+ return (
70
+ <div
71
+ data-slot="empty-description"
72
+ className={cn(
73
+ "text-xs/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ );
79
+ }
80
+
81
+ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
82
+ return (
83
+ <div
84
+ data-slot="empty-content"
85
+ className={cn(
86
+ "flex w-full max-w-sm min-w-0 flex-col items-center gap-2 text-xs/relaxed text-balance",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ export {
95
+ Empty,
96
+ EmptyHeader,
97
+ EmptyTitle,
98
+ EmptyDescription,
99
+ EmptyContent,
100
+ EmptyMedia,
101
+ };
@@ -0,0 +1,52 @@
1
+ import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function ScrollArea({
6
+ className,
7
+ children,
8
+ ...props
9
+ }: ScrollAreaPrimitive.Root.Props) {
10
+ return (
11
+ <ScrollAreaPrimitive.Root
12
+ data-slot="scroll-area"
13
+ className={cn("relative", className)}
14
+ {...props}
15
+ >
16
+ <ScrollAreaPrimitive.Viewport
17
+ data-slot="scroll-area-viewport"
18
+ className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
19
+ >
20
+ {children}
21
+ </ScrollAreaPrimitive.Viewport>
22
+ <ScrollBar />
23
+ <ScrollAreaPrimitive.Corner />
24
+ </ScrollAreaPrimitive.Root>
25
+ );
26
+ }
27
+
28
+ function ScrollBar({
29
+ className,
30
+ orientation = "vertical",
31
+ ...props
32
+ }: ScrollAreaPrimitive.Scrollbar.Props) {
33
+ return (
34
+ <ScrollAreaPrimitive.Scrollbar
35
+ data-slot="scroll-area-scrollbar"
36
+ data-orientation={orientation}
37
+ orientation={orientation}
38
+ className={cn(
39
+ "flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent",
40
+ className,
41
+ )}
42
+ {...props}
43
+ >
44
+ <ScrollAreaPrimitive.Thumb
45
+ data-slot="scroll-area-thumb"
46
+ className="relative flex-1 rounded-full bg-border"
47
+ />
48
+ </ScrollAreaPrimitive.Scrollbar>
49
+ );
50
+ }
51
+
52
+ export { ScrollArea, ScrollBar };
@@ -9,6 +9,13 @@ import * as React from "react";
9
9
  import { Button } from "@/components/ui/button";
10
10
  import { Input } from "@/components/ui/input";
11
11
  import { Separator } from "@/components/ui/separator";
12
+ import {
13
+ Sheet,
14
+ SheetContent,
15
+ SheetDescription,
16
+ SheetHeader,
17
+ SheetTitle,
18
+ } from "@/components/ui/sheet";
12
19
  import { Skeleton } from "@/components/ui/skeleton";
13
20
  import {
14
21
  Tooltip,
@@ -220,44 +227,30 @@ function Sidebar({
220
227
  }
221
228
 
222
229
  if (isMobile) {
223
- if (!openMobile) {
224
- return null;
225
- }
226
-
227
230
  return (
228
- <div
229
- data-sidebar="sidebar"
230
- data-slot="sidebar"
231
- data-mobile="true"
232
- className="fixed inset-0 z-50 md:hidden"
233
- {...props}
234
- >
235
- <button
236
- type="button"
237
- aria-label="Close sidebar"
238
- className="absolute inset-0 bg-sidebar/55 supports-backdrop-filter:backdrop-blur-sm"
239
- onClick={() => setOpenMobile(false)}
240
- />
241
- <dialog
242
- open
243
- aria-modal="true"
244
- aria-label="Sidebar"
245
- className={cn(
246
- "bg-sidebar text-sidebar-foreground absolute inset-y-0 z-10 flex w-(--sidebar-width) flex-col shadow-lg",
247
- side === "left" ? "left-0" : "right-0",
248
- )}
231
+ <Sheet open={openMobile} onOpenChange={setOpenMobile}>
232
+ <SheetContent
233
+ data-sidebar="sidebar"
234
+ data-slot="sidebar"
235
+ data-mobile="true"
236
+ className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) max-w-[calc(100vw-2rem)] p-0"
249
237
  style={
250
238
  {
251
239
  "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
252
240
  } as React.CSSProperties
253
241
  }
242
+ side={side}
243
+ {...props}
254
244
  >
255
- <div className="pointer-events-none absolute inset-x-0 top-full h-32 bg-sidebar" />
256
- <div className="flex h-full w-full flex-col overflow-y-auto bg-sidebar">
245
+ <SheetHeader className="sr-only">
246
+ <SheetTitle>Sidebar</SheetTitle>
247
+ <SheetDescription>Console navigation.</SheetDescription>
248
+ </SheetHeader>
249
+ <div className="flex size-full min-h-0 flex-col pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
257
250
  {children}
258
251
  </div>
259
- </dialog>
260
- </div>
252
+ </SheetContent>
253
+ </Sheet>
261
254
  );
262
255
  }
263
256