@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
@@ -0,0 +1,52 @@
1
+ import { Button } from "@/components/ui/button";
2
+
3
+ export function InsightsPagination({
4
+ hasPrevious,
5
+ label,
6
+ nextCursor,
7
+ onNext,
8
+ onPrevious,
9
+ pageLength,
10
+ pageNumber,
11
+ }: {
12
+ readonly hasPrevious: boolean;
13
+ readonly label: string;
14
+ readonly nextCursor: string | null;
15
+ readonly onNext: () => void;
16
+ readonly onPrevious: () => void;
17
+ readonly pageLength: number;
18
+ readonly pageNumber: number;
19
+ }) {
20
+ return (
21
+ <nav
22
+ aria-label={`${label} pagination`}
23
+ className="flex flex-wrap items-center justify-between gap-3 border-t px-4 py-3 sm:px-6"
24
+ >
25
+ <span className="text-xs text-muted-foreground tabular-nums">
26
+ Page {pageNumber} · {pageLength.toLocaleString()} rows
27
+ </span>
28
+ <div className="flex gap-2">
29
+ <Button
30
+ className="h-11 min-w-11 px-3 lg:h-8 lg:min-w-0 lg:px-2"
31
+ disabled={!hasPrevious}
32
+ onClick={onPrevious}
33
+ size="sm"
34
+ type="button"
35
+ variant="outline"
36
+ >
37
+ Previous
38
+ </Button>
39
+ <Button
40
+ className="h-11 min-w-11 px-3 lg:h-8 lg:min-w-0 lg:px-2"
41
+ disabled={nextCursor === null}
42
+ onClick={onNext}
43
+ size="sm"
44
+ type="button"
45
+ variant="outline"
46
+ >
47
+ Next
48
+ </Button>
49
+ </div>
50
+ </nav>
51
+ );
52
+ }
@@ -1,15 +1,8 @@
1
- import { TriangleAlert } from "lucide-react";
1
+ import { RefreshCw } from "lucide-react";
2
2
 
3
- import { BundleIdDisplay } from "@/components/BundleIdDisplay";
4
- import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
5
- import { Badge } from "@/components/ui/badge";
6
- import {
7
- Card,
8
- CardContent,
9
- CardDescription,
10
- CardHeader,
11
- CardTitle,
12
- } from "@/components/ui/card";
3
+ import { HashValueDisplay } from "@/components/HashValueDisplay";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
13
6
  import { Separator } from "@/components/ui/separator";
14
7
  import { Skeleton } from "@/components/ui/skeleton";
15
8
  import {
@@ -21,28 +14,20 @@ import {
21
14
  TableRow,
22
15
  } from "@/components/ui/table";
23
16
  import type {
24
- InstallationHistoryResult,
25
- InstallationHistoryRow,
26
- InstallationSearchRow,
27
- } from "@/lib/api";
28
-
29
- import { InstallationPagination } from "./InstallationPagination";
17
+ InsightsEventRow,
18
+ InsightsInstallationViewRow,
19
+ InsightsViewPage,
20
+ } from "@/lib/insights-view";
30
21
 
31
- const formatDateTime = (value: string | number | Date | null | undefined) => {
32
- if (!value) return "—";
33
- const date = value instanceof Date ? value : new Date(value);
34
- if (Number.isNaN(date.getTime())) return "—";
35
- return new Intl.DateTimeFormat(undefined, {
36
- year: "numeric",
37
- month: "2-digit",
38
- day: "2-digit",
39
- hour: "2-digit",
40
- minute: "2-digit",
41
- second: "2-digit",
42
- hour12: false,
43
- timeZone: "UTC",
44
- }).format(date);
45
- };
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";
46
31
 
