@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,68 @@
1
+ import type { InsightsCoverage } from "@hot-updater/server/plugins/insights";
2
+
3
+ import type { InsightsWindow } from "./insights-rpc";
4
+
5
+ export type RecoveryInput = {
6
+ readonly platform: "ios" | "android";
7
+ readonly channel: string;
8
+ readonly window: InsightsWindow;
9
+ readonly releaseId?: string;
10
+ };
11
+
12
+ export type RecoveryReport = {
13
+ readonly downloads: number;
14
+ /**
15
+ * Distinct installations that reported in the scope and period, or that
16
+ * launched the release; estimated.
17
+ */
18
+ readonly activeInstallations: number;
19
+ /**
20
+ * Installation-days: each installation once for each UTC day it launched,
21
+ * and once more on a day an update applied or recovered.
22
+ */
23
+ readonly activeDays: number;
24
+ readonly failedLaunches: number;
25
+ readonly points: readonly {
26
+ readonly startMs: number;
27
+ /** That UTC day's daily active installations. */
28
+ readonly dailyActiveInstallations: number;
29
+ readonly failedLaunches: number;
30
+ }[];
31
+ readonly startMs: number;
32
+ readonly endMs: number;
33
+ readonly measuredAtMs: number;
34
+ readonly coverage: InsightsCoverage;
35
+ };
36
+
37
+ export const recoveryWindows = {
38
+ "24h": { durationMs: 86_400_000, intervalMs: 3_600_000 },
39
+ "7d": { durationMs: 7 * 86_400_000, intervalMs: 6 * 3_600_000 },
40
+ "30d": { durationMs: 30 * 86_400_000, intervalMs: 86_400_000 },
41
+ } as const;
42
+
43
+ /**
44
+ * Where a period ends: the end of the current UTC hour, as the server's
45
+ * reporting overview ends. The counters keep whole hours, so this is the
46
+ * latest end that still counts a report received just now; every Insights
47
+ * card ends here, so their numbers cover the same hours.
48
+ */
49
+ export const insightsPeriodEnd = (now: number): number =>
50
+ Math.ceil(now / 3_600_000) * 3_600_000;
51
+
52
+ export function readRecoveryInput(input: RecoveryInput): RecoveryInput {
53
+ if (
54
+ !input ||
55
+ (input.platform !== "ios" && input.platform !== "android") ||
56
+ typeof input.channel !== "string" ||
57
+ !input.channel.trim() ||
58
+ input.channel.length > 1_024 ||
59
+ !Object.hasOwn(recoveryWindows, input.window) ||
60
+ (input.releaseId !== undefined &&
61
+ (typeof input.releaseId !== "string" ||
62
+ !input.releaseId.trim() ||
63
+ input.releaseId.length > 1_024))
64
+ ) {
65
+ throw new Error("Choose a platform, channel, and time window.");
66
+ }
67
+ return input;
68
+ }
@@ -0,0 +1,21 @@
1
+ /** How long the server's `insights()` keeps rows, in days. */
2
+ export interface InsightsRetentionDays {
3
+ /** Raw events and hourly totals. */
4
+ readonly rawDays: number;
5
+ /** Daily totals, distributions, and each installation's latest report. */
6
+ readonly dailyDays: number;
7
+ }
8
+
9
+ /** `insights()`'s defaults, for a server that does not report its periods. */
10
+ export const DEFAULT_INSIGHTS_RETENTION: InsightsRetentionDays = {
11
+ rawDays: 90,
12
+ dailyDays: 400,
13
+ };
14
+
15
+ export const formatDays = (days: number): string =>
16
+ `${days.toLocaleString("en")} ${days === 1 ? "day" : "days"}`;
17
+
18
+ /** Whether daily totals cover App usage's 12-month period: 52 weeks. */
19
+ export const keepsTwelveMonths = ({
20
+ dailyDays,
21
+ }: InsightsRetentionDays): boolean => dailyDays >= 364;
@@ -1,13 +1,13 @@
1
1
  import type {
2
- ReportingOverview,
3
2
  InsightsScope,
3
+ ReportingOverview,
4
4
  InsightsEventPageInput,
5
5
  InsightsInstallationEventPageInput,
6
6
  ActiveInstallationWindow,
7
- InsightsProvider,
8
- } from "@hot-updater/server";
7
+ } from "@hot-updater/server/plugins/insights";
9
8
  import { createServerFn } from "@tanstack/react-start";
