@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
@@ -1,61 +1,126 @@
1
- import type { ReleaseModel, ReleaseRow } from "@hot-updater/plugin-core";
1
+ import type {
2
+ HotUpdaterCoreApi,
3
+ ReleaseFilter,
4
+ ReleaseRow,
5
+ } from "@hot-updater/plugin-core";
2
6
 
3
7
  export interface ReleaseListInput {
4
- afterReleaseId?: string;
5
- beforeReleaseId?: string;
6
- bundleId?: string;
7
- channelId?: string;
8
- enabled?: boolean;
9
- limit: number;
10
- page?: number;
11
- platform?: "ios" | "android";
12
- targetAppVersion?: string;
8
+ /** One of the filter sets the release indexes serve. */
9
+ readonly filter: ReleaseFilter;
10
+ /** Releases older than this id: the next page. */
11
+ readonly beforeReleaseId?: string;
12
+ /** Releases newer than this id: the previous page. */
13
+ readonly afterReleaseId?: string;
14
+ readonly limit: number;
13
15
  }
14
16
 
15
17
  export interface ReleaseListResult {
16
- data: readonly ReleaseRow[];
17
- pagination: {
18
- currentPage: number;
19
- hasNextPage: boolean;
20
- hasPreviousPage: boolean;
21
- };
18
+ /** Newest first. */
19
+ readonly data: readonly ReleaseRow[];
20
+ /** `beforeReleaseId` of the next page, when there may be older releases. */
21
+ readonly next?: string;
22
+ /** `afterReleaseId` of the previous page, when there may be newer releases. */
23
+ readonly previous?: string;
22
24
  }
23
25
 
