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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/HashValueDisplay-CB0Eskit.js +1 -0
  3. package/.output/public/assets/InsightsPageHeader-Car5LNiZ.js +1 -0
  4. package/.output/public/assets/api-keys-BzKocqRy.js +1 -0
  5. package/.output/public/assets/api-keys-api-BHcODB7q.js +1 -0
  6. package/.output/public/assets/card-Dhr_eFYj.js +1 -0
  7. package/.output/public/assets/{chart-no-axes-combined-FaYMGmQS.js → chart-no-axes-combined-Dy4WJv2U.js} +1 -1
  8. package/.output/public/assets/composite-Bp98A0Ld.js +1 -0
  9. package/.output/public/assets/createServerFn-CA8fs7QD.js +3 -0
  10. package/.output/public/assets/dialog-CUTrcPlB.js +1 -0
  11. package/.output/public/assets/{dist-DspNKfMj.js → dist-C_NydL0e.js} +1 -1
  12. package/.output/public/assets/index-DdAz-CXv.js +10 -0
  13. package/.output/public/assets/input-group-cSOG4wh_.js +1 -0
  14. package/.output/public/assets/insights-C0PBSZ-n.js +1 -0
  15. package/.output/public/assets/installations-CE33wgbY.js +2 -0
  16. package/.output/public/assets/installations-CStZja8x.js +1 -0
  17. package/.output/public/assets/lazyRouteComponent-Ccp3bwBY.js +1 -0
  18. package/.output/public/assets/link-CI8idAUi.js +1 -0
  19. package/.output/public/assets/loader-circle-BoxLrsOu.js +1 -0
  20. package/.output/public/assets/refresh-cw-Bqc72UZG.js +1 -0
  21. package/.output/public/assets/{routes-DcQnVajO.js → routes-DGVXdzpw.js} +2 -2
  22. package/.output/public/assets/routes-DTlwOr8I.js +1 -0
  23. package/.output/public/assets/{shield-check-DE_hVuHu.js → shield-check-uK3MHWJZ.js} +1 -1
  24. package/.output/public/assets/signing-CMf4aqDO.js +1 -0
  25. package/.output/public/assets/styles-BCvbtKsm.css +2 -0
  26. package/.output/public/assets/table-2vcV89QB.js +1 -0
  27. package/.output/public/assets/useCompositeListItem-CHLITE_4.js +1 -0
  28. package/.output/public/assets/x-DCd0j8c6.js +1 -0
  29. package/.output/server/{__23tanstack-start-server-fn-resolver-DBP9TQ7K.mjs → __23tanstack-start-server-fn-resolver-B7ioJhtE.mjs} +44 -52
  30. package/.output/server/_libs/@base-ui/react+[...].mjs +1906 -4040
  31. package/.output/server/_libs/@tanstack/react-router+[...].mjs +52 -4
  32. package/.output/server/_libs/@tanstack/react-router-ssr-query+[...].mjs +2 -2
  33. package/.output/server/_libs/lucide-react.mjs +36 -3
  34. package/.output/server/_libs/next-themes.mjs +2 -2
  35. package/.output/server/_libs/sonner.mjs +2 -2
  36. package/.output/server/_libs/tanstack__react-query.mjs +2 -2
  37. package/.output/server/_libs/verkit.mjs +20 -15
  38. package/.output/server/_runtime.mjs +1 -10
  39. package/.output/server/_ssr/HashValueDisplay-DwpGLfrl.mjs +55 -0
  40. package/.output/server/_ssr/InsightsPageHeader-Df4Brj8u.mjs +484 -0
  41. package/.output/server/_ssr/{api-keys-BnM_o2nS.mjs → api-keys-CZeTPQys.mjs} +11 -13
  42. package/.output/server/_ssr/{api-keys-api-CIywsSF5.mjs → api-keys-api-BUPQVSpk.mjs} +2 -2
  43. package/.output/server/_ssr/{api-keys-rpc-BSp1wkIV.mjs → api-keys-rpc-Cs7tS5_j.mjs} +5 -5
  44. package/.output/server/_ssr/{api-rpc-cG5wd_tO.mjs → api-rpc-tAupfGQm.mjs} +73 -142
  45. package/.output/server/_ssr/{auth-rpc-BrJ6obHJ.mjs → auth-rpc-DUaeOWKK.mjs} +4 -4
  46. package/.output/server/_ssr/{auth.server-DwKoZoQM.mjs → auth.server-DW71WzsD.mjs} +2 -5
  47. package/.output/server/_ssr/{bundle-signing-rpc-COoQhW8E.mjs → bundle-signing-rpc-BhnBw0-d.mjs} +2 -2
  48. package/.output/server/_ssr/bundle-signing.server-C4NW5mWx.mjs +12 -0
  49. package/.output/server/_ssr/{alert-Dim3MGY5.mjs → card-BRiPmi0C.mjs} +61 -39
  50. package/.output/server/_ssr/{config.server-DEiLhKmy.mjs → config.server-afDuAwaG.mjs} +15 -8
  51. package/.output/server/_ssr/{createSsrRpc-9P0S0lJ3.mjs → createSsrRpc-DjWNJ0uc.mjs} +6 -6
  52. package/.output/server/_ssr/databaseJsonValue-d6I3nAS4.mjs +45 -0
  53. package/.output/server/_ssr/{databaseRows-CwbNwYSl.mjs → databaseRows-5bCRTxm-.mjs} +4 -44
  54. package/.output/server/_ssr/{deleteBundle-Cu_XOMt0.mjs → deleteBundle-BTGwBgzx.mjs} +2 -2
  55. package/.output/server/_ssr/{dialog-CQuRMNZ4.mjs → dialog-DS-nw-pA.mjs} +2 -2
  56. package/.output/server/_ssr/{dist-BVSLTpoP.mjs → dist-CyzPaI5v.mjs} +2 -2
  57. package/.output/server/_ssr/{getBundleChildren-BcRMFoDF.mjs → getBundleChildren-C0vh4ZvE.mjs} +2 -2
  58. package/.output/server/_ssr/input-group-BpsnmvZw.mjs +70 -0
  59. package/.output/server/_ssr/insights-BdS364Xu.mjs +460 -0
  60. package/.output/server/_ssr/insights-rpc-38DujK0G.mjs +57 -0
  61. package/.output/server/_ssr/insightsContract-fjJDBeEE.mjs +303 -0
  62. package/.output/server/_ssr/installations-CtA59MQG.mjs +29 -0
  63. package/.output/server/_ssr/installations-DiVKEF5T.mjs +627 -0
  64. package/.output/server/_ssr/{router-C-KUBfOi.mjs → router-B9wG_9f8.mjs} +37 -46
  65. package/.output/server/_ssr/{routes-Z7TwC6nC.mjs → routes-CKrrS2q2.mjs} +3 -3
  66. package/.output/server/_ssr/{routes-BM4ht9jb.mjs → routes-D7Yh_jfj.mjs} +402 -390
  67. package/.output/server/_ssr/runtime.server-DacU1TBV.mjs +438 -0
  68. package/.output/server/_ssr/{server-BjzmvXnO.mjs → server-D_C2rMzf.mjs} +5 -5
  69. package/.output/server/_ssr/{signing-k8GxyBDF.mjs → signing-Dz1WkZOf.mjs} +16 -85
  70. package/.output/server/_ssr/ssr.mjs +1 -1
  71. package/.output/server/_ssr/{field-DRIXKjZG.mjs → table-CnZmon_Z.mjs} +51 -5
  72. package/.output/server/_ssr/{uuidv7-BbJ-9D_I.mjs → uuidv7-D0QFkPL_.mjs} +3 -3
  73. package/.output/server/_tanstack-start-manifest_v-2MjTyxil.mjs +97 -0
  74. package/.output/server/index.mjs +168 -203
  75. package/lib/index.d.mts +4 -4
  76. package/lib/vite-internal.mjs +1 -1
  77. package/lib/vite.d.mts +0 -1
  78. package/package.json +8 -8
  79. package/src/components/AppSidebar.tsx +12 -16
  80. package/src/components/ConsoleAccessPage.tsx +1 -1
  81. package/src/components/HashValueDisplay.tsx +11 -2
  82. package/src/components/features/api-keys/ApiKeysPage.tsx +1 -1
  83. package/src/components/features/bundles/BundleChildrenPanel.tsx +7 -6
  84. package/src/components/features/bundles/BundleMetadata.tsx +1 -20
  85. package/src/components/features/channels/ChannelManagementDialog.tsx +1 -1
  86. package/src/components/features/insights/EventDetails.tsx +151 -0
  87. package/src/components/features/insights/EventHistoryCard.tsx +245 -0
  88. package/src/components/features/insights/EventHistoryList.tsx +63 -0
  89. package/src/components/features/insights/InsightsBundleReports.tsx +51 -0
  90. package/src/components/features/insights/InsightsControls.tsx +121 -0
  91. package/src/components/features/{analytics/AnalyticsErrorAlert.tsx → insights/InsightsErrorAlert.tsx} +6 -6
  92. package/src/components/features/insights/InsightsOverview.tsx +203 -0
  93. package/src/components/features/insights/InsightsPageHeader.tsx +48 -0
  94. package/src/components/features/insights/InsightsPagination.tsx +52 -0
  95. package/src/components/features/insights/InstallationHistoryCard.tsx +250 -0
  96. package/src/components/features/insights/InstallationMatchesCard.tsx +178 -0
  97. package/src/components/features/insights/InstallationPageHeader.tsx +90 -0
  98. package/src/components/features/releases/ReleaseEditorSheet.tsx +24 -24
  99. package/src/components/features/signing/BundleSigningPage.tsx +18 -126
  100. package/src/components/ui/badge.tsx +2 -0
  101. package/src/components/ui/combobox.tsx +1 -0
  102. package/src/components/ui/skeleton.tsx +4 -1
  103. package/src/components/ui/tooltip.tsx +1 -1
  104. package/src/index.ts +7 -3
  105. package/src/lib/api-rpc.ts +19 -90
  106. package/src/lib/api.ts +0 -99
  107. package/src/lib/insights-api.ts +100 -0
  108. package/src/lib/insights-rpc.ts +88 -0
  109. package/src/lib/insights-view.ts +15 -0
  110. package/src/lib/public-bundle-error.ts +19 -0
  111. package/src/lib/server/bundle-signing.server.ts +5 -82
  112. package/src/lib/server/console-runtime.server.ts +0 -9
  113. package/src/lib/server/releaseReachability.ts +1 -4
  114. package/src/lib/server/runtime.server.ts +6 -142
  115. package/src/routeTree.gen.ts +21 -21
  116. package/src/router.tsx +3 -0
  117. package/src/routes/-installations-search.ts +25 -12
  118. package/src/routes/__root.tsx +6 -16
  119. package/src/routes/api-keys.tsx +1 -1
  120. package/src/routes/index.tsx +21 -81
  121. package/src/routes/insights.tsx +84 -0
  122. package/src/routes/installations.tsx +294 -142
  123. package/src/styles.css +6 -0
  124. package/.output/public/assets/AnalyticsCapabilityContext-BCke57-7.js +0 -1
  125. package/.output/public/assets/BundleActivityChart-CnsBe_1J.js +0 -42
  126. package/.output/public/assets/BundleIdDisplay-Dky75zLU.js +0 -1
  127. package/.output/public/assets/alert-C-1KvDW8.js +0 -1
  128. package/.output/public/assets/analytics-Bqz0c-BT.js +0 -1
  129. package/.output/public/assets/analytics-api-CJdrTLLO.js +0 -1
  130. package/.output/public/assets/api-B_Rh-uzC.js +0 -1
  131. package/.output/public/assets/api-keys-BurCFhzH.js +0 -1
  132. package/.output/public/assets/api-keys-api-BEDJdQg3.js +0 -1
  133. package/.output/public/assets/badge-Bf0fZ-Gx.js +0 -1
  134. package/.output/public/assets/createServerFn-gM2-LFBy.js +0 -3
  135. package/.output/public/assets/dialog-BS60BRr2.js +0 -1
  136. package/.output/public/assets/download-O-yNBc9H.js +0 -1
  137. package/.output/public/assets/field-Buq148-A.js +0 -1
  138. package/.output/public/assets/index-kFhd2S78.js +0 -10
  139. package/.output/public/assets/installations-BrQ-od41.js +0 -2
  140. package/.output/public/assets/installations-CGlZ3ROz.js +0 -1
  141. package/.output/public/assets/lazyRouteComponent-rPzgA7S-.js +0 -1
  142. package/.output/public/assets/loader-circle-DCYF-wJl.js +0 -1
  143. package/.output/public/assets/refresh-cw-n8XYbPPj.js +0 -1
  144. package/.output/public/assets/routes-CrdokJ80.js +0 -1
  145. package/.output/public/assets/search-DLhbWw3W.js +0 -1
  146. package/.output/public/assets/signing-HRexfjsH.js +0 -1
  147. package/.output/public/assets/styles-eB4EtvFS.css +0 -2
  148. package/.output/public/assets/table-D8CLieHX.js +0 -1
  149. package/.output/public/assets/textarea-YJLTVAoq.js +0 -1
  150. package/.output/public/assets/useMutation-JsGVjQMY.js +0 -1
  151. package/.output/public/assets/useOpenInteractionType-4cRD5xO0.js +0 -1
  152. package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +0 -1554
  153. package/.output/server/_libs/d3-array.mjs +0 -231
  154. package/.output/server/_libs/d3-color.mjs +0 -330
  155. package/.output/server/_libs/d3-format.mjs +0 -252
  156. package/.output/server/_libs/d3-interpolate.mjs +0 -207
  157. package/.output/server/_libs/d3-path.mjs +0 -77
  158. package/.output/server/_libs/d3-scale+[...].mjs +0 -1907
  159. package/.output/server/_libs/d3-shape.mjs +0 -818
  160. package/.output/server/_libs/decimal.js-light.mjs +0 -884
  161. package/.output/server/_libs/es-toolkit.mjs +0 -1134
  162. package/.output/server/_libs/eventemitter3.mjs +0 -236
  163. package/.output/server/_libs/immer.mjs +0 -532
  164. package/.output/server/_libs/react-redux+[...].mjs +0 -205
  165. package/.output/server/_libs/recharts+[...].mjs +0 -14117
  166. package/.output/server/_ssr/AnalyticsCapabilityContext-ui4DX8GO.mjs +0 -15
  167. package/.output/server/_ssr/BundleActivityChart-BiB9ZORH.mjs +0 -288
  168. package/.output/server/_ssr/BundleIdDisplay-Czk6iR6p.mjs +0 -13
  169. package/.output/server/_ssr/analytics-DtMTn8fn.mjs +0 -858
  170. package/.output/server/_ssr/analytics-api-Cg1M5v_H.mjs +0 -71
  171. package/.output/server/_ssr/analytics-input-fRRRn0uA.mjs +0 -96
  172. package/.output/server/_ssr/analytics-rpc-DY5uXvHT.mjs +0 -155
  173. package/.output/server/_ssr/api-BTSv1tZb.mjs +0 -282
  174. package/.output/server/_ssr/badge-DCxbEeof.mjs +0 -26
  175. package/.output/server/_ssr/bundle-signing.server-D5u8KkMp.mjs +0 -51
  176. package/.output/server/_ssr/installations-B-azgAvX.mjs +0 -19
  177. package/.output/server/_ssr/installations-Ctvzp2JH.mjs +0 -535
  178. package/.output/server/_ssr/runtime.server-awTLiiXW.mjs +0 -527
  179. package/.output/server/_ssr/table-v1yCvYo7.mjs +0 -53
  180. package/.output/server/_ssr/textarea-DYHnskhb.mjs +0 -14
  181. package/.output/server/_tanstack-start-manifest_v-CH4StzJu.mjs +0 -108
  182. package/src/components/features/analytics/ActivityChart.tsx +0 -150
  183. package/src/components/features/analytics/AnalyticsCapabilityContext.tsx +0 -24
  184. package/src/components/features/analytics/AnalyticsControls.tsx +0 -115
  185. package/src/components/features/analytics/AnalyticsOverview.tsx +0 -208
  186. package/src/components/features/analytics/BundleDistribution.tsx +0 -133
  187. package/src/components/features/analytics/BundleSelector.tsx +0 -114
  188. package/src/components/features/analytics/InstallationHistoryCard.tsx +0 -248
  189. package/src/components/features/analytics/InstallationMatchesCard.tsx +0 -132
  190. package/src/components/features/analytics/InstallationPageHeader.tsx +0 -138
  191. package/src/components/features/analytics/InstallationPagination.tsx +0 -48
  192. package/src/components/features/analytics/InstallationSearch.tsx +0 -230
  193. package/src/components/features/analytics/UpdateOutcomes.tsx +0 -147
  194. package/src/components/features/bundles/BundleActivityChart.tsx +0 -193
  195. package/src/components/features/bundles/BundleAnalyticsSummary.tsx +0 -123
  196. package/src/lib/analytics-api.ts +0 -145
  197. package/src/lib/analytics-error.ts +0 -19
  198. package/src/lib/analytics-input.ts +0 -187
  199. package/src/lib/analytics-overview.ts +0 -170
  200. package/src/lib/analytics-rpc.ts +0 -218
  201. package/src/lib/server/releaseActivity.ts +0 -27
  202. package/src/routes/analytics.tsx +0 -139
  203. /package/.output/public/assets/{preload-helper-TKOBYGYD.js → preload-helper-CQ36nxok.js} +0 -0