47
32
  const getUserLabel = (event: {
48
33
  readonly username: string | null;
@@ -50,7 +35,7 @@ const getUserLabel = (event: {
50
35
  }) => event.userId ?? event.username ?? "—";
51
36
 
52
37
  const getLastKnownBundleId = (
53
- event: InstallationHistoryRow | InstallationSearchRow,
38
+ event: InsightsEventRow | InsightsInstallationViewRow,
54
39
  ) =>
55
40
  "lastKnownBundleId" in event
56
41
  ? event.lastKnownBundleId
@@ -60,188 +45,205 @@ export function InstallationHistoryCard({
60
45
  error,
61
46
  history,
62
47
  isLoading,
63
- limit,
64
- offset,
65
- onOffsetChange,
48
+ onNext,
49
+ onPrevious,
50
+ onRefresh,
51
+ pageNumber,
66
52
  selectedEvent,
67
53
  selectedInstallId,
68
54
  }: {
69
55
  readonly error: unknown;
70
- readonly history: InstallationHistoryResult | undefined;
56
+ readonly history: InsightsViewPage<InsightsEventRow> | undefined;
71
57
  readonly isLoading: boolean;
72
- readonly limit: number;
73
- readonly offset: number;
74
- readonly onOffsetChange: (offset: number) => void;
58
+ readonly onRefresh?: () => void;
59
+ readonly onNext: () => void;
60
+ readonly onPrevious: () => void;
61
+ readonly pageNumber: number;
75
62
  readonly selectedEvent:
76
- | InstallationHistoryRow
77
- | InstallationSearchRow
63
+ | InsightsEventRow
64
+ | InsightsInstallationViewRow
78
65
  | undefined;
79
- readonly selectedInstallId: string;
66
+ readonly selectedInstallId: string | undefined;
80
67
  }) {
68
+ const dateTimeFormat = useInsightsTimeFormat();
81
69
  const lastKnownBundleId = selectedEvent
82
70
  ? getLastKnownBundleId(selectedEvent)
83
71
  : undefined;
84
72
 
85
73
  return (
86
- <Card className="min-h-0 min-w-0">
87
- <CardHeader className="p-6 pb-5">
88
- <div className="flex items-start justify-between gap-4">
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">
89
77
  <div className="flex min-w-0 flex-col gap-1.5">
90
78
  <CardTitle className="text-sm font-medium">
91
79
  <h2>
92
- {selectedInstallId
80
+ {selectedInstallId !== undefined
93
81
  ? "Installation history"
94
82
  : "Select an installation"}
95
83
  </h2>
96
84
  </CardTitle>
97
- <CardDescription>
98
- {selectedInstallId
99
- ? "Recorded bundle changes for this installation."
100
- : "Choose a match to view its recorded bundle changes."}
101
- </CardDescription>
102
85
  </div>
103
- {history && history.pagination.total > 0 ? (
104
- <Badge variant="outline">{history.pagination.total} events</Badge>
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>
105
97
  ) : null}
106
98
  </div>
107
99
  </CardHeader>
108
100
  <CardContent className="min-h-0 p-0">
109
- {selectedInstallId ? (
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 ? (
110
147
  isLoading ? (
111
- <div className="flex flex-col gap-3 p-5">
148
+ <div
149
+ className="flex flex-col gap-3 p-6"
150
+ role="status"
151
+ aria-label="Loading installation history"
152
+ >
112
153
  <Skeleton className="h-10 w-full" />
113
154
  <Skeleton className="h-10 w-full" />
114
155
  <Skeleton className="h-10 w-full" />
115
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>
116
164
  ) : history && history.data.length > 0 ? (
117
165
  <>
118
- <section
119
- aria-labelledby="latest-installation-state"
120
- className="px-6 pb-6"
121
- >
122
- <h3
123
- className="text-xs font-medium"
124
- id="latest-installation-state"
125
- >
126
- Latest reported state
127
- </h3>
128
- <dl className="mt-4 grid gap-6 xl:grid-cols-3">
129
- <div className="min-w-0">
130
- <dt className="text-xs text-muted-foreground">
131
- Installation
132
- </dt>
133
- <dd className="mt-1 font-mono text-xs">
134
- <BundleIdDisplay bundleId={selectedInstallId} />
135
- </dd>
136
- </div>
137
- <div className="min-w-0">
138
- <dt className="text-xs text-muted-foreground">User ID</dt>
139
- <dd className="mt-1 truncate text-xs font-medium">
140
- {selectedEvent ? getUserLabel(selectedEvent) : "—"}
141
- </dd>
142
- </div>
143
- <div className="min-w-0">
144
- <dt className="text-xs text-muted-foreground">
145
- Last known bundle
146
- </dt>
147
- <dd className="mt-1 font-mono text-xs">
148
- {lastKnownBundleId ? (
149
- <BundleIdDisplay bundleId={lastKnownBundleId} />
150
- ) : (
151
- "—"
152
- )}
153
- </dd>
154
- </div>
155
- </dl>
156
- </section>
157
- <Separator />
158
- <Table className="min-w-3xl table-fixed">
159
- <TableHeader>
160
- <TableRow className="hover:bg-transparent [&>th]:h-12 [&>th]:px-5">
161
- <TableHead className="w-1/5 whitespace-normal">
162
- Reported (UTC)
163
- </TableHead>
164
- <TableHead className="w-1/6 whitespace-normal">
165
- App version
166
- </TableHead>
167
- <TableHead className="w-1/5 whitespace-normal">
168
- Change
169
- </TableHead>
170
- <TableHead className="whitespace-normal">
171
- Bundle transition
172
- </TableHead>
173
- </TableRow>
174
- </TableHeader>
175
- <TableBody>
176
- {history.data.map((event) => (
177
- <TableRow
178
- className="[&>td]:px-5 [&>td]:py-4"
179
- key={event.id}
180
- >
181
- <TableCell className="whitespace-normal align-top text-sm text-muted-foreground">
182
- {formatDateTime(event.receivedAtMs)}
183
- </TableCell>
184
- <TableCell className="whitespace-normal align-top font-mono text-xs">
185
- {event.appVersion}
186
- </TableCell>
187
- <TableCell className="whitespace-normal align-top">
188
- <Badge variant="secondary">
189
- {event.type === "UPDATE_APPLIED"
190
- ? "Bundle applied"
191
- : "Recovered"}
192
- </Badge>
193
- </TableCell>
194
- <TableCell className="whitespace-normal align-top">
195
- <div className="grid grid-cols-[2rem_minmax(0,1fr)] gap-x-3 gap-y-3">
196
- <span className="text-muted-foreground">From</span>
197
- {event.fromBundleId ? (
198
- <BundleIdDisplay bundleId={event.fromBundleId} />
199
- ) : (
200
- "—"
201
- )}
202
- <span className="text-muted-foreground">To</span>
203
- {event.toBundleId ? (
204
- <BundleIdDisplay bundleId={event.toBundleId} />
205
- ) : (
206
- "—"
207
- )}
208
- </div>
209
- </TableCell>
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>
210
180
  </TableRow>
211
- ))}
212
- </TableBody>
213
- </Table>
214
- <InstallationPagination
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}
215
218
  label="Installation history"
216
- limit={limit}
217
- offset={offset}
219
+ nextCursor={history.nextCursor}
220
+ onNext={onNext}
221
+ onPrevious={onPrevious}
218
222
  pageLength={history.data.length}
219
- total={history.pagination.total}
220
- onOffsetChange={onOffsetChange}
223
+ pageNumber={pageNumber}
221
224
  />
222
225
  </>
223
- ) : error ? (
224
- <div className="p-5 pt-0">
225
- <Alert variant="destructive">
226
- <TriangleAlert aria-hidden="true" />
227
- <AlertTitle>Installation history unavailable</AlertTitle>
228
- <AlertDescription>
229
- {error instanceof Error
230
- ? error.message
231
- : "Failed to load installation history."}
232
- </AlertDescription>
233
- </Alert>
234
- </div>
235
226
  ) : (
236
- <div className="p-5 pt-0 text-sm text-muted-foreground">
237
- No history is available for this installation.
238
- </div>
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
+ </>
239
245
  )
240
- ) : (
241
- <div className="p-5 pt-0 text-sm text-muted-foreground">
242
- Choose an installation to inspect its update history.
243
- </div>
244
- )}
246
+ ) : null}
245
247
  </CardContent>
246
248
  </Card>
247
249
  );