@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
@@ -3,6 +3,7 @@ import { ChartNoAxesCombined, ListIcon } from "lucide-react";
3
3
 
4
4
  import { buttonVariants } from "@/components/ui/button";
5
5
  import { SidebarTrigger } from "@/components/ui/sidebar";
6
+ import { useConsoleFeature } from "@/lib/console-features-api";
6
7
  import type { InsightsSearch } from "@/lib/insights-search";
7
8
  import { cn } from "@/lib/utils";
8
9
 
@@ -13,6 +14,8 @@ export function InsightsPageHeader({
13
14
  readonly view: "overview" | "events" | "distribution";
14
15
  readonly overviewSearch?: InsightsSearch;
15
16
  }) {
17
+ // A self-hosted server's admin API serves events, not the overview.
18
+ const overview = useConsoleFeature("insightsAnalytics");
16
19
  return (
17
20
  <header className="sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-3 border-b bg-background px-3 py-3 sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm">
18
21
  <SidebarTrigger className="-ml-1 size-11 lg:size-7" />
@@ -24,20 +27,22 @@ export function InsightsPageHeader({
24
27
  <h1 className="text-sm font-medium">Insights</h1>
25
28
  </div>
26
29
  <nav aria-label="Insights views" className="ml-auto flex gap-2">
27
- <Link
28
- aria-current={view === "overview" ? "page" : undefined}
29
- className={cn(
30
- buttonVariants({
31
- className: "h-11 px-3 lg:h-8 lg:px-2.5",
32
- size: "lg",
33
- variant: view === "overview" ? "secondary" : "ghost",
34
- }),
35
- )}
36
- search={overviewSearch}
37
- to="/insights"
38
- >
39
- Overview
40
- </Link>
30
+ {overview ? (
31
+ <Link
32
+ aria-current={view === "overview" ? "page" : undefined}
33
+ className={cn(
34
+ buttonVariants({
35
+ className: "h-11 px-3 lg:h-8 lg:px-2.5",
36
+ size: "lg",
37
+ variant: view === "overview" ? "secondary" : "ghost",
38
+ }),
39
+ )}
40
+ search={overviewSearch}
41
+ to="/insights"
42
+ >
43
+ Overview
44
+ </Link>
45
+ ) : null}
41
46
  <Link
42
47
  aria-current={view === "events" ? "page" : undefined}
43
48
  className={cn(
@@ -1,28 +1,36 @@
1
1
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
2
2
  import type { InsightsWindow } from "@/lib/insights-api";
3
- import { usageMetrics } from "@/lib/insights-usage";
3
+ import { usageMetrics, type UsageWindow } from "@/lib/insights-usage";
4
4
 
5
- export function InsightsPeriodSelector({
6
- window,
7
- onWindowChange,
5
+ /** A segmented control over fixed periods, each shown by its short value. */
6
+ export function PeriodSelector<TPeriod extends string>({
7
+ label,
8
+ onPeriodChange,
9
+ period,
10
+ periods,
8
11
  }: {
9
- readonly window: InsightsWindow;
10
- readonly onWindowChange: (window: InsightsWindow) => void;
12
+ readonly label: string;
13
+ readonly onPeriodChange: (period: TPeriod) => void;
14
+ readonly period: TPeriod;
15
+ readonly periods: readonly {
16
+ readonly value: TPeriod;
17
+ readonly name: string;
18
+ }[];
11
19
  }) {
12
20
  return (
13
21
  <Tabs
14
- value={window}
22
+ value={period}
15
23
  onValueChange={(value) => {
16
- if (value === "24h" || value === "7d" || value === "30d")
17
- onWindowChange(value);
24
+ const next = periods.find((item) => item.value === value);
25
+ if (next) onPeriodChange(next.value);
18
26
  }}
19
27
  >
20
- <TabsList aria-label="Reporting period" className="min-h-11 sm:min-h-9">
21
- {(["24h", "7d", "30d"] as const).map((value) => (
28
+ <TabsList aria-label={label} className="min-h-11 sm:min-h-9">
29
+ {periods.map(({ value, name }) => (
22
30
  <TabsTrigger
23
31
  key={value}
24
32
  value={value}
25
- aria-label={usageMetrics[value].period}
33
+ aria-label={name}
26
34
  className="min-w-12 px-3"
27
35
  >
28
36
  {value}
@@ -32,3 +40,50 @@ export function InsightsPeriodSelector({
32
40
  </Tabs>
33
41
  );
34
42
  }
43
+
44
+ const reportingPeriods = (["24h", "7d", "30d"] as const).map((value) => ({
45
+ value,
46
+ name: usageMetrics[value].period,
47
+ }));
48
+ const usagePeriods = (Object.keys(usageMetrics) as UsageWindow[]).map(
49
+ (value) => ({ value, name: usageMetrics[value].period }),
50
+ );
51
+
52
+ export function InsightsPeriodSelector({
53
+ window,
54
+ onWindowChange,
55
+ }: {
56
+ readonly window: InsightsWindow;
57
+ readonly onWindowChange: (window: InsightsWindow) => void;
58
+ }) {
59
+ return (
60
+ <PeriodSelector
61
+ label="Reporting period"
62
+ onPeriodChange={onWindowChange}
63
+ period={window}
64
+ periods={reportingPeriods}
65
+ />
66
+ );
67
+ }
68
+
69
+ /** App usage's periods: the reporting periods, and 12 months where daily totals cover it. */
70
+ export function UsagePeriodSelector({
71
+ window,
72
+ onWindowChange,
73
+ twelveMonths = true,
74
+ }: {
75
+ readonly window: UsageWindow;
76
+ readonly onWindowChange: (window: UsageWindow) => void;
77
+ readonly twelveMonths?: boolean;
78
+ }) {
79
+ return (
80
+ <PeriodSelector
81
+ label="Reporting period"
82
+ onPeriodChange={onWindowChange}
83
+ period={window}
84
+ periods={usagePeriods.filter(
85
+ ({ value }) => twelveMonths || value !== "12m",
86
+ )}
87
+ />
88
+ );
89
+ }
@@ -2,7 +2,13 @@ import { RefreshCw } from "lucide-react";
2
2
 
3
3
  import { HashValueDisplay } from "@/components/HashValueDisplay";
4
4
  import { Button } from "@/components/ui/button";
5
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
6
12
  import { Separator } from "@/components/ui/separator";
7
13
  import { Skeleton } from "@/components/ui/skeleton";
8
14
  import {
@@ -13,33 +19,41 @@ import {
13
19
  TableHeader,
14
20
  TableRow,
15
21
  } from "@/components/ui/table";
22
+ import {
23
+ DEFAULT_INSIGHTS_RETENTION,
24
+ formatDays,
25
+ type InsightsRetentionDays,
26
+ } from "@/lib/insights-retention";
16
27
  import type {
17
28
  InsightsEventRow,
18
29
  InsightsInstallationViewRow,
19
30
  InsightsViewPage,
20
31
  } from "@/lib/insights-view";
21
32
 
33
+ import { BuiltinBundleBadge, isBuiltinBundle } from "./BuiltinBundle";
22
34
  import {
23
35
  EventBundleTransition,
24
36
  EventTimestamp,
25
37
  EventTypeDetails,
38
+ HttpResponseDetails,
26
39
  useInsightsTimeFormat,
27
40
  } from "./EventDetails";
28
41
  import { EventHistoryList } from "./EventHistoryList";
29
42
  import { InsightsErrorAlert } from "./InsightsErrorAlert";
30
43
  import { InsightsPagination } from "./InsightsPagination";
31
44
 
32
- const getUserLabel = (event: {
33
- readonly username: string | null;
34
- readonly userId: string | null;
35
- }) => event.userId ?? event.username ?? "—";
45
+ const getUserLabel = (event: { readonly userId: string | null }) =>
46
+ event.userId ?? "—";
36
47
 
48
+ /** A download or a failed update leaves the installation on the bundle it ran. */
37
49
  const getLastKnownBundleId = (
38
50
  event: InsightsEventRow | InsightsInstallationViewRow,
39
51
  ) =>
40
52
  "lastKnownBundleId" in event
41
53
  ? event.lastKnownBundleId
42
- : (event.toBundleId ?? event.fromBundleId);
54
+ : event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_FAILED"
55
+ ? event.fromBundleId
56
+ : event.toBundleId;
43
57
 
44
58
  export function InstallationHistoryCard({
45
59
  error,
@@ -49,6 +63,7 @@ export function InstallationHistoryCard({
49
63
  onPrevious,
50
64
  onRefresh,
51
65
  pageNumber,
66
+ retention = DEFAULT_INSIGHTS_RETENTION,
52
67
  selectedEvent,
53
68
  selectedInstallId,
54
69
  }: {
@@ -59,6 +74,8 @@ export function InstallationHistoryCard({
59
74
  readonly onNext: () => void;
60
75
  readonly onPrevious: () => void;
61
76
  readonly pageNumber: number;
77
+ /** How long the server keeps Insights rows. */
78
+ readonly retention?: InsightsRetentionDays;
62
79
  readonly selectedEvent:
63
80
  | InsightsEventRow
64
81
  | InsightsInstallationViewRow
@@ -70,6 +87,15 @@ export function InstallationHistoryCard({
70
87
  ? getLastKnownBundleId(selectedEvent)
71
88
  : undefined;
72
89
 
90
+ const pendingBundleId =
91
+ selectedEvent && "pendingBundleId" in selectedEvent
92
+ ? selectedEvent.pendingBundleId
93
+ : selectedEvent &&
94
+ "type" in selectedEvent &&
95
+ selectedEvent.type === "UPDATE_DOWNLOADED"
96
+ ? selectedEvent.toBundleId
97
+ : null;
98
+
73
99
  return (
74
100
  <Card className="@container min-h-0 min-w-0 shadow-sm">
75
101
  <CardHeader className="p-4 sm:p-6">
@@ -82,6 +108,14 @@ export function InstallationHistoryCard({
82
108
  : "Select an installation"}
83
109
  </h2>
84
110
  </CardTitle>
111
+ {selectedInstallId !== undefined ? (
112
+ <CardDescription>
113
+ Bundle changes and update failures are kept for{" "}
114
+ {formatDays(retention.rawDays)}; the latest report, for{" "}
115
+ {formatDays(retention.dailyDays)} after the installation last
116
+ reports.
117
+ </CardDescription>
118
+ ) : null}
85
119
  </div>
86
120
  {onRefresh && selectedInstallId !== undefined ? (
87
121
  <Button
@@ -127,7 +161,7 @@ export function InstallationHistoryCard({
127
161
  <dt className="text-xs text-muted-foreground">
128
162
  Last known bundle
129
163
  </dt>
130
- <dd className="mt-1 font-mono text-xs">
164
+ <dd className="mt-1 flex min-w-0 flex-wrap items-center gap-2 font-mono text-xs">
131
165
  {lastKnownBundleId ? (
132
166
  <HashValueDisplay
133
167
  value={lastKnownBundleId}
@@ -136,9 +170,30 @@ export function InstallationHistoryCard({
136
170
  ) : (
137
171
  "—"
138
172
  )}
173
+ {isBuiltinBundle(
174
+ lastKnownBundleId,
175
+ selectedEvent.minBundleId,
176
+ ) ? (
177
+ <BuiltinBundleBadge />
178
+ ) : null}
139
179
  </dd>
140
180
  </div>
181
+ {pendingBundleId ? (
182
+ <div className="col-span-2 min-w-0">
183
+ <dt className="text-xs text-muted-foreground">
184
+ Downloaded · Not launched yet
185
+ </dt>
186
+ <dd className="mt-1 text-xs">
187
+ <HashValueDisplay value={pendingBundleId} />
188
+ </dd>
189
+ </div>
190
+ ) : null}
141
191
  </dl>
192
+ {selectedEvent.httpResponse ? (
193
+ <div className="mt-4">
194
+ <HttpResponseDetails event={selectedEvent} />
195
+ </div>
196
+ ) : null}
142
197
  </section>
143
198
  <Separator />
144
199
  </>
@@ -192,7 +247,7 @@ export function InstallationHistoryCard({
192
247
  />
193
248
  </TableCell>
194
249
  <TableCell>
195
- <EventTypeDetails type={event.type} />
250
+ <EventTypeDetails event={event} />
196
251
  </TableCell>
197
252
  <TableCell className="whitespace-normal text-xs">
198
253
  <div className="flex flex-col gap-2">
@@ -226,8 +281,8 @@ export function InstallationHistoryCard({
226
281
  ) : (
227
282
  <>
228
283
  <div className="p-6 text-sm text-muted-foreground">
229
- {pageNumber > 1 || history?.nextCursor
230
- ? "No bundle changes on this page."
284
+ {pageNumber > 1
285
+ ? "No older bundle changes."
231
286
  : "No bundle changes recorded yet."}
232
287
  </div>
233
288
  {history && (pageNumber > 1 || history.nextCursor) ? (
@@ -17,7 +17,7 @@ const getLastKnownBundleId = (event: InsightsInstallationViewRow) =>
17
17
  event.lastKnownBundleId;
18
18
 
19
19
  const getUserLabel = (event: InsightsInstallationViewRow) =>
20
- event.userId ?? event.username ?? "—";
20
+ event.userId ?? "—";
21
21
 
22
22
  export function InstallationMatchesCard({
23
23
  error,
@@ -0,0 +1,308 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { ChartLine } from "lucide-react";
3
+
4
+ import { buttonVariants } from "@/components/ui/button";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardFooter,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
12
+ import { Separator } from "@/components/ui/separator";
13
+ import { Skeleton } from "@/components/ui/skeleton";
14
+ import type {
15
+ BundleActivityInput,
16
+ BundleActivityReport,
17
+ } from "@/lib/bundle-activity";
18
+ import { useBundleActivityQuery } from "@/lib/bundle-activity";
19
+ import { useConsoleFeature } from "@/lib/console-features-api";
20
+ import { useUpdateFailuresQuery } from "@/lib/insights-api";
21
+ import { failureRate, formatRate } from "@/lib/insights-failures";
22
+ import { adoptionWindow } from "@/lib/insights-search";
23
+ import { cn } from "@/lib/utils";
24
+
25
+ import type { ReleaseColumn } from "../FeatureSlots";
26
+ import { InsightsInfo } from "./InsightsInfo";
27
+
28
+ const activityStats = [
29
+ { label: "Downloaded", field: "downloads", color: "text-primary/85" },
30
+ { label: "Launched", field: "launched", color: "text-success/85" },
31
+ { label: "Crashed", field: "failedLaunches", color: "text-warning/85" },
32
+ ] as const;
33
+
34
+ // As Release health rates crashes: over launches plus crashes, since an
35
+ // installation that crashes never reports running the bundle.
36
+ const crashRate = (report: BundleActivityReport): string => {
37
+ const attempts = report.launched + report.failedLaunches;
38
+ return attempts === 0
39
+ ? "—"
40
+ : `${((report.failedLaunches / attempts) * 100).toFixed(2)}%`;
41
+ };
42
+
43
+ /** Downloads that neither launched nor crashed yet: waiting for a restart, or passed over for a newer bundle. */
44
+ const notLaunched = (report: BundleActivityReport) =>
45
+ Math.max(0, report.downloads - report.launched - report.failedLaunches);
46
+
47
+ function ReleaseMetricsInfo() {
48
+ return (
49
+ <InsightsInfo label="About release insight metrics">
50
+ Downloaded: got this bundle. Launched: ran it. Crashed: crashed on it and
51
+ rolled back. Once every installation restarts, Downloaded = Launched +
52
+ Crashed.
53
+ </InsightsInfo>
54
+ );
55
+ }
56
+
57
+ export function BundleMovementSummary({
58
+ report,
59
+ loading = false,
60
+ input,
61
+ variant = "inline",
62
+ }: {
63
+ readonly report?: BundleActivityReport;
64
+ readonly loading?: boolean;
65
+ readonly input?: BundleActivityInput;
66
+ readonly variant?: "inline" | "card";
67
+ }) {
68
+ if (!report) {
69
+ return loading ? (
70
+ <Skeleton
71
+ aria-label="Loading release insights"
72
+ className={variant === "card" ? "h-16 w-full" : "h-4 w-40"}
73
+ />
74
+ ) : (
75
+ <span
76
+ aria-label="Release insights unavailable"
77
+ className="text-sm text-muted-foreground"
78
+ >
79
+ —
80
+ </span>
81
+ );
82
+ }
83
+ // A release of the built-in bundle comes with the app: nothing downloads it.
84
+ const builtIn = input?.builtIn === true;
85
+ const waiting = builtIn ? 0 : notLaunched(report);
86
+ const metrics = (
87
+ <dl
88
+ className={cn(
89
+ variant === "card"
90
+ ? "grid grid-cols-[1fr_1fr_auto] gap-4"
91
+ : "flex min-w-[220px] flex-wrap items-baseline gap-x-3 gap-y-1",
92
+ )}
93
+ >
94
+ {activityStats.map(({ label, field, color }) => (
95
+ <div
96
+ key={field}
97
+ className={cn(
98
+ "flex",
99
+ variant === "card"
100
+ ? "min-w-0 flex-col gap-1"
101
+ : "items-baseline gap-1.5 whitespace-nowrap",
102
+ )}
103
+ >
104
+ <dt className="text-xs text-muted-foreground">{label}</dt>
105
+ <dd
106
+ className={cn(
107
+ "flex flex-wrap items-baseline gap-x-1.5 tabular-nums",
108
+ variant === "card"
109
+ ? "text-xl font-semibold"
110
+ : "text-sm font-medium",
111
+ report[field] > 0 && !(field === "downloads" && builtIn)
112
+ ? color
113
+ : "text-muted-foreground",
114
+ )}
115
+ >
116
+ {field === "downloads" && builtIn ? (
117
+ <span title="The built-in bundle is never downloaded">—</span>
118
+ ) : (
119
+ report[field].toLocaleString()
120
+ )}
121
+ {field === "failedLaunches" ? (
122
+ <span className="text-xs font-normal text-muted-foreground">
123
+ ({crashRate(report)})
124
+ </span>
125
+ ) : null}
126
+ </dd>
127
+ </div>
128
+ ))}
129
+ </dl>
130
+ );
131
+ return (
132
+ <div
133
+ className={cn(
134
+ "flex",
135
+ variant === "card" ? "flex-col gap-2" : "items-center gap-1",
136
+ )}
137
+ >
138
+ {input ? (
139
+ <Link
140
+ className="min-w-0 rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
141
+ to="/insights"
142
+ search={{
143
+ healthPlatform: input.platform,
144
+ healthChannel: input.channel,
145
+ releaseId: input.releaseId,
146
+ bundleWindow: adoptionWindow(input.releaseId),
147
+ }}
148
+ >
149
+ {metrics}
150
+ </Link>
151
+ ) : (
152
+ metrics
153
+ )}
154
+ {variant === "card" && waiting > 0 ? (
155
+ <p className="text-xs text-muted-foreground">
156
+ {waiting.toLocaleString()} downloaded but not launched yet: waiting
157
+ for a restart, or passed over for a newer bundle.
158
+ </p>
159
+ ) : null}
160
+ {variant === "inline" ? <ReleaseMetricsInfo /> : null}
161
+ </div>
162
+ );
163
+ }
164
+
165
+ /** The release's failed downloads and installs since its first report, and their rate. */
166
+ export function BundleDownloadFailures({
167
+ input,
168
+ }: {
169
+ readonly input: BundleActivityInput;
170
+ }) {
171
+ const query = useUpdateFailuresQuery({
172
+ platform: input.platform,
173
+ channel: input.channel,
174
+ releaseId: input.releaseId,
175
+ });
176
+ const report = query.data;
177
+ return (
178
+ <div className="flex items-center gap-1">
179
+ {report ? (
180
+ <dl>
181
+ <div className="flex flex-wrap items-baseline gap-x-1.5">
182
+ <dt className="text-xs text-muted-foreground">Download failures</dt>
183
+ <dd
184
+ className={cn(
185
+ "flex items-baseline gap-1.5 text-sm font-medium tabular-nums",
186
+ report.failedUpdates > 0
187
+ ? "text-warning/85"
188
+ : "text-muted-foreground",
189
+ )}
190
+ >
191
+ {report.failedUpdates.toLocaleString()}
192
+ <span className="text-xs font-normal text-muted-foreground">
193
+ ({formatRate(failureRate(report))})
194
+ </span>
195
+ </dd>
196
+ </div>
197
+ </dl>
198
+ ) : query.isPending ? (
199
+ <Skeleton aria-label="Loading download failures" className="h-4 w-40" />
200
+ ) : (
201
+ <span
202
+ aria-label="Download failures unavailable"
203
+ className="text-sm text-muted-foreground"
204
+ >
205
+ —
206
+ </span>
207
+ )}
208
+ <InsightsInfo label="About download failures">
209
+ Failed downloads or installs of this bundle, at most one per
210
+ installation a day. Rate = failures ÷ (failures + downloads).
211
+ </InsightsInfo>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ /** The bundle detail's Insights card, a section of the release editor: activity and download failures. */
217
+ export function BundleInsightsSummary({
218
+ input,
219
+ }: {
220
+ readonly input: BundleActivityInput;
221
+ }) {
222
+ const query = useBundleActivityQuery([input]);
223
+ return (
224
+ <Card>
225
+ <CardHeader className="flex-row items-center justify-between px-4 pt-4 pb-3">
226
+ <CardTitle className="text-sm font-medium">Insights</CardTitle>
227
+ <ReleaseMetricsInfo />
228
+ </CardHeader>
229
+ <CardContent className="flex flex-col gap-3 px-4 pb-4">
230
+ <BundleMovementSummary
231
+ variant="card"
232
+ input={input}
233
+ loading={query.isFetching}
234
+ report={query.data?.[input.releaseId]}
235
+ />
236
+ <Separator />
237
+ <BundleDownloadFailures input={input} />
238
+ </CardContent>
239
+ <CardFooter className="px-4 pb-4">
240
+ <Link
241
+ className={buttonVariants({ variant: "outline", size: "sm" })}
242
+ to="/insights"
243
+ search={{
244
+ healthPlatform: input.platform,
245
+ healthChannel: input.channel,
246
+ releaseId: input.releaseId,
247
+ bundleWindow: adoptionWindow(input.releaseId),
248
+ healthChart: "adoption",
249
+ }}
250
+ >
251
+ <ChartLine aria-hidden="true" data-icon="inline-start" />
252
+ View adoption
253
+ </Link>
254
+ </CardFooter>
255
+ </Card>
256
+ );
257
+ }
258
+
259
+ /**
260
+ * The release editor's Insights card where the console reads no release
261
+ * activity, as through a self-hosted server's admin API: download failures
262
+ * alone. With release activity, its card shows them.
263
+ */
264
+ export function ReleaseFailuresSection({
265
+ input,
266
+ }: {
267
+ readonly input: BundleActivityInput;
268
+ }) {
269
+ const activity = useConsoleFeature("insightsAnalytics");
270
+ if (activity) return null;
271
+ return (
272
+ <Card>
273
+ <CardHeader className="px-4 pt-4 pb-3">
274
+ <CardTitle className="text-sm font-medium">Insights</CardTitle>
275
+ </CardHeader>
276
+ <CardContent className="px-4 pb-4">
277
+ <BundleDownloadFailures input={input} />
278
+ </CardContent>
279
+ </Card>
280
+ );
281
+ }
282
+
283
+ /**
284
+ * One release's activity in the Bundles page's Insights column. Every cell
285
+ * asks for the page's releases under one query key, so the page makes one
286
+ * request.
287
+ */
288
+ function ReleaseActivityCell({
289
+ releaseId,
290
+ releases,
291
+ }: {
292
+ readonly releaseId: string;
293
+ readonly releases: readonly BundleActivityInput[];
294
+ }) {
295
+ const query = useBundleActivityQuery(releases);
296
+ return (
297
+ <BundleMovementSummary
298
+ input={releases.find((release) => release.releaseId === releaseId)}
299
+ loading={query.isFetching}
300
+ report={query.data?.[releaseId]}
301
+ />
302
+ );
303
+ }
304
+
305
+ /** The Bundles page's Insights column: each release's downloads, launches, and crashes. */
306
+ export const releaseActivityColumn: ReleaseColumn = {
307
+ Cell: ReleaseActivityCell,
308
+ };
@@ -1,7 +1,7 @@
1
1
  import { Skeleton } from "@/components/ui/skeleton";
2
- import type { InsightsWindow } from "@/lib/insights-api";
3
- import { usageMetrics } from "@/lib/insights-usage";
2
+ import { usageMetrics, type UsageWindow } from "@/lib/insights-usage";
4
3
 
4
+ import { EstimatedCount } from "./EstimatedCount";
5
5
  import { InsightsInfo } from "./InsightsInfo";
6
6
 
7
7
  export function ReportingDevicesSummary({
@@ -10,7 +10,7 @@ export function ReportingDevicesSummary({
10
10
  isPending,
11
11
  partial,
12
12
  }: {
13
- readonly window: InsightsWindow;
13
+ readonly window: UsageWindow;
14
14
  readonly count: number | undefined;
15
15
  readonly isPending: boolean;
16
16
  readonly partial: boolean;
@@ -26,21 +26,21 @@ export function ReportingDevicesSummary({
26
26
  <Skeleton aria-label="Loading active users" className="h-9 w-12" />
27
27
  ) : (
28
28
  <p className="text-4xl font-semibold tracking-tight tabular-nums">
29
- {count === undefined
30
- ? "—"
31
- : `${partial ? "≥" : ""}${count.toLocaleString()}`}
29
+ {count === undefined ? (
30
+ "—"
31
+ ) : partial ? (
32
+ `≥${count.toLocaleString()}`
33
+ ) : (
34
+ <EstimatedCount value={count} />
35
+ )}
32
36
  </p>
33
37
  )}
34
38
  <div className="flex items-center gap-1 text-sm text-muted-foreground">
35
39
  <span>{metric.label}</span>
36
40
  <InsightsInfo label={`How ${metric.label} is counted`}>
37
- Unique app installations that reported activity in the last{" "}
38
- {metric.period}, including no-change reports. Filtered by platform,
39
- channel, and app version. The chart counts unique reporting
40
- installations per {metric.interval}.
41
- {partial
42
- ? " Only part of the history is available; the total is a lower bound."
43
- : ""}
41
+ Installations that reported in the last {metric.period}, estimated
42
+ within about 3%. The chart shows them per {metric.interval}.
43
+ {partial ? " Some history is missing, so this is a minimum." : ""}
44
44
  </InsightsInfo>
45
45
  </div>
46
46
  </div>