@hot-updater/console 1.0.0-rc.3 → 1.0.0-rc.31

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-BH7LLqAg.js +1 -0
  3. package/.output/public/assets/EventDetails-DZby1OQH.js +1 -0
  4. package/.output/public/assets/InsightsInfo-B8i-PPsH.js +1 -0
  5. package/.output/public/assets/InsightsPageHeader-DUgKPOEO.js +1 -0
  6. package/.output/public/assets/NotFoundPage-CkgdpWvl.js +1 -0
  7. package/.output/public/assets/alert-dialog-DXxrSU0G.js +1 -0
  8. package/.output/public/assets/api-BiZP_i-u.js +1 -0
  9. package/.output/public/assets/api-keys-DQDqmKtq.js +1 -0
  10. package/.output/public/assets/api-keys-DTQ1HrkR.js +1 -0
  11. package/.output/public/assets/arrow-left-BPK7iqvj.js +1 -0
  12. package/.output/public/assets/card-DKr2pZMA.js +1 -0
  13. package/.output/public/assets/console-features-api-BhUIxeL-.js +1 -0
  14. package/.output/public/assets/dialog-DxjO7ofz.js +1 -0
  15. package/.output/public/assets/dist-BzWwzt98.js +1 -0
  16. package/.output/public/assets/empty-DtangHOp.js +1 -0
  17. package/.output/public/assets/field-bPoDrs6K.js +1 -0
  18. package/.output/public/assets/index-CukOHJfR.js +11 -0
  19. package/.output/public/assets/input-group-_PD0i7wZ.js +1 -0
  20. package/.output/public/assets/insights-BBya_x8N.js +1 -0
  21. package/.output/public/assets/insights-BfxN6oJt.js +2 -0
  22. package/.output/public/assets/insights-DSo4OVJK.js +42 -0
  23. package/.output/public/assets/insights-usage-rpc-Ceygyqz5.js +1 -0
  24. package/.output/public/assets/insights_.distribution-BKOhL2-k.js +2 -0
  25. package/.output/public/assets/insights_.distribution-Qq4cisRR.js +1 -0
  26. package/.output/public/assets/insights_.distribution-WTcKITzb.js +1 -0
  27. package/.output/public/assets/installations-CstW4tEL.js +1 -0
  28. package/.output/public/assets/installations-DKMg-0wU.js +2 -0
  29. package/.output/public/assets/installations-_UEoKAn_.js +1 -0
  30. package/.output/public/assets/loader-circle-iTGehfyV.js +1 -0
  31. package/.output/public/assets/mutation-B4EZNS91.js +1 -0
  32. package/.output/public/assets/preload-helper-DuV6lz46.js +1 -0
  33. package/.output/public/assets/refresh-cw-3fSQ1FYY.js +1 -0
  34. package/.output/public/assets/routes-TFPucRmX.js +2 -0
  35. package/.output/public/assets/routes-_sHQ8uxu.js +1 -0
  36. package/.output/public/assets/sidebar-RBvHzq8e.js +3 -0
  37. package/.output/public/assets/styles-DmbQqD7a.css +2 -0
  38. package/.output/public/assets/table-BYmouPt9.js +1 -0
  39. package/.output/public/assets/useCompositeListItem-BxJspDxS.js +1 -0
  40. package/.output/public/assets/valueToPercent-BjsIMB74.js +1 -0
  41. package/.output/server/{__23tanstack-start-server-fn-resolver-EMel9KYs.mjs → __23tanstack-start-server-fn-resolver-B5vj_i3l.mjs} +63 -51
  42. package/.output/server/_libs/@base-ui/react+[...].mjs +6891 -3364
  43. package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
  44. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
  45. package/.output/server/_libs/lucide-react.mjs +71 -31
  46. package/.output/server/_libs/next-themes.mjs +1 -1
  47. package/.output/server/_libs/react-redux+[...].mjs +1 -1
  48. package/.output/server/_libs/recharts+[...].mjs +1 -1
  49. package/.output/server/_libs/sonner.mjs +1 -1
  50. package/.output/server/_libs/tanstack__history.mjs +68 -41
  51. package/.output/server/_libs/tanstack__query-core.mjs +1994 -310
  52. package/.output/server/_libs/tanstack__react-query.mjs +448 -21
  53. package/.output/server/_libs/verkit.mjs +99 -1
  54. package/.output/server/_ssr/ConsoleFeatureUnavailable-BuWwEnk_.mjs +58 -0
  55. package/.output/server/_ssr/EventDetails-ksr9nh9K.mjs +289 -0
  56. package/.output/server/_ssr/InsightsInfo-C4VjjzZR.mjs +75 -0
  57. package/.output/server/_ssr/InsightsPageHeader-vt4sJWTO.mjs +161 -0
  58. package/.output/server/_ssr/NotFoundPage-B_NdWE3y.mjs +37 -0
  59. package/.output/server/_ssr/{dialog-DNtKYR5K.mjs → alert-dialog-CKVhOIS_.mjs} +6 -83
  60. package/.output/server/_ssr/api-gs0XqRQX.mjs +203 -0
  61. package/.output/server/_ssr/{api-keys-BBl84_lN.mjs → api-keys-BLS-VTWq.mjs} +49 -8
  62. package/.output/server/_ssr/api-keys-C8VVGWjc.mjs +5 -0
  63. package/.output/server/_ssr/api-keys-rpc-xuz9tsHh.mjs +52 -0
  64. package/.output/server/_ssr/api-rpc-CCiLRUac.mjs +296 -0
  65. package/.output/server/_ssr/{auth-rpc-gaibR719.mjs → auth-rpc-CUFtJtD0.mjs} +6 -6
  66. package/.output/server/_ssr/auth.server-BUj7X-LJ.mjs +5712 -0
  67. package/.output/server/_ssr/bundleActivity-H7ccYo-o.mjs +18 -0
  68. package/.output/server/_ssr/card-CTElTN0R.mjs +49 -0
  69. package/.output/server/_ssr/config-nsIcsHh8.mjs +15 -0
  70. package/.output/server/_ssr/config.server-BEyzkj4G.mjs +36 -0
  71. package/.output/server/_ssr/console-access-Ys4Zkk_1.mjs +17 -0
  72. package/.output/server/_ssr/console-features-AGV2b_l5.mjs +55 -0
  73. package/.output/server/_ssr/console-features-api-BmJFrtS2.mjs +43 -0
  74. package/.output/server/_ssr/console-features-rpc-C8w1DJhN.mjs +19 -0
  75. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-BdBf_ezN.mjs} +67 -56
  76. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-B_y2lrwA.mjs} +2 -2
  77. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Bhxciy7Z.mjs} +28 -5
  78. package/.output/server/_ssr/databaseRows-DwMAjaF8.mjs +74 -0
  79. package/.output/server/_ssr/dialog-CTvSCO60.mjs +84 -0
  80. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-Cxo9ghYv.mjs} +140 -6
  81. package/.output/server/_ssr/{downloadBundle-A9zYC2go.mjs → downloadBundle-D10vIrle.mjs} +10 -10
  82. package/.output/server/_ssr/empty-IW1FKTmA.mjs +49 -0
  83. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CYdinE-i.mjs} +1 -1
  84. package/.output/server/_ssr/failureReports-CRYHdQBQ.mjs +33 -0
  85. package/.output/server/_ssr/{table-QQ9cnWf2.mjs → field-BDdwYmC9.mjs} +62 -50
  86. package/.output/server/_ssr/getBundleChildren-DLGwrnCV.mjs +22 -0
  87. package/.output/server/_ssr/{input-group-Oj4kUnE4.mjs → input-group-CtXqWBma.mjs} +52 -3
  88. package/.output/server/_ssr/insights-BwcXtX7g.mjs +6 -0
  89. package/.output/server/_ssr/insights-C8ASzAF4.mjs +2311 -0
  90. package/.output/server/_ssr/insights-ElGkOnT7.mjs +15 -0
  91. package/.output/server/_ssr/insights-api-DG2TYhoW.mjs +122 -0
  92. package/.output/server/_ssr/insights-errors-F-7hOApm.mjs +34 -0
  93. package/.output/server/_ssr/insights-failures-BZaGNNij.mjs +47 -0
  94. package/.output/server/_ssr/insights-failures-rpc-tmzvyUYN.mjs +36 -0
  95. package/.output/server/_ssr/{insights-recovery-DSFM-YTL.mjs → insights-recovery-Dpk6Mfb9.mjs} +9 -2
  96. package/.output/server/_ssr/insights-recovery-rpc-BeiUA-UY.mjs +32 -0
  97. package/.output/server/_ssr/{insights-rpc-DBf0ouen.mjs → insights-rpc-CmHYtKGn.mjs} +22 -14
  98. package/.output/server/_ssr/insights-usage-CLUWjUFz.mjs +29 -0
  99. package/.output/server/_ssr/insights-usage-rpc-CCYPPFtN.mjs +29 -0
  100. package/.output/server/_ssr/insights-usage-rpc-CIHxaYDx.mjs +41 -0
  101. package/.output/server/_ssr/insightsUsage-9X2sp3ja.mjs +56 -0
  102. package/.output/server/_ssr/insights_.distribution-BIrvxsSd.mjs +334 -0
  103. package/.output/server/_ssr/insights_.distribution-BrmjQspC.mjs +5 -0
  104. package/.output/server/_ssr/insights_.distribution-CLPoeEKH.mjs +14 -0
  105. package/.output/server/_ssr/installations-BYDcd1BU.mjs +69 -0
  106. package/.output/server/_ssr/{installations-DQyJN-pZ.mjs → installations-CiW7qX4b.mjs} +428 -97
  107. package/.output/server/_ssr/installations-DfvIE7EI.mjs +5 -0
  108. package/.output/server/_ssr/listReleases-BitHzOYB.mjs +79 -0
  109. package/.output/server/_ssr/release-adoption-C2b8n-Xs.mjs +55 -0
  110. package/.output/server/_ssr/release-adoption-rpc-BENRIE3G.mjs +41 -0
  111. package/.output/server/_ssr/releaseAdoption-DW_6eea-.mjs +96 -0
  112. package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
  113. package/.output/server/_ssr/{router-C8J4nYEG.mjs → router-Cu1yxD7n.mjs} +134 -138
  114. package/.output/server/_ssr/routes-DZeygWIs.mjs +369 -0
  115. package/.output/server/_ssr/{routes-NlTRKPML.mjs → routes-lIwWp0Ch.mjs} +175 -389
  116. package/.output/server/_ssr/runtime.server-Dkb3vsY2.mjs +1500 -0
  117. package/.output/server/_ssr/schema-BKY9SqaY.mjs +25 -0
  118. package/.output/server/_ssr/select-dVdHxJGf.mjs +107 -0
  119. package/.output/server/_ssr/server-BjWNP8Qv.mjs +8330 -0
  120. package/.output/server/_ssr/{createSsrRpc-4rr4IJh3.mjs → sidebar-BZrY-cut.mjs} +107 -47
  121. package/.output/server/_ssr/ssr.mjs +1 -1
  122. package/.output/server/_ssr/start-BXUOv_n8.mjs +4 -0
  123. package/.output/server/_ssr/table-BhkGaNvb.mjs +53 -0
  124. package/.output/server/_ssr/updateFailures-BP-A8Ppr.mjs +40 -0
  125. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C8bwiZr8.mjs} +2 -2
  126. package/.output/server/_tanstack-start-manifest_v-DWJb58Ez.mjs +123 -0
  127. package/.output/server/index.mjs +241 -164
  128. package/LICENSE +13 -14
  129. package/README.md +82 -72
  130. package/lib/index.d.mts +23 -11
  131. package/lib/vite-internal.mjs +13 -3
  132. package/lib/vite.d.mts +2 -0
  133. package/package.json +14 -14
  134. package/src/components/AppSidebar.tsx +100 -71
  135. package/src/components/ConsoleAccessPage.tsx +5 -1
  136. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  137. package/src/components/NotFoundPage.tsx +7 -7
  138. package/src/components/features/FeatureSlots.tsx +99 -0
  139. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  140. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  141. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  142. package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
  143. package/src/components/features/insights/AppUsage.tsx +370 -0
  144. package/src/components/features/insights/ComparedBundlesChart.tsx +256 -0
  145. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  146. package/src/components/features/insights/EventDetails.tsx +294 -37
  147. package/src/components/features/insights/EventHistoryCard.tsx +112 -26
  148. package/src/components/features/insights/EventHistoryList.tsx +2 -2
  149. package/src/components/features/insights/FailureReports.tsx +394 -0
  150. package/src/components/features/insights/InsightsControls.tsx +265 -102
  151. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  152. package/src/components/features/insights/InsightsInfo.tsx +48 -0
  153. package/src/components/features/insights/InsightsOverview.tsx +583 -167
  154. package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
  155. package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
  156. package/src/components/features/insights/InstallationHistoryCard.tsx +58 -10
  157. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  158. package/src/components/features/insights/ReleaseActivity.tsx +290 -0
  159. package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
  160. package/src/components/features/insights/RollbackButton.tsx +118 -0
  161. package/src/components/features/insights/UpdateFailures.tsx +451 -0
  162. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  163. package/src/components/features/releases/ReleaseEditorSheet.tsx +30 -20
  164. package/src/components/ui/card.tsx +1 -1
  165. package/src/components/ui/scroll-area.tsx +52 -0
  166. package/src/components/ui/sidebar.tsx +22 -29
  167. package/src/components/ui/tabs.tsx +81 -0
  168. package/src/index.ts +26 -19
  169. package/src/lib/api-keys-api.ts +0 -28
  170. package/src/lib/api-keys-rpc.ts +27 -38
  171. package/src/lib/api-rpc.ts +189 -266
  172. package/src/lib/api.ts +19 -102
  173. package/src/lib/bundle-activity.ts +30 -0
  174. package/src/lib/console-access.ts +20 -0
  175. package/src/lib/console-features-api.ts +68 -0
  176. package/src/lib/console-features-rpc.ts +13 -0
  177. package/src/lib/console-features.ts +91 -0
  178. package/src/lib/insights-api.ts +56 -2
  179. package/src/lib/insights-errors.ts +64 -0
  180. package/src/lib/insights-failures-rpc.ts +36 -0
  181. package/src/lib/insights-failures.ts +140 -0
  182. package/src/lib/insights-recovery-rpc.ts +28 -8
  183. package/src/lib/insights-recovery.ts +10 -23
  184. package/src/lib/insights-retention.ts +21 -0
  185. package/src/lib/insights-rpc.ts +30 -12
  186. package/src/lib/insights-search.ts +93 -0
  187. package/src/lib/insights-usage-rpc.ts +29 -0
  188. package/src/lib/insights-usage.ts +74 -0
  189. package/src/lib/insights-view.ts +38 -2
  190. package/src/lib/release-adoption-api.ts +214 -0
  191. package/src/lib/release-adoption-rpc.ts +49 -0
  192. package/src/lib/release-adoption.ts +159 -0
  193. package/src/lib/scroll-restoration.ts +21 -0
  194. package/src/lib/server/adminInsights.ts +124 -0
  195. package/src/lib/server/auth.server.ts +13 -5
  196. package/src/lib/server/bundleActivity.ts +36 -0
  197. package/src/lib/server/config.server.ts +21 -74
  198. package/src/lib/server/console-feature-apis.ts +59 -0
  199. package/src/lib/server/console-runtime.server.ts +82 -38
  200. package/src/lib/server/downloadBundle.ts +16 -15
  201. package/src/lib/server/failureReports.ts +49 -0
  202. package/src/lib/server/getBundleChildren.ts +31 -122
  203. package/src/lib/server/insightsUsage.ts +63 -0
  204. package/src/lib/server/listReleases.ts +109 -44
  205. package/src/lib/server/releaseAdoption.ts +124 -0
  206. package/src/lib/server/releaseReachability.ts +8 -7
  207. package/src/lib/server/runtime.server.ts +120 -31
  208. package/src/lib/server/updateFailures.ts +65 -0
  209. package/src/routeTree.gen.ts +44 -44
  210. package/src/router.tsx +3 -2
  211. package/src/routes/-installations-search.ts +32 -8
  212. package/src/routes/-releases-search.ts +76 -29
  213. package/src/routes/__root.tsx +10 -4
  214. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  215. package/src/routes/api-keys.tsx +5 -2
  216. package/src/routes/index.tsx +167 -104
  217. package/src/routes/insights.tsx +157 -60
  218. package/src/routes/insights_.distribution.tsx +398 -0
  219. package/src/routes/installations.tsx +98 -41
  220. package/src/styles.css +9 -0
  221. package/src/vite-internal.ts +15 -3
  222. package/src/vite.ts +2 -0
  223. package/.output/public/assets/InsightsErrorAlert-1cg6LDJG.js +0 -1
  224. package/.output/public/assets/InsightsPageHeader-oNIg_lcd.js +0 -1
  225. package/.output/public/assets/InsightsRolloutCard-Zsv9gBjU.js +0 -42
  226. package/.output/public/assets/api-keys-ChJjsbP2.js +0 -1
  227. package/.output/public/assets/api-keys-api-LchP65vQ.js +0 -1
  228. package/.output/public/assets/card-iD7iiy4T.js +0 -1
  229. package/.output/public/assets/chart-no-axes-combined-C0G0fQ3J.js +0 -1
  230. package/.output/public/assets/createServerFn-DHc2erub.js +0 -3
  231. package/.output/public/assets/dialog-DX6o8qCL.js +0 -1
  232. package/.output/public/assets/dist-BPrb_vuw.js +0 -1
  233. package/.output/public/assets/index-C6Nb14xG.js +0 -10
  234. package/.output/public/assets/input-group-Pa3SAbEI.js +0 -1
  235. package/.output/public/assets/insights-C1uKy3p2.js +0 -1
  236. package/.output/public/assets/installations-Bj5AzZ3T.js +0 -1
  237. package/.output/public/assets/installations-DDTw9DSK.js +0 -2
  238. package/.output/public/assets/lazyRouteComponent-BN0MS05o.js +0 -1
  239. package/.output/public/assets/link-BG06whwB.js +0 -1
  240. package/.output/public/assets/loader-circle-Bbp8LGVC.js +0 -1
  241. package/.output/public/assets/preload-helper-B_l5jfgx.js +0 -1
  242. package/.output/public/assets/refresh-cw-DZgiAOMk.js +0 -1
  243. package/.output/public/assets/routes-9E2okfpG.js +0 -1
  244. package/.output/public/assets/routes-B3JviaDV.js +0 -2
  245. package/.output/public/assets/shield-check-DoIiaaXU.js +0 -1
  246. package/.output/public/assets/signing-BKy9fIdB.js +0 -1
  247. package/.output/public/assets/styles-C5-su40i.css +0 -2
  248. package/.output/public/assets/table-bpgpQkPz.js +0 -1
  249. package/.output/public/assets/useOpenInteractionType-4dPacbwb.js +0 -1
  250. package/.output/public/assets/x-Cr_sPM8E.js +0 -1
  251. package/.output/server/_ssr/InsightsErrorAlert-DzDVdfgW.mjs +0 -169
  252. package/.output/server/_ssr/InsightsPageHeader-CZ2AaQGk.mjs +0 -372
  253. package/.output/server/_ssr/InsightsRolloutCard-DHB2iM4t.mjs +0 -693
  254. package/.output/server/_ssr/api-keys-api-KSH04dLB.mjs +0 -60
  255. package/.output/server/_ssr/api-keys-rpc-DBmgnW5o.mjs +0 -136
  256. package/.output/server/_ssr/api-rpc-DX5B6b-L.mjs +0 -1399
  257. package/.output/server/_ssr/auth.server-DF_shCeq.mjs +0 -47
  258. package/.output/server/_ssr/bundle-signing-rpc-C01EKpb-.mjs +0 -16
  259. package/.output/server/_ssr/bundle-signing.server-Co_zHdJx.mjs +0 -12
  260. package/.output/server/_ssr/card-gGh8F6kQ.mjs +0 -108
  261. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  262. package/.output/server/_ssr/config.server-NIXXHk-e.mjs +0 -368
  263. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  264. package/.output/server/_ssr/deleteBundle-C71v1GBr.mjs +0 -66
  265. package/.output/server/_ssr/getBundleChildren-CMGSGBWv.mjs +0 -50
  266. package/.output/server/_ssr/insights-oK2n5WBp.mjs +0 -406
  267. package/.output/server/_ssr/insights-recovery-rpc-CgeCAs2K.mjs +0 -16
  268. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
  269. package/.output/server/_ssr/insightsRecovery-DFkXGFjd.mjs +0 -108
  270. package/.output/server/_ssr/installations-DBmYuVBi.mjs +0 -29
  271. package/.output/server/_ssr/routes-C2RIYhyl.mjs +0 -37
  272. package/.output/server/_ssr/runtime.server-DLZpzphL.mjs +0 -438
  273. package/.output/server/_ssr/server-hgrXpJQY.mjs +0 -7468
  274. package/.output/server/_ssr/signing-bfOvUMSJ.mjs +0 -133
  275. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  276. package/.output/server/_tanstack-start-manifest_v-BAb_a_eC.mjs +0 -97
  277. package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
  278. package/src/components/features/insights/InsightsRolloutCard.tsx +0 -505
  279. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  280. package/src/lib/bundle-signing-api.ts +0 -14
  281. package/src/lib/bundle-signing-rpc.ts +0 -16
  282. package/src/lib/server/bundle-signing.server.ts +0 -23
  283. package/src/lib/server/deleteBundle.ts +0 -145
  284. package/src/lib/server/insightsRecovery.ts +0 -157
  285. package/src/routes/signing.tsx +0 -37