@@ -0,0 +1,51 @@
1
+ import type { InsightsBundleSelection } from "@hot-updater/server";
2
+ import { useState } from "react";
3
+
4
+ import { useInsightsEventsQuery } from "@/lib/insights-api";
5
+ import { outcomeLabels } from "@/lib/insights-view";
6
+
7
+ import { EventHistoryCard } from "./EventHistoryCard";
8
+
9
+ export type InsightsReportSelection = {
10
+ readonly bundle: InsightsBundleSelection;
11
+ readonly sinceMs: number;
12
+ readonly beforeReceivedAtMs: number;
13
+ };
14
+
15
+ export function InsightsBundleReports({
16
+ selection,
17
+ }: {
18
+ readonly selection: InsightsReportSelection;
19
+ }) {
20
+ const [cursors, setCursors] = useState<readonly (string | undefined)[]>([
21
+ undefined,
22
+ ]);
23
+ const events = useInsightsEventsQuery(
24
+ { ...selection, cursor: cursors.at(-1), limit: 50 },
25
+ true,
26
+ );
27
+ return (
28
+ <EventHistoryCard
29
+ title={outcomeLabels[selection.bundle.outcome]}
30
+ error={events.error}
31
+ eventsLocation={{ eventsBefore: selection.beforeReceivedAtMs }}
32
+ history={events.data}
33
+ isFetching={events.isFetching}
34
+ isLoading={events.isLoading}
35
+ onNext={() => {
36
+ const cursor = events.data?.nextCursor;
37
+ if (cursor) setCursors((previous) => [...previous, cursor]);
38
+ }}
39
+ onPrevious={() => setCursors((previous) => previous.slice(0, -1))}
40
+ onRefresh={() => {
41
+ void events.refetch();
42
+ }}
43
+ pageNumber={cursors.length}
44
+ >
45
+ <p className="text-sm text-muted-foreground">
46
+ Reports for the selected bundle and the measured period. New reports
47
+ appear after refreshing the overview.
48
+ </p>
49
+ </EventHistoryCard>
50
+ );
51
+ }
@@ -0,0 +1,121 @@
1
+ import { useState } from "react";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
5
+ import { Input } from "@/components/ui/input";
6
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
7
+ import type { InsightsOverviewInput, InsightsWindow } from "@/lib/insights-api";
8
+
9
+ const windows = [
10
+ { value: "24h", label: "24 hours", shortLabel: "24h" },
11
+ { value: "7d", label: "7 days", shortLabel: "7d" },
12
+ { value: "30d", label: "30 days", shortLabel: "30d" },
13
+ ] as const;
14
+
15
+ export function InsightsControls({
16
+ onWindowChange,
17
+ onScopeChange,
18
+ scope,
19
+ window,
20
+ }: {
21
+ readonly onWindowChange: (window: InsightsWindow) => void;
22
+ readonly onScopeChange: (
23
+ scope: Omit<InsightsOverviewInput, "window">,
24
+ ) => void;
25
+ readonly scope: Omit<InsightsOverviewInput, "window">;
26
+ readonly window: InsightsWindow;
27
+ }) {
28
+ const [platform, setPlatform] = useState(scope.platform);
29
+ const [channel, setChannel] = useState(scope.channel);
30
+ const [bundleId, setBundleId] = useState(scope.bundleId ?? "");
31
+
32
+ return (
33
+ <section aria-label="Insights controls" className="flex flex-col gap-4">
34
+ <form
35
+ onSubmit={(event) => {
36
+ event.preventDefault();
37
+ onScopeChange({ platform, channel, bundleId: bundleId || undefined });
38
+ }}
39
+ >
40
+ <FieldGroup className="items-end sm:flex-row">
41
+ <Field className="w-full sm:w-auto">
42
+ <FieldLabel>Platform</FieldLabel>
43
+ <ToggleGroup
44
+ aria-label="Platform"
45
+ value={[platform]}
46
+ onValueChange={(value) => {
47
+ if (value[0] === "ios" || value[0] === "android")
48
+ setPlatform(value[0]);
49
+ }}
50
+ spacing={0}
51
+ variant="outline"
52
+ >
53
+ <ToggleGroupItem className="h-11 lg:h-8" value="ios">
54
+ iOS
55
+ </ToggleGroupItem>
56
+ <ToggleGroupItem className="h-11 lg:h-8" value="android">
57
+ Android
58
+ </ToggleGroupItem>
59
+ </ToggleGroup>
60
+ </Field>
61
+ <Field>
62
+ <FieldLabel htmlFor="insights-channel">Channel</FieldLabel>
63
+ <Input
64
+ className="h-11 lg:h-8"
65
+ id="insights-channel"
66
+ value={channel}
67
+ onChange={(event) => setChannel(event.target.value)}
68
+ required
69
+ maxLength={1024}
70
+ />
71
+ </Field>
72
+ <Field>
73
+ <FieldLabel htmlFor="insights-bundle">
74
+ Bundle ID (optional)
75
+ </FieldLabel>
76
+ <Input
77
+ className="h-11 lg:h-8"
78
+ id="insights-bundle"
79
+ placeholder="Select a bundle by ID"
80
+ value={bundleId}
81
+ onChange={(event) => setBundleId(event.target.value)}
82
+ maxLength={1024}
83
+ />
84
+ </Field>
85
+ <Button
86
+ className="h-11 w-full sm:w-auto lg:h-8"
87
+ type="submit"
88
+ variant="outline"
89
+ >
90
+ Apply filters
91
+ </Button>
92
+ </FieldGroup>
93
+ </form>
94
+ <Field orientation="horizontal" className="w-full">
95
+ <FieldLabel className="sr-only">Reporting period</FieldLabel>
96
+ <ToggleGroup
97
+ aria-label="Reporting period"
98
+ className="w-full sm:w-fit"
99
+ onValueChange={(value) => {
100
+ if (value[0]) onWindowChange(value[0] as InsightsWindow);
101
+ }}
102
+ spacing={0}
103
+ size="lg"
104
+ value={[window]}
105
+ variant="outline"
106
+ >
107
+ {windows.map((item) => (
108
+ <ToggleGroupItem
109
+ aria-label={item.label}
110
+ className="h-11 flex-1 px-4 sm:flex-none lg:h-8 lg:px-2.5"
111
+ key={item.value}
112
+ value={item.value}
113
+ >
114
+ {item.shortLabel}
115
+ </ToggleGroupItem>
116
+ ))}
117
+ </ToggleGroup>
118
+ </Field>
119
+ </section>
120
+ );
121
+ }
@@ -1,21 +1,21 @@
1
1
  import { TriangleAlert } from "lucide-react";
