@hot-updater/console 1.0.0-rc.2 → 1.0.0-rc.4
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/InsightsPageHeader-C-Xc2MB9.js +1 -0
- package/.output/public/assets/api-keys-C1sgC6ko.js +1 -0
- package/.output/public/assets/{api-keys-api-BHcODB7q.js → api-keys-api-LchP65vQ.js} +1 -1
- package/.output/public/assets/badge-BuMDU0Rf.js +1 -0
- package/.output/public/assets/card-BEvx_QKh.js +1 -0
- package/.output/public/assets/{chart-no-axes-combined-Dy4WJv2U.js → chart-no-axes-combined-C0G0fQ3J.js} +1 -1
- package/.output/public/assets/{composite-Bp98A0Ld.js → composite-CHTH1Xhh.js} +1 -1
- package/.output/public/assets/createServerFn-DHc2erub.js +3 -0
- package/.output/public/assets/dialog-dyZ1SphZ.js +1 -0
- package/.output/public/assets/{dist-C_NydL0e.js → dist-Bh1gaDfB.js} +1 -1
- package/.output/public/assets/field-79SVH26s.js +1 -0
- package/.output/public/assets/index-BNkxg4jz.js +10 -0
- package/.output/public/assets/input-group-BbHfaBzQ.js +1 -0
- package/.output/public/assets/insights-D_7uSKfc.js +1 -0
- package/.output/public/assets/installations-BzoHWuXw.js +1 -0
- package/.output/public/assets/{installations-CE33wgbY.js → installations-DvjbUqd1.js} +2 -2
- package/.output/public/assets/{lazyRouteComponent-Ccp3bwBY.js → lazyRouteComponent-BN0MS05o.js} +1 -1
- package/.output/public/assets/{link-CI8idAUi.js → link-BG06whwB.js} +1 -1
- package/.output/public/assets/{loader-circle-BoxLrsOu.js → loader-circle-Bbp8LGVC.js} +1 -1
- package/.output/public/assets/{refresh-cw-Bqc72UZG.js → refresh-cw-DZgiAOMk.js} +1 -1
- package/.output/public/assets/routes-BFE-oRIT.js +1 -0
- package/.output/public/assets/{routes-DGVXdzpw.js → routes-DaPMY31P.js} +2 -2
- package/.output/public/assets/{shield-check-uK3MHWJZ.js → shield-check-DoIiaaXU.js} +1 -1
- package/.output/public/assets/signing-BTRhaF5D.js +1 -0
- package/.output/public/assets/styles-C7U0oVke.css +2 -0
- package/.output/public/assets/table-BBEzJ7Xl.js +1 -0
- package/.output/public/assets/useCompositeListItem-BkPDaqyj.js +42 -0
- package/.output/server/{__23tanstack-start-server-fn-resolver-B7ioJhtE.mjs → __23tanstack-start-server-fn-resolver-Dr2ixDgQ.mjs} +40 -32
- package/.output/server/_libs/@base-ui/react+[...].mjs +1 -1
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +1 -1
- package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +1 -1
- 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 +9 -2
- package/.output/server/_libs/next-themes.mjs +1 -1
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+[...].mjs +15054 -0
- package/.output/server/_libs/sonner.mjs +1 -1
- package/.output/server/_libs/tanstack__react-query.mjs +1 -1
- package/.output/server/_runtime.mjs +10 -1
- package/.output/server/_ssr/InsightsPageHeader-C5Has8Ry.mjs +60 -0
- package/.output/server/_ssr/{api-keys-CZeTPQys.mjs → api-keys-DJml_XoK.mjs} +10 -8
- package/.output/server/_ssr/{api-keys-api-BUPQVSpk.mjs → api-keys-api-CrCZyvnl.mjs} +2 -2
- package/.output/server/_ssr/{api-keys-rpc-Cs7tS5_j.mjs → api-keys-rpc-CFaNf-q0.mjs} +3 -3
- package/.output/server/_ssr/{api-rpc-tAupfGQm.mjs → api-rpc-B61KL3Eo.mjs} +25 -25
- package/.output/server/_ssr/{auth-rpc-DUaeOWKK.mjs → auth-rpc-D_hv52-J.mjs} +4 -4
- package/.output/server/_ssr/{auth.server-DW71WzsD.mjs → auth.server-DF_shCeq.mjs} +1 -1
- package/.output/server/_ssr/badge-BgsCP9Dp.mjs +28 -0
- package/.output/server/_ssr/{bundle-signing-rpc-BhnBw0-d.mjs → bundle-signing-rpc-BBipZnFs.mjs} +2 -2
- package/.output/server/_ssr/{bundle-signing.server-C4NW5mWx.mjs → bundle-signing.server-Co_zHdJx.mjs} +1 -1
- package/.output/server/_ssr/bundleActivity-D3_fIda2.mjs +27 -0
- package/.output/server/_ssr/{card-BRiPmi0C.mjs → card-DrbxCPtG.mjs} +3 -25
- package/.output/server/_ssr/chart-qxa-_30a.mjs +162 -0
- package/.output/server/_ssr/{config.server-afDuAwaG.mjs → config.server-CBr6PDj5.mjs} +5 -5
- package/.output/server/_ssr/{createSsrRpc-DjWNJ0uc.mjs → createSsrRpc-NHF2PxuK.mjs} +4 -4
- package/.output/server/_ssr/{deleteBundle-BTGwBgzx.mjs → deleteBundle-BAq3JuGr.mjs} +1 -1
- package/.output/server/_ssr/{dialog-DS-nw-pA.mjs → dialog-DUaIgpgd.mjs} +1 -1
- package/.output/server/_ssr/{downloadBundle-CNnYxYO2.mjs → downloadBundle-B3ert-pY.mjs} +1 -1
- package/.output/server/_ssr/{table-CnZmon_Z.mjs → field-CqNUlqUq.mjs} +4 -50
- package/.output/server/_ssr/{getBundleChildren-C0vh4ZvE.mjs → getBundleChildren-COMt16m_.mjs} +1 -1
- package/.output/server/_ssr/{input-group-BpsnmvZw.mjs → input-group-Bb8ueylQ.mjs} +51 -2
- package/.output/server/_ssr/insights-B-DUbksX.mjs +502 -0
- package/.output/server/_ssr/insights-recovery-DSFM-YTL.mjs +21 -0
- package/.output/server/_ssr/insights-recovery-rpc-P71FH66n.mjs +37 -0
- package/.output/server/_ssr/{insights-rpc-38DujK0G.mjs → insights-rpc-DrIQCjpD.mjs} +2 -2
- package/.output/server/_ssr/insightsRecovery-CqcosG7B.mjs +149 -0
- package/.output/server/_ssr/{installations-CtA59MQG.mjs → installations-BQrppti7.mjs} +2 -2
- package/.output/server/_ssr/{installations-DiVKEF5T.mjs → installations-YumqpNpa.mjs} +426 -11
- package/.output/server/_ssr/{router-B9wG_9f8.mjs → router-iP95Vqi2.mjs} +14 -14
- package/.output/server/_ssr/{routes-CKrrS2q2.mjs → routes-BDabsmc7.mjs} +2 -2
- package/.output/server/_ssr/{routes-D7Yh_jfj.mjs → routes-ivsV2TDu.mjs} +224 -13
- package/.output/server/_ssr/{runtime.server-DacU1TBV.mjs → runtime.server-jmZOKIQM.mjs} +1 -1
- package/.output/server/_ssr/{server-D_C2rMzf.mjs → server-p7yrVBfD.mjs} +3 -3
- package/.output/server/_ssr/{signing-Dz1WkZOf.mjs → signing-CxCbnFCt.mjs} +5 -4
- package/.output/server/_ssr/ssr.mjs +1 -1
- package/.output/server/_ssr/table-D1mL8fJu.mjs +53 -0
- package/.output/server/_tanstack-start-manifest_v-Cwpu0C9Q.mjs +100 -0
- package/.output/server/index.mjs +154 -154
- package/package.json +3 -3
- package/src/components/features/bundles/BundleActivityChart.tsx +95 -0
- package/src/components/features/bundles/BundleInsightsSummary.tsx +121 -0
- package/src/components/features/insights/InsightsControls.tsx +1 -15
- package/src/components/features/insights/InsightsOverview.tsx +368 -179
- package/src/components/features/releases/ReleaseEditorSheet.tsx +10 -0
- package/src/components/ui/empty.tsx +101 -0
- package/src/lib/bundle-activity.ts +21 -0
- package/src/lib/insights-recovery-rpc.ts +38 -0
- package/src/lib/insights-recovery.ts +59 -0
- package/src/lib/server/bundleActivity.ts +37 -0
- package/src/lib/server/insightsHistory.ts +52 -0
- package/src/lib/server/insightsRecovery.ts +186 -0
- package/src/routes/index.tsx +33 -3
- package/src/routes/insights.tsx +1 -42
- 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/card-Dhr_eFYj.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/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-CStZja8x.js +0 -1
- package/.output/public/assets/routes-DTlwOr8I.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/HashValueDisplay-DwpGLfrl.mjs +0 -55
- package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +0 -484
- package/.output/server/_ssr/insights-BdS364Xu.mjs +0 -460
- package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +0 -97
- package/src/components/features/insights/InsightsBundleReports.tsx +0 -51
- /package/.output/public/assets/{preload-helper-CQ36nxok.js → preload-helper-uTix4PVD.js} +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="empty"
|
|
9
|
+
className={cn(
|
|
10
|
+
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="empty-header"
|
|
22
|
+
className={cn("flex max-w-sm flex-col items-center gap-1", className)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const emptyMediaVariants = cva(
|
|
29
|
+
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
30
|
+
{
|
|
31
|
+
variants: {
|
|
32
|
+
variant: {
|
|
33
|
+
default: "bg-transparent",
|
|
34
|
+
icon: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
variant: "default",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function EmptyMedia({
|
|
44
|
+
className,
|
|
45
|
+
variant = "default",
|
|
46
|
+
...props
|
|
47
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
data-slot="empty-icon"
|
|
51
|
+
data-variant={variant}
|
|
52
|
+
className={cn(emptyMediaVariants({ variant, className }))}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="empty-title"
|
|
62
|
+
className={cn("text-sm font-medium tracking-tight", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
69
|
+
return (
|
|
70
|
+
<div
|
|
71
|
+
data-slot="empty-description"
|
|
72
|
+
className={cn(
|
|
73
|
+
"text-xs/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-slot="empty-content"
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2 text-xs/relaxed text-balance",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
Empty,
|
|
96
|
+
EmptyHeader,
|
|
97
|
+
EmptyTitle,
|
|
98
|
+
EmptyDescription,
|
|
99
|
+
EmptyContent,
|
|
100
|
+
EmptyMedia,
|
|
101
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
|
|
3
|
+
import type { RecoveryInput } from "./insights-recovery";
|
|
4
|
+
import { getBundleActivityRpc } from "./insights-recovery-rpc";
|
|
5
|
+
|
|
6
|
+
export type BundleActivityInput = Pick<
|
|
7
|
+
RecoveryInput,
|
|
8
|
+
"platform" | "channel"
|
|
9
|
+
> & { readonly releaseId: string };
|
|
10
|
+
|
|
11
|
+
export function useBundleActivityQuery(inputs: readonly BundleActivityInput[]) {
|
|
12
|
+
const sorted = [...inputs].sort((a, b) =>
|
|
13
|
+
a.releaseId.localeCompare(b.releaseId),
|
|
14
|
+
);
|
|
15
|
+
return useQuery({
|
|
16
|
+
queryKey: ["bundle-activity", sorted],
|
|
17
|
+
queryFn: () => getBundleActivityRpc({ data: sorted }),
|
|
18
|
+
enabled: sorted.length > 0,
|
|
19
|
+
staleTime: 30_000,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
|
|
3
|
+
import type { BundleActivityInput } from "./bundle-activity";
|
|
4
|
+
import { readRecoveryInput } from "./insights-recovery";
|
|
5
|
+
|
|
6
|
+
export function readBundleActivityInput(
|
|
7
|
+
inputs: readonly BundleActivityInput[],
|
|
8
|
+
) {
|
|
9
|
+
if (!Array.isArray(inputs) || inputs.length > 20)
|
|
10
|
+
throw new Error("Choose up to 20 bundles.");
|
|
11
|
+
return inputs.map((input) => {
|
|
12
|
+
readRecoveryInput({ ...input, window: "30d" });
|
|
13
|
+
if (!input.releaseId) throw new Error("Choose a bundle ID.");
|
|
14
|
+
return input;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const getBundleActivityRpc = createServerFn({ method: "GET" })
|
|
19
|
+
.validator(readBundleActivityInput)
|
|
20
|
+
.handler(async ({ data }) => {
|
|
21
|
+
const [{ prepareConfig }, { getBundleActivity }] = await Promise.all([
|
|
22
|
+
import("./server/config.server"),
|
|
23
|
+
import("./server/bundleActivity"),
|
|
24
|
+
]);
|
|
25
|
+
const { config } = await prepareConfig();
|
|
26
|
+
return getBundleActivity(config.database.models.insights, data);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const getRecoveryReportRpc = createServerFn({ method: "GET" })
|
|
30
|
+
.validator(readRecoveryInput)
|
|
31
|
+
.handler(async ({ data }) => {
|
|
32
|
+
const [{ prepareConfig }, { getRecoveryReport }] = await Promise.all([
|
|
33
|
+
import("./server/config.server"),
|
|
34
|
+
import("./server/insightsRecovery"),
|
|
35
|
+
]);
|
|
36
|
+
const { config } = await prepareConfig();
|
|
37
|
+
return getRecoveryReport(config.database.models.insights, data);
|
|
38
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { InsightsWindow } from "./insights-rpc";
|
|
2
|
+
|
|
3
|
+
export type RecoveryInput = {
|
|
4
|
+
readonly platform: "ios" | "android";
|
|
5
|
+
readonly channel: string;
|
|
6
|
+
readonly window: InsightsWindow;
|
|
7
|
+
readonly releaseId?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RecoveryPoint = {
|
|
11
|
+
readonly startMs: number;
|
|
12
|
+
readonly active: number | null;
|
|
13
|
+
readonly recoveredInstallations: number;
|
|
14
|
+
readonly adopted: number;
|
|
15
|
+
readonly recovered: number;
|
|
16
|
+
readonly rate: number | null;
|
|
17
|
+
readonly spike: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RecoverySeries = {
|
|
21
|
+
readonly releaseId: string;
|
|
22
|
+
readonly firstAdoptedAtMs: number | null;
|
|
23
|
+
readonly activeInstallations: number;
|
|
24
|
+
readonly recoveredInstallations: number;
|
|
25
|
+
readonly points: readonly RecoveryPoint[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type RecoveryReport = {
|
|
29
|
+
readonly sinceMs: number;
|
|
30
|
+
readonly beforeReceivedAtMs: number;
|
|
31
|
+
readonly intervalMs: number;
|
|
32
|
+
readonly truncated: boolean;
|
|
33
|
+
readonly unattributedInstallations: number;
|
|
34
|
+
readonly series: readonly RecoverySeries[];
|
|
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
|
+
export function readRecoveryInput(input: RecoveryInput): RecoveryInput {
|
|
44
|
+
if (
|
|
45
|
+
!input ||
|
|
46
|
+
(input.platform !== "ios" && input.platform !== "android") ||
|
|
47
|
+
typeof input.channel !== "string" ||
|
|
48
|
+
!input.channel.trim() ||
|
|
49
|
+
input.channel.length > 1_024 ||
|
|
50
|
+
!Object.hasOwn(recoveryWindows, input.window) ||
|
|
51
|
+
(input.releaseId !== undefined &&
|
|
52
|
+
(typeof input.releaseId !== "string" ||
|
|
53
|
+
!input.releaseId.trim() ||
|
|
54
|
+
input.releaseId.length > 1_024))
|
|
55
|
+
) {
|
|
56
|
+
throw new Error("Choose a platform, channel, and time window.");
|
|
57
|
+
}
|
|
58
|
+
return input;
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { InsightsModel } from "@hot-updater/plugin-core";
|
|
2
|
+
|
|
3
|
+
import type { BundleActivityInput } from "../bundle-activity";
|
|
4
|
+
import type { RecoveryReport } from "../insights-recovery";
|
|
5
|
+
import { readInsightsHistory } from "./insightsHistory";
|
|
6
|
+
import { buildRecoveryReport } from "./insightsRecovery";
|
|
7
|
+
|
|
8
|
+
export async function getBundleActivity(
|
|
9
|
+
model: InsightsModel,
|
|
10
|
+
inputs: readonly BundleActivityInput[],
|
|
11
|
+
now = Date.now(),
|
|
12
|
+
) {
|
|
13
|
+
if (inputs.length === 0) return {};
|
|
14
|
+
// One bounded read for the visible page, independent of the management query.
|
|
15
|
+
const history = await readInsightsHistory(model, "30d", now);
|
|
16
|
+
const scopes = new Map<string, RecoveryReport>();
|
|
17
|
+
const result = new Map<string, RecoveryReport>();
|
|
18
|
+
for (const input of inputs) {
|
|
19
|
+
const key = JSON.stringify([input.platform, input.channel]);
|
|
20
|
+
let report = scopes.get(key);
|
|
21
|
+
if (!report) {
|
|
22
|
+
report = buildRecoveryReport(history, {
|
|
23
|
+
platform: input.platform,
|
|
24
|
+
channel: input.channel,
|
|
25
|
+
window: "30d",
|
|
26
|
+
});
|
|
27
|
+
scopes.set(key, report);
|
|
28
|
+
}
|
|
29
|
+
result.set(input.releaseId, {
|
|
30
|
+
...report,
|
|
31
|
+
series: report.series.filter(
|
|
32
|
+
(series) => series.releaseId === input.releaseId,
|
|
33
|
+
),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return Object.fromEntries(result);
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BundleEventRow,
|
|
3
|
+
InsightsEventCursor,
|
|
4
|
+
InsightsModel,
|
|
5
|
+
} from "@hot-updater/plugin-core";
|
|
6
|
+
|
|
7
|
+
import { recoveryWindows, type RecoveryInput } from "../insights-recovery";
|
|
8
|
+
|
|
9
|
+
export async function readInsightsHistory(
|
|
10
|
+
model: InsightsModel,
|
|
11
|
+
window: RecoveryInput["window"],
|
|
12
|
+
beforeReceivedAtMs = Date.now(),
|
|
13
|
+
) {
|
|
14
|
+
const { durationMs, intervalMs } = recoveryWindows[window];
|
|
15
|
+
const sinceMs = Math.max(0, beforeReceivedAtMs - durationMs);
|
|
16
|
+
let completeSinceMs = sinceMs;
|
|
17
|
+
let after: InsightsEventCursor | undefined;
|
|
18
|
+
let truncated = false;
|
|
19
|
+
const events: BundleEventRow[] = [];
|
|
20
|
+
while (events.length < 50_000) {
|
|
21
|
+
const limit = Math.min(100, 50_000 - events.length);
|
|
22
|
+
const rows = await model.listEvents({
|
|
23
|
+
filter: { kind: "all" },
|
|
24
|
+
sinceMs,
|
|
25
|
+
beforeReceivedAtMs,
|
|
26
|
+
after,
|
|
27
|
+
limit: limit + 1,
|
|
28
|
+
});
|
|
29
|
+
const page = rows.slice(0, limit);
|
|
30
|
+
events.push(...page);
|
|
31
|
+
const last = page.at(-1);
|
|
32
|
+
if (!last || rows.length <= limit) break;
|
|
33
|
+
after = { id: last.id, receivedAtMs: last.received_at_ms };
|
|
34
|
+
if (events.length === 50_000) {
|
|
35
|
+
truncated = true;
|
|
36
|
+
// Exclude the partially read interval, including its unknown denominator.
|
|
37
|
+
completeSinceMs =
|
|
38
|
+
Math.floor(last.received_at_ms / intervalMs) * intervalMs + intervalMs;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
events: events
|
|
43
|
+
.filter((row) => row.received_at_ms >= completeSinceMs)
|
|
44
|
+
.reverse(),
|
|
45
|
+
sinceMs: completeSinceMs,
|
|
46
|
+
beforeReceivedAtMs,
|
|
47
|
+
intervalMs,
|
|
48
|
+
truncated,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type InsightsHistory = Awaited<ReturnType<typeof readInsightsHistory>>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { InsightsModel } from "@hot-updater/plugin-core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
readRecoveryInput,
|
|
5
|
+
type RecoveryInput,
|
|
6
|
+
type RecoveryReport,
|
|
7
|
+
type RecoverySeries,
|
|
8
|
+
} from "../insights-recovery";
|
|
9
|
+
import { readInsightsHistory, type InsightsHistory } from "./insightsHistory";
|
|
10
|
+
|
|
11
|
+
export async function getRecoveryReport(
|
|
12
|
+
model: InsightsModel,
|
|
13
|
+
input: RecoveryInput,
|
|
14
|
+
beforeReceivedAtMs = Date.now(),
|
|
15
|
+
): Promise<RecoveryReport> {
|
|
16
|
+
readRecoveryInput(input);
|
|
17
|
+
return buildRecoveryReport(
|
|
18
|
+
await readInsightsHistory(model, input.window, beforeReceivedAtMs),
|
|
19
|
+
input,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildRecoveryReport(
|
|
24
|
+
history: InsightsHistory,
|
|
25
|
+
input: RecoveryInput,
|
|
26
|
+
): RecoveryReport {
|
|
27
|
+
const { events, sinceMs, beforeReceivedAtMs, intervalMs, truncated } =
|
|
28
|
+
history;
|
|
29
|
+
const bucketStart = (time: number) =>
|
|
30
|
+
Math.floor(time / intervalMs) * intervalMs;
|
|
31
|
+
const releases = new Map<
|
|
32
|
+
string,
|
|
33
|
+
{
|
|
34
|
+
firstAdoptedAtMs: number | null;
|
|
35
|
+
recovered: Set<string>;
|
|
36
|
+
buckets: Map<
|
|
37
|
+
number,
|
|
38
|
+
{
|
|
39
|
+
adopted: number;
|
|
40
|
+
recovered: number;
|
|
41
|
+
installations: Set<string>;
|
|
42
|
+
lastRecoveredAtMs: number;
|
|
43
|
+
}
|
|
44
|
+
>;
|
|
45
|
+
}
|
|
46
|
+
>();
|
|
47
|
+
const ensureRelease = (id: string) => {
|
|
48
|
+
let release = releases.get(id);
|
|
49
|
+
if (!release) {
|
|
50
|
+
release = {
|
|
51
|
+
firstAdoptedAtMs: null,
|
|
52
|
+
recovered: new Set(),
|
|
53
|
+
buckets: new Map(),
|
|
54
|
+
};
|
|
55
|
+
releases.set(id, release);
|
|
56
|
+
}
|
|
57
|
+
return release;
|
|
58
|
+
};
|
|
59
|
+
const latest = new Map<
|
|
60
|
+
string,
|
|
61
|
+
{ releaseId: string | null; bundleId: string; inScope: boolean }
|
|
62
|
+
>();
|
|
63
|
+
const active = new Map<string, number>();
|
|
64
|
+
const snapshots = new Map<number, Map<string, number> | null>();
|
|
65
|
+
let unattributedInstallations = 0;
|
|
66
|
+
let observed = false;
|
|
67
|
+
let eventIndex = 0;
|
|
68
|
+
for (
|
|
69
|
+
let startMs = bucketStart(sinceMs);
|
|
70
|
+
startMs < beforeReceivedAtMs;
|
|
71
|
+
startMs += intervalMs
|
|
72
|
+
) {
|
|
73
|
+
while (
|
|
74
|
+
eventIndex < events.length &&
|
|
75
|
+
events[eventIndex].received_at_ms < startMs + intervalMs
|
|
76
|
+
) {
|
|
77
|
+
const row = events[eventIndex++];
|
|
78
|
+
const inScope =
|
|
79
|
+
row.platform === input.platform && row.channel === input.channel;
|
|
80
|
+
const previous = latest.get(row.install_id);
|
|
81
|
+
// Older SDKs omit the ID on UNCHANGED. Only retain an observed ID for the same file and scope.
|
|
82
|
+
const releaseId =
|
|
83
|
+
row.to_release_id ??
|
|
84
|
+
(row.type === "UNCHANGED" &&
|
|
85
|
+
previous?.bundleId === row.to_bundle_id &&
|
|
86
|
+
previous.inScope === inScope
|
|
87
|
+
? previous.releaseId
|
|
88
|
+
: null);
|
|
89
|
+
if (previous?.inScope) {
|
|
90
|
+
if (previous.releaseId)
|
|
91
|
+
active.set(
|
|
92
|
+
previous.releaseId,
|
|
93
|
+
(active.get(previous.releaseId) ?? 0) - 1,
|
|
94
|
+
);
|
|
95
|
+
else unattributedInstallations -= 1;
|
|
96
|
+
}
|
|
97
|
+
latest.set(row.install_id, {
|
|
98
|
+
releaseId,
|
|
99
|
+
bundleId: row.to_bundle_id,
|
|
100
|
+
inScope,
|
|
101
|
+
});
|
|
102
|
+
if (!inScope) continue;
|
|
103
|
+
observed = true;
|
|
104
|
+
if (releaseId) {
|
|
105
|
+
ensureRelease(releaseId);
|
|
106
|
+
active.set(releaseId, (active.get(releaseId) ?? 0) + 1);
|
|
107
|
+
} else unattributedInstallations += 1;
|
|
108
|
+
if (row.from_release_id) ensureRelease(row.from_release_id);
|
|
109
|
+
const outcomeId =
|
|
110
|
+
row.type === "RECOVERED" ? row.from_release_id : row.to_release_id;
|
|
111
|
+
if (
|
|
112
|
+
!outcomeId ||
|
|
113
|
+
(row.type !== "RECOVERED" &&
|
|
114
|
+
row.type !== "RELEASE_ADOPTED" &&
|
|
115
|
+
row.type !== "UPDATE_APPLIED")
|
|
116
|
+
)
|
|
117
|
+
continue;
|
|
118
|
+
const release = ensureRelease(outcomeId);
|
|
119
|
+
const bucket = release.buckets.get(startMs) ?? {
|
|
120
|
+
adopted: 0,
|
|
121
|
+
recovered: 0,
|
|
122
|
+
installations: new Set<string>(),
|
|
123
|
+
lastRecoveredAtMs: -1,
|
|
124
|
+
};
|
|
125
|
+
if (row.type === "RECOVERED") {
|
|
126
|
+
bucket.recovered += 1;
|
|
127
|
+
bucket.installations.add(row.install_id);
|
|
128
|
+
bucket.lastRecoveredAtMs = row.received_at_ms;
|
|
129
|
+
release.recovered.add(row.install_id);
|
|
130
|
+
} else {
|
|
131
|
+
bucket.adopted += 1;
|
|
132
|
+
release.firstAdoptedAtMs ??= row.received_at_ms;
|
|
133
|
+
}
|
|
134
|
+
release.buckets.set(startMs, bucket);
|
|
135
|
+
}
|
|
136
|
+
// Before the first report there is no observed installation state to chart.
|
|
137
|
+
snapshots.set(startMs, observed ? new Map(active) : null);
|
|
138
|
+
}
|
|
139
|
+
const series: RecoverySeries[] = [];
|
|
140
|
+
for (const [releaseId, release] of releases) {
|
|
141
|
+
if (input.releaseId && input.releaseId !== releaseId) continue;
|
|
142
|
+
let previousRate: number | null = null;
|
|
143
|
+
const points = Array.from(snapshots, ([startMs, snapshot]) => {
|
|
144
|
+
const bucket = release.buckets.get(startMs);
|
|
145
|
+
const adopted = bucket?.adopted ?? 0;
|
|
146
|
+
const recovered = bucket?.recovered ?? 0;
|
|
147
|
+
const total = adopted + recovered;
|
|
148
|
+
const rate = total === 0 ? null : (recovered / total) * 100;
|
|
149
|
+
const spike =
|
|
150
|
+
release.firstAdoptedAtMs !== null &&
|
|
151
|
+
(bucket?.lastRecoveredAtMs ?? -1) >= release.firstAdoptedAtMs &&
|
|
152
|
+
(!truncated || previousRate !== null) &&
|
|
153
|
+
rate !== null &&
|
|
154
|
+
total >= 10 &&
|
|
155
|
+
recovered >= 3 &&
|
|
156
|
+
rate >= 10 &&
|
|
157
|
+
rate - (previousRate ?? 0) >= 10;
|
|
158
|
+
if (rate !== null) previousRate = rate;
|
|
159
|
+
return {
|
|
160
|
+
startMs,
|
|
161
|
+
active: snapshot ? (snapshot.get(releaseId) ?? 0) : null,
|
|
162
|
+
recoveredInstallations: bucket?.installations.size ?? 0,
|
|
163
|
+
adopted,
|
|
164
|
+
recovered,
|
|
165
|
+
rate,
|
|
166
|
+
spike,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
series.push({
|
|
170
|
+
releaseId,
|
|
171
|
+
firstAdoptedAtMs: release.firstAdoptedAtMs,
|
|
172
|
+
activeInstallations: active.get(releaseId) ?? 0,
|
|
173
|
+
recoveredInstallations: release.recovered.size,
|
|
174
|
+
points,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
series.sort((a, b) => b.releaseId.localeCompare(a.releaseId));
|
|
178
|
+
return {
|
|
179
|
+
sinceMs,
|
|
180
|
+
beforeReceivedAtMs,
|
|
181
|
+
intervalMs,
|
|
182
|
+
truncated,
|
|
183
|
+
unattributedInstallations,
|
|
184
|
+
series,
|
|
185
|
+
};
|
|
186
|
+
}
|
package/src/routes/index.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { BundleIdDisplay } from "@/components/BundleIdDisplay";
|
|
|
19
19
|
import { ChannelBadge } from "@/components/ChannelBadge";
|
|
20
20
|
import { EnabledStatusIcon } from "@/components/EnabledStatusIcon";
|
|
21
21
|
import { BundleChildrenPanel } from "@/components/features/bundles/BundleChildrenPanel";
|
|
22
|
+
import { BundleMovementSummary } from "@/components/features/bundles/BundleInsightsSummary";
|
|
22
23
|
import { ChannelManagementDialog } from "@/components/features/channels/ChannelManagementDialog";
|
|
23
24
|
import { ReleaseEditorSheet } from "@/components/features/releases/ReleaseEditorSheet";
|
|
24
25
|
import { ReleaseStateBadge } from "@/components/features/releases/ReleaseStateBadge";
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
useChannelsQuery,
|
|
56
57
|
useReleasesQuery,
|
|
57
58
|
} from "@/lib/api";
|
|
59
|
+
import { useBundleActivityQuery } from "@/lib/bundle-activity";
|
|
58
60
|
import type { ReleaseListRow } from "@/lib/server/releaseReachability";
|
|
59
61
|
import { cn } from "@/lib/utils";
|
|
60
62
|
|
|
@@ -395,6 +397,16 @@ function BundlesPage() {
|
|
|
395
397
|
const patchCountsQuery = useBundleChildCountsQuery(bundleIds);
|
|
396
398
|
const patchCountsByBundleId = patchCountsQuery.data ?? {};
|
|
397
399
|
const pagination = releasesQuery.data?.pagination;
|
|
400
|
+
const activityQuery = useBundleActivityQuery(
|
|
401
|
+
releases.flatMap((release) => {
|
|
402
|
+
const channel = channels.find(
|
|
403
|
+
(item) => item.id === release.channel_id,
|
|
404
|
+
)?.name;
|
|
405
|
+
return channel
|
|
406
|
+
? [{ releaseId: release.id, platform: release.platform, channel }]
|
|
407
|
+
: [];
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
398
410
|
const channelNames = new Map(
|
|
399
411
|
channels.map((channel) => [channel.id, channel.name]),
|
|
400
412
|
);
|
|
@@ -556,6 +568,17 @@ function BundlesPage() {
|
|
|
556
568
|
: "—"}
|
|
557
569
|
</dd>
|
|
558
570
|
</div>
|
|
571
|
+
<div className="col-span-2 rounded-md bg-muted/40 p-3">
|
|
572
|
+
<dt className="mb-2 text-muted-foreground">
|
|
573
|
+
Activity · 30d
|
|
574
|
+
</dt>
|
|
575
|
+
<dd>
|
|
576
|
+
<BundleMovementSummary
|
|
577
|
+
report={activityQuery.data?.[release.id]}
|
|
578
|
+
loading={activityQuery.isFetching}
|
|
579
|
+
/>
|
|
580
|
+
</dd>
|
|
581
|
+
</div>
|
|
559
582
|
<div className="col-span-2 flex flex-wrap items-center gap-2 rounded-md bg-muted/40 p-3">
|
|
560
583
|
<ReleaseStateBadge release={release} />
|
|
561
584
|
{release.should_force_update ? (
|
|
@@ -618,6 +641,7 @@ function BundlesPage() {
|
|
|
618
641
|
<TableHead>Enabled</TableHead>
|
|
619
642
|
<TableHead>Force update</TableHead>
|
|
620
643
|
<TableHead>Rollout</TableHead>
|
|
644
|
+
<TableHead>Activity · 30d</TableHead>
|
|
621
645
|
<TableHead>Message</TableHead>
|
|
622
646
|
<TableHead>Created</TableHead>
|
|
623
647
|
</TableRow>
|
|
@@ -626,7 +650,7 @@ function BundlesPage() {
|
|
|
626
650
|
{releasesQuery.isPending
|
|
627
651
|
? Array.from({ length: 7 }, (_, index) => (
|
|
628
652
|
<TableRow key={index}>
|
|
629
|
-
<TableCell colSpan={
|
|
653
|
+
<TableCell colSpan={11}>
|
|
630
654
|
<Skeleton className="h-8 w-full" />
|
|
631
655
|
</TableCell>
|
|
632
656
|
</TableRow>
|
|
@@ -783,6 +807,12 @@ function BundlesPage() {
|
|
|
783
807
|
percentage={release.rollout_cohort_count / 10}
|
|
784
808
|
/>
|
|
785
809
|
</TableCell>
|
|
810
|
+
<TableCell>
|
|
811
|
+
<BundleMovementSummary
|
|
812
|
+
report={activityQuery.data?.[release.id]}
|
|
813
|
+
loading={activityQuery.isFetching}
|
|
814
|
+
/>
|
|
815
|
+
</TableCell>
|
|
786
816
|
<TableCell
|
|
787
817
|
className="text-sm text-muted-foreground"
|
|
788
818
|
title={release.message ?? undefined}
|
|
@@ -804,7 +834,7 @@ function BundlesPage() {
|
|
|
804
834
|
<TableRow className="hover:bg-transparent">
|
|
805
835
|
<TableCell
|
|
806
836
|
className="border-t-0 p-0"
|
|
807
|
-
colSpan={
|
|
837
|
+
colSpan={11}
|
|
808
838
|
>
|
|
809
839
|
<ReleaseBundleChildrenPanel
|
|
810
840
|
onDetailClick={openChildBundle}
|
|
@@ -821,7 +851,7 @@ function BundlesPage() {
|
|
|
821
851
|
<TableRow>
|
|
822
852
|
<TableCell
|
|
823
853
|
className="h-40 text-center text-muted-foreground"
|
|
824
|
-
colSpan={
|
|
854
|
+
colSpan={11}
|
|
825
855
|
>
|
|
826
856
|
{search.bundleId ||
|
|
827
857
|
search.channelId ||
|
package/src/routes/insights.tsx
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
InsightsBundleReports,
|
|
6
|
-
type InsightsReportSelection,
|
|
7
|
-
} from "@/components/features/insights/InsightsBundleReports";
|
|
8
4
|
import { InsightsControls } from "@/components/features/insights/InsightsControls";
|
|
9
5
|
import { InsightsOverview } from "@/components/features/insights/InsightsOverview";
|
|
10
6
|
import { InsightsPageHeader } from "@/components/features/insights/InsightsPageHeader";
|
|
11
|
-
import { Button } from "@/components/ui/button";
|
|
12
7
|
import {
|
|
13
8
|
type InsightsWindow,
|
|
14
9
|
type InsightsOverviewInput,
|
|
15
|
-
useReportingInstallationsQuery,
|
|
16
10
|
} from "@/lib/insights-api";
|
|
17
11
|
|
|
18
12
|
export const Route = createFileRoute("/insights")({
|
|
@@ -25,10 +19,6 @@ function InsightsPage() {
|
|
|
25
19
|
platform: "ios",
|
|
26
20
|
channel: "production",
|
|
27
21
|
});
|
|
28
|
-
const [selection, setSelection] = useState<InsightsReportSelection | null>(
|
|
29
|
-
null,
|
|
30
|
-
);
|
|
31
|
-
const active = useReportingInstallationsQuery({ ...scope, window });
|
|
32
22
|
|
|
33
23
|
return (
|
|
34
24
|
<div className="flex h-svh min-h-0 flex-col">
|
|
@@ -39,44 +29,13 @@ function InsightsPage() {
|
|
|
39
29
|
scope={scope}
|
|
40
30
|
onScopeChange={(next) => {
|
|
41
31
|
setScope(next);
|
|
42
|
-
setSelection(null);
|
|
43
32
|
}}
|
|
44
33
|
onWindowChange={(next) => {
|
|
45
34
|
setWindow(next);
|
|
46
|
-
setSelection(null);
|
|
47
35
|
}}
|
|
48
36
|
window={window}
|
|
49
37
|
/>
|
|
50
|
-
<
|
|
51
|
-
className="h-11 self-end lg:h-8"
|
|
52
|
-
variant="outline"
|
|
53
|
-
disabled={active.isFetching}
|
|
54
|
-
onClick={() => {
|
|
55
|
-
setSelection(null);
|
|
56
|
-
void active.refetch();
|
|
57
|
-
}}
|
|
58
|
-
>
|
|
59
|
-
Refresh overview
|
|
60
|
-
</Button>
|
|
61
|
-
{active.isLoading ? (
|
|
62
|
-
<InsightsOverview status="loading" />
|
|
63
|
-
) : active.error ? (
|
|
64
|
-
<InsightsOverview status="error" error={active.error} />
|
|
65
|
-
) : active.data ? (
|
|
66
|
-
<InsightsOverview
|
|
67
|
-
status="success"
|
|
68
|
-
active={active.data}
|
|
69
|
-
onOutcomeSelect={setSelection}
|
|
70
|
-
/>
|
|
71
|
-
) : (
|
|
72
|
-
<InsightsOverview status="loading" />
|
|
73
|
-
)}
|
|
74
|
-
{selection ? (
|
|
75
|
-
<InsightsBundleReports
|
|
76
|
-
key={JSON.stringify(selection)}
|
|
77
|
-
selection={selection}
|
|
78
|
-
/>
|
|
79
|
-
) : null}
|
|
38
|
+
<InsightsOverview input={{ ...scope, window }} />
|
|
80
39
|
</div>
|
|
81
40
|
</div>
|
|
82
41
|
</div>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Gn as e,Zn as t,_ as n,g as r,xn as i,y as a}from"./createServerFn-CA8fs7QD.js";import{n as o}from"./dist-C_NydL0e.js";var s=e(`chevron-down`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),c=e(`rotate-ccw`,[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}]]),l=t(),u=(e,t=12)=>e.length>t?`${e.slice(0,Math.max(1,t-4))}…${e.slice(-4)}`:e;function d({value:e,maxLength:t=12,className:s,buttonClassName:c}){let d=e.length>t,f=u(e,t),p=async()=>{try{await navigator.clipboard.writeText(e),o.success(`Copied to clipboard`,{description:e})}catch{o.error(`Failed to copy value`)}},m=e=>{e.preventDefault(),e.stopPropagation(),p()},h=e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),e.stopPropagation(),p())},g=(0,l.jsx)(`span`,{translate:`no`,className:i(`font-mono text-xs tabular-nums whitespace-nowrap`,s),children:f});return(0,l.jsxs)(r,{children:[(0,l.jsx)(a,{render:(0,l.jsx)(`button`,{type:`button`,"aria-label":`Copy full value: ${e}`,translate:`no`,className:i(`ring-ring/30 bg-muted/40 border-border/70 inline-flex min-w-0 max-w-full cursor-pointer items-center rounded-md border px-1.5 py-0.5 align-top shadow-xs outline-none transition-[background-color,border-color,transform,box-shadow]`,`hover:bg-muted/70 active:scale-[0.98] active:bg-muted/85 focus-visible:ring-[2px]`,`touch-manipulation select-none motion-reduce:transition-none motion-reduce:active:scale-100`,!d&&`cursor-copy`,c),onClick:m,onKeyDown:h}),children:g}),(0,l.jsx)(n,{hidden:!d,children:(0,l.jsx)(`p`,{translate:`no`,className:`break-all font-mono text-xs tabular-nums`,children:e})})]})}export{s as i,u as n,c as r,d as t};
|