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

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 (262) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/BundleIdDisplay-D4ErdU5_.js +1 -0
  3. package/.output/public/assets/InsightsCapabilityContext-B42VqvP6.js +1 -0
  4. package/.output/public/assets/api-keys-api-DGVTTvTJ.js +1 -0
  5. package/.output/public/assets/api-keys-z16OZIP4.js +1 -0
  6. package/.output/public/assets/badge-Br_KDO0J.js +1 -0
  7. package/.output/public/assets/card-DzuEXvyJ.js +1 -0
  8. package/.output/public/assets/chart-no-axes-combined-FaYMGmQS.js +1 -0
  9. package/.output/public/assets/createServerFn-gM2-LFBy.js +3 -0
  10. package/.output/public/assets/dialog-BeAENgez.js +1 -0
  11. package/.output/public/assets/dist-Bssh_cv0.js +1 -0
  12. package/.output/public/assets/field-Buq148-A.js +1 -0
  13. package/.output/public/assets/index-uOpQAIC4.js +10 -0
  14. package/.output/public/assets/input-group-B3p5UpyW.js +1 -0
  15. package/.output/public/assets/insights-DJTr7Ozn.js +1 -0
  16. package/.output/public/assets/insights-api-DVMOoRMS.js +1 -0
  17. package/.output/public/assets/installations-BY64iIAK.js +2 -0
  18. package/.output/public/assets/installations-D5v9i3AL.js +1 -0
  19. package/.output/public/assets/lazyRouteComponent-DKYu5cZJ.js +1 -0
  20. package/.output/public/assets/preload-helper-CQ36nxok.js +1 -0
  21. package/.output/public/assets/refresh-cw-BkbfZ62O.js +1 -0
  22. package/.output/public/assets/routes-BuoI5-Cr.js +2 -0
  23. package/.output/public/assets/routes-C8fvgETO.js +1 -0
  24. package/.output/public/assets/search-qsIDvS2I.js +1 -0
  25. package/.output/public/assets/shield-check-DE_hVuHu.js +1 -0
  26. package/.output/public/assets/signing-mJD88Y4C.js +1 -0
  27. package/.output/public/assets/styles-BZIG7t42.css +2 -0
  28. package/.output/public/assets/table-RamFEclP.js +1 -0
  29. package/.output/public/assets/useCompositeListItem-ChzDqdpc.js +42 -0
  30. package/.output/public/assets/useMutation-JsGVjQMY.js +1 -0
  31. package/.output/public/assets/useOpenInteractionType-Dwfy-SpA.js +1 -0
  32. package/.output/server/__23tanstack-start-server-fn-resolver-Dftxlm2E.mjs +146 -0
  33. package/.output/server/_libs/@base-ui/react+[...].mjs +16124 -0
  34. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1554 -0
  35. package/.output/server/_libs/@tanstack/react-router+[...].mjs +41 -10
  36. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +132 -0
  37. package/.output/server/_libs/d3-array.mjs +231 -0
  38. package/.output/server/_libs/d3-color.mjs +330 -0
  39. package/.output/server/_libs/d3-format.mjs +252 -0
  40. package/.output/server/_libs/d3-interpolate.mjs +207 -0
  41. package/.output/server/_libs/d3-path.mjs +77 -0
  42. package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
  43. package/.output/server/_libs/d3-shape.mjs +818 -0
  44. package/.output/server/_libs/decimal.js-light.mjs +884 -0
  45. package/.output/server/_libs/es-toolkit.mjs +1134 -0
  46. package/.output/server/_libs/eventemitter3.mjs +236 -0
  47. package/.output/server/_libs/immer.mjs +532 -0
  48. package/.output/server/_libs/lucide-react.mjs +141 -13
  49. package/.output/server/_libs/next-themes.mjs +2 -2
  50. package/.output/server/_libs/react-redux+[...].mjs +205 -0
  51. package/.output/server/_libs/recharts+[...].mjs +14117 -0
  52. package/.output/server/_libs/sonner.mjs +2 -2
  53. package/.output/server/_libs/tanstack__query-core.mjs +114 -1
  54. package/.output/server/_libs/tanstack__react-query.mjs +3 -4
  55. package/.output/server/_runtime.mjs +10 -1
  56. package/.output/server/_ssr/BundleActivityChart-CWcdPuUn.mjs +288 -0
  57. package/.output/server/_ssr/BundleIdDisplay-BEvB3MvA.mjs +13 -0
  58. package/.output/server/_ssr/InsightsCapabilityContext-Cx8fjm2s.mjs +15 -0
  59. package/.output/server/_ssr/api-keys-CpBqHpuC.mjs +300 -0
  60. package/.output/server/_ssr/api-keys-api-Czkp57UV.mjs +60 -0
  61. package/.output/server/_ssr/api-keys-rpc-BTtMDSoR.mjs +136 -0
  62. package/.output/server/_ssr/api-rpc-BEX8ISai.mjs +1468 -0
  63. package/.output/server/_ssr/auth-BF8mbAgF.mjs +21 -0
  64. package/.output/server/_ssr/auth-rpc-BFMEC-TT.mjs +24 -0
  65. package/.output/server/_ssr/auth.server-DW71WzsD.mjs +47 -0
  66. package/.output/server/_ssr/badge-CpWXQwyF.mjs +26 -0
  67. package/.output/server/_ssr/bundle-signing-rpc-BbCFUQSu.mjs +16 -0
  68. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
  69. package/.output/server/_ssr/card-Bsc8mRis.mjs +86 -0
  70. package/.output/server/_ssr/config-Dv8qLDZ0.mjs +13 -0
  71. package/.output/server/_ssr/config.server-DQYEr_5X.mjs +361 -0
  72. package/.output/server/_ssr/{createServerFn-DzU0k62V.mjs → createServerFn-B2hcXoPM.mjs} +1 -1
  73. package/.output/server/_ssr/createServerRpc-DB29C_5p.mjs +12 -0
  74. package/.output/server/_ssr/{sidebar-Cv_H0_Jv.mjs → createSsrRpc-DhfrxEjg.mjs} +68 -50
  75. package/.output/server/_ssr/databaseRows-QlCYLi0H.mjs +166 -0
  76. package/.output/server/_ssr/deleteBundle-Cu_XOMt0.mjs +66 -0
  77. package/.output/server/_ssr/dialog-C0vKUp8R.mjs +170 -0
  78. package/.output/server/_ssr/dist-BVSLTpoP.mjs +250 -0
  79. package/.output/server/_ssr/downloadBundle-CNnYxYO2.mjs +24 -0
  80. package/.output/server/_ssr/{empty-plugin-adapters-B4HCzgmM.mjs → empty-plugin-adapters-B5QvGpEl.mjs} +1 -1
  81. package/.output/server/_ssr/field-DASCh-5d.mjs +74 -0
  82. package/.output/server/_ssr/{getBundleChildren-CVtSiH5t.mjs → getBundleChildren-BcRMFoDF.mjs} +10 -18
  83. package/.output/server/_ssr/input-group-etIsBUqv.mjs +288 -0
  84. package/.output/server/_ssr/insights-api-C1F7UreR.mjs +71 -0
  85. package/.output/server/_ssr/insights-input-9fEJF-ZG.mjs +96 -0
  86. package/.output/server/_ssr/insights-rpc-C-8R6kED.mjs +155 -0
  87. package/.output/server/_ssr/insights-ygT_aIfo.mjs +858 -0
  88. package/.output/server/_ssr/installations-DqRT1QTd.mjs +535 -0
  89. package/.output/server/_ssr/installations-K8TCYTYh.mjs +19 -0
  90. package/.output/server/_ssr/router-BxBEvtF1.mjs +513 -0
  91. package/.output/server/_ssr/routes-BC7rn-tf.mjs +37 -0
  92. package/.output/server/_ssr/routes-CM5KAwpB.mjs +2285 -0
  93. package/.output/server/_ssr/runtime.server-la7TRAH_.mjs +527 -0
  94. package/.output/server/_ssr/server-CpzW--lT.mjs +7468 -0
  95. package/.output/server/_ssr/signing-jk4HANnl.mjs +134 -0
  96. package/.output/server/_ssr/ssr.mjs +4 -5740
  97. package/.output/server/_ssr/start-DpViQGnt.mjs +4 -0
  98. package/.output/server/_ssr/table-CU58HeI7.mjs +53 -0
  99. package/.output/server/_ssr/uuidv7-CaKuKXrV.mjs +31 -0
  100. package/.output/server/_tanstack-start-manifest_v-Mz9MmSlD.mjs +103 -0
  101. package/.output/server/index.mjs +229 -47
  102. package/README.md +29 -8
  103. package/lib/index.d.mts +35 -0
  104. package/lib/index.mjs +4 -0
  105. package/lib/vite-internal.mjs +127 -0
  106. package/lib/vite.d.mts +10 -0
  107. package/lib/vite.mjs +5 -0
  108. package/package.json +51 -29
  109. package/public/apple-touch-icon.png +0 -0
  110. package/public/favicon-16x16.png +0 -0
  111. package/public/favicon-32x32.png +0 -0
  112. package/public/favicon.ico +0 -0
  113. package/public/logo.svg +1 -0
  114. package/public/manifest.json +30 -0
  115. package/public/robots.txt +3 -0
  116. package/src/components/AppSidebar.tsx +159 -0
  117. package/src/components/BundleIdDisplay.tsx +18 -0
  118. package/src/components/ChannelBadge.tsx +31 -0
  119. package/src/components/ConsoleAccessPage.tsx +122 -0
  120. package/src/components/EnabledStatusIcon.tsx +54 -0
  121. package/src/components/HashValueDisplay.tsx +85 -0
  122. package/src/components/HotUpdaterLogo.tsx +30 -0
  123. package/src/components/NotFoundPage.tsx +33 -0
  124. package/src/components/PlatformIcon.tsx +39 -0
  125. package/src/components/RolloutPercentageBadge.tsx +28 -0
  126. package/src/components/ThemeProvider.tsx +116 -0
  127. package/src/components/TimestampDisplay.tsx +41 -0
  128. package/src/components/features/api-keys/ApiKeysPage.tsx +377 -0
  129. package/src/components/features/bundles/BundleActivityChart.tsx +193 -0
  130. package/src/components/features/bundles/BundleChildrenPanel.tsx +201 -0
  131. package/src/components/features/bundles/BundleInsightsSummary.tsx +123 -0
  132. package/src/components/features/bundles/BundleMetadata.tsx +136 -0
  133. package/src/components/features/bundles/RolloutCohortsDialog.tsx +201 -0
  134. package/src/components/features/channels/ChannelManagementDialog.tsx +228 -0
  135. package/src/components/features/insights/ActivityChart.tsx +150 -0
  136. package/src/components/features/insights/BundleDistribution.tsx +133 -0
  137. package/src/components/features/insights/BundleSelector.tsx +114 -0
  138. package/src/components/features/insights/InsightsCapabilityContext.tsx +24 -0
  139. package/src/components/features/insights/InsightsControls.tsx +115 -0
  140. package/src/components/features/insights/InsightsErrorAlert.tsx +21 -0
  141. package/src/components/features/insights/InsightsOverview.tsx +208 -0
  142. package/src/components/features/insights/InstallationHistoryCard.tsx +248 -0
  143. package/src/components/features/insights/InstallationMatchesCard.tsx +132 -0
  144. package/src/components/features/insights/InstallationPageHeader.tsx +138 -0
  145. package/src/components/features/insights/InstallationPagination.tsx +48 -0
  146. package/src/components/features/insights/InstallationSearch.tsx +230 -0
  147. package/src/components/features/insights/UpdateOutcomes.tsx +147 -0
  148. package/src/components/features/releases/ReleaseEditorSheet.tsx +879 -0
  149. package/src/components/features/releases/ReleaseStateBadge.tsx +20 -0
  150. package/src/components/features/signing/BundleSigningPage.tsx +139 -0
  151. package/src/components/ui/alert-dialog.tsx +185 -0
  152. package/src/components/ui/alert.tsx +76 -0
  153. package/src/components/ui/badge.tsx +49 -0
  154. package/src/components/ui/button.tsx +58 -0
  155. package/src/components/ui/card.tsx +89 -0
  156. package/src/components/ui/chart.tsx +371 -0
  157. package/src/components/ui/combobox.tsx +303 -0
  158. package/src/components/ui/dialog.tsx +156 -0
  159. package/src/components/ui/dropdown-menu.tsx +270 -0
  160. package/src/components/ui/field.tsx +235 -0
  161. package/src/components/ui/input-group.tsx +147 -0
  162. package/src/components/ui/input.tsx +19 -0
  163. package/src/components/ui/label.tsx +25 -0
  164. package/src/components/ui/progress.tsx +84 -0
  165. package/src/components/ui/select.tsx +205 -0
  166. package/src/components/ui/separator.tsx +23 -0
  167. package/src/components/ui/sheet.tsx +132 -0
  168. package/src/components/ui/sidebar.tsx +779 -0
  169. package/src/components/ui/skeleton.tsx +13 -0
  170. package/src/components/ui/slider.tsx +56 -0
  171. package/src/components/ui/sonner.tsx +45 -0
  172. package/src/components/ui/switch.tsx +25 -0
  173. package/src/components/ui/table.tsx +114 -0
  174. package/src/components/ui/textarea.tsx +18 -0
  175. package/src/components/ui/toggle-group.tsx +89 -0
  176. package/src/components/ui/toggle.tsx +43 -0
  177. package/src/components/ui/tooltip.tsx +66 -0
  178. package/src/hooks/use-mobile.ts +30 -0
  179. package/src/index.ts +47 -0
  180. package/src/lib/api-keys-api.ts +69 -0
  181. package/src/lib/api-keys-rpc.ts +87 -0
  182. package/src/lib/api-rpc.ts +478 -0
  183. package/src/lib/api.ts +444 -0
  184. package/src/lib/auth-rpc.ts +16 -0
  185. package/src/lib/bundle-signing-api.ts +14 -0
  186. package/src/lib/bundle-signing-rpc.ts +16 -0
  187. package/src/lib/constants.ts +1 -0
  188. package/src/lib/git.ts +51 -0
  189. package/src/lib/insights-api.ts +144 -0
  190. package/src/lib/insights-error.ts +19 -0
  191. package/src/lib/insights-input.ts +187 -0
  192. package/src/lib/insights-overview.ts +170 -0
  193. package/src/lib/insights-rpc.ts +218 -0
  194. package/src/lib/server/auth.server.ts +43 -0
  195. package/src/lib/server/bundle-signing.server.ts +23 -0
  196. package/src/lib/server/config.server.ts +109 -0
  197. package/src/lib/server/console-runtime.server.ts +61 -0
  198. package/src/lib/server/deleteBundle.ts +145 -0
  199. package/src/lib/server/downloadBundle.ts +46 -0
  200. package/src/lib/server/getBundleChildren.ts +130 -0
  201. package/src/lib/server/listReleases.ts +61 -0
  202. package/src/lib/server/releaseActivity.ts +27 -0
  203. package/src/lib/server/releaseReachability.ts +95 -0
  204. package/src/lib/server/runtime.server.ts +172 -0
  205. package/src/lib/utils.ts +9 -0
  206. package/src/logo.svg +1 -0
  207. package/src/routeTree.gen.ts +199 -0
  208. package/src/router.tsx +21 -0
  209. package/src/routes/-installations-search.ts +19 -0
  210. package/src/routes/-releases-search.ts +74 -0
  211. package/src/routes/__root.tsx +174 -0
  212. package/src/routes/api/auth/$.ts +19 -0
  213. package/src/routes/api/bundles/$bundleId/download.ts +17 -0
  214. package/src/routes/api-keys.tsx +37 -0
  215. package/src/routes/index.tsx +982 -0
  216. package/src/routes/insights.tsx +139 -0
  217. package/src/routes/installations.tsx +194 -0
  218. package/src/routes/signing.tsx +37 -0
  219. package/src/styles.css +152 -0
  220. package/src/virtual-modules.d.ts +21 -0
  221. package/src/vite-env.d.ts +1 -0
  222. package/src/vite-internal.ts +195 -0
  223. package/src/vite.ts +12 -0
  224. package/.output/public/assets/dist-DJX53faP.js +0 -3
  225. package/.output/public/assets/index-Iylis5kY.js +0 -10
  226. package/.output/public/assets/routes-HT21hs0C.js +0 -54
  227. package/.output/public/assets/styles-C0Re4quB.css +0 -2
  228. package/.output/server/__23tanstack-start-server-fn-resolver-CgwgZ0-i.mjs +0 -66
  229. package/.output/server/_libs/@floating-ui/core+[...].mjs +0 -698
  230. package/.output/server/_libs/@floating-ui/dom+[...].mjs +0 -644
  231. package/.output/server/_libs/@floating-ui/react-dom+[...].mjs +0 -839
  232. package/.output/server/_libs/@radix-ui/react-alert-dialog+[...].mjs +0 -2138
  233. package/.output/server/_libs/@radix-ui/react-popper+[...].mjs +0 -308
  234. package/.output/server/_libs/@radix-ui/react-select+[...].mjs +0 -1038
  235. package/.output/server/_libs/@tanstack/devtools-event-client+[...].mjs +0 -196
  236. package/.output/server/_libs/@tanstack/form-core+[...].mjs +0 -3228
  237. package/.output/server/_libs/@tanstack/react-form+[...].mjs +0 -430
  238. package/.output/server/_libs/@tanstack/react-table+[...].mjs +0 -2382
  239. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  240. package/.output/server/_libs/radix-ui__primitive.mjs +0 -9
  241. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  242. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -78
  243. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -13
  244. package/.output/server/_libs/radix-ui__react-label.mjs +0 -22
  245. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -30
  246. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -510
  247. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -157
  248. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -493
  249. package/.output/server/_ssr/api-rpc-BIbDUKSz.mjs +0 -292
  250. package/.output/server/_ssr/config.server-BSS366KT.mjs +0 -41
  251. package/.output/server/_ssr/deleteBundle-BgbZTtpW.mjs +0 -141
  252. package/.output/server/_ssr/dist-KboydRVk.mjs +0 -120
  253. package/.output/server/_ssr/router-C3hqEy3k.mjs +0 -307
  254. package/.output/server/_ssr/routes-B0lQvvlC.mjs +0 -3048
  255. package/.output/server/_ssr/start-02sLteYG.mjs +0 -4
  256. package/.output/server/_ssr/storageProfile-FVkjPoGb.mjs +0 -9
  257. package/.output/server/_tanstack-start-manifest_v-Bpx52WOU.mjs +0 -20
  258. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  259. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  260. package/.output/server/node_modules/tslib/package.json +0 -47
  261. package/.output/server/node_modules/tslib/tslib.js +0 -484
  262. package/.output/server/package.json +0 -9
