@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
@@ -8,7 +8,6 @@ import {
8
8
  Sun,
9
9
  } from "lucide-react";
10
10
 
11
- import { useAnalyticsCapability } from "@/components/features/analytics/AnalyticsCapabilityContext";
12
11
  import { HotUpdaterLogo } from "@/components/HotUpdaterLogo";
13
12
  import { useTheme } from "@/components/ThemeProvider";
14
13
  import {
@@ -26,15 +25,14 @@ import {
26
25
  import { useApiKeyCapabilityQuery } from "@/lib/api-keys-api";
27
26
 
28
27
  export function AppSidebar() {
29
- const analyticsCapability = useAnalyticsCapability();
30
28
  const apiKeyCapability = useApiKeyCapabilityQuery();
31
29
  const { theme, setTheme } = useTheme();
32
30
  const routerState = useRouterState();
33
31
  const currentPath = routerState.location.pathname;
34
32
 
35
33
  const isBundlesActive = currentPath === "/";
36
- const isAnalyticsActive =
37
- currentPath === "/analytics" || currentPath === "/installations";
34
+ const isInsightsActive =
35
+ currentPath === "/insights" || currentPath === "/installations";
38
36
  const isApiKeysActive = currentPath === "/api-keys";
39
37
  const isSigningActive = currentPath === "/signing";
40
38
 
@@ -98,18 +96,16 @@ export function AppSidebar() {
98
96
  <span>Bundles</span>
99
97
  </SidebarMenuButton>
100
98
  </SidebarMenuItem>
101
- {analyticsCapability.status === "supported" ? (
102
- <SidebarMenuItem>
103
- <SidebarMenuButton
104
- isActive={isAnalyticsActive}
105
- render={<Link to="/analytics" />}
106
- tooltip="Analytics"
107
- >
108
- <ChartNoAxesCombined />
109
- <span>Analytics</span>
110
- </SidebarMenuButton>
111
- </SidebarMenuItem>
112
- ) : null}
99
+ <SidebarMenuItem>
100
+ <SidebarMenuButton
101
+ isActive={isInsightsActive}
102
+ render={<Link to="/insights" />}
103
+ tooltip="Insights"
104
+ >
105
+ <ChartNoAxesCombined />
106
+ <span>Insights</span>
107
+ </SidebarMenuButton>
108
+ </SidebarMenuItem>
113
109
  {apiKeyCapability.data?.apiKeys ? (
114
110
  <SidebarMenuItem>
115
111
  <SidebarMenuButton
@@ -67,7 +67,7 @@ export function ConsoleAccessPage({
67
67
  <div>
68
68
  <h1 className="text-lg font-semibold">Hot Updater Console</h1>
69
69
  <p className="text-muted-foreground text-sm">
70
- Sign in to manage OTA releases.
70
+ Sign in to manage OTA bundles.
71
71
  </p>
72
72
  </div>
73
73
  </div>
@@ -8,15 +8,22 @@ interface HashValueDisplayProps {
8
8
  value: string;
9
9
  maxLength?: number;
10
10
  className?: string;
11
+ buttonClassName?: string;
11
12
  }
12
13
 
14
+ export const shortenIdentifier = (value: string, maxLength = 12) =>
15
+ value.length > maxLength
16
+ ? `${value.slice(0, Math.max(1, maxLength - 4))}…${value.slice(-4)}`
17
+ : value;
18
+
13
19
  export function HashValueDisplay({
14
20
  value,
15
21
  maxLength = 12,
16
22
  className,
23
+ buttonClassName,
17
24
  }: HashValueDisplayProps) {
18
25
  const isTruncated = value.length > maxLength;
19
- const truncated = isTruncated ? value.slice(0, maxLength) : value;
26
+ const truncated = shortenIdentifier(value, maxLength);
20
27
 
21
28
  const copyValue = async () => {
22
29
  try {
@@ -61,12 +68,14 @@ export function HashValueDisplay({
61
68
  render={
62
69
  <button
63
70
  type="button"
71
+ aria-label={`Copy full value: ${value}`}
64
72
  translate="no"
65
73
  className={cn(
66
74
  "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]",
67
75
  "hover:bg-muted/70 active:scale-[0.98] active:bg-muted/85 focus-visible:ring-[2px]",
68
- "touch-manipulation select-none",
76
+ "touch-manipulation select-none motion-reduce:transition-none motion-reduce:active:scale-100",
69
77
  !isTruncated && "cursor-copy",
78
+ buttonClassName,
70
79
  )}
71
80
  onClick={handleClick}
72
81
  onKeyDown={handleKeyDown}
@@ -236,7 +236,7 @@ function RevokeApiKeyDialog({ record }: { record: ApiKeyView }) {
236
236
  <AlertDialogTitle>Revoke {record.name}?</AlertDialogTitle>
237
237
  <AlertDialogDescription>
238
238
  Apps using {record.prefix}… will immediately lose OTA update and
239
- analytics access. This action cannot be undone.
239
+ insights access. This action cannot be undone.
240
240
  </AlertDialogDescription>
241
241
  </AlertDialogHeader>
242
242
  <AlertDialogFooter>
@@ -40,9 +40,10 @@ export function BundleChildrenPanel({
40
40
  id={panelId}
41
41
  >
42
42
  <div className="flex flex-col gap-4">
43
+ <h3 className="text-sm font-medium">Advanced artifact diagnostics</h3>
43
44
  <div className="flex flex-wrap items-center justify-between gap-3">
44
45
  <div className="flex min-w-0 items-start gap-2 text-sm sm:items-center">
45
- <span className="text-muted-foreground">Base bundle</span>
46
+ <span className="text-muted-foreground">Base artifact ID</span>
46
47
  <BundleIdDisplay bundleId={bundle.id} fullOnMobile maxLength={18} />
47
48
  </div>
48
49
  <Badge variant="outline">
@@ -58,7 +59,7 @@ export function BundleChildrenPanel({
58
59
  ) : bundles.length > 0 ? (
59
60
  <div className="flex flex-col gap-2">
60
61
  <div className="text-xs font-semibold uppercase text-muted-foreground/70">
61
- Patch bundles from this base
62
+ Patch artifacts from this base
62
63
  </div>
63
64
  {isMobile ? (
64
65
  <div className="flex flex-col gap-2">
@@ -70,7 +71,7 @@ export function BundleChildrenPanel({
70
71
  <div className="flex flex-col gap-3">
71
72
  <div className="space-y-1">
72
73
  <div className="text-[11px] font-medium uppercase text-muted-foreground/70">
73
- Patch Bundle
74
+ Target artifact ID
74
75
  </div>
75
76
  <BundleIdDisplay
76
77
  bundleId={childBundle.id}
@@ -130,7 +131,7 @@ export function BundleChildrenPanel({
130
131
  <Table className="min-w-max">
131
132
  <TableHeader>
132
133
  <TableRow className="hover:bg-transparent">
133
- <TableHead>Patch Bundle</TableHead>
134
+ <TableHead>Target artifact ID</TableHead>
134
135
  <TableHead>Relation</TableHead>
135
136
  <TableHead>Artifact</TableHead>
136
137
  <TableHead>Created</TableHead>
@@ -188,10 +189,10 @@ export function BundleChildrenPanel({
188
189
  ) : (
189
190
  <div className="flex flex-col gap-2">
190
191
  <div className="text-xs font-semibold uppercase text-muted-foreground/70">
191
- Patch bundles from this base
192
+ Patch artifacts from this base
192
193
  </div>
193
194
  <div className="rounded-md border bg-background p-3 text-sm text-muted-foreground">
194
- No direct patch bundles.
195
+ No direct patch artifacts.
195
196
  </div>
196
197
  </div>
197
198
  )}
@@ -1,12 +1,7 @@
1
- import {
2
- getPatchBaseBundleId,
3
- getPatchBaseFileHash,
4
- getPatchFileHash,
5
- } from "@hot-updater/core";
1
+ import { getPatchBaseFileHash, getPatchFileHash } from "@hot-updater/core";
6
2
  import type { Bundle, ReleaseRow } from "@hot-updater/plugin-core";
7
3
  import { ExternalLink } from "lucide-react";
8
4
 
9
- import { BundleIdDisplay } from "@/components/BundleIdDisplay";
10
5
  import { HashValueDisplay } from "@/components/HashValueDisplay";
11
6
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
12
7
  import { useConfigQuery } from "@/lib/api";
@@ -28,7 +23,6 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) {
28
23
 
29
24
  export function BundleMetadata({ bundle, release }: BundleMetadataProps) {
30
25
  const { data: configData, isFetched } = useConfigQuery();
31
- const patchBaseBundleId = bundle ? getPatchBaseBundleId(bundle) : null;
32
26
  const hbcPatchFileHash = bundle ? getPatchFileHash(bundle) : null;
33
27
  const hbcPatchBaseFileHash = bundle ? getPatchBaseFileHash(bundle) : null;
34
28
  const gitCommitUrl =
@@ -43,7 +37,6 @@ export function BundleMetadata({ bundle, release }: BundleMetadataProps) {
43
37
  target ||
44
38
  bundle?.gitCommitHash ||
45
39
  bundle?.fileHash ||
46
- patchBaseBundleId ||
47
40
  hbcPatchBaseFileHash ||
48
41
  hbcPatchFileHash;
49
42
 
@@ -101,18 +94,6 @@ export function BundleMetadata({ bundle, release }: BundleMetadataProps) {
101
94
  }
102
95
  />
103
96
  ) : null}
104
- {patchBaseBundleId ? (
105
- <Row
106
- label="Patch base"
107
- value={
108
- <BundleIdDisplay
109
- bundleId={patchBaseBundleId}
110
- fullOnMobile
111
- maxLength={18}
112
- />
113
- }
114
- />
115
- ) : null}
116
97
  {hbcPatchBaseFileHash ? (
117
98
  <Row
118
99
  label="Base hash"
@@ -107,7 +107,7 @@ export function ChannelManagementDialog({
107
107
  Channels
108
108
  </DialogTitle>
109
109
  <DialogDescription>
110
- Create deployment channels and remove them when no Release uses
110
+ Create deployment channels and remove them when no bundle uses
111
111
  them. Empty channels remain available until you delete them.
112
112
  </DialogDescription>
113
113
  </DialogHeader>
@@ -0,0 +1,151 @@
1
+ import {
2
+ Activity,
3
+ Check,
4
+ ChevronDown,
5
+ PackageCheck,
6
+ RotateCcw,
7
+ } from "lucide-react";
8
+ import { useEffect, useState } from "react";
9
+
10
+ import { HashValueDisplay } from "@/components/HashValueDisplay";
11
+ import { Badge } from "@/components/ui/badge";
12
+ import type { InsightsEventRow } from "@/lib/insights-view";
13
+
14
+ type EventHistoryRow = InsightsEventRow;
15
+
16
+ const eventTypes = {
17
+ UPDATE_APPLIED: { label: "Bundle applied", variant: "success", icon: Check },
18
+ RECOVERED: { label: "Recovered", variant: "warning", icon: RotateCcw },
19
+ RELEASE_ADOPTED: {
20
+ label: "Bundle adopted",
21
+ variant: "success",
22
+ icon: PackageCheck,
23
+ },
24
+ UNCHANGED: {
25
+ label: "Activity reported",
26
+ variant: "secondary",
27
+ icon: Activity,
28
+ },
29
+ } as const;
30
+
31
+ export function useInsightsTimeFormat() {
32
+ const [timeZone, setTimeZone] = useState("UTC");
33
+ useEffect(() => {
34
+ setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone);
35
+ }, []);
36
+ return new Intl.DateTimeFormat("en", {
37
+ timeZone,
38
+ timeZoneName: "shortOffset",
39
+ year: "numeric",
40
+ month: "2-digit",
41
+ day: "2-digit",
42
+ hour: "2-digit",
43
+ minute: "2-digit",
44
+ second: "2-digit",
45
+ hourCycle: "h23",
46
+ });
47
+ }
48
+
49
+ export function formatInsightsTimestamp(
50
+ value: number,
51
+ formatter: Intl.DateTimeFormat,
52
+ ) {
53
+ const parts = Object.fromEntries(
54
+ formatter
55
+ .formatToParts(new Date(value))
56
+ .map(({ type, value }) => [type, value]),
57
+ );
58
+ const offset = parts.timeZoneName === "GMT" ? "GMT+0" : parts.timeZoneName;
59
+ return `${parts.year}/${parts.month}/${parts.day} ${parts.hour}:${parts.minute}:${parts.second} ${offset}`;
60
+ }
61
+
62
+ export function EventTimestamp({
63
+ value,
64
+ formatter,
65
+ touch = false,
66
+ }: {
67
+ readonly value: number;
68
+ readonly formatter: Intl.DateTimeFormat;
69
+ readonly touch?: boolean;
70
+ }) {
71
+ const date = new Date(value);
72
+ const localTime = formatInsightsTimestamp(value, formatter);
73
+ return (
74
+ <details className="group/time">
75
+ <summary
76
+ className={`cursor-pointer list-none rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring/30 [&::-webkit-details-marker]:hidden ${touch ? "flex min-h-11 items-center" : ""}`}
77
+ >
78
+ <span className="flex items-center gap-1">
79
+ <time dateTime={date.toISOString()} className="whitespace-nowrap">
80
+ {localTime}
81
+ </time>
82
+ <ChevronDown
83
+ aria-hidden="true"
84
+ className="size-3 text-muted-foreground group-open/time:rotate-180"
85
+ />
86
+ </span>
87
+ </summary>
88
+ <p className="mt-2 text-muted-foreground">
89
+ {date.toISOString().replace("T", " ").replace("Z", " UTC")}
90
+ </p>
91
+ </details>
92
+ );
93
+ }
94
+
95
+ export function EventTypeBadge({
96
+ type,
97
+ }: {
98
+ readonly type: EventHistoryRow["type"];
99
+ }) {
100
+ const eventType = eventTypes[type];
101
+ const Icon = eventType.icon;
102
+ return (
103
+ <Badge
104
+ variant={eventType.variant}
105
+ className="gap-1 whitespace-nowrap [&_svg]:size-3"
106
+ title={
107
+ type === "UNCHANGED"
108
+ ? "App activity reported on the current bundle without a bundle transition."
109
+ : type === "RELEASE_ADOPTED"
110
+ ? "A different release was adopted without changing the bundle."
111
+ : undefined
112
+ }
113
+ >
114
+ <Icon aria-hidden="true" />
115
+ {eventType.label}
116
+ </Badge>
117
+ );
118
+ }
119
+
120
+ export function EventBundleTransition({
121
+ event,
122
+ touch = false,
123
+ }: {
124
+ readonly event: Pick<EventHistoryRow, "type" | "fromBundleId" | "toBundleId">;
125
+ readonly touch?: boolean;
126
+ }) {
127
+ return (
128
+ <dl className="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-2 gap-y-2">
129
+ {event.fromBundleId ? (
130
+ <>
131
+ <dt className="text-muted-foreground">From</dt>
132
+ <dd>
133
+ <HashValueDisplay
134
+ value={event.fromBundleId}
135
+ buttonClassName={touch ? "min-h-11 px-3" : undefined}
136
+ />
137
+ </dd>
138
+ </>
139
+ ) : null}
140
+ <dt className="text-muted-foreground">
141
+ {event.type === "UNCHANGED" ? "Current" : "To"}
142
+ </dt>
143
+ <dd>
144
+ <HashValueDisplay
145
+ value={event.toBundleId}
146
+ buttonClassName={touch ? "min-h-11 px-3" : undefined}
147
+ />
148
+ </dd>
149
+ </dl>
150
+ );
151
+ }
@@ -0,0 +1,245 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { ArrowUpRight, RefreshCw } from "lucide-react";
3
+ import type { ReactNode } from "react";
4
+
5
+ import { HashValueDisplay } from "@/components/HashValueDisplay";
6
+ import { Button, buttonVariants } from "@/components/ui/button";
7
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { Skeleton } from "@/components/ui/skeleton";
9
+ import {
10
+ Table,
11
+ TableBody,
12
+ TableCell,
13
+ TableHead,
14
+ TableHeader,
15
+ TableRow,
16
+ } from "@/components/ui/table";
17
+ import type { InsightsEventRow, InsightsViewPage } from "@/lib/insights-view";
18
+
19
+ import {
20
+ EventBundleTransition,
21
+ EventTimestamp,
22
+ EventTypeBadge,
23
+ useInsightsTimeFormat,
24
+ } from "./EventDetails";
25
+ import { EventHistoryList } from "./EventHistoryList";
26
+ import { InsightsErrorAlert } from "./InsightsErrorAlert";
27
+ import { InsightsPagination } from "./InsightsPagination";
28
+
29
+ type EventsLocationState = {
30
+ readonly eventsBefore: number;
31
+ readonly eventsCursor?: string;
32
+ };
33
+
34
+ function EventIdentity({
35
+ event,
36
+ eventsLocation,
37
+ touch = false,
38
+ }: {
39
+ readonly event: InsightsEventRow;
40
+ readonly eventsLocation: EventsLocationState;
41
+ readonly touch?: boolean;
42
+ }) {
43
+ return (
44
+ <div
45
+ className={
46
+ touch
47
+ ? "flex min-w-0 flex-wrap items-center justify-between gap-2"
48
+ : "flex min-w-0 flex-col items-start gap-2"
49
+ }
50
+ >
51
+ <Link
52
+ className={buttonVariants({
53
+ className: touch
54
+ ? "-ml-2 h-auto min-h-11 min-w-0 flex-1 justify-start gap-2 whitespace-normal text-sm"
55
+ : "-ml-2 max-w-full justify-start",
56
+ size: "sm",
57
+ variant: "ghost",
58
+ })}
59
+ to="/installations"
60
+ search={{
61
+ ...eventsLocation,
62
+ query: event.installId || undefined,
63
+ installId: event.installId,
64
+ }}
65
+ aria-label={`View history for ${event.userId ?? event.username ?? "anonymous installation"} (${event.installId})`}
66
+ >
67
+ <span
68
+ className={touch ? "min-w-0 text-left wrap-anywhere" : "truncate"}
69
+ >
70
+ {event.userId ?? event.username ?? "Anonymous installation"}
71
+ </span>
72
+ <ArrowUpRight aria-hidden="true" data-icon="inline-end" />
73
+ </Link>
74
+ <HashValueDisplay
75
+ value={event.installId}
76
+ maxLength={13}
77
+ buttonClassName={touch ? "min-h-11 px-3" : undefined}
78
+ />
79
+ </div>
80
+ );
81
+ }
82
+
83
+ export function EventHistoryCard({
84
+ children,
85
+ error,
86
+ eventsLocation,
87
+ history,
88
+ isFetching,
89
+ isLoading,
90
+ onNext,
91
+ onPrevious,
92
+ onRefresh,
93
+ pageNumber,
94
+ title = "All events",
95
+ }: {
96
+ readonly children?: ReactNode;
97
+ readonly error: Error | null;
98
+ readonly eventsLocation: EventsLocationState;
99
+ readonly history: InsightsViewPage<InsightsEventRow> | undefined;
100
+ readonly isFetching: boolean;
101
+ readonly isLoading: boolean;
102
+ readonly onNext: () => void;
103
+ readonly onPrevious: () => void;
104
+ readonly onRefresh: () => void;
105
+ readonly pageNumber: number;
106
+ readonly title?: string;
107
+ }) {
108
+ const dateTimeFormat = useInsightsTimeFormat();
109
+ const hasPrevious = pageNumber > 1;
110
+
111
+ return (
112
+ <Card className="@container min-w-0 shadow-sm" aria-busy={isFetching}>
113
+ <CardHeader className="gap-4 p-4 sm:p-6">
114
+ <div className="flex flex-wrap items-center justify-between gap-3">
115
+ <CardTitle>
116
+ <h2>{title}</h2>
117
+ </CardTitle>
118
+ <Button
119
+ className="h-11 lg:h-8"
120
+ disabled={isFetching}
121
+ onClick={onRefresh}
122
+ size="lg"
123
+ variant="outline"
124
+ >
125
+ <RefreshCw aria-hidden="true" data-icon="inline-start" />
126
+ Refresh
127
+ </Button>
128
+ </div>
129
+ {children}
130
+ </CardHeader>
131
+ <CardContent className="min-w-0 p-0">
132
+ {isLoading ? (
133
+ <div
134
+ className="flex flex-col gap-4 p-6"
135
+ role="status"
136
+ aria-label="Loading events"
137
+ >
138
+ <Skeleton className="h-12 w-full" />
139
+ <Skeleton className="h-12 w-full" />
140
+ <Skeleton className="h-12 w-full" />
141
+ </div>
142
+ ) : error ? (
143
+ <div className="px-6 pb-6">
144
+ <InsightsErrorAlert
145
+ error={error}
146
+ fallbackTitle="Event history unavailable"
147
+ />
148
+ </div>
149
+ ) : history ? (
150
+ <>
151
+ {history.data.length > 0 ? (
152
+ <>
153
+ <div className="@[58rem]:hidden">
154
+ <EventHistoryList
155
+ events={history.data}
156
+ formatter={dateTimeFormat}
157
+ renderIdentity={(event) => (
158
+ <EventIdentity
159
+ event={event}
160
+ eventsLocation={eventsLocation}
161
+ touch
162
+ />
163
+ )}
164
+ />
165
+ </div>
166
+ <div className="hidden @[58rem]:block">
167
+ <Table className="min-w-4xl table-fixed">
168
+ <TableHeader>
169
+ <TableRow className="[&>th]:px-4 sm:[&>th]:px-6">
170
+ <TableHead className="w-64">Time</TableHead>
171
+ <TableHead className="w-48">Event</TableHead>
172
+ <TableHead className="w-48">
173
+ User / installation
174
+ </TableHead>
175
+ <TableHead className="w-32">App</TableHead>
176
+ <TableHead className="w-52">Bundle</TableHead>
177
+ </TableRow>
178
+ </TableHeader>
179
+ <TableBody>
180
+ {history.data.map((event) => (
181
+ <TableRow
182
+ key={event.id}
183
+ className="[&>td]:px-4 [&>td]:py-4 [&>td]:align-top sm:[&>td]:px-6"
184
+ >
185
+ <TableCell className="whitespace-normal text-xs tabular-nums">
186
+ <EventTimestamp
187
+ value={event.receivedAtMs}
188
+ formatter={dateTimeFormat}
189
+ />
190
+ </TableCell>
191
+ <TableCell>
192
+ <EventTypeBadge type={event.type} />
193
+ </TableCell>
194
+ <TableCell>
195
+ <EventIdentity
196
+ event={event}
197
+ eventsLocation={eventsLocation}
198
+ />
199
+ </TableCell>
200
+ <TableCell className="whitespace-normal text-xs">
201
+ <div className="flex flex-col gap-2">
202
+ <span>
203
+ {event.platform === "ios" ? "iOS" : "Android"}{" "}
204
+ {event.appVersion}
205
+ </span>
206
+ <span className="text-muted-foreground">
207
+ {event.channel}
208
+ </span>
209
+ </div>
210
+ </TableCell>
211
+ <TableCell className="whitespace-normal text-xs">
212
+ <EventBundleTransition event={event} />
213
+ </TableCell>
214
+ </TableRow>
215
+ ))}
216
+ </TableBody>
217
+ </Table>
218
+ </div>
219
+ </>
220
+ ) : (
221
+ <div className="px-6 pb-6 text-sm text-muted-foreground">
222
+ {hasPrevious || history.nextCursor
223
+ ? "No events on this page"
224
+ : title === "All events"
225
+ ? "No events recorded yet"
226
+ : "No matching reports in this period"}
227
+ </div>
228
+ )}
229
+ {history.data.length > 0 || hasPrevious || history.nextCursor ? (
230
+ <InsightsPagination
231
+ hasPrevious={hasPrevious}
232
+ label={title}
233
+ nextCursor={history.nextCursor}
234
+ onNext={onNext}
235
+ onPrevious={onPrevious}
236
+ pageLength={history.data.length}
237
+ pageNumber={pageNumber}
238
+ />
239
+ ) : null}
240
+ </>
241
+ ) : null}
242
+ </CardContent>
243
+ </Card>
244
+ );
245
+ }
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import type { InsightsEventRow } from "@/lib/insights-view";
4
+
5
+ import {
6
+ EventBundleTransition,
7
+ EventTimestamp,
8
+ EventTypeBadge,
9
+ } from "./EventDetails";
10
+
11
+ export function EventHistoryList<T extends InsightsEventRow>({
12
+ events,
13
+ formatter,
14
+ renderIdentity,
15
+ }: {
16
+ readonly events: readonly T[];
17
+ readonly formatter: Intl.DateTimeFormat;
18
+ readonly renderIdentity?: (event: T) => ReactNode;
19
+ }) {
20
+ return (
21
+ <>
22
+ <p className="px-4 pb-2 text-xs text-muted-foreground sm:px-6">
23
+ Time in your browser zone
24
+ </p>
25
+ <ol aria-label="Events" className="divide-y border-y">
26
+ {events.map((event) => (
27
+ <li
28
+ key={event.id}
29
+ className="flex min-w-0 flex-col gap-3 px-4 py-4 sm:px-6"
30
+ >
31
+ <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
32
+ <EventTypeBadge type={event.type} />
33
+ <div className="text-xs tabular-nums">
34
+ <EventTimestamp
35
+ value={event.receivedAtMs}
36
+ formatter={formatter}
37
+ touch
38
+ />
39
+ </div>
40
+ </div>
41
+ {renderIdentity?.(event)}
42
+ <dl className="flex flex-wrap gap-x-2 gap-y-1 text-sm">
43
+ <div>
44
+ <dt className="sr-only">App</dt>
45
+ <dd>
46
+ {event.platform === "ios" ? "iOS" : "Android"}{" "}
47
+ {event.appVersion}
48
+ </dd>
49
+ </div>
50
+ <div className="min-w-0 text-muted-foreground">
51
+ <dt className="sr-only">Channel</dt>
52
+ <dd className="wrap-anywhere">{event.channel}</dd>
53
+ </div>
54
+ </dl>
55
+ <div className="text-xs" aria-label="Bundle">
56
+ <EventBundleTransition event={event} touch />
57
+ </div>
58
+ </li>
59
+ ))}
60
+ </ol>
61
+ </>
62
+ );
63
+ }