@hot-updater/console 1.0.0-rc.1 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +1 -0
  3. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +1 -0
  4. package/.output/public/assets/api-keys-BzKocqRy.js +1 -0
  5. package/.output/public/assets/api-keys-api-BHcODB7q.js +1 -0
  6. package/.output/public/assets/card-Dhr_eFYj.js +1 -0
  7. package/.output/public/assets/{chart-no-axes-combined-FaYMGmQS.js → chart-no-axes-combined-Dy4WJv2U.js} +1 -1
  8. package/.output/public/assets/composite-Bp98A0Ld.js +1 -0
  9. package/.output/public/assets/createServerFn-CA8fs7QD.js +3 -0
  10. package/.output/public/assets/dialog-CUTrcPlB.js +1 -0
  11. package/.output/public/assets/dist-C_NydL0e.js +1 -0
  12. package/.output/public/assets/index-DdAz-CXv.js +10 -0
  13. package/.output/public/assets/input-group-cSOG4wh_.js +1 -0
  14. package/.output/public/assets/insights-C0PBSZ-n.js +1 -0
  15. package/.output/public/assets/installations-CE33wgbY.js +2 -0
  16. package/.output/public/assets/installations-CStZja8x.js +1 -0
  17. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +1 -0
  18. package/.output/public/assets/link-CI8idAUi.js +1 -0
  19. package/.output/public/assets/loader-circle-BoxLrsOu.js +1 -0
  20. package/.output/public/assets/{refresh-cw-BkbfZ62O.js → refresh-cw-Bqc72UZG.js} +1 -1
  21. package/.output/public/assets/{routes-BuoI5-Cr.js → routes-DGVXdzpw.js} +2 -2
  22. package/.output/public/assets/routes-DTlwOr8I.js +1 -0
  23. package/.output/public/assets/{shield-check-DE_hVuHu.js → shield-check-uK3MHWJZ.js} +1 -1
  24. package/.output/public/assets/signing-CMf4aqDO.js +1 -0
  25. package/.output/public/assets/styles-BCvbtKsm.css +2 -0
  26. package/.output/public/assets/table-2vcV89QB.js +1 -0
  27. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +1 -0
  28. package/.output/public/assets/x-DCd0j8c6.js +1 -0
  29. package/.output/server/{__23tanstack-start-server-fn-resolver-Dftxlm2E.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +46 -54
  30. package/.output/server/_libs/@base-ui/react+[...].mjs +283 -2417
  31. package/.output/server/_libs/@tanstack/react-router+[...].mjs +52 -4
  32. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +2 -2
  33. package/.output/server/_libs/lucide-react.mjs +36 -3
  34. package/.output/server/_libs/next-themes.mjs +2 -2
  35. package/.output/server/_libs/sonner.mjs +2 -2
  36. package/.output/server/_libs/tanstack__react-query.mjs +2 -2
  37. package/.output/server/_libs/verkit.mjs +20 -15
  38. package/.output/server/_runtime.mjs +1 -10
  39. package/.output/server/_ssr/HashValueDisplay-DwpGLfrl.mjs +55 -0
  40. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +484 -0
  41. package/.output/server/_ssr/{api-keys-CpBqHpuC.mjs → api-keys-CZeTPQys.mjs} +9 -11
  42. package/.output/server/_ssr/{api-keys-api-Czkp57UV.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
  43. package/.output/server/_ssr/{api-keys-rpc-BTtMDSoR.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
  44. package/.output/server/_ssr/{api-rpc-BEX8ISai.mjs → api-rpc-tAupfGQm.mjs} +73 -142
  45. package/.output/server/_ssr/{auth-rpc-BFMEC-TT.mjs → auth-rpc-DUaeOWKK.mjs} +2 -2
  46. package/.output/server/_ssr/{bundle-signing-rpc-BbCFUQSu.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
  47. package/.output/server/_ssr/{card-Bsc8mRis.mjs → card-BRiPmi0C.mjs} +25 -3
  48. package/.output/server/_ssr/{config.server-DQYEr_5X.mjs → config.server-afDuAwaG.mjs} +14 -7
  49. package/.output/server/_ssr/{createSsrRpc-DhfrxEjg.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
  50. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
  51. package/.output/server/_ssr/{databaseRows-QlCYLi0H.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
  52. package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
  53. package/.output/server/_ssr/{dialog-C0vKUp8R.mjs → dialog-DS-nw-pA.mjs} +2 -2
  54. package/.output/server/_ssr/{dist-BVSLTpoP.mjs → dist-CyzPaI5v.mjs} +2 -2
  55. package/.output/server/_ssr/{getBundleChildren-BcRMFoDF.mjs → getBundleChildren-C0vh4ZvE.mjs} +2 -2
  56. package/.output/server/_ssr/input-group-BpsnmvZw.mjs +70 -0
  57. package/.output/server/_ssr/insights-BdS364Xu.mjs +460 -0
  58. package/.output/server/_ssr/insights-rpc-38DujK0G.mjs +57 -0
  59. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +303 -0
  60. package/.output/server/_ssr/installations-CtA59MQG.mjs +29 -0
  61. package/.output/server/_ssr/installations-DiVKEF5T.mjs +627 -0
  62. package/.output/server/_ssr/{router-BxBEvtF1.mjs → router-B9wG_9f8.mjs} +23 -32
  63. package/.output/server/_ssr/{routes-BC7rn-tf.mjs → routes-CKrrS2q2.mjs} +3 -3
  64. package/.output/server/_ssr/{routes-CM5KAwpB.mjs → routes-D7Yh_jfj.mjs} +402 -389
  65. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
  66. package/.output/server/_ssr/{server-CpzW--lT.mjs → server-D_C2rMzf.mjs} +5 -5
  67. package/.output/server/_ssr/{signing-jk4HANnl.mjs → signing-Dz1WkZOf.mjs} +5 -6
  68. package/.output/server/_ssr/ssr.mjs +1 -1
  69. package/.output/server/_ssr/{field-DASCh-5d.mjs → table-CnZmon_Z.mjs} +51 -5
  70. package/.output/server/_ssr/{uuidv7-CaKuKXrV.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
  71. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
  72. package/.output/server/index.mjs +164 -178
  73. package/lib/index.d.mts +4 -3
  74. package/lib/vite-internal.mjs +1 -1
  75. package/lib/vite.d.mts +0 -1
  76. package/package.json +8 -8
  77. package/src/components/AppSidebar.tsx +10 -14
  78. package/src/components/ConsoleAccessPage.tsx +1 -1
  79. package/src/components/HashValueDisplay.tsx +11 -2
  80. package/src/components/features/bundles/BundleChildrenPanel.tsx +7 -6
  81. package/src/components/features/bundles/BundleMetadata.tsx +1 -20
  82. package/src/components/features/channels/ChannelManagementDialog.tsx +1 -1
  83. package/src/components/features/insights/EventDetails.tsx +151 -0
  84. package/src/components/features/insights/EventHistoryCard.tsx +245 -0
  85. package/src/components/features/insights/EventHistoryList.tsx +63 -0
  86. package/src/components/features/insights/InsightsBundleReports.tsx +51 -0
  87. package/src/components/features/insights/InsightsControls.tsx +82 -76
  88. package/src/components/features/insights/InsightsErrorAlert.tsx +5 -5
  89. package/src/components/features/insights/InsightsOverview.tsx +156 -161
  90. package/src/components/features/insights/InsightsPageHeader.tsx +48 -0
  91. package/src/components/features/insights/InsightsPagination.tsx +52 -0
  92. package/src/components/features/insights/InstallationHistoryCard.tsx +178 -176
  93. package/src/components/features/insights/InstallationMatchesCard.tsx +111 -65
  94. package/src/components/features/insights/InstallationPageHeader.tsx +57 -105
  95. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
  96. package/src/components/ui/badge.tsx +2 -0
  97. package/src/components/ui/combobox.tsx +1 -0
  98. package/src/components/ui/skeleton.tsx +4 -1
  99. package/src/components/ui/tooltip.tsx +1 -1
  100. package/src/index.ts +7 -2
  101. package/src/lib/api-rpc.ts +19 -90
  102. package/src/lib/api.ts +0 -99
  103. package/src/lib/insights-api.ts +93 -137
  104. package/src/lib/insights-rpc.ts +69 -199
  105. package/src/lib/insights-view.ts +15 -0
  106. package/src/lib/public-bundle-error.ts +19 -0
  107. package/src/lib/server/releaseReachability.ts +1 -4
  108. package/src/lib/server/runtime.server.ts +5 -141
  109. package/src/router.tsx +3 -0
  110. package/src/routes/-installations-search.ts +25 -12
  111. package/src/routes/__root.tsx +6 -16
  112. package/src/routes/index.tsx +21 -81
  113. package/src/routes/insights.tsx +50 -105
  114. package/src/routes/installations.tsx +290 -138
  115. package/src/styles.css +6 -0
  116. package/.output/public/assets/BundleIdDisplay-D4ErdU5_.js +0 -1
  117. package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +0 -1
  118. package/.output/public/assets/api-keys-api-DGVTTvTJ.js +0 -1
  119. package/.output/public/assets/api-keys-z16OZIP4.js +0 -1
  120. package/.output/public/assets/badge-Br_KDO0J.js +0 -1
  121. package/.output/public/assets/card-DzuEXvyJ.js +0 -1
  122. package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
  123. package/.output/public/assets/dialog-BeAENgez.js +0 -1
  124. package/.output/public/assets/dist-Bssh_cv0.js +0 -1
  125. package/.output/public/assets/field-Buq148-A.js +0 -1
  126. package/.output/public/assets/index-uOpQAIC4.js +0 -10
  127. package/.output/public/assets/input-group-B3p5UpyW.js +0 -1
  128. package/.output/public/assets/insights-DJTr7Ozn.js +0 -1
  129. package/.output/public/assets/insights-api-DVMOoRMS.js +0 -1
  130. package/.output/public/assets/installations-BY64iIAK.js +0 -2
  131. package/.output/public/assets/installations-D5v9i3AL.js +0 -1
  132. package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +0 -1
  133. package/.output/public/assets/routes-C8fvgETO.js +0 -1
  134. package/.output/public/assets/search-qsIDvS2I.js +0 -1
  135. package/.output/public/assets/signing-mJD88Y4C.js +0 -1
  136. package/.output/public/assets/styles-BZIG7t42.css +0 -2
  137. package/.output/public/assets/table-RamFEclP.js +0 -1
  138. package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +0 -42
  139. package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
  140. package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +0 -1
  141. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1554
  142. package/.output/server/_libs/d3-array.mjs +0 -231
  143. package/.output/server/_libs/d3-color.mjs +0 -330
  144. package/.output/server/_libs/d3-format.mjs +0 -252
  145. package/.output/server/_libs/d3-interpolate.mjs +0 -207
  146. package/.output/server/_libs/d3-path.mjs +0 -77
  147. package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
  148. package/.output/server/_libs/d3-shape.mjs +0 -818
  149. package/.output/server/_libs/decimal.js-light.mjs +0 -884
  150. package/.output/server/_libs/es-toolkit.mjs +0 -1134
  151. package/.output/server/_libs/eventemitter3.mjs +0 -236
  152. package/.output/server/_libs/immer.mjs +0 -532
  153. package/.output/server/_libs/react-redux+[...].mjs +0 -205
  154. package/.output/server/_libs/recharts+[...].mjs +0 -14117
  155. package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +0 -288
  156. package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +0 -13
  157. package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +0 -15
  158. package/.output/server/_ssr/badge-CpWXQwyF.mjs +0 -26
  159. package/.output/server/_ssr/input-group-etIsBUqv.mjs +0 -288
  160. package/.output/server/_ssr/insights-api-C1F7UreR.mjs +0 -71
  161. package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +0 -96
  162. package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +0 -155
  163. package/.output/server/_ssr/insights-ygT_aIfo.mjs +0 -858
  164. package/.output/server/_ssr/installations-DqRT1QTd.mjs +0 -535
  165. package/.output/server/_ssr/installations-K8TCYTYh.mjs +0 -19
  166. package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +0 -527
  167. package/.output/server/_ssr/table-CU58HeI7.mjs +0 -53
  168. package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +0 -103
  169. package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
  170. package/src/components/features/bundles/BundleInsightsSummary.tsx +0 -123
  171. package/src/components/features/insights/ActivityChart.tsx +0 -150
  172. package/src/components/features/insights/BundleDistribution.tsx +0 -133
  173. package/src/components/features/insights/BundleSelector.tsx +0 -114
  174. package/src/components/features/insights/InsightsCapabilityContext.tsx +0 -24
  175. package/src/components/features/insights/InstallationPagination.tsx +0 -48
  176. package/src/components/features/insights/InstallationSearch.tsx +0 -230
  177. package/src/components/features/insights/UpdateOutcomes.tsx +0 -147
  178. package/src/lib/insights-error.ts +0 -19
  179. package/src/lib/insights-input.ts +0 -187
  180. package/src/lib/insights-overview.ts +0 -170
  181. package/src/lib/server/releaseActivity.ts +0 -27
@@ -1,193 +0,0 @@
1
- import type { ActiveInstallationWindow } from "@hot-updater/server";
2
- import { useId } from "react";
3
- import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
4
-
5
- import {
6
- type ChartConfig,
7
- ChartContainer,
8
- ChartTooltip,
9
- ChartTooltipContent,
10
- } from "@/components/ui/chart";
11
-
12
- interface SeriesPoint {
13
- readonly bucketStartMs: number;
14
- readonly value: number;
15
- }
16
-
17
- interface BundleActivityChartProps {
18
- readonly installed: readonly SeriesPoint[];
19
- readonly recovered: readonly SeriesPoint[];
20
- readonly window: ActiveInstallationWindow;
21
- }
22
-
23
- interface ActivityChartPoint {
24
- readonly bucketStartMs: number;
25
- readonly installed: number;
26
- readonly recovered: number;
27
- }
28
-
29
- const chartConfig = {
30
- installed: {
31
- label: "Applied",
32
- color: "var(--chart-2)",
33
- },
34
- recovered: {
35
- label: "Recovered",
36
- color: "var(--muted-foreground)",
37
- },
38
- } satisfies ChartConfig;
39
-
40
- const dateFormatter = new Intl.DateTimeFormat("en", {
41
- day: "numeric",
42
- month: "short",
43
- timeZone: "UTC",
44
- });
45
- const hourFormatter = new Intl.DateTimeFormat("en", {
46
- day: "numeric",
47
- hour: "numeric",
48
- month: "short",
49
- timeZone: "UTC",
50
- });
51
-
52
- const windowLabels = {
53
- "24h": "24 hours",
54
- "7d": "7 days",
55
- "30d": "30 days",
56
- } as const;
57
-
58
- const mergeActivitySeries = (
59
- installed: readonly SeriesPoint[],
60
- recovered: readonly SeriesPoint[],
61
- ): readonly ActivityChartPoint[] => {
62
- const installedByBucket = new Map(
63
- installed.map((point) => [point.bucketStartMs, point.value] as const),
64
- );
65
- const recoveredByBucket = new Map(
66
- recovered.map((point) => [point.bucketStartMs, point.value] as const),
67
- );
68
- const buckets = new Set([
69
- ...installedByBucket.keys(),
70
- ...recoveredByBucket.keys(),
71
- ]);
72
-
73
- return [...buckets]
74
- .sort((left, right) => left - right)
75
- .map((bucketStartMs) => ({
76
- bucketStartMs,
77
- installed: installedByBucket.get(bucketStartMs) ?? 0,
78
- recovered: recoveredByBucket.get(bucketStartMs) ?? 0,
79
- }));
80
- };
81
-
82
- const formatBucket = (
83
- value: number,
84
- window: ActiveInstallationWindow,
85
- ): string =>
86
- (window === "24h" ? hourFormatter : dateFormatter).format(new Date(value));
87
-
88
- export function BundleActivityChart({
89
- installed,
90
- recovered,
91
- window,
92
- }: BundleActivityChartProps) {
93
- const captionId = useId();
94
- const chartData = mergeActivitySeries(installed, recovered);
95
- const hasWindowActivity = chartData.some(
96
- (point) => point.installed > 0 || point.recovered > 0,
97
- );
98
-
99
- if (!hasWindowActivity) {
100
- return (
101
- <div className="flex h-16 items-center justify-center border-t px-4 pt-3 text-center text-xs text-muted-foreground sm:h-20 sm:pt-4">
102
- No device activity in the last 30 days.
103
- </div>
104
- );
105
- }
106
-
107
- return (
108
- <div className="border-t pt-3 sm:pt-4">
109
- <ChartContainer
110
- aria-describedby={captionId}
111
- aria-label={`Bundle movement over ${windowLabels[window]}`}
112
- className="h-24 w-full aspect-auto sm:h-32"
113
- config={chartConfig}
114
- role="img"
115
- >
116
- <AreaChart
117
- accessibilityLayer
118
- data={chartData}
119
- margin={{ left: -16, right: 4, top: 4 }}
120
- >
121
- <CartesianGrid vertical={false} />
122
- <XAxis
123
- axisLine={false}
124
- dataKey="bucketStartMs"
125
- minTickGap={28}
126
- tickFormatter={(value: number) => formatBucket(value, window)}
127
- tickLine={false}
128
- />
129
- <YAxis
130
- allowDecimals={false}
131
- axisLine={false}
132
- tickLine={false}
133
- width={32}
134
- />
135
- <ChartTooltip
136
- content={
137
- <ChartTooltipContent
138
- labelFormatter={(_, payload) => {
139
- const bucket = payload[0]?.payload.bucketStartMs;
140
- return typeof bucket === "number"
141
- ? formatBucket(bucket, window)
142
- : "";
143
- }}
144
- />
145
- }
146
- />
147
- <Area
148
- dataKey="installed"
149
- fill="var(--color-installed)"
150
- fillOpacity={0.12}
151
- isAnimationActive={false}
152
- stroke="var(--color-installed)"
153
- strokeWidth={2}
154
- type="monotone"
155
- />
156
- <Area
157
- dataKey="recovered"
158
- fill="var(--color-recovered)"
159
- fillOpacity={0.04}
160
- isAnimationActive={false}
161
- stroke="var(--color-recovered)"
162
- strokeWidth={1.5}
163
- type="monotone"
164
- />
165
- </AreaChart>
166
- </ChartContainer>
167
- <div className="sr-only">
168
- <table>
169
- <caption id={captionId}>
170
- Distinct bundle movement in each bucket over {windowLabels[window]}.
171
- Times are shown in UTC.
172
- </caption>
173
- <thead>
174
- <tr>
175
- <th scope="col">Date</th>
176
- <th scope="col">Applied</th>
177
- <th scope="col">Recovered</th>
178
- </tr>
179
- </thead>
180
- <tbody>
181
- {chartData.map((point) => (
182
- <tr key={point.bucketStartMs}>
183
- <th scope="row">{formatBucket(point.bucketStartMs, window)}</th>
184
- <td>{point.installed}</td>
185
- <td>{point.recovered}</td>
186
- </tr>
187
- ))}
188
- </tbody>
189
- </table>
190
- </div>
191
- </div>
192
- );
193
- }
@@ -1,123 +0,0 @@
1
- import { useInsightsCapability } from "@/components/features/insights/InsightsCapabilityContext";
2
- import { InsightsErrorAlert } from "@/components/features/insights/InsightsErrorAlert";
3
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4
- import { Skeleton } from "@/components/ui/skeleton";
5
- import { useBundleEventInsightsQuery } from "@/lib/api";
6
-
7
- import { BundleActivityChart } from "./BundleActivityChart";
8
-
9
- interface BundleInsightsSummaryProps {
10
- readonly bundleId: string;
11
- }
12
-
13
- function Metric({
14
- label,
15
- tone,
16
- value,
17
- }: {
18
- readonly label: string;
19
- readonly tone: "applied" | "recovered";
20
- readonly value: number;
21
- }) {
22
- return (
23
- <div className="flex flex-col gap-1">
24
- <dt className="flex items-center gap-2 text-xs text-muted-foreground">
25
- <span
26
- aria-hidden="true"
27
- className={
28
- tone === "applied"
29
- ? "size-2 rounded-full bg-chart-2"
30
- : "size-2 rounded-full bg-muted-foreground"
31
- }
32
- />
33
- {label}
34
- </dt>
35
- <dd className="text-xl font-semibold tracking-tight tabular-nums">
36
- {value}
37
- </dd>
38
- </div>
39
- );
40
- }
41
-
42
- function SupportedBundleInsightsSummary({
43
- bundleId,
44
- }: BundleInsightsSummaryProps) {
45
- const { data, error, isLoading } = useBundleEventInsightsQuery(
46
- {
47
- bundleId,
48
- window: "30d",
49
- limit: 1,
50
- offset: 0,
51
- },
52
- true,
53
- );
54
-
55
- return (
56
- <Card>
57
- <CardHeader className="p-4 pb-3">
58
- <CardTitle className="text-sm font-medium">
59
- Activity · 30 days
60
- </CardTitle>
61
- </CardHeader>
62
- <CardContent className="flex flex-col gap-3 px-4 pb-4">
63
- {isLoading ? (
64
- <div
65
- aria-label="Loading reported bundle outcomes"
66
- className="flex flex-col gap-3"
67
- >
68
- <div className="grid grid-cols-2 gap-4">
69
- <Skeleton className="h-10 w-full" />
70
- <Skeleton className="h-10 w-full" />
71
- </div>
72
- <Skeleton className="h-24 w-full sm:h-32" />
73
- </div>
74
- ) : error ? (
75
- <InsightsErrorAlert
76
- error={
77
- error instanceof Error
78
- ? error
79
- : new Error("Failed to load bundle insights.")
80
- }
81
- fallbackTitle="Insights unavailable"
82
- />
83
- ) : (
84
- <>
85
- <dl className="grid grid-cols-2 divide-x divide-border/70">
86
- <div className="pr-4">
87
- <Metric
88
- label="Applied"
89
- tone="applied"
90
- value={data?.summary.installed ?? 0}
91
- />
92
- </div>
93
- <div className="pl-4">
94
- <Metric
95
- label="Recovered"
96
- tone="recovered"
97
- value={data?.summary.recovered ?? 0}
98
- />
99
- </div>
100
- </dl>
101
- <BundleActivityChart
102
- installed={data?.series.installed ?? []}
103
- recovered={data?.series.recovered ?? []}
104
- window="30d"
105
- />
106
- </>
107
- )}
108
- </CardContent>
109
- </Card>
110
- );
111
- }
112
-
113
- export function BundleInsightsSummary({
114
- bundleId,
115
- }: BundleInsightsSummaryProps) {
116
- const capability = useInsightsCapability();
117
-
118
- if (capability.status !== "supported") {
119
- return null;
120
- }
121
-
122
- return <SupportedBundleInsightsSummary bundleId={bundleId} />;
123
- }
@@ -1,150 +0,0 @@
1
- import type {
2
- ActiveInstallationOverview,
3
- ActiveInstallationWindow,
4
- } from "@hot-updater/server";
5
- import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
6
-
7
- import {
8
- type ChartConfig,
9
- ChartContainer,
10
- ChartTooltip,
11
- ChartTooltipContent,
12
- } from "@/components/ui/chart";
13
-
14
- type ActivitySeries = ActiveInstallationOverview["series"];
15
-
16
- const dayFormatter = new Intl.DateTimeFormat("en", {
17
- day: "numeric",
18
- month: "short",
19
- timeZone: "UTC",
20
- });
21
- const hourFormatter = new Intl.DateTimeFormat("en", {
22
- day: "numeric",
23
- hour: "numeric",
24
- month: "short",
25
- timeZone: "UTC",
26
- });
27
-
28
- const formatBucket = (
29
- bucketStartMs: number,
30
- window: ActiveInstallationWindow,
31
- ): string =>
32
- (window === "24h" ? hourFormatter : dayFormatter).format(
33
- new Date(bucketStartMs),
34
- );
35
-
36
- const chartConfig = {
37
- installations: {
38
- label: "Active installations",
39
- color: "var(--chart-2)",
40
- },
41
- } satisfies ChartConfig;
42
-
43
- export function ActivityChart({
44
- series,
45
- window,
46
- }: {
47
- readonly series: ActivitySeries;
48
- readonly window: ActiveInstallationWindow;
49
- }) {
50
- const chartData = series.map((point) => ({
51
- bucketStartMs: point.bucketStartMs,
52
- installations: point.value,
53
- }));
54
- const hasReports = series.some(({ value }) => value > 0);
55
- const bucketName = window === "24h" ? "hour" : "day";
56
-
57
- return (
58
- <div className="flex min-w-0 flex-col gap-3">
59
- <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
60
- <div>
61
- <h3 className="text-sm font-medium">Activity over time</h3>
62
- <p className="text-xs text-muted-foreground">
63
- Unique active installations per {bucketName}
64
- </p>
65
- </div>
66
- <span className="text-xs text-muted-foreground">UTC</span>
67
- </div>
68
- {!hasReports && (
69
- <p className="text-sm text-muted-foreground">
70
- No installations reported during this period.
71
- </p>
72
- )}
73
- <ChartContainer
74
- aria-label={`Active installations per ${bucketName}`}
75
- className="h-64 w-full aspect-auto"
76
- config={chartConfig}
77
- role="img"
78
- >
79
- <AreaChart
80
- accessibilityLayer
81
- data={chartData}
82
- margin={{ left: -16, right: 4, top: 8 }}
83
- >
84
- <CartesianGrid vertical={false} />
85
- <XAxis
86
- axisLine={false}
87
- dataKey="bucketStartMs"
88
- minTickGap={28}
89
- tickFormatter={(value: number) => formatBucket(value, window)}
90
- tickLine={false}
91
- />
92
- <YAxis
93
- allowDecimals={false}
94
- axisLine={false}
95
- tickLine={false}
96
- width={32}
97
- />
98
- <ChartTooltip
99
- content={
100
- <ChartTooltipContent
101
- labelFormatter={(_, payload) => {
102
- const bucket = payload[0]?.payload.bucketStartMs;
103
- return typeof bucket === "number"
104
- ? formatBucket(bucket, window)
105
- : "";
106
- }}
107
- />
108
- }
109
- />
110
- <Area
111
- dataKey="installations"
112
- fill="var(--color-installations)"
113
- fillOpacity={0.12}
114
- isAnimationActive={false}
115
- stroke="var(--color-installations)"
116
- strokeWidth={2}
117
- type="monotone"
118
- />
119
- </AreaChart>
120
- </ChartContainer>
121
- <p className="text-xs leading-relaxed text-muted-foreground">
122
- Each point counts an installation once in that {bucketName}. The total
123
- above counts it once across the whole period, so the points do not add
124
- up to the total.
125
- </p>
126
- <div className="sr-only">
127
- <table aria-label={`Exact active installations per ${bucketName}`}>
128
- <caption>
129
- Unique installations that reported an update status in each UTC
130
- {` ${bucketName}`}.
131
- </caption>
132
- <thead>
133
- <tr>
134
- <th scope="col">UTC {bucketName}</th>
135
- <th scope="col">Active installations</th>
136
- </tr>
137
- </thead>
138
- <tbody>
139
- {chartData.map((point) => (
140
- <tr key={point.bucketStartMs}>
141
- <th scope="row">{formatBucket(point.bucketStartMs, window)}</th>
142
- <td>{point.installations}</td>
143
- </tr>
144
- ))}
145
- </tbody>
146
- </table>
147
- </div>
148
- </div>
149
- );
150
- }
@@ -1,133 +0,0 @@
1
- import type { ActiveInstallationOverview } from "@hot-updater/server";
2
- import { useState } from "react";
3
-
4
- import { Button } from "@/components/ui/button";
5
- import { Progress } from "@/components/ui/progress";
6
- import {
7
- Table,
8
- TableBody,
9
- TableCell,
10
- TableHead,
11
- TableHeader,
12
- TableRow,
13
- } from "@/components/ui/table";
14
- import type { InsightsOverview } from "@/lib/insights-overview";
15
-
16
- const PAGE_SIZE = 8;
17
-
18
- const percentage = (value: number): string =>
19
- new Intl.NumberFormat(undefined, {
20
- maximumFractionDigits: 1,
21
- style: "percent",
22
- }).format(value);
23
-
24
- export function BundleDistribution({
25
- active,
26
- catalog,
27
- }: {
28
- readonly active: ActiveInstallationOverview;
29
- readonly catalog: InsightsOverview;
30
- }) {
31
- const [page, setPage] = useState(1);
32
- const metadata = new Map(
33
- catalog.configuredRollouts.map(({ bundle, bundleId }) => [
34
- bundleId,
35
- bundle,
36
- ]),
37
- );
38
-
39
- if (active.bundles.length === 0) {
40
- return (
41
- <p className="py-6 text-center text-sm text-muted-foreground">
42
- No observed installations in this range.
43
- </p>
44
- );
45
- }
46
-
47
- const totalPages = Math.ceil(active.bundles.length / PAGE_SIZE);
48
- const currentPage = Math.min(page, totalPages);
49
- const start = (currentPage - 1) * PAGE_SIZE;
50
- const end = Math.min(start + PAGE_SIZE, active.bundles.length);
51
-
52
- return (
53
- <div className="flex min-w-0 flex-col gap-4">
54
- <div className="overflow-x-auto">
55
- <Table aria-label="Observed installations by bundle">
56
- <TableHeader>
57
- <TableRow className="hover:bg-transparent">
58
- <TableHead>Bundle</TableHead>
59
- <TableHead className="hidden w-2/5 sm:table-cell">
60
- Activity
61
- </TableHead>
62
- <TableHead className="text-right">Observed</TableHead>
63
- <TableHead className="text-right">Share</TableHead>
64
- </TableRow>
65
- </TableHeader>
66
- <TableBody>
67
- {active.bundles.slice(start, end).map((row) => {
68
- const bundle = metadata.get(row.bundleId);
69
- const share = row.installations / active.activeInstallations;
70
- const shareLabel = percentage(share);
71
- return (
72
- <TableRow key={row.bundleId}>
73
- <TableCell className="max-w-48 whitespace-normal">
74
- <div className="flex min-w-0 flex-col gap-1">
75
- <code className="break-all text-xs">{row.bundleId}</code>
76
- <span className="text-muted-foreground">
77
- {bundle
78
- ? `${bundle.platform} · ${bundle.channel}`
79
- : "Unknown bundle metadata"}
80
- </span>
81
- </div>
82
- </TableCell>
83
- <TableCell className="hidden sm:table-cell">
84
- <Progress
85
- aria-label={`${row.bundleId} observed share ${shareLabel}`}
86
- aria-valuetext={shareLabel}
87
- className="h-2"
88
- value={share * 100}
89
- />
90
- </TableCell>
91
- <TableCell className="text-right tabular-nums">
92
- {row.installations.toLocaleString()}
93
- </TableCell>
94
- <TableCell className="text-right tabular-nums">
95
- {shareLabel}
96
- </TableCell>
97
- </TableRow>
98
- );
99
- })}
100
- </TableBody>
101
- </Table>
102
- </div>
103
- {totalPages > 1 && (
104
- <div className="flex flex-wrap items-center justify-between gap-2 px-4 pb-4">
105
- <p className="text-xs text-muted-foreground tabular-nums">
106
- Showing {start + 1}–{end} of{" "}
107
- {active.bundles.length.toLocaleString()}
108
- </p>
109
- <div className="flex gap-2">
110
- <Button
111
- disabled={currentPage === 1}
112
- onClick={() => setPage(currentPage - 1)}
113
- size="sm"
114
- type="button"
115
- variant="outline"
116
- >
117
- Previous
118
- </Button>
119
- <Button
120
- disabled={currentPage === totalPages}
121
- onClick={() => setPage(currentPage + 1)}
122
- size="sm"
123
- type="button"
124
- variant="outline"
125
- >
126
- Next
127
- </Button>
128
- </div>
129
- </div>
130
- )}
131
- </div>
132
- );
133
- }