@@ -0,0 +1,115 @@
1
+ import type { ActiveInstallationWindow } from "@hot-updater/server";
2
+ import { Search, X } from "lucide-react";
3
+ import { useState } from "react";
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
7
+ import {
8
+ InputGroup,
9
+ InputGroupAddon,
10
+ InputGroupButton,
11
+ InputGroupInput,
12
+ } from "@/components/ui/input-group";
13
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
14
+
15
+ const windows = [
16
+ { value: "24h", label: "24 hours", shortLabel: "24h" },
17
+ { value: "7d", label: "7 days", shortLabel: "7d" },
18
+ { value: "30d", label: "30 days", shortLabel: "30d" },
19
+ ] as const;
20
+
21
+ export function InsightsControls({
22
+ onInstallationSearch,
23
+ onWindowChange,
24
+ window,
25
+ }: {
26
+ readonly onInstallationSearch: (query: string) => void;
27
+ readonly onWindowChange: (window: ActiveInstallationWindow) => void;
28
+ readonly window: ActiveInstallationWindow;
29
+ }) {
30
+ const [draft, setDraft] = useState("");
31
+
32
+ return (
33
+ <section
34
+ aria-label="Insights controls"
35
+ className="rounded-xl border bg-background px-4 py-3"
36
+ >
37
+ <form
38
+ aria-label="Filter insights"
39
+ className="min-w-0"
40
+ onSubmit={(event) => {
41
+ event.preventDefault();
42
+ const query = draft.trim();
43
+ if (query) onInstallationSearch(query);
44
+ }}
45
+ role="search"
46
+ >
47
+ <FieldGroup className="gap-3 md:grid md:grid-cols-[auto_minmax(18rem,1fr)] md:items-end">
48
+ <Field className="min-w-0">
49
+ <FieldLabel>Reporting period</FieldLabel>
50
+ <ToggleGroup
51
+ aria-label="Reporting period"
52
+ className="w-full lg:w-fit"
53
+ onValueChange={(value) => {
54
+ if (value[0]) {
55
+ onWindowChange(value[0] as ActiveInstallationWindow);
56
+ }
57
+ }}
58
+ spacing={0}
59
+ size="lg"
60
+ value={[window]}
61
+ variant="outline"
62
+ >
63
+ {windows.map((item) => (
64
+ <ToggleGroupItem
65
+ aria-label={item.label}
66
+ className="flex-1 md:flex-none"
67
+ key={item.value}
68
+ value={item.value}
69
+ >
70
+ {item.shortLabel}
71
+ </ToggleGroupItem>
72
+ ))}
73
+ </ToggleGroup>
74
+ </Field>
75
+ <Field className="min-w-0">
76
+ <FieldLabel htmlFor="installation-history-search">
77
+ Installation history
78
+ </FieldLabel>
79
+ <div className="flex min-w-0 flex-col gap-2 sm:flex-row">
80
+ <InputGroup className="h-8 flex-1">
81
+ <InputGroupAddon>
82
+ <Search aria-hidden="true" />
83
+ </InputGroupAddon>
84
+ <InputGroupInput
85
+ aria-label="User or install ID"
86
+ id="installation-history-search"
87
+ maxLength={1024}
88
+ onChange={(event) => setDraft(event.target.value)}
89
+ placeholder="User ID or install ID"
90
+ type="search"
91
+ value={draft}
92
+ />
93
+ {draft && (
94
+ <InputGroupAddon align="inline-end">
95
+ <InputGroupButton
96
+ aria-label="Clear installation search"
97
+ onClick={() => setDraft("")}
98
+ size="icon-xs"
99
+ >
100
+ <X aria-hidden="true" />
101
+ </InputGroupButton>
102
+ </InputGroupAddon>
103
+ )}
104
+ </InputGroup>
105
+ <Button className="w-full sm:w-auto" size="lg" type="submit">
106
+ <Search aria-hidden="true" data-icon="inline-start" />
107
+ Search
108
+ </Button>
109
+ </div>
110
+ </Field>
111
+ </FieldGroup>
112
+ </form>
113
+ </section>
114
+ );
115
+ }
@@ -0,0 +1,21 @@
1
+ import { TriangleAlert } from "lucide-react";
2
+
3
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
4
+ import { getInsightsErrorCopy } from "@/lib/insights-error";
5
+
6
+ export function InsightsErrorAlert({
7
+ error,
8
+ fallbackTitle,
9
+ }: {
10
+ readonly error: Error;
11
+ readonly fallbackTitle: string;
12
+ }) {
13
+ const copy = getInsightsErrorCopy(error, fallbackTitle);
14
+ return (
15
+ <Alert variant="destructive">
16
+ <TriangleAlert aria-hidden="true" />
17
+ <AlertTitle>{copy.title}</AlertTitle>
18
+ <AlertDescription>{copy.description}</AlertDescription>
19
+ </Alert>
20
+ );
21
+ }
@@ -0,0 +1,208 @@
1
+ import type { ActiveInstallationOverview } from "@hot-updater/server";
2
+ import type { ReactNode } from "react";
3
+
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardFooter,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from "@/components/ui/card";
12
+ import { Skeleton } from "@/components/ui/skeleton";
13
+ import type { InsightsOverview as CatalogOverview } from "@/lib/insights-overview";
14
+ import { cn } from "@/lib/utils";
15
+
16
+ import { ActivityChart } from "./ActivityChart";
17
+ import { BundleSelector } from "./BundleSelector";
18
+ import { InsightsErrorAlert } from "./InsightsErrorAlert";
19
+ import { UpdateOutcomes, type UpdateOutcomeState } from "./UpdateOutcomes";
20
+
21
+ type InsightsOverviewProps =
22
+ | { readonly status: "loading" }
23
+ | { readonly status: "error"; readonly error: Error }
24
+ | {
25
+ readonly status: "success";
26
+ readonly active: ActiveInstallationOverview;
27
+ readonly bundleId: string;
28
+ readonly bundles: readonly {
29
+ readonly bundleId: string;
30
+ readonly description: string;
31
+ }[];
32
+ readonly catalog: CatalogOverview;
33
+ readonly onBundleChange: (bundleId: string) => void;
34
+ readonly outcomes: UpdateOutcomeState;
35
+ };
36
+
37
+ const asOfFormatter = new Intl.DateTimeFormat("en", {
38
+ dateStyle: "medium",
39
+ timeStyle: "short",
40
+ timeZone: "UTC",
41
+ });
42
+
43
+ const activityWindowCopy = {
44
+ "24h": {
45
+ label: "Daily active installations",
46
+ period: "last 24 hours",
47
+ },
48
+ "7d": {
49
+ label: "Weekly active installations",
50
+ period: "last 7 days",
51
+ },
52
+ "30d": {
53
+ label: "Monthly active installations",
54
+ period: "last 30 days",
55
+ },
56
+ } as const;
57
+
58
+ function LoadingCard({
59
+ children,
60
+ className,
61
+ label,
62
+ }: {
63
+ readonly children: ReactNode;
64
+ readonly className?: string;
65
+ readonly label: string;
66
+ }) {
67
+ return (
68
+ <Card aria-label={label} className={cn("min-w-0", className)}>
69
+ <CardHeader>
70
+ <Skeleton className="h-4 w-32" />
71
+ <Skeleton className="h-3 w-4/5" />
72
+ </CardHeader>
73
+ <CardContent>{children}</CardContent>
74
+ </Card>
75
+ );
76
+ }
77
+
78
+ export function InsightsOverview(props: InsightsOverviewProps) {
79
+ if (props.status === "loading") {
80
+ return (
81
+ <div
82
+ aria-label="Loading reporting insights"
83
+ className="flex min-w-0 flex-col gap-8"
84
+ >
85
+ <LoadingCard label="Loading installation activity">
86
+ <Skeleton className="h-10 w-24" />
87
+ <Skeleton className="mt-5 h-64 w-full" />
88
+ <div className="mt-5 grid gap-4 border-t pt-5 sm:grid-cols-3">
89
+ <Skeleton className="h-10 w-full" />
90
+ <Skeleton className="h-10 w-full" />
91
+ <Skeleton className="h-10 w-full" />
92
+ </div>
93
+ </LoadingCard>
94
+ <LoadingCard label="Loading bundle detail">
95
+ <div className="flex flex-col gap-4">
96
+ <Skeleton className="h-10 w-full max-w-md" />
97
+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
98
+ <Skeleton className="h-14 w-full" />
99
+ <Skeleton className="h-14 w-full" />
100
+ <Skeleton className="h-14 w-full" />
101
+ <Skeleton className="h-14 w-full" />
102
+ </div>
103
+ <Skeleton className="h-32 w-full" />
104
+ </div>
105
+ </LoadingCard>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ if (props.status === "error") {
111
+ return (
112
+ <InsightsErrorAlert
113
+ error={props.error}
114
+ fallbackTitle="Reporting insights unavailable"
115
+ />
116
+ );
117
+ }
118
+
119
+ const { active, bundleId, bundles, catalog, onBundleChange, outcomes } =
120
+ props;
121
+ const activityCopy = activityWindowCopy[active.window];
122
+ const selectedBundleId =
123
+ outcomes.status === "idle" ? null : outcomes.bundleId;
124
+ const latestBundleInstallations =
125
+ active.bundles.find(({ bundleId }) => bundleId === selectedBundleId)
126
+ ?.installations ?? 0;
127
+ const configuredPercentage =
128
+ catalog.configuredRollouts.find(
129
+ ({ bundleId }) => bundleId === selectedBundleId,
130
+ )?.configuredPercentage ?? null;
131
+
132
+ return (
133
+ <div className="flex min-w-0 flex-col gap-8">
134
+ <section aria-label="Installation activity">
135
+ <Card className="min-w-0 overflow-hidden shadow-sm">
136
+ <CardHeader className="pb-4">
137
+ <CardTitle className="text-sm font-medium">
138
+ <h2>{activityCopy.label}</h2>
139
+ </CardTitle>
140
+ <CardDescription>
141
+ Unique app installations that reported an update status in the{" "}
142
+ {activityCopy.period}.
143
+ </CardDescription>
144
+ </CardHeader>
145
+ <CardContent className="flex flex-col gap-5">
146
+ <div className="flex flex-col gap-1">
147
+ <p className="text-xs font-medium text-muted-foreground">
148
+ Active installations
149
+ </p>
150
+ <div className="flex items-end gap-2">
151
+ <span className="text-4xl font-semibold tracking-tight tabular-nums">
152
+ {active.activeInstallations.toLocaleString()}
153
+ </span>
154
+ <span className="pb-1 text-xs text-muted-foreground">
155
+ unique in {activityCopy.period}
156
+ </span>
157
+ </div>
158
+ </div>
159
+ <div className="border-t pt-5">
160
+ <ActivityChart series={active.series} window={active.window} />
161
+ </div>
162
+ </CardContent>
163
+ <CardFooter className="border-t bg-muted/15 p-0">
164
+ <dl className="grid w-full sm:grid-cols-3 sm:divide-x sm:divide-border/70">
165
+ <div className="flex flex-col gap-1 px-5 py-4">
166
+ <dt className="text-xs text-muted-foreground">
167
+ Reported bundles
168
+ </dt>
169
+ <dd className="text-lg font-semibold tabular-nums">
170
+ {active.bundles.length.toLocaleString()}
171
+ </dd>
172
+ </div>
173
+ <div className="flex flex-col gap-1 border-t px-5 py-4 sm:border-t-0">
174
+ <dt className="text-xs text-muted-foreground">
175
+ Reporting window
176
+ </dt>
177
+ <dd className="text-sm font-medium">{activityCopy.period}</dd>
178
+ </div>
179
+ <div className="flex flex-col gap-1 border-t px-5 py-4 sm:border-t-0">
180
+ <dt className="text-xs text-muted-foreground">As of</dt>
181
+ <dd className="text-xs font-medium tabular-nums">
182
+ {asOfFormatter.format(new Date(active.asOfMs))} UTC
183
+ </dd>
184
+ </div>
185
+ </dl>
186
+ </CardFooter>
187
+ </Card>
188
+ </section>
189
+
190
+ <section aria-labelledby="bundle-detail-heading">
191
+ <UpdateOutcomes
192
+ bundleSelector={
193
+ <BundleSelector
194
+ bundleId={bundleId}
195
+ bundles={bundles}
196
+ onBundleChange={onBundleChange}
197
+ />
198
+ }
199
+ configuredPercentage={configuredPercentage}
200
+ latestBundleInstallations={latestBundleInstallations}
201
+ reportingInstallations={active.activeInstallations}
202
+ state={outcomes}
203
+ window={active.window}
204
+ />
205
+ </section>
206
+ </div>
207
+ );
208
+ }
@@ -0,0 +1,248 @@
1
+ import { TriangleAlert } from "lucide-react";
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";
13
+ import { Separator } from "@/components/ui/separator";
14
+ import { Skeleton } from "@/components/ui/skeleton";
15
+ import {
16
+ Table,
17
+ TableBody,
18
+ TableCell,
19
+ TableHead,
20
+ TableHeader,
21
+ TableRow,
22
+ } from "@/components/ui/table";
23
+ import type {
24
+ InstallationHistoryResult,
25
+ InstallationHistoryRow,
26
+ InstallationSearchRow,
27
+ } from "@/lib/api";
28
+
29
+ import { InstallationPagination } from "./InstallationPagination";
30
+
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
+ };
46
+
47
+ const getUserLabel = (event: {
48
+ readonly username: string | null;
49
+ readonly userId: string | null;
50
+ }) => event.userId ?? event.username ?? "—";
51
+
52
+ const getLastKnownBundleId = (
53
+ event: InstallationHistoryRow | InstallationSearchRow,
54
+ ) =>
55
+ "lastKnownBundleId" in event
56
+ ? event.lastKnownBundleId
57
+ : (event.toBundleId ?? event.fromBundleId);
58
+
59
+ export function InstallationHistoryCard({
60
+ error,
61
+ history,
62
+ isLoading,
63
+ limit,
64
+ offset,
65
+ onOffsetChange,
66
+ selectedEvent,
67
+ selectedInstallId,
68
+ }: {
69
+ readonly error: unknown;
70
+ readonly history: InstallationHistoryResult | undefined;
71
+ readonly isLoading: boolean;
72
+ readonly limit: number;
73
+ readonly offset: number;
74
+ readonly onOffsetChange: (offset: number) => void;
75
+ readonly selectedEvent:
76
+ | InstallationHistoryRow
77
+ | InstallationSearchRow
78
+ | undefined;
79
+ readonly selectedInstallId: string;
80
+ }) {
81
+ const lastKnownBundleId = selectedEvent
82
+ ? getLastKnownBundleId(selectedEvent)
83
+ : undefined;
84
+
85
+ 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">
89
+ <div className="flex min-w-0 flex-col gap-1.5">
90
+ <CardTitle className="text-sm font-medium">
91
+ <h2>
92
+ {selectedInstallId
93
+ ? "Installation history"
94
+ : "Select an installation"}
95
+ </h2>
96
+ </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
+ </div>
103
+ {history && history.pagination.total > 0 ? (
104
+ <Badge variant="outline">{history.pagination.total} events</Badge>
105
+ ) : null}
106
+ </div>
107
+ </CardHeader>
108
+ <CardContent className="min-h-0 p-0">
109
+ {selectedInstallId ? (
110
+ isLoading ? (
111
+ <div className="flex flex-col gap-3 p-5">
112
+ <Skeleton className="h-10 w-full" />
113
+ <Skeleton className="h-10 w-full" />
114
+ <Skeleton className="h-10 w-full" />
115
+ </div>
116
+ ) : history && history.data.length > 0 ? (
117
+ <>
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>
210
+ </TableRow>
211
+ ))}
212
+ </TableBody>
213
+ </Table>
214
+ <InstallationPagination
215
+ label="Installation history"
216
+ limit={limit}
217
+ offset={offset}
218
+ pageLength={history.data.length}
219
+ total={history.pagination.total}
220
+ onOffsetChange={onOffsetChange}
221
+ />
222
+ </>
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
+ ) : (
236
+ <div className="p-5 pt-0 text-sm text-muted-foreground">
237
+ No history is available for this installation.
238
+ </div>
239
+ )
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
+ )}
245
+ </CardContent>
246
+ </Card>
247
+ );
248
+ }