@@ -1,14 +1,21 @@
1
1
  import { Link } from "@tanstack/react-router";
2
- import { ChartNoAxesCombined } from "lucide-react";
2
+ 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";
7
+ import type { InsightsSearch } from "@/lib/insights-search";
8
+ import { cn } from "@/lib/utils";
6
9
 
7
10
  export function InsightsPageHeader({
8
11
  view,
12
+ overviewSearch,
9
13
  }: {
10
- readonly view: "overview" | "events";
14
+ readonly view: "overview" | "events" | "distribution";
15
+ readonly overviewSearch?: InsightsSearch;
11
16
  }) {
17
+ // A self-hosted server's admin API serves events, not the overview.
18
+ const overview = useConsoleFeature("insightsAnalytics");
12
19
  return (
13
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">
14
21
  <SidebarTrigger className="-ml-1 size-11 lg:size-7" />
@@ -19,28 +26,36 @@ export function InsightsPageHeader({
19
26
  />
20
27
  <h1 className="text-sm font-medium">Insights</h1>
21
28
  </div>
22
- <nav aria-label="Insights views" className="ml-auto flex gap-1">
23
- <Link
24
- aria-current={view === "overview" ? "page" : undefined}
25
- className={buttonVariants({
26
- className: "h-11 px-3 lg:h-8 lg:px-2.5",
27
- size: "lg",
28
- variant: view === "overview" ? "secondary" : "ghost",
29
- })}
30
- to="/insights"
31
- >
32
- Overview
33
- </Link>
29
+ <nav aria-label="Insights views" className="ml-auto flex gap-2">
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}
34
46
  <Link
35
47
  aria-current={view === "events" ? "page" : undefined}
36
- className={buttonVariants({
37
- className: "h-11 px-3 lg:h-8 lg:px-2.5",
38
- size: "lg",
39
- variant: view === "events" ? "secondary" : "ghost",
40
- })}
48
+ className={cn(
49
+ buttonVariants({
50
+ className: "h-11 px-3 lg:h-8 lg:px-2.5",
51
+ size: "lg",
52
+ variant: view === "events" ? "secondary" : "ghost",
53
+ }),
54
+ )}
41
55
  to="/installations"
42
56
  >
43
- Events
57
+ <ListIcon aria-hidden="true" data-icon="inline-start" />
58
+ All events
44
59
  </Link>
45
60
  </nav>
46
61
  </header>
@@ -0,0 +1,89 @@
1
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
2
+ import type { InsightsWindow } from "@/lib/insights-api";
3
+ import { usageMetrics, type UsageWindow } from "@/lib/insights-usage";
4
+
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,
11
+ }: {
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
+ }[];
19
+ }) {
20
+ return (
21
+ <Tabs
22
+ value={period}
23
+ onValueChange={(value) => {
24
+ const next = periods.find((item) => item.value === value);
25
+ if (next) onPeriodChange(next.value);
26
+ }}
27
+ >
28
+ <TabsList aria-label={label} className="min-h-11 sm:min-h-9">
29
+ {periods.map(({ value, name }) => (
30
+ <TabsTrigger
31
+ key={value}
32
+ value={value}
33
+ aria-label={name}
34
+ className="min-w-12 px-3"
35
+ >
36
+ {value}
37
+ </TabsTrigger>
38
+ ))}
39
+ </TabsList>
40
+ </Tabs>
41
+ );
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,6 +19,11 @@ 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,
@@ -22,24 +33,26 @@ import type {
22
33
  import {
23
34
  EventBundleTransition,
24
35
  EventTimestamp,
25
- EventTypeBadge,
36
+ EventTypeDetails,
37
+ HttpResponseDetails,
26
38
  useInsightsTimeFormat,
27
39
  } from "./EventDetails";
28
40
  import { EventHistoryList } from "./EventHistoryList";
29
41
  import { InsightsErrorAlert } from "./InsightsErrorAlert";
30
42
  import { InsightsPagination } from "./InsightsPagination";
31
43
 
32
- const getUserLabel = (event: {
33
- readonly username: string | null;
34
- readonly userId: string | null;
35
- }) => event.userId ?? event.username ?? "—";
44
+ const getUserLabel = (event: { readonly userId: string | null }) =>
45
+ event.userId ?? "—";
36
46
 
47
+ /** A download or a failed update leaves the installation on the bundle it ran. */
37
48
  const getLastKnownBundleId = (
38
49
  event: InsightsEventRow | InsightsInstallationViewRow,
39
50
  ) =>
40
51
  "lastKnownBundleId" in event
41
52
  ? event.lastKnownBundleId
42
- : (event.toBundleId ?? event.fromBundleId);
53
+ : event.type === "UPDATE_DOWNLOADED" || event.type === "UPDATE_FAILED"
54
+ ? event.fromBundleId
55
+ : event.toBundleId;
43
56
 
44
57
  export function InstallationHistoryCard({
45
58
  error,
@@ -49,6 +62,7 @@ export function InstallationHistoryCard({
49
62
  onPrevious,
50
63
  onRefresh,
51
64
  pageNumber,
65
+ retention = DEFAULT_INSIGHTS_RETENTION,
52
66
  selectedEvent,
53
67
  selectedInstallId,
54
68
  }: {
@@ -59,6 +73,8 @@ export function InstallationHistoryCard({
59
73
  readonly onNext: () => void;
60
74
  readonly onPrevious: () => void;
61
75
  readonly pageNumber: number;
76
+ /** How long the server keeps Insights rows. */
77
+ readonly retention?: InsightsRetentionDays;
62
78
  readonly selectedEvent:
63
79
  | InsightsEventRow
64
80
  | InsightsInstallationViewRow
@@ -70,6 +86,15 @@ export function InstallationHistoryCard({
70
86
  ? getLastKnownBundleId(selectedEvent)
71
87
  : undefined;
72
88
 
89
+ const pendingBundleId =
90
+ selectedEvent && "pendingBundleId" in selectedEvent
91
+ ? selectedEvent.pendingBundleId
92
+ : selectedEvent &&
93
+ "type" in selectedEvent &&
94
+ selectedEvent.type === "UPDATE_DOWNLOADED"
95
+ ? selectedEvent.toBundleId
96
+ : null;
97
+
73
98
  return (
74
99
  <Card className="@container min-h-0 min-w-0 shadow-sm">
75
100
  <CardHeader className="p-4 sm:p-6">
@@ -82,6 +107,14 @@ export function InstallationHistoryCard({
82
107
  : "Select an installation"}
83
108
  </h2>
84
109
  </CardTitle>
110
+ {selectedInstallId !== undefined ? (
111
+ <CardDescription>
112
+ Bundle changes and update failures are kept for{" "}
113
+ {formatDays(retention.rawDays)}; the latest report, for{" "}
114
+ {formatDays(retention.dailyDays)} after the installation last
115
+ reports.
116
+ </CardDescription>
117
+ ) : null}
85
118
  </div>
86
119
  {onRefresh && selectedInstallId !== undefined ? (
87
120
  <Button
@@ -138,7 +171,22 @@ export function InstallationHistoryCard({
138
171
  )}
139
172
  </dd>
140
173
  </div>
174
+ {pendingBundleId ? (
175
+ <div className="col-span-2 min-w-0">
176
+ <dt className="text-xs text-muted-foreground">
177
+ Downloaded · Pending apply
178
+ </dt>
179
+ <dd className="mt-1 text-xs">
180
+ <HashValueDisplay value={pendingBundleId} />
181
+ </dd>
182
+ </div>
183
+ ) : null}
141
184
  </dl>
185
+ {selectedEvent.httpResponse ? (
186
+ <div className="mt-4">
187
+ <HttpResponseDetails event={selectedEvent} />
188
+ </div>
189
+ ) : null}
142
190
  </section>
143
191
  <Separator />
144
192
  </>
@@ -192,7 +240,7 @@ export function InstallationHistoryCard({
192
240
  />
193
241
  </TableCell>
194
242
  <TableCell>
195
- <EventTypeBadge type={event.type} />
243
+ <EventTypeDetails event={event} />
196
244
  </TableCell>
197
245
  <TableCell className="whitespace-normal text-xs">
198
246
  <div className="flex flex-col gap-2">
@@ -226,8 +274,8 @@ export function InstallationHistoryCard({
226
274
  ) : (
227
275
  <>
228
276
  <div className="p-6 text-sm text-muted-foreground">
229
- {pageNumber > 1 || history?.nextCursor
230
- ? "No bundle changes on this page."
277
+ {pageNumber > 1
278
+ ? "No older bundle changes."
231
279
  : "No bundle changes recorded yet."}
232
280
  </div>
233
281
  {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,290 @@
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: "Downloads", field: "downloads", color: "text-primary/85" },
30
+ { label: "Active days", field: "activeDays", color: "text-success/85" },
31
+ { label: "Known crashes", field: "failedLaunches", color: "text-warning/85" },
32
+ ] as const;
33
+
34
+ const crashRate = (report: BundleActivityReport): string => {
35
+ const attempts = report.activeDays + report.failedLaunches;
36
+ return attempts === 0
37
+ ? "—"
38
+ : `${((report.failedLaunches / attempts) * 100).toFixed(2)}%`;
39
+ };
40
+
41
+ function ReleaseMetricsInfo() {
42
+ return (
43
+ <InsightsInfo label="About release insight metrics">
44
+ Active days count each installation once for each UTC day it launched this
45
+ release. Known crashes are reported OTA launch failures that triggered
46
+ recovery. The rate is known crashes divided by active days plus known
47
+ crashes.
48
+ </InsightsInfo>
49
+ );
50
+ }
51
+
52
+ export function BundleMovementSummary({
53
+ report,
54
+ loading = false,
55
+ input,
56
+ variant = "inline",
57
+ }: {
58
+ readonly report?: BundleActivityReport;
59
+ readonly loading?: boolean;
60
+ readonly input?: BundleActivityInput;
61
+ readonly variant?: "inline" | "card";
62
+ }) {
63
+ if (!report) {
64
+ return loading ? (
65
+ <Skeleton
66
+ aria-label="Loading release insights"
67
+ className={variant === "card" ? "h-16 w-full" : "h-4 w-40"}
68
+ />
69
+ ) : (
70
+ <span
71
+ aria-label="Release insights unavailable"
72
+ className="text-sm text-muted-foreground"
73
+ >
74
+ —
75
+ </span>
76
+ );
77
+ }
78
+ const metrics = (
79
+ <dl
80
+ className={cn(
81
+ variant === "card"
82
+ ? "grid grid-cols-[1fr_1fr_auto] gap-4"
83
+ : "flex min-w-[220px] flex-wrap items-baseline gap-x-3 gap-y-1",
84
+ )}
85
+ >
86
+ {activityStats.map(({ label, field, color }) => (
87
+ <div
88
+ key={field}
89
+ className={cn(
90
+ "flex",
91
+ variant === "card"
92
+ ? "min-w-0 flex-col gap-1"
93
+ : "items-baseline gap-1.5 whitespace-nowrap",
94
+ )}
95
+ >
96
+ <dt className="text-xs text-muted-foreground">{label}</dt>
97
+ <dd
98
+ className={cn(
99
+ "flex flex-wrap items-baseline gap-x-1.5 tabular-nums",
100
+ variant === "card"
101
+ ? "text-xl font-semibold"
102
+ : "text-sm font-medium",
103
+ report[field] > 0 ? color : "text-muted-foreground",
104
+ )}
105
+ >
106
+ {report[field].toLocaleString()}
107
+ {field === "failedLaunches" ? (
108
+ <span className="text-xs font-normal text-muted-foreground">
109
+ ({crashRate(report)})
110
+ </span>
111
+ ) : null}
112
+ </dd>
113
+ </div>
114
+ ))}
115
+ </dl>
116
+ );
117
+ return (
118
+ <div
119
+ className={cn(
120
+ "flex",
121
+ variant === "card" ? "flex-col gap-2" : "items-center gap-1",
122
+ )}
123
+ >
124
+ {input ? (
125
+ <Link
126
+ className="min-w-0 rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
127
+ to="/insights"
128
+ search={{
129
+ healthPlatform: input.platform,
130
+ healthChannel: input.channel,
131
+ releaseId: input.releaseId,
132
+ bundleWindow: "7d",
133
+ }}
134
+ >
135
+ {metrics}
136
+ </Link>
137
+ ) : (
138
+ metrics
139
+ )}
140
+ {variant === "inline" ? <ReleaseMetricsInfo /> : null}
141
+ </div>
142
+ );
143
+ }
144
+
145
+ /** The release's failed downloads and installs since its first report, and their rate. */
146
+ export function BundleDownloadFailures({
147
+ input,
148
+ }: {
149
+ readonly input: BundleActivityInput;
150
+ }) {
151
+ const query = useUpdateFailuresQuery({
152
+ platform: input.platform,
153
+ channel: input.channel,
154
+ releaseId: input.releaseId,
155
+ });
156
+ const report = query.data;
157
+ return (
158
+ <div className="flex items-center gap-1">
159
+ {report ? (
160
+ <dl>
161
+ <div className="flex flex-wrap items-baseline gap-x-1.5">
162
+ <dt className="text-xs text-muted-foreground">Download failures</dt>
163
+ <dd
164
+ className={cn(
165
+ "flex items-baseline gap-1.5 text-sm font-medium tabular-nums",
166
+ report.failedUpdates > 0
167
+ ? "text-warning/85"
168
+ : "text-muted-foreground",
169
+ )}
170
+ >
171
+ {report.failedUpdates.toLocaleString()}
172
+ <span className="text-xs font-normal text-muted-foreground">
173
+ ({formatRate(failureRate(report))})
174
+ </span>
175
+ </dd>
176
+ </div>
177
+ </dl>
178
+ ) : query.isPending ? (
179
+ <Skeleton aria-label="Loading download failures" className="h-4 w-40" />
180
+ ) : (
181
+ <span
182
+ aria-label="Download failures unavailable"
183
+ className="text-sm text-muted-foreground"
184
+ >
185
+ —
186
+ </span>
187
+ )}
188
+ <InsightsInfo label="About download failures">
189
+ Reported failures to download or install this release, since its first
190
+ report; each client reports one at most once a UTC day. The rate is the
191
+ failure rate of update attempts: failures divided by failures plus
192
+ download reports.
193
+ </InsightsInfo>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ /** The bundle detail's Insights card, a section of the release editor: activity and download failures. */
199
+ export function BundleInsightsSummary({
200
+ input,
201
+ }: {
202
+ readonly input: BundleActivityInput;
203
+ }) {
204
+ const query = useBundleActivityQuery([input]);
205
+ return (
206
+ <Card>
207
+ <CardHeader className="flex-row items-center justify-between px-4 pt-4 pb-3">
208
+ <CardTitle className="text-sm font-medium">Insights</CardTitle>
209
+ <ReleaseMetricsInfo />
210
+ </CardHeader>
211
+ <CardContent className="flex flex-col gap-3 px-4 pb-4">
212
+ <BundleMovementSummary
213
+ variant="card"
214
+ input={input}
215
+ loading={query.isFetching}
216
+ report={query.data?.[input.releaseId]}
217
+ />
218
+ <Separator />
219
+ <BundleDownloadFailures input={input} />
220
+ </CardContent>
221
+ <CardFooter className="px-4 pb-4">
222
+ <Link
223
+ className={buttonVariants({ variant: "outline", size: "sm" })}
224
+ to="/insights"
225
+ search={{
226
+ healthPlatform: input.platform,
227
+ healthChannel: input.channel,
228
+ releaseId: input.releaseId,
229
+ bundleWindow: adoptionWindow(input.releaseId),
230
+ healthChart: "adoption",
231
+ }}
232
+ >
233
+ <ChartLine aria-hidden="true" data-icon="inline-start" />
234
+ View adoption
235
+ </Link>
236
+ </CardFooter>
237
+ </Card>
238
+ );
239
+ }
240
+
241
+ /**
242
+ * The release editor's Insights card where the console reads no release
243
+ * activity, as through a self-hosted server's admin API: download failures
244
+ * alone. With release activity, its card shows them.
245
+ */
246
+ export function ReleaseFailuresSection({
247
+ input,
248
+ }: {
249
+ readonly input: BundleActivityInput;
250
+ }) {
251
+ const activity = useConsoleFeature("insightsAnalytics");
252
+ if (activity) return null;
253
+ return (
254
+ <Card>
255
+ <CardHeader className="px-4 pt-4 pb-3">
256
+ <CardTitle className="text-sm font-medium">Insights</CardTitle>
257
+ </CardHeader>
258
+ <CardContent className="px-4 pb-4">
259
+ <BundleDownloadFailures input={input} />
260
+ </CardContent>
261
+ </Card>
262
+ );
263
+ }
264
+
265
+ /**
266
+ * One release's activity in the Bundles page's Insights column. Every cell
267
+ * asks for the page's releases under one query key, so the page makes one
268
+ * request.
269
+ */
270
+ function ReleaseActivityCell({
271
+ releaseId,
272
+ releases,
273
+ }: {
274
+ readonly releaseId: string;
275
+ readonly releases: readonly BundleActivityInput[];
276
+ }) {
277
+ const query = useBundleActivityQuery(releases);
278
+ return (
279
+ <BundleMovementSummary
280
+ input={releases.find((release) => release.releaseId === releaseId)}
281
+ loading={query.isFetching}
282
+ report={query.data?.[releaseId]}
283
+ />
284
+ );
285
+ }
286
+
287
+ /** The Bundles page's Insights column: each release's downloads, active days, and crashes. */
288
+ export const releaseActivityColumn: ReleaseColumn = {
289
+ Cell: ReleaseActivityCell,
290
+ };
@@ -0,0 +1,65 @@
1
+ import { Skeleton } from "@/components/ui/skeleton";
2
+ import {
3
+ DEFAULT_INSIGHTS_RETENTION,
4
+ formatDays,
5
+ type InsightsRetentionDays,
6
+ } from "@/lib/insights-retention";
7
+ import { usageMetrics, type UsageWindow } from "@/lib/insights-usage";
8
+
9
+ import { EstimatedCount } from "./EstimatedCount";
10
+ import { InsightsInfo } from "./InsightsInfo";
11
+
12
+ export function ReportingDevicesSummary({
13
+ window,
14
+ count,
15
+ isPending,
16
+ partial,
17
+ retention = DEFAULT_INSIGHTS_RETENTION,
18
+ }: {
19
+ readonly window: UsageWindow;
20
+ readonly count: number | undefined;
21
+ readonly isPending: boolean;
22
+ readonly partial: boolean;
23
+ readonly retention?: InsightsRetentionDays;
24
+ }) {
25
+ const metric = usageMetrics[window];
26
+ return (
27
+ <div
28
+ aria-label={`${metric.label} over ${metric.period}`}
29
+ role="region"
30
+ className="flex items-center gap-3"
31
+ >
32
+ {isPending ? (
33
+ <Skeleton aria-label="Loading active users" className="h-9 w-12" />
34
+ ) : (
35
+ <p className="text-4xl font-semibold tracking-tight tabular-nums">
36
+ {count === undefined ? (
37
+ "—"
38
+ ) : partial ? (
39
+ `≥${count.toLocaleString()}`
40
+ ) : (
41
+ <EstimatedCount value={count} />
42
+ )}
43
+ </p>
44
+ )}
45
+ <div className="flex items-center gap-1 text-sm text-muted-foreground">
46
+ <span>{metric.label}</span>
47
+ <InsightsInfo label={`How ${metric.label} is counted`}>
48
+ Unique app installations that reported activity in the last{" "}
49
+ {metric.period}, including no-change reports. Filtered by platform,
50
+ channel, and app version. The chart counts unique reporting
51
+ installations per {metric.interval}. Unique counts are estimates,
52
+ typically within about 3%. On DynamoDB and Firestore the server
53
+ updates totals in batches: when the Console cannot write to the
54
+ database, totals can leave out up to a minute of the latest reports.
55
+ Hourly counts are kept for {formatDays(retention.rawDays)} and daily
56
+ counts for {formatDays(retention.dailyDays)}, so 12 months counts
57
+ whole UTC days.
58
+ {partial
59
+ ? " Only part of the history is available; the total is a lower bound."
60
+ : ""}
61
+ </InsightsInfo>
62
+ </div>
63
+ </div>
64
+ );
65
+ }