@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/ConsoleFeatureUnavailable-jB4Loglh.js +1 -0
- package/.output/public/assets/EventDetails-B6yUsyNA.js +1 -0
- package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
- package/.output/public/assets/InsightsInfo-BNO0WznZ.js +1 -0
- package/.output/public/assets/InsightsPageHeader-B892oVml.js +1 -0
- package/.output/public/assets/NotFoundPage-DIqCpWa3.js +1 -0
- package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
- package/.output/public/assets/api-DCM0wr6O.js +1 -0
- package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
- package/.output/public/assets/api-keys-DVTL_--f.js +1 -0
- package/.output/public/assets/arrow-left-CnMYpqB7.js +1 -0
- package/.output/public/assets/arrow-up-right-BtffZA6e.js +1 -0
- package/.output/public/assets/chevron-right-BqD5wtFb.js +1 -0
- package/.output/public/assets/clipboard-DS9ucUQL.js +1 -0
- package/.output/public/assets/console-features-api-BH5m_PtQ.js +1 -0
- package/.output/public/assets/dialog-Bqbb7B0N.js +1 -0
- package/.output/public/assets/dist-LxKJB-Vu.js +1 -0
- package/.output/public/assets/empty-CuT2UhFp.js +1 -0
- package/.output/public/assets/field-BBDpjIqu.js +1 -0
- package/.output/public/assets/index-6o51GkFe.js +11 -0
- package/.output/public/assets/input-group-ja_30qaF.js +1 -0
- package/.output/public/assets/insights-CmraDzQh.js +1 -0
- package/.output/public/assets/insights-CxZ2GCYV.js +2 -0
- package/.output/public/assets/insights-api-BigAU9sZ.js +1 -0
- package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
- package/.output/public/assets/insights-vNRCrfnS.js +42 -0
- package/.output/public/assets/insights_.distribution-ChOerfJt.js +1 -0
- package/.output/public/assets/insights_.distribution-D-e4iga8.js +1 -0
- package/.output/public/assets/insights_.distribution-DZtkibX1.js +2 -0
- package/.output/public/assets/installations-BazSJXE0.js +2 -0
- package/.output/public/assets/installations-ClHTqdAO.js +1 -0
- package/.output/public/assets/installations-DpKsBuHi.js +1 -0
- package/.output/public/assets/key-round-wS_SIZ6b.js +1 -0
- package/.output/public/assets/list-CgrYiadM.js +1 -0
- package/.output/public/assets/refresh-cw-BYgoNCco.js +1 -0
- package/.output/public/assets/remote-config-BWwHOkCK.js +1 -0
- package/.output/public/assets/remote-config-Br56BODb.js +6 -0
- package/.output/public/assets/remote-config-CXOi0LRu.js +1 -0
- package/.output/public/assets/routes-B88Erkpk.js +2 -0
- package/.output/public/assets/routes-CfzsAleC.js +1 -0
- package/.output/public/assets/search-DaTDPW25.js +1 -0
- package/.output/public/assets/select-BZZBXMkr.js +1 -0
- package/.output/public/assets/sidebar-DNVyJPfX.js +3 -0
- package/.output/public/assets/smartphone-DpTFgtKf.js +1 -0
- package/.output/public/assets/styles-DTGEAJFv.css +2 -0
- package/.output/public/assets/switch-DZ1TPwx-.js +1 -0
- package/.output/public/assets/table-Gm4qRWwE.js +1 -0
- package/.output/public/assets/tabs-C7FOEjFC.js +1 -0
- package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
- package/.output/public/assets/useCompositeListItem-9E4pj_CH.js +1 -0
- package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-BpRlos5Q.mjs +170 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +4389 -4207
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +3802 -4022
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +140 -97
- package/.output/server/_libs/lucide-react.mjs +221 -9
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/react-redux+[...].mjs +1 -1
- package/.output/server/_libs/recharts+[...].mjs +483 -901
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_libs/tanstack__history.mjs +68 -41
- package/.output/server/_libs/tanstack__query-core.mjs +2010 -309
- package/.output/server/_libs/tanstack__react-query.mjs +448 -21
- package/.output/server/_libs/verkit.mjs +102 -1
- package/.output/server/_ssr/BundleIdDisplay-x94o9rlU.mjs +13 -0
- package/.output/server/_ssr/ConsoleFeatureUnavailable-ldW8v8-q.mjs +58 -0
- package/.output/server/_ssr/EventDetails-Q8kfj9Nr.mjs +344 -0
- package/.output/server/_ssr/{HashValueDisplay-CyMmr0O9.mjs → HashValueDisplay-n1GholGS.mjs} +3 -3
- package/.output/server/_ssr/InsightsInfo-DuJFatQO.mjs +75 -0
- package/.output/server/_ssr/InsightsPageHeader-BSUX3Ye9.mjs +145 -0
- package/.output/server/_ssr/NotFoundPage-DZzOOzS3.mjs +37 -0
- package/.output/server/_ssr/{dialog-CKvQ4VIE.mjs → alert-dialog-C43Doa0I.mjs} +6 -83
- package/.output/server/_ssr/{api-keys-CzZ7Ont4.mjs → api-keys-B4sYm8dM.mjs} +48 -9
- package/.output/server/_ssr/api-keys-CEnM9fqG.mjs +5 -0
- package/.output/server/_ssr/api-keys-rpc-BIFuUgMt.mjs +52 -0
- package/.output/server/_ssr/api-oZjRn9tP.mjs +186 -0
- package/.output/server/_ssr/api-rpc-BNiFRRK8.mjs +240 -0
- package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
- package/.output/server/_ssr/{auth-rpc-DH6Bhwcz.mjs → auth-rpc-DnZqYxyq.mjs} +6 -6
- package/.output/server/_ssr/auth.server-DFpyxRDd.mjs +5374 -0
- package/.output/server/_ssr/bundleActivity-M5gzBFNg.mjs +18 -0
- package/.output/server/_ssr/config-WVX4IxSR.mjs +15 -0
- package/.output/server/_ssr/config.server-DskXCAyI.mjs +35 -0
- package/.output/server/_ssr/console-access-BJfAXknd.mjs +17 -0
- package/.output/server/_ssr/console-features-Oz0q20jJ.mjs +61 -0
- package/.output/server/_ssr/console-features-api-C9kiKvaV.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-P0GL7Z-R.mjs +19 -0
- package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-Cm6Tn0_Y.mjs} +67 -56
- package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-RLR4H-Fn.mjs} +2 -2
- package/.output/server/_ssr/databaseConfig-C5SkAyUy.mjs +5 -0
- package/.output/server/_ssr/{databaseJsonValue-d6I3nAS4.mjs → databaseJsonValue-Csbuzc9Q.mjs} +28 -5
- package/.output/server/_ssr/databaseRows-CPphvbag.mjs +73 -0
- package/.output/server/_ssr/dialog-BbYDMjdC.mjs +84 -0
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-CbQbBIw-.mjs} +140 -7
- package/.output/server/_ssr/{downloadBundle-CI7JUKvC.mjs → downloadBundle-CZysjmNW.mjs} +10 -10
- package/.output/server/_ssr/empty-CVLGfxuu.mjs +60 -0
- package/.output/server/_ssr/{empty-plugin-adapters-B5QvGpEl.mjs → empty-plugin-adapters-CPtwX8a7.mjs} +1 -1
- package/.output/server/_ssr/failureReports-DKDhVsXA.mjs +33 -0
- package/.output/server/_ssr/field-asw83X0v.mjs +189 -0
- package/.output/server/_ssr/getBundleChildren-Cv9iz1As.mjs +22 -0
- package/.output/server/_ssr/{input-group-BrGSjz4S.mjs → input-group-CuYJHosb.mjs} +2 -2
- package/.output/server/_ssr/insights-B55FG5fm.mjs +14 -0
- package/.output/server/_ssr/insights-CWnHIFUV.mjs +5 -0
- package/.output/server/_ssr/insights-CpLpedFZ.mjs +2315 -0
- package/.output/server/_ssr/insights-api-sjws23WF.mjs +122 -0
- package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
- package/.output/server/_ssr/insights-failures-rpc-BvbWvqNf.mjs +36 -0
- package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
- package/.output/server/_ssr/insights-recovery-BWQhiZaF.mjs +28 -0
- package/.output/server/_ssr/insights-recovery-rpc-GiYXdkaY.mjs +32 -0
- package/.output/server/_ssr/{insights-rpc-DDKmIrpV.mjs → insights-rpc-DP_8Rmz4.mjs} +22 -14
- package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-BiolgB8e.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-KStL0zOx.mjs +41 -0
- package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-CVrnbP3R.mjs +7 -0
- package/.output/server/_ssr/insights_.distribution-D2HaNTAA.mjs +16 -0
- package/.output/server/_ssr/{insights_.distribution-cIsU-kHG.mjs → insights_.distribution-DN9pR422.mjs} +50 -22
- package/.output/server/_ssr/installations-BHmbbnH8.mjs +5 -0
- package/.output/server/_ssr/{installations-C5anhyP0.mjs → installations-BSCmFq8Q.mjs} +279 -367
- package/.output/server/_ssr/installations-DD1g__Nc.mjs +69 -0
- package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
- package/.output/server/_ssr/plugins-BaD1slHx.mjs +2086 -0
- package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
- package/.output/server/_ssr/release-adoption-rpc-BsstqxUp.mjs +41 -0
- package/.output/server/_ssr/releaseAdoption-CtOFkwli.mjs +114 -0
- package/.output/server/_ssr/releaseManagement-DFMUnDF5.mjs +11 -0
- package/.output/server/_ssr/remote-config-C2Ee8lXO.mjs +5 -0
- package/.output/server/_ssr/remote-config-Dv1ccMo-.mjs +17 -0
- package/.output/server/_ssr/remote-config-of0lGQH8.mjs +2648 -0
- package/.output/server/_ssr/remote-config-rpc-BzX31mUS.mjs +115 -0
- package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
- package/.output/server/_ssr/{router-BuanSsRI.mjs → router-D8NuoJ_I.mjs} +107 -134
- package/.output/server/_ssr/{routes-DY-vqU6R.mjs → routes-DgvnFqDy.mjs} +195 -429
- package/.output/server/_ssr/routes-V8sDybGf.mjs +409 -0
- package/.output/server/_ssr/runtime.server-De0QVWc9.mjs +104 -0
- package/.output/server/_ssr/schema-BiAVyEj2.mjs +361 -0
- package/.output/server/_ssr/{select-DD2szs3Y.mjs → select-t3iXRwc9.mjs} +3 -3
- package/.output/server/_ssr/server-h4VxnExB.mjs +8325 -0
- package/.output/server/_ssr/{createSsrRpc-CdlfejmI.mjs → sidebar-DJcErBMo.mjs} +107 -47
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/start-BfFbHKMK.mjs +4 -0
- package/.output/server/_ssr/switch-E-btwMGt.mjs +43 -0
- package/.output/server/_ssr/{table-CTj6n-4F.mjs → table-37Dd23TN.mjs} +2 -2
- package/.output/server/_ssr/tabs-DEFnTm95.mjs +45 -0
- package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-C88s9jWF.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-55AGsehh.mjs +139 -0
- package/.output/server/index.mjs +309 -211
- package/LICENSE +13 -14
- package/README.md +80 -71
- package/lib/index.d.mts +23 -11
- package/lib/vite-internal.mjs +13 -3
- package/lib/vite.d.mts +2 -0
- package/package.json +14 -14
- package/src/components/AppSidebar.tsx +69 -74
- package/src/components/ConsoleFeatureUnavailable.tsx +80 -0
- package/src/components/NotFoundPage.tsx +7 -7
- package/src/components/features/FeatureSlots.tsx +103 -0
- package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
- package/src/components/features/bundles/BundleMetadata.tsx +8 -5
- package/src/components/features/bundles/RolloutCohortsDialog.tsx +1 -1
- package/src/components/features/channels/ChannelManagementDialog.tsx +8 -6
- package/src/components/features/insights/AppUsage.tsx +41 -14
- package/src/components/features/insights/BuiltinBundle.tsx +36 -0
- package/src/components/features/insights/ComparedBundlesChart.tsx +310 -0
- package/src/components/features/insights/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +390 -26
- package/src/components/features/insights/EventHistoryCard.tsx +111 -25
- package/src/components/features/insights/EventHistoryList.tsx +1 -1
- package/src/components/features/insights/FailureReports.tsx +407 -0
- package/src/components/features/insights/InsightsControls.tsx +240 -142
- package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
- package/src/components/features/insights/InsightsOverview.tsx +601 -403
- package/src/components/features/insights/InsightsPageHeader.tsx +19 -14
- package/src/components/features/insights/InsightsPeriodSelector.tsx +67 -12
- package/src/components/features/insights/InstallationHistoryCard.tsx +65 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +308 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +13 -13
- package/src/components/features/insights/RollbackButton.tsx +118 -0
- package/src/components/features/insights/UpdateFailures.tsx +382 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +27 -13
- package/src/components/features/remote-config/ConditionDialog.tsx +592 -0
- package/src/components/features/remote-config/ConditionsCard.tsx +247 -0
- package/src/components/features/remote-config/ParameterDialog.tsx +426 -0
- package/src/components/features/remote-config/ParametersCard.tsx +254 -0
- package/src/components/features/remote-config/PreviewCard.tsx +284 -0
- package/src/components/features/remote-config/PublishDialog.tsx +325 -0
- package/src/components/features/remote-config/RemoteConfigPage.tsx +353 -0
- package/src/components/features/remote-config/RuleSummary.tsx +21 -0
- package/src/components/features/remote-config/ValueInput.tsx +151 -0
- package/src/components/features/remote-config/VersionsCard.tsx +384 -0
- package/src/components/features/remote-config/remoteConfigFeatureUi.tsx +15 -0
- package/src/components/features/remote-config/useDateTimeText.ts +16 -0
- package/src/components/ui/sidebar.tsx +22 -29
- package/src/index.ts +26 -19
- package/src/lib/api-keys-api.ts +0 -28
- package/src/lib/api-keys-rpc.ts +27 -38
- package/src/lib/api-rpc.ts +125 -288
- package/src/lib/api.ts +15 -134
- package/src/lib/bundle-activity.ts +24 -4
- package/src/lib/console-access.ts +20 -0
- package/src/lib/console-features-api.ts +68 -0
- package/src/lib/console-features-rpc.ts +13 -0
- package/src/lib/console-features.ts +98 -0
- package/src/lib/insights-api.ts +56 -2
- package/src/lib/insights-errors.ts +64 -0
- package/src/lib/insights-failures-rpc.ts +36 -0
- package/src/lib/insights-failures.ts +130 -0
- package/src/lib/insights-recovery-rpc.ts +13 -17
- package/src/lib/insights-recovery.ts +12 -36
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +50 -4
- package/src/lib/insights-usage-rpc.ts +15 -9
- package/src/lib/insights-usage.ts +41 -3
- package/src/lib/insights-view.ts +34 -5
- package/src/lib/release-adoption-api.ts +214 -0
- package/src/lib/release-adoption-rpc.ts +49 -0
- package/src/lib/release-adoption.ts +164 -0
- package/src/lib/remote-config-api.ts +119 -0
- package/src/lib/remote-config-draft.ts +666 -0
- package/src/lib/remote-config-rpc.ts +196 -0
- package/src/lib/scroll-restoration.ts +21 -0
- package/src/lib/server/auth.server.ts +13 -5
- package/src/lib/server/bundleActivity.ts +27 -28
- package/src/lib/server/config.server.ts +21 -76
- package/src/lib/server/console-feature-apis.ts +70 -0
- package/src/lib/server/console-runtime.server.ts +82 -38
- package/src/lib/server/downloadBundle.ts +14 -15
- package/src/lib/server/failureReports.ts +50 -0
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsUsage.ts +52 -113
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseAdoption.ts +151 -0
- package/src/lib/server/releaseReachability.ts +8 -8
- package/src/lib/server/remoteConfig.ts +75 -0
- package/src/lib/server/runtime.server.ts +120 -31
- package/src/lib/server/updateFailures.ts +66 -0
- package/src/lib/use-dialog-target.ts +27 -0
- package/src/routeTree.gen.ts +44 -44
- package/src/router.tsx +3 -2
- package/src/routes/-installations-search.ts +32 -8
- package/src/routes/-releases-search.ts +76 -29
- package/src/routes/__root.tsx +7 -1
- package/src/routes/api/bundles/$bundleId/download.ts +2 -5
- package/src/routes/api-keys.tsx +5 -2
- package/src/routes/index.tsx +210 -152
- package/src/routes/insights.tsx +91 -23
- package/src/routes/insights_.distribution.tsx +50 -9
- package/src/routes/installations.tsx +97 -40
- package/src/routes/remote-config.tsx +42 -0
- package/src/styles.css +9 -0
- package/src/vite-internal.ts +15 -3
- package/src/vite.ts +2 -0
- package/.output/public/assets/HashValueDisplay-BpTc3Oyg.js +0 -1
- package/.output/public/assets/InsightsPageHeader-DC2eEst8.js +0 -1
- package/.output/public/assets/api-CdUyUqzR.js +0 -42
- package/.output/public/assets/api-keys-64PM9PwW.js +0 -1
- package/.output/public/assets/api-keys-api-BeaJnvY5.js +0 -1
- package/.output/public/assets/arrow-left-04Rh8Zgp.js +0 -1
- package/.output/public/assets/card-CYHs5FZv.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-DwyMsVFW.js +0 -1
- package/.output/public/assets/createServerFn-DU71xucx.js +0 -3
- package/.output/public/assets/dialog-IVMkBRCq.js +0 -1
- package/.output/public/assets/dist-BR4GzeEu.js +0 -1
- package/.output/public/assets/field-B3CYDpjg.js +0 -1
- package/.output/public/assets/index-Cb_D1D6N.js +0 -10
- package/.output/public/assets/info-C3y9HNoU.js +0 -1
- package/.output/public/assets/input-group-nLZylhWF.js +0 -1
- package/.output/public/assets/insights-Bm9v8uO3.js +0 -2
- package/.output/public/assets/insights-DS3iO7Up.js +0 -1
- package/.output/public/assets/insights-search-Byj3g3Mp.js +0 -1
- package/.output/public/assets/insights-usage-rpc-BaGR3lgq.js +0 -1
- package/.output/public/assets/insights_.distribution-B1i6cjlQ.js +0 -1
- package/.output/public/assets/insights_.distribution-C1Wf27eB.js +0 -2
- package/.output/public/assets/installations-CkFW4fV9.js +0 -2
- package/.output/public/assets/installations-DxTaIcHD.js +0 -1
- package/.output/public/assets/lazyRouteComponent-BapzjYnb.js +0 -1
- package/.output/public/assets/list-Dqt53fWZ.js +0 -1
- package/.output/public/assets/loader-circle-CUKfUqow.js +0 -1
- package/.output/public/assets/preload-helper-ClBMtM1g.js +0 -1
- package/.output/public/assets/refresh-cw-CNE_2PnD.js +0 -1
- package/.output/public/assets/routes-Cgtvspjg.js +0 -2
- package/.output/public/assets/routes-O2r2UUN8.js +0 -1
- package/.output/public/assets/shield-check-BFcJ46kz.js +0 -1
- package/.output/public/assets/signing-BgspC-xP.js +0 -1
- package/.output/public/assets/styles-DnrWuBwd.css +0 -2
- package/.output/public/assets/table-BuWQgxJJ.js +0 -1
- package/.output/public/assets/useMutation-CSV9NCUl.js +0 -1
- package/.output/public/assets/useOpenInteractionType-D5kduCTD.js +0 -1
- package/.output/public/assets/valueToPercent-BAtcbSLk.js +0 -1
- package/.output/server/__23tanstack-start-server-fn-resolver-DBRojHaG.mjs +0 -150
- package/.output/server/_ssr/InsightsPageHeader-mtxVuZKK.mjs +0 -64
- package/.output/server/_ssr/api-BfCgYlj5.mjs +0 -332
- package/.output/server/_ssr/api-keys-api-C3oeq01R.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-CdPFdGOx.mjs +0 -136
- package/.output/server/_ssr/api-rpc-B2E1ZIG5.mjs +0 -1399
- package/.output/server/_ssr/auth.server-CmicHh0J.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-Di7-I6fE.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-Uqt1OF49.mjs +0 -12
- package/.output/server/_ssr/bundleActivity-pBrPSG01.mjs +0 -28
- package/.output/server/_ssr/card-Bemj0dpY.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-B8WsT3s0.mjs +0 -368
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/deleteBundle-8X-jq7Z7.mjs +0 -66
- package/.output/server/_ssr/field-BrQ2u0fT.mjs +0 -74
- package/.output/server/_ssr/getBundleChildren-DoL8l_AS.mjs +0 -50
- package/.output/server/_ssr/insights-BwQ-wF2a.mjs +0 -10
- package/.output/server/_ssr/insights-gr1I_dJU.mjs +0 -938
- package/.output/server/_ssr/insights-recovery-Cv6RSyeR.mjs +0 -21
- package/.output/server/_ssr/insights-recovery-rpc-DOzxfYfv.mjs +0 -37
- package/.output/server/_ssr/insights-search-1czJMZgk.mjs +0 -21
- package/.output/server/_ssr/insights-usage-rpc-4ImOq_0p.mjs +0 -184
- package/.output/server/_ssr/insights-usage-rpc-BUruODjE.mjs +0 -24
- package/.output/server/_ssr/insightsContract-B09K_Px1.mjs +0 -303
- package/.output/server/_ssr/insightsHistory-Ci4rYAgl.mjs +0 -41
- package/.output/server/_ssr/insightsRecovery-CaghWL2t.mjs +0 -113
- package/.output/server/_ssr/insightsUsage-DNIj6jxF.mjs +0 -71
- package/.output/server/_ssr/insights_.distribution-VmHuikRE.mjs +0 -10
- package/.output/server/_ssr/installations-HXMF_kKf.mjs +0 -29
- package/.output/server/_ssr/routes-Dru3IRj0.mjs +0 -37
- package/.output/server/_ssr/runtime.server-CsiKFp_Z.mjs +0 -436
- package/.output/server/_ssr/server-DJm72Kj-.mjs +0 -7468
- package/.output/server/_ssr/signing-H8Lg4lPe.mjs +0 -133
- package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-DsFcTLw_.mjs +0 -125
- package/src/components/features/bundles/BundleActivityChart.tsx +0 -97
- package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -126
- package/src/components/features/signing/BundleSigningPage.tsx +0 -139
- package/src/lib/bundle-signing-api.ts +0 -14
- package/src/lib/bundle-signing-rpc.ts +0 -16
- package/src/lib/server/bundle-signing.server.ts +0 -23
- package/src/lib/server/deleteBundle.ts +0 -145
- package/src/lib/server/insightsHistory.ts +0 -52
- package/src/lib/server/insightsRecovery.ts +0 -187
- 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
|
+
};
|