@hot-updater/console 1.0.0-rc.4 → 1.0.0-rc.41
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-DRzeMGRM.js +1 -0
- package/.output/public/assets/EventDetails-SB5ygJzS.js +1 -0
- package/.output/public/assets/HashValueDisplay-BFX_kY3-.js +1 -0
- package/.output/public/assets/InsightsInfo-btYppp0B.js +1 -0
- package/.output/public/assets/InsightsPageHeader-Ba8psD8t.js +1 -0
- package/.output/public/assets/NotFoundPage-CyRG2eca.js +1 -0
- package/.output/public/assets/alert-dialog-Bbk_4-WJ.js +1 -0
- package/.output/public/assets/api-DUgI6yOI.js +1 -0
- package/.output/public/assets/api-keys-CIO9L_wu.js +1 -0
- package/.output/public/assets/api-keys-CmkNKOhS.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-BMsI3Bj8.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-BEfQ2_7f.js +11 -0
- package/.output/public/assets/input-group-ja_30qaF.js +1 -0
- package/.output/public/assets/insights-B0ib9qOE.js +2 -0
- package/.output/public/assets/insights-BIMJx-0p.js +1 -0
- package/.output/public/assets/insights-CndqcWEj.js +42 -0
- package/.output/public/assets/insights-api-B6Ewc-OG.js +1 -0
- package/.output/public/assets/insights-usage-rpc-DYEhsCKT.js +1 -0
- package/.output/public/assets/insights_.distribution-B26PMy4k.js +2 -0
- package/.output/public/assets/insights_.distribution-BBDtqqYS.js +1 -0
- package/.output/public/assets/insights_.distribution-DhHmYS8k.js +1 -0
- package/.output/public/assets/installations-C3cMuwRx.js +1 -0
- package/.output/public/assets/installations-C3yrjwDh.js +2 -0
- package/.output/public/assets/installations-D3edlN75.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-0i-gDkeU.js +1 -0
- package/.output/public/assets/remote-config-An1ROIOQ.js +6 -0
- package/.output/public/assets/remote-config-CoeWgA9i.js +1 -0
- package/.output/public/assets/routes-DNoaJIQV.js +2 -0
- package/.output/public/assets/routes-cWBDtghZ.js +1 -0
- package/.output/public/assets/search-DaTDPW25.js +1 -0
- package/.output/public/assets/select-CZMQKT7F.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-D8mxMvCB.css +2 -0
- package/.output/public/assets/switch-CU4-fupc.js +1 -0
- package/.output/public/assets/table-Gm4qRWwE.js +1 -0
- package/.output/public/assets/tabs-R1I8CRjR.js +1 -0
- package/.output/public/assets/undo-2-CmrHjwFc.js +1 -0
- package/.output/public/assets/useCompositeListItem-BaPeC9ZO.js +1 -0
- package/.output/public/assets/valueToPercent-CH8GCHaL.js +1 -0
- package/.output/server/__23tanstack-start-server-fn-resolver-DTAqlLRd.mjs +178 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +6450 -2741
- 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 +235 -31
- 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-BNoVW3Dv.mjs +13 -0
- package/.output/server/_ssr/ConsoleFeatureUnavailable-BPFa-cmJ.mjs +58 -0
- package/.output/server/_ssr/EventDetails-9CeshfdR.mjs +344 -0
- package/.output/server/_ssr/HashValueDisplay-DXZlkcps.mjs +55 -0
- package/.output/server/_ssr/InsightsInfo-DLNdJQ0m.mjs +75 -0
- package/.output/server/_ssr/InsightsPageHeader-DZGVDgvv.mjs +145 -0
- package/.output/server/_ssr/NotFoundPage-mYHHg-2s.mjs +37 -0
- package/.output/server/_ssr/{dialog-DUaIgpgd.mjs → alert-dialog-Chkim7vZ.mjs} +6 -83
- package/.output/server/_ssr/api-C-Il5-sa.mjs +203 -0
- package/.output/server/_ssr/api-keys-01FGqj4d.mjs +5 -0
- package/.output/server/_ssr/{api-keys-DJml_XoK.mjs → api-keys-B_f9h7ln.mjs} +48 -10
- package/.output/server/_ssr/api-keys-rpc-Cf4YCyFc.mjs +52 -0
- package/.output/server/_ssr/api-rpc-DRqwxxT_.mjs +296 -0
- package/.output/server/_ssr/{auth-BF8mbAgF.mjs → auth-UP0wXs4i.mjs} +1 -1
- package/.output/server/_ssr/{auth-rpc-D_hv52-J.mjs → auth-rpc-D8nFpU9C.mjs} +6 -6
- package/.output/server/_ssr/auth.server-COZoXu4i.mjs +5425 -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-Dm2l7w0O.mjs +36 -0
- package/.output/server/_ssr/console-access--o9FXikU.mjs +17 -0
- package/.output/server/_ssr/console-features-BftSkf3f.mjs +60 -0
- package/.output/server/_ssr/console-features-api-CYclUQ4h.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-1WaAkPM_.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-BUuNU-qt.mjs +74 -0
- package/.output/server/_ssr/dialog-mfSANqjC.mjs +84 -0
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-wmnTk7w6.mjs} +140 -6
- package/.output/server/_ssr/{downloadBundle-B3ert-pY.mjs → downloadBundle-DViuIa9U.mjs} +10 -10
- package/.output/server/_ssr/empty-BPayFad3.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-DjZ5t7rH.mjs +189 -0
- package/.output/server/_ssr/getBundleChildren-CmjDg6Re.mjs +22 -0
- package/.output/server/_ssr/{input-group-Bb8ueylQ.mjs → input-group-Dx-MEkNM.mjs} +3 -52
- package/.output/server/_ssr/insights-BopxyJjk.mjs +14 -0
- package/.output/server/_ssr/insights-Bvm03d61.mjs +5 -0
- package/.output/server/_ssr/insights-CrLAHSDy.mjs +2315 -0
- package/.output/server/_ssr/insights-api-Bg8ZV_h_.mjs +122 -0
- package/.output/server/_ssr/insights-errors-BjnFUyZj.mjs +34 -0
- package/.output/server/_ssr/insights-failures-rpc-tyy5m9Cc.mjs +36 -0
- package/.output/server/_ssr/insights-failures-xaa41Buf.mjs +46 -0
- package/.output/server/_ssr/{insights-recovery-DSFM-YTL.mjs → insights-recovery-BWQhiZaF.mjs} +9 -2
- package/.output/server/_ssr/insights-recovery-rpc-CPJOiJAV.mjs +32 -0
- package/.output/server/_ssr/{insights-rpc-DrIQCjpD.mjs → insights-rpc-DoC3hla9.mjs} +22 -14
- package/.output/server/_ssr/insights-usage-BpBM0U0c.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-CFuTYp3T.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-O2ruP4iE.mjs +41 -0
- package/.output/server/_ssr/insightsUsage-CzhVjsc8.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-B3BVPD00.mjs +16 -0
- package/.output/server/_ssr/insights_.distribution-BMoisxRG.mjs +347 -0
- package/.output/server/_ssr/insights_.distribution-BiCcFyrG.mjs +7 -0
- package/.output/server/_ssr/{installations-YumqpNpa.mjs → installations-Cx3jIrwl.mjs} +280 -364
- package/.output/server/_ssr/installations-DTGPaKXx.mjs +69 -0
- package/.output/server/_ssr/installations-osiAuNKr.mjs +5 -0
- package/.output/server/_ssr/listReleases-BfUBlCHo.mjs +79 -0
- package/.output/server/_ssr/release-adoption-005tQRpo.mjs +55 -0
- package/.output/server/_ssr/release-adoption-rpc-BVySDR0L.mjs +41 -0
- package/.output/server/_ssr/releaseAdoption-2PJPA09N.mjs +114 -0
- package/.output/server/_ssr/releaseManagement-BTCWEY8-.mjs +11 -0
- package/.output/server/_ssr/remote-config-CUoAxUMy.mjs +430 -0
- package/.output/server/_ssr/remote-config-Cc5P4J-r.mjs +17 -0
- package/.output/server/_ssr/remote-config-CjdxcZ9B.mjs +5 -0
- package/.output/server/_ssr/remote-config-D2GmbGwI.mjs +2594 -0
- package/.output/server/_ssr/remote-config-rpc-kjoZ9PRS.mjs +115 -0
- package/.output/server/_ssr/rolldown-runtime-rSIU-vHC.mjs +13 -0
- package/.output/server/_ssr/{router-iP95Vqi2.mjs → router-DwGVewL-.mjs} +141 -139
- package/.output/server/_ssr/{routes-ivsV2TDu.mjs → routes-C4yxv3JU.mjs} +192 -685
- package/.output/server/_ssr/routes-D3_aegLY.mjs +409 -0
- package/.output/server/_ssr/runtime.server-BoWf4QhU.mjs +1716 -0
- package/.output/server/_ssr/schema-DrOtkRVD.mjs +361 -0
- package/.output/server/_ssr/select-DkzM2miy.mjs +107 -0
- package/.output/server/_ssr/server-BW0gzB0p.mjs +8325 -0
- package/.output/server/_ssr/{createSsrRpc-NHF2PxuK.mjs → sidebar-DPcOijvL.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-BqJ2mAwC.mjs +17 -0
- package/.output/server/_ssr/{table-D1mL8fJu.mjs → table-R7RY92q9.mjs} +2 -2
- package/.output/server/_ssr/tabs-trDcbRkT.mjs +45 -0
- package/.output/server/_ssr/updateFailures-H6pnnYK4.mjs +40 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-ctA_XVZA.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-BwInaUTw.mjs +139 -0
- package/.output/server/index.mjs +315 -154
- package/LICENSE +13 -14
- package/README.md +82 -72
- 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 +100 -71
- package/src/components/ConsoleAccessPage.tsx +5 -1
- 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 +2 -5
- package/src/components/features/insights/AppUsage.tsx +366 -0
- 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 +400 -39
- package/src/components/features/insights/EventHistoryCard.tsx +112 -26
- package/src/components/features/insights/EventHistoryList.tsx +2 -2
- package/src/components/features/insights/FailureReports.tsx +407 -0
- package/src/components/features/insights/InsightsControls.tsx +265 -88
- package/src/components/features/insights/InsightsErrorAlert.tsx +14 -1
- package/src/components/features/insights/InsightsInfo.tsx +48 -0
- package/src/components/features/insights/InsightsOverview.tsx +604 -355
- package/src/components/features/insights/InsightsPageHeader.tsx +35 -20
- package/src/components/features/insights/InsightsPeriodSelector.tsx +89 -0
- package/src/components/features/insights/InstallationHistoryCard.tsx +66 -11
- 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 +48 -0
- 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 +26 -12
- package/src/components/features/remote-config/ConditionDialog.tsx +581 -0
- package/src/components/features/remote-config/ConditionsCard.tsx +244 -0
- package/src/components/features/remote-config/ParameterDialog.tsx +407 -0
- package/src/components/features/remote-config/ParametersCard.tsx +244 -0
- package/src/components/features/remote-config/PreviewCard.tsx +284 -0
- package/src/components/features/remote-config/PublishDialog.tsx +319 -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 +148 -0
- package/src/components/features/remote-config/VersionsCard.tsx +361 -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/card.tsx +1 -1
- package/src/components/ui/scroll-area.tsx +52 -0
- package/src/components/ui/sidebar.tsx +22 -29
- package/src/components/ui/tabs.tsx +81 -0
- 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 +189 -266
- package/src/lib/api.ts +19 -102
- 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 +97 -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 +10 -27
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +93 -0
- package/src/lib/insights-usage-rpc.ts +29 -0
- package/src/lib/insights-usage.ts +76 -0
- 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 +652 -0
- package/src/lib/remote-config-rpc.ts +196 -0
- package/src/lib/scroll-restoration.ts +21 -0
- package/src/lib/server/adminInsights.ts +124 -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 -74
- 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 +16 -15
- package/src/lib/server/failureReports.ts +49 -0
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsUsage.ts +63 -0
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseAdoption.ts +154 -0
- package/src/lib/server/releaseReachability.ts +8 -7
- package/src/lib/server/remoteConfig.ts +191 -0
- package/src/lib/server/runtime.server.ts +120 -31
- package/src/lib/server/updateFailures.ts +65 -0
- package/src/routeTree.gen.ts +65 -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 +10 -4
- 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 +133 -18
- package/src/routes/insights_.distribution.tsx +422 -0
- package/src/routes/installations.tsx +98 -41
- 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/InsightsPageHeader-C-Xc2MB9.js +0 -1
- package/.output/public/assets/api-keys-C1sgC6ko.js +0 -1
- package/.output/public/assets/api-keys-api-LchP65vQ.js +0 -1
- package/.output/public/assets/badge-BuMDU0Rf.js +0 -1
- package/.output/public/assets/card-BEvx_QKh.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-C0G0fQ3J.js +0 -1
- package/.output/public/assets/composite-CHTH1Xhh.js +0 -1
- package/.output/public/assets/createServerFn-DHc2erub.js +0 -3
- package/.output/public/assets/dialog-dyZ1SphZ.js +0 -1
- package/.output/public/assets/dist-Bh1gaDfB.js +0 -1
- package/.output/public/assets/field-79SVH26s.js +0 -1
- package/.output/public/assets/index-BNkxg4jz.js +0 -10
- package/.output/public/assets/input-group-BbHfaBzQ.js +0 -1
- package/.output/public/assets/insights-D_7uSKfc.js +0 -1
- package/.output/public/assets/installations-BzoHWuXw.js +0 -1
- package/.output/public/assets/installations-DvjbUqd1.js +0 -2
- package/.output/public/assets/lazyRouteComponent-BN0MS05o.js +0 -1
- package/.output/public/assets/link-BG06whwB.js +0 -1
- package/.output/public/assets/loader-circle-Bbp8LGVC.js +0 -1
- package/.output/public/assets/preload-helper-uTix4PVD.js +0 -1
- package/.output/public/assets/refresh-cw-DZgiAOMk.js +0 -1
- package/.output/public/assets/routes-BFE-oRIT.js +0 -1
- package/.output/public/assets/routes-DaPMY31P.js +0 -2
- package/.output/public/assets/shield-check-DoIiaaXU.js +0 -1
- package/.output/public/assets/signing-BTRhaF5D.js +0 -1
- package/.output/public/assets/styles-C7U0oVke.css +0 -2
- package/.output/public/assets/table-BBEzJ7Xl.js +0 -1
- package/.output/public/assets/useCompositeListItem-BkPDaqyj.js +0 -42
- package/.output/server/__23tanstack-start-server-fn-resolver-Dr2ixDgQ.mjs +0 -146
- package/.output/server/_ssr/InsightsPageHeader-C5Has8Ry.mjs +0 -60
- package/.output/server/_ssr/api-keys-api-CrCZyvnl.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-CFaNf-q0.mjs +0 -136
- package/.output/server/_ssr/api-rpc-B61KL3Eo.mjs +0 -1399
- package/.output/server/_ssr/auth.server-DF_shCeq.mjs +0 -47
- package/.output/server/_ssr/badge-BgsCP9Dp.mjs +0 -28
- package/.output/server/_ssr/bundle-signing-rpc-BBipZnFs.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-Co_zHdJx.mjs +0 -12
- package/.output/server/_ssr/bundleActivity-D3_fIda2.mjs +0 -27
- package/.output/server/_ssr/card-DrbxCPtG.mjs +0 -86
- package/.output/server/_ssr/chart-qxa-_30a.mjs +0 -162
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-CBr6PDj5.mjs +0 -368
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/deleteBundle-BAq3JuGr.mjs +0 -66
- package/.output/server/_ssr/field-CqNUlqUq.mjs +0 -74
- package/.output/server/_ssr/getBundleChildren-COMt16m_.mjs +0 -50
- package/.output/server/_ssr/insights-B-DUbksX.mjs +0 -502
- package/.output/server/_ssr/insights-recovery-rpc-P71FH66n.mjs +0 -37
- package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
- package/.output/server/_ssr/insightsRecovery-CqcosG7B.mjs +0 -149
- package/.output/server/_ssr/installations-BQrppti7.mjs +0 -29
- package/.output/server/_ssr/routes-BDabsmc7.mjs +0 -37
- package/.output/server/_ssr/runtime.server-jmZOKIQM.mjs +0 -438
- package/.output/server/_ssr/server-p7yrVBfD.mjs +0 -7468
- package/.output/server/_ssr/signing-CxCbnFCt.mjs +0 -134
- package/.output/server/_ssr/start-DpViQGnt.mjs +0 -4
- package/.output/server/_tanstack-start-manifest_v-Cwpu0C9Q.mjs +0 -100
- package/src/components/features/bundles/BundleActivityChart.tsx +0 -95
- package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -121
- 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 -186
- package/src/routes/signing.tsx +0 -37
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { HotUpdaterCoreApi, ReleaseRow } from "@hot-updater/plugin-core";
|
|
2
|
+
import type {
|
|
3
|
+
InsightsBundleEventFilter,
|
|
4
|
+
InsightsModel,
|
|
5
|
+
} from "@hot-updater/server/plugins/insights";
|
|
6
|
+
|
|
7
|
+
import { insightsPeriodEnd, recoveryWindows } from "../insights-recovery";
|
|
8
|
+
import {
|
|
9
|
+
ADOPTION_CANDIDATES,
|
|
10
|
+
type AdoptionRelease,
|
|
11
|
+
type AdoptionReleaseInput,
|
|
12
|
+
type AdoptionReleaseResult,
|
|
13
|
+
type AdoptionReleasesInput,
|
|
14
|
+
type BundleEventsInput,
|
|
15
|
+
type BundleEventsSeries,
|
|
16
|
+
readAdoptionReleaseInput,
|
|
17
|
+
readAdoptionReleasesInput,
|
|
18
|
+
readBundleEventsInput,
|
|
19
|
+
} from "../release-adoption";
|
|
20
|
+
|
|
21
|
+
type ReleaseReads = Pick<
|
|
22
|
+
HotUpdaterCoreApi,
|
|
23
|
+
"findChannelByName" | "getRelease" | "listReleases"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
/** Rows read past a focused release to find the bundle before it. */
|
|
27
|
+
const PREVIOUS_ROWS = 4;
|
|
28
|
+
|
|
29
|
+
const labelOf = (release: ReleaseRow): AdoptionRelease => ({
|
|
30
|
+
releaseId: release.id,
|
|
31
|
+
bundleId: release.bundle_id!,
|
|
32
|
+
deployedAtMs: release.created_at_ms,
|
|
33
|
+
targetAppVersion: release.target_app_version,
|
|
34
|
+
enabled: release.enabled,
|
|
35
|
+
revision: release.revision,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// A rollback to the built-in bundle has nothing to apply.
|
|
39
|
+
const bundles = (releases: readonly ReleaseRow[]) =>
|
|
40
|
+
releases
|
|
41
|
+
.filter((release) => release.kind === "BUNDLE" && release.bundle_id)
|
|
42
|
+
.map(labelOf);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The newest bundle deployments of a channel and platform, newest first: one
|
|
46
|
+
* read of the channel and one of its newest releases.
|
|
47
|
+
*/
|
|
48
|
+
export async function listAdoptionReleases(
|
|
49
|
+
core: ReleaseReads,
|
|
50
|
+
input: AdoptionReleasesInput,
|
|
51
|
+
): Promise<readonly AdoptionRelease[]> {
|
|
52
|
+
const { platform, channel } = readAdoptionReleasesInput(input);
|
|
53
|
+
const row = await core.findChannelByName(channel);
|
|
54
|
+
if (row === null) return [];
|
|
55
|
+
return bundles(
|
|
56
|
+
await core.listReleases({
|
|
57
|
+
filter: { kind: "channelPlatform", channelId: row.id, platform },
|
|
58
|
+
order: "desc",
|
|
59
|
+
limit: ADOPTION_CANDIDATES,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One bundle deployment of the channel and platform, for a focus the newest
|
|
66
|
+
* ones leave out, and with `withPrevious` the bundle deployed before it.
|
|
67
|
+
*/
|
|
68
|
+
export async function getAdoptionRelease(
|
|
69
|
+
core: ReleaseReads,
|
|
70
|
+
input: AdoptionReleaseInput,
|
|
71
|
+
): Promise<AdoptionReleaseResult> {
|
|
72
|
+
const { platform, channel, releaseId, withPrevious } =
|
|
73
|
+
readAdoptionReleaseInput(input);
|
|
74
|
+
const [row, release] = await Promise.all([
|
|
75
|
+
core.findChannelByName(channel),
|
|
76
|
+
core.getRelease(releaseId),
|
|
77
|
+
]);
|
|
78
|
+
if (
|
|
79
|
+
row === null ||
|
|
80
|
+
release === null ||
|
|
81
|
+
release.kind !== "BUNDLE" ||
|
|
82
|
+
!release.bundle_id ||
|
|
83
|
+
release.platform !== platform ||
|
|
84
|
+
release.channel_id !== row.id
|
|
85
|
+
)
|
|
86
|
+
return withPrevious ? { release: null, previous: null } : { release: null };
|
|
87
|
+
if (!withPrevious) return { release: labelOf(release) };
|
|
88
|
+
// Newest first, `after` a release lists older ones.
|
|
89
|
+
const older = await core.listReleases({
|
|
90
|
+
filter: { kind: "channelPlatform", channelId: row.id, platform },
|
|
91
|
+
order: "desc",
|
|
92
|
+
after: release.id,
|
|
93
|
+
limit: PREVIOUS_ROWS,
|
|
94
|
+
});
|
|
95
|
+
return { release: labelOf(release), previous: bundles(older)[0] ?? null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One bundle's counts of one type in each interval of the period, from
|
|
100
|
+
* their hourly counts, never the events or the release table. Downloads are
|
|
101
|
+
* the bundle's download hours, which count the downloads a launch or crash
|
|
102
|
+
* implied too. Launches add two series: apply reports, and the kept
|
|
103
|
+
* UNCHANGED reports that moved an installation to the bundle with no apply
|
|
104
|
+
* report.
|
|
105
|
+
*/
|
|
106
|
+
export async function getBundleEvents(
|
|
107
|
+
model: Pick<InsightsModel, "countEventSeries">,
|
|
108
|
+
input: BundleEventsInput,
|
|
109
|
+
now = Date.now(),
|
|
110
|
+
): Promise<BundleEventsSeries> {
|
|
111
|
+
const { platform, channel, window, endMs, bundleId, type } =
|
|
112
|
+
readBundleEventsInput(input);
|
|
113
|
+
const { durationMs, intervalMs } = recoveryWindows[window];
|
|
114
|
+
const end = Math.min(endMs, insightsPeriodEnd(now));
|
|
115
|
+
const timeRange = { start: Math.max(0, end - durationMs), end };
|
|
116
|
+
const series = (filter: InsightsBundleEventFilter) =>
|
|
117
|
+
model.countEventSeries({ filter, timeRange, intervalMs });
|
|
118
|
+
const points =
|
|
119
|
+
type === "RECOVERED"
|
|
120
|
+
? await series({ platform, channel, type, fromBundleId: bundleId })
|
|
121
|
+
: type === "DOWNLOADED"
|
|
122
|
+
? await series({
|
|
123
|
+
platform,
|
|
124
|
+
channel,
|
|
125
|
+
type: "UPDATE_DOWNLOADED",
|
|
126
|
+
toBundleId: bundleId,
|
|
127
|
+
})
|
|
128
|
+
: await Promise.all([
|
|
129
|
+
series({
|
|
130
|
+
platform,
|
|
131
|
+
channel,
|
|
132
|
+
type: "UPDATE_APPLIED",
|
|
133
|
+
toBundleId: bundleId,
|
|
134
|
+
}),
|
|
135
|
+
series({
|
|
136
|
+
platform,
|
|
137
|
+
channel,
|
|
138
|
+
type: "UNCHANGED",
|
|
139
|
+
toBundleId: bundleId,
|
|
140
|
+
}),
|
|
141
|
+
]).then(([applied, moved]) =>
|
|
142
|
+
applied.map((point, index) => ({
|
|
143
|
+
startMs: point.startMs,
|
|
144
|
+
events: point.events + (moved[index]?.events ?? 0),
|
|
145
|
+
})),
|
|
146
|
+
);
|
|
147
|
+
return {
|
|
148
|
+
bundleId,
|
|
149
|
+
type,
|
|
150
|
+
measuredAtMs: now,
|
|
151
|
+
points,
|
|
152
|
+
total: points.reduce((sum, point) => sum + point.events, 0),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isReleaseEligibleForCohort,
|
|
3
|
-
NUMERIC_COHORT_SIZE,
|
|
4
|
-
} from "@hot-updater/core";
|
|
5
1
|
import type {
|
|
6
2
|
CompiledReleaseCatalog,
|
|
7
|
-
|
|
3
|
+
HotUpdaterCoreApi,
|
|
8
4
|
ReleaseRow,
|
|
9
5
|
} from "@hot-updater/plugin-core";
|
|
6
|
+
import {
|
|
7
|
+
isReleaseEligibleForCohort,
|
|
8
|
+
NUMERIC_COHORT_SIZE,
|
|
9
|
+
} from "@hot-updater/protocol";
|
|
10
10
|
|
|
11
11
|
export type ReleaseReachabilityRow = ReleaseRow & {
|
|
12
12
|
readonly currentlyUnreachable: boolean;
|
|
@@ -63,15 +63,16 @@ export function collectCurrentlyReachableReleaseIds(
|
|
|
63
63
|
return reachableReleaseIds;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Marks each release its scope's compiled catalog no longer serves: one catalog read per scope. */
|
|
66
67
|
export async function addReleaseReachability(
|
|
67
|
-
|
|
68
|
+
core: Pick<HotUpdaterCoreApi, "getReleaseCatalogRow">,
|
|
68
69
|
releases: readonly ReleaseRow[],
|
|
69
70
|
): Promise<readonly ReleaseReachabilityRow[]> {
|
|
70
71
|
const scopeKeys = [...new Set(releases.map(({ scope_key }) => scope_key))];
|
|
71
72
|
const reachableByScope = new Map(
|
|
72
73
|
await Promise.all(
|
|
73
74
|
scopeKeys.map(async (scopeKey) => {
|
|
74
|
-
const row = await
|
|
75
|
+
const row = await core.getReleaseCatalogRow(scopeKey);
|
|
75
76
|
const reachableReleaseIds =
|
|
76
77
|
row === null
|
|
77
78
|
? new Set<string>()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { RemoteDatabase } from "@hot-updater/plugin-core";
|
|
2
|
+
import {
|
|
3
|
+
type RemoteConfigActive,
|
|
4
|
+
type RemoteConfigApi,
|
|
5
|
+
type RemoteConfigPublishResult,
|
|
6
|
+
type RemoteConfigTemplateIssue,
|
|
7
|
+
RemoteConfigValidationError,
|
|
8
|
+
type RemoteConfigVersion,
|
|
9
|
+
type RemoteConfigVersionDetail,
|
|
10
|
+
type RemoteConfigVersionsPage,
|
|
11
|
+
} from "@hot-updater/server/plugins/remote-config";
|
|
12
|
+
|
|
13
|
+
import { ConsoleFeatureUnavailableError } from "../console-features";
|
|
14
|
+
|
|
15
|
+
/** How a publish from the console ended; `invalid` lists what to fix. */
|
|
16
|
+
export type ConsolePublishResult =
|
|
17
|
+
| RemoteConfigPublishResult
|
|
18
|
+
| {
|
|
19
|
+
readonly status: "invalid";
|
|
20
|
+
readonly issues: readonly RemoteConfigTemplateIssue[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** The Remote Config the console pages use. */
|
|
24
|
+
export interface ConsoleRemoteConfig {
|
|
25
|
+
getActive(): Promise<RemoteConfigActive>;
|
|
26
|
+
listVersions(input: {
|
|
27
|
+
readonly limit?: number;
|
|
28
|
+
readonly cursor?: string;
|
|
29
|
+
}): Promise<RemoteConfigVersionsPage>;
|
|
30
|
+
getVersion(version: number): Promise<RemoteConfigVersionDetail | null>;
|
|
31
|
+
publish(input: {
|
|
32
|
+
readonly template: unknown;
|
|
33
|
+
readonly baseVersion: number;
|
|
34
|
+
readonly description?: string;
|
|
35
|
+
}): Promise<ConsolePublishResult>;
|
|
36
|
+
rollback(input: {
|
|
37
|
+
readonly version: number;
|
|
38
|
+
readonly baseVersion: number;
|
|
39
|
+
}): Promise<ConsolePublishResult | { readonly status: "not_found" }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const invalidOf = (error: unknown): ConsolePublishResult => {
|
|
43
|
+
if (error instanceof RemoteConfigValidationError) {
|
|
44
|
+
return { status: "invalid", issues: error.issues };
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Remote Config over the `remoteConfig()` API the console assembled on the database. */
|
|
50
|
+
export const createLocalRemoteConfig = (
|
|
51
|
+
api: RemoteConfigApi,
|
|
52
|
+
): ConsoleRemoteConfig => ({
|
|
53
|
+
getActive: () => api.getActive(),
|
|
54
|
+
listVersions: (input) => api.listVersions(input),
|
|
55
|
+
getVersion: (version) => api.getVersion(version),
|
|
56
|
+
publish: (input) => api.publish(input).catch(invalidOf),
|
|
57
|
+
rollback: (input) => api.rollback(input),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const readBody = async (response: Response): Promise<unknown> =>
|
|
61
|
+
response.json().catch(() => null);
|
|
62
|
+
|
|
63
|
+
const field = (body: unknown, name: string): unknown =>
|
|
64
|
+
typeof body === "object" && body !== null
|
|
65
|
+
? Reflect.get(body, name)
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
/** A published version's record, as a publish or a rollback answers it. */
|
|
69
|
+
const isPublishedVersion = (body: unknown): body is RemoteConfigVersion => {
|
|
70
|
+
const version = field(body, "version");
|
|
71
|
+
const updateType = field(body, "updateType");
|
|
72
|
+
return (
|
|
73
|
+
typeof version === "number" &&
|
|
74
|
+
Number.isSafeInteger(version) &&
|
|
75
|
+
version > 0 &&
|
|
76
|
+
(updateType === "PUBLISH" || updateType === "ROLLBACK") &&
|
|
77
|
+
typeof field(body, "createdAtMs") === "number"
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const errorText = (body: unknown): string | undefined => {
|
|
82
|
+
const error = field(body, "error");
|
|
83
|
+
return typeof error === "string" ? error : undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remote Config over a self-hosted server's admin routes. A route the server
|
|
88
|
+
* does not mount answers core's 404: it runs without `remoteConfig()`.
|
|
89
|
+
*/
|
|
90
|
+
export const createAdminRemoteConfig = (
|
|
91
|
+
fetchAdmin: RemoteDatabase["fetchAdmin"],
|
|
92
|
+
): ConsoleRemoteConfig => {
|
|
93
|
+
const unavailable = () =>
|
|
94
|
+
new ConsoleFeatureUnavailableError("remoteConfig", { remote: true });
|
|
95
|
+
const request = async (
|
|
96
|
+
path: string,
|
|
97
|
+
init?: { readonly method: string; readonly body: unknown },
|
|
98
|
+
): Promise<{ readonly status: number; readonly body: unknown }> => {
|
|
99
|
+
const response = await fetchAdmin(
|
|
100
|
+
path,
|
|
101
|
+
init === undefined
|
|
102
|
+
? undefined
|
|
103
|
+
: {
|
|
104
|
+
method: init.method,
|
|
105
|
+
headers: { "content-type": "application/json" },
|
|
106
|
+
body: JSON.stringify(init.body),
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
const body = await readBody(response);
|
|
110
|
+
// Core's own 404 for a path no route matches.
|
|
111
|
+
if (response.status === 404 && errorText(body) === "Not found") {
|
|
112
|
+
throw unavailable();
|
|
113
|
+
}
|
|
114
|
+
return { status: response.status, body };
|
|
115
|
+
};
|
|
116
|
+
const expectOk = <T>(
|
|
117
|
+
path: string,
|
|
118
|
+
{ status, body }: { status: number; body: unknown },
|
|
119
|
+
): T => {
|
|
120
|
+
if (status >= 200 && status < 300) return body as T;
|
|
121
|
+
throw new Error(
|
|
122
|
+
errorText(body) ??
|
|
123
|
+
`The server answered ${path.split("?")[0]} with ${status}.`,
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
const published = (
|
|
127
|
+
path: string,
|
|
128
|
+
result: { status: number; body: unknown },
|
|
129
|
+
): ConsolePublishResult => {
|
|
130
|
+
const currentVersion = field(result.body, "currentVersion");
|
|
131
|
+
if (
|
|
132
|
+
result.status === 409 &&
|
|
133
|
+
typeof currentVersion === "number" &&
|
|
134
|
+
Number.isSafeInteger(currentVersion)
|
|
135
|
+
) {
|
|
136
|
+
return { status: "conflict", currentVersion };
|
|
137
|
+
}
|
|
138
|
+
const issues = field(result.body, "issues");
|
|
139
|
+
if (result.status === 400 && Array.isArray(issues)) {
|
|
140
|
+
return { status: "invalid", issues };
|
|
141
|
+
}
|
|
142
|
+
const version = expectOk<unknown>(path, result);
|
|
143
|
+
// Anything else, such as the active template a server that ignores the
|
|
144
|
+
// method answers, published nothing.
|
|
145
|
+
if (!isPublishedVersion(version)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`The server answered ${path} without the version it published.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return { status: "published", version };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
getActive: async () => {
|
|
155
|
+
const path = "/remote-config/template";
|
|
156
|
+
return expectOk(path, await request(path));
|
|
157
|
+
},
|
|
158
|
+
listVersions: async ({ limit, cursor }) => {
|
|
159
|
+
const query = new URLSearchParams();
|
|
160
|
+
if (limit !== undefined) query.set("limit", String(limit));
|
|
161
|
+
if (cursor !== undefined) query.set("cursor", cursor);
|
|
162
|
+
const path = `/remote-config/versions${query.size === 0 ? "" : `?${query}`}`;
|
|
163
|
+
return expectOk(path, await request(path));
|
|
164
|
+
},
|
|
165
|
+
getVersion: async (version) => {
|
|
166
|
+
const path = `/remote-config/versions/${version}`;
|
|
167
|
+
const result = await request(path);
|
|
168
|
+
return result.status === 404 ? null : expectOk(path, result);
|
|
169
|
+
},
|
|
170
|
+
publish: async ({ template, baseVersion, description }) => {
|
|
171
|
+
const path = "/remote-config/template";
|
|
172
|
+
return published(
|
|
173
|
+
path,
|
|
174
|
+
await request(path, {
|
|
175
|
+
method: "PUT",
|
|
176
|
+
body: { template, baseVersion, description },
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
rollback: async ({ version, baseVersion }) => {
|
|
181
|
+
const path = `/remote-config/versions/${version}/rollback`;
|
|
182
|
+
const result = await request(path, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
body: { baseVersion },
|
|
185
|
+
});
|
|
186
|
+
return result.status === 404
|
|
187
|
+
? { status: "not_found" }
|
|
188
|
+
: published(path, result);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
};
|
|
@@ -1,36 +1,125 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
DatabasePlugin,
|
|
1
|
+
import {
|
|
2
|
+
type ConfiguredDatabase,
|
|
3
|
+
isRemoteDatabase,
|
|
5
4
|
} from "@hot-updater/plugin-core";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type ConsoleFeature,
|
|
8
|
+
ConsoleFeatureUnavailableError,
|
|
9
|
+
type ConsoleFeatures,
|
|
10
|
+
consoleFeatures,
|
|
11
|
+
resolveConsoleFeatures,
|
|
12
|
+
} from "../console-features";
|
|
6
13
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
type ConsoleFeatureApis,
|
|
15
|
+
consoleFeatureApis,
|
|
16
|
+
} from "./console-feature-apis";
|
|
17
|
+
|
|
18
|
+
export type { ConsoleFeatureApis } from "./console-feature-apis";
|
|
19
|
+
|
|
20
|
+
export interface ConsoleRuntime {
|
|
21
|
+
/** Whether the console reaches a self-hosted server through its admin API. */
|
|
22
|
+
readonly remote: boolean;
|
|
23
|
+
/** The features on here, from the plugins the console's config lists. */
|
|
24
|
+
features(): Promise<ConsoleFeatures>;
|
|
25
|
+
/** What serves the features the console can serve here; {@link requireFeature} hands it out. */
|
|
26
|
+
readonly apis: Partial<ConsoleFeatureApis>;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
30
|
+
typeof value === "object" && value !== null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether one of Hot Updater's own factories made `plugin`: the brand they
|
|
34
|
+
* set under a registered symbol, read here with a copy of the key so the
|
|
35
|
+
* console imports no package's internals. Assembly refuses any other plugin
|
|
36
|
+
* that takes one of their ids, so features key on it.
|
|
37
|
+
*/
|
|
38
|
+
const isOfficialPlugin = (plugin: unknown): boolean =>
|
|
39
|
+
isRecord(plugin) &&
|
|
40
|
+
Reflect.get(plugin, Symbol.for("@hot-updater/server/official-plugin")) ===
|
|
41
|
+
true;
|
|
42
|
+
|
|
43
|
+
const featureIds = Object.keys(consoleFeatureApis) as ConsoleFeature[];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What serves each feature, built by its factory: over the plugins' APIs the
|
|
47
|
+
* console assembled, or over a self-hosted server's admin API. A feature
|
|
48
|
+
* without its plugin, or without a factory for the admin API, has none.
|
|
49
|
+
*/
|
|
50
|
+
const featureApis = (
|
|
51
|
+
build: (feature: ConsoleFeature) => unknown,
|
|
52
|
+
): Partial<ConsoleFeatureApis> =>
|
|
53
|
+
Object.fromEntries(
|
|
54
|
+
featureIds.flatMap((feature) => {
|
|
55
|
+
const api = build(feature);
|
|
56
|
+
return api === undefined ? [] : [[feature, api]];
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The console's features and what serves them, for the plugins its config
|
|
62
|
+
* lists, the plugins the server runs:
|
|
63
|
+
* - a self-hosted server (`standaloneRepository`) runs them itself; the
|
|
64
|
+
* console serves what that server's admin API serves of their features,
|
|
65
|
+
* and nothing that needs the database;
|
|
66
|
+
* - otherwise the console runs them over the database, as the server does,
|
|
67
|
+
* and serves their features through `api`, their APIs.
|
|
68
|
+
*/
|
|
69
|
+
export const createConsoleRuntime = (config: {
|
|
70
|
+
readonly database: ConfiguredDatabase;
|
|
71
|
+
readonly plugins: readonly unknown[];
|
|
72
|
+
readonly api?: Readonly<Record<string, unknown>>;
|
|
73
|
+
}): ConsoleRuntime => {
|
|
74
|
+
const { database } = config;
|
|
75
|
+
// A feature's plugin is Hot Updater's own, which alone may take its id:
|
|
76
|
+
// the server's assembly refuses any other, and features key on the
|
|
77
|
+
// factory's mark, so another plugin under that id turns on nothing.
|
|
78
|
+
const official = new Set(
|
|
79
|
+
config.plugins
|
|
80
|
+
.filter(isOfficialPlugin)
|
|
81
|
+
.map((plugin) => (plugin as { id: string }).id),
|
|
82
|
+
);
|
|
83
|
+
if (isRemoteDatabase(database)) {
|
|
84
|
+
const { fetchAdmin } = database;
|
|
85
|
+
const features = resolveConsoleFeatures([...official], { remote: true });
|
|
86
|
+
return {
|
|
87
|
+
remote: true,
|
|
88
|
+
features: async () => features,
|
|
89
|
+
apis: featureApis((feature) =>
|
|
90
|
+
consoleFeatureApis[feature].remote?.(fetchAdmin),
|
|
91
|
+
),
|
|
92
|
+
};
|
|
34
93
|
}
|
|
35
|
-
|
|
36
|
-
}
|
|
94
|
+
const api = config.api ?? {};
|
|
95
|
+
const features = resolveConsoleFeatures([...official], { remote: false });
|
|
96
|
+
return {
|
|
97
|
+
remote: false,
|
|
98
|
+
features: async () => features,
|
|
99
|
+
apis: featureApis((feature) => {
|
|
100
|
+
const { plugin } = consoleFeatures[feature];
|
|
101
|
+
const pluginApi = official.has(plugin) ? api[plugin] : undefined;
|
|
102
|
+
return pluginApi === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: consoleFeatureApis[feature].local(pluginApi as never);
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What serves `feature`. Every feature's server function asks for it first,
|
|
111
|
+
* so a feature the console does not serve is refused with one error the
|
|
112
|
+
* client recognizes, before anything is read.
|
|
113
|
+
*/
|
|
114
|
+
export const requireFeature = async <F extends ConsoleFeature>(
|
|
115
|
+
runtime: ConsoleRuntime,
|
|
116
|
+
feature: F,
|
|
117
|
+
): Promise<ConsoleFeatureApis[F]> => {
|
|
118
|
+
const api = runtime.apis[feature];
|
|
119
|
+
if (api === undefined || !(await runtime.features())[feature]) {
|
|
120
|
+
throw new ConsoleFeatureUnavailableError(feature, {
|
|
121
|
+
remote: runtime.remote,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return api as ConsoleFeatureApis[F];
|
|
125
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkFailureRate,
|
|
3
|
+
failureRate,
|
|
4
|
+
readUpdateFailuresInput,
|
|
5
|
+
type UpdateFailuresInput,
|
|
6
|
+
type UpdateFailuresReport,
|
|
7
|
+
} from "../insights-failures";
|
|
8
|
+
import { insightsPeriodEnd, recoveryWindows } from "../insights-recovery";
|
|
9
|
+
import type { ConsoleInsightsReads } from "./adminInsights";
|
|
10
|
+
|
|
11
|
+
/** A release's or a channel's update failures over a period that ends with the current hour. */
|
|
12
|
+
export async function getUpdateFailuresReport(
|
|
13
|
+
reads: Pick<ConsoleInsightsReads, "getUpdateFailures">,
|
|
14
|
+
input: UpdateFailuresInput,
|
|
15
|
+
now = Date.now(),
|
|
16
|
+
): Promise<UpdateFailuresReport> {
|
|
17
|
+
readUpdateFailuresInput(input);
|
|
18
|
+
const end = insightsPeriodEnd(now);
|
|
19
|
+
const timeRange =
|
|
20
|
+
input.window === undefined
|
|
21
|
+
? undefined
|
|
22
|
+
: {
|
|
23
|
+
start: Math.max(0, end - recoveryWindows[input.window].durationMs),
|
|
24
|
+
end,
|
|
25
|
+
};
|
|
26
|
+
const scope = {
|
|
27
|
+
platform: input.platform,
|
|
28
|
+
channel: input.channel,
|
|
29
|
+
...(input.releaseId === undefined ? {} : { releaseId: input.releaseId }),
|
|
30
|
+
};
|
|
31
|
+
const previousRange =
|
|
32
|
+
timeRange && timeRange.start >= timeRange.end - timeRange.start
|
|
33
|
+
? {
|
|
34
|
+
start: timeRange.start - (timeRange.end - timeRange.start),
|
|
35
|
+
end: timeRange.start,
|
|
36
|
+
}
|
|
37
|
+
: undefined;
|
|
38
|
+
const [failures, previous] = await Promise.all([
|
|
39
|
+
reads.getUpdateFailures({
|
|
40
|
+
...scope,
|
|
41
|
+
...(timeRange === undefined ? {} : { timeRange }),
|
|
42
|
+
}),
|
|
43
|
+
previousRange === undefined
|
|
44
|
+
? undefined
|
|
45
|
+
: reads.getUpdateFailures({ ...scope, timeRange: previousRange }),
|
|
46
|
+
]);
|
|
47
|
+
return {
|
|
48
|
+
...failures,
|
|
49
|
+
startMs: timeRange?.start ?? null,
|
|
50
|
+
endMs: end,
|
|
51
|
+
...(previous === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: {
|
|
54
|
+
previous: {
|
|
55
|
+
attemptRate: failureRate(previous),
|
|
56
|
+
checkRate: previous.checks
|
|
57
|
+
? checkFailureRate(previous.checks)
|
|
58
|
+
: null,
|
|
59
|
+
complete:
|
|
60
|
+
previous.coverage.kind === "complete" &&
|
|
61
|
+
failures.coverage.kind === "complete",
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
}
|