@hot-updater/console 1.0.0-rc.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/ConsoleFeatureUnavailable-jB4Loglh.js +1 -0
  3. package/.output/public/assets/EventDetails-B6yUsyNA.js +1 -0
  4. package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
  5. package/.output/public/assets/InsightsInfo-BNO0WznZ.js +1 -0
  6. package/.output/public/assets/InsightsPageHeader-B892oVml.js +1 -0
  7. package/.output/public/assets/NotFoundPage-DIqCpWa3.js +1 -0
  8. package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
  9. package/.output/public/assets/api-DCM0wr6O.js +1 -0
  10. package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
  11. package/.output/public/assets/api-keys-DVTL_--f.js +1 -0
  12. package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
  13. package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
  14. package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
  15. package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
  16. package/.output/public/assets/console-features-api-BH5m_PtQ.js +1 -0
  17. package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
  18. package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
  19. package/.output/public/assets/empty-CuT2UhFp.js +1 -0
  20. package/.output/public/assets/field-BBDpjIqu.js +1 -0
  21. package/.output/public/assets/index-6o51GkFe.js +11 -0
  22. package/.output/public/assets/input-group-ja_30qaF.js +1 -0
  23. package/.output/public/assets/insights-CmraDzQh.js +1 -0
  24. package/.output/public/assets/insights-CxZ2GCYV.js +2 -0
  25. package/.output/public/assets/insights-api-BigAU9sZ.js +1 -0
  26. package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
  27. package/.output/public/assets/insights-vNRCrfnS.js +42 -0
  28. package/.output/public/assets/insights_.distribution-ChOerfJt.js +1 -0
  29. package/.output/public/assets/insights_.distribution-D-e4iga8.js +1 -0
  30. package/.output/public/assets/insights_.distribution-DZtkibX1.js +2 -0
  31. package/.output/public/assets/installations-BazSJXE0.js +2 -0
  32. package/.output/public/assets/installations-ClHTqdAO.js +1 -0
  33. package/.output/public/assets/installations-DpKsBuHi.js +1 -0
  34. package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
  35. package/.output/public/assets/list-CgrYiadM.js +1 -0
  36. package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
  37. package/.output/public/assets/remote-config-BWwHOkCK.js +1 -0
  38. package/.output/public/assets/remote-config-Br56BODb.js +6 -0
  39. package/.output/public/assets/remote-config-CXOi0LRu.js +1 -0
  40. package/.output/public/assets/routes-B88Erkpk.js +2 -0
  41. package/.output/public/assets/routes-CfzsAleC.js +1 -0
  42. package/.output/public/assets/search-DaTDPW25.js +1 -0
  43. package/.output/public/assets/select-BZZBXMkr.js +1 -0
  44. package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
  45. package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
  46. package/.output/public/assets/styles-DTGEAJFv.css +2 -0
  47. package/.output/public/assets/switch-DZ1TPwx-.js +1 -0
  48. package/.output/public/assets/table-Gm4qRWwE.js +1 -0
  49. package/.output/public/assets/tabs-C7FOEjFC.js +1 -0
  50. package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
  51. package/.output/public/assets/useCompositeListItem-9E4pj_CH.js +1 -0
  52. package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
  53. package/.output/server/__23tanstack-start-server-fn-resolver-BpRlos5Q.mjs +170 -0
  54. package/.output/server/_libs/@base-ui/react+[...].mjs +4389 -4207
  55. package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
  56. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
  57. package/.output/server/_libs/lucide-react.mjs +221 -9
  58. package/.output/server/_libs/next-themes.mjs +1 -1
  59. package/.output/server/_libs/react-redux+[...].mjs +1 -1
  60. package/.output/server/_libs/recharts+[...].mjs +483 -901
  61. package/.output/server/_libs/sonner.mjs +1 -1
  62. package/.output/server/_libs/tanstack__history.mjs +68 -41
  63. package/.output/server/_libs/tanstack__query-core.mjs +2010 -309
  64. package/.output/server/_libs/tanstack__react-query.mjs +448 -21
  65. package/.output/server/_libs/verkit.mjs +102 -1
  66. package/.output/server/_ssr/BundleIdDisplay-x94o9rlU.mjs +13 -0
  67. package/.output/server/_ssr/ConsoleFeatureUnavailable-ldW8v8-q.mjs +58 -0
  68. package/.output/server/_ssr/EventDetails-Q8kfj9Nr.mjs +344 -0
  69. package/.output/server/_ssr/{HashValueDisplay-CyMmr0O9.mjs → HashValueDisplay-n1GholGS.mjs} +3 -3
  70. package/.output/server/_ssr/InsightsInfo-DuJFatQO.mjs +75 -0
  71. package/.output/server/_ssr/InsightsPageHeader-BSUX3Ye9.mjs +145 -0
  72. package/.output/server/_ssr/NotFoundPage-DZzOOzS3.mjs +37 -0
  73. package/.output/server/_ssr/{dialog-CKvQ4VIE.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
  74. package/.output/server/_ssr/{api-keys-CzZ7Ont4.mjs → api-keys-B4sYm8dM.mjs} +48 -9
  75. package/.output/server/_ssr/api-keys-CEnM9fqG.mjs +5 -0
  76. package/.output/server/_ssr/api-keys-rpc-BIFuUgMt.mjs +52 -0
  77. package/.output/server/_ssr/api-oZjRn9tP.mjs +186 -0
  78. package/.output/server/_ssr/api-rpc-BNiFRRK8.mjs +240 -0
  79. package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
  80. package/.output/server/_ssr/{auth-rpc-DH6Bhwcz.mjs → auth-rpc-DnZqYxyq.mjs} +6 -6
  81. package/.output/server/_ssr/auth.server-DFpyxRDd.mjs +5374 -0
  82. package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
  83. package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
  84. package/.output/server/_ssr/config.server-DskXCAyI.mjs +35 -0
  85. package/.output/server/_ssr/console-access-BJfAXknd.mjs +17 -0
  86. package/.output/server/_ssr/console-features-Oz0q20jJ.mjs +61 -0
  87. package/.output/server/_ssr/console-features-api-C9kiKvaV.mjs +43 -0
  88. package/.output/server/_ssr/console-features-rpc-P0GL7Z-R.mjs +19 -0
  89. package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
  90. package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
  91. package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
  92. package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
  93. package/.output/server/_ssr/databaseRows-CPphvbag.mjs +73 -0
  94. package/.output/server/_ssr/dialog-BbYDMjdC.mjs +84 -0
  95. package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-CbQbBIw-.mjs} +140 -7
  96. package/.output/server/_ssr/{downloadBundle-CI7JUKvC.mjs → downloadBundle-CZysjmNW.mjs} +10 -10
  97. package/.output/server/_ssr/empty-CVLGfxuu.mjs +60 -0
  98. package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
  99. package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
  100. package/.output/server/_ssr/field-asw83X0v.mjs +189 -0
  101. package/.output/server/_ssr/getBundleChildren-Cv9iz1As.mjs +22 -0
  102. package/.output/server/_ssr/{input-group-BrGSjz4S.mjs → input-group-CuYJHosb.mjs} +2 -2
  103. package/.output/server/_ssr/insights-B55FG5fm.mjs +14 -0
  104. package/.output/server/_ssr/insights-CWnHIFUV.mjs +5 -0
  105. package/.output/server/_ssr/insights-CpLpedFZ.mjs +2315 -0
  106. package/.output/server/_ssr/insights-api-sjws23WF.mjs +122 -0
  107. package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
  108. package/.output/server/_ssr/insights-failures-rpc-BvbWvqNf.mjs +36 -0
  109. package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
  110. package/.output/server/_ssr/insights-recovery-BWQhiZaF.mjs +28 -0
  111. package/.output/server/_ssr/insights-recovery-rpc-GiYXdkaY.mjs +32 -0
  112. package/.output/server/_ssr/{insights-rpc-DDKmIrpV.mjs → insights-rpc-DP_8Rmz4.mjs} +22 -14
  113. package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
  114. package/.output/server/_ssr/insights-usage-rpc-BiolgB8e.mjs +29 -0
  115. package/.output/server/_ssr/insights-usage-rpc-KStL0zOx.mjs +41 -0
  116. package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
  117. package/.output/server/_ssr/insights_.distribution-CVrnbP3R.mjs +7 -0
  118. package/.output/server/_ssr/insights_.distribution-D2HaNTAA.mjs +16 -0
  119. package/.output/server/_ssr/{insights_.distribution-cIsU-kHG.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
  120. package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
  121. package/.output/server/_ssr/{installations-C5anhyP0.mjs → installations-BSCmFq8Q.mjs} +279 -367
  122. package/.output/server/_ssr/installations-DD1g__Nc.mjs +69 -0
  123. package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
  124. package/.output/server/_ssr/plugins-BaD1slHx.mjs +2086 -0
  125. package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
  126. package/.output/server/_ssr/release-adoption-rpc-BsstqxUp.mjs +41 -0
  127. package/.output/server/_ssr/releaseAdoption-CtOFkwli.mjs +114 -0
  128. package/.output/server/_ssr/releaseManagement-DFMUnDF5.mjs +11 -0
  129. package/.output/server/_ssr/remote-config-C2Ee8lXO.mjs +5 -0
  130. package/.output/server/_ssr/remote-config-Dv1ccMo-.mjs +17 -0
  131. package/.output/server/_ssr/remote-config-of0lGQH8.mjs +2648 -0
  132. package/.output/server/_ssr/remote-config-rpc-BzX31mUS.mjs +115 -0
  133. package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
  134. package/.output/server/_ssr/{router-BuanSsRI.mjs → router-D8NuoJ_I.mjs} +107 -134
  135. package/.output/server/_ssr/{routes-DY-vqU6R.mjs → routes-DgvnFqDy.mjs} +195 -429
  136. package/.output/server/_ssr/routes-V8sDybGf.mjs +409 -0
  137. package/.output/server/_ssr/runtime.server-De0QVWc9.mjs +104 -0
  138. package/.output/server/_ssr/schema-BiAVyEj2.mjs +361 -0
  139. package/.output/server/_ssr/{select-DD2szs3Y.mjs → select-t3iXRwc9.mjs} +3 -3
  140. package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
  141. package/.output/server/_ssr/{createSsrRpc-CdlfejmI.mjs → sidebar-DJcErBMo.mjs} +107 -47
  142. package/.output/server/_ssr/ssr.mjs +1 -1
  143. package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
  144. package/.output/server/_ssr/switch-E-btwMGt.mjs +43 -0
  145. package/.output/server/_ssr/{table-CTj6n-4F.mjs → table-37Dd23TN.mjs} +2 -2
  146. package/.output/server/_ssr/tabs-DEFnTm95.mjs +45 -0
  147. package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
  148. package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C88s9jWF.mjs} +2 -2
  149. package/.output/server/_tanstack-start-manifest_v-55AGsehh.mjs +139 -0
  150. package/.output/server/index.mjs +309 -211
  151. package/LICENSE +13 -14
  152. package/README.md +80 -71
  153. package/lib/index.d.mts +23 -11
  154. package/lib/vite-internal.mjs +13 -3
  155. package/lib/vite.d.mts +2 -0
  156. package/package.json +14 -14
  157. package/src/components/AppSidebar.tsx +69 -74
  158. package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
  159. package/src/components/NotFoundPage.tsx +7 -7
  160. package/src/components/features/FeatureSlots.tsx +103 -0
  161. package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
  162. package/src/components/features/bundles/BundleMetadata.tsx +8 -5
  163. package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
  164. package/src/components/features/channels/ChannelManagementDialog.tsx +8 -6
  165. package/src/components/features/insights/AppUsage.tsx +41 -14
  166. package/src/components/features/insights/BuiltinBundle.tsx +36 -0
  167. package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
  168. package/src/components/features/insights/EstimatedCount.tsx +18 -0
  169. package/src/components/features/insights/EventDetails.tsx +390 -26
  170. package/src/components/features/insights/EventHistoryCard.tsx +111 -25
  171. package/src/components/features/insights/EventHistoryList.tsx +1 -1
  172. package/src/components/features/insights/FailureReports.tsx +407 -0
  173. package/src/components/features/insights/InsightsControls.tsx +240 -142
  174. package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
  175. package/src/components/features/insights/InsightsOverview.tsx +601 -403
  176. package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
  177. package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
  178. package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
  179. package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
  180. package/src/components/features/insights/ReleaseActivity.tsx +308 -0
  181. package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
  182. package/src/components/features/insights/RollbackButton.tsx +118 -0
  183. package/src/components/features/insights/UpdateFailures.tsx +382 -0
  184. package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
  185. package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
  186. package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
  187. package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
  188. package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
  189. package/src/components/features/remote-config/ParametersCard.tsx +254 -0
  190. package/src/components/features/remote-config/PreviewCard.tsx +284 -0
  191. package/src/components/features/remote-config/PublishDialog.tsx +325 -0
  192. package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
  193. package/src/components/features/remote-config/RuleSummary.tsx +21 -0
  194. package/src/components/features/remote-config/ValueInput.tsx +151 -0
  195. package/src/components/features/remote-config/VersionsCard.tsx +384 -0
  196. package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
  197. package/src/components/features/remote-config/useDateTimeText.ts +16 -0
  198. package/src/components/ui/sidebar.tsx +22 -29
  199. package/src/index.ts +26 -19
  200. package/src/lib/api-keys-api.ts +0 -28
  201. package/src/lib/api-keys-rpc.ts +27 -38
  202. package/src/lib/api-rpc.ts +125 -288
  203. package/src/lib/api.ts +15 -134
  204. package/src/lib/bundle-activity.ts +24 -4
  205. package/src/lib/console-access.ts +20 -0
  206. package/src/lib/console-features-api.ts +68 -0
  207. package/src/lib/console-features-rpc.ts +13 -0
  208. package/src/lib/console-features.ts +98 -0
  209. package/src/lib/insights-api.ts +56 -2
  210. package/src/lib/insights-errors.ts +64 -0
  211. package/src/lib/insights-failures-rpc.ts +36 -0
  212. package/src/lib/insights-failures.ts +130 -0
  213. package/src/lib/insights-recovery-rpc.ts +13 -17
  214. package/src/lib/insights-recovery.ts +12 -36
  215. package/src/lib/insights-retention.ts +21 -0
  216. package/src/lib/insights-rpc.ts +30 -12
  217. package/src/lib/insights-search.ts +50 -4
  218. package/src/lib/insights-usage-rpc.ts +15 -9
  219. package/src/lib/insights-usage.ts +41 -3
  220. package/src/lib/insights-view.ts +34 -5
  221. package/src/lib/release-adoption-api.ts +214 -0
  222. package/src/lib/release-adoption-rpc.ts +49 -0
  223. package/src/lib/release-adoption.ts +164 -0
  224. package/src/lib/remote-config-api.ts +119 -0
  225. package/src/lib/remote-config-draft.ts +666 -0
  226. package/src/lib/remote-config-rpc.ts +196 -0
  227. package/src/lib/scroll-restoration.ts +21 -0
  228. package/src/lib/server/auth.server.ts +13 -5
  229. package/src/lib/server/bundleActivity.ts +27 -28
  230. package/src/lib/server/config.server.ts +21 -76
  231. package/src/lib/server/console-feature-apis.ts +70 -0
  232. package/src/lib/server/console-runtime.server.ts +82 -38
  233. package/src/lib/server/downloadBundle.ts +14 -15
  234. package/src/lib/server/failureReports.ts +50 -0
  235. package/src/lib/server/getBundleChildren.ts +31 -122
  236. package/src/lib/server/insightsUsage.ts +52 -113
  237. package/src/lib/server/listReleases.ts +109 -44
  238. package/src/lib/server/releaseAdoption.ts +151 -0
  239. package/src/lib/server/releaseReachability.ts +8 -8
  240. package/src/lib/server/remoteConfig.ts +75 -0
  241. package/src/lib/server/runtime.server.ts +120 -31
  242. package/src/lib/server/updateFailures.ts +66 -0
  243. package/src/lib/use-dialog-target.ts +27 -0
  244. package/src/routeTree.gen.ts +44 -44
  245. package/src/router.tsx +3 -2
  246. package/src/routes/-installations-search.ts +32 -8
  247. package/src/routes/-releases-search.ts +76 -29
  248. package/src/routes/__root.tsx +7 -1
  249. package/src/routes/api/bundles/$bundleId/download.ts +2 -5
  250. package/src/routes/api-keys.tsx +5 -2
  251. package/src/routes/index.tsx +210 -152
  252. package/src/routes/insights.tsx +91 -23
  253. package/src/routes/insights_.distribution.tsx +50 -9
  254. package/src/routes/installations.tsx +97 -40
  255. package/src/routes/remote-config.tsx +42 -0
  256. package/src/styles.css +9 -0
  257. package/src/vite-internal.ts +15 -3
  258. package/src/vite.ts +2 -0
  259. package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
  260. package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
  261. package/.output/public/assets/api-CdUyUqzR.js +0 -42
  262. package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
  263. package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
  264. package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
  265. package/.output/public/assets/card-CYHs5FZv.js +0 -1
  266. package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
  267. package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
  268. package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
  269. package/.output/public/assets/dist-BR4GzeEu.js +0 -1
  270. package/.output/public/assets/field-B3CYDpjg.js +0 -1
  271. package/.output/public/assets/index-Cb_D1D6N.js +0 -10
  272. package/.output/public/assets/info-C3y9HNoU.js +0 -1
  273. package/.output/public/assets/input-group-nLZylhWF.js +0 -1
  274. package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
  275. package/.output/public/assets/insights-DS3iO7Up.js +0 -1
  276. package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
  277. package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
  278. package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
  279. package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
  280. package/.output/public/assets/installations-CkFW4fV9.js +0 -2
  281. package/.output/public/assets/installations-DxTaIcHD.js +0 -1
  282. package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
  283. package/.output/public/assets/list-Dqt53fWZ.js +0 -1
  284. package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
  285. package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
  286. package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
  287. package/.output/public/assets/routes-Cgtvspjg.js +0 -2
  288. package/.output/public/assets/routes-O2r2UUN8.js +0 -1
  289. package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
  290. package/.output/public/assets/signing-BgspC-xP.js +0 -1
  291. package/.output/public/assets/styles-DnrWuBwd.css +0 -2
  292. package/.output/public/assets/table-BuWQgxJJ.js +0 -1
  293. package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
  294. package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
  295. package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
  296. package/.output/server/__23tanstack-start-server-fn-resolver-DBRojHaG.mjs +0 -150
  297. package/.output/server/_ssr/InsightsPageHeader-mtxVuZKK.mjs +0 -64
  298. package/.output/server/_ssr/api-BfCgYlj5.mjs +0 -332
  299. package/.output/server/_ssr/api-keys-api-C3oeq01R.mjs +0 -60
  300. package/.output/server/_ssr/api-keys-rpc-CdPFdGOx.mjs +0 -136
  301. package/.output/server/_ssr/api-rpc-B2E1ZIG5.mjs +0 -1399
  302. package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
  303. package/.output/server/_ssr/bundle-signing-rpc-Di7-I6fE.mjs +0 -16
  304. package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
  305. package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
  306. package/.output/server/_ssr/card-Bemj0dpY.mjs +0 -108
  307. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
  308. package/.output/server/_ssr/config.server-B8WsT3s0.mjs +0 -368
  309. package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
  310. package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
  311. package/.output/server/_ssr/field-BrQ2u0fT.mjs +0 -74
  312. package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
  313. package/.output/server/_ssr/insights-BwQ-wF2a.mjs +0 -10
  314. package/.output/server/_ssr/insights-gr1I_dJU.mjs +0 -938
  315. package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
  316. package/.output/server/_ssr/insights-recovery-rpc-DOzxfYfv.mjs +0 -37
  317. package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
  318. package/.output/server/_ssr/insights-usage-rpc-4ImOq_0p.mjs +0 -184
  319. package/.output/server/_ssr/insights-usage-rpc-BUruODjE.mjs +0 -24
  320. package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
  321. package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
  322. package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
  323. package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
  324. package/.output/server/_ssr/insights_.distribution-VmHuikRE.mjs +0 -10
  325. package/.output/server/_ssr/installations-HXMF_kKf.mjs +0 -29
  326. package/.output/server/_ssr/routes-Dru3IRj0.mjs +0 -37
  327. package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
  328. package/.output/server/_ssr/server-DJm72Kj-.mjs +0 -7468
  329. package/.output/server/_ssr/signing-H8Lg4lPe.mjs +0 -133
  330. package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
  331. package/.output/server/_tanstack-start-manifest_v-DsFcTLw_.mjs +0 -125
  332. package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
  333. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
  334. package/src/components/features/signing/BundleSigningPage.tsx +0 -139
  335. package/src/lib/bundle-signing-api.ts +0 -14
  336. package/src/lib/bundle-signing-rpc.ts +0 -16
  337. package/src/lib/server/bundle-signing.server.ts +0 -23
  338. package/src/lib/server/deleteBundle.ts +0 -145
  339. package/src/lib/server/insightsHistory.ts +0 -52
  340. package/src/lib/server/insightsRecovery.ts +0 -187
  341. package/src/routes/signing.tsx +0 -37