2
2
 
3
3
  import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
4
- import { getAnalyticsErrorCopy } from "@/lib/analytics-error";
5
4
 
6
- export function AnalyticsErrorAlert({
7
- error,
5
+ export function InsightsErrorAlert({
8
6
  fallbackTitle,
9
7
  }: {
10
8
  readonly error: Error;
11
9
  readonly fallbackTitle: string;
12
10
  }) {
13
- const copy = getAnalyticsErrorCopy(error, fallbackTitle);
14
11
  return (
15
12
  <Alert variant="destructive">
16
13
  <TriangleAlert aria-hidden="true" />
17
- <AlertTitle>{copy.title}</AlertTitle>
18
- <AlertDescription>{copy.description}</AlertDescription>
14
+ <AlertTitle>{fallbackTitle}</AlertTitle>
15
+ <AlertDescription>
16
+ Refresh to try again. If this keeps happening, check your Insights
17
+ provider connection.
18
+ </AlertDescription>
19
19
  </Alert>
20
20
  );
21
21
  }
@@ -0,0 +1,203 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { ArrowUpRight } from "lucide-react";
3
+
4
+ import { Button, buttonVariants } from "@/components/ui/button";
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardFooter,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Skeleton } from "@/components/ui/skeleton";
14
+ import type { ReportingInstallations } from "@/lib/insights-api";
15
+ import { outcomeLabels } from "@/lib/insights-view";
16
+
17
+ import { EventTimestamp, useInsightsTimeFormat } from "./EventDetails";
18
+ import type { InsightsReportSelection } from "./InsightsBundleReports";
19
+ import { InsightsErrorAlert } from "./InsightsErrorAlert";
20
+
21
+ type InsightsOverviewProps =
22
+ | { readonly status: "loading" }
23
+ | { readonly status: "error"; readonly error: Error }
24
+ | {
25
+ readonly status: "success";
26
+ readonly active: ReportingInstallations;
27
+ readonly onOutcomeSelect: (selection: InsightsReportSelection) => void;
28
+ };
29
+
30
+ const windowCopy = {
31
+ "24h": "the last 24 hours",
32
+ "7d": "the last 7 days",
33
+ "30d": "the last 30 days",
34
+ } as const;
35
+
36
+ export function InsightsOverview(props: InsightsOverviewProps) {
37
+ const dateTimeFormat = useInsightsTimeFormat();
38
+
39
+ if (props.status === "loading") {
40
+ return (
41
+ <Card aria-label="Loading reporting installations" className="shadow-sm">
42
+ <CardHeader className="px-4 pt-4 pb-2 sm:px-6 sm:pt-6">
43
+ <Skeleton className="h-4 w-40" />
44
+ <Skeleton className="h-3 w-full max-w-md" />
45
+ </CardHeader>
46
+ <CardContent className="px-4 pb-6 sm:px-6">
47
+ <Skeleton className="h-12 w-28" />
48
+ </CardContent>
49
+ <CardFooter className="border-t px-4 py-3 sm:px-6">
50
+ <Skeleton className="h-8 w-full max-w-sm" />
51
+ </CardFooter>
52
+ </Card>
53
+ );
54
+ }
55
+
56
+ if (props.status === "error") {
57
+ return (
58
+ <InsightsErrorAlert
59
+ error={props.error}
60
+ fallbackTitle="Reporting installations unavailable"
61
+ />
62
+ );
63
+ }
64
+
65
+ const { active } = props;
66
+ const bundle = active.bundle;
67
+
68
+ return (
69
+ <section
70
+ aria-labelledby="reporting-installations-heading"
71
+ className="flex flex-col gap-4"
72
+ >
73
+ <Card className="min-w-0 overflow-hidden shadow-sm">
74
+ <CardHeader className="gap-2 px-4 pt-4 pb-2 sm:px-6 sm:pt-6">
75
+ <CardTitle className="text-sm font-medium">
76
+ <h2 id="reporting-installations-heading">
77
+ Reporting installations
78
+ </h2>
79
+ </CardTitle>
80
+ <CardDescription>
81
+ {active.platform === "ios" ? "iOS" : "Android"} · {active.channel} ·{" "}
82
+ {windowCopy[active.window]}. Installations are counted by their last
83
+ report received.
84
+ </CardDescription>
85
+ </CardHeader>
86
+ <CardContent className="px-4 pt-2 pb-5 sm:px-6 sm:pb-6">
87
+ <p className="text-4xl font-semibold tracking-tight tabular-nums sm:text-5xl">
88
+ {active.reportingInstallations.count.toLocaleString()}
89
+ </p>
90
+ </CardContent>
91
+ <CardFooter className="flex flex-wrap items-center justify-between gap-3 border-t bg-muted/15 px-4 py-3 sm:px-6">
92
+ <dl className="min-w-0 text-xs">
93
+ <dt className="text-muted-foreground">Measured at</dt>
94
+ <dd className="font-medium tabular-nums">
95
+ <EventTimestamp
96
+ formatter={dateTimeFormat}
97
+ touch
98
+ value={active.reportingInstallations.measuredAtMs}
99
+ />
100
+ </dd>
101
+ </dl>
102
+ <Link
103
+ className={buttonVariants({
104
+ className: "h-11 px-3 lg:h-8",
105
+ variant: "outline",
106
+ })}
107
+ to="/installations"
108
+ >
109
+ View events
110
+ <ArrowUpRight aria-hidden="true" data-icon="inline-end" />
111
+ </Link>
112
+ </CardFooter>
113
+ </Card>
114
+ {bundle ? (
115
+ <Card className="min-w-0 overflow-hidden shadow-sm">
116
+ <CardHeader>
117
+ <CardTitle>Selected bundle</CardTitle>
118
+ <CardDescription className="break-all">
119
+ {bundle.bundleId}
120
+ </CardDescription>
121
+ </CardHeader>
122
+ <CardContent>
123
+ <dl className="flex flex-col gap-5">
124
+ <div className="flex flex-wrap items-center justify-between gap-3">
125
+ <div>
126
+ <dt className="text-sm">Selected bundle installations</dt>
127
+ <dd className="mt-1 text-2xl font-semibold tabular-nums">
128
+ {bundle.reportingInstallations.count.toLocaleString()}
129
+ </dd>
130
+ </div>
131
+ <div className="text-xs text-muted-foreground">
132
+ Measured at{" "}
133
+ <EventTimestamp
134
+ formatter={dateTimeFormat}
135
+ touch
136
+ value={bundle.reportingInstallations.measuredAtMs}
137
+ />
138
+ </div>
139
+ </div>
140
+ {(
141
+ [
142
+ ["applied", bundle.appliedReports],
143
+ ["recovered", bundle.recoveredReports],
144
+ ["adopted", bundle.adoptedReports],
145
+ ] as const
146
+ ).map(([outcome, measurement]) => (
147
+ <div
148
+ className="flex flex-wrap items-center justify-between gap-3"
149
+ key={outcome}
150
+ >
151
+ <div>
152
+ <dt className="text-sm">{outcomeLabels[outcome]}</dt>
153
+ <dd className="mt-1 text-2xl font-semibold tabular-nums">
154
+ {measurement.count.toLocaleString()}
155
+ </dd>
156
+ <div className="mt-1 text-xs text-muted-foreground">
157
+ Measured at{" "}
158
+ <EventTimestamp
159
+ formatter={dateTimeFormat}
160
+ touch
161
+ value={measurement.measuredAtMs}
162
+ />
163
+ </div>
164
+ </div>
165
+ <Button
166
+ className="h-11 lg:h-8"
167
+ variant="outline"
168
+ onClick={() =>
169
+ props.onOutcomeSelect({
170
+ bundle: {
171
+ platform: active.platform,
172
+ channel: active.channel,
173
+ bundleId: bundle.bundleId,
174
+ outcome,
175
+ },
176
+ sinceMs: active.sinceMs,
177
+ beforeReceivedAtMs: active.beforeReceivedAtMs,
178
+ })
179
+ }
180
+ >
181
+ View {outcome} reports
182
+ <ArrowUpRight aria-hidden="true" data-icon="inline-end" />
183
+ </Button>
184
+ </div>
185
+ ))}
186
+ </dl>
187
+ </CardContent>
188
+ <CardFooter>
189
+ <p className="text-xs text-muted-foreground">
190
+ Installation counts are independent live measurements. Outcome
191
+ counts are accepted reports, not unique devices or update
192
+ attempts. Recovery reports identify the bundle recovered from.
193
+ </p>
194
+ </CardFooter>
195
+ </Card>
196
+ ) : null}
197
+ <p className="text-xs text-muted-foreground">
198
+ Offline installations, opt-outs, and reports that did not reach the
199
+ server are absent.
200
+ </p>
201
+ </section>
202
+ );
203
+ }
@@ -0,0 +1,48 @@
1
+ import { Link } from "@tanstack/react-router";
2
+ import { ChartNoAxesCombined } from "lucide-react";
3
+
4
+ import { buttonVariants } from "@/components/ui/button";
5
+ import { SidebarTrigger } from "@/components/ui/sidebar";
6
+
7
+ export function InsightsPageHeader({
8
+ view,
9
+ }: {
10
+ readonly view: "overview" | "events";
11
+ }) {
12
+ return (
13
+ <header className="sticky top-0 z-10 flex shrink-0 flex-wrap items-center gap-3 border-b bg-background px-3 py-3 sm:bg-card/70 sm:px-4 sm:backdrop-blur-sm">
14
+ <SidebarTrigger className="-ml-1 size-11 lg:size-7" />
15
+ <div className="flex items-center gap-1.5">
16
+ <ChartNoAxesCombined
17
+ aria-hidden="true"
18
+ className="size-3.5 text-muted-foreground"
19
+ />
20
+ <h1 className="text-sm font-medium">Insights</h1>
21
+ </div>
22
+ <nav aria-label="Insights views" className="ml-auto flex gap-1">
23
+ <Link
24
+ aria-current={view === "overview" ? "page" : undefined}
25
+ className={buttonVariants({
26
+ className: "h-11 px-3 lg:h-8 lg:px-2.5",
27
+ size: "lg",
28
+ variant: view === "overview" ? "secondary" : "ghost",
29
+ })}
30
+ to="/insights"
31
+ >
32
+ Overview
33
+ </Link>
34
+ <Link
35
+ aria-current={view === "events" ? "page" : undefined}
36
+ className={buttonVariants({
37
+ className: "h-11 px-3 lg:h-8 lg:px-2.5",
38
+ size: "lg",
39
+ variant: view === "events" ? "secondary" : "ghost",
40
+ })}
41
+ to="/installations"
42
+ >
43
+ Events
44
+ </Link>
45
+ </nav>
46
+ </header>
47
+ );
48
+ }
@@ -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
+ }