@hot-updater/console 1.0.0-rc.0 → 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 (203) 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-DspNKfMj.js → dist-C_NydL0e.js} +1 -1
  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-Bqc72UZG.js +1 -0
  21. package/.output/public/assets/{routes-DcQnVajO.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-DBP9TQ7K.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +44 -52
  30. package/.output/server/_libs/@base-ui/react+[...].mjs +1906 -4040
  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-BnM_o2nS.mjs → api-keys-CZeTPQys.mjs} +11 -13
  42. package/.output/server/_ssr/{api-keys-api-CIywsSF5.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
  43. package/.output/server/_ssr/{api-keys-rpc-BSp1wkIV.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
  44. package/.output/server/_ssr/{api-rpc-cG5wd_tO.mjs → api-rpc-tAupfGQm.mjs} +73 -142
  45. package/.output/server/_ssr/{auth-rpc-BrJ6obHJ.mjs → auth-rpc-DUaeOWKK.mjs} +4 -4
  46. package/.output/server/_ssr/{auth.server-DwKoZoQM.mjs → auth.server-DW71WzsD.mjs} +2 -5
  47. package/.output/server/_ssr/{bundle-signing-rpc-COoQhW8E.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
  48. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
  49. package/.output/server/_ssr/{alert-Dim3MGY5.mjs → card-BRiPmi0C.mjs} +61 -39
  50. package/.output/server/_ssr/{config.server-DEiLhKmy.mjs → config.server-afDuAwaG.mjs} +15 -8
  51. package/.output/server/_ssr/{createSsrRpc-9P0S0lJ3.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
  52. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
  53. package/.output/server/_ssr/{databaseRows-CwbNwYSl.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
  54. package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
  55. package/.output/server/_ssr/{dialog-CQuRMNZ4.mjs → dialog-DS-nw-pA.mjs} +2 -2
  56. package/.output/server/_ssr/{dist-BVSLTpoP.mjs → dist-CyzPaI5v.mjs} +2 -2
  57. package/.output/server/_ssr/{getBundleChildren-BcRMFoDF.mjs → getBundleChildren-C0vh4ZvE.mjs} +2 -2
  58. package/.output/server/_ssr/input-group-BpsnmvZw.mjs +70 -0
  59. package/.output/server/_ssr/insights-BdS364Xu.mjs +460 -0
  60. package/.output/server/_ssr/insights-rpc-38DujK0G.mjs +57 -0
  61. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +303 -0
  62. package/.output/server/_ssr/installations-CtA59MQG.mjs +29 -0
  63. package/.output/server/_ssr/installations-DiVKEF5T.mjs +627 -0
  64. package/.output/server/_ssr/{router-C-KUBfOi.mjs → router-B9wG_9f8.mjs} +37 -46
  65. package/.output/server/_ssr/{routes-Z7TwC6nC.mjs → routes-CKrrS2q2.mjs} +3 -3
  66. package/.output/server/_ssr/{routes-BM4ht9jb.mjs → routes-D7Yh_jfj.mjs} +402 -390
  67. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
  68. package/.output/server/_ssr/{server-BjzmvXnO.mjs → server-D_C2rMzf.mjs} +5 -5
  69. package/.output/server/_ssr/{signing-k8GxyBDF.mjs → signing-Dz1WkZOf.mjs} +16 -85
  70. package/.output/server/_ssr/ssr.mjs +1 -1
  71. package/.output/server/_ssr/{field-DRIXKjZG.mjs → table-CnZmon_Z.mjs} +51 -5
  72. package/.output/server/_ssr/{uuidv7-BbJ-9D_I.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
  73. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
  74. package/.output/server/index.mjs +168 -203
  75. package/lib/index.d.mts +4 -4
  76. package/lib/vite-internal.mjs +1 -1
  77. package/lib/vite.d.mts +0 -1
  78. package/package.json +8 -8
  79. package/src/components/AppSidebar.tsx +12 -16
  80. package/src/components/ConsoleAccessPage.tsx +1 -1
  81. package/src/components/HashValueDisplay.tsx +11 -2
  82. package/src/components/features/api-keys/ApiKeysPage.tsx +1 -1
  83. package/src/components/features/bundles/BundleChildrenPanel.tsx +7 -6
  84. package/src/components/features/bundles/BundleMetadata.tsx +1 -20
  85. package/src/components/features/channels/ChannelManagementDialog.tsx +1 -1
  86. package/src/components/features/insights/EventDetails.tsx +151 -0
  87. package/src/components/features/insights/EventHistoryCard.tsx +245 -0
  88. package/src/components/features/insights/EventHistoryList.tsx +63 -0
  89. package/src/components/features/insights/InsightsBundleReports.tsx +51 -0
  90. package/src/components/features/insights/InsightsControls.tsx +121 -0
  91. package/src/components/features/{analytics/AnalyticsErrorAlert.tsx → insights/InsightsErrorAlert.tsx} +6 -6
  92. package/src/components/features/insights/InsightsOverview.tsx +203 -0
  93. package/src/components/features/insights/InsightsPageHeader.tsx +48 -0
  94. package/src/components/features/insights/InsightsPagination.tsx +52 -0
  95. package/src/components/features/insights/InstallationHistoryCard.tsx +250 -0
  96. package/src/components/features/insights/InstallationMatchesCard.tsx +178 -0
  97. package/src/components/features/insights/InstallationPageHeader.tsx +90 -0
  98. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
  99. package/src/components/features/signing/BundleSigningPage.tsx +18 -126
  100. package/src/components/ui/badge.tsx +2 -0
  101. package/src/components/ui/combobox.tsx +1 -0
  102. package/src/components/ui/skeleton.tsx +4 -1
  103. package/src/components/ui/tooltip.tsx +1 -1
  104. package/src/index.ts +7 -3
  105. package/src/lib/api-rpc.ts +19 -90
  106. package/src/lib/api.ts +0 -99
  107. package/src/lib/insights-api.ts +100 -0
  108. package/src/lib/insights-rpc.ts +88 -0
  109. package/src/lib/insights-view.ts +15 -0
  110. package/src/lib/public-bundle-error.ts +19 -0
  111. package/src/lib/server/bundle-signing.server.ts +5 -82
  112. package/src/lib/server/console-runtime.server.ts +0 -9
  113. package/src/lib/server/releaseReachability.ts +1 -4
  114. package/src/lib/server/runtime.server.ts +6 -142
  115. package/src/routeTree.gen.ts +21 -21
  116. package/src/router.tsx +3 -0
  117. package/src/routes/-installations-search.ts +25 -12
  118. package/src/routes/__root.tsx +6 -16
  119. package/src/routes/api-keys.tsx +1 -1
  120. package/src/routes/index.tsx +21 -81
  121. package/src/routes/insights.tsx +84 -0
  122. package/src/routes/installations.tsx +294 -142
  123. package/src/styles.css +6 -0
  124. package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +0 -1
  125. package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +0 -42
  126. package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +0 -1
  127. package/.output/public/assets/alert-C-1KvDW8.js +0 -1
  128. package/.output/public/assets/analytics-Bqz0c-BT.js +0 -1
  129. package/.output/public/assets/analytics-api-CJdrTLLO.js +0 -1
  130. package/.output/public/assets/api-B_Rh-uzC.js +0 -1
  131. package/.output/public/assets/api-keys-BurCFhzH.js +0 -1
  132. package/.output/public/assets/api-keys-api-BEDJdQg3.js +0 -1
  133. package/.output/public/assets/badge-Bf0fZ-Gx.js +0 -1
  134. package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
  135. package/.output/public/assets/dialog-BS60BRr2.js +0 -1
  136. package/.output/public/assets/download-O-yNBc9H.js +0 -1
  137. package/.output/public/assets/field-Buq148-A.js +0 -1
  138. package/.output/public/assets/index-kFhd2S78.js +0 -10
  139. package/.output/public/assets/installations-BrQ-od41.js +0 -2
  140. package/.output/public/assets/installations-CGlZ3ROz.js +0 -1
  141. package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +0 -1
  142. package/.output/public/assets/loader-circle-DCYF-wJl.js +0 -1
  143. package/.output/public/assets/refresh-cw-n8XYbPPj.js +0 -1
  144. package/.output/public/assets/routes-CrdokJ80.js +0 -1
  145. package/.output/public/assets/search-DLhbWw3W.js +0 -1
  146. package/.output/public/assets/signing-HRexfjsH.js +0 -1
  147. package/.output/public/assets/styles-eB4EtvFS.css +0 -2
  148. package/.output/public/assets/table-D8CLieHX.js +0 -1
  149. package/.output/public/assets/textarea-YJLTVAoq.js +0 -1
  150. package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
  151. package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +0 -1
  152. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1554
  153. package/.output/server/_libs/d3-array.mjs +0 -231
  154. package/.output/server/_libs/d3-color.mjs +0 -330
  155. package/.output/server/_libs/d3-format.mjs +0 -252
  156. package/.output/server/_libs/d3-interpolate.mjs +0 -207
  157. package/.output/server/_libs/d3-path.mjs +0 -77
  158. package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
  159. package/.output/server/_libs/d3-shape.mjs +0 -818
  160. package/.output/server/_libs/decimal.js-light.mjs +0 -884
  161. package/.output/server/_libs/es-toolkit.mjs +0 -1134
  162. package/.output/server/_libs/eventemitter3.mjs +0 -236
  163. package/.output/server/_libs/immer.mjs +0 -532
  164. package/.output/server/_libs/react-redux+[...].mjs +0 -205
  165. package/.output/server/_libs/recharts+[...].mjs +0 -14117
  166. package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +0 -15
  167. package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +0 -288
  168. package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +0 -13
  169. package/.output/server/_ssr/analytics-DtMTn8fn.mjs +0 -858
  170. package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +0 -71
  171. package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +0 -96
  172. package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +0 -155
  173. package/.output/server/_ssr/api-BTSv1tZb.mjs +0 -282
  174. package/.output/server/_ssr/badge-DCxbEeof.mjs +0 -26
  175. package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +0 -51
  176. package/.output/server/_ssr/installations-B-azgAvX.mjs +0 -19
  177. package/.output/server/_ssr/installations-Ctvzp2JH.mjs +0 -535
  178. package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +0 -527
  179. package/.output/server/_ssr/table-v1yCvYo7.mjs +0 -53
  180. package/.output/server/_ssr/textarea-DYHnskhb.mjs +0 -14
  181. package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +0 -108
  182. package/src/components/features/analytics/ActivityChart.tsx +0 -150
  183. package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +0 -24
  184. package/src/components/features/analytics/AnalyticsControls.tsx +0 -115
  185. package/src/components/features/analytics/AnalyticsOverview.tsx +0 -208
  186. package/src/components/features/analytics/BundleDistribution.tsx +0 -133
  187. package/src/components/features/analytics/BundleSelector.tsx +0 -114
  188. package/src/components/features/analytics/InstallationHistoryCard.tsx +0 -248
  189. package/src/components/features/analytics/InstallationMatchesCard.tsx +0 -132
  190. package/src/components/features/analytics/InstallationPageHeader.tsx +0 -138
  191. package/src/components/features/analytics/InstallationPagination.tsx +0 -48
  192. package/src/components/features/analytics/InstallationSearch.tsx +0 -230
  193. package/src/components/features/analytics/UpdateOutcomes.tsx +0 -147
  194. package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
  195. package/src/components/features/bundles/BundleAnalyticsSummary.tsx +0 -123
  196. package/src/lib/analytics-api.ts +0 -145
  197. package/src/lib/analytics-error.ts +0 -19
  198. package/src/lib/analytics-input.ts +0 -187
  199. package/src/lib/analytics-overview.ts +0 -170
  200. package/src/lib/analytics-rpc.ts +0 -218
  201. package/src/lib/server/releaseActivity.ts +0 -27
  202. package/src/routes/analytics.tsx +0 -139
  203. /package/.output/public/assets/{preload-helper-TKOBYGYD.js → preload-helper-CQ36nxok.js} +0 -0
@@ -0,0 +1,250 @@
1
+ import { RefreshCw } from "lucide-react";
2
+
3
+ import { HashValueDisplay } from "@/components/HashValueDisplay";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Separator } from "@/components/ui/separator";
7
+ import { Skeleton } from "@/components/ui/skeleton";
8
+ import {
9
+ Table,
10
+ TableBody,
11
+ TableCell,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ } from "@/components/ui/table";
16
+ import type {
17
+ InsightsEventRow,
18
+ InsightsInstallationViewRow,
19
+ InsightsViewPage,
20
+ } from "@/lib/insights-view";
21
+
22
+ import {
23
+ EventBundleTransition,
24
+ EventTimestamp,
25
+ EventTypeBadge,
26
+ useInsightsTimeFormat,
27
+ } from "./EventDetails";
28
+ import { EventHistoryList } from "./EventHistoryList";
29
+ import { InsightsErrorAlert } from "./InsightsErrorAlert";
30
+ import { InsightsPagination } from "./InsightsPagination";
31
+
32
+ const getUserLabel = (event: {
33
+ readonly username: string | null;
34
+ readonly userId: string | null;
35
+ }) => event.userId ?? event.username ?? "—";
36
+
37
+ const getLastKnownBundleId = (
38
+ event: InsightsEventRow | InsightsInstallationViewRow,
39
+ ) =>
40
+ "lastKnownBundleId" in event
41
+ ? event.lastKnownBundleId
42
+ : (event.toBundleId ?? event.fromBundleId);
43
+
44
+ export function InstallationHistoryCard({
45
+ error,
46
+ history,
47
+ isLoading,
48
+ onNext,
49
+ onPrevious,
50
+ onRefresh,
51
+ pageNumber,
52
+ selectedEvent,
53
+ selectedInstallId,
54
+ }: {
55
+ readonly error: unknown;
56
+ readonly history: InsightsViewPage<InsightsEventRow> | undefined;
57
+ readonly isLoading: boolean;
58
+ readonly onRefresh?: () => void;
59
+ readonly onNext: () => void;
60
+ readonly onPrevious: () => void;
61
+ readonly pageNumber: number;
62
+ readonly selectedEvent:
63
+ | InsightsEventRow
64
+ | InsightsInstallationViewRow
65
+ | undefined;
66
+ readonly selectedInstallId: string | undefined;
67
+ }) {
68
+ const dateTimeFormat = useInsightsTimeFormat();
69
+ const lastKnownBundleId = selectedEvent
70
+ ? getLastKnownBundleId(selectedEvent)
71
+ : undefined;
72
+
73
+ return (
74
+ <Card className="@container min-h-0 min-w-0 shadow-sm">
75
+ <CardHeader className="p-4 sm:p-6">
76
+ <div className="flex flex-wrap items-center justify-between gap-3">
77
+ <div className="flex min-w-0 flex-col gap-1.5">
78
+ <CardTitle className="text-sm font-medium">
79
+ <h2>
80
+ {selectedInstallId !== undefined
81
+ ? "Installation history"
82
+ : "Select an installation"}
83
+ </h2>
84
+ </CardTitle>
85
+ </div>
86
+ {onRefresh && selectedInstallId !== undefined ? (
87
+ <Button
88
+ className="h-11 lg:h-8"
89
+ size="lg"
90
+ variant="outline"
91
+ onClick={onRefresh}
92
+ disabled={isLoading}
93
+ >
94
+ <RefreshCw aria-hidden="true" data-icon="inline-start" />
95
+ Refresh
96
+ </Button>
97
+ ) : null}
98
+ </div>
99
+ </CardHeader>
100
+ <CardContent className="min-h-0 p-0">
101
+ {selectedInstallId !== undefined && selectedEvent ? (
102
+ <>
103
+ <section
104
+ aria-labelledby="latest-installation-state"
105
+ className="px-4 pb-4 sm:px-6 sm:pb-6"
106
+ >
107
+ <h3 className="sr-only" id="latest-installation-state">
108
+ Latest reported state
109
+ </h3>
110
+ <dl className="grid grid-cols-2 gap-4 @[36rem]:grid-cols-3">
111
+ <div className="col-span-2 min-w-0 @[36rem]:col-span-1">
112
+ <dt className="text-xs text-muted-foreground">User ID</dt>
113
+ <dd className="mt-1 text-sm font-medium wrap-anywhere">
114
+ {getUserLabel(selectedEvent)}
115
+ </dd>
116
+ </div>
117
+ <div className="min-w-0">
118
+ <dt className="text-xs text-muted-foreground">Install ID</dt>
119
+ <dd className="mt-1 font-mono text-xs">
120
+ <HashValueDisplay
121
+ value={selectedInstallId}
122
+ buttonClassName="min-h-11 px-3 lg:min-h-0 lg:px-1.5"
123
+ />
124
+ </dd>
125
+ </div>
126
+ <div className="min-w-0">
127
+ <dt className="text-xs text-muted-foreground">
128
+ Last known bundle
129
+ </dt>
130
+ <dd className="mt-1 font-mono text-xs">
131
+ {lastKnownBundleId ? (
132
+ <HashValueDisplay
133
+ value={lastKnownBundleId}
134
+ buttonClassName="min-h-11 px-3 lg:min-h-0 lg:px-1.5"
135
+ />
136
+ ) : (
137
+ "—"
138
+ )}
139
+ </dd>
140
+ </div>
141
+ </dl>
142
+ </section>
143
+ <Separator />
144
+ </>
145
+ ) : null}
146
+ {selectedInstallId !== undefined ? (
147
+ isLoading ? (
148
+ <div
149
+ className="flex flex-col gap-3 p-6"
150
+ role="status"
151
+ aria-label="Loading installation history"
152
+ >
153
+ <Skeleton className="h-10 w-full" />
154
+ <Skeleton className="h-10 w-full" />
155
+ <Skeleton className="h-10 w-full" />
156
+ </div>
157
+ ) : error ? (
158
+ <div className="p-6">
159
+ <InsightsErrorAlert
160
+ error={error instanceof Error ? error : new Error()}
161
+ fallbackTitle="Installation history unavailable"
162
+ />
163
+ </div>
164
+ ) : history && history.data.length > 0 ? (
165
+ <>
166
+ <div className="@[48rem]:hidden">
167
+ <EventHistoryList
168
+ events={history.data}
169
+ formatter={dateTimeFormat}
170
+ />
171
+ </div>
172
+ <div className="hidden @[48rem]:block">
173
+ <Table className="min-w-3xl table-fixed">
174
+ <TableHeader>
175
+ <TableRow className="[&>th]:px-4 sm:[&>th]:px-6">
176
+ <TableHead className="w-64">Time</TableHead>
177
+ <TableHead className="w-48">Event</TableHead>
178
+ <TableHead className="w-32">App</TableHead>
179
+ <TableHead className="w-52">Bundle</TableHead>
180
+ </TableRow>
181
+ </TableHeader>
182
+ <TableBody>
183
+ {history.data.map((event) => (
184
+ <TableRow
185
+ className="[&>td]:px-4 [&>td]:py-4 [&>td]:align-top sm:[&>td]:px-6"
186
+ key={event.id}
187
+ >
188
+ <TableCell className="whitespace-normal text-xs tabular-nums">
189
+ <EventTimestamp
190
+ value={event.receivedAtMs}
191
+ formatter={dateTimeFormat}
192
+ />
193
+ </TableCell>
194
+ <TableCell>
195
+ <EventTypeBadge type={event.type} />
196
+ </TableCell>
197
+ <TableCell className="whitespace-normal text-xs">
198
+ <div className="flex flex-col gap-2">
199
+ <span>
200
+ {event.platform === "ios" ? "iOS" : "Android"}{" "}
201
+ {event.appVersion}
202
+ </span>
203
+ <span className="text-muted-foreground">
204
+ {event.channel}
205
+ </span>
206
+ </div>
207
+ </TableCell>
208
+ <TableCell className="whitespace-normal text-xs">
209
+ <EventBundleTransition event={event} />
210
+ </TableCell>
211
+ </TableRow>
212
+ ))}
213
+ </TableBody>
214
+ </Table>
215
+ </div>
216
+ <InsightsPagination
217
+ hasPrevious={pageNumber > 1}
218
+ label="Installation history"
219
+ nextCursor={history.nextCursor}
220
+ onNext={onNext}
221
+ onPrevious={onPrevious}
222
+ pageLength={history.data.length}
223
+ pageNumber={pageNumber}
224
+ />
225
+ </>
226
+ ) : (
227
+ <>
228
+ <div className="p-6 text-sm text-muted-foreground">
229
+ {pageNumber > 1 || history?.nextCursor
230
+ ? "No bundle changes on this page."
231
+ : "No bundle changes recorded yet."}
232
+ </div>
233
+ {history && (pageNumber > 1 || history.nextCursor) ? (
234
+ <InsightsPagination
235
+ hasPrevious={pageNumber > 1}
236
+ label="Installation history"
237
+ nextCursor={history.nextCursor}
238
+ onNext={onNext}
239
+ onPrevious={onPrevious}
240
+ pageLength={0}
241
+ pageNumber={pageNumber}
242
+ />
243
+ ) : null}
244
+ </>
245
+ )
246
+ ) : null}
247
+ </CardContent>
248
+ </Card>
249
+ );
250
+ }
@@ -0,0 +1,178 @@
1
+ import { Check, ChevronDown } from "lucide-react";
2
+ import { useId, useRef, useState } from "react";
3
+
4
+ import { shortenIdentifier } from "@/components/HashValueDisplay";
5
+ import { Button, buttonVariants } from "@/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import type {
8
+ InsightsInstallationViewRow,
9
+ InsightsViewPage,
10
+ } from "@/lib/insights-view";
11
+ import { cn } from "@/lib/utils";
12
+
13
+ import { InsightsErrorAlert } from "./InsightsErrorAlert";
14
+ import { InsightsPagination } from "./InsightsPagination";
15
+
16
+ const getLastKnownBundleId = (event: InsightsInstallationViewRow) =>
17
+ event.lastKnownBundleId;
18
+
19
+ const getUserLabel = (event: InsightsInstallationViewRow) =>
20
+ event.userId ?? event.username ?? "—";
21
+
22
+ export function InstallationMatchesCard({
23
+ error,
24
+ onNext,
25
+ onPrevious,
26
+ onSelect,
27
+ pageNumber,
28
+ results,
29
+ selectedInstallId,
30
+ }: {
31
+ readonly error: unknown;
32
+ readonly onNext: () => void;
33
+ readonly onPrevious: () => void;
34
+ readonly onSelect: (installId: string) => void;
35
+ readonly pageNumber: number;
36
+ readonly results: InsightsViewPage<InsightsInstallationViewRow> | undefined;
37
+ readonly selectedInstallId: string | undefined;
38
+ }) {
39
+ const [expanded, setExpanded] = useState(false);
40
+ const contentId = useId();
41
+ const triggerRef = useRef<HTMLButtonElement>(null);
42
+ const canCollapse = !error && results && results.data.length > 0;
43
+
44
+ return (
45
+ <Card className="min-h-0 min-w-0 shadow-sm">
46
+ <CardHeader className="p-4 sm:px-6">
47
+ <div className="flex items-center justify-between gap-3">
48
+ <div className="flex min-w-0 flex-col gap-1.5">
49
+ <CardTitle className="text-sm font-medium">
50
+ <h2>Matching installations</h2>
51
+ </CardTitle>
52
+ </div>
53
+ <div className="flex shrink-0 items-center gap-2">
54
+ {canCollapse ? (
55
+ <Button
56
+ aria-controls={contentId}
57
+ aria-expanded={expanded}
58
+ aria-label={
59
+ expanded
60
+ ? "Hide matching installations"
61
+ : "Show matching installations"
62
+ }
63
+ className="size-11 lg:hidden"
64
+ onClick={() => setExpanded(!expanded)}
65
+ ref={triggerRef}
66
+ size="icon-lg"
67
+ variant="ghost"
68
+ >
69
+ <ChevronDown
70
+ aria-hidden="true"
71
+ className={cn(expanded && "rotate-180")}
72
+ />
73
+ </Button>
74
+ ) : null}
75
+ </div>
76
+ </div>
77
+ </CardHeader>
78
+ <CardContent
79
+ className={cn(
80
+ "min-h-0 p-0",
81
+ canCollapse && !expanded && "hidden lg:block",
82
+ )}
83
+ id={contentId}
84
+ >
85
+ {error ? (
86
+ <div className="p-6">
87
+ <InsightsErrorAlert
88
+ error={error instanceof Error ? error : new Error()}
89
+ fallbackTitle="Installation search unavailable"
90
+ />
91
+ </div>
92
+ ) : results && results.data.length > 0 ? (
93
+ <ul aria-label="Matching installations" className="divide-y">
94
+ {results.data.map((event) => {
95
+ const currentBundleId = getLastKnownBundleId(event);
96
+ const isSelected = event.installId === selectedInstallId;
97
+ return (
98
+ <li key={event.installId}>
99
+ <button
100
+ aria-label={`${getUserLabel(event)}, install ID ${event.installId}`}
101
+ aria-pressed={isSelected}
102
+ className="group flex w-full min-w-0 flex-col gap-2 border-l-2 border-transparent px-4 py-4 text-left outline-none transition-colors hover:bg-muted/40 focus-visible:border-ring focus-visible:bg-muted/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/30 aria-pressed:border-primary aria-pressed:bg-muted/60 motion-reduce:transition-none sm:px-6"
103
+ onClick={() => {
104
+ onSelect(event.installId);
105
+ setExpanded(false);
106
+ triggerRef.current?.focus();
107
+ }}
108
+ type="button"
109
+ >
110
+ <span className="flex w-full min-w-0 items-center justify-between gap-3">
111
+ <span className="min-w-0 truncate text-sm font-medium">
112
+ {getUserLabel(event)}
113
+ </span>
114
+ {isSelected ? (
115
+ <span className="flex shrink-0 items-center gap-1 text-xs font-medium text-primary">
116
+ <Check aria-hidden="true" className="size-3.5" />
117
+ <span className="sr-only">Selected</span>
118
+ </span>
119
+ ) : null}
120
+ </span>
121
+ <span className="flex flex-wrap items-center gap-2">
122
+ <span
123
+ className="font-mono text-xs"
124
+ title={event.installId}
125
+ >
126
+ {shortenIdentifier(event.installId)}
127
+ </span>
128
+ </span>
129
+ <span className="flex min-w-0 flex-wrap items-center gap-2">
130
+ <span className="text-xs text-muted-foreground">
131
+ Bundle
132
+ </span>
133
+ {currentBundleId ? (
134
+ <span
135
+ className="font-mono text-xs"
136
+ title={currentBundleId}
137
+ >
138
+ {shortenIdentifier(currentBundleId)}
139
+ </span>
140
+ ) : (
141
+ <span className="text-sm text-muted-foreground">—</span>
142
+ )}
143
+ </span>
144
+ </button>
145
+ </li>
146
+ );
147
+ })}
148
+ </ul>
149
+ ) : (
150
+ <div className="flex flex-wrap items-center justify-between gap-3 p-4 sm:px-6">
151
+ <p className="text-sm text-muted-foreground">No matches</p>
152
+ <a
153
+ className={buttonVariants({
154
+ variant: "outline",
155
+ className: "h-11 px-3 lg:h-8",
156
+ })}
157
+ href="#installation-history-search"
158
+ >
159
+ Edit search
160
+ </a>
161
+ </div>
162
+ )}
163
+ {results &&
164
+ (results.data.length > 0 || pageNumber > 1 || results.nextCursor) ? (
165
+ <InsightsPagination
166
+ hasPrevious={pageNumber > 1}
167
+ label="Installation results"
168
+ nextCursor={results.nextCursor}
169
+ onNext={onNext}
170
+ onPrevious={onPrevious}
171
+ pageLength={results.data.length}
172
+ pageNumber={pageNumber}
173
+ />
174
+ ) : null}
175
+ </CardContent>
176
+ </Card>
177
+ );
178
+ }
@@ -0,0 +1,90 @@
1
+ import { Search, X } from "lucide-react";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Field, FieldLabel } from "@/components/ui/field";
5
+ import {
6
+ InputGroup,
7
+ InputGroupAddon,
8
+ InputGroupButton,
9
+ InputGroupInput,
10
+ } from "@/components/ui/input-group";
11
+ import { Skeleton } from "@/components/ui/skeleton";
12
+
13
+ export function InstallationSearchPanel({
14
+ draftQuery,
15
+ onDraftQueryChange,
16
+ onSubmit,
17
+ onClear,
18
+ }: {
19
+ readonly draftQuery: string;
20
+ readonly onDraftQueryChange: (value: string) => void;
21
+ readonly onSubmit: () => void;
22
+ readonly onClear: () => void;
23
+ }) {
24
+ return (
25
+ <form
26
+ aria-label="Find installation"
27
+ onSubmit={(event) => {
28
+ event.preventDefault();
29
+ if (draftQuery.trim()) onSubmit();
30
+ }}
31
+ role="search"
32
+ >
33
+ <Field className="gap-2">
34
+ <FieldLabel className="sr-only" htmlFor="installation-history-search">
35
+ User ID or installation ID
36
+ </FieldLabel>
37
+ <div className="flex flex-wrap gap-2">
38
+ <InputGroup className="h-11 w-auto min-w-0 flex-1 basis-44 lg:h-8">
39
+ <InputGroupAddon>
40
+ <Search aria-hidden="true" />
41
+ </InputGroupAddon>
42
+ <InputGroupInput
43
+ autoComplete="off"
44
+ className="h-11 text-base md:text-base lg:h-8 lg:text-xs"
45
+ id="installation-history-search"
46
+ maxLength={1024}
47
+ name="installation"
48
+ onChange={(event) => onDraftQueryChange(event.target.value)}
49
+ placeholder="User ID or installation ID"
50
+ spellCheck={false}
51
+ type="search"
52
+ value={draftQuery}
53
+ />
54
+ {draftQuery.length > 0 ? (
55
+ <InputGroupAddon align="inline-end">
56
+ <InputGroupButton
57
+ aria-label="Clear installation lookup"
58
+ className="size-11 lg:size-5"
59
+ onClick={onClear}
60
+ size="icon-xs"
61
+ >
62
+ <X aria-hidden="true" />
63
+ </InputGroupButton>
64
+ </InputGroupAddon>
65
+ ) : null}
66
+ </InputGroup>
67
+ <Button
68
+ aria-label="Find installation"
69
+ className="h-11 px-3 lg:h-8"
70
+ disabled={draftQuery.trim().length === 0}
71
+ size="lg"
72
+ type="submit"
73
+ variant="outline"
74
+ >
75
+ Find
76
+ </Button>
77
+ </div>
78
+ </Field>
79
+ </form>
80
+ );
81
+ }
82
+
83
+ export function InstallationResultsSkeleton() {
84
+ return (
85
+ <div className="grid items-stretch gap-6 lg:min-h-96 lg:grid-cols-[minmax(18rem,20rem)_minmax(0,1fr)]">
86
+ <Skeleton className="h-64 w-full lg:h-full" />
87
+ <Skeleton className="h-64 w-full lg:h-full" />
88
+ </div>
89
+ );
90
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ getPatchBaseBundleId,
2
3
  INVALID_COHORT_ERROR_MESSAGE,
3
4
  isValidCohort,
4
5
  normalizeCohortValue,
@@ -14,7 +15,6 @@ import { toast } from "sonner";
14
15
  import { normalizeRange } from "verkit";
15
16
 
16
17
  import { BundleIdDisplay } from "@/components/BundleIdDisplay";
17
- import { BundleAnalyticsSummary } from "@/components/features/bundles/BundleAnalyticsSummary";
18
18
  import { BundleMetadata } from "@/components/features/bundles/BundleMetadata";
19
19
  import { RolloutCohortsDialog } from "@/components/features/bundles/RolloutCohortsDialog";
20
20
  import { PlatformIcon } from "@/components/PlatformIcon";
@@ -215,6 +215,7 @@ export function ReleaseEditorSheet({
215
215
  const release = releaseQuery.data;
216
216
  const bundleQuery = useBundleQuery(release?.bundle_id ?? "");
217
217
  const bundle = bundleQuery.data;
218
+ const patchBaseBundleId = bundle ? getPatchBaseBundleId(bundle) : null;
218
219
  const diagnostics = useReleaseCatalogDiagnosticsQuery(
219
220
  release?.scope_key ?? "",
220
221
  );
@@ -295,6 +296,7 @@ export function ReleaseEditorSheet({
295
296
  await preflight.mutateAsync(input);
296
297
  await update.mutateAsync(input);
297
298
  toast.success("Bundle settings saved");
299
+ onOpenChange(false);
298
300
  } catch (caught) {
299
301
  setError(
300
302
  caught instanceof Error
@@ -357,21 +359,18 @@ export function ReleaseEditorSheet({
357
359
  </div>
358
360
  </div>
359
361
  <div className="flex flex-wrap items-start gap-2">
360
- <span className="font-medium text-muted-foreground">
361
- Bundle
362
- </span>
362
+ <span className="font-medium text-muted-foreground">ID</span>
363
363
  <span className="min-w-0 basis-full sm:basis-auto">
364
- {release.bundle_id ? (
365
- <BundleIdDisplay
366
- bundleId={release.bundle_id}
367
- fullOnMobile
368
- maxLength={18}
369
- />
370
- ) : (
364
+ <BundleIdDisplay
365
+ bundleId={release.id}
366
+ fullOnMobile
367
+ maxLength={18}
368
+ />
369
+ {!release.bundle_id ? (
371
370
  <span className="text-xs text-foreground">
372
371
  Built-in app
373
372
  </span>
374
- )}
373
+ ) : null}
375
374
  </span>
376
375
  </div>
377
376
  <div className="flex flex-wrap items-center gap-2">
@@ -409,10 +408,6 @@ export function ReleaseEditorSheet({
409
408
 
410
409
  {release && draft ? (
411
410
  <div className="flex flex-col gap-6 px-4 pb-4 sm:px-6 sm:pb-6">
412
- {release.bundle_id ? (
413
- <BundleAnalyticsSummary bundleId={release.bundle_id} />
414
- ) : null}
415
-
416
411
  <section
417
412
  aria-labelledby="delivery-settings-heading"
418
413
  className="flex flex-col gap-4"
@@ -478,7 +473,7 @@ export function ReleaseEditorSheet({
478
473
  </div>
479
474
  <FieldDescription>
480
475
  Disabling rolls devices back to the previous enabled
481
- Release on their next check, or to the built-in app when
476
+ bundle on their next check, or to the built-in app when
482
477
  none remains.
483
478
  </FieldDescription>
484
479
  </Field>
@@ -655,9 +650,9 @@ export function ReleaseEditorSheet({
655
650
  </summary>
656
651
  <dl className="mt-4 grid gap-3 text-muted-foreground sm:grid-cols-2">
657
652
  <div className="sm:col-span-2">
658
- <dt>Release ID</dt>
653
+ <dt>Artifact ID</dt>
659
654
  <dd className="mt-1 break-all font-mono text-foreground">
660
- {release.id}
655
+ {release.bundle_id ?? "Built-in app"}
661
656
  </dd>
662
657
  </div>
663
658
  <div>
@@ -666,6 +661,14 @@ export function ReleaseEditorSheet({
666
661
  {release.revision}
667
662
  </dd>
668
663
  </div>
664
+ {patchBaseBundleId ? (
665
+ <div className="sm:col-span-2">
666
+ <dt>Patch base artifact ID</dt>
667
+ <dd className="mt-1 break-all font-mono text-foreground">
668
+ {patchBaseBundleId}
669
+ </dd>
670
+ </div>
671
+ ) : null}
669
672
  <div>
670
673
  <dt>Catalog generation</dt>
671
674
  <dd className="mt-1 font-mono text-foreground">
@@ -730,8 +733,8 @@ export function ReleaseEditorSheet({
730
733
  Remove this Bundle from {channelName ?? "the channel"}?
731
734
  </AlertDialogTitle>
732
735
  <AlertDialogDescription>
733
- The Bundle file stays available. This only removes it from the
734
- channel.
736
+ The uploaded artifact stays available. This only removes the
737
+ Bundle from the channel.
735
738
  </AlertDialogDescription>
736
739
  </AlertDialogHeader>
737
740
  <AlertDialogFooter>
@@ -850,9 +853,6 @@ export function ReleaseEditorSheet({
850
853
  isCopyPromotion
851
854
  ? `Bundle copied to ${normalizedTargetChannel}`
852
855
  : `Bundle moved to ${normalizedTargetChannel}`,
853
- release.bundle_id
854
- ? { description: `bundleId: ${release.bundle_id}` }
855
- : undefined,
856
856
  );
857
857
  } catch (caught) {
858
858
  toast.error(