10
9
 
10
+ import { consoleAccess } from "./console-access";
11
11
  import type {
12
12
  InsightsInstallationViewRow,
13
13
  InsightsViewPage,
@@ -30,10 +30,14 @@ type InstallationsPageInput = {
30
30
  readonly limit: number;
31
31
  };
32
32
 
33
- const runtime = async (): Promise<InsightsProvider> => {
34
- const { prepareConfig } = await import("./server/config.server");
35
- const { hotUpdater } = await prepareConfig();
36
- return hotUpdater;
33
+ /** The Insights reads, once the console's access check and the feature guard pass. */
34
+ const insightsReads = async () => {
35
+ const [{ prepareConfig }, { requireFeature }] = await Promise.all([
36
+ import("./server/config.server"),
37
+ import("./server/runtime.server"),
38
+ ]);
39
+ const { runtime } = await prepareConfig();
40
+ return requireFeature(runtime, "insights");
37
41
  };
38
42
 
39
43
  const readOverview = (input: InsightsOverviewInput) => input;
@@ -44,27 +48,41 @@ const readInstallationPage = (input: InstallationsPageInput) => input;
44
48
  const readInstallId = (input: { readonly installId: string }) => input;
45
49
 
46
50
  export const getReportingInstallationsRpc = createServerFn({ method: "GET" })
51
+ .middleware([consoleAccess])
47
52
  .validator(readOverview)
48
- .handler(async ({ data }) => (await runtime()).getReportingOverview(data));
53
+ .handler(async ({ data }) =>
54
+ (await insightsReads()).getReportingOverview(data),
55
+ );
49
56
 
50
57
  export const listInsightsEventsRpc = createServerFn({ method: "GET" })
58
+ .middleware([consoleAccess])
51
59
  .validator(readEventsPage)
52
- .handler(async ({ data }) => (await runtime()).listEvents(data));
60
+ .handler(async ({ data }) => (await insightsReads()).listEvents(data));
53
61
 
54
62
  export const listInsightsInstallationEventsRpc = createServerFn({
55
63
  method: "GET",
56
64
  })
65
+ .middleware([consoleAccess])
57
66
  .validator(readInstallationEventsPage)
58
- .handler(async ({ data }) => (await runtime()).listInstallationEvents(data));
67
+ .handler(async ({ data }) =>
68
+ (await insightsReads()).listInstallationEvents(data),
69
+ );
59
70
 
60
71
  export const getInsightsInstallationRpc = createServerFn({ method: "GET" })
72
+ .middleware([consoleAccess])
61
73
  .validator(readInstallId)
62
- .handler(async ({ data }) => (await runtime()).getInstallation(data));
74
+ .handler(async ({ data }) => (await insightsReads()).getInstallation(data));
75
+
76
+ /** How long the server's `insights()` keeps rows. */
77
+ export const getInsightsRetentionRpc = createServerFn({ method: "GET" })
78
+ .middleware([consoleAccess])
79
+ .handler(async () => (await insightsReads()).getRetention());
63
80
 
64
81
  export const findInsightsInstallationsRpc = createServerFn({ method: "GET" })
82
+ .middleware([consoleAccess])
65
83
  .validator(readInstallationPage)
66
84
  .handler(async ({ data }) => {
67
- const insights = await runtime();
85
+ const insights = await insightsReads();
68
86
  const install = data.cursor
69
87
  ? null
70
88
  : await insights.getInstallation({ installId: data.identity });
@@ -0,0 +1,58 @@
1
+ import type { InsightsWindow } from "./insights-api";
2
+ import type { AppUsageScope, UsageWindow } from "./insights-usage";
3
+
4
+ const readText = (value: unknown) =>
5
+ typeof value === "string" && value.trim().length > 0 && value.length <= 1024
6
+ ? value
7
+ : undefined;
8
+ const readWindow = (value: unknown): InsightsWindow | undefined =>
9
+ value === "24h" || value === "7d" || value === "30d" ? value : undefined;
10
+ const readUsageWindow = (value: unknown): UsageWindow | undefined =>
11
+ value === "12m" ? value : readWindow(value);
12
+
13
+ export type InsightsSearch = {
14
+ platform?: AppUsageScope["platform"];
15
+ channel?: string;
16
+ appVersion?: string;
17
+ window?: UsageWindow;
18
+ bundleWindow?: InsightsWindow;
19
+ healthPlatform?: "ios" | "android";
20
+ healthChannel?: string;
21
+ releaseId?: string;
22
+ };
23
+
24
+ export function validateInsightsSearch(
25
+ search: Record<string, unknown>,
26
+ ): InsightsSearch {
27
+ return {
28
+ platform:
29
+ search.platform === "all" ||
30
+ search.platform === "ios" ||
31
+ search.platform === "android"
32
+ ? search.platform
33
+ : undefined,
34
+ channel: readText(search.channel),
35
+ appVersion: readText(search.appVersion),
36
+ window: readUsageWindow(search.window),
37
+ bundleWindow: readWindow(search.bundleWindow),
38
+ healthPlatform:
39
+ search.healthPlatform === "ios" || search.healthPlatform === "android"
40
+ ? search.healthPlatform
41
+ : undefined,
42
+ healthChannel: readText(search.healthChannel),
43
+ releaseId: readText(search.releaseId),
44
+ };
45
+ }
46
+
47
+ export function validateDistributionSearch(search: Record<string, unknown>) {
48
+ return {
49
+ ...validateInsightsSearch(search),
50
+ version: readText(search.version),
51
+ page:
52
+ typeof search.page === "number" &&
53
+ Number.isSafeInteger(search.page) &&
54
+ search.page > 0
55
+ ? search.page
56
+ : undefined,
57
+ };
58
+ }
@@ -0,0 +1,29 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+
3
+ import { consoleAccess } from "./console-access";
4
+ import { readAppUsageInput, type AppUsageInput } from "./insights-usage";
5
+
6
+ export const getAppUsageReportRpc = createServerFn({ method: "GET" })
7
+ .middleware([consoleAccess])
8
+ .validator((input: AppUsageInput) => {
9
+ readAppUsageInput(input);
10
+ return {
11
+ platform: input.platform,
12
+ channel: input.channel,
13
+ appVersion: input.appVersion,
14
+ window: input.window,
15
+ };
16
+ })
17
+ .handler(async ({ data }) => {
18
+ const [{ prepareConfig }, { getAppUsageReport }, { requireFeature }] =
19
+ await Promise.all([
20
+ import("./server/config.server"),
21
+ import("./server/insightsUsage"),
22
+ import("./server/runtime.server"),
23
+ ]);
24
+ const { runtime } = await prepareConfig();
25
+ return getAppUsageReport(
26
+ await requireFeature(runtime, "insightsAnalytics"),
27
+ data,
28
+ );
29
+ });
@@ -0,0 +1,74 @@
1
+ export type AppUsageInput = {
2
+ readonly platform: "all" | "ios" | "android";
3
+ readonly channel: string;
4
+ readonly appVersion?: string;
5
+ readonly window: UsageWindow;
6
+ };
7
+ export type AppUsageScope = Omit<AppUsageInput, "window">;
8
+
9
+ export type UsageDistribution = {
10
+ readonly name: string;
11
+ readonly installations: number;
12
+ };
13
+
14
+ export type BundleDistribution = {
15
+ readonly appVersion: string;
16
+ readonly platform: "ios" | "android";
17
+ readonly releaseId: string | null;
18
+ readonly installations: number;
19
+ };
20
+
21
+ export type AppUsageReport = {
22
+ readonly bundleDistribution: readonly BundleDistribution[];
23
+ readonly activeInstallations: number;
24
+ readonly sinceMs: number;
25
+ /**
26
+ * Where the distribution starts: the start of the UTC day that contains the
27
+ * period's start. Latest reports are counted by UTC day, so the
28
+ * distribution covers every UTC day the period touches.
29
+ */
30
+ readonly distributionSinceMs: number;
31
+ readonly beforeReceivedAtMs: number;
32
+ readonly intervalMs: number;
33
+ readonly truncated: boolean;
34
+ readonly appVersions: readonly string[];
35
+ readonly versions: readonly UsageDistribution[];
36
+ readonly platforms: readonly UsageDistribution[];
37
+ readonly points: readonly {
38
+ readonly startMs: number;
39
+ readonly installations: number | null;
40
+ }[];
41
+ };
42
+
43
+ export const usageMetrics = {
44
+ "24h": { label: "DAU", period: "24 hours", interval: "hour" },
45
+ "7d": { label: "WAU", period: "7 days", interval: "6 hours" },
46
+ "30d": { label: "MAU", period: "30 days", interval: "day" },
47
+ "12m": { label: "YAU", period: "12 months", interval: "week" },
48
+ } as const;
49
+
50
+ /**
51
+ * An App usage period. Hourly counts are kept 90 days, so the 12-month
52
+ * period counts whole UTC days, from the daily counts kept 13 months.
53
+ */
54
+ export type UsageWindow = keyof typeof usageMetrics;
55
+
56
+ export function readAppUsageInput(input: AppUsageInput): AppUsageInput {
57
+ if (
58
+ !input ||
59
+ (input.platform !== "all" &&
60
+ input.platform !== "ios" &&
61
+ input.platform !== "android") ||
62
+ typeof input.channel !== "string" ||
63
+ !input.channel.trim() ||
64
+ input.channel.length > 1_024 ||
65
+ !Object.hasOwn(usageMetrics, input.window) ||
66
+ (input.appVersion !== undefined &&
67
+ (typeof input.appVersion !== "string" ||
68
+ !input.appVersion.trim() ||
69
+ input.appVersion.length > 1_024))
70
+ ) {
71
+ throw new Error("Choose App usage filters and a time window.");
72
+ }
73
+ return input;
74
+ }
@@ -1,4 +1,7 @@
1
- import type { EventHistoryRow, InstallationRow } from "@hot-updater/server";
1
+ import type {
2
+ EventHistoryRow,
3
+ InstallationRow,
4
+ } from "@hot-updater/server/plugins/insights";
2
5
 
3
6
  export type InsightsEventRow = EventHistoryRow;
4
7
  export type InsightsInstallationViewRow = InstallationRow;
@@ -9,7 +12,40 @@ export type InsightsViewPage<TRow> = {
9
12
  };
10
13
 
11
14
  export const outcomeLabels = {
15
+ downloaded: "Downloaded reports",
12
16
  applied: "Applied reports",
13
17
  recovered: "Recovered-from reports",
14
- adopted: "Adopted reports",
18
+ failed: "Failed update reports",
15
19
  } as const;
20
+
21
+ const DAY_MS = 86_400_000;
22
+
23
+ /** The time ranges an event list reads, shortest first. */
24
+ export const eventRanges = {
25
+ "24h": { period: "24 hours", durationMs: DAY_MS },
26
+ "7d": { period: "7 days", durationMs: 7 * DAY_MS },
27
+ "30d": { period: "30 days", durationMs: 30 * DAY_MS },
28
+ "90d": { period: "90 days", durationMs: 90 * DAY_MS },
29
+ } as const;
30
+
31
+ export type EventRange = keyof typeof eventRanges;
32
+
33
+ export const EVENT_RANGES = Object.keys(eventRanges) as readonly EventRange[];
34
+
35
+ export const DEFAULT_EVENT_RANGE: EventRange = "7d";
36
+
37
+ /** The next longer range, or undefined for the longest. */
38
+ export const widerEventRange = (range: EventRange): EventRange | undefined =>
39
+ EVENT_RANGES[EVENT_RANGES.indexOf(range) + 1];
40
+
41
+ /**
42
+ * The receipt interval [sinceMs, beforeReceivedAtMs) a range covers. The end
43
+ * stays fixed while the list pages, so every page reads the same interval.
44
+ */
45
+ export const eventRangeBounds = (
46
+ range: EventRange,
47
+ beforeReceivedAtMs: number,
48
+ ) => ({
49
+ beforeReceivedAtMs,
50
+ sinceMs: Math.max(0, beforeReceivedAtMs - eventRanges[range].durationMs),
51
+ });
@@ -0,0 +1,21 @@
1
+ import type { AnyRouter, ParsedLocation } from "@tanstack/react-router";
2
+
3
+ declare module "@tanstack/react-router" {
4
+ interface StaticDataRouteOption {
5
+ /**
6
+ * The route's scroll-restoration key for a location; without one, each
7
+ * history entry keeps its own scroll position.
8
+ */
9
+ readonly scrollRestorationKey?: (location: ParsedLocation) => string;
10
+ }
11
+ }
12
+
13
+ /** A location's scroll-restoration key: its route's own, else its history entry's. */
14
+ export const routeScrollRestorationKey = (
15
+ router: Pick<AnyRouter, "getMatchedRoutes">,
16
+ location: ParsedLocation,
17
+ ): string => {
18
+ const [, , foundRoute] = router.getMatchedRoutes(location.pathname);
19
+ const routeKey = foundRoute?.options.staticData?.scrollRestorationKey;
20
+ return routeKey?.(location) ?? location.state.__TSR_key!;
21
+ };
@@ -0,0 +1,124 @@
1
+ import type {
2
+ InsightsEventPageInput,
3
+ InsightsProvider,
4
+ InsightsUpdateFailures,
5
+ InsightsUpdateFailuresInput,
6
+ } from "@hot-updater/server/plugins/insights";
7
+
8
+ import { ConsoleFeatureUnavailableError } from "../console-features";
9
+ import type { InsightsRetentionDays } from "../insights-retention";
10
+
11
+ /** The Insights reads the console pages use, update failures, and how long rows are kept. */
12
+ export type ConsoleInsightsReads = Omit<
13
+ InsightsProvider,
14
+ "appendBundleEvent"
15
+ > & {
16
+ getRetention(): Promise<InsightsRetentionDays>;
17
+ getUpdateFailures(
18
+ input: InsightsUpdateFailuresInput,
19
+ ): Promise<InsightsUpdateFailures>;
20
+ };
21
+
22
+ /** A GET on a self-hosted server's admin handler. */
23
+ export type FetchAdmin = (path: string) => Promise<Response>;
24
+
25
+ /** The `error` a failed admin route answered with, or a message naming its status. */
26
+ const errorOf = async (response: Response, path: string): Promise<Error> => {
27
+ const body: unknown = await response.json().catch(() => null);
28
+ return new Error(
29
+ typeof body === "object" &&
30
+ body !== null &&
31
+ typeof Reflect.get(body, "error") === "string"
32
+ ? String(Reflect.get(body, "error"))
33
+ : `The server answered ${path.split("?")[0]} with ${response.status}.`,
34
+ );
35
+ };
36
+
37
+ const withQuery = (
38
+ path: string,
39
+ input: Readonly<Record<string, number | string | undefined>>,
40
+ ): string => {
41
+ const query = new URLSearchParams();
42
+ for (const [key, value] of Object.entries(input)) {
43
+ if (value !== undefined) query.set(key, String(value));
44
+ }
45
+ const value = query.toString();
46
+ return value.length === 0 ? path : `${path}?${value}`;
47
+ };
48
+
49
+ const eventQuery = ({
50
+ beforeReceivedAtMs,
51
+ sinceMs,
52
+ cursor,
53
+ limit,
54
+ }: Omit<InsightsEventPageInput, "bundle">) => ({
55
+ beforeReceivedAtMs,
56
+ sinceMs,
57
+ cursor,
58
+ limit,
59
+ });
60
+
61
+ /**
62
+ * A self-hosted server's Insights, read through the admin routes its
63
+ * `insights()` plugin serves.
64
+ */
65
+ export const createAdminInsightsReads = (
66
+ fetchAdmin: FetchAdmin,
67
+ ): ConsoleInsightsReads => {
68
+ const read = async <T>(path: string): Promise<T | null> => {
69
+ const response = await fetchAdmin(path);
70
+ if (response.status === 404) return null;
71
+ if (!response.ok) throw await errorOf(response, path);
72
+ return (await response.json()) as T;
73
+ };
74
+ // A route no plugin serves answers 404: the server runs without insights(),
75
+ // as after a restart without it once the console read its plugins.
76
+ const required = async <T>(path: string): Promise<T> => {
77
+ const value = await read<T>(path);
78
+ if (value === null) {
79
+ throw new ConsoleFeatureUnavailableError("insights", { remote: true });
80
+ }
81
+ return value;
82
+ };
83
+
84
+ return {
85
+ getReportingOverview: ({ platform, channel, window, bundleId }) =>
86
+ required(withQuery("/overview", { platform, channel, window, bundleId })),
87
+ listEvents: ({ bundle, ...input }) =>
88
+ required(
89
+ withQuery("/events", {
90
+ ...eventQuery(input),
91
+ ...(bundle === undefined
92
+ ? {}
93
+ : {
94
+ platform: bundle.platform,
95
+ channel: bundle.channel,
96
+ bundleId: bundle.bundleId,
97
+ outcome: bundle.outcome,
98
+ }),
99
+ }),
100
+ ),
101
+ listInstallationEvents: ({ installId, ...input }) =>
102
+ required(
103
+ withQuery(
104
+ `/installations/${encodeURIComponent(installId)}/events`,
105
+ eventQuery(input),
106
+ ),
107
+ ),
108
+ getInstallation: ({ installId }) =>
109
+ read(`/installations/${encodeURIComponent(installId)}`),
110
+ pageInstallationsByCurrentUserId: ({ userId, cursor, limit }) =>
111
+ required(withQuery("/installations", { userId, cursor, limit })),
112
+ getUpdateFailures: ({ platform, channel, releaseId, timeRange }) =>
113
+ required(
114
+ withQuery("/failures", {
115
+ platform,
116
+ channel,
117
+ releaseId,
118
+ start: timeRange?.start,
119
+ end: timeRange?.end,
120
+ }),
121
+ ),
122
+ getRetention: () => required("/retention"),
123
+ };
124
+ };
@@ -13,11 +13,19 @@ const accessError = (
13
13
  { status: access.status === "unauthenticated" ? 401 : 403 },
14
14
  );
15
15
 
16
- export const getConsoleAccess = async (
17
- request: Request,
18
- ): Promise<ConsoleAccess> => {
19
- const adapter = await getConsoleAuthAdapter();
20
- return adapter.getAccess(request);
16
+ // Server function middleware checks access before input, and prepareConfig
17
+ // checks again for its callers: one request asks the adapter once.
18
+ const accessByRequest = new WeakMap<Request, Promise<ConsoleAccess>>();
19
+
20
+ export const getConsoleAccess = (request: Request): Promise<ConsoleAccess> => {
21
+ let access = accessByRequest.get(request);
22
+ if (!access) {
23
+ access = getConsoleAuthAdapter().then((adapter) =>
24
+ adapter.getAccess(request),
25
+ );
26
+ accessByRequest.set(request, access);
27
+ }
28
+ return access;
21
29
  };
22
30
 
23
31
  export const getConsoleAuthProviders = async (
@@ -0,0 +1,37 @@
1
+ import type {
2
+ InsightsModel,
3
+ ReleaseReference,
4
+ } from "@hot-updater/server/plugins/insights";
5
+
6
+ import type { BundleActivityInput } from "../bundle-activity";
7
+
8
+ export async function getBundleActivity(
9
+ model: InsightsModel,
10
+ inputs: readonly BundleActivityInput[],
11
+ ) {
12
+ if (inputs.length === 0) return {};
13
+ const releases: ReleaseReference[] = inputs.map((input) => ({
14
+ releaseId: input.releaseId,
15
+ platform: input.platform,
16
+ channel: input.channel,
17
+ }));
18
+ const activity = await model.getReleaseActivity({ releases });
19
+ return Object.fromEntries(
20
+ activity.data.flatMap((item) =>
21
+ item.release === undefined
22
+ ? []
23
+ : [
24
+ [
25
+ item.release.releaseId,
26
+ {
27
+ downloads: item.metrics.downloads,
28
+ activeDays: item.metrics.launches,
29
+ failedLaunches: item.metrics.failedLaunches,
30
+ measuredAtMs: activity.measuredAtMs,
31
+ coverage: activity.coverage,
32
+ },
33
+ ],
34
+ ],
35
+ ),
36
+ );
37
+ }