@@ -0,0 +1,666 @@
1
+ import type {
2
+ RemoteConfigCondition,
3
+ RemoteConfigParameter,
4
+ RemoteConfigParameterValue,
5
+ RemoteConfigRule,
6
+ RemoteConfigRuleType,
7
+ RemoteConfigTemplate,
8
+ RemoteConfigValueType,
9
+ } from "@hot-updater/server/plugins";
10
+
11
+ /**
12
+ * The edits the Remote Config page makes to a draft template before a
13
+ * publish. Each returns a new template; the server validates the whole one
14
+ * again when it is published.
15
+ */
16
+
17
+ export type {
18
+ RemoteConfigCondition,
19
+ RemoteConfigParameter,
20
+ RemoteConfigParameterValue,
21
+ RemoteConfigRule,
22
+ RemoteConfigRuleType,
23
+ RemoteConfigTemplate,
24
+ RemoteConfigValueType,
25
+ };
26
+
27
+ export const EMPTY_TEMPLATE: RemoteConfigTemplate = {
28
+ conditions: [],
29
+ parameters: {},
30
+ };
31
+
32
+ export const VALUE_TYPE_LABELS: Readonly<
33
+ Record<RemoteConfigValueType, string>
34
+ > = {
35
+ STRING: "String",
36
+ NUMBER: "Number",
37
+ BOOLEAN: "Boolean",
38
+ JSON: "JSON",
39
+ };
40
+
41
+ export const RULE_TYPE_LABELS: Readonly<Record<RemoteConfigRuleType, string>> =
42
+ {
43
+ platform: "Platform",
44
+ channel: "Channel",
45
+ appVersion: "App version",
46
+ percent: "Percentage of installs",
47
+ cohort: "Cohort",
48
+ fingerprint: "Fingerprint",
49
+ dateTime: "Date and time",
50
+ };
51
+
52
+ /**
53
+ * A parameter's value for a condition. Condition names come from users, so
54
+ * `constructor` must name theirs, never Object.prototype's.
55
+ */
56
+ export const conditionalValueFor = (
57
+ values: Readonly<Record<string, RemoteConfigParameterValue>> | undefined,
58
+ name: string,
59
+ ): RemoteConfigParameterValue | undefined =>
60
+ values !== undefined && Object.hasOwn(values, name)
61
+ ? values[name]
62
+ : undefined;
63
+
64
+ /** Whether two templates publish the same thing. */
65
+ export const isSameTemplate = (
66
+ left: RemoteConfigTemplate,
67
+ right: RemoteConfigTemplate,
68
+ ): boolean => JSON.stringify(left) === JSON.stringify(right);
69
+
70
+ /** Adds a parameter, or replaces `previousKey`'s in its place. */
71
+ export const upsertParameter = (
72
+ template: RemoteConfigTemplate,
73
+ previousKey: string | null,
74
+ key: string,
75
+ parameter: RemoteConfigParameter,
76
+ ): RemoteConfigTemplate => {
77
+ const entries = Object.entries(template.parameters);
78
+ const index =
79
+ previousKey === null
80
+ ? -1
81
+ : entries.findIndex(([existing]) => existing === previousKey);
82
+ const next: [string, RemoteConfigParameter][] =
83
+ index === -1
84
+ ? [...entries.filter(([existing]) => existing !== key), [key, parameter]]
85
+ : entries.map((entry, position) =>
86
+ position === index ? [key, parameter] : entry,
87
+ );
88
+ return { ...template, parameters: Object.fromEntries(next) };
89
+ };
90
+
91
+ export const removeParameter = (
92
+ template: RemoteConfigTemplate,
93
+ key: string,
94
+ ): RemoteConfigTemplate => ({
95
+ ...template,
96
+ parameters: Object.fromEntries(
97
+ Object.entries(template.parameters).filter(
98
+ ([existing]) => existing !== key,
99
+ ),
100
+ ),
101
+ });
102
+
103
+ /** Every parameter's conditional values, with `rename` applied to their condition names. */
104
+ const mapConditionalValues = (
105
+ template: RemoteConfigTemplate,
106
+ rename: (name: string) => string | null,
107
+ ): RemoteConfigTemplate["parameters"] =>
108
+ Object.fromEntries(
109
+ Object.entries(template.parameters).map(([key, parameter]) => {
110
+ const { conditionalValues, ...rest } = parameter;
111
+ const kept = Object.entries(conditionalValues ?? {}).flatMap(
112
+ ([name, value]) => {
113
+ const next = rename(name);
114
+ return next === null ? [] : [[next, value] as const];
115
+ },
116
+ );
117
+ return [
118
+ key,
119
+ kept.length === 0
120
+ ? rest
121
+ : { ...rest, conditionalValues: Object.fromEntries(kept) },
122
+ ];
123
+ }),
124
+ );
125
+
126
+ /**
127
+ * Adds a condition last, or replaces `previousName`'s in its place; a
128
+ * rename carries the parameters' values for it to the new name.
129
+ */
130
+ export const upsertCondition = (
131
+ template: RemoteConfigTemplate,
132
+ previousName: string | null,
133
+ condition: RemoteConfigCondition,
134
+ ): RemoteConfigTemplate => {
135
+ if (previousName === null) {
136
+ return { ...template, conditions: [...template.conditions, condition] };
137
+ }
138
+ return {
139
+ conditions: template.conditions.map((existing) =>
140
+ existing.name === previousName ? condition : existing,
141
+ ),
142
+ parameters:
143
+ previousName === condition.name
144
+ ? template.parameters
145
+ : mapConditionalValues(template, (name) =>
146
+ name === previousName ? condition.name : name,
147
+ ),
148
+ };
149
+ };
150
+
151
+ /** Removes a condition and every parameter's value for it. */
152
+ export const removeCondition = (
153
+ template: RemoteConfigTemplate,
154
+ name: string,
155
+ ): RemoteConfigTemplate => ({
156
+ conditions: template.conditions.filter(
157
+ (condition) => condition.name !== name,
158
+ ),
159
+ parameters: mapConditionalValues(template, (existing) =>
160
+ existing === name ? null : existing,
161
+ ),
162
+ });
163
+
164
+ /** Moves a condition up (`-1`) or down (`1`) in priority. */
165
+ export const moveCondition = (
166
+ template: RemoteConfigTemplate,
167
+ index: number,
168
+ offset: -1 | 1,
169
+ ): RemoteConfigTemplate => {
170
+ const target = index + offset;
171
+ if (target < 0 || target >= template.conditions.length) return template;
172
+ const conditions = [...template.conditions];
173
+ [conditions[index], conditions[target]] = [
174
+ conditions[target]!,
175
+ conditions[index]!,
176
+ ];
177
+ return { ...template, conditions };
178
+ };
179
+
180
+ /** The parameters that give a condition a value. */
181
+ export const parametersUsing = (
182
+ template: RemoteConfigTemplate,
183
+ name: string,
184
+ ): string[] =>
185
+ Object.entries(template.parameters)
186
+ .filter(
187
+ ([, parameter]) =>
188
+ conditionalValueFor(parameter.conditionalValues, name) !== undefined,
189
+ )
190
+ .map(([key]) => key);
191
+
192
+ const listText = (values: readonly string[], limit = 3): string =>
193
+ values.length <= limit
194
+ ? values.join(", ")
195
+ : `${values.slice(0, limit).join(", ")} +${values.length - limit}`;
196
+
197
+ const percentText = (value: number) =>
198
+ Number.isInteger(value) ? String(value) : value.toFixed(1);
199
+
200
+ /** Formats an instant for the page, such as `Oct 8, 2026, 10:00 GMT+9`. */
201
+ export type DateTimeText = (iso: string) => string;
202
+
203
+ /** Dates in English with their offset, as Insights shows them. */
204
+ export const createDateTimeText = (timeZone: string): DateTimeText => {
205
+ const format = new Intl.DateTimeFormat("en", {
206
+ timeZone,
207
+ // "UTC" for UTC, an offset such as "GMT+9" for any other zone.
208
+ timeZoneName: timeZone === "UTC" ? "short" : "shortOffset",
209
+ year: "numeric",
210
+ month: "short",
211
+ day: "numeric",
212
+ hour: "2-digit",
213
+ minute: "2-digit",
214
+ hourCycle: "h23",
215
+ });
216
+ return (iso) => format.format(new Date(iso));
217
+ };
218
+
219
+ /** The server renders in UTC; the page switches to the viewer's zone once mounted. */
220
+ const UTC_DATE_TIME_TEXT = createDateTimeText("UTC");
221
+
222
+ /** The quick choices for a rule's start, as UTC instants. */
223
+ export const startPresets = (
224
+ now: Date = new Date(),
225
+ ): readonly { readonly label: string; readonly iso: string }[] => {
226
+ const nextHour = new Date(now);
227
+ nextHour.setMinutes(60, 0, 0);
228
+ const tomorrow = new Date(now);
229
+ tomorrow.setDate(tomorrow.getDate() + 1);
230
+ tomorrow.setHours(9, 0, 0, 0);
231
+ const minute = new Date(now);
232
+ minute.setSeconds(0, 0);
233
+ return [
234
+ { label: "Now", iso: minute.toISOString() },
235
+ { label: "Next hour", iso: nextHour.toISOString() },
236
+ { label: "Tomorrow 09:00", iso: tomorrow.toISOString() },
237
+ ];
238
+ };
239
+
240
+ /** The quick choices for a rule's end, counted from its start or from now. */
241
+ export const endPresets = (
242
+ from: string | undefined,
243
+ now: Date = new Date(),
244
+ ): readonly { readonly label: string; readonly iso: string }[] => {
245
+ const start = from === undefined ? now : new Date(from);
246
+ const after = (days: number) => {
247
+ const end = new Date(start);
248
+ end.setDate(end.getDate() + days);
249
+ end.setSeconds(0, 0);
250
+ return end.toISOString();
251
+ };
252
+ return [
253
+ { label: "+1 day", iso: after(1) },
254
+ { label: "+1 week", iso: after(7) },
255
+ ];
256
+ };
257
+
258
+ /** An instant as a `datetime-local` input shows it, in the viewer's time zone. */
259
+ export const toLocalDateTimeInput = (iso: string | undefined): string => {
260
+ if (iso === undefined) return "";
261
+ const date = new Date(iso);
262
+ const pad = (value: number) => String(value).padStart(2, "0");
263
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
264
+ };
265
+
266
+ /** A `datetime-local` input's value, in the viewer's time zone, as a UTC instant. */
267
+ export const fromLocalDateTimeInput = (value: string): string | undefined => {
268
+ if (value.length === 0) return undefined;
269
+ const ms = new Date(value).getTime();
270
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
271
+ };
272
+
273
+ /** A rule in a few words, such as `Channel is beta, qa`. */
274
+ export const describeRule = (
275
+ rule: RemoteConfigRule,
276
+ dateTimeText: DateTimeText = UTC_DATE_TIME_TEXT,
277
+ ): string => {
278
+ switch (rule.type) {
279
+ case "platform":
280
+ return `Platform is ${listText(
281
+ rule.platforms.map((platform) =>
282
+ platform === "ios" ? "iOS" : "Android",
283
+ ),
284
+ )}`;
285
+ case "channel":
286
+ return `Channel is ${listText(rule.channels)}`;
287
+ case "appVersion":
288
+ return `App version ${rule.range}`;
289
+ case "cohort":
290
+ return `Cohort is ${listText(rule.cohorts)}`;
291
+ case "percent":
292
+ return rule.from === 0
293
+ ? `${percentText(rule.to)}% of installs`
294
+ : `${percentText(rule.from)}–${percentText(rule.to)}% of installs`;
295
+ case "fingerprint":
296
+ return `Fingerprint is ${listText(
297
+ rule.hashes.map((hash) => hash.slice(0, 8)),
298
+ )}`;
299
+ case "dateTime":
300
+ return rule.from !== undefined && rule.to !== undefined
301
+ ? `From ${dateTimeText(rule.from)} until ${dateTimeText(rule.to)}`
302
+ : rule.from !== undefined
303
+ ? `From ${dateTimeText(rule.from)}`
304
+ : `Until ${dateTimeText(rule.to ?? "")}`;
305
+ }
306
+ };
307
+
308
+ /** A value as the page shows it: its text, or the app's own default. */
309
+ export const describeValue = (value: RemoteConfigParameterValue): string =>
310
+ "value" in value
311
+ ? value.value.length === 0
312
+ ? '""'
313
+ : value.value
314
+ : "In-app default";
315
+
316
+ /** A short random seed, so a new percentage rule picks its own installs. */
317
+ export const createSeed = (): string => {
318
+ const bytes = crypto.getRandomValues(new Uint8Array(6));
319
+ return Array.from(bytes, (byte) => byte.toString(36).padStart(2, "0"))
320
+ .join("")
321
+ .slice(0, 8);
322
+ };
323
+
324
+ /** The parameter and condition keys that a draft adds, changes, or removes. */
325
+ export interface TemplateChanges {
326
+ readonly added: readonly string[];
327
+ readonly changed: readonly string[];
328
+ readonly removed: readonly string[];
329
+ }
330
+
331
+ const diffKeys = <T>(
332
+ base: Readonly<Record<string, T>>,
333
+ draft: Readonly<Record<string, T>>,
334
+ ): TemplateChanges => ({
335
+ added: Object.keys(draft).filter((key) => !Object.hasOwn(base, key)),
336
+ changed: Object.keys(draft).filter(
337
+ (key) =>
338
+ Object.hasOwn(base, key) &&
339
+ JSON.stringify(base[key]) !== JSON.stringify(draft[key]),
340
+ ),
341
+ removed: Object.keys(base).filter((key) => !Object.hasOwn(draft, key)),
342
+ });
343
+
344
+ /** What a publish changes, for the publish dialog. */
345
+ export const summarizeChanges = (
346
+ base: RemoteConfigTemplate,
347
+ draft: RemoteConfigTemplate,
348
+ ): {
349
+ readonly parameters: TemplateChanges;
350
+ readonly conditions: TemplateChanges;
351
+ /** The conditions' priority order changed. */
352
+ readonly reordered: boolean;
353
+ } => {
354
+ const byName = (template: RemoteConfigTemplate) =>
355
+ Object.fromEntries(
356
+ template.conditions.map((condition) => [condition.name, condition]),
357
+ );
358
+ const kept = (template: RemoteConfigTemplate, other: RemoteConfigTemplate) =>
359
+ template.conditions
360
+ .map(({ name }) => name)
361
+ .filter((name) => other.conditions.some((it) => it.name === name));
362
+ return {
363
+ parameters: diffKeys(base.parameters, draft.parameters),
364
+ conditions: diffKeys(byName(base), byName(draft)),
365
+ reordered: kept(base, draft).join("\n") !== kept(draft, base).join("\n"),
366
+ };
367
+ };
368
+
369
+ /** One thing a changed parameter or condition changes, before and after. */
370
+ export interface FieldChange {
371
+ readonly field: string;
372
+ /** Null for a field of a parameter or condition the draft adds. */
373
+ readonly before: string | null;
374
+ readonly after: string;
375
+ }
376
+
377
+ const NOT_SET = "Not set";
378
+
379
+ /**
380
+ * What changes in a parameter the draft changes, field by field, or what a
381
+ * parameter the draft adds sets, with `before` null.
382
+ */
383
+ export const describeParameterChange = (
384
+ before: RemoteConfigParameter | null,
385
+ after: RemoteConfigParameter,
386
+ ): readonly FieldChange[] => {
387
+ const changes: FieldChange[] = [];
388
+ const add = (field: string, from: string | null, to: string) => {
389
+ if (from === null ? to !== NOT_SET : from !== to) {
390
+ changes.push({ field, before: from, after: to });
391
+ }
392
+ };
393
+ add(
394
+ "Type",
395
+ before === null ? null : VALUE_TYPE_LABELS[before.valueType],
396
+ VALUE_TYPE_LABELS[after.valueType],
397
+ );
398
+ add(
399
+ "Default",
400
+ before === null ? null : describeValue(before.defaultValue),
401
+ describeValue(after.defaultValue),
402
+ );
403
+ const conditionNames = new Set([
404
+ ...Object.keys(before?.conditionalValues ?? {}),
405
+ ...Object.keys(after.conditionalValues ?? {}),
406
+ ]);
407
+ for (const name of conditionNames) {
408
+ const from = conditionalValueFor(before?.conditionalValues, name);
409
+ const to = conditionalValueFor(after.conditionalValues, name);
410
+ add(
411
+ `When ${name}`,
412
+ before === null
413
+ ? null
414
+ : from === undefined
415
+ ? NOT_SET
416
+ : describeValue(from),
417
+ to === undefined ? NOT_SET : describeValue(to),
418
+ );
419
+ }
420
+ add(
421
+ "Description",
422
+ before === null ? null : (before.description ?? NOT_SET),
423
+ after.description ?? NOT_SET,
424
+ );
425
+ return changes;
426
+ };
427
+
428
+ /**
429
+ * What changes in a condition the draft changes, its rules, or the rules of
430
+ * a condition the draft adds, with `before` null.
431
+ */
432
+ export const describeConditionChange = (
433
+ before: RemoteConfigCondition | null,
434
+ after: RemoteConfigCondition,
435
+ dateTimeText: DateTimeText = UTC_DATE_TIME_TEXT,
436
+ ): readonly FieldChange[] => {
437
+ const rules = (condition: RemoteConfigCondition) =>
438
+ condition.rules.map((rule) => describeRule(rule, dateTimeText)).join(" · ");
439
+ const to = rules(after);
440
+ if (before === null) return [{ field: "Rules", before: null, after: to }];
441
+ const from = rules(before);
442
+ return from === to ? [] : [{ field: "Rules", before: from, after: to }];
443
+ };
444
+
445
+ /** Unpublished edits, the published template they started from, and its version. */
446
+ export interface RemoteConfigDraft {
447
+ readonly baseVersion: number;
448
+ readonly base: RemoteConfigTemplate;
449
+ readonly template: RemoteConfigTemplate;
450
+ }
451
+
452
+ const DRAFT_STORAGE_KEY = "hot-updater:remote-config-draft";
453
+
454
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
455
+ typeof value === "object" && value !== null && !Array.isArray(value);
456
+
457
+ const isParameterValueShape = (value: unknown): boolean =>
458
+ isRecord(value) &&
459
+ (typeof value.value === "string" || value.useInAppDefault === true);
460
+
461
+ /** Whether a stored value has a template's shape, so the page can render it. */
462
+ const isTemplateShape = (value: unknown): value is RemoteConfigTemplate =>
463
+ isRecord(value) &&
464
+ isRecord(value.parameters) &&
465
+ Array.isArray(value.conditions) &&
466
+ Object.values(value.parameters).every(
467
+ (parameter) =>
468
+ isRecord(parameter) &&
469
+ Object.hasOwn(VALUE_TYPE_LABELS, String(parameter.valueType)) &&
470
+ isParameterValueShape(parameter.defaultValue) &&
471
+ (parameter.conditionalValues === undefined ||
472
+ (isRecord(parameter.conditionalValues) &&
473
+ Object.values(parameter.conditionalValues).every(
474
+ isParameterValueShape,
475
+ ))),
476
+ ) &&
477
+ value.conditions.every(
478
+ (condition) =>
479
+ isRecord(condition) &&
480
+ typeof condition.name === "string" &&
481
+ Array.isArray(condition.rules) &&
482
+ condition.rules.every(
483
+ (rule) =>
484
+ isRecord(rule) && Object.hasOwn(RULE_TYPE_LABELS, String(rule.type)),
485
+ ),
486
+ );
487
+
488
+ /**
489
+ * The draft this browser kept for a server whose active version is
490
+ * `published`, or null. Several projects' Consoles can share one origin, so
491
+ * a draft comes back only onto the version and template it started from.
492
+ */
493
+ export const readStoredDraft = (published: {
494
+ readonly version: number;
495
+ readonly template: RemoteConfigTemplate;
496
+ }): RemoteConfigDraft | null => {
497
+ try {
498
+ const raw = window.localStorage.getItem(DRAFT_STORAGE_KEY);
499
+ if (raw === null) return null;
500
+ const stored: unknown = JSON.parse(raw);
501
+ if (
502
+ !isRecord(stored) ||
503
+ stored.baseVersion !== published.version ||
504
+ !isTemplateShape(stored.base) ||
505
+ !isTemplateShape(stored.template) ||
506
+ !isSameTemplate(stored.base, published.template) ||
507
+ isSameTemplate(stored.template, published.template)
508
+ ) {
509
+ return null;
510
+ }
511
+ return {
512
+ baseVersion: published.version,
513
+ base: stored.base,
514
+ template: stored.template,
515
+ };
516
+ } catch {
517
+ return null;
518
+ }
519
+ };
520
+
521
+ /** Keeps the draft in this browser, or forgets it; false when the browser refuses. */
522
+ export const storeDraft = (draft: RemoteConfigDraft | null): boolean => {
523
+ try {
524
+ if (draft === null) window.localStorage.removeItem(DRAFT_STORAGE_KEY);
525
+ else window.localStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(draft));
526
+ return true;
527
+ } catch {
528
+ return false;
529
+ }
530
+ };
531
+
532
+ const PARAMETER_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/u;
533
+
534
+ /** Why a parameter key cannot be used, or null. */
535
+ export const parameterKeyError = (
536
+ key: string,
537
+ taken: readonly string[],
538
+ ): string | null => {
539
+ if (key.length === 0) return "Enter a key.";
540
+ if (!PARAMETER_KEY_PATTERN.test(key)) {
541
+ return "Start with a letter or _, then use letters, digits, and _.";
542
+ }
543
+ return taken.includes(key) ? "Another parameter uses this key." : null;
544
+ };
545
+
546
+ /** Why a value's text does not suit its type, or null. */
547
+ export const valueTextError = (
548
+ valueType: RemoteConfigValueType,
549
+ text: string,
550
+ ): string | null => {
551
+ if (valueType === "NUMBER") {
552
+ return text.trim().length > 0 && Number.isFinite(Number(text))
553
+ ? null
554
+ : "Enter a number.";
555
+ }
556
+ if (valueType === "JSON") {
557
+ try {
558
+ JSON.parse(text);
559
+ return null;
560
+ } catch {
561
+ return "Enter valid JSON.";
562
+ }
563
+ }
564
+ if (valueType === "BOOLEAN" && text !== "true" && text !== "false") {
565
+ return "Choose true or false.";
566
+ }
567
+ return null;
568
+ };
569
+
570
+ /**
571
+ * A name for a condition saved without one: its rules in a few words, kept
572
+ * within 100 characters and apart from the names in use.
573
+ */
574
+ export const suggestConditionName = (
575
+ rules: readonly RemoteConfigRule[],
576
+ taken: readonly string[],
577
+ dateTimeText?: DateTimeText,
578
+ ): string => {
579
+ const text = rules
580
+ .map((rule) => describeRule(rule, dateTimeText))
581
+ .join(" · ");
582
+ const base = text.length <= 100 ? text : `${text.slice(0, 99).trimEnd()}…`;
583
+ if (base.length === 0 || !taken.includes(base)) return base;
584
+ for (let copy = 2; ; copy += 1) {
585
+ const suffix = ` (${copy})`;
586
+ const name = `${base.slice(0, 100 - suffix.length)}${suffix}`;
587
+ if (!taken.includes(name)) return name;
588
+ }
589
+ };
590
+
591
+ /** Why a condition name cannot be used, or null. */
592
+ export const conditionNameError = (
593
+ name: string,
594
+ taken: readonly string[],
595
+ ): string | null => {
596
+ const trimmed = name.trim();
597
+ if (trimmed.length === 0) return "Enter a name.";
598
+ if (trimmed.length > 100) return "Use 100 characters or fewer.";
599
+ return taken.includes(trimmed) ? "Another condition uses this name." : null;
600
+ };
601
+
602
+ /** Splits a comma- or newline-separated list, without blanks or repeats. */
603
+ export const splitList = (text: string): string[] => [
604
+ ...new Set(
605
+ text
606
+ .split(/[\n,]/u)
607
+ .map((entry) => entry.trim())
608
+ .filter((entry) => entry.length > 0),
609
+ ),
610
+ ];
611
+
612
+ /** The problem with a rule, for its editor; null when the server would take it. */
613
+ export const ruleError = (rule: RemoteConfigRule): string | null => {
614
+ switch (rule.type) {
615
+ case "platform":
616
+ return rule.platforms.length === 0 ? "Choose a platform." : null;
617
+ case "channel":
618
+ return rule.channels.length === 0 ? "Enter a channel." : null;
619
+ case "appVersion":
620
+ return rule.range.trim().length === 0
621
+ ? "Enter a version range, such as >=1.4.0."
622
+ : null;
623
+ case "cohort":
624
+ return rule.cohorts.length === 0 ? "Enter a cohort." : null;
625
+ case "percent":
626
+ return rule.from >= rule.to ||
627
+ rule.from < 0 ||
628
+ rule.to > 100 ||
629
+ Math.abs(rule.to * 10 - Math.round(rule.to * 10)) > 1e-9 ||
630
+ Math.abs(rule.from * 10 - Math.round(rule.from * 10)) > 1e-9
631
+ ? "Use a range from 0 to 100 in steps of 0.1."
632
+ : null;
633
+ case "fingerprint":
634
+ return rule.hashes.length === 0 ? "Enter a fingerprint." : null;
635
+ case "dateTime":
636
+ if (rule.from === undefined && rule.to === undefined) {
637
+ return "Choose a start, an end, or both.";
638
+ }
639
+ return rule.from !== undefined &&
640
+ rule.to !== undefined &&
641
+ rule.from >= rule.to
642
+ ? "End after the start."
643
+ : null;
644
+ }
645
+ };
646
+
647
+ /** A new rule of a type, with sensible starting values. */
648
+ export const createRule = (type: RemoteConfigRuleType): RemoteConfigRule => {
649
+ switch (type) {
650
+ case "platform":
651
+ return { type, platforms: ["ios"] };
652
+ case "channel":
653
+ return { type, channels: [] };
654
+ case "appVersion":
655
+ return { type, range: "" };
656
+ case "cohort":
657
+ return { type, cohorts: [] };
658
+ case "percent":
659
+ return { type, seed: createSeed(), from: 0, to: 10 };
660
+ case "fingerprint":
661
+ return { type, hashes: [] };
662
+ case "dateTime":
663
+ // The start of the next hour, a time worth editing rather than now.
664
+ return { type, from: startPresets()[1]!.iso };
665
+ }
666
+ };