26
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
27
+ typeof value === "object" && value !== null;
28
+
29
+ const text = (value: unknown, name: string): string => {
30
+ if (typeof value !== "string" || value.length === 0) {
31
+ throw new Error(`Release filter ${name} is required.`);
32
+ }
33
+ return value;
34
+ };
35
+
36
+ const enabledOf = (value: unknown) => {
37
+ if (value === undefined) return {};
38
+ if (typeof value !== "boolean") {
39
+ throw new Error("Release filter enabled must be true or false.");
40
+ }
41
+ return { enabled: value };
42
+ };
43
+
44
+ /**
45
+ * A release list's filter: none, a channel with its platform (and enabled),
46
+ * a bundle, or a scope (and enabled). Those are the sets the release indexes
47
+ * serve, so any other combination is refused.
48
+ */
49
+ export const readReleaseFilter = (value: unknown): ReleaseFilter => {
50
+ if (value === undefined) return { kind: "all" };
51
+ if (!isRecord(value)) throw new Error("Invalid release filter.");
52
+ switch (value.kind) {
53
+ case "all":
54
+ return { kind: "all" };
55
+ case "channelPlatform": {
56
+ if (value.platform !== "ios" && value.platform !== "android") {
57
+ throw new Error("Release filter platform must be ios or android.");
58
+ }
59
+ return {
60
+ kind: "channelPlatform",
61
+ channelId: text(value.channelId, "channelId"),
62
+ platform: value.platform,
63
+ ...enabledOf(value.enabled),
64
+ };
65
+ }
66
+ case "bundle":
67
+ return { kind: "bundle", bundleId: text(value.bundleId, "bundleId") };
68
+ case "scope":
69
+ return {
70
+ kind: "scope",
71
+ scopeKey: text(value.scopeKey, "scopeKey"),
72
+ ...enabledOf(value.enabled),
73
+ };
74
+ default:
75
+ throw new Error("Invalid release filter.");
76
+ }
77
+ };
78
+
79
+ /**
80
+ * One page of releases, newest first, through the index the filter names:
81
+ * each page reads only its own rows. `next` is set for a full page, so the
82
+ * last page may lead to an empty one.
83
+ */
24
84
  export async function listReleases(
25
- releases: ReleaseModel,
85
+ core: Pick<HotUpdaterCoreApi, "listReleases">,
26
86
  input: ReleaseListInput,
27
87
  ): Promise<ReleaseListResult> {
28
- const page = input.page ?? 1;
29
- const rows = await releases.findMany({
30
- ...(input.afterReleaseId === undefined
31
- ? {}
32
- : { afterReleaseId: input.afterReleaseId }),
88
+ const { filter, limit } = input;
89
+ if (input.afterReleaseId !== undefined) {
90
+ // Newer releases, read oldest first from the cursor, then turned around.
91
+ const data = (
92
+ await core.listReleases({
93
+ filter,
94
+ order: "asc",
95
+ after: input.afterReleaseId,
96
+ limit,
97
+ })
98
+ ).reverse();
99
+ const first = data[0]?.id;
100
+ const last = data.at(-1)?.id;
101
+ return {
102
+ data,
103
+ ...(last === undefined ? {} : { next: last }),
104
+ ...(first !== undefined && data.length === limit
105
+ ? { previous: first }
106
+ : {}),
107
+ };
108
+ }
109
+ const data = await core.listReleases({
110
+ filter,
111
+ order: "desc",
112
+ limit,
33
113
  ...(input.beforeReleaseId === undefined
34
114
  ? {}
35
- : { beforeReleaseId: input.beforeReleaseId }),
36
- ...(input.bundleId === undefined ? {} : { bundleId: input.bundleId }),
37
- ...(input.channelId === undefined ? {} : { channelId: input.channelId }),
38
- ...(input.enabled === undefined ? {} : { enabled: input.enabled }),
39
- ...(input.platform === undefined ? {} : { platform: input.platform }),
40
- ...(input.targetAppVersion === undefined
41
- ? {}
42
- : { targetAppVersion: input.targetAppVersion }),
43
- limit: input.limit + 1,
115
+ : { after: input.beforeReleaseId }),
44
116
  });
45
- const isPreviousRequest = input.afterReleaseId !== undefined;
46
- const hasExtraRow = rows.length > input.limit;
47
- const data = isPreviousRequest
48
- ? hasExtraRow
49
- ? rows.slice(1)
50
- : rows
51
- : rows.slice(0, input.limit);
52
-
117
+ const first = data[0]?.id;
118
+ const last = data.at(-1)?.id;
53
119
  return {
54
120
  data,
55
- pagination: {
56
- currentPage: page,
57
- hasNextPage: isPreviousRequest || hasExtraRow,
58
- hasPreviousPage: page > 1,
59
- },
121
+ ...(last !== undefined && data.length === limit ? { next: last } : {}),
122
+ ...(first !== undefined && input.beforeReleaseId !== undefined
123
+ ? { previous: first }
124
+ : {}),
60
125
  };
61
126
  }
@@ -1,12 +1,12 @@
1
- import {
2
- isReleaseEligibleForCohort,
3
- NUMERIC_COHORT_SIZE,
4
- } from "@hot-updater/core";
5
1
  import type {
6
2
  CompiledReleaseCatalog,
7
- ReleaseCatalogModel,
3
+ HotUpdaterCoreApi,
8
4
  ReleaseRow,
9
5
  } from "@hot-updater/plugin-core";
6
+ import {
7
+ isReleaseEligibleForCohort,
8
+ NUMERIC_COHORT_SIZE,
9
+ } from "@hot-updater/protocol";
10
10
 
11
11
  export type ReleaseReachabilityRow = ReleaseRow & {
12
12
  readonly currentlyUnreachable: boolean;
@@ -63,15 +63,16 @@ export function collectCurrentlyReachableReleaseIds(
63
63
  return reachableReleaseIds;
64
64
  }
65
65
 
66
+ /** Marks each release its scope's compiled catalog no longer serves: one catalog read per scope. */
66
67
  export async function addReleaseReachability(
67
- releaseCatalogs: Pick<ReleaseCatalogModel, "findByScopeKey">,
68
+ core: Pick<HotUpdaterCoreApi, "getReleaseCatalogRow">,
68
69
  releases: readonly ReleaseRow[],
69
70
  ): Promise<readonly ReleaseReachabilityRow[]> {
70
71
  const scopeKeys = [...new Set(releases.map(({ scope_key }) => scope_key))];
71
72
  const reachableByScope = new Map(
72
73
  await Promise.all(
73
74
  scopeKeys.map(async (scopeKey) => {
74
- const row = await releaseCatalogs.findByScopeKey(scopeKey);
75
+ const row = await core.getReleaseCatalogRow(scopeKey);
75
76
  const reachableReleaseIds =
76
77
  row === null
77
78
  ? new Set<string>()
@@ -1,36 +1,125 @@
1
- import type {
2
- ApiKeyModel,
3
- BundleRepository,
4
- DatabasePlugin,
1
+ import {
2
+ type ConfiguredDatabase,
3
+ isRemoteDatabase,
5
4
  } from "@hot-updater/plugin-core";
5
+
6
+ import {
7
+ type ConsoleFeature,
8
+ ConsoleFeatureUnavailableError,
9
+ type ConsoleFeatures,
10
+ consoleFeatures,
11
+ resolveConsoleFeatures,
12
+ } from "../console-features";
6
13
  import {
7
- createInsightsProvider,
8
- type InsightsProvider,
9
- } from "@hot-updater/server";
10
-
11
- export function createRuntimeHotUpdater(config: {
12
- readonly database: DatabasePlugin;
13
- }): InsightsProvider {
14
- return createInsightsProvider(config.database.models.insights);
14
+ type ConsoleFeatureApis,
15
+ consoleFeatureApis,
16
+ } from "./console-feature-apis";
17
+
18
+ export type { ConsoleFeatureApis } from "./console-feature-apis";
19
+
20
+ export interface ConsoleRuntime {
21
+ /** Whether the console reaches a self-hosted server through its admin API. */
22
+ readonly remote: boolean;
23
+ /** The features on here, from the plugins the console's config lists. */
24
+ features(): Promise<ConsoleFeatures>;
25
+ /** What serves the features the console can serve here; {@link requireFeature} hands it out. */
26
+ readonly apis: Partial<ConsoleFeatureApis>;
15
27
  }
16
28
 
17
- export function createApiKeyStore(config: {
18
- readonly database: BundleRepository;
19
- }): ApiKeyModel | null {
20
- const models: unknown = Reflect.get(config.database, "models");
21
- const apiKeys: unknown =
22
- typeof models === "object" && models !== null
23
- ? Reflect.get(models, "apiKeys")
24
- : undefined;
25
- if (
26
- typeof apiKeys !== "object" ||
27
- apiKeys === null ||
28
- typeof Reflect.get(apiKeys, "create") !== "function" ||
29
- typeof Reflect.get(apiKeys, "findByHash") !== "function" ||
30
- typeof Reflect.get(apiKeys, "list") !== "function" ||
31
- typeof Reflect.get(apiKeys, "revoke") !== "function"
32
- ) {
33
- return null;
29
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
30
+ typeof value === "object" && value !== null;
31
+
32
+ /**
33
+ * Whether one of Hot Updater's own factories made `plugin`: the brand they
34
+ * set under a registered symbol, read here with a copy of the key so the
35
+ * console imports no package's internals. Assembly refuses any other plugin
36
+ * that takes one of their ids, so features key on it.
37
+ */
38
+ const isOfficialPlugin = (plugin: unknown): boolean =>
39
+ isRecord(plugin) &&
40
+ Reflect.get(plugin, Symbol.for("@hot-updater/server/official-plugin")) ===
41
+ true;
42
+
43
+ const featureIds = Object.keys(consoleFeatureApis) as ConsoleFeature[];
44
+
45
+ /**
46
+ * What serves each feature, built by its factory: over the plugins' APIs the
47
+ * console assembled, or over a self-hosted server's admin API. A feature
48
+ * without its plugin, or without a factory for the admin API, has none.
49
+ */
50
+ const featureApis = (
51
+ build: (feature: ConsoleFeature) => unknown,
52
+ ): Partial<ConsoleFeatureApis> =>
53
+ Object.fromEntries(
54
+ featureIds.flatMap((feature) => {
55
+ const api = build(feature);
56
+ return api === undefined ? [] : [[feature, api]];
57
+ }),
58
+ );
59
+
60
+ /**
61
+ * The console's features and what serves them, for the plugins its config
62
+ * lists, the plugins the server runs:
63
+ * - a self-hosted server (`standaloneRepository`) runs them itself; the
64
+ * console serves what that server's admin API serves of their features,
65
+ * and nothing that needs the database;
66
+ * - otherwise the console runs them over the database, as the server does,
67
+ * and serves their features through `api`, their APIs.
68
+ */
69
+ export const createConsoleRuntime = (config: {
70
+ readonly database: ConfiguredDatabase;
71
+ readonly plugins: readonly unknown[];
72
+ readonly api?: Readonly<Record<string, unknown>>;
73
+ }): ConsoleRuntime => {
74
+ const { database } = config;
75
+ // A feature's plugin is Hot Updater's own, which alone may take its id:
76
+ // the server's assembly refuses any other, and features key on the
77
+ // factory's mark, so another plugin under that id turns on nothing.
78
+ const official = new Set(
79
+ config.plugins
80
+ .filter(isOfficialPlugin)
81
+ .map((plugin) => (plugin as { id: string }).id),
82
+ );
83
+ if (isRemoteDatabase(database)) {
84
+ const { fetchAdmin } = database;
85
+ const features = resolveConsoleFeatures([...official], { remote: true });
86
+ return {
87
+ remote: true,
88
+ features: async () => features,
89
+ apis: featureApis((feature) =>
90
+ consoleFeatureApis[feature].remote?.(fetchAdmin),
91
+ ),
92
+ };
34
93
  }
35
- return apiKeys as ApiKeyModel;
36
- }
94
+ const api = config.api ?? {};
95
+ const features = resolveConsoleFeatures([...official], { remote: false });
96
+ return {
97
+ remote: false,
98
+ features: async () => features,
99
+ apis: featureApis((feature) => {
100
+ const { plugin } = consoleFeatures[feature];
101
+ const pluginApi = official.has(plugin) ? api[plugin] : undefined;
102
+ return pluginApi === undefined
103
+ ? undefined
104
+ : consoleFeatureApis[feature].local(pluginApi as never);
105
+ }),
106
+ };
107
+ };
108
+
109
+ /**
110
+ * What serves `feature`. Every feature's server function asks for it first,
111
+ * so a feature the console does not serve is refused with one error the
112
+ * client recognizes, before anything is read.
113
+ */
114
+ export const requireFeature = async <F extends ConsoleFeature>(
115
+ runtime: ConsoleRuntime,
116
+ feature: F,
117
+ ): Promise<ConsoleFeatureApis[F]> => {
118
+ const api = runtime.apis[feature];
119
+ if (api === undefined || !(await runtime.features())[feature]) {
120
+ throw new ConsoleFeatureUnavailableError(feature, {
121
+ remote: runtime.remote,
122
+ });
123
+ }
124
+ return api as ConsoleFeatureApis[F];
125
+ };
@@ -0,0 +1,31 @@
1
+ import {
2
+ readUpdateFailuresInput,
3
+ type UpdateFailuresInput,
4
+ type UpdateFailuresReport,
5
+ } from "../insights-failures";
6
+ import { insightsPeriodEnd, recoveryWindows } from "../insights-recovery";
7
+ import type { ConsoleInsightsReads } from "./adminInsights";
8
+
9
+ /** A release's or a channel's update failures over a period that ends with the current hour. */
10
+ export async function getUpdateFailuresReport(
11
+ reads: Pick<ConsoleInsightsReads, "getUpdateFailures">,
12
+ input: UpdateFailuresInput,
13
+ now = Date.now(),
14
+ ): Promise<UpdateFailuresReport> {
15
+ readUpdateFailuresInput(input);
16
+ const end = insightsPeriodEnd(now);
17
+ const timeRange =
18
+ input.window === undefined
19
+ ? undefined
20
+ : {
21
+ start: Math.max(0, end - recoveryWindows[input.window].durationMs),
22
+ end,
23
+ };
24
+ const failures = await reads.getUpdateFailures({
25
+ platform: input.platform,
26
+ channel: input.channel,
27
+ ...(input.releaseId === undefined ? {} : { releaseId: input.releaseId }),
28
+ ...(timeRange === undefined ? {} : { timeRange }),
29
+ });
30
+ return { ...failures, startMs: timeRange?.start ?? null, endMs: end };
31
+ }
@@ -9,22 +9,22 @@
9
9
  // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
10
 
11
11
  import { Route as rootRouteImport } from './routes/__root'
12
- import { Route as SigningRouteImport } from './routes/signing'
13
- import { Route as InstallationsRouteImport } from './routes/installations'
14
- import { Route as InsightsRouteImport } from './routes/insights'
15
- import { Route as ApiKeysRouteImport } from './routes/api-keys'
16
12
  import { Route as IndexRouteImport } from './routes/index'
13
+ import { Route as ApiKeysRouteImport } from './routes/api-keys'
14
+ import { Route as InsightsRouteImport } from './routes/insights'
15
+ import { Route as InstallationsRouteImport } from './routes/installations'
16
+ import { Route as InsightsDistributionRouteImport } from './routes/insights_.distribution'
17
17
  import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
18
18
  import { Route as ApiBundlesBundleIdDownloadRouteImport } from './routes/api/bundles/$bundleId/download'
19
19
 
20
- const SigningRoute = SigningRouteImport.update({
21
- id: '/signing',
22
- path: '/signing',
20
+ const IndexRoute = IndexRouteImport.update({
21
+ id: '/',
22
+ path: '/',
23
23
  getParentRoute: () => rootRouteImport,
24
24
  } as any)
25
- const InstallationsRoute = InstallationsRouteImport.update({
26
- id: '/installations',
27
- path: '/installations',
25
+ const ApiKeysRoute = ApiKeysRouteImport.update({
26
+ id: '/api-keys',
27
+ path: '/api-keys',
28
28
  getParentRoute: () => rootRouteImport,
29
29
  } as any)
30
30
  const InsightsRoute = InsightsRouteImport.update({
@@ -32,14 +32,14 @@ const InsightsRoute = InsightsRouteImport.update({
32
32
  path: '/insights',
33
33
  getParentRoute: () => rootRouteImport,
34
34
  } as any)
35
- const ApiKeysRoute = ApiKeysRouteImport.update({
36
- id: '/api-keys',
37
- path: '/api-keys',
35
+ const InstallationsRoute = InstallationsRouteImport.update({
36
+ id: '/installations',
37
+ path: '/installations',
38
38
  getParentRoute: () => rootRouteImport,
39
39
  } as any)
40
- const IndexRoute = IndexRouteImport.update({
41
- id: '/',
42
- path: '/',
40
+ const InsightsDistributionRoute = InsightsDistributionRouteImport.update({
41
+ id: '/insights_/distribution',
42
+ path: '/insights/distribution',
43
43
  getParentRoute: () => rootRouteImport,
44
44
  } as any)
45
45
  const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
@@ -59,7 +59,7 @@ export interface FileRoutesByFullPath {
59
59
  '/api-keys': typeof ApiKeysRoute
60
60
  '/insights': typeof InsightsRoute
61
61
  '/installations': typeof InstallationsRoute
62
- '/signing': typeof SigningRoute
62
+ '/insights/distribution': typeof InsightsDistributionRoute
63
63
  '/api/auth/$': typeof ApiAuthSplatRoute
64
64
  '/api/bundles/$bundleId/download': typeof ApiBundlesBundleIdDownloadRoute
65
65
  }
@@ -68,7 +68,7 @@ export interface FileRoutesByTo {
68
68
  '/api-keys': typeof ApiKeysRoute
69
69
  '/insights': typeof InsightsRoute
70
70
  '/installations': typeof InstallationsRoute
71
- '/signing': typeof SigningRoute
71
+ '/insights/distribution': typeof InsightsDistributionRoute
72
72
  '/api/auth/$': typeof ApiAuthSplatRoute
73
73
  '/api/bundles/$bundleId/download': typeof ApiBundlesBundleIdDownloadRoute
74
74
  }
@@ -78,7 +78,7 @@ export interface FileRoutesById {
78
78
  '/api-keys': typeof ApiKeysRoute
79
79
  '/insights': typeof InsightsRoute
80
80
  '/installations': typeof InstallationsRoute
81
- '/signing': typeof SigningRoute
81
+ '/insights_/distribution': typeof InsightsDistributionRoute
82
82
  '/api/auth/$': typeof ApiAuthSplatRoute
83
83
  '/api/bundles/$bundleId/download': typeof ApiBundlesBundleIdDownloadRoute
84
84
  }
@@ -89,7 +89,7 @@ export interface FileRouteTypes {
89
89
  | '/api-keys'
90
90
  | '/insights'
91
91
  | '/installations'
92
- | '/signing'
92
+ | '/insights/distribution'
93
93
  | '/api/auth/$'
94
94
  | '/api/bundles/$bundleId/download'
95
95
  fileRoutesByTo: FileRoutesByTo
@@ -98,7 +98,7 @@ export interface FileRouteTypes {
98
98
  | '/api-keys'
99
99
  | '/insights'
100
100
  | '/installations'
101
- | '/signing'
101
+ | '/insights/distribution'
102
102
  | '/api/auth/$'
103
103
  | '/api/bundles/$bundleId/download'
104
104
  id:
@@ -107,7 +107,7 @@ export interface FileRouteTypes {
107
107
  | '/api-keys'
108
108
  | '/insights'
109
109
  | '/installations'
110
- | '/signing'
110
+ | '/insights_/distribution'
111
111
  | '/api/auth/$'
112
112
  | '/api/bundles/$bundleId/download'
113
113
  fileRoutesById: FileRoutesById
@@ -117,25 +117,25 @@ export interface RootRouteChildren {
117
117
  ApiKeysRoute: typeof ApiKeysRoute
118
118
  InsightsRoute: typeof InsightsRoute
119
119
  InstallationsRoute: typeof InstallationsRoute
120
- SigningRoute: typeof SigningRoute
120
+ InsightsDistributionRoute: typeof InsightsDistributionRoute
121
121
  ApiAuthSplatRoute: typeof ApiAuthSplatRoute
122
122
  ApiBundlesBundleIdDownloadRoute: typeof ApiBundlesBundleIdDownloadRoute
123
123
  }
124
124
 
125
125
  declare module '@tanstack/react-router' {
126
126
  interface FileRoutesByPath {
127
- '/signing': {
128
- id: '/signing'
129
- path: '/signing'
130
- fullPath: '/signing'
131
- preLoaderRoute: typeof SigningRouteImport
127
+ '/': {
128
+ id: '/'
129
+ path: '/'
130
+ fullPath: '/'
131
+ preLoaderRoute: typeof IndexRouteImport
132
132
  parentRoute: typeof rootRouteImport
133
133
  }
134
- '/installations': {
135
- id: '/installations'
136
- path: '/installations'
137
- fullPath: '/installations'
138
- preLoaderRoute: typeof InstallationsRouteImport
134
+ '/api-keys': {
135
+ id: '/api-keys'
136
+ path: '/api-keys'
137
+ fullPath: '/api-keys'
138
+ preLoaderRoute: typeof ApiKeysRouteImport
139
139
  parentRoute: typeof rootRouteImport
140
140
  }
141
141
  '/insights': {
@@ -145,18 +145,18 @@ declare module '@tanstack/react-router' {
145
145
  preLoaderRoute: typeof InsightsRouteImport
146
146
  parentRoute: typeof rootRouteImport
147
147
  }
148
- '/api-keys': {
149
- id: '/api-keys'
150
- path: '/api-keys'
151
- fullPath: '/api-keys'
152
- preLoaderRoute: typeof ApiKeysRouteImport
148
+ '/installations': {
149
+ id: '/installations'
150
+ path: '/installations'
151
+ fullPath: '/installations'
152
+ preLoaderRoute: typeof InstallationsRouteImport
153
153
  parentRoute: typeof rootRouteImport
154
154
  }
155
- '/': {
156
- id: '/'
157
- path: '/'
158
- fullPath: '/'
159
- preLoaderRoute: typeof IndexRouteImport
155
+ '/insights_/distribution': {
156
+ id: '/insights_/distribution'
157
+ path: '/insights/distribution'
158
+ fullPath: '/insights/distribution'
159
+ preLoaderRoute: typeof InsightsDistributionRouteImport
160
160
  parentRoute: typeof rootRouteImport
161
161
  }
162
162
  '/api/auth/$': {
@@ -181,7 +181,7 @@ const rootRouteChildren: RootRouteChildren = {
181
181
  ApiKeysRoute: ApiKeysRoute,
182
182
  InsightsRoute: InsightsRoute,
183
183
  InstallationsRoute: InstallationsRoute,
184
- SigningRoute: SigningRoute,
184
+ InsightsDistributionRoute: InsightsDistributionRoute,
185
185
  ApiAuthSplatRoute: ApiAuthSplatRoute,
186
186
  ApiBundlesBundleIdDownloadRoute: ApiBundlesBundleIdDownloadRoute,
187
187
  }
package/src/router.tsx CHANGED
@@ -4,7 +4,7 @@ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query
4
4
  // Import the route tree selected by the local or hosted Vite integration.
5
5
  import { routeTree } from "virtual:hot-updater-console/route-tree";
6
6
 
7
- import { getInsightsScrollRestorationKey } from "./routes/-installations-search";
7
+ import { routeScrollRestorationKey } from "./lib/scroll-restoration";
8
8
 
9
9
  // Create a new router instance
10
10
  export const getRouter = () => {
@@ -14,7 +14,8 @@ export const getRouter = () => {
14
14
  context: { queryClient },
15
15
 
16
16
  scrollRestoration: true,
17
- getScrollRestorationKey: getInsightsScrollRestorationKey,
17
+ getScrollRestorationKey: (location): string =>
18
+ routeScrollRestorationKey(router, location),
18
19
  defaultPreloadStaleTime: 0,
19
20
  });
20
21
 
@@ -1,5 +1,23 @@
1
1
  import type { ParsedLocation } from "@tanstack/react-router";
2
2
 
3
+ import {
4
+ DEFAULT_EVENT_RANGE,
5
+ EVENT_RANGES,
6
+ type EventRange,
7
+ } from "../lib/insights-view";
8
+
9
+ export type InsightsPaginationState = {
10
+ readonly eventsBack?: readonly string[];
11
+ readonly searchBack?: readonly string[];
12
+ readonly historyBack?: readonly string[];
13
+ };
14
+
15
+ declare module "@tanstack/react-router" {
16
+ interface HistoryState {
17
+ insightsPagination?: InsightsPaginationState;
18
+ }
19
+ }
20
+
3
21
  const readCursor = (value: unknown): string | undefined =>
4
22
  typeof value === "string" && value.length > 0 ? value : undefined;
5
23
 
@@ -8,14 +26,19 @@ const readTimestamp = (value: unknown): number | undefined =>
8
26
  ? value
9
27
  : undefined;
10
28
 
11
- export function getInsightsScrollRestorationKey(location: ParsedLocation) {
12
- if (location.pathname === "/installations") {
13
- const search = validateInstallationsSearch(location.search);
14
- if (search.query === undefined && search.installId === undefined) {
15
- return `/installations?eventsBefore=${search.eventsBefore ?? "new"}&eventsCursor=${search.eventsCursor ?? "first"}`;
16
- }
17
- }
18
- return location.state.__TSR_key!;
29
+ const readEventRange = (value: unknown): EventRange | undefined =>
30
+ EVENT_RANGES.find((range) => range === value);
31
+
32
+ /**
33
+ * The installations route's scroll-restoration key: every visit to one page
34
+ * of the event list shares its scroll position, and a lookup keeps its own
35
+ * for each history entry.
36
+ */
37
+ export function installationsScrollRestorationKey(location: ParsedLocation) {
38
+ const search = validateInstallationsSearch(location.search);
39
+ return search.query === undefined && search.installId === undefined
40
+ ? `/installations?eventsBefore=${search.eventsBefore ?? "new"}&eventsRange=${search.eventsRange ?? DEFAULT_EVENT_RANGE}&eventsCursor=${search.eventsCursor ?? "first"}`
41
+ : location.state.__TSR_key!;
19
42
  }
20
43
 
21
44
  export function validateInstallationsSearch(search: Record<string, unknown>) {
@@ -25,6 +48,7 @@ export function validateInstallationsSearch(search: Record<string, unknown>) {
25
48
  typeof search.installId === "string" ? search.installId : undefined,
26
49
  eventsCursor: readCursor(search.eventsCursor),
27
50
  eventsBefore: readTimestamp(search.eventsBefore),
51
+ eventsRange: readEventRange(search.eventsRange),
28
52
  searchCursor: readCursor(search.searchCursor),
29
53
  historyCursor: readCursor(search.historyCursor),
30
54
  historyBefore: readTimestamp(search.historyBefore),