@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.20
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-B9VYGA3N.js +1 -0
- package/.output/public/assets/HashValueDisplay-D880zntR.js +1 -0
- package/.output/public/assets/InsightsInfo-By_sPjTE.js +1 -0
- package/.output/public/assets/InsightsPageHeader-9_kx_LL7.js +1 -0
- package/.output/public/assets/NotFoundPage-Dys8C0cW.js +1 -0
- package/.output/public/assets/api-CtarlV0l.js +1 -0
- package/.output/public/assets/api-keys-DFF_QqtN.js +1 -0
- package/.output/public/assets/api-keys-evfsQ4sN.js +1 -0
- package/.output/public/assets/arrow-left-BP9D-ta_.js +1 -0
- package/.output/public/assets/arrow-up-right-CDP9Tevw.js +1 -0
- package/.output/public/assets/card-DzCPSRsh.js +1 -0
- package/.output/public/assets/console-features-api-yv7rm3DX.js +1 -0
- package/.output/public/assets/dialog-BDvCy_QO.js +1 -0
- package/.output/public/assets/dist-Clb6cQ9l.js +1 -0
- package/.output/public/assets/empty-C5x9mn_s.js +1 -0
- package/.output/public/assets/field-Bou7shoO.js +1 -0
- package/.output/public/assets/index-0eg3l8aC.js +10 -0
- package/.output/public/assets/input-group-DVtWFUUF.js +1 -0
- package/.output/public/assets/insights-2MbdiSNv.js +1 -0
- package/.output/public/assets/insights-C6tmCZHk.js +2 -0
- package/.output/public/assets/insights-CadS_aIA.js +42 -0
- package/.output/public/assets/insights-recovery-rpc-wvdyM4LD.js +1 -0
- package/.output/public/assets/insights-search-KIF7mRtB.js +1 -0
- package/.output/public/assets/insights-usage-rpc-qUkNtrWr.js +1 -0
- package/.output/public/assets/insights_.distribution-BhBs6PNA.js +1 -0
- package/.output/public/assets/insights_.distribution-CItZw3-8.js +1 -0
- package/.output/public/assets/insights_.distribution-CJUH_wAS.js +2 -0
- package/.output/public/assets/installations-CH2H7JQt.js +2 -0
- package/.output/public/assets/installations-DMeMuwsW.js +1 -0
- package/.output/public/assets/installations-DnvxI9Dt.js +1 -0
- package/.output/public/assets/loader-circle-Ca7C9YY1.js +1 -0
- package/.output/public/assets/mutation-x9L28_CD.js +1 -0
- package/.output/public/assets/preload-helper-BN-UkjOE.js +1 -0
- package/.output/public/assets/refresh-cw-CVGqvy8Z.js +1 -0
- package/.output/public/assets/routes-BhK8DuZA.js +1 -0
- package/.output/public/assets/routes-veKtDW39.js +2 -0
- package/.output/public/assets/sidebar-B45aMqRm.js +3 -0
- package/.output/public/assets/styles-Dn0pK1EX.css +2 -0
- package/.output/public/assets/table-CwLnN6KX.js +1 -0
- package/.output/public/assets/useCompositeListItem-CF7vsfcA.js +1 -0
- package/.output/public/assets/useMutation-CN1ygYEr.js +1 -0
- package/.output/public/assets/useOpenInteractionType-AnAMWYIY.js +1 -0
- package/.output/public/assets/valueToPercent-C-t8FT0y.js +1 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-CUQLz7hs.mjs} +52 -40
- package/.output/server/_libs/@base-ui/react+[...].mjs +7044 -3622
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +49 -3
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +4 -4
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +207 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +818 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/es-toolkit.mjs +1159 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/immer.mjs +532 -0
- package/.output/server/_libs/lucide-react.mjs +46 -32
- package/.output/server/_libs/next-themes.mjs +2 -2
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +14350 -0
- package/.output/server/_libs/sonner.mjs +2 -2
- package/.output/server/_libs/tanstack__react-query.mjs +8 -3
- package/.output/server/_libs/verkit.mjs +99 -1
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/ConsoleFeatureUnavailable-DoxET2AT.mjs +58 -0
- package/.output/server/_ssr/{HashValueDisplay-DwpGLfrl.mjs → HashValueDisplay-BplboHEb.mjs} +3 -3
- package/.output/server/_ssr/InsightsInfo-o2YPd5Qc.mjs +37 -0
- package/.output/server/_ssr/InsightsPageHeader-NExCJL1I.mjs +154 -0
- package/.output/server/_ssr/NotFoundPage-Cd-A8ock.mjs +37 -0
- package/.output/server/_ssr/api-B1Ro0uys.mjs +202 -0
- package/.output/server/_ssr/api-C0G9MTMI.mjs +4205 -0
- package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-CT3HG7KT.mjs} +49 -9
- package/.output/server/_ssr/api-keys-Dvvy1sem.mjs +5 -0
- package/.output/server/_ssr/api-keys-rpc-b1qTTFfG.mjs +52 -0
- package/.output/server/_ssr/api-rpc-Cbns2MGx.mjs +338 -0
- package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-DChS7m9y.mjs} +6 -6
- package/.output/server/_ssr/auth.server-Ccqz2iVv.mjs +37 -0
- package/.output/server/_ssr/bundleActivity-D17Pb_h5.mjs +19 -0
- package/.output/server/_ssr/card-DMbcvlPz.mjs +49 -0
- package/.output/server/_ssr/config-DMEJdhoW.mjs +13 -0
- package/.output/server/_ssr/config.server-DyypN6cQ.mjs +60 -0
- package/.output/server/_ssr/console-access-KpPv5lin.mjs +17 -0
- package/.output/server/_ssr/console-features-CsbyHYfK.mjs +55 -0
- package/.output/server/_ssr/console-features-api-B-KVuo4p.mjs +43 -0
- package/.output/server/_ssr/console-features-rpc-Djyo5740.mjs +19 -0
- package/.output/server/_ssr/{createServerFn-B2hcXoPM.mjs → createMiddleware-B_I3Ll4M.mjs} +28 -2
- package/.output/server/_ssr/{createServerRpc-DB29C_5p.mjs → createServerRpc-DuNm5mmo.mjs} +2 -2
- package/.output/server/_ssr/databaseRows-CXxf_VIB.mjs +132 -0
- package/.output/server/_ssr/{deleteBundle-BTGwBgzx.mjs → deleteBundle-Dcc21Lbd.mjs} +9 -19
- package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-cOmLTUV2.mjs} +2 -2
- package/.output/server/_ssr/{dist-CyzPaI5v.mjs → dist-B-r4u3Lg.mjs} +28 -5
- package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-CexOFoZ4.mjs} +6 -7
- package/.output/server/_ssr/empty-BHoY_5jt.mjs +49 -0
- package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-mjhcNEXD.mjs} +63 -51
- package/.output/server/_ssr/getBundleChildren-DbDXe9wB.mjs +22 -0
- package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-96H30HWC.mjs} +2 -2
- package/.output/server/_ssr/insights-D25VDVaz.mjs +5 -0
- package/.output/server/_ssr/insights-D359GJFL.mjs +14 -0
- package/.output/server/_ssr/insights-DgFv1NsO.mjs +1238 -0
- package/.output/server/_ssr/insights-api-ChAKrbhG.mjs +105 -0
- package/.output/server/_ssr/insights-failures-WQ-ttjOx.mjs +44 -0
- package/.output/server/_ssr/insights-failures-rpc-BFsqlltN.mjs +21 -0
- package/.output/server/_ssr/insights-recovery-DkL6bxMW.mjs +28 -0
- package/.output/server/_ssr/insights-recovery-rpc-2P5m8F62.mjs +46 -0
- package/.output/server/_ssr/insights-recovery-rpc-Tj8vNC19.mjs +20 -0
- package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-DrkVmV9o.mjs} +22 -14
- package/.output/server/_ssr/insights-search-BsalnNw-.mjs +25 -0
- package/.output/server/_ssr/insights-usage-1nVTQrLy.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-Ct_4xHhZ.mjs +29 -0
- package/.output/server/_ssr/insights-usage-rpc-DaYIRtFK.mjs +41 -0
- package/.output/server/_ssr/insightsRecovery-CpGucWN1.mjs +45 -0
- package/.output/server/_ssr/insightsUsage-JYpKZAF7.mjs +56 -0
- package/.output/server/_ssr/insights_.distribution-Bcu6uYs_.mjs +5 -0
- package/.output/server/_ssr/insights_.distribution-D3Csuyjw.mjs +14 -0
- package/.output/server/_ssr/insights_.distribution-jQZEF07J.mjs +335 -0
- package/.output/server/_ssr/installations-B8Vh54lH.mjs +69 -0
- package/.output/server/_ssr/installations-BI5kelHv.mjs +5 -0
- package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-Cxikllmx.mjs} +517 -54
- package/.output/server/_ssr/listReleases-DqU9t1l3.mjs +79 -0
- package/.output/server/_ssr/releaseManagement-B3e7I8J9.mjs +11 -0
- package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-CRkm-wHD.mjs} +117 -124
- package/.output/server/_ssr/routes-B3IJo_jL.mjs +292 -0
- package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-CNAi0Mmv.mjs} +176 -455
- package/.output/server/_ssr/runtime.server-NucbYjc7.mjs +1372 -0
- package/.output/server/_ssr/select-BkPAJFL7.mjs +107 -0
- package/.output/server/_ssr/{server-D_C2rMzf.mjs → server-DKJYsaCP.mjs} +6 -32
- package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → sidebar-Cw1VuC7d.mjs} +117 -63
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/table-q0P9wz7x.mjs +53 -0
- package/.output/server/_ssr/updateFailures-DMU9f0pK.mjs +24 -0
- package/.output/server/_ssr/{uuidv7-D0QFkPL_.mjs → uuidv7-BzSO-fr-.mjs} +2 -2
- package/.output/server/_tanstack-start-manifest_v-Bb0gKDbb.mjs +124 -0
- package/.output/server/index.mjs +273 -168
- package/README.md +71 -65
- package/lib/index.d.mts +13 -11
- package/lib/vite-internal.mjs +6 -3
- package/package.json +8 -8
- package/src/components/AppSidebar.tsx +91 -71
- package/src/components/ConsoleAccessPage.tsx +5 -1
- package/src/components/ConsoleFeatureUnavailable.tsx +82 -0
- package/src/components/NotFoundPage.tsx +7 -7
- package/src/components/features/FeatureSlots.tsx +99 -0
- package/src/components/features/api-keys/apiKeysFeatureUi.tsx +15 -0
- package/src/components/features/bundles/BundleMetadata.tsx +7 -4
- package/src/components/features/channels/ChannelManagementDialog.tsx +2 -5
- package/src/components/features/insights/AppUsage.tsx +370 -0
- package/src/components/features/insights/EstimatedCount.tsx +18 -0
- package/src/components/features/insights/EventDetails.tsx +117 -37
- package/src/components/features/insights/EventHistoryCard.tsx +112 -26
- package/src/components/features/insights/EventHistoryList.tsx +2 -2
- package/src/components/features/insights/InsightsControls.tsx +265 -102
- 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 +176 -173
- 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 +52 -10
- package/src/components/features/insights/InstallationMatchesCard.tsx +1 -1
- package/src/components/features/insights/ReleaseActivity.tsx +188 -0
- package/src/components/features/insights/ReportingDevicesSummary.tsx +65 -0
- package/src/components/features/insights/UpdateFailures.tsx +319 -0
- package/src/components/features/insights/insightsFeatureUi.tsx +35 -0
- package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -1
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/empty.tsx +101 -0
- 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 +12 -17
- package/src/lib/api-keys-api.ts +0 -28
- package/src/lib/api-keys-rpc.ts +27 -38
- package/src/lib/api-rpc.ts +197 -221
- package/src/lib/api.ts +11 -13
- package/src/lib/bundle-activity.ts +31 -0
- 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 +91 -0
- package/src/lib/insights-api.ts +37 -1
- package/src/lib/insights-failures-rpc.ts +21 -0
- package/src/lib/insights-failures.ts +129 -0
- package/src/lib/insights-recovery-rpc.ts +51 -0
- package/src/lib/insights-recovery.ts +68 -0
- package/src/lib/insights-retention.ts +21 -0
- package/src/lib/insights-rpc.ts +30 -12
- package/src/lib/insights-search.ts +58 -0
- package/src/lib/insights-usage-rpc.ts +29 -0
- package/src/lib/insights-usage.ts +74 -0
- package/src/lib/insights-view.ts +38 -2
- 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 +37 -0
- package/src/lib/server/config.server.ts +16 -32
- package/src/lib/server/console-feature-apis.ts +59 -0
- package/src/lib/server/console-runtime.server.ts +6 -40
- package/src/lib/server/deleteBundle.ts +16 -25
- package/src/lib/server/downloadBundle.ts +7 -10
- package/src/lib/server/getBundleChildren.ts +31 -122
- package/src/lib/server/insightsRecovery.ts +55 -0
- package/src/lib/server/insightsUsage.ts +63 -0
- package/src/lib/server/listReleases.ts +109 -44
- package/src/lib/server/releaseReachability.ts +4 -3
- package/src/lib/server/runtime.server.ts +132 -31
- package/src/lib/server/updateFailures.ts +31 -0
- package/src/routeTree.gen.ts +21 -21
- 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 +167 -104
- package/src/routes/insights.tsx +100 -59
- package/src/routes/insights_.distribution.tsx +398 -0
- package/src/routes/installations.tsx +98 -41
- package/src/vite-internal.ts +6 -3
- package/.output/public/assets/HashValueDisplay-CB0Eskit.js +0 -1
- package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +0 -1
- package/.output/public/assets/api-keys-BzKocqRy.js +0 -1
- package/.output/public/assets/api-keys-api-BHcODB7q.js +0 -1
- package/.output/public/assets/card-Dhr_eFYj.js +0 -1
- package/.output/public/assets/chart-no-axes-combined-Dy4WJv2U.js +0 -1
- package/.output/public/assets/composite-Bp98A0Ld.js +0 -1
- package/.output/public/assets/createServerFn-CA8fs7QD.js +0 -3
- package/.output/public/assets/dialog-CUTrcPlB.js +0 -1
- package/.output/public/assets/dist-C_NydL0e.js +0 -1
- package/.output/public/assets/index-DdAz-CXv.js +0 -10
- package/.output/public/assets/input-group-cSOG4wh_.js +0 -1
- package/.output/public/assets/insights-C0PBSZ-n.js +0 -1
- package/.output/public/assets/installations-CE33wgbY.js +0 -2
- package/.output/public/assets/installations-CStZja8x.js +0 -1
- package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +0 -1
- package/.output/public/assets/link-CI8idAUi.js +0 -1
- package/.output/public/assets/loader-circle-BoxLrsOu.js +0 -1
- package/.output/public/assets/preload-helper-CQ36nxok.js +0 -1
- package/.output/public/assets/refresh-cw-Bqc72UZG.js +0 -1
- package/.output/public/assets/routes-DGVXdzpw.js +0 -2
- package/.output/public/assets/routes-DTlwOr8I.js +0 -1
- package/.output/public/assets/shield-check-uK3MHWJZ.js +0 -1
- package/.output/public/assets/signing-CMf4aqDO.js +0 -1
- package/.output/public/assets/styles-BCvbtKsm.css +0 -2
- package/.output/public/assets/table-2vcV89QB.js +0 -1
- package/.output/public/assets/useCompositeListItem-CHLITE_4.js +0 -1
- package/.output/public/assets/x-DCd0j8c6.js +0 -1
- package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
- package/.output/server/_ssr/api-keys-api-BUPQVSpk.mjs +0 -60
- package/.output/server/_ssr/api-keys-rpc-Cs7tS5_j.mjs +0 -136
- package/.output/server/_ssr/api-rpc-tAupfGQm.mjs +0 -1399
- package/.output/server/_ssr/auth.server-DW71WzsD.mjs +0 -47
- package/.output/server/_ssr/bundle-signing-rpc-BhnBw0-d.mjs +0 -16
- package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +0 -12
- package/.output/server/_ssr/card-BRiPmi0C.mjs +0 -108
- package/.output/server/_ssr/config-Dv8qLDZ0.mjs +0 -13
- package/.output/server/_ssr/config.server-afDuAwaG.mjs +0 -368
- package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +0 -45
- package/.output/server/_ssr/databaseRows-5bCRTxm-.mjs +0 -126
- package/.output/server/_ssr/getBundleChildren-C0vh4ZvE.mjs +0 -50
- package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
- package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +0 -303
- package/.output/server/_ssr/installations-CtA59MQG.mjs +0 -29
- package/.output/server/_ssr/routes-CKrrS2q2.mjs +0 -37
- package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +0 -438
- package/.output/server/_ssr/signing-Dz1WkZOf.mjs +0 -133
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
- package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
- 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/routes/signing.tsx +0 -37
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Refuses a caller the console does not authorize before a server function
|
|
5
|
+
* reads its input. TanStack Start runs a function's middleware before the
|
|
6
|
+
* function's own validator, so an anonymous or unlisted caller gets the access
|
|
7
|
+
* check's 401 or 403, never an input error, whatever it sends. Every server
|
|
8
|
+
* function except the sign-in reads in `auth-rpc.ts` uses it, and
|
|
9
|
+
* `console-access.spec.ts` checks that.
|
|
10
|
+
*/
|
|
11
|
+
export const consoleAccess = createMiddleware({ type: "function" }).server(
|
|
12
|
+
async ({ next }) => {
|
|
13
|
+
const [{ getRequest }, { requireConsoleAccess }] = await Promise.all([
|
|
14
|
+
import("@tanstack/react-start/server"),
|
|
15
|
+
import("./server/auth.server"),
|
|
16
|
+
]);
|
|
17
|
+
await requireConsoleAccess(getRequest());
|
|
18
|
+
return next();
|
|
19
|
+
},
|
|
20
|
+
);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type QueryClient,
|
|
3
|
+
queryOptions,
|
|
4
|
+
useQuery,
|
|
5
|
+
} from "@tanstack/react-query";
|
|
6
|
+
import { notFound } from "@tanstack/react-router";
|
|
7
|
+
|
|
8
|
+
import { type ConsoleFeature, consoleFeatures } from "./console-features";
|
|
9
|
+
import { getConsoleFeaturesRpc } from "./console-features-rpc";
|
|
10
|
+
|
|
11
|
+
export const consoleFeaturesQueryKey = ["console", "features"] as const;
|
|
12
|
+
|
|
13
|
+
export const consoleFeaturesQueryOptions = () =>
|
|
14
|
+
queryOptions({
|
|
15
|
+
queryKey: consoleFeaturesQueryKey,
|
|
16
|
+
queryFn: () => getConsoleFeaturesRpc(),
|
|
17
|
+
// A server's plugins change only when it restarts: one read serves a visit.
|
|
18
|
+
staleTime: Infinity,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** The features the console serves; the authorized layout loads them first. */
|
|
22
|
+
export const useConsoleFeatures = () => useQuery(consoleFeaturesQueryOptions());
|
|
23
|
+
|
|
24
|
+
/** Whether `feature` is on; false until the features load. */
|
|
25
|
+
export const useConsoleFeature = (feature: ConsoleFeature): boolean =>
|
|
26
|
+
useConsoleFeatures().data?.features[feature] === true;
|
|
27
|
+
|
|
28
|
+
/** A feature route's not-found data: the feature its guard found off. */
|
|
29
|
+
type ConsoleFeatureNotFoundData = { readonly feature: ConsoleFeature };
|
|
30
|
+
|
|
31
|
+
/** The feature in a route's not-found data, when a feature guard threw it. */
|
|
32
|
+
export const notFoundFeature = (data: unknown): ConsoleFeature | undefined => {
|
|
33
|
+
const feature =
|
|
34
|
+
typeof data === "object" && data !== null
|
|
35
|
+
? Reflect.get(data, "feature")
|
|
36
|
+
: undefined;
|
|
37
|
+
return typeof feature === "string" && Object.hasOwn(consoleFeatures, feature)
|
|
38
|
+
? (feature as ConsoleFeature)
|
|
39
|
+
: undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A feature route's `beforeLoad`: not found, naming the feature, when the
|
|
44
|
+
* console does not serve it. A failed features read lets the route load: the
|
|
45
|
+
* root layout answers a visitor it does not authorize, and the route's own
|
|
46
|
+
* reads report any other failure. The failure leaves the cache, which a
|
|
47
|
+
* server render sends to the browser: a refused visitor's failure is the 401
|
|
48
|
+
* or 403 Response, which the render cannot serialize.
|
|
49
|
+
*/
|
|
50
|
+
export const requireConsoleFeature = async (
|
|
51
|
+
queryClient: QueryClient,
|
|
52
|
+
feature: ConsoleFeature,
|
|
53
|
+
): Promise<void> => {
|
|
54
|
+
const featureSet = await queryClient
|
|
55
|
+
.ensureQueryData(consoleFeaturesQueryOptions())
|
|
56
|
+
.catch(() => {
|
|
57
|
+
queryClient.removeQueries({
|
|
58
|
+
queryKey: consoleFeaturesQueryKey,
|
|
59
|
+
exact: true,
|
|
60
|
+
});
|
|
61
|
+
return undefined;
|
|
62
|
+
});
|
|
63
|
+
if (featureSet !== undefined && !featureSet.features[feature]) {
|
|
64
|
+
throw notFound({
|
|
65
|
+
data: { feature } satisfies ConsoleFeatureNotFoundData,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
|
|
3
|
+
import { consoleAccess } from "./console-access";
|
|
4
|
+
import type { ConsoleFeatureSet } from "./console-features";
|
|
5
|
+
|
|
6
|
+
/** The features this console serves, for navigation and route guards. */
|
|
7
|
+
export const getConsoleFeaturesRpc = createServerFn({ method: "GET" })
|
|
8
|
+
.middleware([consoleAccess])
|
|
9
|
+
.handler(async (): Promise<ConsoleFeatureSet> => {
|
|
10
|
+
const { prepareConfig } = await import("./server/config.server");
|
|
11
|
+
const { runtime } = await prepareConfig();
|
|
12
|
+
return { features: await runtime.features(), remote: runtime.remote };
|
|
13
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console's features and the server plugin each one needs. The server is
|
|
3
|
+
* the source of truth: the console serves a feature only when the server runs
|
|
4
|
+
* its plugin, and against a self-hosted server (`standaloneRepository`) only
|
|
5
|
+
* the features its admin API serves (`remote`); the rest read the database.
|
|
6
|
+
* Navigation, route guards, and server functions all read this registry. Its
|
|
7
|
+
* other halves, keyed by the same features, hold what serves each feature on
|
|
8
|
+
* the server (`server/console-feature-apis.ts`) and what each adds to pages
|
|
9
|
+
* (`components/features/console-feature-ui.tsx`).
|
|
10
|
+
*/
|
|
11
|
+
export const consoleFeatures = {
|
|
12
|
+
/** Event history and installation lookups, which the admin API serves too. */
|
|
13
|
+
insights: { plugin: "insights", label: "Insights", remote: true },
|
|
14
|
+
/** App usage, distribution, and release activity, read from the database. */
|
|
15
|
+
insightsAnalytics: { plugin: "insights", label: "Insights", remote: false },
|
|
16
|
+
/** API key management: `apiKeys()` mounts no admin routes to manage them. */
|
|
17
|
+
apiKeys: { plugin: "apiKeys", label: "API keys", remote: false },
|
|
18
|
+
} as const satisfies Readonly<
|
|
19
|
+
Record<
|
|
20
|
+
string,
|
|
21
|
+
{
|
|
22
|
+
readonly plugin: string;
|
|
23
|
+
/** How navigation and the not-installed page name the feature. */
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly remote: boolean;
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
>;
|
|
29
|
+
|
|
30
|
+
export type ConsoleFeature = keyof typeof consoleFeatures;
|
|
31
|
+
|
|
32
|
+
/** Whether each feature is on. */
|
|
33
|
+
export type ConsoleFeatures = Readonly<Record<ConsoleFeature, boolean>>;
|
|
34
|
+
|
|
35
|
+
/** What the console serves, as its client reads it. */
|
|
36
|
+
export type ConsoleFeatureSet = {
|
|
37
|
+
readonly features: ConsoleFeatures;
|
|
38
|
+
/** Whether the console reaches a self-hosted server through its admin API. */
|
|
39
|
+
readonly remote: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const featureIds = Object.keys(consoleFeatures) as ConsoleFeature[];
|
|
43
|
+
|
|
44
|
+
/** The features on for the plugins a server runs, from where the console reads them. */
|
|
45
|
+
export const resolveConsoleFeatures = (
|
|
46
|
+
plugins: readonly string[],
|
|
47
|
+
{ remote }: { readonly remote: boolean },
|
|
48
|
+
): ConsoleFeatures => {
|
|
49
|
+
const installed = new Set(plugins);
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
featureIds.map((feature) => {
|
|
52
|
+
const { plugin, remote: servedRemotely } = consoleFeatures[feature];
|
|
53
|
+
return [feature, installed.has(plugin) && (servedRemotely || !remote)];
|
|
54
|
+
}),
|
|
55
|
+
) as Record<ConsoleFeature, boolean>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Why a feature is off: its plugin is missing, or it reads a database the console does not open. */
|
|
59
|
+
export const unavailableReason = (
|
|
60
|
+
feature: ConsoleFeature,
|
|
61
|
+
{ remote }: { readonly remote: boolean },
|
|
62
|
+
): "plugin" | "remote" =>
|
|
63
|
+
remote && !consoleFeatures[feature].remote ? "remote" : "plugin";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A server function refused a feature the console does not serve: the
|
|
67
|
+
* features' 404. The client recognizes it by `name`, which survives
|
|
68
|
+
* serialization where the class does not.
|
|
69
|
+
*/
|
|
70
|
+
export class ConsoleFeatureUnavailableError extends Error {
|
|
71
|
+
override readonly name = "ConsoleFeatureUnavailableError";
|
|
72
|
+
readonly status = 404;
|
|
73
|
+
|
|
74
|
+
constructor(
|
|
75
|
+
readonly feature: ConsoleFeature,
|
|
76
|
+
{ remote }: { readonly remote: boolean },
|
|
77
|
+
) {
|
|
78
|
+
super(
|
|
79
|
+
unavailableReason(feature, { remote }) === "remote"
|
|
80
|
+
? "The console reads this from the database, and it reaches a self-hosted server through its admin API instead."
|
|
81
|
+
: `The server runs without the ${consoleFeatures[feature].plugin}() plugin.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const isConsoleFeatureUnavailableError = (
|
|
87
|
+
error: unknown,
|
|
88
|
+
): error is Error & { readonly feature: ConsoleFeature } =>
|
|
89
|
+
error instanceof Error &&
|
|
90
|
+
error.name === "ConsoleFeatureUnavailableError" &&
|
|
91
|
+
featureIds.includes(Reflect.get(error, "feature") as ConsoleFeature);
|
package/src/lib/insights-api.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import type { InsightsEventPageInput } from "@hot-updater/server";
|
|
1
|
+
import type { InsightsEventPageInput } from "@hot-updater/server/plugins/insights";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
readUpdateFailuresInput,
|
|
6
|
+
type UpdateFailuresInput,
|
|
7
|
+
} from "./insights-failures";
|
|
8
|
+
import { getUpdateFailuresRpc } from "./insights-failures-rpc";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_INSIGHTS_RETENTION,
|
|
11
|
+
type InsightsRetentionDays,
|
|
12
|
+
} from "./insights-retention";
|
|
4
13
|
import {
|
|
5
14
|
findInsightsInstallationsRpc,
|
|
6
15
|
getInsightsInstallationRpc,
|
|
16
|
+
getInsightsRetentionRpc,
|
|
7
17
|
getReportingInstallationsRpc,
|
|
8
18
|
listInsightsEventsRpc,
|
|
9
19
|
listInsightsInstallationEventsRpc,
|
|
@@ -98,3 +108,29 @@ export const useInsightsInstallationEventsQuery = (
|
|
|
98
108
|
refetchOnWindowFocus: true,
|
|
99
109
|
staleTime: STALE_TIME_MS,
|
|
100
110
|
});
|
|
111
|
+
|
|
112
|
+
/** A release's or a channel's update failures; lifetime for a release without a window. */
|
|
113
|
+
export const useUpdateFailuresQuery = (
|
|
114
|
+
input: UpdateFailuresInput,
|
|
115
|
+
enabled = true,
|
|
116
|
+
) =>
|
|
117
|
+
useQuery({
|
|
118
|
+
queryKey: ["insights", "update-failures", input],
|
|
119
|
+
queryFn: () =>
|
|
120
|
+
getUpdateFailuresRpc({ data: readUpdateFailuresInput(input) }),
|
|
121
|
+
enabled,
|
|
122
|
+
refetchOnWindowFocus: true,
|
|
123
|
+
staleTime: STALE_TIME_MS,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* How long the server keeps Insights rows; the defaults until it answers, or
|
|
128
|
+
* when it does not report them.
|
|
129
|
+
*/
|
|
130
|
+
export const useInsightsRetention = (): InsightsRetentionDays =>
|
|
131
|
+
useQuery({
|
|
132
|
+
queryKey: ["insights", "retention"],
|
|
133
|
+
queryFn: () => getInsightsRetentionRpc(),
|
|
134
|
+
// A server's periods change only when it restarts.
|
|
135
|
+
staleTime: Infinity,
|
|
136
|
+
}).data ?? DEFAULT_INSIGHTS_RETENTION;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
|
|
3
|
+
import { consoleAccess } from "./console-access";
|
|
4
|
+
import { readUpdateFailuresInput } from "./insights-failures";
|
|
5
|
+
|
|
6
|
+
export const getUpdateFailuresRpc = createServerFn({ method: "GET" })
|
|
7
|
+
.middleware([consoleAccess])
|
|
8
|
+
.validator(readUpdateFailuresInput)
|
|
9
|
+
.handler(async ({ data }) => {
|
|
10
|
+
const [{ prepareConfig }, { getUpdateFailuresReport }, { requireFeature }] =
|
|
11
|
+
await Promise.all([
|
|
12
|
+
import("./server/config.server"),
|
|
13
|
+
import("./server/updateFailures"),
|
|
14
|
+
import("./server/runtime.server"),
|
|
15
|
+
]);
|
|
16
|
+
const { runtime } = await prepareConfig();
|
|
17
|
+
return getUpdateFailuresReport(
|
|
18
|
+
await requireFeature(runtime, "insights"),
|
|
19
|
+
data,
|
|
20
|
+
);
|
|
21
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InsightsFailureBreakdown,
|
|
3
|
+
InsightsUpdateFailures,
|
|
4
|
+
} from "@hot-updater/server/plugins/insights";
|
|
5
|
+
|
|
6
|
+
import { recoveryWindows } from "./insights-recovery";
|
|
7
|
+
import type { InsightsWindow } from "./insights-rpc";
|
|
8
|
+
import type { InsightsEventRow } from "./insights-view";
|
|
9
|
+
|
|
10
|
+
export type { InsightsFailureBreakdown };
|
|
11
|
+
|
|
12
|
+
export type UpdateFailuresInput = {
|
|
13
|
+
readonly platform: "ios" | "android";
|
|
14
|
+
readonly channel: string;
|
|
15
|
+
/** A release's failures; the channel's without it. */
|
|
16
|
+
readonly releaseId?: string;
|
|
17
|
+
/** The period that ends with the current hour; a release's since its first report without it. */
|
|
18
|
+
readonly window?: InsightsWindow;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type UpdateFailuresReport = InsightsUpdateFailures & {
|
|
22
|
+
/** Where the period starts, or null since the release's first report. */
|
|
23
|
+
readonly startMs: number | null;
|
|
24
|
+
readonly endMs: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function readUpdateFailuresInput(
|
|
28
|
+
input: UpdateFailuresInput,
|
|
29
|
+
): UpdateFailuresInput {
|
|
30
|
+
if (
|
|
31
|
+
!input ||
|
|
32
|
+
(input.platform !== "ios" && input.platform !== "android") ||
|
|
33
|
+
typeof input.channel !== "string" ||
|
|
34
|
+
!input.channel.trim() ||
|
|
35
|
+
input.channel.length > 1_024 ||
|
|
36
|
+
(input.releaseId !== undefined &&
|
|
37
|
+
(typeof input.releaseId !== "string" ||
|
|
38
|
+
!input.releaseId.trim() ||
|
|
39
|
+
input.releaseId.length > 36)) ||
|
|
40
|
+
(input.window === undefined
|
|
41
|
+
? input.releaseId === undefined
|
|
42
|
+
: !Object.hasOwn(recoveryWindows, input.window))
|
|
43
|
+
) {
|
|
44
|
+
throw new Error("Choose a platform, channel, and time window.");
|
|
45
|
+
}
|
|
46
|
+
return input;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The failure rate of update attempts: failed download and install reports,
|
|
51
|
+
* over those plus download reports. Null without either.
|
|
52
|
+
*/
|
|
53
|
+
export const failureRate = ({
|
|
54
|
+
failedUpdates,
|
|
55
|
+
downloads,
|
|
56
|
+
}: Pick<InsightsUpdateFailures, "failedUpdates" | "downloads">):
|
|
57
|
+
| number
|
|
58
|
+
| null =>
|
|
59
|
+
failedUpdates + downloads === 0
|
|
60
|
+
? null
|
|
61
|
+
: failedUpdates / (failedUpdates + downloads);
|
|
62
|
+
|
|
63
|
+
/** Of the downloads that tried a patch, the share that fell back to files or the archive. */
|
|
64
|
+
export const patchFallbackRate = ({
|
|
65
|
+
patchDownloads,
|
|
66
|
+
patchFallbacks,
|
|
67
|
+
}: Pick<InsightsUpdateFailures, "patchDownloads" | "patchFallbacks">):
|
|
68
|
+
| number
|
|
69
|
+
| null =>
|
|
70
|
+
patchDownloads + patchFallbacks === 0
|
|
71
|
+
? null
|
|
72
|
+
: patchFallbacks / (patchDownloads + patchFallbacks);
|
|
73
|
+
|
|
74
|
+
/** Installations whose update check failed, over the active ones; estimates, so at most 100%. */
|
|
75
|
+
export const checkFailureRate = ({
|
|
76
|
+
failedInstallations,
|
|
77
|
+
activeInstallations,
|
|
78
|
+
}: NonNullable<InsightsUpdateFailures["checks"]>): number | null =>
|
|
79
|
+
activeInstallations === 0
|
|
80
|
+
? null
|
|
81
|
+
: Math.min(1, failedInstallations / activeInstallations);
|
|
82
|
+
|
|
83
|
+
export const formatRate = (rate: number | null): string =>
|
|
84
|
+
rate === null ? "—" : `${(rate * 100).toFixed(2)}%`;
|
|
85
|
+
|
|
86
|
+
const STAGES: Readonly<Record<string, string>> = {
|
|
87
|
+
check: "Update check",
|
|
88
|
+
download: "Download",
|
|
89
|
+
install: "Install",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const REASONS: Readonly<Record<string, string>> = {
|
|
93
|
+
network: "Network",
|
|
94
|
+
http: "HTTP error",
|
|
95
|
+
invalid_response: "Invalid response",
|
|
96
|
+
hash_mismatch: "Hash mismatch",
|
|
97
|
+
signature: "Signature",
|
|
98
|
+
patch: "Patch",
|
|
99
|
+
extract: "Extract",
|
|
100
|
+
storage: "Storage",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const failureStageLabel = (stage: string): string =>
|
|
104
|
+
STAGES[stage] ?? "Unknown stage";
|
|
105
|
+
|
|
106
|
+
export const failureReasonLabel = (reason: string): string =>
|
|
107
|
+
REASONS[reason] ?? "Unknown reason";
|
|
108
|
+
|
|
109
|
+
/** What else the client knew: its resource, HTTP status, origin code, and transport. */
|
|
110
|
+
export const failureDetailParts = (detail: {
|
|
111
|
+
readonly resource?: string | null;
|
|
112
|
+
readonly httpStatus?: number | null;
|
|
113
|
+
readonly originCode?: string | null;
|
|
114
|
+
readonly transport?: string | null;
|
|
115
|
+
}): string[] => [
|
|
116
|
+
...(detail.httpStatus == null ? [] : [`HTTP ${detail.httpStatus}`]),
|
|
117
|
+
...(detail.originCode == null ? [] : [detail.originCode]),
|
|
118
|
+
...(detail.transport == null ? [] : [detail.transport]),
|
|
119
|
+
...(detail.resource == null ? [] : [detail.resource]),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
/** An update failure as one line: where, why, and what else the client knew. */
|
|
123
|
+
export const describeFailure = (
|
|
124
|
+
failure: NonNullable<InsightsEventRow["failure"]>,
|
|
125
|
+
): string =>
|
|
126
|
+
[
|
|
127
|
+
`${failureStageLabel(failure.stage)} failed: ${failureReasonLabel(failure.reason)}`,
|
|
128
|
+
...failureDetailParts(failure),
|
|
129
|
+
].join(" · ");
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
|
|
3
|
+
import type { BundleActivityInput } from "./bundle-activity";
|
|
4
|
+
import { consoleAccess } from "./console-access";
|
|
5
|
+
import { readRecoveryInput } from "./insights-recovery";
|
|
6
|
+
|
|
7
|
+
export function readBundleActivityInput(
|
|
8
|
+
inputs: readonly BundleActivityInput[],
|
|
9
|
+
) {
|
|
10
|
+
if (!Array.isArray(inputs) || inputs.length > 20)
|
|
11
|
+
throw new Error("Choose up to 20 bundles.");
|
|
12
|
+
return inputs.map((input) => {
|
|
13
|
+
readRecoveryInput({ ...input, window: "30d" });
|
|
14
|
+
if (!input.releaseId) throw new Error("Choose a bundle ID.");
|
|
15
|
+
return input;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const getBundleActivityRpc = createServerFn({ method: "GET" })
|
|
20
|
+
.middleware([consoleAccess])
|
|
21
|
+
.validator(readBundleActivityInput)
|
|
22
|
+
.handler(async ({ data }) => {
|
|
23
|
+
const [{ prepareConfig }, { getBundleActivity }, { requireFeature }] =
|
|
24
|
+
await Promise.all([
|
|
25
|
+
import("./server/config.server"),
|
|
26
|
+
import("./server/bundleActivity"),
|
|
27
|
+
import("./server/runtime.server"),
|
|
28
|
+
]);
|
|
29
|
+
const { runtime } = await prepareConfig();
|
|
30
|
+
return getBundleActivity(
|
|
31
|
+
await requireFeature(runtime, "insightsAnalytics"),
|
|
32
|
+
data,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const getRecoveryReportRpc = createServerFn({ method: "GET" })
|
|
37
|
+
.middleware([consoleAccess])
|
|
38
|
+
.validator(readRecoveryInput)
|
|
39
|
+
.handler(async ({ data }) => {
|
|
40
|
+
const [{ prepareConfig }, { getRecoveryReport }, { requireFeature }] =
|
|
41
|
+
await Promise.all([
|
|
42
|
+
import("./server/config.server"),
|
|
43
|
+
import("./server/insightsRecovery"),
|
|
44
|
+
import("./server/runtime.server"),
|
|
45
|
+
]);
|
|
46
|
+
const { runtime } = await prepareConfig();
|
|
47
|
+
return getRecoveryReport(
|
|
48
|
+
await requireFeature(runtime, "insightsAnalytics"),
|
|
49
|
+
data,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { InsightsCoverage } from "@hot-updater/server/plugins/insights";
|
|
2
|
+
|
|
3
|
+
import type { InsightsWindow } from "./insights-rpc";
|
|
4
|
+
|
|
5
|
+
export type RecoveryInput = {
|
|
6
|
+
readonly platform: "ios" | "android";
|
|
7
|
+
readonly channel: string;
|
|
8
|
+
readonly window: InsightsWindow;
|
|
9
|
+
readonly releaseId?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RecoveryReport = {
|
|
13
|
+
readonly downloads: number;
|
|
14
|
+
/**
|
|
15
|
+
* Distinct installations that reported in the scope and period, or that
|
|
16
|
+
* launched the release; estimated.
|
|
17
|
+
*/
|
|
18
|
+
readonly activeInstallations: number;
|
|
19
|
+
/**
|
|
20
|
+
* Installation-days: each installation once for each UTC day it launched,
|
|
21
|
+
* and once more on a day an update applied or recovered.
|
|
22
|
+
*/
|
|
23
|
+
readonly activeDays: number;
|
|
24
|
+
readonly failedLaunches: number;
|
|
25
|
+
readonly points: readonly {
|
|
26
|
+
readonly startMs: number;
|
|
27
|
+
/** That UTC day's daily active installations. */
|
|
28
|
+
readonly dailyActiveInstallations: number;
|
|
29
|
+
readonly failedLaunches: number;
|
|
30
|
+
}[];
|
|
31
|
+
readonly startMs: number;
|
|
32
|
+
readonly endMs: number;
|
|
33
|
+
readonly measuredAtMs: number;
|
|
34
|
+
readonly coverage: InsightsCoverage;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const recoveryWindows = {
|
|
38
|
+
"24h": { durationMs: 86_400_000, intervalMs: 3_600_000 },
|
|
39
|
+
"7d": { durationMs: 7 * 86_400_000, intervalMs: 6 * 3_600_000 },
|
|
40
|
+
"30d": { durationMs: 30 * 86_400_000, intervalMs: 86_400_000 },
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where a period ends: the end of the current UTC hour, as the server's
|
|
45
|
+
* reporting overview ends. The counters keep whole hours, so this is the
|
|
46
|
+
* latest end that still counts a report received just now; every Insights
|
|
47
|
+
* card ends here, so their numbers cover the same hours.
|
|
48
|
+
*/
|
|
49
|
+
export const insightsPeriodEnd = (now: number): number =>
|
|
50
|
+
Math.ceil(now / 3_600_000) * 3_600_000;
|
|
51
|
+
|
|
52
|
+
export function readRecoveryInput(input: RecoveryInput): RecoveryInput {
|
|
53
|
+
if (
|
|
54
|
+
!input ||
|
|
55
|
+
(input.platform !== "ios" && input.platform !== "android") ||
|
|
56
|
+
typeof input.channel !== "string" ||
|
|
57
|
+
!input.channel.trim() ||
|
|
58
|
+
input.channel.length > 1_024 ||
|
|
59
|
+
!Object.hasOwn(recoveryWindows, input.window) ||
|
|
60
|
+
(input.releaseId !== undefined &&
|
|
61
|
+
(typeof input.releaseId !== "string" ||
|
|
62
|
+
!input.releaseId.trim() ||
|
|
63
|
+
input.releaseId.length > 1_024))
|
|
64
|
+
) {
|
|
65
|
+
throw new Error("Choose a platform, channel, and time window.");
|
|
66
|
+
}
|
|
67
|
+
return input;
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** How long the server's `insights()` keeps rows, in days. */
|
|
2
|
+
export interface InsightsRetentionDays {
|
|
3
|
+
/** Raw events and hourly totals. */
|
|
4
|
+
readonly rawDays: number;
|
|
5
|
+
/** Daily totals, distributions, and each installation's latest report. */
|
|
6
|
+
readonly dailyDays: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** `insights()`'s defaults, for a server that does not report its periods. */
|
|
10
|
+
export const DEFAULT_INSIGHTS_RETENTION: InsightsRetentionDays = {
|
|
11
|
+
rawDays: 90,
|
|
12
|
+
dailyDays: 400,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const formatDays = (days: number): string =>
|
|
16
|
+
`${days.toLocaleString("en")} ${days === 1 ? "day" : "days"}`;
|
|
17
|
+
|
|
18
|
+
/** Whether daily totals cover App usage's 12-month period: 52 weeks. */
|
|
19
|
+
export const keepsTwelveMonths = ({
|
|
20
|
+
dailyDays,
|
|
21
|
+
}: InsightsRetentionDays): boolean => dailyDays >= 364;
|
package/src/lib/insights-rpc.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ReportingOverview,
|
|
3
2
|
InsightsScope,
|
|
3
|
+
ReportingOverview,
|
|
4
4
|
InsightsEventPageInput,
|
|
5
5
|
InsightsInstallationEventPageInput,
|
|
6
6
|
ActiveInstallationWindow,
|
|
7
|
-
|
|
8
|
-
} from "@hot-updater/server";
|
|
7
|
+
} from "@hot-updater/server/plugins/insights";
|
|
9
8
|
import { createServerFn } from "@tanstack/react-start";
|
|
10
9
|
|
|
10
|
+
import { consoleAccess } from "./console-access";
|
|
11
11
|
import type {
|
|
12
12
|
InsightsInstallationViewRow,
|
|
13
13
|
InsightsViewPage,
|
|
@@ -30,10 +30,14 @@ type InstallationsPageInput = {
|
|
|
30
30
|
readonly limit: number;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const {
|
|
36
|
-
|
|
33
|
+
/** The Insights reads, once the console's access check and the feature guard pass. */
|
|
34
|
+
const insightsReads = async () => {
|
|
35
|
+
const [{ prepareConfig }, { requireFeature }] = await Promise.all([
|
|
36
|
+
import("./server/config.server"),
|
|
37
|
+
import("./server/runtime.server"),
|
|
38
|
+
]);
|
|
39
|
+
const { runtime } = await prepareConfig();
|
|
40
|
+
return requireFeature(runtime, "insights");
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
const readOverview = (input: InsightsOverviewInput) => input;
|
|
@@ -44,27 +48,41 @@ const readInstallationPage = (input: InstallationsPageInput) => input;
|
|
|
44
48
|
const readInstallId = (input: { readonly installId: string }) => input;
|
|
45
49
|
|
|
46
50
|
export const getReportingInstallationsRpc = createServerFn({ method: "GET" })
|
|
51
|
+
.middleware([consoleAccess])
|
|
47
52
|
.validator(readOverview)
|
|
48
|
-
.handler(async ({ data }) =>
|
|
53
|
+
.handler(async ({ data }) =>
|
|
54
|
+
(await insightsReads()).getReportingOverview(data),
|
|
55
|
+
);
|
|
49
56
|
|
|
50
57
|
export const listInsightsEventsRpc = createServerFn({ method: "GET" })
|
|
58
|
+
.middleware([consoleAccess])
|
|
51
59
|
.validator(readEventsPage)
|
|
52
|
-
.handler(async ({ data }) => (await
|
|
60
|
+
.handler(async ({ data }) => (await insightsReads()).listEvents(data));
|
|
53
61
|
|
|
54
62
|
export const listInsightsInstallationEventsRpc = createServerFn({
|
|
55
63
|
method: "GET",
|
|
56
64
|
})
|
|
65
|
+
.middleware([consoleAccess])
|
|
57
66
|
.validator(readInstallationEventsPage)
|
|
58
|
-
.handler(async ({ data }) =>
|
|
67
|
+
.handler(async ({ data }) =>
|
|
68
|
+
(await insightsReads()).listInstallationEvents(data),
|
|
69
|
+
);
|
|
59
70
|
|
|
60
71
|
export const getInsightsInstallationRpc = createServerFn({ method: "GET" })
|
|
72
|
+
.middleware([consoleAccess])
|
|
61
73
|
.validator(readInstallId)
|
|
62
|
-
.handler(async ({ data }) => (await
|
|
74
|
+
.handler(async ({ data }) => (await insightsReads()).getInstallation(data));
|
|
75
|
+
|
|
76
|
+
/** How long the server's `insights()` keeps rows. */
|
|
77
|
+
export const getInsightsRetentionRpc = createServerFn({ method: "GET" })
|
|
78
|
+
.middleware([consoleAccess])
|
|
79
|
+
.handler(async () => (await insightsReads()).getRetention());
|
|
63
80
|
|
|
64
81
|
export const findInsightsInstallationsRpc = createServerFn({ method: "GET" })
|
|
82
|
+
.middleware([consoleAccess])
|
|
65
83
|
.validator(readInstallationPage)
|
|
66
84
|
.handler(async ({ data }) => {
|
|
67
|
-
const insights = await
|
|
85
|
+
const insights = await insightsReads();
|
|
68
86
|
const install = data.cursor
|
|
69
87
|
? null
|
|
70
88
|
: await insights.getInstallation({ installId: data.identity });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { InsightsWindow } from "./insights-api";
|
|
2
|
+
import type { AppUsageScope, UsageWindow } from "./insights-usage";
|
|
3
|
+
|
|
4
|
+
const readText = (value: unknown) =>
|
|
5
|
+
typeof value === "string" && value.trim().length > 0 && value.length <= 1024
|
|
6
|
+
? value
|
|
7
|
+
: undefined;
|
|
8
|
+
const readWindow = (value: unknown): InsightsWindow | undefined =>
|
|
9
|
+
value === "24h" || value === "7d" || value === "30d" ? value : undefined;
|
|
10
|
+
const readUsageWindow = (value: unknown): UsageWindow | undefined =>
|
|
11
|
+
value === "12m" ? value : readWindow(value);
|
|
12
|
+
|
|
13
|
+
export type InsightsSearch = {
|
|
14
|
+
platform?: AppUsageScope["platform"];
|
|
15
|
+
channel?: string;
|
|
16
|
+
appVersion?: string;
|
|
17
|
+
window?: UsageWindow;
|
|
18
|
+
bundleWindow?: InsightsWindow;
|
|
19
|
+
healthPlatform?: "ios" | "android";
|
|
20
|
+
healthChannel?: string;
|
|
21
|
+
releaseId?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function validateInsightsSearch(
|
|
25
|
+
search: Record<string, unknown>,
|
|
26
|
+
): InsightsSearch {
|
|
27
|
+
return {
|
|
28
|
+
platform:
|
|
29
|
+
search.platform === "all" ||
|
|
30
|
+
search.platform === "ios" ||
|
|
31
|
+
search.platform === "android"
|
|
32
|
+
? search.platform
|
|
33
|
+
: undefined,
|
|
34
|
+
channel: readText(search.channel),
|
|
35
|
+
appVersion: readText(search.appVersion),
|
|
36
|
+
window: readUsageWindow(search.window),
|
|
37
|
+
bundleWindow: readWindow(search.bundleWindow),
|
|
38
|
+
healthPlatform:
|
|
39
|
+
search.healthPlatform === "ios" || search.healthPlatform === "android"
|
|
40
|
+
? search.healthPlatform
|
|
41
|
+
: undefined,
|
|
42
|
+
healthChannel: readText(search.healthChannel),
|
|
43
|
+
releaseId: readText(search.releaseId),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateDistributionSearch(search: Record<string, unknown>) {
|
|
48
|
+
return {
|
|
49
|
+
...validateInsightsSearch(search),
|
|
50
|
+
version: readText(search.version),
|
|
51
|
+
page:
|
|
52
|
+
typeof search.page === "number" &&
|
|
53
|
+
Number.isSafeInteger(search.page) &&
|
|
54
|
+
search.page > 0
|
|
55
|
+
? search.page
|
|
56
|
+
: undefined,
|
|
57
|
+
};
|
|
58
|
+
}
|